lib

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

build_info.rs (18361B)


      1 //! Deterministic build identity shared by hardened services.
      2 
      3 use core::fmt;
      4 
      5 use serde::Serialize;
      6 
      7 /// Environment variable containing the consuming service's exact source revision.
      8 pub const SERVICE_REVISION_ENV: &str = "RADROOTS_SERVICE_REVISION";
      9 /// Environment variable containing the exact Radroots Lib revision.
     10 pub const LIB_REVISION_ENV: &str = "RADROOTS_LIB_REVISION";
     11 /// Environment variable containing the exact governed Rust version.
     12 pub const RUST_VERSION_ENV: &str = "RADROOTS_RUST_VERSION";
     13 /// Environment variable containing the exact Rust target triple.
     14 pub const BUILD_TARGET_ENV: &str = "RADROOTS_BUILD_TARGET";
     15 
     16 /// Maximum byte length of service versions, Rust versions, targets, and feature profiles.
     17 pub const BUILD_INFO_TEXT_MAX_BYTES: usize = 128;
     18 const DEVELOPMENT_REVISION: &str = "0000000000000000000000000000000000000000";
     19 const DEVELOPMENT_VALUE: &str = "development";
     20 
     21 /// Determines whether missing compile-time metadata is an error.
     22 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
     23 pub enum BuildMode {
     24     Development,
     25     Release,
     26 }
     27 
     28 /// Exact versions for every service-host contract family.
     29 #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
     30 #[serde(deny_unknown_fields)]
     31 pub struct ContractVersions {
     32     config: u32,
     33     state: u32,
     34     admin: u32,
     35     status: u32,
     36     provider: u32,
     37 }
     38 
     39 impl ContractVersions {
     40     /// Creates a contract-version cohort, rejecting zero as an unpublished identity.
     41     pub fn new(
     42         config: u32,
     43         state: u32,
     44         admin: u32,
     45         status: u32,
     46         provider: u32,
     47     ) -> Result<Self, BuildInfoError> {
     48         let versions = Self {
     49             config,
     50             state,
     51             admin,
     52             status,
     53             provider,
     54         };
     55         if [config, state, admin, status, provider].contains(&0) {
     56             return Err(BuildInfoError::InvalidValue(
     57                 BuildInfoField::ContractVersion,
     58             ));
     59         }
     60         Ok(versions)
     61     }
     62 
     63     #[must_use]
     64     pub const fn config(self) -> u32 {
     65         self.config
     66     }
     67 
     68     #[must_use]
     69     pub const fn state(self) -> u32 {
     70         self.state
     71     }
     72 
     73     #[must_use]
     74     pub const fn admin(self) -> u32 {
     75         self.admin
     76     }
     77 
     78     #[must_use]
     79     pub const fn status(self) -> u32 {
     80         self.status
     81     }
     82 
     83     #[must_use]
     84     pub const fn provider(self) -> u32 {
     85         self.provider
     86     }
     87 }
     88 
     89 /// Compile-time strings captured by [`crate::compile_time_build_info!`].
     90 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
     91 pub struct BuildInfoEnvironment<'a> {
     92     pub service_version: Option<&'a str>,
     93     pub service_commit: Option<&'a str>,
     94     pub lib_revision: Option<&'a str>,
     95     pub rust_version: Option<&'a str>,
     96     pub target: Option<&'a str>,
     97     pub feature_profile: Option<&'a str>,
     98     pub contract_versions: ContractVersions,
     99 }
    100 
    101 /// A deterministic, timestamp-free service build identity.
    102 #[derive(Clone, Debug, PartialEq, Eq, Serialize)]
    103 #[serde(deny_unknown_fields)]
    104 pub struct BuildInfo {
    105     service_version: String,
    106     service_commit: String,
    107     lib_revision: String,
    108     rust_version: String,
    109     target: String,
    110     feature_profile: String,
    111     contract_versions: ContractVersions,
    112 }
    113 
    114 /// The exact build-information projection frozen by the service status contracts.
    115 #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
    116 #[serde(deny_unknown_fields)]
    117 pub struct StatusBuildInfo<'a> {
    118     version: &'a str,
    119     revision: &'a str,
    120     toolchain: &'a str,
    121     contract_versions: ContractVersions,
    122 }
    123 
    124 impl BuildInfo {
    125     /// Validates captured compile-time values and constructs one stable build identity.
    126     pub fn from_compile_time(
    127         mode: BuildMode,
    128         environment: BuildInfoEnvironment<'_>,
    129     ) -> Result<Self, BuildInfoError> {
    130         let service_version = required_text(
    131             mode,
    132             environment.service_version,
    133             "CARGO_PKG_VERSION",
    134             BuildInfoField::ServiceVersion,
    135         )?;
    136         let service_commit = required_revision(
    137             mode,
    138             environment.service_commit,
    139             SERVICE_REVISION_ENV,
    140             BuildInfoField::ServiceCommit,
    141         )?;
    142         let lib_revision = required_revision(
    143             mode,
    144             environment.lib_revision,
    145             LIB_REVISION_ENV,
    146             BuildInfoField::LibRevision,
    147         )?;
    148         let rust_version = required_text(
    149             mode,
    150             environment.rust_version,
    151             RUST_VERSION_ENV,
    152             BuildInfoField::RustVersion,
    153         )?;
    154         let target = required_text(
    155             mode,
    156             environment.target,
    157             BUILD_TARGET_ENV,
    158             BuildInfoField::Target,
    159         )?;
    160         let feature_profile = required_text(
    161             mode,
    162             environment.feature_profile,
    163             "feature_profile",
    164             BuildInfoField::FeatureProfile,
    165         )?;
    166 
    167         Ok(Self {
    168             service_version,
    169             service_commit,
    170             lib_revision,
    171             rust_version,
    172             target,
    173             feature_profile,
    174             contract_versions: environment.contract_versions,
    175         })
    176     }
    177 
    178     #[must_use]
    179     pub fn service_version(&self) -> &str {
    180         &self.service_version
    181     }
    182 
    183     #[must_use]
    184     pub fn service_commit(&self) -> &str {
    185         &self.service_commit
    186     }
    187 
    188     #[must_use]
    189     pub fn lib_revision(&self) -> &str {
    190         &self.lib_revision
    191     }
    192 
    193     #[must_use]
    194     pub fn rust_version(&self) -> &str {
    195         &self.rust_version
    196     }
    197 
    198     #[must_use]
    199     pub fn target(&self) -> &str {
    200         &self.target
    201     }
    202 
    203     #[must_use]
    204     pub fn feature_profile(&self) -> &str {
    205         &self.feature_profile
    206     }
    207 
    208     #[must_use]
    209     pub const fn contract_versions(&self) -> ContractVersions {
    210         self.contract_versions
    211     }
    212 
    213     /// Projects the complete identity into the frozen Myc/RHI status shape.
    214     ///
    215     /// The full Lib revision, target, and feature profile remain available on `BuildInfo` and are
    216     /// bound by source-lock verification; the status contract intentionally exposes only these
    217     /// four fields.
    218     #[must_use]
    219     pub fn status_projection(&self) -> StatusBuildInfo<'_> {
    220         StatusBuildInfo {
    221             version: &self.service_version,
    222             revision: &self.service_commit,
    223             toolchain: &self.rust_version,
    224             contract_versions: self.contract_versions,
    225         }
    226     }
    227 }
    228 
    229 /// Identifies the invalid field without retaining rejected input.
    230 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
    231 pub enum BuildInfoField {
    232     ServiceVersion,
    233     ServiceCommit,
    234     LibRevision,
    235     RustVersion,
    236     Target,
    237     FeatureProfile,
    238     ContractVersion,
    239 }
    240 
    241 impl fmt::Display for BuildInfoField {
    242     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    243         formatter.write_str(match self {
    244             Self::ServiceVersion => "service_version",
    245             Self::ServiceCommit => "service_commit",
    246             Self::LibRevision => "lib_revision",
    247             Self::RustVersion => "rust_version",
    248             Self::Target => "target",
    249             Self::FeatureProfile => "feature_profile",
    250             Self::ContractVersion => "contract_version",
    251         })
    252     }
    253 }
    254 
    255 /// Validation failure for deterministic build metadata.
    256 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
    257 pub enum BuildInfoError {
    258     MissingVariable(&'static str),
    259     InvalidValue(BuildInfoField),
    260 }
    261 
    262 impl fmt::Display for BuildInfoError {
    263     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    264         match self {
    265             Self::MissingVariable(variable) => {
    266                 write!(formatter, "required build variable {variable} is missing")
    267             }
    268             Self::InvalidValue(field) => write!(formatter, "build field {field} is invalid"),
    269         }
    270     }
    271 }
    272 
    273 impl std::error::Error for BuildInfoError {}
    274 
    275 fn required_text(
    276     mode: BuildMode,
    277     value: Option<&str>,
    278     variable: &'static str,
    279     field: BuildInfoField,
    280 ) -> Result<String, BuildInfoError> {
    281     match value {
    282         Some(value) if valid_text(value) => Ok(value.to_owned()),
    283         Some(_) => Err(BuildInfoError::InvalidValue(field)),
    284         None if mode == BuildMode::Development => Ok(DEVELOPMENT_VALUE.to_owned()),
    285         None => Err(BuildInfoError::MissingVariable(variable)),
    286     }
    287 }
    288 
    289 fn required_revision(
    290     mode: BuildMode,
    291     value: Option<&str>,
    292     variable: &'static str,
    293     field: BuildInfoField,
    294 ) -> Result<String, BuildInfoError> {
    295     match value {
    296         Some(value)
    297             if valid_revision(value)
    298                 && (mode != BuildMode::Release || value != DEVELOPMENT_REVISION) =>
    299         {
    300             Ok(value.to_owned())
    301         }
    302         Some(_) => Err(BuildInfoError::InvalidValue(field)),
    303         None if mode == BuildMode::Development => Ok(DEVELOPMENT_REVISION.to_owned()),
    304         None => Err(BuildInfoError::MissingVariable(variable)),
    305     }
    306 }
    307 
    308 fn valid_text(value: &str) -> bool {
    309     let mut bytes = value.bytes();
    310     let Some(first) = bytes.next() else {
    311         return false;
    312     };
    313     value.len() <= BUILD_INFO_TEXT_MAX_BYTES
    314         && first.is_ascii_alphanumeric()
    315         && bytes
    316             .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b':' | b'-'))
    317 }
    318 
    319 fn valid_revision(value: &str) -> bool {
    320     value.len() == 40
    321         && value
    322             .bytes()
    323             .all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f'))
    324 }
    325 
    326 #[cfg(test)]
    327 mod tests {
    328     use super::*;
    329 
    330     const SERVICE_REVISION: &str = "0123456789abcdef0123456789abcdef01234567";
    331     const LIB_REVISION: &str = "89abcdef0123456789abcdef0123456789abcdef";
    332 
    333     fn contracts() -> ContractVersions {
    334         ContractVersions::new(1, 2, 3, 4, 5).expect("contract versions")
    335     }
    336 
    337     fn complete_environment() -> BuildInfoEnvironment<'static> {
    338         BuildInfoEnvironment {
    339             service_version: Some("0.1.0-alpha"),
    340             service_commit: Some(SERVICE_REVISION),
    341             lib_revision: Some(LIB_REVISION),
    342             rust_version: Some("1.97.1"),
    343             target: Some("x86_64-unknown-linux-gnu"),
    344             feature_profile: Some("service-host"),
    345             contract_versions: contracts(),
    346         }
    347     }
    348 
    349     #[test]
    350     fn serialization_is_an_exact_timestamp_free_snapshot() {
    351         let build = BuildInfo::from_compile_time(BuildMode::Release, complete_environment())
    352             .expect("valid build information");
    353         let json = serde_json::to_string_pretty(&build).expect("serialize build information");
    354 
    355         assert_eq!(
    356             json,
    357             r#"{
    358   "service_version": "0.1.0-alpha",
    359   "service_commit": "0123456789abcdef0123456789abcdef01234567",
    360   "lib_revision": "89abcdef0123456789abcdef0123456789abcdef",
    361   "rust_version": "1.97.1",
    362   "target": "x86_64-unknown-linux-gnu",
    363   "feature_profile": "service-host",
    364   "contract_versions": {
    365     "config": 1,
    366     "state": 2,
    367     "admin": 3,
    368     "status": 4,
    369     "provider": 5
    370   }
    371 }"#
    372         );
    373         assert!(!json.contains("time"));
    374         assert!(!json.contains("date"));
    375 
    376         let status_json = serde_json::to_string_pretty(&build.status_projection())
    377             .expect("serialize status build information");
    378         assert_eq!(
    379             status_json,
    380             r#"{
    381   "version": "0.1.0-alpha",
    382   "revision": "0123456789abcdef0123456789abcdef01234567",
    383   "toolchain": "1.97.1",
    384   "contract_versions": {
    385     "config": 1,
    386     "state": 2,
    387     "admin": 3,
    388     "status": 4,
    389     "provider": 5
    390   }
    391 }"#
    392         );
    393     }
    394 
    395     #[test]
    396     fn release_mode_fails_closed_for_every_missing_build_variable() {
    397         let complete = complete_environment();
    398         let cases = [
    399             (
    400                 BuildInfoEnvironment {
    401                     service_version: None,
    402                     ..complete
    403                 },
    404                 "CARGO_PKG_VERSION",
    405             ),
    406             (
    407                 BuildInfoEnvironment {
    408                     service_commit: None,
    409                     ..complete
    410                 },
    411                 SERVICE_REVISION_ENV,
    412             ),
    413             (
    414                 BuildInfoEnvironment {
    415                     lib_revision: None,
    416                     ..complete
    417                 },
    418                 LIB_REVISION_ENV,
    419             ),
    420             (
    421                 BuildInfoEnvironment {
    422                     rust_version: None,
    423                     ..complete
    424                 },
    425                 RUST_VERSION_ENV,
    426             ),
    427             (
    428                 BuildInfoEnvironment {
    429                     target: None,
    430                     ..complete
    431                 },
    432                 BUILD_TARGET_ENV,
    433             ),
    434             (
    435                 BuildInfoEnvironment {
    436                     feature_profile: None,
    437                     ..complete
    438                 },
    439                 "feature_profile",
    440             ),
    441         ];
    442 
    443         for (environment, variable) in cases {
    444             assert_eq!(
    445                 BuildInfo::from_compile_time(BuildMode::Release, environment),
    446                 Err(BuildInfoError::MissingVariable(variable))
    447             );
    448         }
    449     }
    450 
    451     #[test]
    452     fn identical_inputs_are_equal_and_development_fallbacks_are_stable() {
    453         let left = BuildInfo::from_compile_time(BuildMode::Release, complete_environment())
    454             .expect("left build information");
    455         let right = BuildInfo::from_compile_time(BuildMode::Release, complete_environment())
    456             .expect("right build information");
    457         assert_eq!(left, right);
    458 
    459         let development = BuildInfo::from_compile_time(
    460             BuildMode::Development,
    461             BuildInfoEnvironment {
    462                 service_version: None,
    463                 service_commit: None,
    464                 lib_revision: None,
    465                 rust_version: None,
    466                 target: None,
    467                 feature_profile: None,
    468                 contract_versions: contracts(),
    469             },
    470         )
    471         .expect("development fallbacks");
    472         assert_eq!(development.service_version(), DEVELOPMENT_VALUE);
    473         assert_eq!(development.service_commit(), DEVELOPMENT_REVISION);
    474         assert_eq!(development.lib_revision(), DEVELOPMENT_REVISION);
    475         assert_eq!(development.rust_version(), DEVELOPMENT_VALUE);
    476         assert_eq!(development.target(), DEVELOPMENT_VALUE);
    477         assert_eq!(development.feature_profile(), DEVELOPMENT_VALUE);
    478     }
    479 
    480     #[test]
    481     fn malformed_values_and_zero_contract_versions_are_rejected() {
    482         for revision in [
    483             "short",
    484             "ABCDEF0123456789abcdef0123456789abcdef01",
    485             DEVELOPMENT_REVISION,
    486         ] {
    487             let mut environment = complete_environment();
    488             environment.service_commit = Some(revision);
    489             assert_eq!(
    490                 BuildInfo::from_compile_time(BuildMode::Release, environment),
    491                 Err(BuildInfoError::InvalidValue(BuildInfoField::ServiceCommit))
    492             );
    493 
    494             let mut environment = complete_environment();
    495             environment.lib_revision = Some(revision);
    496             assert_eq!(
    497                 BuildInfo::from_compile_time(BuildMode::Release, environment),
    498                 Err(BuildInfoError::InvalidValue(BuildInfoField::LibRevision))
    499             );
    500         }
    501         assert_eq!(
    502             ContractVersions::new(1, 1, 0, 1, 1),
    503             Err(BuildInfoError::InvalidValue(
    504                 BuildInfoField::ContractVersion
    505             ))
    506         );
    507 
    508         for invalid in ["bad version", " rustc", ".1.97.1", "rustc/+nightly"] {
    509             let mut environment = complete_environment();
    510             environment.service_version = Some(invalid);
    511             assert_eq!(
    512                 BuildInfo::from_compile_time(BuildMode::Release, environment),
    513                 Err(BuildInfoError::InvalidValue(BuildInfoField::ServiceVersion))
    514             );
    515 
    516             let mut environment = complete_environment();
    517             environment.rust_version = Some(invalid);
    518             assert_eq!(
    519                 BuildInfo::from_compile_time(BuildMode::Release, environment),
    520                 Err(BuildInfoError::InvalidValue(BuildInfoField::RustVersion))
    521             );
    522         }
    523     }
    524 
    525     #[test]
    526     fn macro_captures_the_consuming_crate_version() {
    527         let result = crate::compile_time_build_info!(
    528             feature_profile: "service-host",
    529             contract_versions: contracts(),
    530         );
    531 
    532         if cfg!(debug_assertions) {
    533             let build = result.expect("debug builds accept missing release variables");
    534             assert_eq!(build.service_version(), env!("CARGO_PKG_VERSION"));
    535             assert_eq!(build.contract_versions(), contracts());
    536         } else {
    537             assert_eq!(
    538                 result,
    539                 Err(BuildInfoError::MissingVariable(SERVICE_REVISION_ENV))
    540             );
    541         }
    542     }
    543 
    544     #[test]
    545     fn public_accessors_and_text_boundaries_are_exact() {
    546         let build =
    547             BuildInfo::from_compile_time(BuildMode::Release, complete_environment()).unwrap();
    548         assert_eq!(build.service_commit(), SERVICE_REVISION);
    549         assert_eq!(build.lib_revision(), LIB_REVISION);
    550         assert_eq!(build.rust_version(), "1.97.1");
    551         assert_eq!(build.target(), "x86_64-unknown-linux-gnu");
    552         assert_eq!(build.feature_profile(), "service-host");
    553         assert_eq!(build.contract_versions().config(), 1);
    554         assert_eq!(build.contract_versions().state(), 2);
    555         assert_eq!(build.contract_versions().admin(), 3);
    556         assert_eq!(build.contract_versions().status(), 4);
    557         assert_eq!(build.contract_versions().provider(), 5);
    558 
    559         assert!(!valid_text(""));
    560         assert!(valid_text(&"a".repeat(BUILD_INFO_TEXT_MAX_BYTES)));
    561         assert!(!valid_text(&"a".repeat(BUILD_INFO_TEXT_MAX_BYTES + 1)));
    562         assert!(valid_text("a.b_c:d-e"));
    563         assert!(!valid_text("a/b"));
    564 
    565         let mut environment = complete_environment();
    566         environment.service_commit = Some(DEVELOPMENT_REVISION);
    567         environment.lib_revision = Some(DEVELOPMENT_REVISION);
    568         assert!(BuildInfo::from_compile_time(BuildMode::Development, environment).is_ok());
    569 
    570         for error in [
    571             BuildInfoError::InvalidValue(BuildInfoField::Target),
    572             BuildInfoError::MissingVariable(BUILD_TARGET_ENV),
    573         ] {
    574             assert!(!error.to_string().is_empty());
    575             assert!(std::error::Error::source(&error).is_none());
    576         }
    577     }
    578 }