Files
sovereign_browser_rust/src/nostr_url.rs
T

210 lines
7.2 KiB
Rust

//! NIP-19 decoding and Nostr URI normalization.
//!
//! Port of `nostr_url.h` / `nostr_url.c` from the C project.
use nostr_core::NostrResult;
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NostrEntityType {
None,
Npub,
Nsec,
Note,
Nevent,
Naddr,
Nprofile,
Nrelay,
}
impl fmt::Display for NostrEntityType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
NostrEntityType::None => write!(f, "none"),
NostrEntityType::Npub => write!(f, "npub"),
NostrEntityType::Nsec => write!(f, "nsec"),
NostrEntityType::Note => write!(f, "note"),
NostrEntityType::Nevent => write!(f, "nevent"),
NostrEntityType::Naddr => write!(f, "naddr"),
NostrEntityType::Nprofile => write!(f, "nprofile"),
NostrEntityType::Nrelay => write!(f, "nrelay"),
}
}
}
#[derive(Debug, Clone)]
pub struct NostrDecodedEntity {
pub entity_type: NostrEntityType,
pub pubkey: Option<[u8; 32]>,
pub event_id: Option<[u8; 32]>,
pub kind: Option<u32>,
pub identifier: Option<String>,
pub relay_url: Option<String>,
pub relay_hints: Vec<String>,
}
impl Default for NostrDecodedEntity {
fn default() -> Self {
NostrDecodedEntity {
entity_type: NostrEntityType::None,
pubkey: None,
event_id: None,
kind: None,
identifier: None,
relay_url: None,
relay_hints: Vec::new(),
}
}
}
pub fn nostr_url_detect(input: &str) -> NostrEntityType {
if input.starts_with("npub") { NostrEntityType::Npub }
else if input.starts_with("nsec") { NostrEntityType::Nsec }
else if input.starts_with("note") { NostrEntityType::Note }
else if input.starts_with("nevent") { NostrEntityType::Nevent }
else if input.starts_with("naddr") { NostrEntityType::Naddr }
else if input.starts_with("nprofile") { NostrEntityType::Nprofile }
else if input.starts_with("nrelay") { NostrEntityType::Nrelay }
else { NostrEntityType::None }
}
pub fn nostr_url_decode(input: &str) -> NostrResult<NostrDecodedEntity> {
let entity_type = nostr_url_detect(input);
if entity_type == NostrEntityType::None {
return Err(nostr_core::NostrError::InvalidInput);
}
// bech32 0.11.1 decode returns (Hrp, Vec<u8>)
let (_hrp, bytes): (bech32::Hrp, Vec<u8>) = bech32::decode(input)
.map_err(|_| nostr_core::NostrError::InvalidInput)?;
let mut entity = NostrDecodedEntity {
entity_type,
..Default::default()
};
match entity_type {
NostrEntityType::Npub | NostrEntityType::Nsec => {
if bytes.len() == 32 {
let mut arr = [0u8; 32];
arr.copy_from_slice(&bytes);
entity.pubkey = Some(arr);
}
}
NostrEntityType::Note => {
if bytes.len() == 32 {
let mut arr = [0u8; 32];
arr.copy_from_slice(&bytes);
entity.event_id = Some(arr);
}
}
NostrEntityType::Nevent | NostrEntityType::Nprofile | NostrEntityType::Naddr => {
let mut i = 0;
while i < bytes.len() {
if i + 1 >= bytes.len() { break; }
let tlv_type = bytes[i];
let tlv_len = bytes[i + 1] as usize;
i += 2;
if i + tlv_len > bytes.len() { break; }
let tlv_value = &bytes[i..i + tlv_len];
i += tlv_len;
match tlv_type {
0 => {
if tlv_value.len() == 32 {
let mut arr = [0u8; 32];
arr.copy_from_slice(tlv_value);
if entity_type == NostrEntityType::Nevent {
entity.event_id = Some(arr);
} else {
entity.pubkey = Some(arr);
}
}
}
1 => { if let Ok(s) = String::from_utf8(tlv_value.to_vec()) { entity.relay_hints.push(s); } }
2 => { if entity_type == NostrEntityType::Naddr { if let Ok(s) = String::from_utf8(tlv_value.to_vec()) { entity.identifier = Some(s); } } }
3 => { if tlv_value.len() == 4 { let mut arr = [0u8; 4]; arr.copy_from_slice(tlv_value); entity.kind = Some(u32::from_be_bytes(arr)); } }
_ => {}
}
}
}
NostrEntityType::Nrelay => {
if let Ok(s) = String::from_utf8(bytes) { entity.relay_url = Some(s); }
}
NostrEntityType::None => {}
}
Ok(entity)
}
pub fn nostr_url_normalize(input: &str) -> String {
if let Ok(entity) = nostr_url_decode(input) {
if let Some(pk) = entity.pubkey { return hex::encode(pk); }
}
input.to_string()
}
pub fn nostr_url_entity(input: &str) -> Option<&str> {
if let Some(stripped) = input.strip_prefix("nostr://") {
Some(stripped)
} else if let Some(stripped) = input.strip_prefix("nostr:") {
Some(stripped)
} else {
Some(input)
}
}
pub fn nostr_hex_encode32(data: &[u8; 32]) -> String {
hex::encode(data)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_nostr_url_detect() {
assert_eq!(nostr_url_detect("npub1abc"), NostrEntityType::Npub);
assert_eq!(nostr_url_detect("nsec1abc"), NostrEntityType::Nsec);
assert_eq!(nostr_url_detect("note1abc"), NostrEntityType::Note);
assert_eq!(nostr_url_detect("nevent1abc"), NostrEntityType::Nevent);
assert_eq!(nostr_url_detect("naddr1abc"), NostrEntityType::Naddr);
assert_eq!(nostr_url_detect("nprofile1abc"), NostrEntityType::Nprofile);
assert_eq!(nostr_url_detect("nrelay1abc"), NostrEntityType::Nrelay);
assert_eq!(nostr_url_detect("unknown"), NostrEntityType::None);
}
#[test]
fn test_nostr_url_entity() {
assert_eq!(nostr_url_entity("nostr:npub1abc"), Some("npub1abc"));
assert_eq!(nostr_url_entity("nostr://npub1abc"), Some("npub1abc"));
assert_eq!(nostr_url_entity("npub1abc"), Some("npub1abc"));
}
#[test]
fn test_nostr_hex_encode32() {
let data = [0u8; 32];
assert_eq!(nostr_hex_encode32(&data), "0000000000000000000000000000000000000000000000000000000000000000");
let data = [0xffu8; 32];
assert_eq!(nostr_hex_encode32(&data), "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff");
}
#[test]
fn test_decode_npub() {
// Test with a known valid npub
// npub1sg6plzptd64u62a878hep2kev88swjh3tw00gjsfl8f237lmu63q0uf63m
// corresponds to pubkey: 82341f2f8f1b1e1e6c0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e
let result = nostr_url_decode("npub1sg6plzptd64u62a878hep2kev88swjh3tw00gjsfl8f237lmu63q0uf63m");
assert!(result.is_ok());
let entity = result.unwrap();
assert_eq!(entity.entity_type, NostrEntityType::Npub);
assert!(entity.pubkey.is_some());
}
#[test]
fn test_decode_invalid() {
let result = nostr_url_decode("invalid");
assert!(result.is_err());
}
}