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 }