lib

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

lib.rs (17074B)


      1 #![no_std]
      2 #![forbid(unsafe_code)]
      3 #![cfg_attr(coverage_nightly, feature(coverage_attribute))]
      4 
      5 extern crate alloc;
      6 
      7 mod contract;
      8 mod message;
      9 
     10 pub use contract::{
     11     RETICULUM_V1_MAX_PAYLOAD_BYTES, ReticulumCapabilityReportV1, ReticulumDestinationV1,
     12     ReticulumDuplicateFragmentBehaviorV1, ReticulumFragmentIntegrityV1, ReticulumFragmentPolicyV1,
     13     ReticulumFragmentationModeV1, ReticulumGatewaySemanticsV1, ReticulumPayloadPolicyV1,
     14     ReticulumPrivacySemanticsV1, ReticulumRoutingMetadataV1,
     15 };
     16 pub use message::{
     17     RADROOTS_RETICULUM_ENDPOINT_URI, RADROOTS_RETICULUM_SCOPE_ID,
     18     RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE,
     19 };
     20 
     21 use alloc::borrow::ToOwned;
     22 use alloc::boxed::Box;
     23 use alloc::string::String;
     24 use alloc::vec::Vec;
     25 use core::fmt;
     26 use radroots_transport::capability::{
     27     Availability, Maturity, SinkCapabilities, SourceCapabilities,
     28 };
     29 use radroots_transport::outcome::{DeliveryOutcome, FetchTargetOutcome, FetchTargetState};
     30 use radroots_transport::sink::{
     31     DeliveryReceipt, DeliveryRequest, DeliveryTargetReceipt, EventSink, SinkStatus,
     32 };
     33 use radroots_transport::source::{EventSource, FetchPage, FetchRequest, NextPage, SourceStatus};
     34 use radroots_transport::target::TargetScope;
     35 use radroots_transport::{Error as TransportError, Target, TransportId};
     36 
     37 const DEFAULT_PROFILE_ID: &str = "transport.reticulum.default";
     38 const RETICULUM_AGENT_ENDPOINT_PREFIX: &str = "reticulum-agent:";
     39 const UNAVAILABLE_CODE: &str = "transport_unavailable";
     40 
     41 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
     42 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
     43 pub enum RadrootsReticulumBehavior {
     44     #[default]
     45     RejectDeliveryAttempts,
     46     DeferDeliveryPlans,
     47 }
     48 
     49 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
     50 #[derive(Clone, Debug, PartialEq, Eq)]
     51 pub struct RadrootsReticulumEndpoint {
     52     uri: String,
     53 }
     54 
     55 impl RadrootsReticulumEndpoint {
     56     pub fn parse(raw: impl AsRef<str>) -> Result<Self, RadrootsReticulumError> {
     57         let uri = raw.as_ref();
     58         if uri != RADROOTS_RETICULUM_ENDPOINT_URI {
     59             return Err(RadrootsReticulumError::InvalidEndpoint);
     60         }
     61         Ok(Self {
     62             uri: RADROOTS_RETICULUM_ENDPOINT_URI.to_owned(),
     63         })
     64     }
     65 
     66     pub fn as_str(&self) -> &str {
     67         self.uri.as_str()
     68     }
     69 
     70     pub fn into_string(self) -> String {
     71         self.uri
     72     }
     73 }
     74 
     75 impl Default for RadrootsReticulumEndpoint {
     76     fn default() -> Self {
     77         Self::parse(RADROOTS_RETICULUM_ENDPOINT_URI).expect("default Reticulum endpoint")
     78     }
     79 }
     80 
     81 impl fmt::Display for RadrootsReticulumEndpoint {
     82     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
     83         f.write_str(self.uri.as_str())
     84     }
     85 }
     86 
     87 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
     88 #[derive(Clone, Debug, PartialEq, Eq)]
     89 pub struct RadrootsReticulumAgentEndpoint {
     90     uri: String,
     91 }
     92 
     93 impl RadrootsReticulumAgentEndpoint {
     94     pub fn parse(raw: impl AsRef<str>) -> Result<Self, RadrootsReticulumError> {
     95         let uri = raw.as_ref();
     96         if uri.is_empty()
     97             || uri != uri.trim()
     98             || uri
     99                 .chars()
    100                 .any(|ch| ch.is_ascii_control() || ch.is_ascii_whitespace())
    101             || !uri.starts_with(RETICULUM_AGENT_ENDPOINT_PREFIX)
    102             || uri.len() == RETICULUM_AGENT_ENDPOINT_PREFIX.len()
    103         {
    104             return Err(RadrootsReticulumError::InvalidAgentEndpoint);
    105         }
    106         Ok(Self {
    107             uri: uri.to_owned(),
    108         })
    109     }
    110 
    111     pub fn as_str(&self) -> &str {
    112         self.uri.as_str()
    113     }
    114 
    115     pub fn into_string(self) -> String {
    116         self.uri
    117     }
    118 }
    119 
    120 impl fmt::Display for RadrootsReticulumAgentEndpoint {
    121     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
    122         f.write_str(self.uri.as_str())
    123     }
    124 }
    125 
    126 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
    127 #[derive(Clone, Debug, PartialEq, Eq)]
    128 pub struct RadrootsReticulumProfile {
    129     profile_id: String,
    130     endpoint: RadrootsReticulumEndpoint,
    131     scope: TargetScope,
    132     agent_endpoint: Option<RadrootsReticulumAgentEndpoint>,
    133     behavior: RadrootsReticulumBehavior,
    134     destination: ReticulumDestinationV1,
    135     capability_report: ReticulumCapabilityReportV1,
    136 }
    137 
    138 impl RadrootsReticulumProfile {
    139     pub fn new(
    140         profile_id: impl Into<String>,
    141         endpoint: RadrootsReticulumEndpoint,
    142         scope: TargetScope,
    143         agent_endpoint: Option<RadrootsReticulumAgentEndpoint>,
    144         behavior: RadrootsReticulumBehavior,
    145     ) -> Result<Self, RadrootsReticulumError> {
    146         let profile_id = profile_id.into();
    147         if profile_id.trim().is_empty() || profile_id.chars().any(char::is_whitespace) {
    148             return Err(RadrootsReticulumError::InvalidProfileId);
    149         }
    150         let destination = ReticulumDestinationV1::new(endpoint.as_str(), scope.clone(), None)
    151             .map_err(|_| RadrootsReticulumError::InvalidEndpoint)?;
    152         let capability_report = ReticulumCapabilityReportV1 {
    153             destination: destination.clone(),
    154             payload_policy: ReticulumPayloadPolicyV1::v1(),
    155             ..ReticulumCapabilityReportV1::unavailable_local()
    156         };
    157         Ok(Self {
    158             profile_id,
    159             endpoint,
    160             scope,
    161             agent_endpoint,
    162             behavior,
    163             destination,
    164             capability_report,
    165         })
    166     }
    167 
    168     pub fn deferred_until_implemented() -> Self {
    169         let capability_report = ReticulumCapabilityReportV1::unavailable_local();
    170         Self {
    171             profile_id: DEFAULT_PROFILE_ID.to_owned(),
    172             endpoint: RadrootsReticulumEndpoint::default(),
    173             scope: TargetScope::parse(RADROOTS_RETICULUM_SCOPE_ID).expect("Reticulum scope"),
    174             agent_endpoint: None,
    175             behavior: RadrootsReticulumBehavior::RejectDeliveryAttempts,
    176             destination: capability_report.destination.clone(),
    177             capability_report,
    178         }
    179     }
    180 
    181     pub fn with_behavior(mut self, behavior: RadrootsReticulumBehavior) -> Self {
    182         self.behavior = behavior;
    183         self
    184     }
    185 
    186     pub fn profile_id(&self) -> &str {
    187         self.profile_id.as_str()
    188     }
    189 
    190     pub fn endpoint(&self) -> &RadrootsReticulumEndpoint {
    191         &self.endpoint
    192     }
    193 
    194     pub fn scope(&self) -> &TargetScope {
    195         &self.scope
    196     }
    197 
    198     pub fn agent_endpoint(&self) -> Option<&RadrootsReticulumAgentEndpoint> {
    199         self.agent_endpoint.as_ref()
    200     }
    201 
    202     pub fn with_agent_endpoint(mut self, agent_endpoint: RadrootsReticulumAgentEndpoint) -> Self {
    203         self.agent_endpoint = Some(agent_endpoint);
    204         self
    205     }
    206 
    207     pub fn behavior(&self) -> RadrootsReticulumBehavior {
    208         self.behavior
    209     }
    210 
    211     pub fn destination(&self) -> &ReticulumDestinationV1 {
    212         &self.destination
    213     }
    214 
    215     pub fn capability_report(&self) -> &ReticulumCapabilityReportV1 {
    216         &self.capability_report
    217     }
    218 }
    219 
    220 impl Default for RadrootsReticulumProfile {
    221     fn default() -> Self {
    222         Self::deferred_until_implemented()
    223     }
    224 }
    225 
    226 #[derive(Clone, Debug, PartialEq, Eq)]
    227 pub struct RadrootsReticulumTransport {
    228     profile: RadrootsReticulumProfile,
    229 }
    230 
    231 impl RadrootsReticulumTransport {
    232     pub fn new(profile: RadrootsReticulumProfile) -> Self {
    233         Self { profile }
    234     }
    235 
    236     pub fn profile(&self) -> &RadrootsReticulumProfile {
    237         &self.profile
    238     }
    239 }
    240 
    241 impl Default for RadrootsReticulumTransport {
    242     fn default() -> Self {
    243         Self::new(RadrootsReticulumProfile::default())
    244     }
    245 }
    246 
    247 impl EventSink for RadrootsReticulumTransport {
    248     fn status(&self) -> radroots_transport::BoxFuture<'_, Result<SinkStatus, TransportError>> {
    249         Box::pin(async {
    250             Ok(SinkStatus::new(
    251                 TransportId::RETICULUM,
    252                 true,
    253                 Maturity::Preview,
    254                 Availability::Unavailable,
    255                 SinkCapabilities::NONE,
    256                 RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE,
    257             ))
    258         })
    259     }
    260 
    261     fn deliver(
    262         &self,
    263         request: DeliveryRequest,
    264     ) -> radroots_transport::BoxFuture<'_, Result<DeliveryReceipt, radroots_transport::SinkFailure>>
    265     {
    266         Box::pin(async move {
    267             ensure_reticulum_targets(request.target_set().targets())
    268                 .map_err(|_| radroots_transport::SinkFailure::invalid_contract(&request))?;
    269             let outcome = DeliveryOutcome::unavailable()
    270                 .with_detail(UNAVAILABLE_CODE, RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE)
    271                 .map_err(|_| radroots_transport::SinkFailure::invalid_contract(&request))?;
    272             let receipts = request
    273                 .target_set()
    274                 .targets()
    275                 .iter()
    276                 .cloned()
    277                 .map(|target| DeliveryTargetReceipt::skipped(target, outcome.clone()))
    278                 .collect::<Result<Vec<_>, _>>()
    279                 .map_err(|_| radroots_transport::SinkFailure::invalid_contract(&request))?;
    280             DeliveryReceipt::for_request(&request, receipts)
    281                 .map_err(|_| radroots_transport::SinkFailure::invalid_contract(&request))
    282         })
    283     }
    284 }
    285 
    286 impl EventSource for RadrootsReticulumTransport {
    287     fn status(&self) -> radroots_transport::BoxFuture<'_, Result<SourceStatus, TransportError>> {
    288         Box::pin(async {
    289             Ok(SourceStatus::new(
    290                 TransportId::RETICULUM,
    291                 true,
    292                 Maturity::Preview,
    293                 Availability::Unavailable,
    294                 SourceCapabilities::NONE,
    295                 RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE,
    296             ))
    297         })
    298     }
    299 
    300     fn fetch(
    301         &self,
    302         request: FetchRequest,
    303     ) -> radroots_transport::BoxFuture<'_, Result<FetchPage, TransportError>> {
    304         Box::pin(async move {
    305             ensure_reticulum_targets(request.target_set().targets())
    306                 .map_err(reticulum_error_to_transport_error)?;
    307             let outcomes = request
    308                 .target_set()
    309                 .targets()
    310                 .iter()
    311                 .map(|target| {
    312                     FetchTargetOutcome::new(
    313                         target.fingerprint().clone(),
    314                         FetchTargetState::Unavailable,
    315                     )
    316                     .with_message(RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE)
    317                 })
    318                 .collect();
    319             FetchPage::for_request(&request, Vec::new(), outcomes, NextPage::Complete)
    320         })
    321     }
    322 }
    323 
    324 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
    325 pub enum RadrootsReticulumError {
    326     InvalidEndpoint,
    327     InvalidAgentEndpoint,
    328     InvalidProfileId,
    329     NonReticulumTarget,
    330 }
    331 
    332 impl fmt::Display for RadrootsReticulumError {
    333     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
    334         f.write_str(match self {
    335             Self::InvalidEndpoint => "invalid Reticulum endpoint",
    336             Self::InvalidAgentEndpoint => "invalid Reticulum agent endpoint",
    337             Self::InvalidProfileId => "invalid Reticulum profile id",
    338             Self::NonReticulumTarget => "Reticulum transport received a non-Reticulum target",
    339         })
    340     }
    341 }
    342 
    343 fn reticulum_error_to_transport_error(error: RadrootsReticulumError) -> TransportError {
    344     match error {
    345         RadrootsReticulumError::InvalidEndpoint | RadrootsReticulumError::NonReticulumTarget => {
    346             TransportError::InvalidTargetUri
    347         }
    348         RadrootsReticulumError::InvalidAgentEndpoint | RadrootsReticulumError::InvalidProfileId => {
    349             TransportError::InvalidTransportKind
    350         }
    351     }
    352 }
    353 
    354 fn ensure_reticulum_targets(targets: &[Target]) -> Result<(), RadrootsReticulumError> {
    355     for target in targets {
    356         if target.kind() != &TransportId::RETICULUM {
    357             return Err(RadrootsReticulumError::NonReticulumTarget);
    358         }
    359         if target.uri().as_str() != RADROOTS_RETICULUM_ENDPOINT_URI {
    360             return Err(RadrootsReticulumError::InvalidEndpoint);
    361         }
    362         if target.scope().is_none() {
    363             return Err(RadrootsReticulumError::InvalidEndpoint);
    364         }
    365     }
    366     Ok(())
    367 }
    368 
    369 #[cfg(test)]
    370 #[cfg_attr(coverage_nightly, coverage(off))]
    371 mod tests {
    372     use super::*;
    373     use alloc::{format, vec};
    374     use futures::executor::block_on;
    375     use radroots_event::{SignedEvent, wire::Nip01EventWire};
    376     use radroots_transport::{
    377         DeliveryRequest, EventSink, EventSource, FetchRequest, TargetSet,
    378         outcome::DeliveryOutcomeKind,
    379         policy::{SatisfactionClass, SatisfactionPolicy, TargetPolicy},
    380         sink::DeliveryPayload,
    381         source::FetchBounds,
    382     };
    383 
    384     fn target() -> Target {
    385         ReticulumDestinationV1::local()
    386             .transport_target()
    387             .expect("Reticulum target")
    388     }
    389 
    390     fn signed_event() -> SignedEvent {
    391         let mut wire = Nip01EventWire {
    392             id: "0".repeat(64),
    393             pubkey: "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df".to_owned(),
    394             created_at: 1_800_000_100,
    395             kind: 1,
    396             tags: vec![],
    397             content: "reticulum-preview".to_owned(),
    398             sig: "42".repeat(64),
    399             extra: Default::default(),
    400         };
    401         wire.id = wire.computed_event_id().expect("event id").to_hex();
    402         let raw = format!(
    403             "{{\"id\":\"{}\",\"pubkey\":\"{}\",\"created_at\":{},\"kind\":{},\"tags\":[],\"content\":\"{}\",\"sig\":\"{}\"}}",
    404             wire.id, wire.pubkey, wire.created_at, wire.kind, wire.content, wire.sig
    405         );
    406         SignedEvent::from_wire_verified_id(wire, raw).expect("signed event")
    407     }
    408 
    409     #[test]
    410     fn endpoints_and_profiles_preserve_validated_reticulum_contracts() {
    411         let endpoint = RadrootsReticulumEndpoint::default();
    412         assert_eq!(endpoint.as_str(), RADROOTS_RETICULUM_ENDPOINT_URI);
    413         assert_eq!(format!("{endpoint}"), RADROOTS_RETICULUM_ENDPOINT_URI);
    414         assert!(RadrootsReticulumEndpoint::parse("reticulum:other").is_err());
    415 
    416         let agent =
    417             RadrootsReticulumAgentEndpoint::parse("reticulum-agent:local").expect("agent endpoint");
    418         let profile = RadrootsReticulumProfile::new(
    419             "transport.reticulum.farm",
    420             endpoint,
    421             TargetScope::parse("farm.mesh").expect("scope"),
    422             Some(agent.clone()),
    423             RadrootsReticulumBehavior::DeferDeliveryPlans,
    424         )
    425         .expect("profile");
    426         assert_eq!(profile.agent_endpoint(), Some(&agent));
    427         assert_eq!(
    428             profile.behavior(),
    429             RadrootsReticulumBehavior::DeferDeliveryPlans
    430         );
    431         assert_eq!(
    432             profile.destination(),
    433             &profile.capability_report().destination
    434         );
    435     }
    436 
    437     #[test]
    438     fn canonical_source_and_sink_fail_closed_with_request_bound_evidence() {
    439         let transport = RadrootsReticulumTransport::default();
    440         let sink_status = block_on(EventSink::status(&transport)).expect("sink status");
    441         let source_status = block_on(EventSource::status(&transport)).expect("source status");
    442         assert_eq!(sink_status.transport_id(), TransportId::RETICULUM);
    443         assert_eq!(source_status.transport_id(), TransportId::RETICULUM);
    444         assert_eq!(sink_status.availability(), Availability::Unavailable);
    445         assert_eq!(source_status.availability(), Availability::Unavailable);
    446 
    447         let targets = TargetSet::new(vec![target()]).expect("targets");
    448         let delivery = DeliveryRequest::new(
    449             "reticulum-delivery",
    450             DeliveryPayload::new(signed_event()),
    451             targets.clone(),
    452             SatisfactionPolicy::new(SatisfactionClass::Accepted, TargetPolicy::all()),
    453             1_800_000_200_000,
    454         )
    455         .expect("delivery request");
    456         let receipt = block_on(EventSink::deliver(&transport, delivery.clone()))
    457             .expect("unavailable receipt");
    458         assert_eq!(receipt.target_receipts().len(), 1);
    459         assert!(!receipt.target_receipts()[0].was_attempted());
    460         assert_eq!(
    461             receipt.target_receipts()[0].outcome().kind(),
    462             DeliveryOutcomeKind::Unavailable
    463         );
    464         receipt
    465             .validate_for_request(&delivery)
    466             .expect("request bound");
    467 
    468         let fetch = FetchRequest::new(
    469             "reticulum-fetch",
    470             targets,
    471             FetchBounds::new(10, 1_800_000_200_000).expect("bounds"),
    472         )
    473         .expect("fetch request");
    474         let page = block_on(EventSource::fetch(&transport, fetch.clone())).expect("fetch page");
    475         assert!(page.events().is_empty());
    476         page.validate_for_request(&fetch).expect("request bound");
    477     }
    478 
    479     #[test]
    480     fn non_reticulum_targets_are_rejected_before_adapter_effects() {
    481         let target = Target::nostr_relay("wss://relay.example").expect("Nostr target");
    482         assert_eq!(
    483             ensure_reticulum_targets(&[target]),
    484             Err(RadrootsReticulumError::NonReticulumTarget)
    485         );
    486         assert_eq!(
    487             reticulum_error_to_transport_error(RadrootsReticulumError::NonReticulumTarget),
    488             TransportError::InvalidTargetUri
    489         );
    490     }
    491 }