lib

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

commit 05b27fbea7f618749dd3ca37516e71b2db8f1091
parent 26c93442450372625e67ee59f73122ebeba79732
Author: triesap <tyson@radroots.org>
Date:   Thu, 20 Aug 2026 22:03:11 +0000

build: define service source lock

- freeze the canonical versioned service source-lock model
- validate bounded identities, digests, and contract versions
- bind the machine decision through contract validation
- keep operational update and reachability checks deferred

Diffstat:
MAGENTS.md | 9+++++++++
Acontracts/architecture/decisions/services_hardening_source_lock.v1.json | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtools/xtask/src/main.rs | 3+++
Atools/xtask/src/service_source_lock.rs | 748+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 831 insertions(+), 0 deletions(-)

diff --git a/AGENTS.md b/AGENTS.md @@ -36,6 +36,15 @@ This file exists for compatibility with tools that look for AGENTS.md. - Source-lock consumer identities include `sdk`, `mobile`, `myc`, and `rhi`. Only the first two are generated-artifact product identities; accepting a service consumer marker must not expose an artifact route. +- The canonical service source lock is the bounded, canonical + `radroots.service.source-lock.v1.toml` model. It binds the exact public Lib + repository and full revision, Lib source-archive and workspace-catalog + digests, the service `Cargo.lock` and `flake.lock` digests, Rust `1.97.1`, + the `service-host` feature profile, and positive config, state, admin, + status, and provider contract versions. Keep its model and diagnostics + private to repo tooling, reject noncanonical or extra fields, and never put + credentials, local paths, floating refs, or private repository identity in + it. - 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 @@ -0,0 +1,71 @@ +{ + "schema": "radroots.services-hardening.source-lock-decisions.v1", + "contract_version": 1, + "decision_state": "active", + "lock_filename": "radroots.service.source-lock.v1.toml", + "lock_schema": "radroots.service.source-lock.v1", + "canonical_encoding": "compact_canonical_toml_with_final_newline", + "maximum_lock_utf8_bytes": 4096, + "maximum_service_utf8_bytes": 128, + "canonical_field_order": [ + "schema", + "contract_version", + "service", + "repository", + "revision", + "architecture", + "workspace_catalog_sha256", + "version", + "source_archive_sha256", + "cargo_lock_sha256", + "flake_lock_sha256", + "rust_version", + "host_feature_profile", + "contract_versions.config", + "contract_versions.state", + "contract_versions.admin", + "contract_versions.status", + "contract_versions.provider" + ], + "fixed": { + "repository": "https://github.com/radrootslabs/lib", + "architecture": "radroots.crates.release.v2", + "version": "0.1.0-alpha", + "rust_version": "1.97.1", + "host_feature_profile": "service-host" + }, + "revision_encoding": "git_oid_lowercase_hex_40", + "digest_encoding": "sha256_lowercase_hex_64", + "digest_subjects": { + "workspace_catalog_sha256": "lib/contracts/crates/catalog.v2.toml", + "source_archive_sha256": "canonical_lib_revision_source_archive", + "cargo_lock_sha256": "service/Cargo.lock", + "flake_lock_sha256": "service/flake.lock" + }, + "service_identifier": "ascii_lower_snake_case_starting_with_letter_ending_with_letter_or_digit_no_empty_segments_1_to_128_bytes", + "contract_version_rule": "u32_nonzero", + "negative_error_codes": [ + "invalid_contract_version", + "invalid_digest", + "invalid_feature_profile", + "invalid_fixed_identity", + "invalid_revision", + "invalid_service", + "invalid_toolchain", + "malformed", + "noncanonical", + "too_large" + ], + "canonical_vector": { + "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" + }, + "deferred_operations": [ + "filesystem_digest_verification", + "git_cleanliness", + "manifest_and_cargo_metadata_agreement", + "remote_revision_reachability", + "source_archive_verification", + "update_and_verify_commands" + ] +} diff --git a/tools/xtask/src/main.rs b/tools/xtask/src/main.rs @@ -38,6 +38,7 @@ mod release_qualification; mod safety_qualification; #[cfg_attr(coverage_nightly, coverage(off))] mod sdk_generation; +mod service_source_lock; #[cfg_attr(coverage_nightly, coverage(off))] mod supply_chain_qualification; #[cfg_attr(coverage_nightly, coverage(off))] @@ -347,6 +348,7 @@ fn validate_protocol_contracts() -> Result<(), String> { fn validate_contract() -> Result<(), String> { validate_protocol_contracts()?; let root = workspace_root(); + service_source_lock::validate_contract(&root)?; dto_roots::check(&root)?; generate::protocol::check(&root)?; contract::load_contract_bundle(&root) @@ -363,6 +365,7 @@ fn release_preflight() -> Result<(), String> { fn release_preflight_at(root: &Path) -> Result<(), String> { catalog::check(root)?; + service_source_lock::validate_contract(root)?; for group in ["public_native", "preview", "tools"] { build_control::group_plan(root, group, build_control::Operation::Check, false)?; } diff --git a/tools/xtask/src/service_source_lock.rs b/tools/xtask/src/service_source_lock.rs @@ -0,0 +1,748 @@ +use std::{fmt, fmt::Write as _, fs, io::Read as _, path::Path}; + +use serde::Deserialize; +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"; +const ARCHITECTURE: &str = "radroots.crates.release.v2"; +const LIB_VERSION: &str = "0.1.0-alpha"; +const RUST_VERSION: &str = "1.97.1"; +const HOST_FEATURE_PROFILE: &str = "service-host"; +const MAX_LOCK_BYTES: usize = 4096; +const MAX_SERVICE_BYTES: usize = 128; +const MAX_CONTRACT_BYTES: usize = 32_768; + +const FIELD_ORDER: [&str; 18] = [ + "schema", + "contract_version", + "service", + "repository", + "revision", + "architecture", + "workspace_catalog_sha256", + "version", + "source_archive_sha256", + "cargo_lock_sha256", + "flake_lock_sha256", + "rust_version", + "host_feature_profile", + "contract_versions.config", + "contract_versions.state", + "contract_versions.admin", + "contract_versions.status", + "contract_versions.provider", +]; + +const ERROR_CODES: [&str; 10] = [ + "invalid_contract_version", + "invalid_digest", + "invalid_feature_profile", + "invalid_fixed_identity", + "invalid_revision", + "invalid_service", + "invalid_toolchain", + "malformed", + "noncanonical", + "too_large", +]; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum ServiceSourceLockError { + TooLarge, + Malformed, + Noncanonical, + InvalidFixedIdentity, + InvalidService, + InvalidRevision, + InvalidDigest, + InvalidToolchain, + InvalidFeatureProfile, + InvalidContractVersion, +} + +impl ServiceSourceLockError { + fn code(self) -> &'static str { + match self { + Self::TooLarge => "too_large", + Self::Malformed => "malformed", + Self::Noncanonical => "noncanonical", + Self::InvalidFixedIdentity => "invalid_fixed_identity", + Self::InvalidService => "invalid_service", + Self::InvalidRevision => "invalid_revision", + Self::InvalidDigest => "invalid_digest", + Self::InvalidToolchain => "invalid_toolchain", + Self::InvalidFeatureProfile => "invalid_feature_profile", + Self::InvalidContractVersion => "invalid_contract_version", + } + } +} + +impl fmt::Display for ServiceSourceLockError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(match self { + Self::TooLarge => "service source lock exceeds its byte limit", + Self::Malformed => "service source lock is malformed", + Self::Noncanonical => "service source lock is not canonical", + Self::InvalidFixedIdentity => "service source lock identity is invalid", + Self::InvalidService => "service source lock service is invalid", + Self::InvalidRevision => "service source lock revision is invalid", + Self::InvalidDigest => "service source lock digest is invalid", + Self::InvalidToolchain => "service source lock toolchain is invalid", + Self::InvalidFeatureProfile => "service source lock feature profile is invalid", + Self::InvalidContractVersion => "service source lock contract version is invalid", + }) + } +} + +impl std::error::Error for ServiceSourceLockError {} + +#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq)] +#[serde(deny_unknown_fields)] +struct ContractVersions { + config: u32, + state: u32, + admin: u32, + status: u32, + provider: u32, +} + +impl ContractVersions { + const fn new(config: u32, state: u32, admin: u32, status: u32, provider: u32) -> Self { + Self { + config, + state, + admin, + status, + provider, + } + } + + fn is_valid(self) -> bool { + self.config != 0 + && self.state != 0 + && self.admin != 0 + && self.status != 0 + && self.provider != 0 + } +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq)] +#[serde(deny_unknown_fields)] +struct RawServiceSourceLock { + schema: String, + contract_version: u32, + service: String, + repository: String, + revision: String, + architecture: String, + workspace_catalog_sha256: String, + version: String, + source_archive_sha256: String, + cargo_lock_sha256: String, + flake_lock_sha256: String, + rust_version: String, + host_feature_profile: String, + 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, +} + +#[derive(Clone, Eq, PartialEq)] +struct ServiceSourceLockV1 { + raw: RawServiceSourceLock, + canonical: Box<[u8]>, +} + +impl fmt::Debug for ServiceSourceLockV1 { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("ServiceSourceLockV1") + .finish_non_exhaustive() + } +} + +impl ServiceSourceLockV1 { + fn new(parts: ServiceSourceLockParts<'_>) -> Result<Self, ServiceSourceLockError> { + validate_parts(&parts)?; + let raw = RawServiceSourceLock { + schema: LOCK_SCHEMA.to_owned(), + contract_version: 1, + service: parts.service.to_owned(), + repository: LIB_REPOSITORY.to_owned(), + revision: parts.revision.to_owned(), + architecture: ARCHITECTURE.to_owned(), + workspace_catalog_sha256: parts.workspace_catalog_sha256.to_owned(), + version: LIB_VERSION.to_owned(), + source_archive_sha256: parts.source_archive_sha256.to_owned(), + cargo_lock_sha256: parts.cargo_lock_sha256.to_owned(), + flake_lock_sha256: parts.flake_lock_sha256.to_owned(), + rust_version: RUST_VERSION.to_owned(), + host_feature_profile: HOST_FEATURE_PROFILE.to_owned(), + contract_versions: parts.contract_versions, + }; + Ok(Self::from_validated_raw(raw)) + } + + fn from_canonical_bytes(bytes: &[u8]) -> Result<Self, ServiceSourceLockError> { + if bytes.len() > MAX_LOCK_BYTES { + return Err(ServiceSourceLockError::TooLarge); + } + let text = std::str::from_utf8(bytes).map_err(|_| ServiceSourceLockError::Malformed)?; + let raw = toml::from_str::<RawServiceSourceLock>(text) + .map_err(|_| ServiceSourceLockError::Malformed)?; + validate_raw(&raw)?; + let lock = Self::from_validated_raw(raw); + if lock.canonical.as_ref() != bytes { + return Err(ServiceSourceLockError::Noncanonical); + } + Ok(lock) + } + + fn from_validated_raw(raw: RawServiceSourceLock) -> Self { + let canonical = render(&raw).into_bytes().into_boxed_slice(); + debug_assert!(canonical.len() <= MAX_LOCK_BYTES); + Self { raw, canonical } + } + + fn canonical_bytes(&self) -> &[u8] { + debug_assert_eq!(self.raw.schema, LOCK_SCHEMA); + &self.canonical + } +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct SourceLockDecision { + schema: String, + contract_version: u32, + decision_state: String, + lock_filename: String, + lock_schema: String, + canonical_encoding: String, + maximum_lock_utf8_bytes: usize, + maximum_service_utf8_bytes: usize, + canonical_field_order: Vec<String>, + fixed: FixedDecision, + revision_encoding: String, + digest_encoding: String, + digest_subjects: DigestSubjects, + service_identifier: String, + contract_version_rule: String, + negative_error_codes: Vec<String>, + canonical_vector: CanonicalVector, + deferred_operations: Vec<String>, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct DigestSubjects { + workspace_catalog_sha256: String, + source_archive_sha256: String, + cargo_lock_sha256: String, + flake_lock_sha256: String, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct FixedDecision { + repository: String, + architecture: String, + version: String, + rust_version: String, + host_feature_profile: String, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct CanonicalVector { + toml: String, + sha256: String, +} + +pub(crate) fn validate_contract(workspace_root: &Path) -> Result<(), String> { + validate_contract_inner(workspace_root).map_err(|error| error.to_string()) +} + +fn validate_contract_inner(workspace_root: &Path) -> Result<(), ServiceSourceLockError> { + let path = workspace_root.join(CONTRACT_RELATIVE); + let metadata = fs::symlink_metadata(&path).map_err(|_| ServiceSourceLockError::Malformed)?; + if !metadata.is_file() || metadata.len() > MAX_CONTRACT_BYTES as u64 { + return Err(ServiceSourceLockError::Malformed); + } + let file = fs::File::open(path).map_err(|_| ServiceSourceLockError::Malformed)?; + let mut bytes = Vec::with_capacity(metadata.len() as usize); + file.take(MAX_CONTRACT_BYTES as u64 + 1) + .read_to_end(&mut bytes) + .map_err(|_| ServiceSourceLockError::Malformed)?; + if bytes.len() > MAX_CONTRACT_BYTES { + return Err(ServiceSourceLockError::Malformed); + } + let decision = serde_json::from_slice::<SourceLockDecision>(&bytes) + .map_err(|_| ServiceSourceLockError::Malformed)?; + validate_decision(&decision)?; + + let parsed = + ServiceSourceLockV1::from_canonical_bytes(decision.canonical_vector.toml.as_bytes())?; + let expected = canonical_vector(); + if parsed != expected + || hex::encode(Sha256::digest(parsed.canonical_bytes())) != decision.canonical_vector.sha256 + { + return Err(ServiceSourceLockError::Noncanonical); + } + Ok(()) +} + +fn validate_decision(decision: &SourceLockDecision) -> Result<(), ServiceSourceLockError> { + let error_codes = [ + ServiceSourceLockError::InvalidContractVersion, + ServiceSourceLockError::InvalidDigest, + ServiceSourceLockError::InvalidFeatureProfile, + ServiceSourceLockError::InvalidFixedIdentity, + ServiceSourceLockError::InvalidRevision, + ServiceSourceLockError::InvalidService, + ServiceSourceLockError::InvalidToolchain, + ServiceSourceLockError::Malformed, + ServiceSourceLockError::Noncanonical, + ServiceSourceLockError::TooLarge, + ] + .map(ServiceSourceLockError::code); + let exact = decision.schema == "radroots.services-hardening.source-lock-decisions.v1" + && decision.contract_version == 1 + && decision.decision_state == "active" + && decision.lock_filename == LOCK_FILENAME + && decision.lock_schema == LOCK_SCHEMA + && decision.canonical_encoding == "compact_canonical_toml_with_final_newline" + && decision.maximum_lock_utf8_bytes == MAX_LOCK_BYTES + && decision.maximum_service_utf8_bytes == MAX_SERVICE_BYTES + && decision.canonical_field_order == FIELD_ORDER + && decision.fixed.repository == LIB_REPOSITORY + && decision.fixed.architecture == ARCHITECTURE + && decision.fixed.version == LIB_VERSION + && decision.fixed.rust_version == RUST_VERSION + && decision.fixed.host_feature_profile == HOST_FEATURE_PROFILE + && decision.revision_encoding == "git_oid_lowercase_hex_40" + && decision.digest_encoding == "sha256_lowercase_hex_64" + && decision.digest_subjects.workspace_catalog_sha256 + == "lib/contracts/crates/catalog.v2.toml" + && decision.digest_subjects.source_archive_sha256 + == "canonical_lib_revision_source_archive" + && decision.digest_subjects.cargo_lock_sha256 == "service/Cargo.lock" + && decision.digest_subjects.flake_lock_sha256 == "service/flake.lock" + && decision.service_identifier + == "ascii_lower_snake_case_starting_with_letter_ending_with_letter_or_digit_no_empty_segments_1_to_128_bytes" + && decision.contract_version_rule == "u32_nonzero" + && error_codes == ERROR_CODES + && decision.negative_error_codes == error_codes + && decision.deferred_operations + == [ + "filesystem_digest_verification", + "git_cleanliness", + "manifest_and_cargo_metadata_agreement", + "remote_revision_reachability", + "source_archive_verification", + "update_and_verify_commands", + ]; + if exact { + Ok(()) + } else { + Err(ServiceSourceLockError::InvalidFixedIdentity) + } +} + +fn canonical_vector() -> ServiceSourceLockV1 { + ServiceSourceLockV1::new(ServiceSourceLockParts { + service: "fixture_service", + revision: "1111111111111111111111111111111111111111", + workspace_catalog_sha256: "2222222222222222222222222222222222222222222222222222222222222222", + source_archive_sha256: "3333333333333333333333333333333333333333333333333333333333333333", + cargo_lock_sha256: "4444444444444444444444444444444444444444444444444444444444444444", + flake_lock_sha256: "5555555555555555555555555555555555555555555555555555555555555555", + contract_versions: ContractVersions::new(1, 2, 3, 4, 5), + }) + .expect("the governed source-lock vector is valid") +} + +fn validate_raw(raw: &RawServiceSourceLock) -> Result<(), ServiceSourceLockError> { + if raw.schema != LOCK_SCHEMA + || raw.contract_version != 1 + || raw.repository != LIB_REPOSITORY + || raw.architecture != ARCHITECTURE + || raw.version != LIB_VERSION + { + return Err(ServiceSourceLockError::InvalidFixedIdentity); + } + if raw.rust_version != RUST_VERSION { + return Err(ServiceSourceLockError::InvalidToolchain); + } + if raw.host_feature_profile != HOST_FEATURE_PROFILE { + return Err(ServiceSourceLockError::InvalidFeatureProfile); + } + validate_parts(&ServiceSourceLockParts { + service: &raw.service, + revision: &raw.revision, + workspace_catalog_sha256: &raw.workspace_catalog_sha256, + source_archive_sha256: &raw.source_archive_sha256, + cargo_lock_sha256: &raw.cargo_lock_sha256, + flake_lock_sha256: &raw.flake_lock_sha256, + contract_versions: raw.contract_versions, + }) +} + +fn validate_parts(parts: &ServiceSourceLockParts<'_>) -> Result<(), ServiceSourceLockError> { + if !valid_service(parts.service) { + return Err(ServiceSourceLockError::InvalidService); + } + if !valid_lower_hex(parts.revision, 40) { + return Err(ServiceSourceLockError::InvalidRevision); + } + if ![ + parts.workspace_catalog_sha256, + parts.source_archive_sha256, + parts.cargo_lock_sha256, + parts.flake_lock_sha256, + ] + .into_iter() + .all(|value| valid_lower_hex(value, 64)) + { + return Err(ServiceSourceLockError::InvalidDigest); + } + if !parts.contract_versions.is_valid() { + return Err(ServiceSourceLockError::InvalidContractVersion); + } + Ok(()) +} + +fn valid_service(value: &str) -> bool { + let bytes = value.as_bytes(); + !bytes.is_empty() + && bytes.len() <= MAX_SERVICE_BYTES + && bytes[0].is_ascii_lowercase() + && (bytes[bytes.len() - 1].is_ascii_lowercase() || bytes[bytes.len() - 1].is_ascii_digit()) + && bytes + .iter() + .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || *byte == b'_') + && !bytes.windows(2).any(|pair| pair == b"__") +} + +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 render(raw: &RawServiceSourceLock) -> String { + let mut output = String::with_capacity(1024); + writeln!(output, "schema = \"{}\"", raw.schema).expect("render to String"); + writeln!(output, "contract_version = {}", raw.contract_version).expect("render to String"); + writeln!(output, "service = \"{}\"", raw.service).expect("render to String"); + writeln!(output, "repository = \"{}\"", raw.repository).expect("render to String"); + writeln!(output, "revision = \"{}\"", raw.revision).expect("render to String"); + writeln!(output, "architecture = \"{}\"", raw.architecture).expect("render to String"); + writeln!( + output, + "workspace_catalog_sha256 = \"{}\"", + raw.workspace_catalog_sha256 + ) + .expect("render to String"); + writeln!(output, "version = \"{}\"", raw.version).expect("render to String"); + writeln!( + output, + "source_archive_sha256 = \"{}\"", + raw.source_archive_sha256 + ) + .expect("render to String"); + writeln!(output, "cargo_lock_sha256 = \"{}\"", raw.cargo_lock_sha256) + .expect("render to String"); + writeln!(output, "flake_lock_sha256 = \"{}\"", raw.flake_lock_sha256) + .expect("render to String"); + writeln!(output, "rust_version = \"{}\"", raw.rust_version).expect("render to String"); + writeln!( + output, + "host_feature_profile = \"{}\"", + raw.host_feature_profile + ) + .expect("render to String"); + output.push_str("\n[contract_versions]\n"); + writeln!(output, "config = {}", raw.contract_versions.config).expect("render to String"); + writeln!(output, "state = {}", raw.contract_versions.state).expect("render to String"); + writeln!(output, "admin = {}", raw.contract_versions.admin).expect("render to String"); + writeln!(output, "status = {}", raw.contract_versions.status).expect("render to String"); + writeln!(output, "provider = {}", raw.contract_versions.provider).expect("render to String"); + output +} + +#[cfg(test)] +mod tests { + use std::error::Error as _; + + use super::*; + + #[test] + fn canonical_vector_round_trips_with_exact_digest() { + let lock = canonical_vector(); + assert_eq!( + hex::encode(Sha256::digest(lock.canonical_bytes())), + "2257efc8fb3ff4ee8e429e326effdfe622c5e898b429ee1a8ea3aac38f9810cc" + ); + assert_eq!( + ServiceSourceLockV1::from_canonical_bytes(lock.canonical_bytes()), + Ok(lock) + ); + } + + #[test] + fn parser_rejects_noncanonical_and_ambiguous_toml() { + let canonical = String::from_utf8(canonical_vector().canonical_bytes().to_vec()) + .expect("canonical UTF-8"); + for malformed in [ + canonical.replacen("schema =", "unknown = 1\nschema =", 1), + canonical.replacen( + "contract_version = 1\nservice = \"fixture_service\"", + "service = \"fixture_service\"\ncontract_version = 1", + 1, + ), + format!(" {canonical}"), + canonical.replacen("schema =", "schema=", 1), + canonical.replacen("contract_version = 1", "contract_version = 01", 1), + ] { + assert!(matches!( + ServiceSourceLockV1::from_canonical_bytes(malformed.as_bytes()), + Err(ServiceSourceLockError::Malformed | ServiceSourceLockError::Noncanonical) + )); + } + let duplicate = canonical.replacen( + "service = \"fixture_service\"", + "service = \"fixture_service\"\nservice = \"fixture_service\"", + 1, + ); + assert_eq!( + ServiceSourceLockV1::from_canonical_bytes(duplicate.as_bytes()), + Err(ServiceSourceLockError::Malformed) + ); + } + + #[test] + fn parser_rejects_every_identity_and_bound_drift() { + let canonical = String::from_utf8(canonical_vector().canonical_bytes().to_vec()) + .expect("canonical UTF-8"); + let cases = [ + ( + "radroots.service.source-lock.v1", + "wrong", + ServiceSourceLockError::InvalidFixedIdentity, + ), + ( + "contract_version = 1", + "contract_version = 2", + ServiceSourceLockError::InvalidFixedIdentity, + ), + ( + LIB_REPOSITORY, + "https://example.invalid/lib", + ServiceSourceLockError::InvalidFixedIdentity, + ), + ( + ARCHITECTURE, + "radroots.crates.release.v1", + ServiceSourceLockError::InvalidFixedIdentity, + ), + ( + LIB_VERSION, + "0.1.1", + ServiceSourceLockError::InvalidFixedIdentity, + ), + ( + "fixture_service", + "Fixture", + ServiceSourceLockError::InvalidService, + ), + ( + "service = \"fixture_service\"", + "service = \"\"", + ServiceSourceLockError::InvalidService, + ), + ( + "1111111111111111111111111111111111111111", + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + ServiceSourceLockError::InvalidRevision, + ), + ( + "1111111111111111111111111111111111111111", + "111111111111111111111111111111111111111", + ServiceSourceLockError::InvalidRevision, + ), + ( + "2222222222222222222222222222222222222222222222222222222222222222", + "GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG", + ServiceSourceLockError::InvalidDigest, + ), + ( + "3333333333333333333333333333333333333333333333333333333333333333", + "333333333333333333333333333333333333333333333333333333333333333", + ServiceSourceLockError::InvalidDigest, + ), + ( + "4444444444444444444444444444444444444444444444444444444444444444", + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", + ServiceSourceLockError::InvalidDigest, + ), + ( + "5555555555555555555555555555555555555555555555555555555555555555", + "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", + ServiceSourceLockError::InvalidDigest, + ), + ( + RUST_VERSION, + "1.97.0", + ServiceSourceLockError::InvalidToolchain, + ), + ( + HOST_FEATURE_PROFILE, + "default", + ServiceSourceLockError::InvalidFeatureProfile, + ), + ( + "config = 1", + "config = 0", + ServiceSourceLockError::InvalidContractVersion, + ), + ( + "state = 2", + "state = 0", + ServiceSourceLockError::InvalidContractVersion, + ), + ( + "admin = 3", + "admin = 0", + ServiceSourceLockError::InvalidContractVersion, + ), + ( + "status = 4", + "status = 0", + ServiceSourceLockError::InvalidContractVersion, + ), + ( + "provider = 5", + "provider = 0", + ServiceSourceLockError::InvalidContractVersion, + ), + ]; + for (from, to, expected) in cases { + let mutated = canonical.replacen(from, to, 1); + assert_eq!( + ServiceSourceLockV1::from_canonical_bytes(mutated.as_bytes()), + Err(expected) + ); + } + + let too_large = vec![b' '; MAX_LOCK_BYTES + 1]; + assert_eq!( + ServiceSourceLockV1::from_canonical_bytes(&too_large), + Err(ServiceSourceLockError::TooLarge) + ); + } + + #[test] + fn exact_service_and_contract_version_maxima_are_admitted() { + let service = "a".repeat(MAX_SERVICE_BYTES); + let maximum = ServiceSourceLockV1::new(ServiceSourceLockParts { + service: &service, + revision: &"a".repeat(40), + workspace_catalog_sha256: &"b".repeat(64), + source_archive_sha256: &"c".repeat(64), + cargo_lock_sha256: &"d".repeat(64), + flake_lock_sha256: &"e".repeat(64), + contract_versions: ContractVersions::new( + u32::MAX, + u32::MAX, + u32::MAX, + u32::MAX, + u32::MAX, + ), + }) + .expect("maximum lock"); + assert!(maximum.canonical_bytes().len() <= MAX_LOCK_BYTES); + + let overlong_service = "a".repeat(MAX_SERVICE_BYTES + 1); + assert_eq!( + ServiceSourceLockV1::new(ServiceSourceLockParts { + service: &overlong_service, + revision: &"a".repeat(40), + workspace_catalog_sha256: &"b".repeat(64), + source_archive_sha256: &"c".repeat(64), + cargo_lock_sha256: &"d".repeat(64), + flake_lock_sha256: &"e".repeat(64), + contract_versions: ContractVersions::new(1, 1, 1, 1, 1), + }), + Err(ServiceSourceLockError::InvalidService) + ); + + for invalid in [ + "1service", + "_service", + "service_", + "service__name", + "service-name", + ] { + assert_eq!( + ServiceSourceLockV1::new(ServiceSourceLockParts { + service: invalid, + revision: &"a".repeat(40), + workspace_catalog_sha256: &"b".repeat(64), + source_archive_sha256: &"c".repeat(64), + cargo_lock_sha256: &"d".repeat(64), + flake_lock_sha256: &"e".repeat(64), + contract_versions: ContractVersions::new(1, 1, 1, 1, 1), + }), + Err(ServiceSourceLockError::InvalidService) + ); + } + } + + #[test] + fn diagnostics_are_fixed_and_source_free() { + let lock = canonical_vector(); + let debug = format!("{lock:?}"); + assert_eq!(debug, "ServiceSourceLockV1 { .. }"); + for sensitive in [ + "fixture_service", + "radrootslabs", + "1111111111111111111111111111111111111111", + "2222222222222222222222222222222222222222222222222222222222222222", + ] { + assert!(!debug.contains(sensitive)); + } + let errors = [ + ServiceSourceLockError::TooLarge, + ServiceSourceLockError::Malformed, + ServiceSourceLockError::Noncanonical, + ServiceSourceLockError::InvalidFixedIdentity, + ServiceSourceLockError::InvalidService, + ServiceSourceLockError::InvalidRevision, + ServiceSourceLockError::InvalidDigest, + ServiceSourceLockError::InvalidToolchain, + ServiceSourceLockError::InvalidFeatureProfile, + ServiceSourceLockError::InvalidContractVersion, + ]; + for error in errors { + assert!(error.source().is_none()); + assert!(!error.to_string().contains("fixture_service")); + } + let mut codes = errors.map(ServiceSourceLockError::code); + codes.sort_unstable(); + assert_eq!(codes, ERROR_CODES); + } +}