app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

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 }