retired_surface.rs (6583B)
1 use std::{ 2 collections::BTreeSet, 3 fs, 4 path::{Path, PathBuf}, 5 }; 6 7 const RELEASE_POLICY: &str = include_str!("../../../contracts/releases/publish_policy.toml"); 8 9 const RETIRED_IDENTIFIERS: &[&str] = &[ 10 "IdentityError", 11 "RadrootsEncryptedIdentityFile", 12 "RadrootsIdentity", 13 "RadrootsIdentityEncryptedSecretKeyOptions", 14 "RadrootsIdentityEncryptedSecretKeySecurity", 15 "RadrootsIdentityFile", 16 "RadrootsIdentityId", 17 "RadrootsIdentityProfile", 18 "RadrootsIdentityPublic", 19 "RadrootsIdentitySecretKeyFormat", 20 concat!("Radroots", "PublicKey"), 21 ]; 22 23 const RETIRED_FUNCTIONS_AND_CONSTANTS: &[&str] = &[ 24 "DEFAULT_IDENTITY_PATH", 25 "RADROOTS_ENCRYPTED_IDENTITY_DEFAULT_KEY_SLOT", 26 "RADROOTS_ENCRYPTED_IDENTITY_KEY_SUFFIX", 27 "encrypted_identity_wrapping_key_path", 28 "load_encrypted_identity", 29 "load_identity_profile", 30 "rotate_encrypted_identity", 31 "store_encrypted_identity", 32 "store_identity_profile", 33 ]; 34 35 #[test] 36 fn first_party_production_sources_do_not_restore_the_retired_identity_surface() { 37 let workspace_root = workspace_root(); 38 let mut sources = Vec::new(); 39 collect_production_rust_sources(&workspace_root.join("crates"), &mut sources); 40 assert!( 41 !sources.is_empty(), 42 "workspace production sources are required" 43 ); 44 45 let mut findings = Vec::new(); 46 for path in sources { 47 let source = fs::read_to_string(&path).expect("read production source"); 48 for (line_index, line) in source.lines().enumerate() { 49 if line.trim_start().starts_with("//") { 50 continue; 51 } 52 for retired in RETIRED_IDENTIFIERS { 53 if contains_identifier(line, retired) { 54 findings.push(format!( 55 "{}:{} restores retired identity identifier `{retired}`", 56 relative_path(&workspace_root, &path), 57 line_index + 1, 58 )); 59 } 60 } 61 for retired in RETIRED_FUNCTIONS_AND_CONSTANTS { 62 if contains_identifier(line, retired) { 63 findings.push(format!( 64 "{}:{} restores retired identity operation `{retired}`", 65 relative_path(&workspace_root, &path), 66 line_index + 1, 67 )); 68 } 69 } 70 } 71 } 72 73 assert!( 74 findings.is_empty(), 75 "retired identity surface violations:\n{}", 76 findings.join("\n") 77 ); 78 } 79 80 #[test] 81 fn retired_identity_implementation_modules_are_absent() { 82 let identity_source = workspace_root().join("crates/identity/src"); 83 for retired in ["identity.rs", "storage.rs", "test_fixtures.rs"] { 84 assert!( 85 !identity_source.join(retired).exists(), 86 "retired identity module must remain absent: {retired}" 87 ); 88 } 89 } 90 91 #[test] 92 fn release_policy_enables_replacement_and_keeps_retired_mixed_packages_absent() { 93 let publication = table(RELEASE_POLICY, "[publication]"); 94 assert!( 95 publication.contains("frozen = false") 96 && publication.contains("registry = \"crates-io\"") 97 && publication.contains("final_enablement_step = 305"), 98 "publication validation must remain enabled only through the approved Step 305 policy" 99 ); 100 101 let approved = string_array(RELEASE_POLICY, "[publication]", "approved_packages"); 102 assert!(approved.contains("radroots_identity")); 103 for mixed in [ 104 "radroots_authority", 105 "radroots_nostr_accounts", 106 "radroots_nostr_signer", 107 ] { 108 assert!( 109 !approved.contains(mixed), 110 "mixed package must not be approved for publication: {mixed}" 111 ); 112 let package_directory = workspace_root() 113 .join("crates") 114 .join(mixed.strip_prefix("radroots_").expect("package prefix")); 115 assert!( 116 !package_directory.exists(), 117 "retired mixed package must remain absent: {}", 118 package_directory.display() 119 ); 120 } 121 } 122 123 fn workspace_root() -> PathBuf { 124 Path::new(env!("CARGO_MANIFEST_DIR")) 125 .parent() 126 .and_then(Path::parent) 127 .expect("lib workspace root") 128 .to_path_buf() 129 } 130 131 fn collect_production_rust_sources(directory: &Path, paths: &mut Vec<PathBuf>) { 132 for entry in fs::read_dir(directory).expect("read workspace source directory") { 133 let path = entry.expect("workspace source entry").path(); 134 if path.is_dir() { 135 if path.file_name().and_then(|name| name.to_str()) != Some("target") { 136 collect_production_rust_sources(&path, paths); 137 } 138 } else if path.extension().and_then(|extension| extension.to_str()) == Some("rs") 139 && path 140 .components() 141 .any(|component| component.as_os_str() == "src") 142 { 143 paths.push(path); 144 } 145 } 146 } 147 148 fn contains_identifier(source: &str, identifier: &str) -> bool { 149 source.match_indices(identifier).any(|(index, _)| { 150 let before = source[..index].chars().next_back(); 151 let after = source[index + identifier.len()..].chars().next(); 152 before.is_none_or(|character| !is_identifier_character(character)) 153 && after.is_none_or(|character| !is_identifier_character(character)) 154 }) 155 } 156 157 fn is_identifier_character(character: char) -> bool { 158 character == '_' || character.is_ascii_alphanumeric() 159 } 160 161 fn table<'a>(source: &'a str, heading: &str) -> &'a str { 162 let body = source 163 .split_once(heading) 164 .unwrap_or_else(|| panic!("missing table {heading}")) 165 .1; 166 body.split_once("\n[").map_or(body, |(current, _)| current) 167 } 168 169 fn string_array<'a>(source: &'a str, heading: &str, key: &str) -> BTreeSet<&'a str> { 170 let table = table(source, heading); 171 let marker = format!("{key} = ["); 172 let values = table 173 .split_once(&marker) 174 .unwrap_or_else(|| panic!("missing {heading} {key}")) 175 .1 176 .split_once(']') 177 .unwrap_or_else(|| panic!("unterminated {heading} {key}")) 178 .0; 179 values 180 .split(',') 181 .map(str::trim) 182 .filter_map(|value| { 183 value 184 .strip_prefix('"') 185 .and_then(|value| value.strip_suffix('"')) 186 }) 187 .collect() 188 } 189 190 fn relative_path(root: &Path, path: &Path) -> String { 191 path.strip_prefix(root) 192 .expect("source path is under workspace root") 193 .to_string_lossy() 194 .replace('\\', "/") 195 }