lib

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

commit e56509b9dd3d0695f138d59aa524f64c9911a6c4
parent 05b27fbea7f618749dd3ca37516e71b2db8f1091
Author: triesap <tyson@radroots.org>
Date:   Thu, 20 Aug 2026 23:06:27 +0000

build: add service source lock commands

- add strict write and check modes for standalone service source locks
- bind Cargo, Nix, archive, catalog, toolchain, and remote revision evidence
- reject mixed Lib sources, dirty trees, source drift, and unsafe outputs
- cover update, verification, boundary drift, and redacted failure paths

Diffstat:
MAGENTS.md | 7+++++++
Mcontracts/architecture/decisions/services_hardening_source_lock.v1.json | 41+++++++++++++++++++++++++++++++++++------
Mtools/xtask/README | 3+++
Mtools/xtask/src/main.rs | 55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtools/xtask/src/service_source_lock.rs | 92++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
Atools/xtask/src/service_source_lock_command.rs | 1391+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 1561 insertions(+), 28 deletions(-)

diff --git a/AGENTS.md b/AGENTS.md @@ -45,6 +45,13 @@ This file exists for compatibility with tools that look for AGENTS.md. private to repo tooling, reject noncanonical or extra fields, and never put credentials, local paths, floating refs, or private repository identity in it. +- Generate or verify that lock with `cargo xtask service-source-lock --mode + write|check --service-root <absolute-directory> --source-archive + <absolute-bundle>`. The service root supplies the exact + `workspace.metadata.radroots.service_source_lock` Cargo metadata, and every + Lib dependency, the Cargo lock, the direct revision-pinned Nix input, the + source archive, and the canonical public remote must agree. The command + rejects every source-tree change except the exact generated lock path. - Current source and tests are implementation evidence. They do not silently override `radroots.crates.release.v1`. - Record any evidence-based plan deviation in diff --git a/contracts/architecture/decisions/services_hardening_source_lock.v1.json b/contracts/architecture/decisions/services_hardening_source_lock.v1.json @@ -60,12 +60,41 @@ "toml": "schema = \"radroots.service.source-lock.v1\"\ncontract_version = 1\nservice = \"fixture_service\"\nrepository = \"https://github.com/radrootslabs/lib\"\nrevision = \"1111111111111111111111111111111111111111\"\narchitecture = \"radroots.crates.release.v2\"\nworkspace_catalog_sha256 = \"2222222222222222222222222222222222222222222222222222222222222222\"\nversion = \"0.1.0-alpha\"\nsource_archive_sha256 = \"3333333333333333333333333333333333333333333333333333333333333333\"\ncargo_lock_sha256 = \"4444444444444444444444444444444444444444444444444444444444444444\"\nflake_lock_sha256 = \"5555555555555555555555555555555555555555555555555555555555555555\"\nrust_version = \"1.97.1\"\nhost_feature_profile = \"service-host\"\n\n[contract_versions]\nconfig = 1\nstate = 2\nadmin = 3\nstatus = 4\nprovider = 5\n", "sha256": "2257efc8fb3ff4ee8e429e326effdfe622c5e898b429ee1a8ea3aac38f9810cc" }, + "operations": { + "command": "cargo xtask service-source-lock", + "modes": [ + "check", + "write" + ], + "required_arguments": [ + "mode", + "service_root", + "source_archive" + ], + "service_metadata_path": "Cargo.toml.workspace.metadata.radroots.service_source_lock", + "service_metadata_fields": [ + "service", + "host_feature_profile", + "config_contract_version", + "state_contract_version", + "admin_contract_version", + "status_contract_version", + "provider_contract_version" + ], + "lib_dependency_inventory": "verified_source_archive_workspace_catalog", + "source_cleanliness": "all_changes_forbidden_except_exact_generated_lock_path", + "service_revision_stability": "same_head_before_and_after_evidence_and_output", + "revision_agreement": [ + "cargo_manifests", + "cargo_lock", + "direct_exact_nix_input", + "source_archive", + "canonical_public_remote" + ], + "maximum_source_archive_bytes": 1073741824 + }, "deferred_operations": [ - "filesystem_digest_verification", - "git_cleanliness", - "manifest_and_cargo_metadata_agreement", - "remote_revision_reachability", - "source_archive_verification", - "update_and_verify_commands" + "embedded_build_information_agreement", + "service_fixture_release_graph" ] } diff --git a/tools/xtask/README b/tools/xtask/README @@ -14,6 +14,9 @@ tasks for the `radroots` core libraries. strict enforcement mode; * deterministic `dto-roots --write|--check` generation for every source-manifest package in the workspace DTO authority; + * strict `service-source-lock --mode write|check` generation and verification + for standalone hardened services, including Cargo, Nix, source-archive, + toolchain, cleanliness, and public-revision agreement; * command-dispatch code used for contract, coverage, hygiene, and release paths inside the workspace; * a non-published binary crate used as tooling rather than as a library diff --git a/tools/xtask/src/main.rs b/tools/xtask/src/main.rs @@ -39,6 +39,7 @@ mod safety_qualification; #[cfg_attr(coverage_nightly, coverage(off))] mod sdk_generation; mod service_source_lock; +mod service_source_lock_command; #[cfg_attr(coverage_nightly, coverage(off))] mod supply_chain_qualification; #[cfg_attr(coverage_nightly, coverage(off))] @@ -116,6 +117,14 @@ enum XtaskCommand { #[arg(long)] consumer_root: PathBuf, }, + ServiceSourceLock { + #[arg(long, value_enum)] + mode: ServiceSourceLockMode, + #[arg(long)] + service_root: PathBuf, + #[arg(long)] + source_archive: PathBuf, + }, Source { #[command(subcommand)] command: SourceCommand, @@ -170,6 +179,12 @@ enum SourceMode { } #[derive(Clone, Copy, Debug, ValueEnum)] +enum ServiceSourceLockMode { + Check, + Write, +} + +#[derive(Clone, Copy, Debug, ValueEnum)] enum ArtifactProduct { Sdk, Mobile, @@ -303,6 +318,9 @@ fn usage() { ); eprintln!(" cargo xtask source-lock --consumer-root <absolute-directory>"); eprintln!( + " cargo xtask service-source-lock --mode <check|write> --service-root <absolute-directory> --source-archive <absolute-bundle>" + ); + eprintln!( " cargo xtask source materialize --consumer-root <absolute-directory> --cache-root <absolute-directory> --mode <prefetch|offline>" ); eprintln!(" cargo xtask source archive-verify --archive <bundle> --sha256 <digest>"); @@ -470,6 +488,18 @@ fn run(args: &[String]) -> Result<(), String> { XtaskCommand::SourceLock { consumer_root } => { build_control::validate_consumer(&consumer_root).map(|_| ()) } + XtaskCommand::ServiceSourceLock { + mode, + service_root, + source_archive, + } => service_source_lock_command::run( + match mode { + ServiceSourceLockMode::Check => service_source_lock_command::CommandMode::Check, + ServiceSourceLockMode::Write => service_source_lock_command::CommandMode::Write, + }, + &service_root, + &source_archive, + ), XtaskCommand::Source { command } => match command { SourceCommand::Materialize { consumer_root, @@ -623,6 +653,31 @@ mod tests { assert!( Cli::try_parse_from([ "xtask", + "service-source-lock", + "--mode", + "check", + "--service-root", + "/tmp/service", + ]) + .is_err() + ); + assert!( + Cli::try_parse_from([ + "xtask", + "service-source-lock", + "--mode", + "write", + "--service-root", + "/tmp/service", + "--source-archive", + "/tmp/lib.bundle", + ]) + .is_ok() + ); + + assert!( + Cli::try_parse_from([ + "xtask", "artifact", "--product", "unknown", diff --git a/tools/xtask/src/service_source_lock.rs b/tools/xtask/src/service_source_lock.rs @@ -6,8 +6,8 @@ use sha2::{Digest, Sha256}; const CONTRACT_RELATIVE: &str = "contracts/architecture/decisions/services_hardening_source_lock.v1.json"; const LOCK_SCHEMA: &str = "radroots.service.source-lock.v1"; -const LOCK_FILENAME: &str = "radroots.service.source-lock.v1.toml"; -const LIB_REPOSITORY: &str = "https://github.com/radrootslabs/lib"; +pub(crate) const LOCK_FILENAME: &str = "radroots.service.source-lock.v1.toml"; +pub(crate) const LIB_REPOSITORY: &str = "https://github.com/radrootslabs/lib"; const ARCHITECTURE: &str = "radroots.crates.release.v2"; const LIB_VERSION: &str = "0.1.0-alpha"; const RUST_VERSION: &str = "1.97.1"; @@ -102,7 +102,7 @@ impl std::error::Error for ServiceSourceLockError {} #[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq)] #[serde(deny_unknown_fields)] -struct ContractVersions { +pub(crate) struct ContractVersions { config: u32, state: u32, admin: u32, @@ -111,7 +111,13 @@ struct ContractVersions { } impl ContractVersions { - const fn new(config: u32, state: u32, admin: u32, status: u32, provider: u32) -> Self { + pub(crate) const fn new( + config: u32, + state: u32, + admin: u32, + status: u32, + provider: u32, + ) -> Self { Self { config, state, @@ -149,18 +155,18 @@ struct RawServiceSourceLock { contract_versions: ContractVersions, } -struct ServiceSourceLockParts<'a> { - service: &'a str, - revision: &'a str, - workspace_catalog_sha256: &'a str, - source_archive_sha256: &'a str, - cargo_lock_sha256: &'a str, - flake_lock_sha256: &'a str, - contract_versions: ContractVersions, +pub(crate) struct ServiceSourceLockParts<'a> { + pub(crate) service: &'a str, + pub(crate) revision: &'a str, + pub(crate) workspace_catalog_sha256: &'a str, + pub(crate) source_archive_sha256: &'a str, + pub(crate) cargo_lock_sha256: &'a str, + pub(crate) flake_lock_sha256: &'a str, + pub(crate) contract_versions: ContractVersions, } #[derive(Clone, Eq, PartialEq)] -struct ServiceSourceLockV1 { +pub(crate) struct ServiceSourceLockV1 { raw: RawServiceSourceLock, canonical: Box<[u8]>, } @@ -174,7 +180,7 @@ impl fmt::Debug for ServiceSourceLockV1 { } impl ServiceSourceLockV1 { - fn new(parts: ServiceSourceLockParts<'_>) -> Result<Self, ServiceSourceLockError> { + pub(crate) fn new(parts: ServiceSourceLockParts<'_>) -> Result<Self, ServiceSourceLockError> { validate_parts(&parts)?; let raw = RawServiceSourceLock { schema: LOCK_SCHEMA.to_owned(), @@ -195,7 +201,7 @@ impl ServiceSourceLockV1 { Ok(Self::from_validated_raw(raw)) } - fn from_canonical_bytes(bytes: &[u8]) -> Result<Self, ServiceSourceLockError> { + pub(crate) fn from_canonical_bytes(bytes: &[u8]) -> Result<Self, ServiceSourceLockError> { if bytes.len() > MAX_LOCK_BYTES { return Err(ServiceSourceLockError::TooLarge); } @@ -216,7 +222,7 @@ impl ServiceSourceLockV1 { Self { raw, canonical } } - fn canonical_bytes(&self) -> &[u8] { + pub(crate) fn canonical_bytes(&self) -> &[u8] { debug_assert_eq!(self.raw.schema, LOCK_SCHEMA); &self.canonical } @@ -242,11 +248,27 @@ struct SourceLockDecision { contract_version_rule: String, negative_error_codes: Vec<String>, canonical_vector: CanonicalVector, + operations: OperationsDecision, deferred_operations: Vec<String>, } #[derive(Debug, Deserialize)] #[serde(deny_unknown_fields)] +struct OperationsDecision { + command: String, + modes: Vec<String>, + required_arguments: Vec<String>, + service_metadata_path: String, + service_metadata_fields: Vec<String>, + lib_dependency_inventory: String, + source_cleanliness: String, + service_revision_stability: String, + revision_agreement: Vec<String>, + maximum_source_archive_bytes: u64, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] struct DigestSubjects { workspace_catalog_sha256: String, source_archive_sha256: String, @@ -345,14 +367,40 @@ fn validate_decision(decision: &SourceLockDecision) -> Result<(), ServiceSourceL && decision.contract_version_rule == "u32_nonzero" && error_codes == ERROR_CODES && decision.negative_error_codes == error_codes + && decision.operations.command == "cargo xtask service-source-lock" + && decision.operations.modes == ["check", "write"] + && decision.operations.required_arguments == ["mode", "service_root", "source_archive"] + && decision.operations.service_metadata_path + == "Cargo.toml.workspace.metadata.radroots.service_source_lock" + && decision.operations.service_metadata_fields + == [ + "service", + "host_feature_profile", + "config_contract_version", + "state_contract_version", + "admin_contract_version", + "status_contract_version", + "provider_contract_version", + ] + && decision.operations.lib_dependency_inventory + == "verified_source_archive_workspace_catalog" + && decision.operations.source_cleanliness + == "all_changes_forbidden_except_exact_generated_lock_path" + && decision.operations.service_revision_stability + == "same_head_before_and_after_evidence_and_output" + && decision.operations.revision_agreement + == [ + "cargo_manifests", + "cargo_lock", + "direct_exact_nix_input", + "source_archive", + "canonical_public_remote", + ] + && decision.operations.maximum_source_archive_bytes == 1_073_741_824 && decision.deferred_operations == [ - "filesystem_digest_verification", - "git_cleanliness", - "manifest_and_cargo_metadata_agreement", - "remote_revision_reachability", - "source_archive_verification", - "update_and_verify_commands", + "embedded_build_information_agreement", + "service_fixture_release_graph", ]; if exact { Ok(()) diff --git a/tools/xtask/src/service_source_lock_command.rs b/tools/xtask/src/service_source_lock_command.rs @@ -0,0 +1,1391 @@ +use std::{ + collections::BTreeSet, + ffi::OsStr, + fmt, fs, + io::{Read as _, Seek as _, SeekFrom, Write as _}, + path::{Path, PathBuf}, + process::{Command, Stdio}, +}; + +use serde::Deserialize; +use sha2::{Digest as _, Sha256}; +use tempfile::{NamedTempFile, TempDir}; +use walkdir::WalkDir; + +use crate::service_source_lock::{ + ContractVersions, LIB_REPOSITORY, LOCK_FILENAME, ServiceSourceLockParts, ServiceSourceLockV1, +}; + +const CATALOG_RELATIVE: &str = "contracts/crates/catalog.v2.toml"; +const CARGO_MANIFEST: &str = "Cargo.toml"; +const CARGO_LOCK: &str = "Cargo.lock"; +const FLAKE_LOCK: &str = "flake.lock"; +const RUST_TOOLCHAIN: &str = "rust-toolchain.toml"; +const LIB_VERSION_REQUIREMENT: &str = "=0.1.0-alpha"; +const HOST_PACKAGE: &str = "radroots_service_host"; +const HOST_FEATURE_PROFILE: &str = "service-host"; +const RUST_VERSION: &str = "1.97.1"; +const MAX_MANIFEST_BYTES: usize = 1_048_576; +const MAX_CARGO_LOCK_BYTES: usize = 16_777_216; +const MAX_FLAKE_LOCK_BYTES: usize = 4_194_304; +const MAX_TOOLCHAIN_BYTES: usize = 65_536; +const MAX_CATALOG_BYTES: usize = 4_194_304; +const MAX_GIT_OUTPUT_BYTES: usize = 65_536; +const MAX_ARCHIVE_BYTES: u64 = 1_073_741_824; +const MAX_TREE_ENTRIES: usize = 16_384; +const MAX_MANIFESTS: usize = 512; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum CommandMode { + Check, + Write, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum CommandError { + InvalidServiceRoot, + DirtyServiceSource, + InvalidServiceMetadata, + InvalidCargoManifest, + InvalidCargoLock, + InvalidFlakeLock, + InvalidToolchain, + InvalidSourceArchive, + UnreachableRevision, + InvalidSourceLock, + StaleSourceLock, + WriteFailure, +} + +impl fmt::Display for CommandError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(match self { + Self::InvalidServiceRoot => "service source-lock root is invalid", + Self::DirtyServiceSource => "service source contains an ungoverned change", + Self::InvalidServiceMetadata => "service source-lock metadata is invalid", + Self::InvalidCargoManifest => "service Cargo manifest dependency is invalid", + Self::InvalidCargoLock => "service Cargo lock is invalid", + Self::InvalidFlakeLock => "service flake lock is invalid", + Self::InvalidToolchain => "service Rust toolchain is invalid", + Self::InvalidSourceArchive => "Lib source archive is invalid", + Self::UnreachableRevision => "Lib revision is not remotely reachable", + Self::InvalidSourceLock => "service source lock is invalid", + Self::StaleSourceLock => "service source lock is stale", + Self::WriteFailure => "service source lock could not be updated", + }) + } +} + +impl std::error::Error for CommandError {} + +#[derive(Clone, Debug, Eq, PartialEq)] +struct ServiceMetadata { + service: String, + contract_versions: ContractVersions, +} + +#[derive(Debug, Deserialize)] +struct CargoLockDocument { + #[serde(default)] + package: Vec<CargoLockPackage>, +} + +#[derive(Debug, Deserialize)] +struct CargoLockPackage { + name: String, + version: String, + source: Option<String>, +} + +struct ArchiveEvidence { + revision: String, + archive_sha256: String, + catalog_sha256: String, + package_names: BTreeSet<String>, +} + +trait RevisionReachability { + fn verify(&self, revision: &str) -> Result<(), CommandError>; +} + +struct PublicLibRemote; + +impl RevisionReachability for PublicLibRemote { + fn verify(&self, revision: &str) -> Result<(), CommandError> { + let repository = TempDir::new().map_err(|_| CommandError::UnreachableRevision)?; + git_status(repository.path(), ["init", "--bare", "--quiet"]) + .map_err(|_| CommandError::UnreachableRevision)?; + let status = Command::new("git") + .args(["fetch", "--quiet", "--no-tags", "--depth=1", LIB_REPOSITORY]) + .arg(revision) + .current_dir(repository.path()) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .map_err(|_| CommandError::UnreachableRevision)?; + if !status.success() { + return Err(CommandError::UnreachableRevision); + } + git_status(repository.path(), ["cat-file", "-e", revision]) + .map_err(|_| CommandError::UnreachableRevision) + } +} + +pub(crate) fn run( + mode: CommandMode, + service_root: &Path, + source_archive: &Path, +) -> Result<(), String> { + run_with(mode, service_root, source_archive, &PublicLibRemote) + .map_err(|error| error.to_string()) +} + +fn run_with( + mode: CommandMode, + service_root: &Path, + source_archive: &Path, + reachability: &dyn RevisionReachability, +) -> Result<(), CommandError> { + let service_root = validate_service_root(service_root)?; + let initial_head = service_head(&service_root)?; + validate_service_cleanliness(&service_root)?; + + let root_manifest = read_bounded_regular( + &service_root.join(CARGO_MANIFEST), + MAX_MANIFEST_BYTES, + CommandError::InvalidCargoManifest, + )?; + let root_manifest = parse_toml(&root_manifest, CommandError::InvalidCargoManifest)?; + let metadata = parse_service_metadata(&root_manifest)?; + let revision = validate_cargo_manifests(&service_root, None)?; + + let flake_lock = read_bounded_regular( + &service_root.join(FLAKE_LOCK), + MAX_FLAKE_LOCK_BYTES, + CommandError::InvalidFlakeLock, + )?; + validate_flake_lock(&flake_lock, &revision)?; + let flake_lock_sha256 = sha256(&flake_lock); + + validate_toolchain(&service_root)?; + let archive = validate_archive(source_archive, &revision)?; + let confirmed_revision = validate_cargo_manifests(&service_root, Some(&archive.package_names))?; + if confirmed_revision != revision { + return Err(CommandError::InvalidCargoManifest); + } + let cargo_lock = read_bounded_regular( + &service_root.join(CARGO_LOCK), + MAX_CARGO_LOCK_BYTES, + CommandError::InvalidCargoLock, + )?; + validate_cargo_lock(&cargo_lock, &revision, &archive.package_names)?; + let cargo_lock_sha256 = sha256(&cargo_lock); + reachability.verify(&archive.revision)?; + validate_service_cleanliness(&service_root)?; + if service_head(&service_root)? != initial_head { + return Err(CommandError::DirtyServiceSource); + } + + let desired = ServiceSourceLockV1::new(ServiceSourceLockParts { + service: &metadata.service, + revision: &archive.revision, + workspace_catalog_sha256: &archive.catalog_sha256, + source_archive_sha256: &archive.archive_sha256, + cargo_lock_sha256: &cargo_lock_sha256, + flake_lock_sha256: &flake_lock_sha256, + contract_versions: metadata.contract_versions, + }) + .map_err(|_| CommandError::InvalidServiceMetadata)?; + + let lock_path = service_root.join(LOCK_FILENAME); + let result = match mode { + CommandMode::Check => { + let current = read_bounded_regular(&lock_path, 4096, CommandError::InvalidSourceLock)?; + let current = ServiceSourceLockV1::from_canonical_bytes(&current) + .map_err(|_| CommandError::InvalidSourceLock)?; + if current == desired { + Ok(()) + } else { + Err(CommandError::StaleSourceLock) + } + } + CommandMode::Write => { + atomic_write_lock(&service_root, &lock_path, desired.canonical_bytes())?; + let current = read_bounded_regular(&lock_path, 4096, CommandError::WriteFailure)?; + let current = ServiceSourceLockV1::from_canonical_bytes(&current) + .map_err(|_| CommandError::WriteFailure)?; + if current == desired { + Ok(()) + } else { + Err(CommandError::WriteFailure) + } + } + }; + result?; + validate_service_cleanliness(&service_root)?; + if service_head(&service_root)? == initial_head { + Ok(()) + } else { + Err(CommandError::DirtyServiceSource) + } +} + +fn service_head(root: &Path) -> Result<String, CommandError> { + let bytes = git_stdout(root, ["rev-parse", "HEAD"], 128) + .map_err(|_| CommandError::InvalidServiceRoot)?; + let revision = std::str::from_utf8(&bytes) + .map_err(|_| CommandError::InvalidServiceRoot)? + .trim(); + if valid_lower_hex(revision, 40) { + Ok(revision.to_owned()) + } else { + Err(CommandError::InvalidServiceRoot) + } +} + +fn validate_service_root(path: &Path) -> Result<PathBuf, CommandError> { + if !path.is_absolute() { + return Err(CommandError::InvalidServiceRoot); + } + let metadata = fs::symlink_metadata(path).map_err(|_| CommandError::InvalidServiceRoot)?; + if metadata.file_type().is_symlink() || !metadata.is_dir() { + return Err(CommandError::InvalidServiceRoot); + } + let canonical = fs::canonicalize(path).map_err(|_| CommandError::InvalidServiceRoot)?; + let top = git_stdout( + &canonical, + ["rev-parse", "--show-toplevel"], + MAX_GIT_OUTPUT_BYTES, + ) + .map_err(|_| CommandError::InvalidServiceRoot)?; + let top = std::str::from_utf8(&top).map_err(|_| CommandError::InvalidServiceRoot)?; + let top = fs::canonicalize(top.trim()).map_err(|_| CommandError::InvalidServiceRoot)?; + if top == canonical { + Ok(canonical) + } else { + Err(CommandError::InvalidServiceRoot) + } +} + +fn validate_service_cleanliness(root: &Path) -> Result<(), CommandError> { + git_status( + root, + [ + "diff", + "--quiet", + "--", + ".", + ":(exclude)radroots.service.source-lock.v1.toml", + ], + ) + .map_err(|_| CommandError::DirtyServiceSource)?; + git_status( + root, + [ + "diff", + "--cached", + "--quiet", + "--", + ".", + ":(exclude)radroots.service.source-lock.v1.toml", + ], + ) + .map_err(|_| CommandError::DirtyServiceSource)?; + let untracked = git_stdout( + root, + ["ls-files", "--others", "--exclude-standard", "-z"], + MAX_GIT_OUTPUT_BYTES, + ) + .map_err(|_| CommandError::DirtyServiceSource)?; + let mut paths = untracked + .split(|byte| *byte == 0) + .filter(|path| !path.is_empty()); + if paths.all(|path| path == LOCK_FILENAME.as_bytes()) { + Ok(()) + } else { + Err(CommandError::DirtyServiceSource) + } +} + +fn parse_service_metadata(root: &toml::Value) -> Result<ServiceMetadata, CommandError> { + let table = root + .get("workspace") + .and_then(|value| value.get("metadata")) + .and_then(|value| value.get("radroots")) + .and_then(|value| value.get("service_source_lock")) + .and_then(toml::Value::as_table) + .ok_or(CommandError::InvalidServiceMetadata)?; + let expected = BTreeSet::from([ + "admin_contract_version", + "config_contract_version", + "host_feature_profile", + "provider_contract_version", + "service", + "state_contract_version", + "status_contract_version", + ]); + if table.keys().map(String::as_str).collect::<BTreeSet<_>>() != expected { + return Err(CommandError::InvalidServiceMetadata); + } + let text = |key: &str| { + table + .get(key) + .and_then(toml::Value::as_str) + .ok_or(CommandError::InvalidServiceMetadata) + }; + let version = |key: &str| { + table + .get(key) + .and_then(toml::Value::as_integer) + .and_then(|value| u32::try_from(value).ok()) + .filter(|value| *value != 0) + .ok_or(CommandError::InvalidServiceMetadata) + }; + if text("host_feature_profile")? != HOST_FEATURE_PROFILE { + return Err(CommandError::InvalidServiceMetadata); + } + Ok(ServiceMetadata { + service: text("service")?.to_owned(), + contract_versions: ContractVersions::new( + version("config_contract_version")?, + version("state_contract_version")?, + version("admin_contract_version")?, + version("status_contract_version")?, + version("provider_contract_version")?, + ), + }) +} + +fn validate_cargo_manifests( + root: &Path, + lib_packages: Option<&BTreeSet<String>>, +) -> Result<String, CommandError> { + let mut manifests = Vec::new(); + let mut entries = 0_usize; + let walker = WalkDir::new(root) + .follow_links(false) + .into_iter() + .filter_entry(|entry| { + !matches!( + entry.file_name().to_str(), + Some(".git" | ".gradle" | ".kotlin" | "build" | "node_modules" | "out" | "target") + ) + }); + for entry in walker { + entries = entries + .checked_add(1) + .ok_or(CommandError::InvalidCargoManifest)?; + if entries > MAX_TREE_ENTRIES { + return Err(CommandError::InvalidCargoManifest); + } + let entry = entry.map_err(|_| CommandError::InvalidCargoManifest)?; + if entry.file_name() == OsStr::new(CARGO_MANIFEST) { + if entry.file_type().is_symlink() || !entry.file_type().is_file() { + return Err(CommandError::InvalidCargoManifest); + } + manifests.push(entry.into_path()); + if manifests.len() > MAX_MANIFESTS { + return Err(CommandError::InvalidCargoManifest); + } + } + } + manifests.sort(); + if manifests.is_empty() { + return Err(CommandError::InvalidCargoManifest); + } + let mut state = ManifestState::default(); + for manifest in manifests { + let bytes = read_bounded_regular( + &manifest, + MAX_MANIFEST_BYTES, + CommandError::InvalidCargoManifest, + )?; + let value = parse_toml(&bytes, CommandError::InvalidCargoManifest)?; + validate_manifest_node(&value, None, false, false, lib_packages, &mut state)?; + } + if state.dependencies == 0 || !state.host_dependency { + return Err(CommandError::InvalidCargoManifest); + } + state.revision.ok_or(CommandError::InvalidCargoManifest) +} + +#[derive(Default)] +struct ManifestState { + revision: Option<String>, + dependencies: usize, + host_dependency: bool, +} + +fn validate_manifest_node( + value: &toml::Value, + key: Option<&str>, + in_patch: bool, + in_dependencies: bool, + lib_packages: Option<&BTreeSet<String>>, + state: &mut ManifestState, +) -> Result<(), CommandError> { + match value { + toml::Value::Table(table) => { + let git = table.get("git").and_then(toml::Value::as_str); + let package = table.get("package").and_then(toml::Value::as_str).or(key); + let known_lib_dependency = (in_dependencies || in_patch) + && package.is_some_and(|package| { + lib_packages.map_or(package == HOST_PACKAGE, |packages| { + packages.contains(package) + }) + }); + let canonical_lib_dependency = + (in_dependencies || in_patch) && git.is_some_and(|git| git == LIB_REPOSITORY); + if known_lib_dependency || canonical_lib_dependency { + let git = git.ok_or(CommandError::InvalidCargoManifest)?; + if in_patch + || git != LIB_REPOSITORY + || table.get("version").and_then(toml::Value::as_str) + != Some(LIB_VERSION_REQUIREMENT) + || table.contains_key("branch") + || table.contains_key("tag") + || table.contains_key("path") + { + return Err(CommandError::InvalidCargoManifest); + } + let revision = table + .get("rev") + .and_then(toml::Value::as_str) + .filter(|value| valid_lower_hex(value, 40)) + .ok_or(CommandError::InvalidCargoManifest)?; + match &state.revision { + Some(expected) if expected != revision => { + return Err(CommandError::InvalidCargoManifest); + } + None => state.revision = Some(revision.to_owned()), + _ => {} + } + state.dependencies += 1; + state.host_dependency |= key == Some(HOST_PACKAGE) + || table.get("package").and_then(toml::Value::as_str) == Some(HOST_PACKAGE); + } + for (child_key, child) in table { + if (known_lib_dependency || canonical_lib_dependency) && child_key == "git" { + continue; + } + let child_dependency_scope = in_dependencies + || matches!( + child_key.as_str(), + "dependencies" | "dev-dependencies" | "build-dependencies" + ); + validate_manifest_node( + child, + Some(child_key), + in_patch || child_key == "patch", + child_dependency_scope, + lib_packages, + state, + )?; + } + } + toml::Value::Array(values) => { + for child in values { + validate_manifest_node(child, key, in_patch, in_dependencies, lib_packages, state)?; + } + } + toml::Value::String(_) + if in_dependencies + && key.is_some_and(|key| { + lib_packages.map_or(key == HOST_PACKAGE, |packages| packages.contains(key)) + }) => + { + return Err(CommandError::InvalidCargoManifest); + } + _ => {} + } + Ok(()) +} + +fn validate_cargo_lock( + bytes: &[u8], + revision: &str, + lib_packages: &BTreeSet<String>, +) -> Result<(), CommandError> { + let text = std::str::from_utf8(bytes).map_err(|_| CommandError::InvalidCargoLock)?; + let document = + toml::from_str::<CargoLockDocument>(text).map_err(|_| CommandError::InvalidCargoLock)?; + let expected = format!("git+{LIB_REPOSITORY}?rev={revision}#{revision}"); + let mut count = 0_usize; + let mut host = false; + for package in document.package { + if lib_packages.contains(&package.name) { + let source = package.source.ok_or(CommandError::InvalidCargoLock)?; + if source != expected || package.version != "0.1.0-alpha" { + return Err(CommandError::InvalidCargoLock); + } + count += 1; + host |= package.name == HOST_PACKAGE; + } + } + if count != 0 && host { + Ok(()) + } else { + Err(CommandError::InvalidCargoLock) + } +} + +fn validate_flake_lock(bytes: &[u8], revision: &str) -> Result<(), CommandError> { + let value = serde_json::from_slice::<serde_json::Value>(bytes) + .map_err(|_| CommandError::InvalidFlakeLock)?; + let version = value.get("version").and_then(serde_json::Value::as_u64); + let root_name = value.get("root").and_then(serde_json::Value::as_str); + let nodes = value.get("nodes").and_then(serde_json::Value::as_object); + if version != Some(7) || root_name.is_none() || nodes.is_none() { + return Err(CommandError::InvalidFlakeLock); + } + let nodes = nodes.ok_or(CommandError::InvalidFlakeLock)?; + let root = nodes + .get(root_name.ok_or(CommandError::InvalidFlakeLock)?) + .and_then(|node| node.get("inputs")) + .and_then(serde_json::Value::as_object) + .ok_or(CommandError::InvalidFlakeLock)?; + let direct = root + .values() + .filter_map(serde_json::Value::as_str) + .collect::<Vec<_>>(); + let mut lib_nodes = 0_usize; + let mut exact_direct = 0_usize; + for (name, node) in nodes { + let locked = node.get("locked").and_then(serde_json::Value::as_object); + let original = node.get("original").and_then(serde_json::Value::as_object); + let is_lib = locked.is_some_and(|locked| { + locked.get("owner").and_then(serde_json::Value::as_str) == Some("radrootslabs") + && locked.get("repo").and_then(serde_json::Value::as_str) == Some("lib") + }) || original.is_some_and(|original| { + original.get("owner").and_then(serde_json::Value::as_str) == Some("radrootslabs") + && original.get("repo").and_then(serde_json::Value::as_str) == Some("lib") + }); + if !is_lib { + continue; + } + lib_nodes += 1; + let locked = locked.ok_or(CommandError::InvalidFlakeLock)?; + let original = original.ok_or(CommandError::InvalidFlakeLock)?; + let locked_keys = locked.keys().map(String::as_str).collect::<BTreeSet<_>>(); + let original_keys = original.keys().map(String::as_str).collect::<BTreeSet<_>>(); + let exact = locked_keys + == BTreeSet::from(["lastModified", "narHash", "owner", "repo", "rev", "type"]) + && original_keys == BTreeSet::from(["owner", "repo", "rev", "type"]) + && locked.get("type").and_then(serde_json::Value::as_str) == Some("github") + && locked.get("owner").and_then(serde_json::Value::as_str) == Some("radrootslabs") + && locked.get("repo").and_then(serde_json::Value::as_str) == Some("lib") + && locked.get("rev").and_then(serde_json::Value::as_str) == Some(revision) + && locked + .get("narHash") + .and_then(serde_json::Value::as_str) + .is_some_and(valid_nix_sha256) + && original.get("type").and_then(serde_json::Value::as_str) == Some("github") + && original.get("owner").and_then(serde_json::Value::as_str) == Some("radrootslabs") + && original.get("repo").and_then(serde_json::Value::as_str) == Some("lib") + && original.get("rev").and_then(serde_json::Value::as_str) == Some(revision) + && original.get("ref").is_none(); + if direct.iter().filter(|direct| **direct == name).count() == 1 && exact { + exact_direct += 1; + } + } + if lib_nodes == 1 && exact_direct == 1 { + Ok(()) + } else { + Err(CommandError::InvalidFlakeLock) + } +} + +fn validate_toolchain(root: &Path) -> Result<(), CommandError> { + let bytes = read_bounded_regular( + &root.join(RUST_TOOLCHAIN), + MAX_TOOLCHAIN_BYTES, + CommandError::InvalidToolchain, + )?; + let value = parse_toml(&bytes, CommandError::InvalidToolchain)?; + if value + .get("toolchain") + .and_then(|value| value.get("channel")) + .and_then(toml::Value::as_str) + == Some(RUST_VERSION) + { + Ok(()) + } else { + Err(CommandError::InvalidToolchain) + } +} + +fn validate_archive(path: &Path, revision: &str) -> Result<ArchiveEvidence, CommandError> { + if !path.is_absolute() { + return Err(CommandError::InvalidSourceArchive); + } + let metadata = fs::symlink_metadata(path).map_err(|_| CommandError::InvalidSourceArchive)?; + if metadata.file_type().is_symlink() + || !metadata.is_file() + || metadata.len() == 0 + || metadata.len() > MAX_ARCHIVE_BYTES + { + return Err(CommandError::InvalidSourceArchive); + } + let mut source = fs::File::open(path).map_err(|_| CommandError::InvalidSourceArchive)?; + let before = source + .metadata() + .map_err(|_| CommandError::InvalidSourceArchive)?; + let mut stable = NamedTempFile::new().map_err(|_| CommandError::InvalidSourceArchive)?; + let mut hasher = Sha256::new(); + let mut total = 0_u64; + let mut buffer = [0_u8; 65_536]; + loop { + let read = source + .read(&mut buffer) + .map_err(|_| CommandError::InvalidSourceArchive)?; + if read == 0 { + break; + } + total = total + .checked_add(u64::try_from(read).map_err(|_| CommandError::InvalidSourceArchive)?) + .filter(|total| *total <= MAX_ARCHIVE_BYTES) + .ok_or(CommandError::InvalidSourceArchive)?; + hasher.update(&buffer[..read]); + stable + .write_all(&buffer[..read]) + .map_err(|_| CommandError::InvalidSourceArchive)?; + } + let after = source + .metadata() + .map_err(|_| CommandError::InvalidSourceArchive)?; + if total == 0 || before.len() != total || after.len() != total { + return Err(CommandError::InvalidSourceArchive); + } + stable + .flush() + .and_then(|()| stable.as_file().sync_all()) + .map_err(|_| CommandError::InvalidSourceArchive)?; + stable + .as_file_mut() + .seek(SeekFrom::Start(0)) + .map_err(|_| CommandError::InvalidSourceArchive)?; + + let verification = TempDir::new().map_err(|_| CommandError::InvalidSourceArchive)?; + git_status(verification.path(), ["init", "--bare", "--quiet"]) + .map_err(|_| CommandError::InvalidSourceArchive)?; + let status = Command::new("git") + .args(["bundle", "verify"]) + .arg(stable.path()) + .current_dir(verification.path()) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .map_err(|_| CommandError::InvalidSourceArchive)?; + if !status.success() { + return Err(CommandError::InvalidSourceArchive); + } + let heads = command_stdout( + Command::new("git") + .args(["bundle", "list-heads"]) + .arg(stable.path()), + MAX_GIT_OUTPUT_BYTES, + ) + .map_err(|_| CommandError::InvalidSourceArchive)?; + let expected_head = format!("{revision} refs/heads/archive\n"); + if heads != expected_head.as_bytes() { + return Err(CommandError::InvalidSourceArchive); + } + let fetch = Command::new("git") + .args(["fetch", "--quiet", "--no-tags"]) + .arg(stable.path()) + .arg(format!("{revision}:refs/heads/archive")) + .current_dir(verification.path()) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .map_err(|_| CommandError::InvalidSourceArchive)?; + if !fetch.success() { + return Err(CommandError::InvalidSourceArchive); + } + let catalog_spec = format!("{revision}:{CATALOG_RELATIVE}"); + let catalog = git_stdout( + verification.path(), + ["show", catalog_spec.as_str()], + MAX_CATALOG_BYTES, + ) + .map_err(|_| CommandError::InvalidSourceArchive)?; + if catalog.is_empty() { + return Err(CommandError::InvalidSourceArchive); + } + let package_names = catalog_package_names(&catalog)?; + Ok(ArchiveEvidence { + revision: revision.to_owned(), + archive_sha256: hex::encode(hasher.finalize()), + catalog_sha256: sha256(&catalog), + package_names, + }) +} + +fn catalog_package_names(bytes: &[u8]) -> Result<BTreeSet<String>, CommandError> { + let value = parse_toml(bytes, CommandError::InvalidSourceArchive)?; + if value.get("schema").and_then(toml::Value::as_str) != Some("radroots.workspace.catalog.v2") + || value.get("architecture").and_then(toml::Value::as_str) + != Some("radroots.crates.release.v2") + || value.get("version").and_then(toml::Value::as_str) != Some("0.1.0-alpha") + { + return Err(CommandError::InvalidSourceArchive); + } + let packages = value + .get("package") + .and_then(toml::Value::as_array) + .ok_or(CommandError::InvalidSourceArchive)?; + let expected_count = value + .get("package_count") + .and_then(toml::Value::as_integer) + .and_then(|count| usize::try_from(count).ok()) + .ok_or(CommandError::InvalidSourceArchive)?; + if packages.is_empty() || packages.len() != expected_count || packages.len() > MAX_MANIFESTS { + return Err(CommandError::InvalidSourceArchive); + } + let mut names = BTreeSet::new(); + for package in packages { + let name = package + .get("name") + .and_then(toml::Value::as_str) + .ok_or(CommandError::InvalidSourceArchive)?; + if !names.insert(name.to_owned()) { + return Err(CommandError::InvalidSourceArchive); + } + } + if names.contains(HOST_PACKAGE) { + Ok(names) + } else { + Err(CommandError::InvalidSourceArchive) + } +} + +fn atomic_write_lock(root: &Path, path: &Path, bytes: &[u8]) -> Result<(), CommandError> { + if let Ok(metadata) = fs::symlink_metadata(path) + && (metadata.file_type().is_symlink() || !metadata.is_file()) + { + return Err(CommandError::WriteFailure); + } + let mut temporary = NamedTempFile::new_in(root).map_err(|_| CommandError::WriteFailure)?; + temporary + .write_all(bytes) + .and_then(|()| temporary.as_file().sync_all()) + .map_err(|_| CommandError::WriteFailure)?; + temporary + .persist(path) + .map_err(|_| CommandError::WriteFailure)?; + fs::File::open(root) + .and_then(|directory| directory.sync_all()) + .map_err(|_| CommandError::WriteFailure) +} + +fn read_bounded_regular( + path: &Path, + maximum: usize, + error: CommandError, +) -> Result<Vec<u8>, CommandError> { + let metadata = fs::symlink_metadata(path).map_err(|_| error)?; + if metadata.file_type().is_symlink() || !metadata.is_file() || metadata.len() > maximum as u64 { + return Err(error); + } + let file = fs::File::open(path).map_err(|_| error)?; + let mut bytes = Vec::with_capacity(usize::try_from(metadata.len()).map_err(|_| error)?); + file.take(maximum as u64 + 1) + .read_to_end(&mut bytes) + .map_err(|_| error)?; + if bytes.len() > maximum { + Err(error) + } else { + Ok(bytes) + } +} + +fn parse_toml(bytes: &[u8], error: CommandError) -> Result<toml::Value, CommandError> { + let text = std::str::from_utf8(bytes).map_err(|_| error)?; + toml::from_str(text).map_err(|_| error) +} + +fn sha256(bytes: &[u8]) -> String { + hex::encode(Sha256::digest(bytes)) +} + +fn valid_lower_hex(value: &str, length: usize) -> bool { + value.len() == length + && value + .bytes() + .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) +} + +fn valid_nix_sha256(value: &str) -> bool { + let Some(encoded) = value.strip_prefix("sha256-") else { + return false; + }; + let bytes = encoded.as_bytes(); + bytes.len() == 44 + && bytes[43] == b'=' + && bytes[..43] + .iter() + .copied() + .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'+' | b'/')) +} + +fn git_status<const N: usize>(root: &Path, args: [&str; N]) -> Result<(), ()> { + let status = Command::new("git") + .args(args) + .current_dir(root) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .map_err(|_| ())?; + if status.success() { Ok(()) } else { Err(()) } +} + +fn git_stdout<const N: usize>(root: &Path, args: [&str; N], maximum: usize) -> Result<Vec<u8>, ()> { + let mut command = Command::new("git"); + command.args(args).current_dir(root); + command_stdout(&mut command, maximum) +} + +fn command_stdout(command: &mut Command, maximum: usize) -> Result<Vec<u8>, ()> { + let mut child = command + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::null()) + .spawn() + .map_err(|_| ())?; + let mut stdout = child.stdout.take().ok_or(())?; + let mut bytes = Vec::new(); + if stdout + .by_ref() + .take(maximum as u64 + 1) + .read_to_end(&mut bytes) + .is_err() + { + let _ = child.kill(); + let _ = child.wait(); + return Err(()); + } + if bytes.len() > maximum { + let _ = child.kill(); + let _ = child.wait(); + return Err(()); + } + let status = child.wait().map_err(|_| ())?; + if status.success() { Ok(bytes) } else { Err(()) } +} + +#[cfg(test)] +mod tests { + use std::{ + error::Error as _, + sync::atomic::{AtomicBool, Ordering}, + }; + + use super::*; + + struct Reachability { + reachable: AtomicBool, + } + + impl RevisionReachability for Reachability { + fn verify(&self, _revision: &str) -> Result<(), CommandError> { + if self.reachable.load(Ordering::SeqCst) { + Ok(()) + } else { + Err(CommandError::UnreachableRevision) + } + } + } + + struct CommitOnVerify { + service: PathBuf, + } + + impl RevisionReachability for CommitOnVerify { + fn verify(&self, _revision: &str) -> Result<(), CommandError> { + fs::write(self.service.join("concurrent"), b"changed").expect("concurrent file"); + git(&self.service, &["add", "."]); + git( + &self.service, + &["commit", "--quiet", "-m", "concurrent commit"], + ); + Ok(()) + } + } + + struct Fixture { + root: TempDir, + service: PathBuf, + archive: PathBuf, + revision: String, + } + + impl Fixture { + fn new() -> Self { + let root = TempDir::new().expect("temp root"); + let lib = root.path().join("lib"); + fs::create_dir(&lib).expect("lib root"); + git(&lib, &["init", "--quiet"]); + git(&lib, &["config", "user.email", "fixture@example.invalid"]); + git(&lib, &["config", "user.name", "fixture"]); + let catalog = lib.join(CATALOG_RELATIVE); + fs::create_dir_all(catalog.parent().expect("catalog parent")).expect("catalog parent"); + fs::write( + &catalog, + br#"schema_version = 2 +schema = "radroots.workspace.catalog.v2" +architecture = "radroots.crates.release.v2" +version = "0.1.0-alpha" +package_count = 2 + +[[package]] +name = "radroots_core" + +[[package]] +name = "radroots_service_host" +"#, + ) + .expect("catalog"); + git(&lib, &["add", "."]); + git(&lib, &["commit", "--quiet", "-m", "fixture"]); + git(&lib, &["branch", "-M", "archive"]); + let revision = + String::from_utf8(git_stdout(&lib, ["rev-parse", "HEAD"], 128).expect("revision")) + .expect("UTF-8") + .trim() + .to_owned(); + let archive = root.path().join("lib.bundle"); + let status = Command::new("git") + .args(["bundle", "create"]) + .arg(&archive) + .arg("refs/heads/archive") + .current_dir(&lib) + .status() + .expect("bundle"); + assert!(status.success()); + + let service = root.path().join("service"); + fs::create_dir(&service).expect("service root"); + fs::write( + service.join(CARGO_MANIFEST), + format!( + r#"[workspace] +resolver = "3" + +[workspace.metadata.radroots.service_source_lock] +service = "fixture_service" +host_feature_profile = "service-host" +config_contract_version = 1 +state_contract_version = 2 +admin_contract_version = 3 +status_contract_version = 4 +provider_contract_version = 5 + +[workspace.dependencies] +radroots_service_host = {{ git = "{LIB_REPOSITORY}", rev = "{revision}", version = "=0.1.0-alpha" }} +"#, + ), + ) + .expect("manifest"); + fs::write( + service.join(CARGO_LOCK), + format!( + "version = 4\n\n[[package]]\nname = \"radroots_service_host\"\nversion = \"0.1.0-alpha\"\nsource = \"git+{LIB_REPOSITORY}?rev={revision}#{revision}\"\n" + ), + ) + .expect("Cargo.lock"); + fs::write( + service.join(FLAKE_LOCK), + format!( + 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}} +"#, + ), + ) + .expect("flake.lock"); + fs::write( + service.join(RUST_TOOLCHAIN), + "[toolchain]\nchannel = \"1.97.1\"\ncomponents = [\"clippy\", \"rustfmt\"]\n", + ) + .expect("toolchain"); + git(&service, &["init", "--quiet"]); + git( + &service, + &["config", "user.email", "fixture@example.invalid"], + ); + git(&service, &["config", "user.name", "fixture"]); + git(&service, &["add", "."]); + git(&service, &["commit", "--quiet", "-m", "fixture"]); + Self { + root, + service, + archive, + revision, + } + } + + fn reachable(&self) -> Reachability { + Reachability { + reachable: AtomicBool::new(true), + } + } + } + + fn git(root: &Path, args: &[&str]) { + let status = Command::new("git") + .args(args) + .current_dir(root) + .status() + .expect("git"); + assert!(status.success(), "git {args:?}"); + } + + #[test] + fn update_and_check_bind_complete_local_evidence() { + let fixture = Fixture::new(); + let reachability = fixture.reachable(); + run_with( + CommandMode::Write, + &fixture.service, + &fixture.archive, + &reachability, + ) + .expect("write lock"); + run_with( + CommandMode::Check, + &fixture.service, + &fixture.archive, + &reachability, + ) + .expect("check lock"); + + let bytes = fs::read(fixture.service.join(LOCK_FILENAME)).expect("lock"); + let text = std::str::from_utf8(&bytes).expect("UTF-8"); + assert!(text.contains("service = \"fixture_service\"")); + assert!(text.contains(&format!("revision = \"{}\"", fixture.revision))); + assert!(text.contains("config = 1\nstate = 2\nadmin = 3\nstatus = 4\nprovider = 5")); + assert!(fixture.root.path().exists()); + } + + #[test] + fn check_rejects_stale_lock_and_update_repairs_it() { + let fixture = Fixture::new(); + let reachability = fixture.reachable(); + run_with( + CommandMode::Write, + &fixture.service, + &fixture.archive, + &reachability, + ) + .expect("write lock"); + let lock = fixture.service.join(LOCK_FILENAME); + let stale = + fs::read_to_string(&lock) + .expect("read lock") + .replacen("config = 1", "config = 9", 1); + fs::write(&lock, stale).expect("stale lock"); + assert_eq!( + run_with( + CommandMode::Check, + &fixture.service, + &fixture.archive, + &reachability, + ), + Err(CommandError::StaleSourceLock) + ); + run_with( + CommandMode::Write, + &fixture.service, + &fixture.archive, + &reachability, + ) + .expect("repair lock"); + } + + #[test] + fn dirty_source_and_unreachable_revision_fail_closed() { + let fixture = Fixture::new(); + fs::write(fixture.service.join("untracked-secret"), b"sensitive").expect("dirty file"); + assert_eq!( + run_with( + CommandMode::Write, + &fixture.service, + &fixture.archive, + &fixture.reachable(), + ), + Err(CommandError::DirtyServiceSource) + ); + fs::remove_file(fixture.service.join("untracked-secret")).expect("remove dirty file"); + let unreachable = Reachability { + reachable: AtomicBool::new(false), + }; + assert_eq!( + run_with( + CommandMode::Write, + &fixture.service, + &fixture.archive, + &unreachable, + ), + Err(CommandError::UnreachableRevision) + ); + assert!(!fixture.service.join(LOCK_FILENAME).exists()); + + let fixture = Fixture::new(); + let commit_on_verify = CommitOnVerify { + service: fixture.service.clone(), + }; + assert_eq!( + run_with( + CommandMode::Write, + &fixture.service, + &fixture.archive, + &commit_on_verify, + ), + Err(CommandError::DirtyServiceSource) + ); + assert!(!fixture.service.join(LOCK_FILENAME).exists()); + } + + #[test] + fn cargo_flake_toolchain_and_archive_drift_are_distinct() { + let cases = [ + ( + CARGO_MANIFEST, + "radroots_service_host", + "other", + CommandError::InvalidCargoManifest, + ), + ( + CARGO_LOCK, + "radroots_service_host", + "other", + CommandError::InvalidCargoLock, + ), + ( + FLAKE_LOCK, + "radrootslabs", + "other", + CommandError::InvalidFlakeLock, + ), + ( + RUST_TOOLCHAIN, + "1.97.1", + "1.97.0", + CommandError::InvalidToolchain, + ), + ]; + for (path, from, to, expected) in cases { + let fixture = Fixture::new(); + let path = fixture.service.join(path); + let mutated = fs::read_to_string(&path) + .expect("read fixture") + .replacen(from, to, 1); + fs::write(&path, mutated).expect("mutate fixture"); + git(&fixture.service, &["add", "."]); + git(&fixture.service, &["commit", "--quiet", "-m", "mutate"]); + assert_eq!( + run_with( + CommandMode::Write, + &fixture.service, + &fixture.archive, + &fixture.reachable(), + ), + Err(expected) + ); + } + + let fixture = Fixture::new(); + let other = fixture.root.path().join("other"); + fs::create_dir(&other).expect("other repo"); + git(&other, &["init", "--quiet"]); + git(&other, &["config", "user.email", "fixture@example.invalid"]); + git(&other, &["config", "user.name", "fixture"]); + fs::write(other.join("other"), b"other").expect("other file"); + git(&other, &["add", "."]); + git(&other, &["commit", "--quiet", "-m", "other"]); + git(&other, &["branch", "-M", "archive"]); + let bad_archive = fixture.root.path().join("other.bundle"); + let status = Command::new("git") + .args(["bundle", "create"]) + .arg(&bad_archive) + .arg("refs/heads/archive") + .current_dir(&other) + .status() + .expect("other bundle"); + assert!(status.success()); + assert_eq!( + run_with( + CommandMode::Write, + &fixture.service, + &bad_archive, + &fixture.reachable(), + ), + Err(CommandError::InvalidSourceArchive) + ); + + let fixture = Fixture::new(); + let oversized = fixture.root.path().join("oversized.bundle"); + fs::File::create(&oversized) + .and_then(|file| file.set_len(MAX_ARCHIVE_BYTES + 1)) + .expect("sparse oversized archive"); + assert_eq!( + run_with( + CommandMode::Write, + &fixture.service, + &oversized, + &fixture.reachable(), + ), + Err(CommandError::InvalidSourceArchive) + ); + } + + #[test] + fn mixed_local_or_unlocked_lib_dependencies_fail_closed() { + let fixture = Fixture::new(); + let manifest = fixture.service.join(CARGO_MANIFEST); + let mut text = fs::read_to_string(&manifest).expect("manifest"); + text.push_str("radroots_core = { path = \"../lib/crates/core\" }\n"); + fs::write(&manifest, text).expect("local Lib dependency"); + git(&fixture.service, &["add", "."]); + git( + &fixture.service, + &["commit", "--quiet", "-m", "local dependency"], + ); + assert_eq!( + run_with( + CommandMode::Write, + &fixture.service, + &fixture.archive, + &fixture.reachable(), + ), + Err(CommandError::InvalidCargoManifest) + ); + + let fixture = Fixture::new(); + let cargo_lock = fixture.service.join(CARGO_LOCK); + let mut text = fs::read_to_string(&cargo_lock).expect("Cargo.lock"); + text.push_str("\n[[package]]\nname = \"radroots_core\"\nversion = \"0.1.0-alpha\"\n"); + fs::write(&cargo_lock, text).expect("unlocked Lib package"); + git(&fixture.service, &["add", "."]); + git( + &fixture.service, + &["commit", "--quiet", "-m", "unlocked dependency"], + ); + assert_eq!( + run_with( + CommandMode::Write, + &fixture.service, + &fixture.archive, + &fixture.reachable(), + ), + Err(CommandError::InvalidCargoLock) + ); + + let fixture = Fixture::new(); + let manifest = fixture.service.join(CARGO_MANIFEST); + let mut text = fs::read_to_string(&manifest).expect("manifest"); + text.push_str(&format!( + "\n[patch.crates-io]\nradroots_core = {{ git = \"{LIB_REPOSITORY}\", rev = \"{}\", version = \"=0.1.0-alpha\" }}\n", + fixture.revision + )); + fs::write(&manifest, text).expect("Lib patch"); + git(&fixture.service, &["add", "."]); + git(&fixture.service, &["commit", "--quiet", "-m", "Lib patch"]); + assert_eq!( + run_with( + CommandMode::Write, + &fixture.service, + &fixture.archive, + &fixture.reachable(), + ), + Err(CommandError::InvalidCargoManifest) + ); + } + + #[test] + fn floating_or_unbounded_nix_input_fails_closed() { + for mutation in [ + ("\"rev\":\"", "\"ref\":\"master\",\"rev\":\""), + ( + "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "sha256-invalid", + ), + ("\"lastModified\":1,", "\"dir\":\"sub\",\"lastModified\":1,"), + ] { + let fixture = Fixture::new(); + let path = fixture.service.join(FLAKE_LOCK); + let current = fs::read_to_string(&path).expect("flake lock"); + let mutated = if mutation.0 == "\"rev\":\"" { + let second = current.rfind(mutation.0).expect("original revision field"); + format!( + "{}{}{}", + &current[..second], + mutation.1, + &current[second + mutation.0.len()..] + ) + } else { + current.replacen(mutation.0, mutation.1, 1) + }; + fs::write(&path, mutated).expect("mutate flake lock"); + git(&fixture.service, &["add", "."]); + git(&fixture.service, &["commit", "--quiet", "-m", "mutate"]); + assert_eq!( + run_with( + CommandMode::Write, + &fixture.service, + &fixture.archive, + &fixture.reachable(), + ), + Err(CommandError::InvalidFlakeLock) + ); + } + } + + #[test] + fn source_lock_output_rejects_symlink_replacement() { + let fixture = Fixture::new(); + let target = fixture.root.path().join("foreign"); + fs::write(&target, b"foreign").expect("foreign"); + #[cfg(unix)] + std::os::unix::fs::symlink(&target, fixture.service.join(LOCK_FILENAME)).expect("symlink"); + #[cfg(unix)] + assert_eq!( + run_with( + CommandMode::Write, + &fixture.service, + &fixture.archive, + &fixture.reachable(), + ), + Err(CommandError::WriteFailure) + ); + #[cfg(unix)] + assert_eq!(fs::read(&target).expect("foreign remains"), b"foreign"); + } + + #[test] + fn operational_diagnostics_are_fixed_and_source_free() { + let errors = [ + CommandError::InvalidServiceRoot, + CommandError::DirtyServiceSource, + CommandError::InvalidServiceMetadata, + CommandError::InvalidCargoManifest, + CommandError::InvalidCargoLock, + CommandError::InvalidFlakeLock, + CommandError::InvalidToolchain, + CommandError::InvalidSourceArchive, + CommandError::UnreachableRevision, + CommandError::InvalidSourceLock, + CommandError::StaleSourceLock, + CommandError::WriteFailure, + ]; + for error in errors { + assert!(error.source().is_none()); + let rendered = format!("{error:?} {error}"); + assert!(!rendered.contains("fixture_service")); + assert!(!rendered.contains("radrootslabs")); + assert!(!rendered.contains("/tmp")); + } + } +}