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 }