#
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}