cli

Command-line interface for Radroots
git clone https://radroots.dev/git/cli.git
Log | Files | Refs | README | LICENSE

source_lock.rs (7932B)


      1 use serde_json::Value;
      2 use sha2::{Digest, Sha256};
      3 use std::collections::BTreeMap;
      4 use std::io::Read;
      5 use std::path::Path;
      6 use std::process::{Command, Stdio};
      7 
      8 const MAX_TEXT: u64 = 2 * 1024 * 1024;
      9 const MAX_METADATA: u64 = 4 * MAX_TEXT;
     10 const FIELDS: [&str; 9] = [
     11     "schema",
     12     "repository",
     13     "revision",
     14     "architecture",
     15     "workspace_catalog_sha256",
     16     "version",
     17     "source_archive_sha256",
     18     "lockfile",
     19     "lockfile_sha256",
     20 ];
     21 
     22 pub fn run() -> Result<(), ()> {
     23     let root = Path::new(env!("CARGO_MANIFEST_DIR"))
     24         .parent()
     25         .and_then(Path::parent)
     26         .ok_or(())?;
     27     let lock = read(root.join("radroots.lib.source-lock.v1.toml").as_path())?;
     28     let cargo_lock = read(root.join("Cargo.lock").as_path())?;
     29     let metadata = metadata(root)?;
     30     validate(&lock, &cargo_lock, &metadata)?;
     31     println!("source lock ok: exact digest, public revision and dependency agreement");
     32     Ok(())
     33 }
     34 
     35 fn read(path: &Path) -> Result<Vec<u8>, ()> {
     36     let file_type = std::fs::symlink_metadata(path).map_err(|_| ())?;
     37     if !file_type.is_file() || file_type.len() > MAX_TEXT {
     38         return Err(());
     39     }
     40     let mut bytes = Vec::new();
     41     std::fs::File::open(path)
     42         .map_err(|_| ())?
     43         .take(MAX_TEXT + 1)
     44         .read_to_end(&mut bytes)
     45         .map_err(|_| ())?;
     46     if bytes.len() as u64 > MAX_TEXT {
     47         return Err(());
     48     }
     49     Ok(bytes)
     50 }
     51 
     52 fn metadata(root: &Path) -> Result<Value, ()> {
     53     let mut child = Command::new("cargo")
     54         .args(["metadata", "--locked", "--offline", "--format-version", "1"])
     55         .current_dir(root)
     56         .stdin(Stdio::null())
     57         .stdout(Stdio::piped())
     58         .stderr(Stdio::null())
     59         .spawn()
     60         .map_err(|_| ())?;
     61     let mut bytes = Vec::new();
     62     let read = child
     63         .stdout
     64         .take()
     65         .ok_or(())?
     66         .take(MAX_METADATA + 1)
     67         .read_to_end(&mut bytes);
     68     if read.is_err() || bytes.len() as u64 > MAX_METADATA {
     69         let _ = child.kill();
     70         let _ = child.wait();
     71         return Err(());
     72     }
     73     if !child.wait().map_err(|_| ())?.success() {
     74         return Err(());
     75     }
     76     serde_json::from_slice(&bytes).map_err(|_| ())
     77 }
     78 
     79 fn fields(bytes: &[u8]) -> Result<BTreeMap<&str, &str>, ()> {
     80     let text = std::str::from_utf8(bytes).map_err(|_| ())?;
     81     let mut fields = BTreeMap::new();
     82     for line in text.lines() {
     83         let (key, quoted) = line.split_once(" = ").ok_or(())?;
     84         let value = quoted
     85             .strip_prefix('"')
     86             .and_then(|value| value.strip_suffix('"'))
     87             .ok_or(())?;
     88         if !FIELDS.contains(&key)
     89             || value.is_empty()
     90             || value
     91                 .bytes()
     92                 .any(|byte| !byte.is_ascii_graphic() || matches!(byte, b'"' | b'\\'))
     93             || fields.insert(key, value).is_some()
     94         {
     95             return Err(());
     96         }
     97     }
     98     if fields.len() != FIELDS.len() {
     99         return Err(());
    100     }
    101     Ok(fields)
    102 }
    103 
    104 fn validate(lock: &[u8], cargo_lock: &[u8], metadata: &Value) -> Result<(), ()> {
    105     let fields = fields(lock)?;
    106     for (key, expected) in [
    107         ("schema", "radroots.lib.source-lock.v1"),
    108         ("repository", "https://github.com/radrootslabs/lib"),
    109         ("architecture", "radroots.crates.release.v2"),
    110         ("lockfile", "Cargo.lock"),
    111     ] {
    112         if fields[key] != expected {
    113             return Err(());
    114         }
    115     }
    116     for (key, length) in [
    117         ("revision", 40),
    118         ("workspace_catalog_sha256", 64),
    119         ("source_archive_sha256", 64),
    120         ("lockfile_sha256", 64),
    121     ] {
    122         if fields[key].len() != length
    123             || !fields[key]
    124                 .bytes()
    125                 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
    126         {
    127             return Err(());
    128         }
    129     }
    130     if fields["lockfile_sha256"] != hex::encode(Sha256::digest(cargo_lock)) {
    131         return Err(());
    132     }
    133     let source = format!(
    134         "git+https://github.com/radrootslabs/lib.git?rev={}",
    135         fields["revision"]
    136     );
    137     let resolved = format!("{source}#{}", fields["revision"]);
    138     let packages = metadata["packages"].as_array().ok_or(())?;
    139     let cli = packages
    140         .iter()
    141         .find(|package| package["name"] == "radroots_cli")
    142         .ok_or(())?;
    143     let dependencies = cli["dependencies"].as_array().ok_or(())?;
    144     let facade = dependencies
    145         .iter()
    146         .find(|dependency| dependency["name"] == "radroots")
    147         .ok_or(())?;
    148     if facade["source"] != source
    149         || facade["req"] != format!("={}", fields["version"])
    150         || !facade["rename"].is_null()
    151     {
    152         return Err(());
    153     }
    154     let mut shared_count = 0;
    155     for package in packages {
    156         let name = package["name"].as_str().ok_or(())?;
    157         if (name == "radroots" || name.starts_with("radroots_"))
    158             && !matches!(name, "radroots_cli" | "radroots_cli_xtask")
    159         {
    160             if package["source"] != resolved || package["version"] != fields["version"] {
    161                 return Err(());
    162             }
    163             shared_count += 1;
    164         }
    165     }
    166     if shared_count == 0 {
    167         return Err(());
    168     }
    169     Ok(())
    170 }
    171 
    172 #[cfg(test)]
    173 mod tests {
    174     use super::*;
    175     use serde_json::json;
    176 
    177     fn fixture() -> (String, Value) {
    178         let lock = include_str!("../../../radroots.lib.source-lock.v1.toml");
    179         let lock = lock.replace(
    180             fields(lock.as_bytes()).expect("canonical lock")["lockfile_sha256"],
    181             &hex::encode(Sha256::digest(b"synthetic cargo lock")),
    182         );
    183         let revision = fields(lock.as_bytes()).expect("canonical lock")["revision"];
    184         let source = format!("git+https://github.com/radrootslabs/lib.git?rev={revision}");
    185         let metadata = json!({"packages": [
    186             {"name": "radroots_cli", "dependencies": [{"name": "radroots", "source": source, "req": "=0.1.0-alpha", "rename": null}]},
    187             {"name": "radroots", "source": format!("{source}#{revision}"), "version": "0.1.0-alpha"}
    188         ]});
    189         (lock, metadata)
    190     }
    191 
    192     #[test]
    193     fn exact_lock_and_dependency_inputs_pass() {
    194         let (lock, metadata) = fixture();
    195         assert!(validate(lock.as_bytes(), b"synthetic cargo lock", &metadata).is_ok());
    196     }
    197 
    198     #[test]
    199     fn stale_lock_bytes_fail_even_when_revision_agrees() {
    200         let (lock, metadata) = fixture();
    201         assert!(validate(lock.as_bytes(), b"changed cargo lock", &metadata).is_err());
    202     }
    203 
    204     #[test]
    205     fn local_or_mismatched_resolved_shared_sources_fail() {
    206         let (lock, metadata) = fixture();
    207         for source in [
    208             Value::Null,
    209             json!("git+https://github.com/radrootslabs/lib.git?branch=master"),
    210         ] {
    211             let mut changed = metadata.clone();
    212             changed["packages"][1]["source"] = source;
    213             assert!(validate(lock.as_bytes(), b"synthetic cargo lock", &changed).is_err());
    214         }
    215     }
    216 
    217     #[test]
    218     fn manifest_revision_or_version_drift_fails() {
    219         let (lock, metadata) = fixture();
    220         for (field, value) in [
    221             ("source", "git+https://example.invalid/lib"),
    222             ("req", "^0.1"),
    223         ] {
    224             let mut changed = metadata.clone();
    225             changed["packages"][0]["dependencies"][0][field] = json!(value);
    226             assert!(validate(lock.as_bytes(), b"synthetic cargo lock", &changed).is_err());
    227         }
    228     }
    229 
    230     #[test]
    231     fn duplicate_unknown_missing_or_unsafe_lock_fields_fail() {
    232         let (lock, metadata) = fixture();
    233         for changed in [
    234             format!("{lock}revision = \"{}\"\n", "a".repeat(40)),
    235             format!("{lock}extra = \"value\"\n"),
    236             lock.lines().skip(1).collect::<Vec<_>>().join("\n"),
    237             lock.replace("lockfile = \"Cargo.lock\"", "lockfile = \"../Cargo.lock\""),
    238         ] {
    239             assert!(validate(changed.as_bytes(), b"synthetic cargo lock", &metadata).is_err());
    240         }
    241     }
    242 }