lib

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

package_boundary.rs (5775B)


      1 use std::collections::BTreeSet;
      2 use std::fs;
      3 use std::path::{Path, PathBuf};
      4 
      5 const MANIFEST: &str = include_str!("../Cargo.toml");
      6 const ROOT: &str = include_str!("../src/lib.rs");
      7 const README: &str = include_str!("../README.md");
      8 const EXAMPLE: &str = include_str!("../examples/explicit_memory_provider.rs");
      9 
     10 #[test]
     11 fn manifest_has_final_identity_features_and_no_radroots_dependencies() {
     12     for required in [
     13         "name = \"radroots_secrets\"",
     14         "version = \"0.1.0-alpha\"",
     15         "publish = [\"crates-io\"]",
     16         "documentation = \"https://docs.rs/radroots_secrets\"",
     17         "[lib]\nname = \"radroots_secrets\"",
     18         "default = [\"std\", \"serde\"]",
     19         "memory = [\"std\"]",
     20         "file = [\"std\", \"dep:tempfile\"]",
     21         "keyring = [\"std\", \"dep:keyring\"]",
     22     ] {
     23         assert!(
     24             MANIFEST.contains(required),
     25             "manifest is missing `{required}`"
     26         );
     27     }
     28 
     29     assert_eq!(
     30         table_keys(MANIFEST, "[features]"),
     31         BTreeSet::from(["default", "file", "keyring", "memory", "serde", "std"])
     32     );
     33     assert!(
     34         table_keys(MANIFEST, "[dependencies]")
     35             .into_iter()
     36             .all(|dependency| !dependency.starts_with("radroots_")),
     37         "security SPI must not depend on another Radroots package"
     38     );
     39     assert_eq!(
     40         table_keys(MANIFEST, "[dependencies]"),
     41         BTreeSet::from([
     42             "chacha20poly1305",
     43             "keyring",
     44             "serde",
     45             "sha2",
     46             "subtle",
     47             "tempfile",
     48             "zeroize"
     49         ])
     50     );
     51     assert_eq!(
     52         table_keys(MANIFEST, "[dev-dependencies]"),
     53         BTreeSet::from(["futures-executor", "hex", "serde_json"])
     54     );
     55 }
     56 
     57 #[test]
     58 fn production_sources_publish_only_the_approved_traits() {
     59     let source_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
     60     let mut paths = Vec::new();
     61     collect_rust_sources(&source_root, &mut paths);
     62     let mut public_traits = BTreeSet::new();
     63 
     64     for path in paths {
     65         let source = fs::read_to_string(&path).expect("read secrets source");
     66         let production = source.split("\n#[cfg(test)]").next().unwrap_or(&source);
     67         for line in production.lines() {
     68             if let Some(name) = line
     69                 .trim_start()
     70                 .strip_prefix("pub trait ")
     71                 .and_then(|rest| rest.split([':', '<', ' ']).next())
     72             {
     73                 public_traits.insert(name.to_owned());
     74             }
     75         }
     76     }
     77 
     78     assert_eq!(
     79         public_traits,
     80         ["KeyWrapping", "SecretProvider"]
     81             .into_iter()
     82             .map(str::to_owned)
     83             .collect()
     84     );
     85 }
     86 
     87 #[test]
     88 fn crate_root_contains_only_the_approved_module_skeleton() {
     89     assert!(ROOT.contains("#![cfg_attr(not(feature = \"std\"), no_std)]"));
     90     assert_eq!(
     91         declarations(ROOT, "pub mod "),
     92         BTreeSet::from([
     93             "context", "envelope", "error", "file", "id", "keyring", "memory", "provider",
     94             "wrapping",
     95         ])
     96     );
     97     assert_eq!(
     98         ROOT.lines()
     99             .map(str::trim)
    100             .filter(|line| line.starts_with("pub use "))
    101             .collect::<BTreeSet<_>>(),
    102         BTreeSet::from([
    103             "pub use envelope::EncryptedEnvelope;",
    104             "pub use error::Error;",
    105             "pub use id::{SecretId, SecretRef};",
    106             "pub use provider::SecretProvider;",
    107             "pub use wrapping::KeyWrapping;"
    108         ])
    109     );
    110 }
    111 
    112 #[test]
    113 fn package_documentation_covers_the_security_and_host_contract() {
    114     for required in [
    115         "## Canonical surface",
    116         "## Explicit provider and envelope flow",
    117         "## Features and supported targets",
    118         "## Security and serialization contract",
    119         "## Side effects, cancellation, and commit points",
    120         "## Intended consumers",
    121         "public API baseline",
    122         "implicit retry or fallback",
    123     ] {
    124         assert!(README.contains(required), "README is missing `{required}`");
    125     }
    126     assert!(ROOT.contains("#![doc = include_str!(\"../README.md\")]"));
    127     assert!(MANIFEST.contains("name = \"explicit_memory_provider\""));
    128     assert!(MANIFEST.contains("required-features = [\"memory\"]"));
    129     for required in [
    130         "MemoryProvider::new()",
    131         "provider.provision(",
    132         "EncryptedEnvelope::seal",
    133         "EncryptedEnvelope::decode",
    134         "decoded.open(&provider, &context)",
    135     ] {
    136         assert!(
    137             EXAMPLE.contains(required),
    138             "example is missing `{required}`"
    139         );
    140     }
    141 }
    142 
    143 fn table_keys<'a>(source: &'a str, table: &str) -> BTreeSet<&'a str> {
    144     let Some((_, body)) = source.split_once(table) else {
    145         return BTreeSet::new();
    146     };
    147     body.lines()
    148         .skip(1)
    149         .take_while(|line| !line.starts_with('['))
    150         .filter_map(|line| line.split_once('=').map(|(key, _)| key.trim()))
    151         .filter(|key| {
    152             !key.is_empty()
    153                 && key.chars().all(|character| {
    154                     character.is_ascii_alphanumeric() || matches!(character, '_' | '-')
    155                 })
    156         })
    157         .collect()
    158 }
    159 
    160 fn declarations<'a>(source: &'a str, prefix: &str) -> BTreeSet<&'a str> {
    161     source
    162         .lines()
    163         .map(str::trim)
    164         .filter_map(|line| line.strip_prefix(prefix))
    165         .filter_map(|line| line.strip_suffix(';'))
    166         .collect()
    167 }
    168 
    169 fn collect_rust_sources(root: &Path, paths: &mut Vec<PathBuf>) {
    170     for entry in fs::read_dir(root).expect("read source directory") {
    171         let path = entry.expect("source entry").path();
    172         if path.is_dir() {
    173             collect_rust_sources(&path, paths);
    174         } else if path.extension().and_then(|extension| extension.to_str()) == Some("rs") {
    175             paths.push(path);
    176         }
    177     }
    178 }