reference.rs (5057B)
1 use nostr::EventId; 2 use nostr::nips::nip19::{FromBech32, Nip19, ToBech32}; 3 4 const MAX_REFERENCE_BYTES: usize = 2_048; 5 6 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 7 pub enum NostrReferenceKind { 8 EventId, 9 PublicKey, 10 Profile, 11 Note, 12 NostrEvent, 13 Address, 14 PrivateKeyRejected, 15 Invalid, 16 } 17 18 #[derive(Clone, Debug, Eq, PartialEq)] 19 pub struct NostrReferenceParse { 20 pub classification: NostrReferenceKind, 21 pub canonical_reference: Option<String>, 22 } 23 24 #[must_use] 25 pub fn classify_reference(raw: &str) -> NostrReferenceParse { 26 if raw.is_empty() 27 || raw.len() > MAX_REFERENCE_BYTES 28 || raw.chars().any(char::is_control) 29 || raw.trim() != raw 30 { 31 return invalid(); 32 } 33 let value = raw.strip_prefix("nostr:").unwrap_or(raw); 34 if value.is_empty() || value.starts_with("nostr:") { 35 return invalid(); 36 } 37 if value.len() == 64 && value.bytes().all(|byte| byte.is_ascii_hexdigit()) { 38 return EventId::from_hex(value).map_or_else( 39 |_| invalid(), 40 |event_id| valid(NostrReferenceKind::EventId, event_id.to_hex()), 41 ); 42 } 43 match Nip19::from_bech32(value) { 44 Ok(Nip19::Secret(_)) => NostrReferenceParse { 45 classification: NostrReferenceKind::PrivateKeyRejected, 46 canonical_reference: None, 47 }, 48 Ok(reference @ Nip19::Pubkey(_)) => canonical(NostrReferenceKind::PublicKey, reference), 49 Ok(reference @ Nip19::Profile(_)) => canonical(NostrReferenceKind::Profile, reference), 50 Ok(reference @ Nip19::EventId(_)) => canonical(NostrReferenceKind::Note, reference), 51 Ok(reference @ Nip19::Event(_)) => canonical(NostrReferenceKind::NostrEvent, reference), 52 Ok(reference @ Nip19::Coordinate(_)) => canonical(NostrReferenceKind::Address, reference), 53 Err(_) => invalid(), 54 } 55 } 56 57 fn canonical(kind: NostrReferenceKind, reference: Nip19) -> NostrReferenceParse { 58 reference 59 .to_bech32() 60 .map_or_else(|_| invalid(), |value| valid(kind, value)) 61 } 62 63 fn valid(kind: NostrReferenceKind, canonical_reference: String) -> NostrReferenceParse { 64 NostrReferenceParse { 65 classification: kind, 66 canonical_reference: Some(canonical_reference), 67 } 68 } 69 70 fn invalid() -> NostrReferenceParse { 71 NostrReferenceParse { 72 classification: NostrReferenceKind::Invalid, 73 canonical_reference: None, 74 } 75 } 76 77 #[cfg(test)] 78 mod tests { 79 use super::{NostrReferenceKind, classify_reference}; 80 81 const NPUB: &str = "npub14f8usejl26twx0dhuxjh9cas7keav9vr0v8nvtwtrjqx3vycc76qqh9nsy"; 82 const NSEC: &str = "nsec1j4c6269y9w0q2er2xjw8sv2ehyrtfxq3jwgdlxj6qfn8z4gjsq5qfvfk99"; 83 const NOTE: &str = "note1m99r7nwc0wdrkzldrqan96gklg5usqspq7z9696j6unf0ljnpxjspqfw99"; 84 const NPROFILE: &str = "nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gppemhxue69uhhytnc9e3k7mf0qyt8wumn8ghj7er2vfshxtnnv9jxkc3wvdhk6tclr7lsh"; 85 const NEVENT: &str = "nevent1qqsdhet4232flykq3048jzc9msmaa3hnxuesxy3lnc33vd0wt9xwk6szyqewrqnkx4zsaweutf739s0cu7et29zrntqs5elw70vlm8zudr3y24sqsgy"; 86 const NADDR: &str = "naddr1qqxnzd3exgersv33xymnsve3qgs8suecw4luyht9ekff89x4uacneapk8r5dyk0gmn6uwwurf6u9rusrqsqqqa282m3gxt"; 87 88 #[test] 89 fn classifies_canonical_public_references_and_exact_event_hex() { 90 for (value, expected) in [ 91 (NPUB, NostrReferenceKind::PublicKey), 92 (NPROFILE, NostrReferenceKind::Profile), 93 (NOTE, NostrReferenceKind::Note), 94 (NEVENT, NostrReferenceKind::NostrEvent), 95 (NADDR, NostrReferenceKind::Address), 96 ( 97 "d94a3f4dd87b9a3b0bed183b32e916fa29c8020107845d1752d72697fe5309a5", 98 NostrReferenceKind::EventId, 99 ), 100 ] { 101 assert_eq!(classify_reference(value).classification, expected); 102 assert_eq!( 103 classify_reference(&format!("nostr:{value}")).classification, 104 expected 105 ); 106 assert!(classify_reference(value).canonical_reference.is_some()); 107 } 108 } 109 110 #[test] 111 fn separates_private_keys_and_rejects_noncanonical_or_unbounded_input() { 112 assert_eq!( 113 classify_reference(NSEC).classification, 114 NostrReferenceKind::PrivateKeyRejected 115 ); 116 assert_eq!( 117 classify_reference(&format!("nostr:{NSEC}")).classification, 118 NostrReferenceKind::PrivateKeyRejected 119 ); 120 assert!(classify_reference(NSEC).canonical_reference.is_none()); 121 for invalid in [ 122 "", 123 " note1m99r7nwc0wdrkzldrqan96gklg5usqspq7z9696j6unf0ljnpxjspqfw99", 124 "nostr:nostr:bad", 125 "note1qqqqqq", 126 "00", 127 "npub1bad\n", 128 ] { 129 assert_eq!( 130 classify_reference(invalid).classification, 131 NostrReferenceKind::Invalid 132 ); 133 } 134 assert_eq!( 135 classify_reference(&"a".repeat(2_049)).classification, 136 NostrReferenceKind::Invalid 137 ); 138 } 139 }