source_lock.rs (5786B)
1 #![forbid(unsafe_code)] 2 3 use serde::Deserialize; 4 use serde_json::{Value, json}; 5 use sha2::{Digest, Sha256}; 6 use std::process::Command; 7 8 #[derive(Deserialize)] 9 #[serde(deny_unknown_fields)] 10 struct SourceLock { 11 schema: String, 12 repository: String, 13 revision: String, 14 architecture: String, 15 workspace_catalog_sha256: String, 16 version: String, 17 source_archive_sha256: String, 18 lockfile: String, 19 lockfile_sha256: String, 20 } 21 22 fn validate(lock: &str, cargo_lock: &[u8], metadata: &Value) -> Result<(), &'static str> { 23 let lock: SourceLock = toml::from_str(lock).map_err(|_| "invalid source lock")?; 24 if lock.schema != "radroots.lib.source-lock.v1" 25 || lock.repository != "https://github.com/radrootslabs/lib" 26 || lock.architecture != "radroots.crates.release.v2" 27 || lock.lockfile != "Cargo.lock" 28 { 29 return Err("invalid source identity"); 30 } 31 for (value, length) in [ 32 (&lock.revision, 40), 33 (&lock.workspace_catalog_sha256, 64), 34 (&lock.source_archive_sha256, 64), 35 (&lock.lockfile_sha256, 64), 36 ] { 37 if value.len() != length 38 || !value 39 .bytes() 40 .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b)) 41 { 42 return Err("invalid source digest"); 43 } 44 } 45 if lock.lockfile_sha256 != format!("{:x}", Sha256::digest(cargo_lock)) { 46 return Err("stale Cargo.lock digest"); 47 } 48 let source = format!( 49 "git+https://github.com/radrootslabs/lib.git?rev={}", 50 lock.revision 51 ); 52 let resolved = format!("{source}#{}", lock.revision); 53 let packages = metadata["packages"].as_array().ok_or("invalid metadata")?; 54 let daemon = packages 55 .iter() 56 .find(|p| p["name"] == "radrootsd") 57 .ok_or("missing daemon")?; 58 let dependencies = daemon["dependencies"] 59 .as_array() 60 .ok_or("missing dependencies")?; 61 let mut direct = 0; 62 for dependency in dependencies { 63 let name = dependency["name"] 64 .as_str() 65 .ok_or("missing dependency name")?; 66 if name.starts_with("radroots_") { 67 if dependency["source"] != source 68 || dependency["req"] != format!("={}", lock.version) 69 || !dependency["rename"].is_null() 70 { 71 return Err("unlocked direct foundation dependency"); 72 } 73 direct += 1; 74 } 75 } 76 let mut shared = 0; 77 for package in packages { 78 let name = package["name"].as_str().ok_or("missing package name")?; 79 if name == "radroots" || name.starts_with("radroots_") { 80 if package["source"] != resolved || package["version"] != lock.version { 81 return Err("unlocked resolved foundation package"); 82 } 83 shared += 1; 84 } 85 } 86 if direct == 0 || shared == 0 { 87 return Err("missing foundation dependencies"); 88 } 89 Ok(()) 90 } 91 92 #[test] 93 fn committed_source_lock_matches_actual_lock_and_offline_resolved_graph() { 94 let metadata = Command::new("cargo") 95 .args(["metadata", "--locked", "--offline", "--format-version", "1"]) 96 .current_dir(env!("CARGO_MANIFEST_DIR")) 97 .output() 98 .expect("offline locked metadata"); 99 assert!(metadata.status.success(), "offline locked metadata failed"); 100 let metadata = serde_json::from_slice(&metadata.stdout).expect("metadata JSON"); 101 validate( 102 include_str!("../radroots.lib.source-lock.v1.toml"), 103 include_bytes!("../Cargo.lock"), 104 &metadata, 105 ) 106 .expect("exact source-lock agreement"); 107 } 108 109 fn fixture() -> (String, Value) { 110 let text = include_str!("../radroots.lib.source-lock.v1.toml"); 111 let lock: SourceLock = toml::from_str(text).expect("source lock"); 112 let source = format!( 113 "git+https://github.com/radrootslabs/lib.git?rev={}", 114 lock.revision 115 ); 116 let metadata = json!({"packages": [ 117 {"name": "radrootsd", "dependencies": [{"name": "radroots_protocol", "source": source, "req": "=0.1.0-alpha", "rename": null}]}, 118 {"name": "radroots_protocol", "source": format!("{source}#{}", lock.revision), "version": "0.1.0-alpha"} 119 ]}); 120 ( 121 text.replace( 122 &lock.lockfile_sha256, 123 &format!("{:x}", Sha256::digest(b"fixture lock")), 124 ), 125 metadata, 126 ) 127 } 128 129 #[test] 130 fn changed_lock_bytes_and_malformed_source_identity_are_rejected() { 131 let (lock, metadata) = fixture(); 132 assert!(validate(&lock, b"fixture lock", &metadata).is_ok()); 133 assert_eq!( 134 validate(&lock, b"different lock", &metadata), 135 Err("stale Cargo.lock digest") 136 ); 137 for changed in [ 138 format!("{lock}unknown = true\n"), 139 format!("{lock}schema = \"duplicate\"\n"), 140 lock.replace("lockfile = \"Cargo.lock\"", "lockfile = \"../Cargo.lock\""), 141 ] { 142 assert!(validate(&changed, b"fixture lock", &metadata).is_err()); 143 } 144 } 145 146 #[test] 147 fn path_or_unlocked_dependency_overrides_are_rejected() { 148 let (lock, metadata) = fixture(); 149 for (field, value) in [ 150 ("source", Value::Null), 151 ( 152 "source", 153 json!("git+https://github.com/radrootslabs/lib.git?branch=master"), 154 ), 155 ("req", json!("^0.1")), 156 ("rename", json!("hidden_dependency")), 157 ] { 158 let mut changed = metadata.clone(); 159 changed["packages"][0]["dependencies"][0][field] = value; 160 assert!(validate(&lock, b"fixture lock", &changed).is_err()); 161 } 162 for (field, value) in [("source", Value::Null), ("version", json!("0.2.0"))] { 163 let mut changed = metadata.clone(); 164 changed["packages"][1][field] = value; 165 assert!(validate(&lock, b"fixture lock", &changed).is_err()); 166 } 167 }