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 }