lib

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

ids.rs (19844B)


      1 #![forbid(unsafe_code)]
      2 
      3 #[cfg(not(feature = "std"))]
      4 use alloc::{string::String, string::ToString, vec::Vec};
      5 
      6 #[cfg(feature = "std")]
      7 use std::{string::String, vec::Vec};
      8 
      9 use core::{borrow::Borrow, fmt, ops::Deref, str::FromStr};
     10 
     11 #[derive(Clone, Debug, PartialEq, Eq)]
     12 pub enum RadrootsIdParseError {
     13     Empty,
     14     InvalidFormat,
     15     InvalidLength { expected: usize, actual: usize },
     16     InvalidCharacter,
     17     TooLong { max: usize, actual: usize },
     18 }
     19 
     20 impl fmt::Display for RadrootsIdParseError {
     21     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
     22         match self {
     23             Self::Empty => write!(f, "identifier is empty"),
     24             Self::InvalidFormat => write!(f, "identifier has invalid format"),
     25             Self::InvalidLength { expected, actual } => {
     26                 write!(
     27                     f,
     28                     "identifier length {actual} does not match required length {expected}"
     29                 )
     30             }
     31             Self::InvalidCharacter => write!(f, "identifier contains an invalid character"),
     32             Self::TooLong { max, actual } => {
     33                 write!(f, "identifier length {actual} exceeds maximum length {max}")
     34             }
     35         }
     36     }
     37 }
     38 
     39 #[cfg(feature = "std")]
     40 impl std::error::Error for RadrootsIdParseError {}
     41 
     42 macro_rules! validated_string_id {
     43     ($name:ident, $validator:ident) => {
     44         #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
     45         pub struct $name(String);
     46 
     47         impl $name {
     48             pub fn parse(value: impl AsRef<str>) -> Result<Self, RadrootsIdParseError> {
     49                 $validator(value.as_ref()).map(Self)
     50             }
     51 
     52             #[inline]
     53             pub fn as_str(&self) -> &str {
     54                 self.0.as_str()
     55             }
     56 
     57             #[inline]
     58             pub fn into_string(self) -> String {
     59                 self.0
     60             }
     61         }
     62 
     63         impl AsRef<str> for $name {
     64             #[inline]
     65             fn as_ref(&self) -> &str {
     66                 self.as_str()
     67             }
     68         }
     69 
     70         impl Deref for $name {
     71             type Target = str;
     72 
     73             #[inline]
     74             fn deref(&self) -> &Self::Target {
     75                 self.as_str()
     76             }
     77         }
     78 
     79         impl Borrow<str> for $name {
     80             #[inline]
     81             fn borrow(&self) -> &str {
     82                 self.as_str()
     83             }
     84         }
     85 
     86         impl From<$name> for String {
     87             #[inline]
     88             fn from(value: $name) -> Self {
     89                 value.into_string()
     90             }
     91         }
     92 
     93         impl PartialEq<&str> for $name {
     94             #[inline]
     95             fn eq(&self, other: &&str) -> bool {
     96                 self.as_str() == *other
     97             }
     98         }
     99 
    100         impl PartialEq<$name> for &str {
    101             #[inline]
    102             fn eq(&self, other: &$name) -> bool {
    103                 *self == other.as_str()
    104             }
    105         }
    106 
    107         impl PartialEq<String> for $name {
    108             #[inline]
    109             fn eq(&self, other: &String) -> bool {
    110                 self.as_str() == other.as_str()
    111             }
    112         }
    113 
    114         impl PartialEq<$name> for String {
    115             #[inline]
    116             fn eq(&self, other: &$name) -> bool {
    117                 self.as_str() == other.as_str()
    118             }
    119         }
    120 
    121         impl fmt::Display for $name {
    122             fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
    123                 f.write_str(self.as_str())
    124             }
    125         }
    126 
    127         impl FromStr for $name {
    128             type Err = RadrootsIdParseError;
    129 
    130             fn from_str(value: &str) -> Result<Self, Self::Err> {
    131                 Self::parse(value)
    132             }
    133         }
    134 
    135         impl TryFrom<&str> for $name {
    136             type Error = RadrootsIdParseError;
    137 
    138             fn try_from(value: &str) -> Result<Self, Self::Error> {
    139                 Self::parse(value)
    140             }
    141         }
    142 
    143         impl TryFrom<String> for $name {
    144             type Error = RadrootsIdParseError;
    145 
    146             fn try_from(value: String) -> Result<Self, Self::Error> {
    147                 Self::parse(value)
    148             }
    149         }
    150 
    151         #[cfg(feature = "serde")]
    152         impl serde::Serialize for $name {
    153             fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    154             where
    155                 S: serde::Serializer,
    156             {
    157                 serializer.serialize_str(self.as_str())
    158             }
    159         }
    160 
    161         #[cfg(feature = "serde")]
    162         impl<'de> serde::Deserialize<'de> for $name {
    163             fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    164             where
    165                 D: serde::Deserializer<'de>,
    166             {
    167                 let value = String::deserialize(deserializer)?;
    168                 Self::parse(value).map_err(serde::de::Error::custom)
    169             }
    170         }
    171     };
    172 }
    173 
    174 validated_string_id!(RadrootsPublicKey, validate_hex_64);
    175 validated_string_id!(RadrootsEventId, validate_hex_64);
    176 validated_string_id!(RadrootsEventSignature, validate_hex_128);
    177 validated_string_id!(RadrootsDTag, validate_d_tag);
    178 validated_string_id!(
    179     RadrootsAddressableCoordinate,
    180     validate_addressable_coordinate
    181 );
    182 validated_string_id!(RadrootsListingAddress, validate_addressable_coordinate);
    183 validated_string_id!(RadrootsOrderId, validate_commercial_id);
    184 validated_string_id!(RadrootsOrderRevisionId, validate_commercial_id);
    185 validated_string_id!(RadrootsOrderQuoteId, validate_commercial_id);
    186 validated_string_id!(RadrootsInventoryBinId, validate_commercial_id);
    187 validated_string_id!(RadrootsEconomicsDigest, validate_economics_digest);
    188 validated_string_id!(RadrootsEventPointer, validate_hex_64);
    189 
    190 #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
    191 pub struct RadrootsAddressableCoordinateParts {
    192     pub kind: u32,
    193     pub pubkey: RadrootsPublicKey,
    194     pub d_tag: RadrootsDTag,
    195 }
    196 
    197 impl RadrootsAddressableCoordinateParts {
    198     pub fn parse(value: impl AsRef<str>) -> Result<Self, RadrootsIdParseError> {
    199         parse_addressable_coordinate_parts(value.as_ref())
    200     }
    201 }
    202 
    203 #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
    204 pub struct RadrootsNostrEventPointer {
    205     pub event_id: RadrootsEventId,
    206     pub relays: Vec<String>,
    207 }
    208 
    209 impl RadrootsNostrEventPointer {
    210     pub fn new<I, S>(event_id: RadrootsEventId, relays: I) -> Result<Self, RadrootsIdParseError>
    211     where
    212         I: IntoIterator<Item = S>,
    213         S: Into<String>,
    214     {
    215         let mut canonical_relays = Vec::new();
    216         for relay in relays {
    217             let relay = relay.into();
    218             if relay.is_empty()
    219                 || relay.trim() != relay
    220                 || relay.chars().any(|character| character.is_control())
    221             {
    222                 return Err(RadrootsIdParseError::InvalidCharacter);
    223             }
    224             canonical_relays.push(relay);
    225         }
    226         Ok(Self {
    227             event_id,
    228             relays: canonical_relays,
    229         })
    230     }
    231 }
    232 
    233 fn validate_hex_64(value: &str) -> Result<String, RadrootsIdParseError> {
    234     validate_hex(value, 64)
    235 }
    236 
    237 fn validate_hex_128(value: &str) -> Result<String, RadrootsIdParseError> {
    238     validate_hex(value, 128)
    239 }
    240 
    241 fn validate_hex(value: &str, expected_len: usize) -> Result<String, RadrootsIdParseError> {
    242     if value.len() != expected_len {
    243         return Err(RadrootsIdParseError::InvalidLength {
    244             expected: expected_len,
    245             actual: value.len(),
    246         });
    247     }
    248 
    249     let mut canonical = String::with_capacity(expected_len);
    250     for byte in value.bytes() {
    251         match byte {
    252             b'0'..=b'9' => canonical.push(byte as char),
    253             b'a'..=b'f' => canonical.push(byte as char),
    254             b'A'..=b'F' => canonical.push((byte + 32) as char),
    255             _ => return Err(RadrootsIdParseError::InvalidCharacter),
    256         }
    257     }
    258     Ok(canonical)
    259 }
    260 
    261 fn validate_d_tag(value: &str) -> Result<String, RadrootsIdParseError> {
    262     validate_visible_token(value, 512)
    263 }
    264 
    265 fn validate_commercial_id(value: &str) -> Result<String, RadrootsIdParseError> {
    266     validate_visible_token(value, 128)
    267 }
    268 
    269 fn validate_economics_digest(value: &str) -> Result<String, RadrootsIdParseError> {
    270     if let Some(hex) = value.strip_prefix("sha256:") {
    271         validate_hex(hex, 64)?;
    272         return Ok(value.to_string());
    273     }
    274     validate_visible_token(value, 128)
    275 }
    276 
    277 fn validate_addressable_coordinate(value: &str) -> Result<String, RadrootsIdParseError> {
    278     parse_addressable_coordinate_parts(value)?;
    279     Ok(value.to_string())
    280 }
    281 
    282 fn parse_addressable_coordinate_parts(
    283     value: &str,
    284 ) -> Result<RadrootsAddressableCoordinateParts, RadrootsIdParseError> {
    285     let (kind, remainder) = value
    286         .split_once(':')
    287         .ok_or(RadrootsIdParseError::InvalidFormat)?;
    288     let (pubkey, d_tag) = remainder
    289         .split_once(':')
    290         .ok_or(RadrootsIdParseError::InvalidFormat)?;
    291     let kind = kind
    292         .parse::<u32>()
    293         .map_err(|_| RadrootsIdParseError::InvalidFormat)?;
    294     let pubkey = RadrootsPublicKey::parse(pubkey)?;
    295     let d_tag = RadrootsDTag::parse(d_tag)?;
    296     Ok(RadrootsAddressableCoordinateParts {
    297         kind,
    298         pubkey,
    299         d_tag,
    300     })
    301 }
    302 
    303 fn validate_visible_token(value: &str, max_len: usize) -> Result<String, RadrootsIdParseError> {
    304     if value.is_empty() {
    305         return Err(RadrootsIdParseError::Empty);
    306     }
    307     if value.len() > max_len {
    308         return Err(RadrootsIdParseError::TooLong {
    309             max: max_len,
    310             actual: value.len(),
    311         });
    312     }
    313     if value.trim() != value
    314         || value
    315             .chars()
    316             .any(|character| character.is_control() || character.is_whitespace())
    317     {
    318         return Err(RadrootsIdParseError::InvalidCharacter);
    319     }
    320     Ok(value.to_string())
    321 }
    322 
    323 #[cfg(test)]
    324 mod tests {
    325     use super::*;
    326 
    327     macro_rules! assert_identifier_impls {
    328         ($ty:ty, $value:expr) => {{
    329             let value = $value.to_owned();
    330             let value = value.as_str();
    331             let id = <$ty>::parse(value).expect("parse");
    332 
    333             assert_eq!(id.as_str(), value);
    334             assert_eq!(id.as_ref(), value);
    335             assert_eq!(&*id, value);
    336             assert_eq!(<$ty as core::borrow::Borrow<str>>::borrow(&id), value);
    337             assert_eq!(id.to_string(), value);
    338             assert_eq!(
    339                 <$ty as core::str::FromStr>::from_str(value).expect("from str"),
    340                 id
    341             );
    342             assert_eq!(
    343                 <$ty as TryFrom<&str>>::try_from(value).expect("try from str"),
    344                 id
    345             );
    346             assert_eq!(
    347                 <$ty as TryFrom<String>>::try_from(value.to_owned()).expect("try from string"),
    348                 id
    349             );
    350             assert_eq!(id, value);
    351             assert_eq!(value, id);
    352             assert_eq!(id, value.to_owned());
    353             assert_eq!(value.to_owned(), id);
    354 
    355             let id = <$ty>::parse(value).expect("parse");
    356             let converted: String = String::from(id.clone());
    357             assert_eq!(converted, value);
    358             assert_eq!(id.into_string(), value.to_owned());
    359 
    360             #[cfg(feature = "serde")]
    361             {
    362                 let id = <$ty>::parse(value).expect("parse");
    363                 let encoded = serde_json::to_string(&id).expect("serialize");
    364                 let decoded: $ty = serde_json::from_str(&encoded).expect("deserialize");
    365                 assert_eq!(decoded.as_str(), value);
    366             }
    367         }};
    368     }
    369 
    370     fn hex_64(character: char) -> String {
    371         core::iter::repeat_n(character, 64).collect()
    372     }
    373 
    374     fn hex_128(character: char) -> String {
    375         core::iter::repeat_n(character, 128).collect()
    376     }
    377 
    378     #[test]
    379     fn public_keys_and_event_ids_require_64_hex_chars() {
    380         let upper = "A".repeat(64);
    381         let public_key = RadrootsPublicKey::parse(&upper).expect("public key");
    382         assert_eq!(public_key.as_str(), "a".repeat(64));
    383 
    384         let event_id = RadrootsEventId::parse(hex_64('f')).expect("event id");
    385         assert_eq!(event_id.as_str(), hex_64('f'));
    386         assert_eq!(
    387             RadrootsEventId::parse(" ".repeat(64)).unwrap_err(),
    388             RadrootsIdParseError::InvalidCharacter
    389         );
    390         assert_eq!(
    391             RadrootsEventId::parse("a".repeat(63)).unwrap_err(),
    392             RadrootsIdParseError::InvalidLength {
    393                 expected: 64,
    394                 actual: 63
    395             }
    396         );
    397     }
    398 
    399     #[test]
    400     fn id_parse_errors_have_stable_display_messages() {
    401         let errors = [
    402             RadrootsIdParseError::Empty,
    403             RadrootsIdParseError::InvalidFormat,
    404             RadrootsIdParseError::InvalidLength {
    405                 expected: 64,
    406                 actual: 7,
    407             },
    408             RadrootsIdParseError::InvalidCharacter,
    409             RadrootsIdParseError::TooLong {
    410                 max: 128,
    411                 actual: 129,
    412             },
    413         ];
    414 
    415         for error in errors {
    416             assert!(!error.to_string().is_empty());
    417         }
    418     }
    419 
    420     #[test]
    421     fn signatures_require_128_hex_chars() {
    422         let signature = RadrootsEventSignature::parse(hex_128('B')).expect("signature");
    423         assert_eq!(signature.as_str(), "b".repeat(128));
    424         assert_eq!(
    425             RadrootsEventSignature::parse(hex_64('b')).unwrap_err(),
    426             RadrootsIdParseError::InvalidLength {
    427                 expected: 128,
    428                 actual: 64
    429             }
    430         );
    431     }
    432 
    433     #[test]
    434     fn d_tags_reject_empty_control_and_whitespace() {
    435         assert_eq!(
    436             RadrootsDTag::parse("").unwrap_err(),
    437             RadrootsIdParseError::Empty
    438         );
    439         assert_eq!(
    440             RadrootsDTag::parse(" listing").unwrap_err(),
    441             RadrootsIdParseError::InvalidCharacter
    442         );
    443         assert_eq!(
    444             RadrootsDTag::parse("listing\none").unwrap_err(),
    445             RadrootsIdParseError::InvalidCharacter
    446         );
    447         assert_eq!(
    448             RadrootsDTag::parse("farm:farm-1:members")
    449                 .expect("d tag")
    450                 .as_str(),
    451             "farm:farm-1:members"
    452         );
    453     }
    454 
    455     #[test]
    456     fn addressable_coordinates_validate_kind_pubkey_and_d_tag() {
    457         let addr = format!("30402:{}:listing-1", hex_64('0'));
    458         assert_eq!(
    459             RadrootsAddressableCoordinate::parse(&addr)
    460                 .expect("coordinate")
    461                 .as_str(),
    462             addr
    463         );
    464         assert_eq!(
    465             RadrootsListingAddress::parse("30402:not_hex:listing-1").unwrap_err(),
    466             RadrootsIdParseError::InvalidLength {
    467                 expected: 64,
    468                 actual: 7
    469             }
    470         );
    471         assert_eq!(
    472             RadrootsAddressableCoordinate::parse("30402").unwrap_err(),
    473             RadrootsIdParseError::InvalidFormat
    474         );
    475         assert_eq!(
    476             RadrootsAddressableCoordinate::parse(format!("bad:{}:listing-1", hex_64('a')))
    477                 .unwrap_err(),
    478             RadrootsIdParseError::InvalidFormat
    479         );
    480         assert_eq!(
    481             RadrootsAddressableCoordinate::parse(format!("30402:{}:bad d", hex_64('0')))
    482                 .unwrap_err(),
    483             RadrootsIdParseError::InvalidCharacter
    484         );
    485     }
    486 
    487     #[test]
    488     fn addressable_coordinate_parts_parse_kind_pubkey_and_d_tag() {
    489         let addr = format!("30402:{}:farm:farm-1:members", hex_64('A'));
    490         let parts = RadrootsAddressableCoordinateParts::parse(&addr).expect("coordinate parts");
    491         assert_eq!(parts.kind, 30402);
    492         assert_eq!(parts.pubkey.as_str(), hex_64('a'));
    493         assert_eq!(parts.d_tag.as_str(), "farm:farm-1:members");
    494     }
    495 
    496     #[test]
    497     fn commercial_ids_reject_empty_whitespace_control_and_long_values() {
    498         assert_eq!(
    499             RadrootsOrderId::parse("order-1")
    500                 .expect("order id")
    501                 .as_str(),
    502             "order-1"
    503         );
    504         assert_eq!(
    505             RadrootsOrderRevisionId::parse("rev 1").unwrap_err(),
    506             RadrootsIdParseError::InvalidCharacter
    507         );
    508         assert_eq!(
    509             RadrootsInventoryBinId::parse("a".repeat(129)).unwrap_err(),
    510             RadrootsIdParseError::TooLong {
    511                 max: 128,
    512                 actual: 129
    513             }
    514         );
    515     }
    516 
    517     #[test]
    518     fn economics_digest_accepts_sha256_and_existing_wire_tokens() {
    519         let digest = format!("sha256:{}", hex_64('c'));
    520         assert_eq!(
    521             RadrootsEconomicsDigest::parse(&digest)
    522                 .expect("digest")
    523                 .as_str(),
    524             digest
    525         );
    526         assert_eq!(
    527             RadrootsEconomicsDigest::parse("digest-1")
    528                 .expect("wire v1 digest")
    529                 .as_str(),
    530             "digest-1"
    531         );
    532         assert_eq!(
    533             RadrootsEconomicsDigest::parse("sha256:not-hex").unwrap_err(),
    534             RadrootsIdParseError::InvalidLength {
    535                 expected: 64,
    536                 actual: 7
    537             }
    538         );
    539     }
    540 
    541     #[test]
    542     fn validated_types_do_not_offer_infallible_string_conversion() {
    543         let id = RadrootsOrderQuoteId::try_from(String::from("quote-1")).expect("quote id");
    544         assert_eq!(id.as_ref(), "quote-1");
    545         let parsed: RadrootsEventPointer = hex_64('d').parse().expect("event pointer");
    546         assert_eq!(parsed.as_str(), hex_64('d'));
    547     }
    548 
    549     #[test]
    550     fn validated_identifier_wrappers_expose_consistent_traits() {
    551         let addressable = format!("30402:{}:listing-1", hex_64('0'));
    552 
    553         assert_identifier_impls!(RadrootsPublicKey, hex_64('a').as_str());
    554         assert_identifier_impls!(RadrootsEventId, hex_64('b').as_str());
    555         assert_identifier_impls!(RadrootsEventSignature, hex_128('c').as_str());
    556         assert_identifier_impls!(RadrootsDTag, "listing-1");
    557         assert_identifier_impls!(RadrootsAddressableCoordinate, addressable.as_str());
    558         assert_identifier_impls!(RadrootsListingAddress, addressable.as_str());
    559         assert_identifier_impls!(RadrootsOrderId, "order-1");
    560         assert_identifier_impls!(RadrootsOrderRevisionId, "revision-1");
    561         assert_identifier_impls!(RadrootsOrderQuoteId, "quote-1");
    562         assert_identifier_impls!(RadrootsInventoryBinId, "bin-1");
    563         assert_identifier_impls!(RadrootsEconomicsDigest, "digest-1");
    564         assert_identifier_impls!(RadrootsEventPointer, hex_64('d').as_str());
    565     }
    566 
    567     #[test]
    568     fn nostr_event_pointers_validate_relay_values() {
    569         let event_id = RadrootsEventId::parse(hex_64('e')).expect("event id");
    570         let pointer = RadrootsNostrEventPointer::new(
    571             event_id.clone(),
    572             ["wss://relay.one.example", "wss://relay.two.example"],
    573         )
    574         .expect("pointer");
    575 
    576         assert_eq!(pointer.event_id, event_id);
    577         assert_eq!(
    578             pointer.relays,
    579             vec![
    580                 "wss://relay.one.example".to_owned(),
    581                 "wss://relay.two.example".to_owned()
    582             ]
    583         );
    584 
    585         for relay in [
    586             "",
    587             " wss://relay.example",
    588             "wss://relay.example\n",
    589             "wss://relay.example/\u{7}",
    590         ] {
    591             assert_eq!(
    592                 RadrootsNostrEventPointer::new(
    593                     RadrootsEventId::parse(hex_64('e')).expect("event id"),
    594                     [relay],
    595                 )
    596                 .unwrap_err(),
    597                 RadrootsIdParseError::InvalidCharacter
    598             );
    599         }
    600     }
    601 
    602     #[cfg(feature = "serde")]
    603     #[test]
    604     fn serde_deserialization_validates_identifiers() {
    605         let encoded = format!("\"{}\"", hex_64('E'));
    606         let event_id: RadrootsEventId = serde_json::from_str(&encoded).expect("event id");
    607         assert_eq!(event_id.as_str(), hex_64('e'));
    608 
    609         let invalid = serde_json::from_str::<RadrootsOrderId>("\"bad id\"");
    610         assert!(invalid.is_err());
    611         assert_eq!(
    612             serde_json::to_string(&event_id).expect("json"),
    613             format!("\"{}\"", hex_64('e'))
    614         );
    615     }
    616 }