lib

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

field_helpers.rs (11763B)


      1 #[cfg(not(feature = "std"))]
      2 use alloc::{
      3     format,
      4     string::{String, ToString},
      5     vec,
      6     vec::Vec,
      7 };
      8 
      9 use crate::d_tag::validate_d_tag;
     10 use crate::d_tag::validate_d_tag_tag;
     11 use crate::error::EventEncodeError;
     12 use crate::error::EventParseError;
     13 
     14 #[derive(Clone, Debug, PartialEq, Eq)]
     15 pub(crate) struct RadrootsAddress {
     16     pub kind: u32,
     17     pub pubkey: String,
     18     pub d_tag: String,
     19 }
     20 
     21 pub(crate) fn address_string(
     22     kind: u32,
     23     pubkey: &str,
     24     d_tag: &str,
     25     field: &'static str,
     26 ) -> Result<String, EventEncodeError> {
     27     validate_non_empty_field(pubkey, field)?;
     28     validate_d_tag(d_tag, field)?;
     29     Ok(format!("{kind}:{pubkey}:{d_tag}"))
     30 }
     31 
     32 pub(crate) fn parse_address_tag(
     33     value: &str,
     34     tag: &'static str,
     35 ) -> Result<RadrootsAddress, EventParseError> {
     36     let mut parts = value.split(':');
     37     let kind = parts
     38         .next()
     39         .ok_or(EventParseError::InvalidTag(tag))?
     40         .parse::<u32>()
     41         .map_err(|err| EventParseError::InvalidNumber(tag, err))?;
     42     let pubkey = parts
     43         .next()
     44         .map(ToString::to_string)
     45         .ok_or(EventParseError::InvalidTag(tag))?;
     46     let d_tag = parts
     47         .next()
     48         .map(ToString::to_string)
     49         .ok_or(EventParseError::InvalidTag(tag))?;
     50     if parts.next().is_some() {
     51         return Err(EventParseError::InvalidTag(tag));
     52     }
     53     validate_non_empty_tag_value(&pubkey, tag)?;
     54     validate_d_tag_tag(&d_tag, tag)?;
     55     Ok(RadrootsAddress {
     56         kind,
     57         pubkey,
     58         d_tag,
     59     })
     60 }
     61 
     62 pub(crate) fn parse_address_tag_with_kind(
     63     value: &str,
     64     expected_kind: u32,
     65     tag: &'static str,
     66 ) -> Result<RadrootsAddress, EventParseError> {
     67     let address = parse_address_tag(value, tag)?;
     68     if address.kind != expected_kind {
     69         return Err(EventParseError::InvalidTag(tag));
     70     }
     71     Ok(address)
     72 }
     73 
     74 pub(crate) fn is_lowercase_hex_64(value: &str) -> bool {
     75     value.len() == 64
     76         && value
     77             .as_bytes()
     78             .iter()
     79             .all(|byte| matches!(byte, b'0'..=b'9' | b'a'..=b'f'))
     80 }
     81 
     82 pub(crate) fn validate_lowercase_hex_64(
     83     value: &str,
     84     field: &'static str,
     85 ) -> Result<(), EventEncodeError> {
     86     if is_lowercase_hex_64(value) {
     87         Ok(())
     88     } else {
     89         Err(EventEncodeError::InvalidField(field))
     90     }
     91 }
     92 
     93 pub(crate) fn validate_lowercase_hex_64_tag(
     94     value: &str,
     95     tag: &'static str,
     96 ) -> Result<(), EventParseError> {
     97     if is_lowercase_hex_64(value) {
     98         Ok(())
     99     } else {
    100         Err(EventParseError::InvalidTag(tag))
    101     }
    102 }
    103 
    104 pub(crate) fn is_non_empty_base64url(value: &str) -> bool {
    105     !value.is_empty()
    106         && value.as_bytes().iter().all(|byte| {
    107             matches!(
    108                 byte,
    109                 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_'
    110             )
    111         })
    112 }
    113 
    114 pub(crate) fn validate_non_empty_base64url(
    115     value: &str,
    116     field: &'static str,
    117 ) -> Result<(), EventEncodeError> {
    118     if is_non_empty_base64url(value) {
    119         Ok(())
    120     } else {
    121         Err(EventEncodeError::InvalidField(field))
    122     }
    123 }
    124 
    125 pub(crate) fn validate_non_empty_field(
    126     value: &str,
    127     field: &'static str,
    128 ) -> Result<(), EventEncodeError> {
    129     if value.trim().is_empty() {
    130         Err(EventEncodeError::EmptyRequiredField(field))
    131     } else {
    132         Ok(())
    133     }
    134 }
    135 
    136 pub(crate) fn validate_non_empty_tag_value(
    137     value: &str,
    138     tag: &'static str,
    139 ) -> Result<(), EventParseError> {
    140     if value.trim().is_empty() {
    141         Err(EventParseError::InvalidTag(tag))
    142     } else {
    143         Ok(())
    144     }
    145 }
    146 
    147 pub(crate) fn push_tag(tags: &mut Vec<Vec<String>>, key: &str, value: impl Into<String>) {
    148     tags.push(vec![key.to_string(), value.into()]);
    149 }
    150 
    151 pub(crate) fn push_optional_tag(tags: &mut Vec<Vec<String>>, key: &str, value: Option<&str>) {
    152     if let Some(value) = value
    153         && !value.trim().is_empty()
    154     {
    155         push_tag(tags, key, value);
    156     }
    157 }
    158 
    159 pub(crate) fn push_tag_values<I, S>(tags: &mut Vec<Vec<String>>, key: &str, values: I)
    160 where
    161     I: IntoIterator<Item = S>,
    162     S: Into<String>,
    163 {
    164     let mut tag = vec![key.to_string()];
    165     tag.extend(values.into_iter().map(Into::into));
    166     tags.push(tag);
    167 }
    168 
    169 pub(crate) fn required_tag_value(
    170     tags: &[Vec<String>],
    171     key: &'static str,
    172 ) -> Result<String, EventParseError> {
    173     tags.iter()
    174         .find(|tag| tag.first().map(|value| value.as_str()) == Some(key))
    175         .ok_or(EventParseError::MissingTag(key))
    176         .and_then(|tag| {
    177             tag.get(1)
    178                 .map(ToString::to_string)
    179                 .ok_or(EventParseError::InvalidTag(key))
    180         })
    181         .and_then(|value| {
    182             validate_non_empty_tag_value(&value, key)?;
    183             Ok(value)
    184         })
    185 }
    186 
    187 pub(crate) fn required_unique_tag_value(
    188     tags: &[Vec<String>],
    189     key: &'static str,
    190 ) -> Result<String, EventParseError> {
    191     unique_tag_value(tags, key)?.ok_or(EventParseError::MissingTag(key))
    192 }
    193 
    194 pub(crate) fn optional_tag_value(
    195     tags: &[Vec<String>],
    196     key: &'static str,
    197 ) -> Result<Option<String>, EventParseError> {
    198     let Some(tag) = tags
    199         .iter()
    200         .find(|tag| tag.first().map(|value| value.as_str()) == Some(key))
    201     else {
    202         return Ok(None);
    203     };
    204     let value = tag
    205         .get(1)
    206         .map(ToString::to_string)
    207         .ok_or(EventParseError::InvalidTag(key))?;
    208     validate_non_empty_tag_value(&value, key)?;
    209     Ok(Some(value))
    210 }
    211 
    212 pub(crate) fn optional_unique_tag_value(
    213     tags: &[Vec<String>],
    214     key: &'static str,
    215 ) -> Result<Option<String>, EventParseError> {
    216     unique_tag_value(tags, key)
    217 }
    218 
    219 fn unique_tag_value(
    220     tags: &[Vec<String>],
    221     key: &'static str,
    222 ) -> Result<Option<String>, EventParseError> {
    223     let mut matches = tags
    224         .iter()
    225         .filter(|tag| tag.first().map(|value| value.as_str()) == Some(key));
    226     let Some(tag) = matches.next() else {
    227         return Ok(None);
    228     };
    229     if matches.next().is_some() {
    230         return Err(EventParseError::DuplicateTag(key));
    231     }
    232     let value = tag
    233         .get(1)
    234         .map(ToString::to_string)
    235         .ok_or(EventParseError::InvalidTag(key))?;
    236     validate_non_empty_tag_value(&value, key)?;
    237     Ok(Some(value))
    238 }
    239 
    240 pub(crate) fn tag_values(
    241     tags: &[Vec<String>],
    242     key: &'static str,
    243 ) -> Result<Vec<String>, EventParseError> {
    244     tags.iter()
    245         .filter(|tag| tag.first().map(|value| value.as_str()) == Some(key))
    246         .map(|tag| {
    247             tag.get(1)
    248                 .map(ToString::to_string)
    249                 .ok_or(EventParseError::InvalidTag(key))
    250                 .and_then(|value| {
    251                     validate_non_empty_tag_value(&value, key)?;
    252                     Ok(value)
    253                 })
    254         })
    255         .collect()
    256 }
    257 
    258 pub(crate) fn require_empty_content(
    259     content: &str,
    260     field: &'static str,
    261 ) -> Result<(), EventParseError> {
    262     if content.is_empty() {
    263         Ok(())
    264     } else {
    265         Err(EventParseError::InvalidJson(field))
    266     }
    267 }
    268 
    269 #[cfg(test)]
    270 mod tests {
    271     use super::*;
    272 
    273     const VALID_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAAAA";
    274     const VALID_HASH: &str = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
    275 
    276     #[test]
    277     fn address_string_formats_valid_radroots_address() {
    278         let address = address_string(30078, "workspace_pubkey", VALID_D_TAG, "workspace")
    279             .expect("valid address");
    280 
    281         assert_eq!(address, "30078:workspace_pubkey:AAAAAAAAAAAAAAAAAAAAAA");
    282     }
    283 
    284     #[test]
    285     fn address_string_rejects_empty_pubkey_and_bad_d_tag() {
    286         assert!(matches!(
    287             address_string(30078, "", VALID_D_TAG, "workspace"),
    288             Err(EventEncodeError::EmptyRequiredField("workspace"))
    289         ));
    290         assert!(matches!(
    291             address_string(30078, "workspace_pubkey", "bad", "workspace"),
    292             Err(EventEncodeError::InvalidField("workspace"))
    293         ));
    294     }
    295 
    296     #[test]
    297     fn address_parser_accepts_valid_radroots_address() {
    298         let address = parse_address_tag("30078:workspace_pubkey:AAAAAAAAAAAAAAAAAAAAAA", "a")
    299             .expect("valid address");
    300 
    301         assert_eq!(address.kind, 30078);
    302         assert_eq!(address.pubkey, "workspace_pubkey");
    303         assert_eq!(address.d_tag, VALID_D_TAG);
    304     }
    305 
    306     #[test]
    307     fn address_parser_rejects_invalid_radroots_addresses() {
    308         assert!(matches!(
    309             parse_address_tag("30078:workspace_pubkey", "a"),
    310             Err(EventParseError::InvalidTag("a"))
    311         ));
    312         assert!(matches!(
    313             parse_address_tag("30078::AAAAAAAAAAAAAAAAAAAAAA", "a"),
    314             Err(EventParseError::InvalidTag("a"))
    315         ));
    316         assert!(matches!(
    317             parse_address_tag("bad:workspace_pubkey:AAAAAAAAAAAAAAAAAAAAAA", "a"),
    318             Err(EventParseError::InvalidNumber("a", _))
    319         ));
    320         assert!(matches!(
    321             parse_address_tag("30078:workspace_pubkey:bad", "a"),
    322             Err(EventParseError::InvalidTag("a"))
    323         ));
    324         assert!(matches!(
    325             parse_address_tag_with_kind("78:workspace_pubkey:AAAAAAAAAAAAAAAAAAAAAA", 30078, "a"),
    326             Err(EventParseError::InvalidTag("a"))
    327         ));
    328     }
    329 
    330     #[test]
    331     fn lowercase_hex_hash_validation_accepts_only_sha256_shape() {
    332         assert!(is_lowercase_hex_64(VALID_HASH));
    333         assert!(!is_lowercase_hex_64(
    334             "0123456789ABCDEF0123456789abcdef0123456789abcdef0123456789abcdef"
    335         ));
    336         assert!(!is_lowercase_hex_64(
    337             "0123456789xyzdef0123456789abcdef0123456789abcdef0123456789abcdef"
    338         ));
    339         assert!(!is_lowercase_hex_64("0123456789abcdef"));
    340         assert!(matches!(
    341             validate_lowercase_hex_64("0123456789abcdef", "payload"),
    342             Err(EventEncodeError::InvalidField("payload"))
    343         ));
    344     }
    345 
    346     #[test]
    347     fn lowercase_hex_tag_validation_maps_to_parse_error() {
    348         assert!(validate_lowercase_hex_64_tag(VALID_HASH, "x").is_ok());
    349         assert!(matches!(
    350             validate_lowercase_hex_64_tag("0123456789abcdef", "x"),
    351             Err(EventParseError::InvalidTag("x"))
    352         ));
    353     }
    354 
    355     #[test]
    356     fn base64url_validation_accepts_non_empty_unpadded_payloads() {
    357         assert!(is_non_empty_base64url("abc-DEF_012"));
    358         assert!(!is_non_empty_base64url(""));
    359         assert!(!is_non_empty_base64url("abc="));
    360         assert!(!is_non_empty_base64url("abc/def"));
    361         assert!(matches!(
    362             validate_non_empty_base64url("abc=", "encoded_change"),
    363             Err(EventEncodeError::InvalidField("encoded_change"))
    364         ));
    365     }
    366 
    367     #[test]
    368     fn tag_helpers_parse_required_optional_and_repeated_values() {
    369         let tags = vec![
    370             vec!["h".to_string(), "group".to_string()],
    371             vec!["t".to_string(), "radroots:farm:crdt".to_string()],
    372             vec!["t".to_string(), "task".to_string()],
    373         ];
    374 
    375         assert_eq!(required_tag_value(&tags, "h").unwrap(), "group");
    376         assert_eq!(optional_tag_value(&tags, "missing").unwrap(), None);
    377         assert_eq!(
    378             tag_values(&tags, "t").unwrap(),
    379             vec!["radroots:farm:crdt".to_string(), "task".to_string()]
    380         );
    381     }
    382 
    383     #[test]
    384     fn tag_helpers_build_simple_and_repeated_tags() {
    385         let mut tags = Vec::new();
    386 
    387         push_tag(&mut tags, "h", "group");
    388         push_optional_tag(&mut tags, "p", Some("pubkey"));
    389         push_optional_tag(&mut tags, "p", Some(""));
    390         push_tag_values(&mut tags, "roles", ["member", "admin"]);
    391 
    392         assert_eq!(
    393             tags,
    394             vec![
    395                 vec!["h".to_string(), "group".to_string()],
    396                 vec!["p".to_string(), "pubkey".to_string()],
    397                 vec![
    398                     "roles".to_string(),
    399                     "member".to_string(),
    400                     "admin".to_string()
    401                 ],
    402             ]
    403         );
    404     }
    405 }