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