hashtree_core/
nhash.rs

1//! Bech32-encoded identifiers for hashtree content
2//!
3//! Similar to nostr's nip19 (npub, nprofile, nevent, naddr),
4//! provides human-readable, copy-pasteable identifiers.
5//!
6//! Types:
7//! - nhash: Permalink (hash + optional path + optional decrypt key)
8//! - nref: Live reference (pubkey + tree + optional path + optional decrypt key)
9
10use crate::types::Hash;
11use thiserror::Error;
12
13/// TLV type constants
14mod tlv {
15    /// 32-byte hash (required for nhash)
16    pub const HASH: u8 = 0;
17    /// 32-byte nostr pubkey (required for nref)
18    pub const PUBKEY: u8 = 2;
19    /// UTF-8 tree name (required for nref)
20    pub const TREE_NAME: u8 = 3;
21    /// UTF-8 path segment (can appear multiple times, in order)
22    pub const PATH: u8 = 4;
23    /// 32-byte decryption key (optional)
24    pub const DECRYPT_KEY: u8 = 5;
25}
26
27/// Errors for nhash/npath encoding/decoding
28#[derive(Debug, Error)]
29pub enum NHashError {
30    #[error("Bech32 error: {0}")]
31    Bech32(String),
32    #[error("Invalid prefix: expected {expected}, got {got}")]
33    InvalidPrefix { expected: String, got: String },
34    #[error("Invalid hash length: expected 32 bytes, got {0}")]
35    InvalidHashLength(usize),
36    #[error("Invalid key length: expected 32 bytes, got {0}")]
37    InvalidKeyLength(usize),
38    #[error("Invalid pubkey length: expected 32 bytes, got {0}")]
39    InvalidPubkeyLength(usize),
40    #[error("Missing required field: {0}")]
41    MissingField(String),
42    #[error("TLV error: {0}")]
43    TlvError(String),
44    #[error("UTF-8 error: {0}")]
45    Utf8Error(#[from] std::string::FromUtf8Error),
46    #[error("Hex error: {0}")]
47    HexError(#[from] hex::FromHexError),
48}
49
50/// NHash data - permalink to content by hash
51#[derive(Debug, Clone, PartialEq)]
52pub struct NHashData {
53    /// 32-byte merkle hash
54    pub hash: Hash,
55    /// Path segments (optional, e.g., ["folder", "file.txt"])
56    pub path: Vec<String>,
57    /// 32-byte decryption key (optional)
58    pub decrypt_key: Option<[u8; 32]>,
59}
60
61/// NRef data - live reference via pubkey + tree + path
62#[derive(Debug, Clone, PartialEq)]
63pub struct NRefData {
64    /// 32-byte nostr pubkey
65    pub pubkey: [u8; 32],
66    /// Tree name (e.g., "home", "photos")
67    pub tree_name: String,
68    /// Path segments within the tree (optional, e.g., ["folder", "file.txt"])
69    pub path: Vec<String>,
70    /// 32-byte decryption key (optional)
71    pub decrypt_key: Option<[u8; 32]>,
72}
73
74/// Decode result
75#[derive(Debug, Clone, PartialEq)]
76pub enum DecodeResult {
77    NHash(NHashData),
78    NRef(NRefData),
79}
80
81/// Parse TLV-encoded data into a map of type -> values
82fn parse_tlv(data: &[u8]) -> Result<std::collections::HashMap<u8, Vec<Vec<u8>>>, NHashError> {
83    let mut result: std::collections::HashMap<u8, Vec<Vec<u8>>> = std::collections::HashMap::new();
84    let mut offset = 0;
85
86    while offset < data.len() {
87        if offset + 2 > data.len() {
88            return Err(NHashError::TlvError("unexpected end of data".into()));
89        }
90        let t = data[offset];
91        let l = data[offset + 1] as usize;
92        offset += 2;
93
94        if offset + l > data.len() {
95            return Err(NHashError::TlvError(format!(
96                "not enough data for type {}, need {} bytes",
97                t, l
98            )));
99        }
100        let v = data[offset..offset + l].to_vec();
101        offset += l;
102
103        result.entry(t).or_default().push(v);
104    }
105
106    Ok(result)
107}
108
109/// Encode TLV data to bytes
110fn encode_tlv(tlv: &std::collections::HashMap<u8, Vec<Vec<u8>>>) -> Result<Vec<u8>, NHashError> {
111    let mut entries: Vec<u8> = Vec::new();
112
113    // Process in ascending key order for consistent encoding
114    let mut keys: Vec<u8> = tlv.keys().copied().collect();
115    keys.sort();
116
117    for t in keys {
118        if let Some(values) = tlv.get(&t) {
119            for v in values {
120                if v.len() > 255 {
121                    return Err(NHashError::TlvError(format!(
122                        "value too long for type {}: {} bytes",
123                        t,
124                        v.len()
125                    )));
126                }
127                entries.push(t);
128                entries.push(v.len() as u8);
129                entries.extend_from_slice(v);
130            }
131        }
132    }
133
134    Ok(entries)
135}
136
137/// Encode bech32 with given prefix and data
138fn encode_bech32(hrp: &str, data: &[u8]) -> Result<String, NHashError> {
139    use bech32::{Bech32m, Hrp};
140
141    let hrp = Hrp::parse(hrp).map_err(|e| NHashError::Bech32(e.to_string()))?;
142    bech32::encode::<Bech32m>(hrp, data)
143        .map_err(|e| NHashError::Bech32(e.to_string()))
144}
145
146/// Decode bech32 and return (hrp, data)
147fn decode_bech32(s: &str) -> Result<(String, Vec<u8>), NHashError> {
148    let (hrp, data) = bech32::decode(s)
149        .map_err(|e| NHashError::Bech32(e.to_string()))?;
150
151    Ok((hrp.to_string(), data))
152}
153
154// ============================================================================
155// nhash - Permalink (hash + optional path + optional decrypt key)
156// ============================================================================
157
158/// Encode an nhash permalink from just a hash
159pub fn nhash_encode(hash: &Hash) -> Result<String, NHashError> {
160    encode_bech32("nhash", hash)
161}
162
163/// Encode an nhash permalink with optional path and decrypt key
164pub fn nhash_encode_full(data: &NHashData) -> Result<String, NHashError> {
165    // No path or decrypt key - simple encoding (just the hash bytes)
166    if data.path.is_empty() && data.decrypt_key.is_none() {
167        return encode_bech32("nhash", &data.hash);
168    }
169
170    // Has path or decrypt key - use TLV
171    let mut tlv: std::collections::HashMap<u8, Vec<Vec<u8>>> = std::collections::HashMap::new();
172    tlv.insert(tlv::HASH, vec![data.hash.to_vec()]);
173
174    if !data.path.is_empty() {
175        tlv.insert(
176            tlv::PATH,
177            data.path.iter().map(|p| p.as_bytes().to_vec()).collect(),
178        );
179    }
180
181    if let Some(key) = &data.decrypt_key {
182        tlv.insert(tlv::DECRYPT_KEY, vec![key.to_vec()]);
183    }
184
185    encode_bech32("nhash", &encode_tlv(&tlv)?)
186}
187
188/// Decode an nhash string
189pub fn nhash_decode(code: &str) -> Result<NHashData, NHashError> {
190    // Strip optional prefix
191    let code = code.strip_prefix("hashtree:").unwrap_or(code);
192
193    let (prefix, data) = decode_bech32(code)?;
194
195    if prefix != "nhash" {
196        return Err(NHashError::InvalidPrefix {
197            expected: "nhash".into(),
198            got: prefix,
199        });
200    }
201
202    // Simple 32-byte hash (no TLV)
203    if data.len() == 32 {
204        let mut hash = [0u8; 32];
205        hash.copy_from_slice(&data);
206        return Ok(NHashData {
207            hash,
208            path: Vec::new(),
209            decrypt_key: None,
210        });
211    }
212
213    // Parse TLV
214    let tlv = parse_tlv(&data)?;
215
216    let hash_bytes = tlv
217        .get(&tlv::HASH)
218        .and_then(|v| v.first())
219        .ok_or_else(|| NHashError::MissingField("hash".into()))?;
220
221    if hash_bytes.len() != 32 {
222        return Err(NHashError::InvalidHashLength(hash_bytes.len()));
223    }
224
225    let mut hash = [0u8; 32];
226    hash.copy_from_slice(hash_bytes);
227
228    // Path segments
229    let path = if let Some(paths) = tlv.get(&tlv::PATH) {
230        paths
231            .iter()
232            .map(|p| String::from_utf8(p.clone()))
233            .collect::<Result<Vec<_>, _>>()?
234    } else {
235        Vec::new()
236    };
237
238    let decrypt_key = if let Some(keys) = tlv.get(&tlv::DECRYPT_KEY) {
239        if let Some(key_bytes) = keys.first() {
240            if key_bytes.len() != 32 {
241                return Err(NHashError::InvalidKeyLength(key_bytes.len()));
242            }
243            let mut key = [0u8; 32];
244            key.copy_from_slice(key_bytes);
245            Some(key)
246        } else {
247            None
248        }
249    } else {
250        None
251    };
252
253    Ok(NHashData { hash, path, decrypt_key })
254}
255
256// ============================================================================
257// nref - Live reference (pubkey + tree + optional path + optional decrypt key)
258// ============================================================================
259
260/// Encode an nref live reference
261pub fn nref_encode(data: &NRefData) -> Result<String, NHashError> {
262    let mut tlv: std::collections::HashMap<u8, Vec<Vec<u8>>> = std::collections::HashMap::new();
263
264    tlv.insert(tlv::PUBKEY, vec![data.pubkey.to_vec()]);
265    tlv.insert(tlv::TREE_NAME, vec![data.tree_name.as_bytes().to_vec()]);
266
267    if !data.path.is_empty() {
268        tlv.insert(
269            tlv::PATH,
270            data.path.iter().map(|p| p.as_bytes().to_vec()).collect(),
271        );
272    }
273
274    if let Some(key) = &data.decrypt_key {
275        tlv.insert(tlv::DECRYPT_KEY, vec![key.to_vec()]);
276    }
277
278    encode_bech32("nref", &encode_tlv(&tlv)?)
279}
280
281/// Decode an nref string
282pub fn nref_decode(code: &str) -> Result<NRefData, NHashError> {
283    // Strip optional prefix
284    let code = code.strip_prefix("hashtree:").unwrap_or(code);
285
286    let (prefix, data) = decode_bech32(code)?;
287
288    if prefix != "nref" {
289        return Err(NHashError::InvalidPrefix {
290            expected: "nref".into(),
291            got: prefix,
292        });
293    }
294
295    let tlv = parse_tlv(&data)?;
296
297    // Pubkey
298    let pubkey_bytes = tlv
299        .get(&tlv::PUBKEY)
300        .and_then(|v| v.first())
301        .ok_or_else(|| NHashError::MissingField("pubkey".into()))?;
302
303    if pubkey_bytes.len() != 32 {
304        return Err(NHashError::InvalidPubkeyLength(pubkey_bytes.len()));
305    }
306
307    let mut pubkey = [0u8; 32];
308    pubkey.copy_from_slice(pubkey_bytes);
309
310    // Tree name
311    let tree_name_bytes = tlv
312        .get(&tlv::TREE_NAME)
313        .and_then(|v| v.first())
314        .ok_or_else(|| NHashError::MissingField("tree_name".into()))?;
315
316    let tree_name = String::from_utf8(tree_name_bytes.clone())?;
317
318    // Path segments
319    let path = if let Some(paths) = tlv.get(&tlv::PATH) {
320        paths
321            .iter()
322            .map(|p| String::from_utf8(p.clone()))
323            .collect::<Result<Vec<_>, _>>()?
324    } else {
325        Vec::new()
326    };
327
328    // Decrypt key
329    let decrypt_key = if let Some(keys) = tlv.get(&tlv::DECRYPT_KEY) {
330        if let Some(key_bytes) = keys.first() {
331            if key_bytes.len() != 32 {
332                return Err(NHashError::InvalidKeyLength(key_bytes.len()));
333            }
334            let mut key = [0u8; 32];
335            key.copy_from_slice(key_bytes);
336            Some(key)
337        } else {
338            None
339        }
340    } else {
341        None
342    };
343
344    Ok(NRefData {
345        pubkey,
346        tree_name,
347        path,
348        decrypt_key,
349    })
350}
351
352// ============================================================================
353// Generic decode
354// ============================================================================
355
356/// Decode any nhash or nref string
357pub fn decode(code: &str) -> Result<DecodeResult, NHashError> {
358    let code = code.strip_prefix("hashtree:").unwrap_or(code);
359
360    if code.starts_with("nhash1") {
361        return Ok(DecodeResult::NHash(nhash_decode(code)?));
362    }
363    if code.starts_with("nref1") {
364        return Ok(DecodeResult::NRef(nref_decode(code)?));
365    }
366
367    Err(NHashError::InvalidPrefix {
368        expected: "nhash1 or nref1".into(),
369        got: code.chars().take(10).collect(),
370    })
371}
372
373// ============================================================================
374// Type guards
375// ============================================================================
376
377/// Check if a string is an nhash
378pub fn is_nhash(value: &str) -> bool {
379    value.starts_with("nhash1")
380}
381
382/// Check if a string is an nref
383pub fn is_nref(value: &str) -> bool {
384    value.starts_with("nref1")
385}
386
387#[cfg(test)]
388mod tests {
389    use super::*;
390
391    #[test]
392    fn test_nhash_simple() {
393        let hash: Hash = [
394            0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
395            0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10,
396            0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18,
397            0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20,
398        ];
399
400        let encoded = nhash_encode(&hash).unwrap();
401        assert!(encoded.starts_with("nhash1"));
402
403        let decoded = nhash_decode(&encoded).unwrap();
404        assert_eq!(decoded.hash, hash);
405        assert!(decoded.path.is_empty());
406        assert!(decoded.decrypt_key.is_none());
407    }
408
409    #[test]
410    fn test_nhash_with_path() {
411        let hash: Hash = [0xaa; 32];
412
413        let data = NHashData {
414            hash,
415            path: vec!["folder".into(), "file.txt".into()],
416            decrypt_key: None,
417        };
418
419        let encoded = nhash_encode_full(&data).unwrap();
420        assert!(encoded.starts_with("nhash1"));
421
422        let decoded = nhash_decode(&encoded).unwrap();
423        assert_eq!(decoded.hash, hash);
424        assert_eq!(decoded.path, vec!["folder", "file.txt"]);
425        assert!(decoded.decrypt_key.is_none());
426    }
427
428    #[test]
429    fn test_nhash_with_key() {
430        let hash: Hash = [0xaa; 32];
431        let key: [u8; 32] = [0xbb; 32];
432
433        let data = NHashData {
434            hash,
435            path: vec![],
436            decrypt_key: Some(key),
437        };
438
439        let encoded = nhash_encode_full(&data).unwrap();
440        assert!(encoded.starts_with("nhash1"));
441
442        let decoded = nhash_decode(&encoded).unwrap();
443        assert_eq!(decoded.hash, hash);
444        assert!(decoded.path.is_empty());
445        assert_eq!(decoded.decrypt_key, Some(key));
446    }
447
448    #[test]
449    fn test_nhash_with_path_and_key() {
450        let hash: Hash = [0xaa; 32];
451        let key: [u8; 32] = [0xbb; 32];
452
453        let data = NHashData {
454            hash,
455            path: vec!["docs".into()],
456            decrypt_key: Some(key),
457        };
458
459        let encoded = nhash_encode_full(&data).unwrap();
460        let decoded = nhash_decode(&encoded).unwrap();
461        assert_eq!(decoded.hash, hash);
462        assert_eq!(decoded.path, vec!["docs"]);
463        assert_eq!(decoded.decrypt_key, Some(key));
464    }
465
466    #[test]
467    fn test_nref_simple() {
468        let pubkey: [u8; 32] = [0xcc; 32];
469        let data = NRefData {
470            pubkey,
471            tree_name: "home".into(),
472            path: vec![],
473            decrypt_key: None,
474        };
475
476        let encoded = nref_encode(&data).unwrap();
477        assert!(encoded.starts_with("nref1"));
478
479        let decoded = nref_decode(&encoded).unwrap();
480        assert_eq!(decoded.pubkey, pubkey);
481        assert_eq!(decoded.tree_name, "home");
482        assert!(decoded.path.is_empty());
483        assert!(decoded.decrypt_key.is_none());
484    }
485
486    #[test]
487    fn test_nref_with_path_and_key() {
488        let pubkey: [u8; 32] = [0xdd; 32];
489        let key: [u8; 32] = [0xee; 32];
490
491        let data = NRefData {
492            pubkey,
493            tree_name: "photos".into(),
494            path: vec!["vacation".into(), "beach.jpg".into()],
495            decrypt_key: Some(key),
496        };
497
498        let encoded = nref_encode(&data).unwrap();
499        assert!(encoded.starts_with("nref1"));
500
501        let decoded = nref_decode(&encoded).unwrap();
502        assert_eq!(decoded.pubkey, pubkey);
503        assert_eq!(decoded.tree_name, "photos");
504        assert_eq!(decoded.path, vec!["vacation", "beach.jpg"]);
505        assert_eq!(decoded.decrypt_key, Some(key));
506    }
507
508    #[test]
509    fn test_decode_generic() {
510        let hash: Hash = [0x11; 32];
511        let nhash = nhash_encode(&hash).unwrap();
512
513        match decode(&nhash).unwrap() {
514            DecodeResult::NHash(data) => assert_eq!(data.hash, hash),
515            _ => panic!("expected NHash"),
516        }
517
518        let pubkey: [u8; 32] = [0x22; 32];
519        let nref_data = NRefData {
520            pubkey,
521            tree_name: "test".into(),
522            path: vec![],
523            decrypt_key: None,
524        };
525        let nref = nref_encode(&nref_data).unwrap();
526
527        match decode(&nref).unwrap() {
528            DecodeResult::NRef(data) => {
529                assert_eq!(data.pubkey, pubkey);
530                assert_eq!(data.tree_name, "test");
531            }
532            _ => panic!("expected NRef"),
533        }
534    }
535
536    #[test]
537    fn test_is_nhash() {
538        assert!(is_nhash("nhash1abc"));
539        assert!(!is_nhash("nref1abc"));
540        assert!(!is_nhash("npub1abc"));
541    }
542
543    #[test]
544    fn test_is_nref() {
545        assert!(is_nref("nref1abc"));
546        assert!(!is_nref("nhash1abc"));
547        assert!(!is_nref("npub1abc"));
548    }
549}