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 }