lib

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

field_helpers.rs (10787B)


      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 optional_tag_value(
    188     tags: &[Vec<String>],
    189     key: &'static str,
    190 ) -> Result<Option<String>, EventParseError> {
    191     let Some(tag) = tags
    192         .iter()
    193         .find(|tag| tag.first().map(|value| value.as_str()) == Some(key))
    194     else {
    195         return Ok(None);
    196     };
    197     let value = tag
    198         .get(1)
    199         .map(ToString::to_string)
    200         .ok_or(EventParseError::InvalidTag(key))?;
    201     validate_non_empty_tag_value(&value, key)?;
    202     Ok(Some(value))
    203 }
    204 
    205 pub(crate) fn tag_values(
    206     tags: &[Vec<String>],
    207     key: &'static str,
    208 ) -> Result<Vec<String>, EventParseError> {
    209     tags.iter()
    210         .filter(|tag| tag.first().map(|value| value.as_str()) == Some(key))
    211         .map(|tag| {
    212             tag.get(1)
    213                 .map(ToString::to_string)
    214                 .ok_or(EventParseError::InvalidTag(key))
    215                 .and_then(|value| {
    216                     validate_non_empty_tag_value(&value, key)?;
    217                     Ok(value)
    218                 })
    219         })
    220         .collect()
    221 }
    222 
    223 pub(crate) fn require_empty_content(
    224     content: &str,
    225     field: &'static str,
    226 ) -> Result<(), EventParseError> {
    227     if content.is_empty() {
    228         Ok(())
    229     } else {
    230         Err(EventParseError::InvalidJson(field))
    231     }
    232 }
    233 
    234 #[cfg(test)]
    235 mod tests {
    236     use super::*;
    237 
    238     const VALID_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAAAA";
    239     const VALID_HASH: &str = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
    240 
    241     #[test]
    242     fn address_string_formats_valid_radroots_address() {
    243         let address = address_string(30078, "workspace_pubkey", VALID_D_TAG, "workspace")
    244             .expect("valid address");
    245 
    246         assert_eq!(address, "30078:workspace_pubkey:AAAAAAAAAAAAAAAAAAAAAA");
    247     }
    248 
    249     #[test]
    250     fn address_string_rejects_empty_pubkey_and_bad_d_tag() {
    251         assert!(matches!(
    252             address_string(30078, "", VALID_D_TAG, "workspace"),
    253             Err(EventEncodeError::EmptyRequiredField("workspace"))
    254         ));
    255         assert!(matches!(
    256             address_string(30078, "workspace_pubkey", "bad", "workspace"),
    257             Err(EventEncodeError::InvalidField("workspace"))
    258         ));
    259     }
    260 
    261     #[test]
    262     fn address_parser_accepts_valid_radroots_address() {
    263         let address = parse_address_tag("30078:workspace_pubkey:AAAAAAAAAAAAAAAAAAAAAA", "a")
    264             .expect("valid address");
    265 
    266         assert_eq!(address.kind, 30078);
    267         assert_eq!(address.pubkey, "workspace_pubkey");
    268         assert_eq!(address.d_tag, VALID_D_TAG);
    269     }
    270 
    271     #[test]
    272     fn address_parser_rejects_invalid_radroots_addresses() {
    273         assert!(matches!(
    274             parse_address_tag("30078:workspace_pubkey", "a"),
    275             Err(EventParseError::InvalidTag("a"))
    276         ));
    277         assert!(matches!(
    278             parse_address_tag("30078::AAAAAAAAAAAAAAAAAAAAAA", "a"),
    279             Err(EventParseError::InvalidTag("a"))
    280         ));
    281         assert!(matches!(
    282             parse_address_tag("bad:workspace_pubkey:AAAAAAAAAAAAAAAAAAAAAA", "a"),
    283             Err(EventParseError::InvalidNumber("a", _))
    284         ));
    285         assert!(matches!(
    286             parse_address_tag("30078:workspace_pubkey:bad", "a"),
    287             Err(EventParseError::InvalidTag("a"))
    288         ));
    289         assert!(matches!(
    290             parse_address_tag_with_kind("78:workspace_pubkey:AAAAAAAAAAAAAAAAAAAAAA", 30078, "a"),
    291             Err(EventParseError::InvalidTag("a"))
    292         ));
    293     }
    294 
    295     #[test]
    296     fn lowercase_hex_hash_validation_accepts_only_sha256_shape() {
    297         assert!(is_lowercase_hex_64(VALID_HASH));
    298         assert!(!is_lowercase_hex_64(
    299             "0123456789ABCDEF0123456789abcdef0123456789abcdef0123456789abcdef"
    300         ));
    301         assert!(!is_lowercase_hex_64(
    302             "0123456789xyzdef0123456789abcdef0123456789abcdef0123456789abcdef"
    303         ));
    304         assert!(!is_lowercase_hex_64("0123456789abcdef"));
    305         assert!(matches!(
    306             validate_lowercase_hex_64("0123456789abcdef", "payload"),
    307             Err(EventEncodeError::InvalidField("payload"))
    308         ));
    309     }
    310 
    311     #[test]
    312     fn lowercase_hex_tag_validation_maps_to_parse_error() {
    313         assert!(validate_lowercase_hex_64_tag(VALID_HASH, "x").is_ok());
    314         assert!(matches!(
    315             validate_lowercase_hex_64_tag("0123456789abcdef", "x"),
    316             Err(EventParseError::InvalidTag("x"))
    317         ));
    318     }
    319 
    320     #[test]
    321     fn base64url_validation_accepts_non_empty_unpadded_payloads() {
    322         assert!(is_non_empty_base64url("abc-DEF_012"));
    323         assert!(!is_non_empty_base64url(""));
    324         assert!(!is_non_empty_base64url("abc="));
    325         assert!(!is_non_empty_base64url("abc/def"));
    326         assert!(matches!(
    327             validate_non_empty_base64url("abc=", "encoded_change"),
    328             Err(EventEncodeError::InvalidField("encoded_change"))
    329         ));
    330     }
    331 
    332     #[test]
    333     fn tag_helpers_parse_required_optional_and_repeated_values() {
    334         let tags = vec![
    335             vec!["h".to_string(), "group".to_string()],
    336             vec!["t".to_string(), "radroots:farm:crdt".to_string()],
    337             vec!["t".to_string(), "task".to_string()],
    338         ];
    339 
    340         assert_eq!(required_tag_value(&tags, "h").unwrap(), "group");
    341         assert_eq!(optional_tag_value(&tags, "missing").unwrap(), None);
    342         assert_eq!(
    343             tag_values(&tags, "t").unwrap(),
    344             vec!["radroots:farm:crdt".to_string(), "task".to_string()]
    345         );
    346     }
    347 
    348     #[test]
    349     fn tag_helpers_build_simple_and_repeated_tags() {
    350         let mut tags = Vec::new();
    351 
    352         push_tag(&mut tags, "h", "group");
    353         push_optional_tag(&mut tags, "p", Some("pubkey"));
    354         push_optional_tag(&mut tags, "p", Some(""));
    355         push_tag_values(&mut tags, "roles", ["member", "admin"]);
    356 
    357         assert_eq!(
    358             tags,
    359             vec![
    360                 vec!["h".to_string(), "group".to_string()],
    361                 vec!["p".to_string(), "pubkey".to_string()],
    362                 vec![
    363                     "roles".to_string(),
    364                     "member".to_string(),
    365                     "admin".to_string()
    366                 ],
    367             ]
    368         );
    369     }
    370 }