lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

service_source_lock_command.rs (72542B)


      1 use std::{
      2     collections::BTreeSet,
      3     ffi::OsStr,
      4     fmt, fs,
      5     io::{Read as _, Seek as _, SeekFrom, Write as _},
      6     path::{Path, PathBuf},
      7     process::{Command, Stdio},
      8 };
      9 
     10 use serde::Deserialize;
     11 use sha2::{Digest as _, Sha256};
     12 use tempfile::{NamedTempFile, TempDir};
     13 use walkdir::WalkDir;
     14 
     15 use crate::build_output::is_build_output_directory;
     16 use crate::service_source_lock::{
     17     ContractVersions, LIB_REPOSITORY, LOCK_FILENAME, NixMaterialParts, NixMaterialState,
     18     PREDECESSOR_LOCK_FILENAME, ServiceSourceLockParts, ServiceSourceLockV2,
     19     validate_deferred_nix_material,
     20 };
     21 
     22 const CATALOG_RELATIVE: &str = "contracts/crates/catalog.v2.toml";
     23 const CARGO_MANIFEST: &str = "Cargo.toml";
     24 const CARGO_LOCK: &str = "Cargo.lock";
     25 const FLAKE_NIX: &str = "flake.nix";
     26 const FLAKE_LOCK: &str = "flake.lock";
     27 const RUST_TOOLCHAIN: &str = "rust-toolchain.toml";
     28 const LIB_VERSION_REQUIREMENT: &str = "=0.1.0-alpha";
     29 const HOST_PACKAGE: &str = "radroots_service_host";
     30 const HOST_FEATURE_PROFILE: &str = "service-host";
     31 const RUST_VERSION: &str = "1.97.1";
     32 const MAX_MANIFEST_BYTES: usize = 1_048_576;
     33 const MAX_CARGO_LOCK_BYTES: usize = 16_777_216;
     34 const MAX_FLAKE_LOCK_BYTES: usize = 4_194_304;
     35 const MAX_FLAKE_NIX_BYTES: usize = 1_048_576;
     36 const MAX_TOOLCHAIN_BYTES: usize = 65_536;
     37 const MAX_CATALOG_BYTES: usize = 4_194_304;
     38 const MAX_GIT_OUTPUT_BYTES: usize = 65_536;
     39 const MAX_ARCHIVE_BYTES: u64 = 1_073_741_824;
     40 const MAX_TREE_ENTRIES: usize = 16_384;
     41 const MAX_MANIFESTS: usize = 512;
     42 
     43 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
     44 pub(crate) enum CommandMode {
     45     Check,
     46     Write,
     47 }
     48 
     49 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
     50 enum CommandError {
     51     InvalidServiceRoot,
     52     DirtyServiceSource,
     53     InvalidServiceMetadata,
     54     InvalidCargoManifest,
     55     InvalidCargoLock,
     56     InvalidNixMaterial,
     57     InvalidFlakeLock,
     58     InvalidToolchain,
     59     InvalidSourceArchive,
     60     UnreachableRevision,
     61     InvalidSourceLock,
     62     StaleSourceLock,
     63     WriteFailure,
     64 }
     65 
     66 impl fmt::Display for CommandError {
     67     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
     68         formatter.write_str(match self {
     69             Self::InvalidServiceRoot => "service source-lock root is invalid",
     70             Self::DirtyServiceSource => "service source contains an ungoverned change",
     71             Self::InvalidServiceMetadata => "service source-lock metadata is invalid",
     72             Self::InvalidCargoManifest => "service Cargo manifest dependency is invalid",
     73             Self::InvalidCargoLock => "service Cargo lock is invalid",
     74             Self::InvalidNixMaterial => "service deferred Nix material is invalid",
     75             Self::InvalidFlakeLock => "service flake lock is invalid",
     76             Self::InvalidToolchain => "service Rust toolchain is invalid",
     77             Self::InvalidSourceArchive => "Lib source archive is invalid",
     78             Self::UnreachableRevision => "Lib revision is not remotely reachable",
     79             Self::InvalidSourceLock => "service source lock is invalid",
     80             Self::StaleSourceLock => "service source lock is stale",
     81             Self::WriteFailure => "service source lock could not be updated",
     82         })
     83     }
     84 }
     85 
     86 impl std::error::Error for CommandError {}
     87 
     88 #[derive(Clone, Debug, Eq, PartialEq)]
     89 struct ServiceMetadata {
     90     service: String,
     91     nix_material: NixMaterialState,
     92     contract_versions: ContractVersions,
     93 }
     94 
     95 #[derive(Clone, Debug, Eq, PartialEq)]
     96 enum NixMaterialEvidence {
     97     Absent,
     98     Deferred {
     99         lib_revision: String,
    100         flake_lock_sha256: String,
    101     },
    102 }
    103 
    104 impl NixMaterialEvidence {
    105     fn as_parts(&self) -> NixMaterialParts<'_> {
    106         match self {
    107             Self::Absent => NixMaterialParts::Absent,
    108             Self::Deferred {
    109                 lib_revision,
    110                 flake_lock_sha256,
    111             } => NixMaterialParts::Deferred {
    112                 lib_revision,
    113                 flake_lock_sha256,
    114             },
    115         }
    116     }
    117 
    118     fn revision(&self) -> Option<&str> {
    119         match self {
    120             Self::Absent => None,
    121             Self::Deferred { lib_revision, .. } => Some(lib_revision),
    122         }
    123     }
    124 }
    125 
    126 #[derive(Debug, Deserialize)]
    127 struct CargoLockDocument {
    128     #[serde(default)]
    129     package: Vec<CargoLockPackage>,
    130 }
    131 
    132 #[derive(Debug, Deserialize)]
    133 struct CargoLockPackage {
    134     name: String,
    135     version: String,
    136     source: Option<String>,
    137 }
    138 
    139 struct ArchiveEvidence {
    140     revision: String,
    141     archive_sha256: String,
    142     catalog_sha256: String,
    143     package_names: BTreeSet<String>,
    144 }
    145 
    146 trait RevisionReachability {
    147     fn verify(&self, revision: &str) -> Result<(), CommandError>;
    148 }
    149 
    150 struct PublicLibRemote;
    151 
    152 impl RevisionReachability for PublicLibRemote {
    153     fn verify(&self, revision: &str) -> Result<(), CommandError> {
    154         let repository = TempDir::new().map_err(|_| CommandError::UnreachableRevision)?;
    155         git_status(repository.path(), ["init", "--bare", "--quiet"])
    156             .map_err(|_| CommandError::UnreachableRevision)?;
    157         let status = Command::new("git")
    158             .args(["fetch", "--quiet", "--no-tags", "--depth=1", LIB_REPOSITORY])
    159             .arg(revision)
    160             .current_dir(repository.path())
    161             .stdin(Stdio::null())
    162             .stdout(Stdio::null())
    163             .stderr(Stdio::null())
    164             .status()
    165             .map_err(|_| CommandError::UnreachableRevision)?;
    166         if !status.success() {
    167             return Err(CommandError::UnreachableRevision);
    168         }
    169         git_status(repository.path(), ["cat-file", "-e", revision])
    170             .map_err(|_| CommandError::UnreachableRevision)
    171     }
    172 }
    173 
    174 pub(crate) fn run(
    175     mode: CommandMode,
    176     service_root: &Path,
    177     source_archive: &Path,
    178 ) -> Result<(), String> {
    179     run_with(mode, service_root, source_archive, &PublicLibRemote)
    180         .map_err(|error| error.to_string())
    181 }
    182 
    183 fn run_with(
    184     mode: CommandMode,
    185     service_root: &Path,
    186     source_archive: &Path,
    187     reachability: &dyn RevisionReachability,
    188 ) -> Result<(), CommandError> {
    189     let service_root = validate_service_root(service_root)?;
    190     match fs::symlink_metadata(service_root.join(PREDECESSOR_LOCK_FILENAME)) {
    191         Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
    192         _ => return Err(CommandError::InvalidSourceLock),
    193     }
    194     let initial_head = service_head(&service_root)?;
    195     validate_service_cleanliness(&service_root)?;
    196 
    197     let root_manifest = read_bounded_regular(
    198         &service_root.join(CARGO_MANIFEST),
    199         MAX_MANIFEST_BYTES,
    200         CommandError::InvalidCargoManifest,
    201     )?;
    202     let root_manifest = parse_toml(&root_manifest, CommandError::InvalidCargoManifest)?;
    203     let metadata = parse_service_metadata(&root_manifest)?;
    204     let revision = validate_cargo_manifests(&service_root, None)?;
    205 
    206     let nix = read_nix_material(&service_root, metadata.nix_material)?;
    207 
    208     validate_toolchain(&service_root)?;
    209     let archive = validate_archive(source_archive, &revision)?;
    210     let confirmed_revision = validate_cargo_manifests(&service_root, Some(&archive.package_names))?;
    211     if confirmed_revision != revision {
    212         return Err(CommandError::InvalidCargoManifest);
    213     }
    214     let cargo_lock = read_bounded_regular(
    215         &service_root.join(CARGO_LOCK),
    216         MAX_CARGO_LOCK_BYTES,
    217         CommandError::InvalidCargoLock,
    218     )?;
    219     validate_cargo_lock(&cargo_lock, &revision, &archive.package_names)?;
    220     let cargo_lock_sha256 = sha256(&cargo_lock);
    221     reachability.verify(&archive.revision)?;
    222     if let Some(nix_revision) = nix.revision()
    223         && nix_revision != archive.revision
    224     {
    225         reachability.verify(nix_revision)?;
    226     }
    227     validate_service_cleanliness(&service_root)?;
    228     if service_head(&service_root)? != initial_head {
    229         return Err(CommandError::DirtyServiceSource);
    230     }
    231 
    232     let desired = ServiceSourceLockV2::new(ServiceSourceLockParts {
    233         service: &metadata.service,
    234         revision: &archive.revision,
    235         workspace_catalog_sha256: &archive.catalog_sha256,
    236         source_archive_sha256: &archive.archive_sha256,
    237         cargo_lock_sha256: &cargo_lock_sha256,
    238         nix: nix.as_parts(),
    239         contract_versions: metadata.contract_versions,
    240     })
    241     .map_err(|_| CommandError::InvalidServiceMetadata)?;
    242 
    243     let lock_path = service_root.join(LOCK_FILENAME);
    244     let result = match mode {
    245         CommandMode::Check => {
    246             let current = read_bounded_regular(&lock_path, 4096, CommandError::InvalidSourceLock)?;
    247             let current = ServiceSourceLockV2::from_canonical_bytes(&current)
    248                 .map_err(|_| CommandError::InvalidSourceLock)?;
    249             if current == desired {
    250                 Ok(())
    251             } else {
    252                 Err(CommandError::StaleSourceLock)
    253             }
    254         }
    255         CommandMode::Write => {
    256             atomic_write_lock(&service_root, &lock_path, desired.canonical_bytes())?;
    257             let current = read_bounded_regular(&lock_path, 4096, CommandError::WriteFailure)?;
    258             let current = ServiceSourceLockV2::from_canonical_bytes(&current)
    259                 .map_err(|_| CommandError::WriteFailure)?;
    260             if current == desired {
    261                 Ok(())
    262             } else {
    263                 Err(CommandError::WriteFailure)
    264             }
    265         }
    266     };
    267     result?;
    268     validate_service_cleanliness(&service_root)?;
    269     if service_head(&service_root)? == initial_head {
    270         Ok(())
    271     } else {
    272         Err(CommandError::DirtyServiceSource)
    273     }
    274 }
    275 
    276 fn service_head(root: &Path) -> Result<String, CommandError> {
    277     let bytes = git_stdout(root, ["rev-parse", "HEAD"], 128)
    278         .map_err(|_| CommandError::InvalidServiceRoot)?;
    279     let revision = std::str::from_utf8(&bytes)
    280         .map_err(|_| CommandError::InvalidServiceRoot)?
    281         .trim();
    282     if valid_lower_hex(revision, 40) {
    283         Ok(revision.to_owned())
    284     } else {
    285         Err(CommandError::InvalidServiceRoot)
    286     }
    287 }
    288 
    289 fn validate_service_root(path: &Path) -> Result<PathBuf, CommandError> {
    290     if !path.is_absolute() {
    291         return Err(CommandError::InvalidServiceRoot);
    292     }
    293     let metadata = fs::symlink_metadata(path).map_err(|_| CommandError::InvalidServiceRoot)?;
    294     if metadata.file_type().is_symlink() || !metadata.is_dir() {
    295         return Err(CommandError::InvalidServiceRoot);
    296     }
    297     let canonical = fs::canonicalize(path).map_err(|_| CommandError::InvalidServiceRoot)?;
    298     let top = git_stdout(
    299         &canonical,
    300         ["rev-parse", "--show-toplevel"],
    301         MAX_GIT_OUTPUT_BYTES,
    302     )
    303     .map_err(|_| CommandError::InvalidServiceRoot)?;
    304     let top = std::str::from_utf8(&top).map_err(|_| CommandError::InvalidServiceRoot)?;
    305     let top = fs::canonicalize(top.trim()).map_err(|_| CommandError::InvalidServiceRoot)?;
    306     if top == canonical {
    307         Ok(canonical)
    308     } else {
    309         Err(CommandError::InvalidServiceRoot)
    310     }
    311 }
    312 
    313 fn validate_service_cleanliness(root: &Path) -> Result<(), CommandError> {
    314     git_status(
    315         root,
    316         [
    317             "diff",
    318             "--quiet",
    319             "--",
    320             ".",
    321             ":(exclude)radroots.service.source-lock.v2.toml",
    322         ],
    323     )
    324     .map_err(|_| CommandError::DirtyServiceSource)?;
    325     git_status(
    326         root,
    327         [
    328             "diff",
    329             "--cached",
    330             "--quiet",
    331             "--",
    332             ".",
    333             ":(exclude)radroots.service.source-lock.v2.toml",
    334         ],
    335     )
    336     .map_err(|_| CommandError::DirtyServiceSource)?;
    337     let untracked = git_stdout(
    338         root,
    339         ["ls-files", "--others", "--exclude-standard", "-z"],
    340         MAX_GIT_OUTPUT_BYTES,
    341     )
    342     .map_err(|_| CommandError::DirtyServiceSource)?;
    343     let mut paths = untracked
    344         .split(|byte| *byte == 0)
    345         .filter(|path| !path.is_empty());
    346     if paths.all(|path| path == LOCK_FILENAME.as_bytes()) {
    347         Ok(())
    348     } else {
    349         Err(CommandError::DirtyServiceSource)
    350     }
    351 }
    352 
    353 fn parse_service_metadata(root: &toml::Value) -> Result<ServiceMetadata, CommandError> {
    354     let table = root
    355         .get("workspace")
    356         .and_then(|value| value.get("metadata"))
    357         .and_then(|value| value.get("radroots"))
    358         .and_then(|value| value.get("service_source_lock"))
    359         .and_then(toml::Value::as_table)
    360         .ok_or(CommandError::InvalidServiceMetadata)?;
    361     let expected = BTreeSet::from([
    362         "admin_contract_version",
    363         "config_contract_version",
    364         "host_feature_profile",
    365         "nix_material",
    366         "provider_contract_version",
    367         "service",
    368         "state_contract_version",
    369         "status_contract_version",
    370     ]);
    371     if table.keys().map(String::as_str).collect::<BTreeSet<_>>() != expected {
    372         return Err(CommandError::InvalidServiceMetadata);
    373     }
    374     let text = |key: &str| {
    375         table
    376             .get(key)
    377             .and_then(toml::Value::as_str)
    378             .ok_or(CommandError::InvalidServiceMetadata)
    379     };
    380     let version = |key: &str| {
    381         table
    382             .get(key)
    383             .and_then(toml::Value::as_integer)
    384             .and_then(|value| u32::try_from(value).ok())
    385             .filter(|value| *value != 0)
    386             .ok_or(CommandError::InvalidServiceMetadata)
    387     };
    388     if text("host_feature_profile")? != HOST_FEATURE_PROFILE {
    389         return Err(CommandError::InvalidServiceMetadata);
    390     }
    391     let nix_material = match text("nix_material")? {
    392         "absent" => NixMaterialState::Absent,
    393         "deferred" => NixMaterialState::Deferred,
    394         _ => return Err(CommandError::InvalidServiceMetadata),
    395     };
    396     Ok(ServiceMetadata {
    397         service: text("service")?.to_owned(),
    398         nix_material,
    399         contract_versions: ContractVersions::new(
    400             version("config_contract_version")?,
    401             version("state_contract_version")?,
    402             version("admin_contract_version")?,
    403             version("status_contract_version")?,
    404             version("provider_contract_version")?,
    405         ),
    406     })
    407 }
    408 
    409 fn validate_cargo_manifests(
    410     root: &Path,
    411     lib_packages: Option<&BTreeSet<String>>,
    412 ) -> Result<String, CommandError> {
    413     let mut manifests = Vec::new();
    414     let mut entries = 0_usize;
    415     let walker = WalkDir::new(root)
    416         .follow_links(false)
    417         .into_iter()
    418         .filter_entry(|entry| {
    419             entry.file_name() != OsStr::new(".git") && !is_build_output_directory(entry.file_name())
    420         });
    421     for entry in walker {
    422         entries = entries
    423             .checked_add(1)
    424             .ok_or(CommandError::InvalidCargoManifest)?;
    425         if entries > MAX_TREE_ENTRIES {
    426             return Err(CommandError::InvalidCargoManifest);
    427         }
    428         let entry = entry.map_err(|_| CommandError::InvalidCargoManifest)?;
    429         if entry.file_name() == OsStr::new(CARGO_MANIFEST) {
    430             if entry.file_type().is_symlink() || !entry.file_type().is_file() {
    431                 return Err(CommandError::InvalidCargoManifest);
    432             }
    433             manifests.push(entry.into_path());
    434             if manifests.len() > MAX_MANIFESTS {
    435                 return Err(CommandError::InvalidCargoManifest);
    436             }
    437         }
    438     }
    439     manifests.sort();
    440     if manifests.is_empty() {
    441         return Err(CommandError::InvalidCargoManifest);
    442     }
    443     let mut state = ManifestState::default();
    444     for manifest in manifests {
    445         let bytes = read_bounded_regular(
    446             &manifest,
    447             MAX_MANIFEST_BYTES,
    448             CommandError::InvalidCargoManifest,
    449         )?;
    450         let value = parse_toml(&bytes, CommandError::InvalidCargoManifest)?;
    451         validate_manifest_node(&value, None, false, false, lib_packages, &mut state)?;
    452     }
    453     if state.dependencies == 0 || !state.host_dependency {
    454         return Err(CommandError::InvalidCargoManifest);
    455     }
    456     state.revision.ok_or(CommandError::InvalidCargoManifest)
    457 }
    458 
    459 #[derive(Default)]
    460 struct ManifestState {
    461     revision: Option<String>,
    462     dependencies: usize,
    463     host_dependency: bool,
    464 }
    465 
    466 fn validate_manifest_node(
    467     value: &toml::Value,
    468     key: Option<&str>,
    469     in_patch: bool,
    470     in_dependencies: bool,
    471     lib_packages: Option<&BTreeSet<String>>,
    472     state: &mut ManifestState,
    473 ) -> Result<(), CommandError> {
    474     match value {
    475         toml::Value::Table(table) => {
    476             let git = table.get("git").and_then(toml::Value::as_str);
    477             let package = table.get("package").and_then(toml::Value::as_str).or(key);
    478             let known_lib_dependency = (in_dependencies || in_patch)
    479                 && package.is_some_and(|package| {
    480                     lib_packages.map_or(package == HOST_PACKAGE, |packages| {
    481                         packages.contains(package)
    482                     })
    483                 });
    484             let canonical_lib_dependency =
    485                 (in_dependencies || in_patch) && git.is_some_and(|git| git == LIB_REPOSITORY);
    486             if known_lib_dependency || canonical_lib_dependency {
    487                 let git = git.ok_or(CommandError::InvalidCargoManifest)?;
    488                 if in_patch
    489                     || git != LIB_REPOSITORY
    490                     || table.get("version").and_then(toml::Value::as_str)
    491                         != Some(LIB_VERSION_REQUIREMENT)
    492                     || table.contains_key("branch")
    493                     || table.contains_key("tag")
    494                     || table.contains_key("path")
    495                 {
    496                     return Err(CommandError::InvalidCargoManifest);
    497                 }
    498                 let revision = table
    499                     .get("rev")
    500                     .and_then(toml::Value::as_str)
    501                     .filter(|value| valid_lower_hex(value, 40))
    502                     .ok_or(CommandError::InvalidCargoManifest)?;
    503                 match &state.revision {
    504                     Some(expected) if expected != revision => {
    505                         return Err(CommandError::InvalidCargoManifest);
    506                     }
    507                     None => state.revision = Some(revision.to_owned()),
    508                     _ => {}
    509                 }
    510                 state.dependencies += 1;
    511                 state.host_dependency |= key == Some(HOST_PACKAGE)
    512                     || table.get("package").and_then(toml::Value::as_str) == Some(HOST_PACKAGE);
    513             }
    514             for (child_key, child) in table {
    515                 if (known_lib_dependency || canonical_lib_dependency) && child_key == "git" {
    516                     continue;
    517                 }
    518                 let child_dependency_scope = in_dependencies
    519                     || matches!(
    520                         child_key.as_str(),
    521                         "dependencies" | "dev-dependencies" | "build-dependencies"
    522                     );
    523                 validate_manifest_node(
    524                     child,
    525                     Some(child_key),
    526                     in_patch || child_key == "patch",
    527                     child_dependency_scope,
    528                     lib_packages,
    529                     state,
    530                 )?;
    531             }
    532         }
    533         toml::Value::Array(values) => {
    534             for child in values {
    535                 validate_manifest_node(child, key, in_patch, in_dependencies, lib_packages, state)?;
    536             }
    537         }
    538         toml::Value::String(_)
    539             if in_dependencies
    540                 && key.is_some_and(|key| {
    541                     lib_packages.map_or(key == HOST_PACKAGE, |packages| packages.contains(key))
    542                 }) =>
    543         {
    544             return Err(CommandError::InvalidCargoManifest);
    545         }
    546         _ => {}
    547     }
    548     Ok(())
    549 }
    550 
    551 fn validate_cargo_lock(
    552     bytes: &[u8],
    553     revision: &str,
    554     lib_packages: &BTreeSet<String>,
    555 ) -> Result<(), CommandError> {
    556     let text = std::str::from_utf8(bytes).map_err(|_| CommandError::InvalidCargoLock)?;
    557     let document =
    558         toml::from_str::<CargoLockDocument>(text).map_err(|_| CommandError::InvalidCargoLock)?;
    559     let expected = format!("git+{LIB_REPOSITORY}?rev={revision}#{revision}");
    560     let mut count = 0_usize;
    561     let mut host = false;
    562     for package in document.package {
    563         if lib_packages.contains(&package.name) {
    564             let source = package.source.ok_or(CommandError::InvalidCargoLock)?;
    565             if source != expected || package.version != "0.1.0-alpha" {
    566                 return Err(CommandError::InvalidCargoLock);
    567             }
    568             count += 1;
    569             host |= package.name == HOST_PACKAGE;
    570         }
    571     }
    572     if count != 0 && host {
    573         Ok(())
    574     } else {
    575         Err(CommandError::InvalidCargoLock)
    576     }
    577 }
    578 
    579 fn read_nix_material(
    580     root: &Path,
    581     state: NixMaterialState,
    582 ) -> Result<NixMaterialEvidence, CommandError> {
    583     match state {
    584         NixMaterialState::Absent => {
    585             for name in [FLAKE_NIX, FLAKE_LOCK] {
    586                 match fs::symlink_metadata(root.join(name)) {
    587                     Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
    588                     _ => return Err(CommandError::InvalidNixMaterial),
    589                 }
    590             }
    591             Ok(NixMaterialEvidence::Absent)
    592         }
    593         NixMaterialState::Deferred => {
    594             let expression = read_bounded_regular(
    595                 &root.join(FLAKE_NIX),
    596                 MAX_FLAKE_NIX_BYTES,
    597                 CommandError::InvalidNixMaterial,
    598             )?;
    599             let lock = read_bounded_regular(
    600                 &root.join(FLAKE_LOCK),
    601                 MAX_FLAKE_LOCK_BYTES,
    602                 CommandError::InvalidFlakeLock,
    603             )?;
    604             crate::service_source_lock::validate_deferred_nix_lock(&lock)
    605                 .map_err(|_| CommandError::InvalidFlakeLock)?;
    606             let evidence = validate_deferred_nix_material(&expression, &lock)
    607                 .map_err(|_| CommandError::InvalidNixMaterial)?;
    608             Ok(NixMaterialEvidence::Deferred {
    609                 lib_revision: evidence.lib_revision().to_owned(),
    610                 flake_lock_sha256: evidence.flake_lock_sha256().to_owned(),
    611             })
    612         }
    613     }
    614 }
    615 
    616 #[cfg(test)]
    617 fn validate_flake_lock(bytes: &[u8]) -> Result<String, CommandError> {
    618     crate::service_source_lock::validate_deferred_nix_lock(bytes)
    619         .map_err(|_| CommandError::InvalidFlakeLock)
    620 }
    621 
    622 fn validate_toolchain(root: &Path) -> Result<(), CommandError> {
    623     let bytes = read_bounded_regular(
    624         &root.join(RUST_TOOLCHAIN),
    625         MAX_TOOLCHAIN_BYTES,
    626         CommandError::InvalidToolchain,
    627     )?;
    628     let value = parse_toml(&bytes, CommandError::InvalidToolchain)?;
    629     if value
    630         .get("toolchain")
    631         .and_then(|value| value.get("channel"))
    632         .and_then(toml::Value::as_str)
    633         == Some(RUST_VERSION)
    634     {
    635         Ok(())
    636     } else {
    637         Err(CommandError::InvalidToolchain)
    638     }
    639 }
    640 
    641 fn validate_archive(path: &Path, revision: &str) -> Result<ArchiveEvidence, CommandError> {
    642     if !path.is_absolute() {
    643         return Err(CommandError::InvalidSourceArchive);
    644     }
    645     let metadata = fs::symlink_metadata(path).map_err(|_| CommandError::InvalidSourceArchive)?;
    646     if metadata.file_type().is_symlink()
    647         || !metadata.is_file()
    648         || metadata.len() == 0
    649         || metadata.len() > MAX_ARCHIVE_BYTES
    650     {
    651         return Err(CommandError::InvalidSourceArchive);
    652     }
    653     let mut source = fs::File::open(path).map_err(|_| CommandError::InvalidSourceArchive)?;
    654     let before = source
    655         .metadata()
    656         .map_err(|_| CommandError::InvalidSourceArchive)?;
    657     let mut stable = NamedTempFile::new().map_err(|_| CommandError::InvalidSourceArchive)?;
    658     let mut hasher = Sha256::new();
    659     let mut total = 0_u64;
    660     let mut buffer = [0_u8; 65_536];
    661     loop {
    662         let read = source
    663             .read(&mut buffer)
    664             .map_err(|_| CommandError::InvalidSourceArchive)?;
    665         if read == 0 {
    666             break;
    667         }
    668         total = total
    669             .checked_add(u64::try_from(read).map_err(|_| CommandError::InvalidSourceArchive)?)
    670             .filter(|total| *total <= MAX_ARCHIVE_BYTES)
    671             .ok_or(CommandError::InvalidSourceArchive)?;
    672         hasher.update(&buffer[..read]);
    673         stable
    674             .write_all(&buffer[..read])
    675             .map_err(|_| CommandError::InvalidSourceArchive)?;
    676     }
    677     let after = source
    678         .metadata()
    679         .map_err(|_| CommandError::InvalidSourceArchive)?;
    680     if total == 0 || before.len() != total || after.len() != total {
    681         return Err(CommandError::InvalidSourceArchive);
    682     }
    683     stable
    684         .flush()
    685         .and_then(|()| stable.as_file().sync_all())
    686         .map_err(|_| CommandError::InvalidSourceArchive)?;
    687     stable
    688         .as_file_mut()
    689         .seek(SeekFrom::Start(0))
    690         .map_err(|_| CommandError::InvalidSourceArchive)?;
    691 
    692     let verification = TempDir::new().map_err(|_| CommandError::InvalidSourceArchive)?;
    693     git_status(verification.path(), ["init", "--bare", "--quiet"])
    694         .map_err(|_| CommandError::InvalidSourceArchive)?;
    695     let status = Command::new("git")
    696         .args(["bundle", "verify"])
    697         .arg(stable.path())
    698         .current_dir(verification.path())
    699         .stdin(Stdio::null())
    700         .stdout(Stdio::null())
    701         .stderr(Stdio::null())
    702         .status()
    703         .map_err(|_| CommandError::InvalidSourceArchive)?;
    704     if !status.success() {
    705         return Err(CommandError::InvalidSourceArchive);
    706     }
    707     let heads = command_stdout(
    708         Command::new("git")
    709             .args(["bundle", "list-heads"])
    710             .arg(stable.path()),
    711         MAX_GIT_OUTPUT_BYTES,
    712     )
    713     .map_err(|_| CommandError::InvalidSourceArchive)?;
    714     let expected_head = format!("{revision} refs/heads/archive\n");
    715     if heads != expected_head.as_bytes() {
    716         return Err(CommandError::InvalidSourceArchive);
    717     }
    718     let fetch = Command::new("git")
    719         .args(["fetch", "--quiet", "--no-tags"])
    720         .arg(stable.path())
    721         .arg(format!("{revision}:refs/heads/archive"))
    722         .current_dir(verification.path())
    723         .stdin(Stdio::null())
    724         .stdout(Stdio::null())
    725         .stderr(Stdio::null())
    726         .status()
    727         .map_err(|_| CommandError::InvalidSourceArchive)?;
    728     if !fetch.success() {
    729         return Err(CommandError::InvalidSourceArchive);
    730     }
    731     let catalog_spec = format!("{revision}:{CATALOG_RELATIVE}");
    732     let catalog = git_stdout(
    733         verification.path(),
    734         ["show", catalog_spec.as_str()],
    735         MAX_CATALOG_BYTES,
    736     )
    737     .map_err(|_| CommandError::InvalidSourceArchive)?;
    738     if catalog.is_empty() {
    739         return Err(CommandError::InvalidSourceArchive);
    740     }
    741     let package_names = catalog_package_names(&catalog)?;
    742     Ok(ArchiveEvidence {
    743         revision: revision.to_owned(),
    744         archive_sha256: hex::encode(hasher.finalize()),
    745         catalog_sha256: sha256(&catalog),
    746         package_names,
    747     })
    748 }
    749 
    750 fn catalog_package_names(bytes: &[u8]) -> Result<BTreeSet<String>, CommandError> {
    751     let value = parse_toml(bytes, CommandError::InvalidSourceArchive)?;
    752     if value.get("schema").and_then(toml::Value::as_str) != Some("radroots.workspace.catalog.v2")
    753         || value.get("architecture").and_then(toml::Value::as_str)
    754             != Some("radroots.crates.release.v2")
    755         || value.get("version").and_then(toml::Value::as_str) != Some("0.1.0-alpha")
    756     {
    757         return Err(CommandError::InvalidSourceArchive);
    758     }
    759     let packages = value
    760         .get("package")
    761         .and_then(toml::Value::as_array)
    762         .ok_or(CommandError::InvalidSourceArchive)?;
    763     let expected_count = value
    764         .get("package_count")
    765         .and_then(toml::Value::as_integer)
    766         .and_then(|count| usize::try_from(count).ok())
    767         .ok_or(CommandError::InvalidSourceArchive)?;
    768     if packages.is_empty() || packages.len() != expected_count || packages.len() > MAX_MANIFESTS {
    769         return Err(CommandError::InvalidSourceArchive);
    770     }
    771     let mut names = BTreeSet::new();
    772     for package in packages {
    773         let name = package
    774             .get("name")
    775             .and_then(toml::Value::as_str)
    776             .ok_or(CommandError::InvalidSourceArchive)?;
    777         if !names.insert(name.to_owned()) {
    778             return Err(CommandError::InvalidSourceArchive);
    779         }
    780     }
    781     if names.contains(HOST_PACKAGE) {
    782         Ok(names)
    783     } else {
    784         Err(CommandError::InvalidSourceArchive)
    785     }
    786 }
    787 
    788 fn atomic_write_lock(root: &Path, path: &Path, bytes: &[u8]) -> Result<(), CommandError> {
    789     if let Ok(metadata) = fs::symlink_metadata(path)
    790         && (metadata.file_type().is_symlink() || !metadata.is_file())
    791     {
    792         return Err(CommandError::WriteFailure);
    793     }
    794     let mut temporary = NamedTempFile::new_in(root).map_err(|_| CommandError::WriteFailure)?;
    795     temporary
    796         .write_all(bytes)
    797         .and_then(|()| temporary.as_file().sync_all())
    798         .map_err(|_| CommandError::WriteFailure)?;
    799     temporary
    800         .persist(path)
    801         .map_err(|_| CommandError::WriteFailure)?;
    802     fs::File::open(root)
    803         .and_then(|directory| directory.sync_all())
    804         .map_err(|_| CommandError::WriteFailure)
    805 }
    806 
    807 fn read_bounded_regular(
    808     path: &Path,
    809     maximum: usize,
    810     error: CommandError,
    811 ) -> Result<Vec<u8>, CommandError> {
    812     let metadata = fs::symlink_metadata(path).map_err(|_| error)?;
    813     if metadata.file_type().is_symlink() || !metadata.is_file() || metadata.len() > maximum as u64 {
    814         return Err(error);
    815     }
    816     let file = fs::File::open(path).map_err(|_| error)?;
    817     let mut bytes = Vec::with_capacity(usize::try_from(metadata.len()).map_err(|_| error)?);
    818     file.take(maximum as u64 + 1)
    819         .read_to_end(&mut bytes)
    820         .map_err(|_| error)?;
    821     if bytes.len() > maximum {
    822         Err(error)
    823     } else {
    824         Ok(bytes)
    825     }
    826 }
    827 
    828 fn parse_toml(bytes: &[u8], error: CommandError) -> Result<toml::Value, CommandError> {
    829     let text = std::str::from_utf8(bytes).map_err(|_| error)?;
    830     toml::from_str(text).map_err(|_| error)
    831 }
    832 
    833 fn sha256(bytes: &[u8]) -> String {
    834     hex::encode(Sha256::digest(bytes))
    835 }
    836 
    837 fn valid_lower_hex(value: &str, length: usize) -> bool {
    838     value.len() == length
    839         && value
    840             .bytes()
    841             .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
    842 }
    843 
    844 #[cfg(test)]
    845 fn valid_nix_sha256(value: &str) -> bool {
    846     let Some(encoded) = value.strip_prefix("sha256-") else {
    847         return false;
    848     };
    849     let bytes = encoded.as_bytes();
    850     bytes.len() == 44
    851         && bytes[43] == b'='
    852         && bytes[..43]
    853             .iter()
    854             .copied()
    855             .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'+' | b'/'))
    856 }
    857 
    858 fn git_status<const N: usize>(root: &Path, args: [&str; N]) -> Result<(), ()> {
    859     let status = Command::new("git")
    860         .args(args)
    861         .current_dir(root)
    862         .stdin(Stdio::null())
    863         .stdout(Stdio::null())
    864         .stderr(Stdio::null())
    865         .status()
    866         .map_err(|_| ())?;
    867     if status.success() { Ok(()) } else { Err(()) }
    868 }
    869 
    870 fn git_stdout<const N: usize>(root: &Path, args: [&str; N], maximum: usize) -> Result<Vec<u8>, ()> {
    871     let mut command = Command::new("git");
    872     command.args(args).current_dir(root);
    873     command_stdout(&mut command, maximum)
    874 }
    875 
    876 fn command_stdout(command: &mut Command, maximum: usize) -> Result<Vec<u8>, ()> {
    877     let mut child = command
    878         .stdin(Stdio::null())
    879         .stdout(Stdio::piped())
    880         .stderr(Stdio::null())
    881         .spawn()
    882         .map_err(|_| ())?;
    883     let mut stdout = child.stdout.take().ok_or(())?;
    884     let mut bytes = Vec::new();
    885     if stdout
    886         .by_ref()
    887         .take(maximum as u64 + 1)
    888         .read_to_end(&mut bytes)
    889         .is_err()
    890     {
    891         let _ = child.kill();
    892         let _ = child.wait();
    893         return Err(());
    894     }
    895     if bytes.len() > maximum {
    896         let _ = child.kill();
    897         let _ = child.wait();
    898         return Err(());
    899     }
    900     let status = child.wait().map_err(|_| ())?;
    901     if status.success() { Ok(bytes) } else { Err(()) }
    902 }
    903 
    904 #[cfg(test)]
    905 mod tests {
    906     use std::{
    907         error::Error as _,
    908         sync::atomic::{AtomicBool, Ordering},
    909     };
    910 
    911     use super::*;
    912 
    913     struct Reachability {
    914         reachable: AtomicBool,
    915     }
    916 
    917     impl RevisionReachability for Reachability {
    918         fn verify(&self, _revision: &str) -> Result<(), CommandError> {
    919             if self.reachable.load(Ordering::SeqCst) {
    920                 Ok(())
    921             } else {
    922                 Err(CommandError::UnreachableRevision)
    923             }
    924         }
    925     }
    926 
    927     struct CommitOnVerify {
    928         service: PathBuf,
    929     }
    930 
    931     impl RevisionReachability for CommitOnVerify {
    932         fn verify(&self, _revision: &str) -> Result<(), CommandError> {
    933             fs::write(self.service.join("concurrent"), b"changed").expect("concurrent file");
    934             git(&self.service, &["add", "."]);
    935             git(
    936                 &self.service,
    937                 &["commit", "--quiet", "-m", "concurrent commit"],
    938             );
    939             Ok(())
    940         }
    941     }
    942 
    943     struct Fixture {
    944         root: TempDir,
    945         service: PathBuf,
    946         archive: PathBuf,
    947         revision: String,
    948     }
    949 
    950     impl Fixture {
    951         fn new() -> Self {
    952             let root = TempDir::new().expect("temp root");
    953             let lib = root.path().join("lib");
    954             fs::create_dir(&lib).expect("lib root");
    955             git(&lib, &["init", "--quiet"]);
    956             git(&lib, &["config", "user.email", "fixture@example.invalid"]);
    957             git(&lib, &["config", "user.name", "fixture"]);
    958             let catalog = lib.join(CATALOG_RELATIVE);
    959             fs::create_dir_all(catalog.parent().expect("catalog parent")).expect("catalog parent");
    960             fs::write(
    961                 &catalog,
    962                 br#"schema_version = 2
    963 schema = "radroots.workspace.catalog.v2"
    964 architecture = "radroots.crates.release.v2"
    965 version = "0.1.0-alpha"
    966 package_count = 2
    967 
    968 [[package]]
    969 name = "radroots_core"
    970 
    971 [[package]]
    972 name = "radroots_service_host"
    973 "#,
    974             )
    975             .expect("catalog");
    976             git(&lib, &["add", "."]);
    977             git(&lib, &["commit", "--quiet", "-m", "fixture"]);
    978             git(&lib, &["branch", "-M", "archive"]);
    979             let revision =
    980                 String::from_utf8(git_stdout(&lib, ["rev-parse", "HEAD"], 128).expect("revision"))
    981                     .expect("UTF-8")
    982                     .trim()
    983                     .to_owned();
    984             let archive = root.path().join("lib.bundle");
    985             let status = Command::new("git")
    986                 .args(["bundle", "create"])
    987                 .arg(&archive)
    988                 .arg("refs/heads/archive")
    989                 .current_dir(&lib)
    990                 .status()
    991                 .expect("bundle");
    992             assert!(status.success());
    993 
    994             let service = root.path().join("service");
    995             fs::create_dir(&service).expect("service root");
    996             fs::write(
    997                 service.join(CARGO_MANIFEST),
    998                 format!(
    999                     r#"[workspace]
   1000 resolver = "3"
   1001 
   1002 [workspace.metadata.radroots.service_source_lock]
   1003 service = "fixture_service"
   1004 host_feature_profile = "service-host"
   1005 nix_material = "deferred"
   1006 config_contract_version = 1
   1007 state_contract_version = 2
   1008 admin_contract_version = 3
   1009 status_contract_version = 4
   1010 provider_contract_version = 5
   1011 
   1012 [workspace.dependencies]
   1013 radroots_service_host = {{ git = "{LIB_REPOSITORY}", rev = "{revision}", version = "=0.1.0-alpha" }}
   1014 "#,
   1015                 ),
   1016             )
   1017             .expect("manifest");
   1018             fs::write(
   1019                 service.join(CARGO_LOCK),
   1020                 format!(
   1021                     "version = 4\n\n[[package]]\nname = \"radroots_service_host\"\nversion = \"0.1.0-alpha\"\nsource = \"git+{LIB_REPOSITORY}?rev={revision}#{revision}\"\n"
   1022                 ),
   1023             )
   1024             .expect("Cargo.lock");
   1025             fs::write(
   1026                 service.join(FLAKE_NIX),
   1027                 format!(
   1028                     "{{\n  inputs.lib = {{\n    url = \"github:radrootslabs/lib/{revision}\";\n    flake = false;\n  }};\n  outputs = {{ ... }}: {{ }};\n}}\n"
   1029                 ),
   1030             )
   1031             .expect("flake.nix");
   1032             fs::write(
   1033                 service.join(FLAKE_LOCK),
   1034                 format!(
   1035                     r#"{{"nodes":{{"root":{{"inputs":{{"lib":"lib"}}}},"lib":{{"locked":{{"lastModified":1,"narHash":"sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=","owner":"radrootslabs","repo":"lib","rev":"{revision}","type":"github"}},"original":{{"owner":"radrootslabs","repo":"lib","rev":"{revision}","type":"github"}}}}}},"root":"root","version":7}}
   1036 "#,
   1037                 ),
   1038             )
   1039             .expect("flake.lock");
   1040             fs::write(
   1041                 service.join(RUST_TOOLCHAIN),
   1042                 "[toolchain]\nchannel = \"1.97.1\"\ncomponents = [\"clippy\", \"rustfmt\"]\n",
   1043             )
   1044             .expect("toolchain");
   1045             git(&service, &["init", "--quiet"]);
   1046             git(
   1047                 &service,
   1048                 &["config", "user.email", "fixture@example.invalid"],
   1049             );
   1050             git(&service, &["config", "user.name", "fixture"]);
   1051             git(&service, &["add", "."]);
   1052             git(&service, &["commit", "--quiet", "-m", "fixture"]);
   1053             Self {
   1054                 root,
   1055                 service,
   1056                 archive,
   1057                 revision,
   1058             }
   1059         }
   1060 
   1061         fn reachable(&self) -> Reachability {
   1062             Reachability {
   1063                 reachable: AtomicBool::new(true),
   1064             }
   1065         }
   1066     }
   1067 
   1068     fn git(root: &Path, args: &[&str]) {
   1069         let status = Command::new("git")
   1070             .args(args)
   1071             .current_dir(root)
   1072             .status()
   1073             .expect("git");
   1074         assert!(status.success(), "git {args:?}");
   1075     }
   1076 
   1077     #[test]
   1078     fn update_and_check_bind_complete_local_evidence() {
   1079         let fixture = Fixture::new();
   1080         let reachability = fixture.reachable();
   1081         run_with(
   1082             CommandMode::Write,
   1083             &fixture.service,
   1084             &fixture.archive,
   1085             &reachability,
   1086         )
   1087         .expect("write lock");
   1088         run_with(
   1089             CommandMode::Check,
   1090             &fixture.service,
   1091             &fixture.archive,
   1092             &reachability,
   1093         )
   1094         .expect("check lock");
   1095 
   1096         let bytes = fs::read(fixture.service.join(LOCK_FILENAME)).expect("lock");
   1097         let text = std::str::from_utf8(&bytes).expect("UTF-8");
   1098         assert!(text.contains("service = \"fixture_service\""));
   1099         assert!(text.contains(&format!("revision = \"{}\"", fixture.revision)));
   1100         assert!(text.contains("material = \"deferred\""));
   1101         assert!(text.contains(&format!("lib_revision = \"{}\"", fixture.revision)));
   1102         assert!(text.contains("config = 1\nstate = 2\nadmin = 3\nstatus = 4\nprovider = 5"));
   1103         assert!(fixture.root.path().exists());
   1104     }
   1105 
   1106     #[test]
   1107     fn check_rejects_stale_lock_and_update_repairs_it() {
   1108         let fixture = Fixture::new();
   1109         let reachability = fixture.reachable();
   1110         run_with(
   1111             CommandMode::Write,
   1112             &fixture.service,
   1113             &fixture.archive,
   1114             &reachability,
   1115         )
   1116         .expect("write lock");
   1117         let lock = fixture.service.join(LOCK_FILENAME);
   1118         let stale =
   1119             fs::read_to_string(&lock)
   1120                 .expect("read lock")
   1121                 .replacen("config = 1", "config = 9", 1);
   1122         fs::write(&lock, stale).expect("stale lock");
   1123         assert_eq!(
   1124             run_with(
   1125                 CommandMode::Check,
   1126                 &fixture.service,
   1127                 &fixture.archive,
   1128                 &reachability,
   1129             ),
   1130             Err(CommandError::StaleSourceLock)
   1131         );
   1132         run_with(
   1133             CommandMode::Write,
   1134             &fixture.service,
   1135             &fixture.archive,
   1136             &reachability,
   1137         )
   1138         .expect("repair lock");
   1139     }
   1140 
   1141     #[test]
   1142     fn dirty_source_and_unreachable_revision_fail_closed() {
   1143         let fixture = Fixture::new();
   1144         fs::write(fixture.service.join("untracked-secret"), b"sensitive").expect("dirty file");
   1145         assert_eq!(
   1146             run_with(
   1147                 CommandMode::Write,
   1148                 &fixture.service,
   1149                 &fixture.archive,
   1150                 &fixture.reachable(),
   1151             ),
   1152             Err(CommandError::DirtyServiceSource)
   1153         );
   1154         fs::remove_file(fixture.service.join("untracked-secret")).expect("remove dirty file");
   1155         let unreachable = Reachability {
   1156             reachable: AtomicBool::new(false),
   1157         };
   1158         assert_eq!(
   1159             run_with(
   1160                 CommandMode::Write,
   1161                 &fixture.service,
   1162                 &fixture.archive,
   1163                 &unreachable,
   1164             ),
   1165             Err(CommandError::UnreachableRevision)
   1166         );
   1167         assert!(!fixture.service.join(LOCK_FILENAME).exists());
   1168 
   1169         let fixture = Fixture::new();
   1170         let commit_on_verify = CommitOnVerify {
   1171             service: fixture.service.clone(),
   1172         };
   1173         assert_eq!(
   1174             run_with(
   1175                 CommandMode::Write,
   1176                 &fixture.service,
   1177                 &fixture.archive,
   1178                 &commit_on_verify,
   1179             ),
   1180             Err(CommandError::DirtyServiceSource)
   1181         );
   1182         assert!(!fixture.service.join(LOCK_FILENAME).exists());
   1183     }
   1184 
   1185     #[test]
   1186     fn cargo_flake_toolchain_and_archive_drift_are_distinct() {
   1187         let cases = [
   1188             (
   1189                 CARGO_MANIFEST,
   1190                 "radroots_service_host",
   1191                 "other",
   1192                 CommandError::InvalidCargoManifest,
   1193             ),
   1194             (
   1195                 CARGO_LOCK,
   1196                 "radroots_service_host",
   1197                 "other",
   1198                 CommandError::InvalidCargoLock,
   1199             ),
   1200             (
   1201                 FLAKE_NIX,
   1202                 "radrootslabs/lib",
   1203                 "example.invalid/lib",
   1204                 CommandError::InvalidNixMaterial,
   1205             ),
   1206             (
   1207                 FLAKE_NIX,
   1208                 "inputs.lib = {",
   1209                 "other = {",
   1210                 CommandError::InvalidNixMaterial,
   1211             ),
   1212             (
   1213                 FLAKE_LOCK,
   1214                 "radrootslabs",
   1215                 "other",
   1216                 CommandError::InvalidFlakeLock,
   1217             ),
   1218             (
   1219                 RUST_TOOLCHAIN,
   1220                 "1.97.1",
   1221                 "1.97.0",
   1222                 CommandError::InvalidToolchain,
   1223             ),
   1224         ];
   1225         for (path, from, to, expected) in cases {
   1226             let fixture = Fixture::new();
   1227             let path = fixture.service.join(path);
   1228             let mutated = fs::read_to_string(&path)
   1229                 .expect("read fixture")
   1230                 .replacen(from, to, 1);
   1231             fs::write(&path, mutated).expect("mutate fixture");
   1232             git(&fixture.service, &["add", "."]);
   1233             git(&fixture.service, &["commit", "--quiet", "-m", "mutate"]);
   1234             assert_eq!(
   1235                 run_with(
   1236                     CommandMode::Write,
   1237                     &fixture.service,
   1238                     &fixture.archive,
   1239                     &fixture.reachable(),
   1240                 ),
   1241                 Err(expected)
   1242             );
   1243         }
   1244 
   1245         let fixture = Fixture::new();
   1246         let other = fixture.root.path().join("other");
   1247         fs::create_dir(&other).expect("other repo");
   1248         git(&other, &["init", "--quiet"]);
   1249         git(&other, &["config", "user.email", "fixture@example.invalid"]);
   1250         git(&other, &["config", "user.name", "fixture"]);
   1251         fs::write(other.join("other"), b"other").expect("other file");
   1252         git(&other, &["add", "."]);
   1253         git(&other, &["commit", "--quiet", "-m", "other"]);
   1254         git(&other, &["branch", "-M", "archive"]);
   1255         let bad_archive = fixture.root.path().join("other.bundle");
   1256         let status = Command::new("git")
   1257             .args(["bundle", "create"])
   1258             .arg(&bad_archive)
   1259             .arg("refs/heads/archive")
   1260             .current_dir(&other)
   1261             .status()
   1262             .expect("other bundle");
   1263         assert!(status.success());
   1264         assert_eq!(
   1265             run_with(
   1266                 CommandMode::Write,
   1267                 &fixture.service,
   1268                 &bad_archive,
   1269                 &fixture.reachable(),
   1270             ),
   1271             Err(CommandError::InvalidSourceArchive)
   1272         );
   1273 
   1274         let fixture = Fixture::new();
   1275         let oversized = fixture.root.path().join("oversized.bundle");
   1276         fs::File::create(&oversized)
   1277             .and_then(|file| file.set_len(MAX_ARCHIVE_BYTES + 1))
   1278             .expect("sparse oversized archive");
   1279         assert_eq!(
   1280             run_with(
   1281                 CommandMode::Write,
   1282                 &fixture.service,
   1283                 &oversized,
   1284                 &fixture.reachable(),
   1285             ),
   1286             Err(CommandError::InvalidSourceArchive)
   1287         );
   1288     }
   1289 
   1290     #[test]
   1291     fn mixed_local_or_unlocked_lib_dependencies_fail_closed() {
   1292         let fixture = Fixture::new();
   1293         let manifest = fixture.service.join(CARGO_MANIFEST);
   1294         let mut text = fs::read_to_string(&manifest).expect("manifest");
   1295         text.push_str("radroots_core = { path = \"../lib/crates/core\" }\n");
   1296         fs::write(&manifest, text).expect("local Lib dependency");
   1297         git(&fixture.service, &["add", "."]);
   1298         git(
   1299             &fixture.service,
   1300             &["commit", "--quiet", "-m", "local dependency"],
   1301         );
   1302         assert_eq!(
   1303             run_with(
   1304                 CommandMode::Write,
   1305                 &fixture.service,
   1306                 &fixture.archive,
   1307                 &fixture.reachable(),
   1308             ),
   1309             Err(CommandError::InvalidCargoManifest)
   1310         );
   1311 
   1312         let fixture = Fixture::new();
   1313         let cargo_lock = fixture.service.join(CARGO_LOCK);
   1314         let mut text = fs::read_to_string(&cargo_lock).expect("Cargo.lock");
   1315         text.push_str("\n[[package]]\nname = \"radroots_core\"\nversion = \"0.1.0-alpha\"\n");
   1316         fs::write(&cargo_lock, text).expect("unlocked Lib package");
   1317         git(&fixture.service, &["add", "."]);
   1318         git(
   1319             &fixture.service,
   1320             &["commit", "--quiet", "-m", "unlocked dependency"],
   1321         );
   1322         assert_eq!(
   1323             run_with(
   1324                 CommandMode::Write,
   1325                 &fixture.service,
   1326                 &fixture.archive,
   1327                 &fixture.reachable(),
   1328             ),
   1329             Err(CommandError::InvalidCargoLock)
   1330         );
   1331 
   1332         let fixture = Fixture::new();
   1333         let manifest = fixture.service.join(CARGO_MANIFEST);
   1334         let mut text = fs::read_to_string(&manifest).expect("manifest");
   1335         text.push_str(&format!(
   1336             "\n[patch.crates-io]\nradroots_core = {{ git = \"{LIB_REPOSITORY}\", rev = \"{}\", version = \"=0.1.0-alpha\" }}\n",
   1337             fixture.revision
   1338         ));
   1339         fs::write(&manifest, text).expect("Lib patch");
   1340         git(&fixture.service, &["add", "."]);
   1341         git(&fixture.service, &["commit", "--quiet", "-m", "Lib patch"]);
   1342         assert_eq!(
   1343             run_with(
   1344                 CommandMode::Write,
   1345                 &fixture.service,
   1346                 &fixture.archive,
   1347                 &fixture.reachable(),
   1348             ),
   1349             Err(CommandError::InvalidCargoManifest)
   1350         );
   1351     }
   1352 
   1353     #[test]
   1354     fn floating_or_unbounded_nix_input_fails_closed() {
   1355         for mutation in [
   1356             ("\"rev\":\"", "\"ref\":\"master\",\"rev\":\""),
   1357             (
   1358                 "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
   1359                 "sha256-invalid",
   1360             ),
   1361             ("\"lastModified\":1,", "\"dir\":\"sub\",\"lastModified\":1,"),
   1362         ] {
   1363             let fixture = Fixture::new();
   1364             let path = fixture.service.join(FLAKE_LOCK);
   1365             let current = fs::read_to_string(&path).expect("flake lock");
   1366             let mutated = if mutation.0 == "\"rev\":\"" {
   1367                 let second = current.rfind(mutation.0).expect("original revision field");
   1368                 format!(
   1369                     "{}{}{}",
   1370                     &current[..second],
   1371                     mutation.1,
   1372                     &current[second + mutation.0.len()..]
   1373                 )
   1374             } else {
   1375                 current.replacen(mutation.0, mutation.1, 1)
   1376             };
   1377             fs::write(&path, mutated).expect("mutate flake lock");
   1378             git(&fixture.service, &["add", "."]);
   1379             git(&fixture.service, &["commit", "--quiet", "-m", "mutate"]);
   1380             assert_eq!(
   1381                 run_with(
   1382                     CommandMode::Write,
   1383                     &fixture.service,
   1384                     &fixture.archive,
   1385                     &fixture.reachable(),
   1386                 ),
   1387                 Err(CommandError::InvalidFlakeLock)
   1388             );
   1389         }
   1390     }
   1391 
   1392     #[test]
   1393     fn absent_and_independently_revisioned_deferred_nix_material_are_exact() {
   1394         let fixture = Fixture::new();
   1395         let manifest = fixture.service.join(CARGO_MANIFEST);
   1396         let source = fs::read_to_string(&manifest).expect("manifest");
   1397         fs::write(
   1398             &manifest,
   1399             source.replacen(
   1400                 "nix_material = \"deferred\"",
   1401                 "nix_material = \"absent\"",
   1402                 1,
   1403             ),
   1404         )
   1405         .expect("absent metadata");
   1406         fs::remove_file(fixture.service.join(FLAKE_NIX)).expect("remove flake expression");
   1407         fs::remove_file(fixture.service.join(FLAKE_LOCK)).expect("remove flake lock");
   1408         git(&fixture.service, &["add", "-A"]);
   1409         git(&fixture.service, &["commit", "--quiet", "-m", "absent Nix"]);
   1410         run_with(
   1411             CommandMode::Write,
   1412             &fixture.service,
   1413             &fixture.archive,
   1414             &fixture.reachable(),
   1415         )
   1416         .expect("write absent-Nix lock");
   1417         let lock = fs::read(fixture.service.join(LOCK_FILENAME)).expect("source lock");
   1418         let lock = ServiceSourceLockV2::from_canonical_bytes(&lock).expect("canonical lock");
   1419         assert_eq!(lock.nix_material_state(), NixMaterialState::Absent);
   1420         assert_eq!(lock.nix_lib_revision(), None);
   1421         assert_eq!(lock.flake_lock_sha256(), None);
   1422 
   1423         let fixture = Fixture::new();
   1424         let deferred_revision = "b".repeat(40);
   1425         for name in [FLAKE_NIX, FLAKE_LOCK] {
   1426             let path = fixture.service.join(name);
   1427             let current = fs::read_to_string(&path).expect("Nix source");
   1428             fs::write(
   1429                 &path,
   1430                 current.replace(&fixture.revision, &deferred_revision),
   1431             )
   1432             .expect("deferred revision");
   1433         }
   1434         git(&fixture.service, &["add", "."]);
   1435         git(
   1436             &fixture.service,
   1437             &["commit", "--quiet", "-m", "defer Nix revision"],
   1438         );
   1439         run_with(
   1440             CommandMode::Write,
   1441             &fixture.service,
   1442             &fixture.archive,
   1443             &fixture.reachable(),
   1444         )
   1445         .expect("write independently revisioned lock");
   1446         let lock = fs::read(fixture.service.join(LOCK_FILENAME)).expect("source lock");
   1447         let lock = ServiceSourceLockV2::from_canonical_bytes(&lock).expect("canonical lock");
   1448         assert_eq!(lock.revision(), fixture.revision);
   1449         assert_eq!(lock.nix_material_state(), NixMaterialState::Deferred);
   1450         assert_eq!(lock.nix_lib_revision(), Some(deferred_revision.as_str()));
   1451     }
   1452 
   1453     #[test]
   1454     fn predecessor_source_lock_is_rejected_instead_of_forming_dual_authority() {
   1455         let fixture = Fixture::new();
   1456         fs::write(
   1457             fixture.service.join(PREDECESSOR_LOCK_FILENAME),
   1458             b"historical lock",
   1459         )
   1460         .expect("predecessor lock");
   1461         git(&fixture.service, &["add", "."]);
   1462         git(
   1463             &fixture.service,
   1464             &["commit", "--quiet", "-m", "retain predecessor lock"],
   1465         );
   1466         assert_eq!(
   1467             run_with(
   1468                 CommandMode::Write,
   1469                 &fixture.service,
   1470                 &fixture.archive,
   1471                 &fixture.reachable(),
   1472             ),
   1473             Err(CommandError::InvalidSourceLock)
   1474         );
   1475     }
   1476 
   1477     #[test]
   1478     fn partial_or_mismatched_nix_material_fails_closed() {
   1479         for removed in [FLAKE_NIX, FLAKE_LOCK] {
   1480             let fixture = Fixture::new();
   1481             fs::remove_file(fixture.service.join(removed)).expect("remove one Nix input");
   1482             git(&fixture.service, &["add", "-A"]);
   1483             git(
   1484                 &fixture.service,
   1485                 &["commit", "--quiet", "-m", "partial Nix"],
   1486             );
   1487             assert!(matches!(
   1488                 run_with(
   1489                     CommandMode::Write,
   1490                     &fixture.service,
   1491                     &fixture.archive,
   1492                     &fixture.reachable(),
   1493                 ),
   1494                 Err(CommandError::InvalidNixMaterial | CommandError::InvalidFlakeLock)
   1495             ));
   1496         }
   1497 
   1498         let fixture = Fixture::new();
   1499         let expression = fixture.service.join(FLAKE_NIX);
   1500         let source = fs::read_to_string(&expression).expect("flake expression");
   1501         fs::write(
   1502             &expression,
   1503             source.replace(&fixture.revision, &"b".repeat(40)),
   1504         )
   1505         .expect("mismatch expression");
   1506         git(&fixture.service, &["add", "."]);
   1507         git(
   1508             &fixture.service,
   1509             &["commit", "--quiet", "-m", "mismatch Nix"],
   1510         );
   1511         assert_eq!(
   1512             run_with(
   1513                 CommandMode::Write,
   1514                 &fixture.service,
   1515                 &fixture.archive,
   1516                 &fixture.reachable(),
   1517             ),
   1518             Err(CommandError::InvalidNixMaterial)
   1519         );
   1520     }
   1521 
   1522     #[test]
   1523     fn flake_lock_rejects_each_independent_fixed_field_drift() {
   1524         let fixture = Fixture::new();
   1525         let bytes = fs::read(fixture.service.join(FLAKE_LOCK)).expect("flake lock");
   1526         let canonical = serde_json::from_slice::<serde_json::Value>(&bytes).expect("flake json");
   1527         for (pointer, replacement) in [
   1528             ("/version", serde_json::json!(6)),
   1529             ("/nodes/root/inputs/lib", serde_json::json!("other")),
   1530             ("/nodes/lib/locked/type", serde_json::json!("git")),
   1531             ("/nodes/lib/locked/owner", serde_json::json!("other")),
   1532             ("/nodes/lib/locked/repo", serde_json::json!("other")),
   1533             ("/nodes/lib/locked/rev", serde_json::json!("other")),
   1534             ("/nodes/lib/locked/lastModified", serde_json::json!("1")),
   1535             (
   1536                 "/nodes/lib/locked/narHash",
   1537                 serde_json::json!("sha256-invalid"),
   1538             ),
   1539             ("/nodes/lib/original/type", serde_json::json!("git")),
   1540             ("/nodes/lib/original/owner", serde_json::json!("other")),
   1541             ("/nodes/lib/original/repo", serde_json::json!("other")),
   1542             ("/nodes/lib/original/rev", serde_json::json!("other")),
   1543         ] {
   1544             let mut drifted = canonical.clone();
   1545             *drifted.pointer_mut(pointer).expect("governed field") = replacement;
   1546             assert_eq!(
   1547                 validate_flake_lock(&serde_json::to_vec(&drifted).expect("flake json")),
   1548                 Err(CommandError::InvalidFlakeLock),
   1549                 "accepted drift at {pointer}"
   1550             );
   1551         }
   1552 
   1553         for (section, field) in [
   1554             ("locked", "extra"),
   1555             ("original", "extra"),
   1556             ("original", "ref"),
   1557         ] {
   1558             let mut drifted = canonical.clone();
   1559             drifted["nodes"]["lib"][section]
   1560                 .as_object_mut()
   1561                 .expect("flake section")
   1562                 .insert(field.into(), serde_json::json!("unexpected"));
   1563             assert_eq!(
   1564                 validate_flake_lock(&serde_json::to_vec(&drifted).expect("flake json")),
   1565                 Err(CommandError::InvalidFlakeLock),
   1566                 "accepted {section}.{field}"
   1567             );
   1568         }
   1569 
   1570         let mut extra_node_field = canonical.clone();
   1571         extra_node_field["nodes"]["lib"]["extra"] = serde_json::json!(true);
   1572         assert_eq!(
   1573             validate_flake_lock(&serde_json::to_vec(&extra_node_field).expect("flake json")),
   1574             Err(CommandError::InvalidFlakeLock)
   1575         );
   1576 
   1577         let mut duplicate = canonical.clone();
   1578         duplicate["nodes"]["lib2"] = duplicate["nodes"]["lib"].clone();
   1579         assert_eq!(
   1580             validate_flake_lock(&serde_json::to_vec(&duplicate).expect("flake json")),
   1581             Err(CommandError::InvalidFlakeLock)
   1582         );
   1583 
   1584         let aliased = String::from_utf8(bytes.clone())
   1585             .expect("flake UTF-8")
   1586             .replacen("\"lib\":\"lib\"", "\"other\":\"lib\"", 1);
   1587         assert_eq!(
   1588             validate_flake_lock(aliased.as_bytes()),
   1589             Err(CommandError::InvalidFlakeLock)
   1590         );
   1591         let duplicated = String::from_utf8(bytes).expect("flake UTF-8").replacen(
   1592             "\"version\":7",
   1593             "\"version\":7,\"version\":7",
   1594             1,
   1595         );
   1596         assert_eq!(
   1597             validate_flake_lock(duplicated.as_bytes()),
   1598             Err(CommandError::InvalidFlakeLock)
   1599         );
   1600     }
   1601 
   1602     #[test]
   1603     fn service_metadata_and_catalog_reject_independent_field_drift() {
   1604         let fixture = Fixture::new();
   1605         let bytes = fs::read(fixture.service.join(CARGO_MANIFEST)).expect("manifest");
   1606         let canonical = parse_toml(&bytes, CommandError::InvalidCargoManifest).expect("manifest");
   1607         for (field, replacement) in [
   1608             ("service", toml::Value::Integer(1)),
   1609             ("host_feature_profile", toml::Value::String("other".into())),
   1610             ("nix_material", toml::Value::String("other".into())),
   1611             ("config_contract_version", toml::Value::Integer(0)),
   1612             ("state_contract_version", toml::Value::Integer(0)),
   1613             ("admin_contract_version", toml::Value::Integer(0)),
   1614             ("status_contract_version", toml::Value::Integer(0)),
   1615             ("provider_contract_version", toml::Value::Integer(0)),
   1616         ] {
   1617             let mut drifted = canonical.clone();
   1618             drifted["workspace"]["metadata"]["radroots"]["service_source_lock"][field] =
   1619                 replacement;
   1620             assert_eq!(
   1621                 parse_service_metadata(&drifted),
   1622                 Err(CommandError::InvalidServiceMetadata),
   1623                 "accepted metadata drift at {field}"
   1624             );
   1625         }
   1626         let mut extra = canonical;
   1627         extra["workspace"]["metadata"]["radroots"]["service_source_lock"]
   1628             .as_table_mut()
   1629             .expect("metadata table")
   1630             .insert("extra".into(), toml::Value::Integer(1));
   1631         assert_eq!(
   1632             parse_service_metadata(&extra),
   1633             Err(CommandError::InvalidServiceMetadata)
   1634         );
   1635 
   1636         let catalog = br#"schema = "radroots.workspace.catalog.v2"
   1637 architecture = "radroots.crates.release.v2"
   1638 version = "0.1.0-alpha"
   1639 package_count = 2
   1640 
   1641 [[package]]
   1642 name = "radroots_core"
   1643 
   1644 [[package]]
   1645 name = "radroots_service_host"
   1646 "#;
   1647         let canonical = parse_toml(catalog, CommandError::InvalidSourceArchive).expect("catalog");
   1648         for (field, replacement) in [
   1649             ("schema", toml::Value::String("other".into())),
   1650             ("architecture", toml::Value::String("other".into())),
   1651             ("version", toml::Value::String("other".into())),
   1652             ("package_count", toml::Value::Integer(1)),
   1653         ] {
   1654             let mut drifted = canonical.clone();
   1655             drifted[field] = replacement;
   1656             assert_eq!(
   1657                 catalog_package_names(toml::to_string(&drifted).expect("catalog").as_bytes()),
   1658                 Err(CommandError::InvalidSourceArchive),
   1659                 "accepted catalog drift at {field}"
   1660             );
   1661         }
   1662         let mut duplicate = canonical.clone();
   1663         duplicate["package"][1]["name"] = toml::Value::String("radroots_core".into());
   1664         assert_eq!(
   1665             catalog_package_names(toml::to_string(&duplicate).expect("catalog").as_bytes()),
   1666             Err(CommandError::InvalidSourceArchive)
   1667         );
   1668         let mut missing_host = canonical;
   1669         missing_host["package"][1]["name"] = toml::Value::String("radroots_other".into());
   1670         assert_eq!(
   1671             catalog_package_names(toml::to_string(&missing_host).expect("catalog").as_bytes()),
   1672             Err(CommandError::InvalidSourceArchive)
   1673         );
   1674     }
   1675 
   1676     #[test]
   1677     fn manifest_and_cargo_lock_reject_each_independent_dependency_drift() {
   1678         let revision = "a".repeat(40);
   1679         let canonical = format!(
   1680             "[dependencies]\nradroots_service_host = {{ git = \"{LIB_REPOSITORY}\", rev = \"{revision}\", version = \"{LIB_VERSION_REQUIREMENT}\" }}\n"
   1681         );
   1682         for (from, to) in [
   1683             (LIB_REPOSITORY, "https://example.invalid/lib"),
   1684             (LIB_VERSION_REQUIREMENT, "=0.2.0"),
   1685             (revision.as_str(), "invalid"),
   1686         ] {
   1687             let value =
   1688                 toml::from_str::<toml::Value>(&canonical.replacen(from, to, 1)).expect("manifest");
   1689             assert_eq!(
   1690                 validate_manifest_node(
   1691                     &value,
   1692                     None,
   1693                     false,
   1694                     false,
   1695                     None,
   1696                     &mut ManifestState::default()
   1697                 ),
   1698                 Err(CommandError::InvalidCargoManifest)
   1699             );
   1700         }
   1701         for forbidden in ["branch", "tag", "path"] {
   1702             let mutated = canonical.replacen(
   1703                 "version = \"=0.1.0-alpha\"",
   1704                 &format!("version = \"=0.1.0-alpha\", {forbidden} = \"forbidden\""),
   1705                 1,
   1706             );
   1707             let value = toml::from_str::<toml::Value>(&mutated).expect("manifest");
   1708             assert_eq!(
   1709                 validate_manifest_node(
   1710                     &value,
   1711                     None,
   1712                     false,
   1713                     false,
   1714                     None,
   1715                     &mut ManifestState::default()
   1716                 ),
   1717                 Err(CommandError::InvalidCargoManifest),
   1718                 "accepted {forbidden}"
   1719             );
   1720         }
   1721         let value = toml::from_str::<toml::Value>(&canonical).expect("manifest");
   1722         assert_eq!(
   1723             validate_manifest_node(
   1724                 &value,
   1725                 None,
   1726                 true,
   1727                 false,
   1728                 None,
   1729                 &mut ManifestState::default()
   1730             ),
   1731             Err(CommandError::InvalidCargoManifest)
   1732         );
   1733         let mut state = ManifestState {
   1734             revision: Some("b".repeat(40)),
   1735             ..ManifestState::default()
   1736         };
   1737         assert_eq!(
   1738             validate_manifest_node(&value, None, false, false, None, &mut state),
   1739             Err(CommandError::InvalidCargoManifest)
   1740         );
   1741 
   1742         let packages = BTreeSet::from([HOST_PACKAGE.to_owned(), "radroots_core".to_owned()]);
   1743         let expected = format!("git+{LIB_REPOSITORY}?rev={revision}#{revision}");
   1744         for document in [
   1745             format!(
   1746                 "version = 4\n\n[[package]]\nname = \"{HOST_PACKAGE}\"\nversion = \"0.1.0-alpha\"\n"
   1747             ),
   1748             format!(
   1749                 "version = 4\n\n[[package]]\nname = \"{HOST_PACKAGE}\"\nversion = \"0.1.0-alpha\"\nsource = \"other\"\n"
   1750             ),
   1751             format!(
   1752                 "version = 4\n\n[[package]]\nname = \"{HOST_PACKAGE}\"\nversion = \"0.2.0\"\nsource = \"{expected}\"\n"
   1753             ),
   1754             format!(
   1755                 "version = 4\n\n[[package]]\nname = \"radroots_core\"\nversion = \"0.1.0-alpha\"\nsource = \"{expected}\"\n"
   1756             ),
   1757             "version = 4\n".to_owned(),
   1758         ] {
   1759             assert_eq!(
   1760                 validate_cargo_lock(document.as_bytes(), &revision, &packages),
   1761                 Err(CommandError::InvalidCargoLock)
   1762             );
   1763         }
   1764     }
   1765 
   1766     #[test]
   1767     fn archive_and_manifest_filesystem_admission_is_fail_closed() {
   1768         let root = TempDir::new().expect("filesystem fixture");
   1769         let empty = root.path().join("empty");
   1770         fs::write(&empty, b"").expect("empty file");
   1771         assert!(matches!(
   1772             validate_archive(Path::new("relative"), &"a".repeat(40)),
   1773             Err(CommandError::InvalidSourceArchive)
   1774         ));
   1775         assert!(matches!(
   1776             validate_archive(root.path(), &"a".repeat(40)),
   1777             Err(CommandError::InvalidSourceArchive)
   1778         ));
   1779         assert!(matches!(
   1780             validate_archive(&empty, &"a".repeat(40)),
   1781             Err(CommandError::InvalidSourceArchive)
   1782         ));
   1783 
   1784         let no_manifest = root.path().join("no-manifest");
   1785         fs::create_dir(&no_manifest).expect("empty tree");
   1786         assert_eq!(
   1787             validate_cargo_manifests(&no_manifest, None),
   1788             Err(CommandError::InvalidCargoManifest)
   1789         );
   1790         let no_dependency = root.path().join("no-dependency");
   1791         fs::create_dir(&no_dependency).expect("manifest tree");
   1792         fs::write(
   1793             no_dependency.join(CARGO_MANIFEST),
   1794             "[workspace]\nresolver = \"3\"\n",
   1795         )
   1796         .expect("manifest");
   1797         assert_eq!(
   1798             validate_cargo_manifests(&no_dependency, None),
   1799             Err(CommandError::InvalidCargoManifest)
   1800         );
   1801 
   1802         #[cfg(unix)]
   1803         {
   1804             let archive_link = root.path().join("archive-link");
   1805             std::os::unix::fs::symlink(&empty, &archive_link).expect("archive symlink");
   1806             assert!(matches!(
   1807                 validate_archive(&archive_link, &"a".repeat(40)),
   1808                 Err(CommandError::InvalidSourceArchive)
   1809             ));
   1810             let manifest_link_root = root.path().join("manifest-link");
   1811             fs::create_dir(&manifest_link_root).expect("manifest symlink tree");
   1812             std::os::unix::fs::symlink(&empty, manifest_link_root.join(CARGO_MANIFEST))
   1813                 .expect("manifest symlink");
   1814             assert_eq!(
   1815                 validate_cargo_manifests(&manifest_link_root, None),
   1816                 Err(CommandError::InvalidCargoManifest)
   1817             );
   1818         }
   1819     }
   1820 
   1821     #[test]
   1822     fn service_manifest_discovery_skips_swiftpm_build_output_only() {
   1823         let fixture = Fixture::new();
   1824         let swiftpm_checkout = fixture.service.join(".build/checkouts/dependency");
   1825         fs::create_dir_all(&swiftpm_checkout).expect("SwiftPM checkout directory");
   1826         fs::write(swiftpm_checkout.join(CARGO_MANIFEST), "not valid TOML")
   1827             .expect("SwiftPM build manifest");
   1828         assert_eq!(
   1829             validate_cargo_manifests(&fixture.service, None),
   1830             Ok(fixture.revision)
   1831         );
   1832 
   1833         let source_lookalike = fixture.service.join(".builder");
   1834         fs::create_dir(&source_lookalike).expect("source lookalike directory");
   1835         fs::write(source_lookalike.join(CARGO_MANIFEST), "not valid TOML")
   1836             .expect("source lookalike manifest");
   1837         assert_eq!(
   1838             validate_cargo_manifests(&fixture.service, None),
   1839             Err(CommandError::InvalidCargoManifest)
   1840         );
   1841     }
   1842 
   1843     #[test]
   1844     fn source_lock_output_rejects_symlink_replacement() {
   1845         let fixture = Fixture::new();
   1846         let target = fixture.root.path().join("foreign");
   1847         fs::write(&target, b"foreign").expect("foreign");
   1848         #[cfg(unix)]
   1849         std::os::unix::fs::symlink(&target, fixture.service.join(LOCK_FILENAME)).expect("symlink");
   1850         #[cfg(unix)]
   1851         assert_eq!(
   1852             run_with(
   1853                 CommandMode::Write,
   1854                 &fixture.service,
   1855                 &fixture.archive,
   1856                 &fixture.reachable(),
   1857             ),
   1858             Err(CommandError::WriteFailure)
   1859         );
   1860         #[cfg(unix)]
   1861         assert_eq!(fs::read(&target).expect("foreign remains"), b"foreign");
   1862     }
   1863 
   1864     #[test]
   1865     fn low_level_source_lock_admission_branches_are_qualified() {
   1866         let root = TempDir::new().expect("low-level source-lock fixture");
   1867         let regular = root.path().join("regular");
   1868         fs::write(&regular, b"same").expect("regular file");
   1869         assert_eq!(
   1870             read_bounded_regular(&regular, 4, CommandError::InvalidSourceLock)
   1871                 .expect("bounded regular file"),
   1872             b"same"
   1873         );
   1874         assert_eq!(
   1875             validate_service_root(Path::new("relative")),
   1876             Err(CommandError::InvalidServiceRoot)
   1877         );
   1878         assert_eq!(
   1879             validate_service_root(&regular),
   1880             Err(CommandError::InvalidServiceRoot)
   1881         );
   1882 
   1883         let directory = root.path().join("directory");
   1884         fs::create_dir(&directory).expect("directory fixture");
   1885         assert_eq!(
   1886             read_bounded_regular(&directory, 4, CommandError::InvalidSourceLock),
   1887             Err(CommandError::InvalidSourceLock)
   1888         );
   1889         let oversized = root.path().join("oversized");
   1890         fs::write(&oversized, b"12345").expect("oversized file");
   1891         assert_eq!(
   1892             read_bounded_regular(&oversized, 4, CommandError::InvalidSourceLock),
   1893             Err(CommandError::InvalidSourceLock)
   1894         );
   1895 
   1896         #[cfg(unix)]
   1897         {
   1898             let symlink = root.path().join("regular-link");
   1899             std::os::unix::fs::symlink(&regular, &symlink).expect("regular symlink");
   1900             assert_eq!(
   1901                 validate_service_root(&symlink),
   1902                 Err(CommandError::InvalidServiceRoot)
   1903             );
   1904             assert_eq!(
   1905                 read_bounded_regular(&symlink, 4, CommandError::InvalidSourceLock),
   1906                 Err(CommandError::InvalidSourceLock)
   1907             );
   1908         }
   1909 
   1910         let repository = root.path().join("repository");
   1911         fs::create_dir(&repository).expect("repository");
   1912         git(&repository, &["init", "--quiet"]);
   1913         let child = repository.join("child");
   1914         fs::create_dir(&child).expect("nested directory");
   1915         assert_eq!(
   1916             validate_service_root(&child),
   1917             Err(CommandError::InvalidServiceRoot)
   1918         );
   1919 
   1920         let empty_catalog = br#"schema = "radroots.workspace.catalog.v2"
   1921 architecture = "radroots.crates.release.v2"
   1922 version = "0.1.0-alpha"
   1923 package_count = 0
   1924 package = []
   1925 "#;
   1926         assert_eq!(
   1927             catalog_package_names(empty_catalog),
   1928             Err(CommandError::InvalidSourceArchive)
   1929         );
   1930 
   1931         let dependency = toml::Value::String("=0.1.0-alpha".into());
   1932         let packages = BTreeSet::from([HOST_PACKAGE.to_owned()]);
   1933         assert_eq!(
   1934             validate_manifest_node(
   1935                 &dependency,
   1936                 Some(HOST_PACKAGE),
   1937                 false,
   1938                 true,
   1939                 Some(&packages),
   1940                 &mut ManifestState::default(),
   1941             ),
   1942             Err(CommandError::InvalidCargoManifest)
   1943         );
   1944 
   1945         assert_eq!(
   1946             validate_flake_lock(br#"{"nodes":{"root":{"inputs":{}}},"root":"root","version":7}"#,),
   1947             Err(CommandError::InvalidFlakeLock)
   1948         );
   1949         assert_eq!(
   1950             validate_flake_lock(
   1951                 br#"{"nodes":{"root":{"inputs":{"lib":"lib"}},"lib":{"original":{"owner":"radrootslabs","repo":"lib"}}},"root":"root","version":7}"#,
   1952             ),
   1953             Err(CommandError::InvalidFlakeLock)
   1954         );
   1955 
   1956         assert!(!valid_nix_sha256("not-a-digest"));
   1957         assert!(!valid_nix_sha256(
   1958             "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
   1959         ));
   1960         assert!(!valid_nix_sha256(
   1961             "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA?="
   1962         ));
   1963 
   1964         let mut oversized_stdout = Command::new("sh");
   1965         oversized_stdout.args(["-c", "printf 12345"]);
   1966         assert_eq!(command_stdout(&mut oversized_stdout, 4), Err(()));
   1967         let mut failed_stdout = Command::new("sh");
   1968         failed_stdout.args(["-c", "exit 7"]);
   1969         assert_eq!(command_stdout(&mut failed_stdout, 4), Err(()));
   1970         assert_eq!(
   1971             git_status(root.path(), ["rev-parse", "--verify", "refs/heads/missing"]),
   1972             Err(())
   1973         );
   1974     }
   1975 
   1976     #[test]
   1977     fn operational_diagnostics_are_fixed_and_source_free() {
   1978         let errors = [
   1979             CommandError::InvalidServiceRoot,
   1980             CommandError::DirtyServiceSource,
   1981             CommandError::InvalidServiceMetadata,
   1982             CommandError::InvalidCargoManifest,
   1983             CommandError::InvalidCargoLock,
   1984             CommandError::InvalidNixMaterial,
   1985             CommandError::InvalidFlakeLock,
   1986             CommandError::InvalidToolchain,
   1987             CommandError::InvalidSourceArchive,
   1988             CommandError::UnreachableRevision,
   1989             CommandError::InvalidSourceLock,
   1990             CommandError::StaleSourceLock,
   1991             CommandError::WriteFailure,
   1992         ];
   1993         for error in errors {
   1994             assert!(error.source().is_none());
   1995             let rendered = format!("{error:?} {error}");
   1996             assert!(!rendered.contains("fixture_service"));
   1997             assert!(!rendered.contains("radrootslabs"));
   1998             assert!(!rendered.contains("/tmp"));
   1999         }
   2000     }
   2001 }