lib

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

decode.rs (8962B)


      1 //! Deterministic event decoding boundary.
      2 //!
      3 //! Decoding validates the wire structure and returns a [`RawEvent`]. It does
      4 //! not verify the event identifier, signature, or contract. Those transitions
      5 //! remain explicit under [`crate::verify`].
      6 
      7 use core::fmt;
      8 use radroots_event::{envelope::EventEnvelopeError, wire::EventWireError};
      9 
     10 // Domain-specific parsers live behind the canonical decoding namespace.
     11 pub use crate::error::EventParseError;
     12 pub use crate::parsed::{RadrootsParsedData, RadrootsParsedEvent};
     13 
     14 pub mod event_ref {
     15     pub use crate::event_ref::*;
     16 }
     17 
     18 pub mod parsed {
     19     pub use crate::parsed::*;
     20 }
     21 
     22 pub mod wire {
     23     pub use crate::wire::*;
     24 }
     25 
     26 macro_rules! decode_domain {
     27     ($name:ident, $source:path) => {
     28         pub mod $name {
     29             pub use $source::*;
     30         }
     31     };
     32 }
     33 
     34 decode_domain!(app_data, crate::app_data::decode);
     35 decode_domain!(article, crate::article::decode);
     36 decode_domain!(calendar, crate::calendar::decode);
     37 #[cfg(feature = "json")]
     38 decode_domain!(coop, crate::coop::decode);
     39 #[cfg(feature = "json")]
     40 decode_domain!(document, crate::document::decode);
     41 #[cfg(feature = "json")]
     42 decode_domain!(farm_crdt, crate::farm_crdt::decode);
     43 decode_domain!(farm_file, crate::farm_file::decode);
     44 #[cfg(feature = "json")]
     45 decode_domain!(farm_workspace, crate::farm_workspace::decode);
     46 decode_domain!(file_metadata, crate::file_metadata::decode);
     47 decode_domain!(follow, crate::follow::decode);
     48 decode_domain!(geochat, crate::geochat::decode);
     49 decode_domain!(gift_wrap, crate::gift_wrap::decode);
     50 decode_domain!(group, crate::group::decode);
     51 decode_domain!(http_auth, crate::http_auth::decode);
     52 decode_domain!(list, crate::list::decode);
     53 decode_domain!(list_set, crate::list_set::decode);
     54 decode_domain!(message, crate::message::decode);
     55 decode_domain!(message_file, crate::message_file::decode);
     56 #[cfg(feature = "json")]
     57 decode_domain!(plot, crate::plot::decode);
     58 decode_domain!(reaction, crate::reaction::decode);
     59 decode_domain!(relay_auth, crate::relay_auth::decode);
     60 #[cfg(feature = "json")]
     61 decode_domain!(relay_document, crate::relay_document::decode);
     62 decode_domain!(report, crate::report::decode);
     63 decode_domain!(repost, crate::repost::decode);
     64 #[cfg(feature = "json")]
     65 decode_domain!(resource_area, crate::resource_area::decode);
     66 #[cfg(feature = "json")]
     67 decode_domain!(resource_cap, crate::resource_cap::decode);
     68 decode_domain!(seal, crate::seal::decode);
     69 
     70 pub mod comment {
     71     pub use crate::comment::inbound::*;
     72 }
     73 
     74 pub mod deletion {
     75     pub use crate::deletion::inbound::*;
     76     pub use crate::deletion::reconciliation_v1::inbound as reconciliation_v1;
     77 }
     78 
     79 #[cfg(feature = "json")]
     80 pub mod farm {
     81     pub use crate::farm::decode::*;
     82 }
     83 
     84 pub mod food_availability {
     85     pub use crate::food_availability::inbound::*;
     86 }
     87 
     88 pub mod job {
     89     pub use crate::job::error::*;
     90     pub use crate::job::traits::*;
     91     pub use crate::job::util::*;
     92 
     93     pub mod feedback {
     94         pub use crate::job::feedback::decode::*;
     95     }
     96     pub mod request {
     97         pub use crate::job::request::decode::*;
     98     }
     99     pub mod result {
    100         pub use crate::job::result::decode::*;
    101     }
    102 }
    103 
    104 #[cfg(feature = "knowledge")]
    105 pub mod knowledge {
    106     pub use crate::knowledge::decode::*;
    107 }
    108 
    109 pub mod operational_listing {
    110     pub use crate::operational_listing::decode::*;
    111 }
    112 
    113 #[cfg(feature = "json")]
    114 pub mod order {
    115     pub use crate::order::decode::*;
    116 }
    117 
    118 pub mod post {
    119     pub use crate::post::inbound::*;
    120 }
    121 
    122 #[cfg(feature = "json")]
    123 pub mod profile {
    124     pub use crate::profile::inbound::*;
    125 }
    126 
    127 pub mod reply {
    128     pub use crate::reply::inbound::*;
    129 }
    130 
    131 #[cfg(feature = "json")]
    132 pub mod rhi {
    133     pub use crate::rhi::{
    134         RadrootsRhiEvidenceAttestationError, RadrootsRhiEvidenceAttestationOutcomeV1,
    135         RadrootsRhiEvidenceAttestationSupersessionV1, RadrootsRhiEvidenceAttestationV1,
    136         rhi_evidence_attestation_from_event, rhi_evidence_attestation_from_verified_event,
    137         validate_rhi_evidence_attestation_supersession, validate_rhi_evidence_attestation_tags,
    138     };
    139 }
    140 
    141 #[cfg(feature = "json")]
    142 pub mod trade {
    143     pub use crate::trade::{
    144         RadrootsTradeMutationError, trade_mutation_from_event, trade_mutation_from_verified_event,
    145         validate_trade_mutation_tags,
    146     };
    147 }
    148 
    149 #[cfg(feature = "json")]
    150 use radroots_event::{
    151     SignedEvent,
    152     admission::RawEvent,
    153     wire::{DEFAULT_RAW_JSON_MAX_BYTES, Nip01EventWire},
    154 };
    155 
    156 /// Maximum compact NIP-01 event JSON size accepted by [`event`].
    157 #[cfg(feature = "json")]
    158 pub const MAX_EVENT_JSON_BYTES: usize = DEFAULT_RAW_JSON_MAX_BYTES;
    159 
    160 /// A failure while decoding an untrusted event representation.
    161 #[non_exhaustive]
    162 #[derive(Clone, Debug, PartialEq, Eq)]
    163 pub enum DecodeError {
    164     /// The encoded event exceeds the public input budget.
    165     InputTooLarge { max: usize, actual: usize },
    166     /// The input is not a structurally valid event JSON object.
    167     InvalidJson,
    168     /// A decoded field violates the native event-envelope contract.
    169     InvalidEnvelope(EventEnvelopeError),
    170     /// A decoded field violates the bounded NIP-01 wire contract.
    171     InvalidWire(EventWireError),
    172 }
    173 
    174 impl DecodeError {
    175     /// Returns a stable machine-readable error code.
    176     #[must_use]
    177     pub const fn code(&self) -> &'static str {
    178         match self {
    179             Self::InputTooLarge { .. } => "input_too_large",
    180             Self::InvalidJson => "invalid_json",
    181             Self::InvalidEnvelope(_) => "invalid_envelope",
    182             Self::InvalidWire(_) => "invalid_wire",
    183         }
    184     }
    185 }
    186 
    187 impl fmt::Display for DecodeError {
    188     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    189         match self {
    190             Self::InputTooLarge { max, actual } => {
    191                 write!(formatter, "event JSON size {actual} exceeds {max} bytes")
    192             }
    193             Self::InvalidJson => formatter.write_str("event JSON is invalid"),
    194             Self::InvalidEnvelope(error) => write!(formatter, "event envelope is invalid: {error}"),
    195             Self::InvalidWire(error) => write!(formatter, "event wire is invalid: {error}"),
    196         }
    197     }
    198 }
    199 
    200 #[cfg(feature = "std")]
    201 impl std::error::Error for DecodeError {
    202     fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
    203         match self {
    204             Self::InvalidEnvelope(error) => Some(error),
    205             Self::InvalidWire(error) => Some(error),
    206             Self::InputTooLarge { .. } | Self::InvalidJson => None,
    207         }
    208     }
    209 }
    210 
    211 /// Decodes compact NIP-01 event JSON without hiding later validation stages.
    212 ///
    213 /// The returned [`RawEvent`] must still pass [`crate::verify::id`],
    214 /// [`crate::verify::signature`], and [`crate::verify::contract`] before it is
    215 /// treated as verified or contract-valid.
    216 ///
    217 /// ```no_run
    218 /// # #[cfg(feature = "json")]
    219 /// # {
    220 /// let raw_json = r#"{"id":"00","pubkey":"00","created_at":0,"kind":1,"tags":[],"content":"","sig":"00"}"#;
    221 /// let raw = radroots_event_codec::decode::event(raw_json)?;
    222 /// let id_verified = radroots_event_codec::verify::id(raw)?;
    223 /// # let _ = id_verified;
    224 /// # }
    225 /// # Ok::<(), Box<dyn std::error::Error>>(())
    226 /// ```
    227 #[cfg(feature = "json")]
    228 pub fn event(raw_json: &str) -> Result<RawEvent, DecodeError> {
    229     let max = MAX_EVENT_JSON_BYTES;
    230     let actual = raw_json.len();
    231     if actual > max {
    232         return Err(DecodeError::InputTooLarge { max, actual });
    233     }
    234 
    235     let wire = Nip01EventWire::parse_json_unverified(raw_json).map_err(map_wire_error)?;
    236     let envelope = wire
    237         .into_unverified_envelope()
    238         .map_err(DecodeError::InvalidEnvelope)?;
    239     Ok(RawEvent::new(envelope))
    240 }
    241 
    242 /// Decodes compact NIP-01 JSON into an ID-verified signed event.
    243 #[cfg(feature = "json")]
    244 pub fn signed_event(raw_json: &str) -> Result<SignedEvent, DecodeError> {
    245     let max = MAX_EVENT_JSON_BYTES;
    246     let actual = raw_json.len();
    247     if actual > max {
    248         return Err(DecodeError::InputTooLarge { max, actual });
    249     }
    250     let wire = Nip01EventWire::parse_json(raw_json).map_err(map_wire_error)?;
    251     SignedEvent::from_wire_verified_id(wire, raw_json).map_err(|error| match error {
    252         radroots_event::draft::SignedEventError::Envelope(error) => {
    253             DecodeError::InvalidEnvelope(error)
    254         }
    255         radroots_event::draft::SignedEventError::Wire(error)
    256         | radroots_event::draft::SignedEventError::RawJson(error) => map_wire_error(error),
    257         radroots_event::draft::SignedEventError::RawJsonMismatch => DecodeError::InvalidJson,
    258     })
    259 }
    260 
    261 #[cfg(feature = "json")]
    262 fn map_wire_error(error: EventWireError) -> DecodeError {
    263     match error {
    264         EventWireError::RawJsonTooLarge { max, actual } => {
    265             DecodeError::InputTooLarge { max, actual }
    266         }
    267         EventWireError::Json(_)
    268         | EventWireError::RootNotObject
    269         | EventWireError::MissingField(_)
    270         | EventWireError::InvalidField(_) => DecodeError::InvalidJson,
    271         EventWireError::Envelope(error) => DecodeError::InvalidEnvelope(error),
    272         error => DecodeError::InvalidWire(error),
    273     }
    274 }