lib

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

commit 937d8ba5200dc7c3fe6757ba5ae5651d7eccc1f4
parent 3c34bbb0129df7803db83e6af92b79f84127d56c
Author: triesap <tyson@radroots.org>
Date:   Tue,  7 Jul 2026 06:26:40 +0000

transport: add hardening source boundary guards

- add source-boundary coverage for removed publish proxy and relay-shaped identifiers
- guard the Reticulum preview endpoint against old reticulum:preview identity regressions
- require transport publish capabilities to retain readiness and usability fields
- validate with fmt, transport crate tests, contract validate, and release preflight

Diffstat:
Acrates/transport/tests/source_boundary.rs | 181+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 181 insertions(+), 0 deletions(-)

diff --git a/crates/transport/tests/source_boundary.rs b/crates/transport/tests/source_boundary.rs @@ -0,0 +1,181 @@ +use std::{ + fs, + path::{Path, PathBuf}, +}; + +struct ForbiddenConcept { + pattern: &'static str, + reason: &'static str, +} + +const TRANSPORT_HARDENING_CRATE_SOURCE_ROOTS: &[&str] = &[ + "transport/src", + "transport_reticulum/src", + "transport_publish_protocol/src", + "transport_nostr/src", + "outbox/src", +]; + +const FORBIDDEN_TRANSPORT_CONCEPTS: &[ForbiddenConcept] = &[ + ForbiddenConcept { + pattern: "radrootsd.publish_proxy.v1", + reason: "transport publish protocol v1 proxy identifiers are removed", + }, + ForbiddenConcept { + pattern: "publish.relays.resolve", + reason: "relay-resolution RPC is replaced by transport publish target policy", + }, + ForbiddenConcept { + pattern: "\"publish.event\"", + reason: "publish.event is replaced by transport.publish.event", + }, + ForbiddenConcept { + pattern: "transport_kinds", + reason: "capabilities must expose per-transport readiness instead of kind-only lists", + }, + ForbiddenConcept { + pattern: "allowed_relay_policy", + reason: "relay policy is Nostr-specific and must not be a generic transport API", + }, + ForbiddenConcept { + pattern: "relay_policy", + reason: "relay policy is Nostr-specific and must not be a generic transport API", + }, + ForbiddenConcept { + pattern: "PublishRelayPolicy", + reason: "old relay-shaped publish policy names must not return", + }, + ForbiddenConcept { + pattern: "PublishRelayOutcome", + reason: "old relay-shaped publish outcome names must not return", + }, + ForbiddenConcept { + pattern: "PublishRelaySource", + reason: "old relay-shaped publish source names must not return", + }, +]; + +#[test] +fn transport_hardening_sources_reject_removed_protocol_identifiers() { + let crates_root = Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .expect("transport crate parent"); + let mut findings = Vec::new(); + + for relative_root in TRANSPORT_HARDENING_CRATE_SOURCE_ROOTS { + for path in rust_source_files(crates_root.join(relative_root).as_path()) { + let source = read_source(path.as_path()); + let relative_path = relative_path(crates_root, path.as_path()); + + for concept in FORBIDDEN_TRANSPORT_CONCEPTS { + if contains_forbidden_concept(source.as_str(), concept.pattern) { + findings.push(format!( + "{} contains removed transport concept `{}`: {}", + relative_path, concept.pattern, concept.reason + )); + } + } + + for line in removed_reticulum_preview_endpoint_lines(source.as_str()) { + findings.push(format!( + "{relative_path}:{line} contains removed Reticulum preview endpoint `reticulum:preview`" + )); + } + } + } + + assert!( + findings.is_empty(), + "transport hardening source-boundary violations:\n{}", + findings.join("\n") + ); +} + +#[test] +fn transport_publish_capabilities_keep_readiness_and_usability_fields() { + let source = read_source( + Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .expect("transport crate parent") + .join("transport_publish_protocol/src/lib.rs") + .as_path(), + ); + + for required in [ + "pub implementation_state: TransportPublishImplementationState,", + "pub usable_for_delivery: bool,", + "TransportPublishImplementationState::Available", + "TransportPublishImplementationState::PreviewUnavailable", + "usable_for_delivery: true", + "usable_for_delivery: false", + ] { + assert!( + source.contains(required), + "transport publish capabilities must retain readiness/usability field `{required}`" + ); + } +} + +fn rust_source_files(root: &Path) -> Vec<PathBuf> { + let mut paths = Vec::new(); + collect_rust_source_files(root, &mut paths); + paths.sort(); + paths +} + +fn collect_rust_source_files(root: &Path, paths: &mut Vec<PathBuf>) { + for entry in fs::read_dir(root) + .unwrap_or_else(|error| panic!("failed to read {}: {error}", root.display())) + { + let entry = entry.expect("read source entry"); + let path = entry.path(); + if path.is_dir() { + collect_rust_source_files(path.as_path(), paths); + } else if path.extension().and_then(|extension| extension.to_str()) == Some("rs") { + paths.push(path); + } + } +} + +fn read_source(path: &Path) -> String { + fs::read_to_string(path) + .unwrap_or_else(|error| panic!("failed to read source {}: {error}", path.display())) +} + +fn relative_path(root: &Path, path: &Path) -> String { + path.strip_prefix(root) + .expect("source path is under crate root") + .to_string_lossy() + .replace('\\', "/") +} + +fn contains_forbidden_concept(source: &str, pattern: &str) -> bool { + source.match_indices(pattern).any(|(index, _)| { + let before = source[..index].chars().next_back(); + let after = source[index + pattern.len()..].chars().next(); + before.is_none_or(|character| !is_rust_identifier_character(character)) + && after.is_none_or(|character| !is_rust_identifier_character(character)) + }) +} + +fn removed_reticulum_preview_endpoint_lines(source: &str) -> Vec<usize> { + source + .match_indices("reticulum:preview") + .filter_map(|(index, _)| { + let after = source[index + "reticulum:preview".len()..].chars().next(); + (after != Some('-')).then(|| line_number(source, index)) + }) + .collect() +} + +fn is_rust_identifier_character(character: char) -> bool { + character == '_' || character.is_ascii_alphanumeric() +} + +fn line_number(source: &str, index: usize) -> usize { + source[..index] + .bytes() + .filter(|byte| *byte == b'\n') + .count() + + 1 +}