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, FeatureMap, alt_api_path};
39use flate2::read::GzDecoder;
40use snapbox::prelude::*;
41use std::collections::HashSet;
42use std::fs;
43use std::fs::File;
44use std::io::{self, SeekFrom, prelude::*};
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    pubtime: Option<&str>,
232    v: Option<u32>,
233) -> String {
234    // This emulates what crates.io does to retain backwards compatibility.
235    let (features, features2) = split_index_features(features.clone());
236    let mut json = serde_json::json!({
237        "name": name,
238        "vers": vers,
239        "deps": deps,
240        "cksum": cksum,
241        "features": features,
242        "yanked": yanked,
243        "links": links,
244    });
245    if let Some(f2) = &features2 {
246        json["features2"] = serde_json::json!(f2);
247        json["v"] = serde_json::json!(2);
248    }
249    if let Some(v) = v {
250        json["v"] = serde_json::json!(v);
251    }
252    if let Some(rust_version) = rust_version {
253        json["rust_version"] = serde_json::json!(rust_version);
254    }
255    if let Some(pubtime) = pubtime {
256        json["pubtime"] = serde_json::json!(pubtime);
257    }
258
259    json.to_string()
260}
261
262pub(crate) fn write_to_index(registry_path: &Path, name: &str, line: String, local: bool) {
263    let file = cargo_util::registry::make_dep_path(name, false);
264
265    // Write file/line in the index.
266    let dst = if local {
267        registry_path.join("index").join(&file)
268    } else {
269        registry_path.join(&file)
270    };
271    let prev = fs::read_to_string(&dst).unwrap_or_default();
272    t!(fs::create_dir_all(dst.parent().unwrap()));
273    t!(fs::write(&dst, prev + &line[..] + "\n"));
274
275    // Add the new file to the index.
276    if !local {
277        let repo = t!(git2::Repository::open(&registry_path));
278        let mut index = t!(repo.index());
279        t!(index.add_path(Path::new(&file)));
280        t!(index.write());
281        let id = t!(index.write_tree());
282
283        // Commit this change.
284        let tree = t!(repo.find_tree(id));
285        let sig = t!(repo.signature());
286        let parent = t!(repo.refname_to_id("refs/heads/master"));
287        let parent = t!(repo.find_commit(parent));
288        t!(repo.commit(
289            Some("HEAD"),
290            &sig,
291            &sig,
292            "Another commit",
293            &tree,
294            &[&parent]
295        ));
296    }
297}
298
299fn split_index_features(mut features: FeatureMap) -> (FeatureMap, Option<FeatureMap>) {
300    let mut features2 = FeatureMap::new();
301    for (feat, values) in features.iter_mut() {
302        if values
303            .iter()
304            .any(|value| value.starts_with("dep:") || value.contains("?/"))
305        {
306            let new_values = std::mem::take(values);
307            features2.insert(feat.clone(), new_values);
308        }
309    }
310    if features2.is_empty() {
311        (features, None)
312    } else {
313        (features, Some(features2))
314    }
315}