cargo_test_support/
publish.rs

1//! Helpers for testing `cargo package` / `cargo publish`
2//!
3//! # Example
4//!
5//! ```no_run
6//! # use cargo_test_support::registry::RegistryBuilder;
7//! # use cargo_test_support::publish::validate_upload;
8//! # use cargo_test_support::project;
9//! validate_upload(
10//!     r#"
11//!     {
12//!       "authors": [],
13//!       "badges": {},
14//!       "categories": [],
15//!       "deps": [],
16//!       "description": "foo",
17//!       "documentation": null,
18//!       "features": {},
19//!       "homepage": null,
20//!       "keywords": [],
21//!       "license": "MIT",
22//!       "license_file": null,
23//!       "links": null,
24//!       "name": "foo",
25//!       "readme": null,
26//!       "readme_file": null,
27//!       "repository": null,
28//!       "rust_version": null,
29//!       "vers": "0.0.1"
30//!       }
31//!     "#,
32//!     "foo-0.0.1.crate",
33//!     &["Cargo.lock", "Cargo.toml", "Cargo.toml.orig", "src/main.rs"],
34//! );
35//! ```
36
37use crate::compare::InMemoryDir;
38use crate::registry::{self, alt_api_path, FeatureMap};
39use flate2::read::GzDecoder;
40use snapbox::prelude::*;
41use std::collections::HashSet;
42use std::fs;
43use std::fs::File;
44use std::io::{self, prelude::*, SeekFrom};
45use std::path::Path;
46use tar::Archive;
47
48fn read_le_u32<R>(mut reader: R) -> io::Result<u32>
49where
50    R: Read,
51{
52    let mut buf = [0; 4];
53    reader.read_exact(&mut buf)?;
54    Ok(u32::from_le_bytes(buf))
55}
56
57/// Check the `cargo publish` API call
58#[track_caller]
59pub fn validate_upload(expected_json: &str, expected_crate_name: &str, expected_files: &[&str]) {
60    let new_path = registry::api_path().join("api/v1/crates/new");
61    _validate_upload(
62        &new_path,
63        expected_json,
64        expected_crate_name,
65        expected_files,
66        (),
67    );
68}
69
70/// Check the `cargo publish` API call, with file contents
71#[track_caller]
72pub fn validate_upload_with_contents(
73    expected_json: &str,
74    expected_crate_name: &str,
75    expected_files: &[&str],
76    expected_contents: impl Into<InMemoryDir>,
77) {
78    let new_path = registry::api_path().join("api/v1/crates/new");
79    _validate_upload(
80        &new_path,
81        expected_json,
82        expected_crate_name,
83        expected_files,
84        expected_contents,
85    );
86}
87
88/// Check the `cargo publish` API call to the alternative test registry
89#[track_caller]
90pub fn validate_alt_upload(
91    expected_json: &str,
92    expected_crate_name: &str,
93    expected_files: &[&str],
94) {
95    let new_path = alt_api_path().join("api/v1/crates/new");
96    _validate_upload(
97        &new_path,
98        expected_json,
99        expected_crate_name,
100        expected_files,
101        (),
102    );
103}
104
105#[track_caller]
106fn _validate_upload(
107    new_path: &Path,
108    expected_json: &str,
109    expected_crate_name: &str,
110    expected_files: &[&str],
111    expected_contents: impl Into<InMemoryDir>,
112) {
113    let (actual_json, krate_bytes) = read_new_post(new_path);
114
115    snapbox::assert_data_eq!(actual_json, expected_json.is_json());
116
117    // Verify the tarball.
118    validate_crate_contents(
119        &krate_bytes[..],
120        expected_crate_name,
121        expected_files,
122        expected_contents,
123    );
124}
125
126#[track_caller]
127fn read_new_post(new_path: &Path) -> (Vec<u8>, Vec<u8>) {
128    let mut f = File::open(new_path).unwrap();
129
130    // 32-bit little-endian integer of length of JSON data.
131    let json_sz = read_le_u32(&mut f).expect("read json length");
132    let mut json_bytes = vec![0; json_sz as usize];
133    f.read_exact(&mut json_bytes).expect("read JSON data");
134
135    // 32-bit little-endian integer of length of crate file.
136    let crate_sz = read_le_u32(&mut f).expect("read crate length");
137    let mut krate_bytes = vec![0; crate_sz as usize];
138    f.read_exact(&mut krate_bytes).expect("read crate data");
139
140    // Check at end.
141    let current = f.seek(SeekFrom::Current(0)).unwrap();
142    assert_eq!(f.seek(SeekFrom::End(0)).unwrap(), current);
143
144    (json_bytes, krate_bytes)
145}
146
147/// Checks the contents of a `.crate` file.
148///
149/// - `expected_crate_name` should be something like `foo-0.0.1.crate`.
150/// - `expected_files` should be a complete list of files in the crate
151///   (relative to `expected_crate_name`).
152/// - `expected_contents` should be a list of `(file_name, contents)` tuples
153///   to validate the contents of the given file. Only the listed files will
154///   be checked (others will be ignored).
155#[track_caller]
156pub fn validate_crate_contents(
157    reader: impl Read,
158    expected_crate_name: &str,
159    expected_files: &[&str],
160    expected_contents: impl Into<InMemoryDir>,
161) {
162    let expected_contents = expected_contents.into();
163    validate_crate_contents_(
164        reader,
165        expected_crate_name,
166        expected_files,
167        expected_contents,
168    )
169}
170
171#[track_caller]
172fn validate_crate_contents_(
173    reader: impl Read,
174    expected_crate_name: &str,
175    expected_files: &[&str],
176    expected_contents: InMemoryDir,
177) {
178    let mut rdr = GzDecoder::new(reader);
179    snapbox::assert_data_eq!(rdr.header().unwrap().filename().unwrap(), {
180        let expected: snapbox::Data = expected_crate_name.into();
181        expected.raw()
182    });
183
184    let mut contents = Vec::new();
185    rdr.read_to_end(&mut contents).unwrap();
186    let mut ar = Archive::new(&contents[..]);
187    let base_crate_name = Path::new(
188        expected_crate_name
189            .strip_suffix(".crate")
190            .expect("must end with .crate"),
191    );
192    let actual_contents: InMemoryDir = ar
193        .entries()
194        .unwrap()
195        .map(|entry| {
196            let mut entry = entry.unwrap();
197            let name = entry
198                .path()
199                .unwrap()
200                .strip_prefix(base_crate_name)
201                .unwrap()
202                .to_owned();
203            let mut contents = String::new();
204            entry.read_to_string(&mut contents).unwrap();
205            (name, contents)
206        })
207        .collect();
208    let actual_files: HashSet<&Path> = actual_contents.paths().collect();
209    let expected_files: HashSet<&Path> =
210        expected_files.iter().map(|name| Path::new(name)).collect();
211    let missing: Vec<&&Path> = expected_files.difference(&actual_files).collect();
212    let extra: Vec<&&Path> = actual_files.difference(&expected_files).collect();
213    if !missing.is_empty() || !extra.is_empty() {
214        panic!(
215            "uploaded archive does not match.\nMissing: {:?}\nExtra: {:?}\n",
216            missing, extra
217        );
218    }
219    actual_contents.assert_contains(&expected_contents);
220}
221
222pub(crate) fn create_index_line(
223    name: serde_json::Value,
224    vers: &str,
225    deps: Vec<serde_json::Value>,
226    cksum: &str,
227    features: crate::registry::FeatureMap,
228    yanked: bool,
229    links: Option<String>,
230    rust_version: Option<&str>,
231    v: Option<u32>,
232) -> String {
233    // This emulates what crates.io does to retain backwards compatibility.
234    let (features, features2) = split_index_features(features.clone());
235    let mut json = serde_json::json!({
236        "name": name,
237        "vers": vers,
238        "deps": deps,
239        "cksum": cksum,
240        "features": features,
241        "yanked": yanked,
242        "links": links,
243    });
244    if let Some(f2) = &features2 {
245        json["features2"] = serde_json::json!(f2);
246        json["v"] = serde_json::json!(2);
247    }
248    if let Some(v) = v {
249        json["v"] = serde_json::json!(v);
250    }
251    if let Some(rust_version) = rust_version {
252        json["rust_version"] = serde_json::json!(rust_version);
253    }
254
255    json.to_string()
256}
257
258pub(crate) fn write_to_index(registry_path: &Path, name: &str, line: String, local: bool) {
259    let file = cargo_util::registry::make_dep_path(name, false);
260
261    // Write file/line in the index.
262    let dst = if local {
263        registry_path.join("index").join(&file)
264    } else {
265        registry_path.join(&file)
266    };
267    let prev = fs::read_to_string(&dst).unwrap_or_default();
268    t!(fs::create_dir_all(dst.parent().unwrap()));
269    t!(fs::write(&dst, prev + &line[..] + "\n"));
270
271    // Add the new file to the index.
272    if !local {
273        let repo = t!(git2::Repository::open(&registry_path));
274        let mut index = t!(repo.index());
275        t!(index.add_path(Path::new(&file)));
276        t!(index.write());
277        let id = t!(index.write_tree());
278
279        // Commit this change.
280        let tree = t!(repo.find_tree(id));
281        let sig = t!(repo.signature());
282        let parent = t!(repo.refname_to_id("refs/heads/master"));
283        let parent = t!(repo.find_commit(parent));
284        t!(repo.commit(
285            Some("HEAD"),
286            &sig,
287            &sig,
288            "Another commit",
289            &tree,
290            &[&parent]
291        ));
292    }
293}
294
295fn split_index_features(mut features: FeatureMap) -> (FeatureMap, Option<FeatureMap>) {
296    let mut features2 = FeatureMap::new();
297    for (feat, values) in features.iter_mut() {
298        if values
299            .iter()
300            .any(|value| value.starts_with("dep:") || value.contains("?/"))
301        {
302            let new_values = std::mem::take(values);
303            features2.insert(feat.clone(), new_values);
304        }
305    }
306    if features2.is_empty() {
307        (features, None)
308    } else {
309        (features, Some(features2))
310    }
311}