ndn_discovery/
prefix_announce.rs

1//! Service record publisher and browser.
2//!
3//! Implements the `/ndn/local/sd/services/` naming convention for browsable
4//! prefix advertisement.  Any producer can publish a thin TLV record at a
5//! well-known name; any consumer discovers available prefixes by expressing a
6//! prefix Interest for `/ndn/local/sd/services/`.
7//!
8//! ## Naming convention
9//!
10//! ```text
11//! /ndn/local/sd/services/<prefix-hash>/<node-name>/v=<timestamp-ms>
12//! ```
13//!
14//! - `<prefix-hash>` — first 8 hex bytes of the FNV-1a hash of the announced
15//!   prefix in canonical URI form.  Allows O(1) FIB lookup when many records
16//!   are present.
17//! - `<node-name>` — the producer's NDN name components (flattened into the
18//!   hierarchical name).
19//! - `v=<timestamp-ms>` — version component using NDN naming conventions.
20//!   Consumers express `CanBePrefix=true` to fetch the latest version.
21//!
22//! ## Content TLV
23//!
24//! ```text
25//! ServiceRecord ::= ANNOUNCED-PREFIX TLV
26//!                   NODE-NAME TLV
27//!                   FRESHNESS-MS TLV?
28//!                   CAPABILITIES TLV?
29//! ```
30//!
31//! TLV type assignments (within the `0xC0–0xFF` experimental range):
32//!
33//! | Type | Name |
34//! |------|------|
35//! | 0xD0 | `ANNOUNCED-PREFIX` |
36//! | 0xD1 | `SD-NODE-NAME`     |
37//! | 0xD2 | `FRESHNESS-MS`     |
38//! | 0xD3 | `SD-CAPABILITIES`  |
39//!
40//! The `FRESHNESS-MS` field is advisory: consumers that cache service records
41//! should re-fetch after this many milliseconds even if the CS has not expired
42//! the entry.  It is separate from the NDN `FreshnessPeriod` in MetaInfo (which
43//! controls CS behaviour at the forwarder level).
44
45use bytes::Bytes;
46use ndn_packet::{Name, NameComponent, tlv_type};
47use ndn_tlv::TlvWriter;
48
49use crate::scope::sd_services;
50use crate::wire::{parse_raw_data, write_name_tlv, write_nni};
51
52// ─── TLV type constants ───────────────────────────────────────────────────────
53
54const T_ANNOUNCED_PREFIX: u32 = 0xD0;
55const T_SD_NODE_NAME: u32 = 0xD1;
56const T_FRESHNESS_MS: u32 = 0xD2;
57const T_SD_CAPABILITIES: u32 = 0xD3;
58
59// ─── ServiceRecord ────────────────────────────────────────────────────────────
60
61/// A service advertisement record.
62///
63/// Produced by a prefix producer to announce its prefix to neighbouring nodes.
64/// Consumed by any node doing prefix browsability.
65#[derive(Clone, Debug, PartialEq)]
66pub struct ServiceRecord {
67    /// The prefix being announced (e.g. `/ndn/sensor/temp`).
68    pub announced_prefix: Name,
69    /// The producer's NDN node name.
70    pub node_name: Name,
71    /// How long (ms) this record should be considered fresh.  `0` = rely on
72    /// NDN FreshnessPeriod only.
73    pub freshness_ms: u64,
74    /// Capability flags (same encoding as HelloPayload capabilities).
75    pub capabilities: u8,
76}
77
78impl ServiceRecord {
79    /// Create a minimal record with default freshness (30 s) and no flags.
80    pub fn new(announced_prefix: Name, node_name: Name) -> Self {
81        Self {
82            announced_prefix,
83            node_name,
84            freshness_ms: 30_000,
85            capabilities: 0,
86        }
87    }
88
89    /// Encode as a content TLV blob.
90    pub fn encode(&self) -> Bytes {
91        let mut w = TlvWriter::new();
92        // ANNOUNCED-PREFIX
93        let prefix_bytes = encode_name_raw(&self.announced_prefix);
94        w.write_tlv(T_ANNOUNCED_PREFIX.into(), &prefix_bytes);
95        // SD-NODE-NAME
96        let node_bytes = encode_name_raw(&self.node_name);
97        w.write_tlv(T_SD_NODE_NAME.into(), &node_bytes);
98        // FRESHNESS-MS (omit if zero)
99        if self.freshness_ms > 0 {
100            write_nni_to_writer(&mut w, T_FRESHNESS_MS, self.freshness_ms);
101        }
102        // SD-CAPABILITIES (omit if zero)
103        if self.capabilities != 0 {
104            w.write_tlv(T_SD_CAPABILITIES.into(), &[self.capabilities]);
105        }
106        w.finish()
107    }
108
109    /// Decode from a content TLV blob produced by [`encode`].
110    pub fn decode(b: &[u8]) -> Option<Self> {
111        let mut pos = 0;
112        let mut announced_prefix: Option<Name> = None;
113        let mut node_name: Option<Name> = None;
114        let mut freshness_ms = 0u64;
115        let mut capabilities = 0u8;
116
117        while pos < b.len() {
118            let (typ, len, header_len) = read_tlv_header(b, pos)?;
119            let val_start = pos + header_len;
120            let val_end = val_start + len;
121            if val_end > b.len() {
122                return None;
123            }
124            let val = &b[val_start..val_end];
125            match typ {
126                T_ANNOUNCED_PREFIX => {
127                    announced_prefix = Some(decode_name_raw(val)?);
128                }
129                T_SD_NODE_NAME => {
130                    node_name = Some(decode_name_raw(val)?);
131                }
132                T_FRESHNESS_MS => {
133                    freshness_ms = read_nni(val)?;
134                }
135                T_SD_CAPABILITIES => {
136                    capabilities = *val.first()?;
137                }
138                _ => {} // forward-compatible: ignore unknown fields
139            }
140            pos = val_end;
141        }
142
143        Some(Self {
144            announced_prefix: announced_prefix?,
145            node_name: node_name?,
146            freshness_ms,
147            capabilities,
148        })
149    }
150
151    /// Construct the NDN name for this record.
152    ///
153    /// `timestamp_ms` should be a monotonically increasing value (e.g.
154    /// milliseconds since Unix epoch) so that `CanBePrefix=true` Interests
155    /// retrieve the freshest record.
156    pub fn make_name(&self, timestamp_ms: u64) -> Name {
157        make_record_name(&self.announced_prefix, &self.node_name, timestamp_ms)
158    }
159
160    /// Build a complete NDN Data packet for this record.
161    ///
162    /// The Data name follows the naming convention above; the Content is the
163    /// encoded `ServiceRecord`.  `FreshnessPeriod` is set to `freshness_ms`.
164    pub fn build_data(&self, timestamp_ms: u64) -> Bytes {
165        let name = self.make_name(timestamp_ms);
166        let content = self.encode();
167        let freshness_period = if self.freshness_ms > 0 {
168            self.freshness_ms
169        } else {
170            30_000
171        };
172
173        let mut w = TlvWriter::new();
174        w.write_nested(tlv_type::DATA, |w: &mut TlvWriter| {
175            write_name_tlv(w, &name);
176            w.write_nested(tlv_type::META_INFO, |w: &mut TlvWriter| {
177                write_nni(w, tlv_type::FRESHNESS_PERIOD, freshness_period);
178            });
179            w.write_tlv(tlv_type::CONTENT, &content);
180            w.write_nested(tlv_type::SIGNATURE_INFO, |w: &mut TlvWriter| {
181                w.write_tlv(tlv_type::SIGNATURE_TYPE, &[0u8]);
182            });
183            w.write_tlv(tlv_type::SIGNATURE_VALUE, &[0u8; 32]);
184        });
185        w.finish()
186    }
187
188    /// Extract and decode the `ServiceRecord` from a raw NDN Data packet.
189    ///
190    /// Returns `None` if the packet is not a service record Data or the content
191    /// cannot be decoded.
192    pub fn from_data_packet(raw: &Bytes) -> Option<Self> {
193        let parsed = parse_raw_data(raw)?;
194        if !parsed.name.has_prefix(sd_services()) {
195            return None;
196        }
197        let content = parsed.content?;
198        Self::decode(&content)
199    }
200}
201
202// ─── Name construction ────────────────────────────────────────────────────────
203
204/// Construct the full Data name for a service record.
205///
206/// `/ndn/local/sd/services/<prefix-hash>/<node-name...>/v=<timestamp-ms>`
207pub fn make_record_name(announced_prefix: &Name, node_name: &Name, timestamp_ms: u64) -> Name {
208    let hash = fnv1a_hash_name(announced_prefix);
209    let hash_hex = format!("{hash:016x}");
210
211    // Start with /ndn/local/sd/services
212    let mut comps: Vec<NameComponent> = sd_services().components().to_vec();
213
214    // <prefix-hash> as a single GenericNameComponent
215    comps.push(NameComponent {
216        typ: tlv_type::NAME_COMPONENT,
217        value: hash_hex.as_bytes().to_vec().into(),
218    });
219
220    // <node-name> flattened
221    comps.extend(node_name.components().iter().cloned());
222
223    // v=<timestamp-ms> as a VersionNameComponent (type 0x0D)
224    // NDN convention: version component type 0x0D, value = big-endian u64.
225    comps.push(NameComponent {
226        typ: 0x0D,
227        value: timestamp_ms.to_be_bytes().to_vec().into(),
228    });
229
230    Name::from_components(comps)
231}
232
233/// Build a prefix Interest for browsing.
234///
235/// Returns a raw TLV Interest for `/ndn/local/sd/services/` with
236/// `CanBePrefix=true` and `MustBeFresh=true`.
237pub fn build_browse_interest() -> Bytes {
238    let prefix = sd_services();
239    let mut w = TlvWriter::new();
240    w.write_nested(tlv_type::INTEREST, |w: &mut TlvWriter| {
241        write_name_tlv(w, prefix);
242        // CanBePrefix = empty TLV 0x21
243        w.write_tlv(0x21, &[]);
244        // MustBeFresh = empty TLV 0x12
245        w.write_tlv(tlv_type::MUST_BE_FRESH, &[]);
246        write_nni(w, tlv_type::INTEREST_LIFETIME, 4000);
247    });
248    w.finish()
249}
250
251// ─── FNV-1a hash ─────────────────────────────────────────────────────────────
252
253/// FNV-1a 64-bit hash of the canonical URI string of a name.
254///
255/// Used to derive the first path component of a service record name.
256/// Collision probability for < 10 000 distinct prefixes is negligible.
257fn fnv1a_hash_name(name: &Name) -> u64 {
258    const OFFSET: u64 = 14695981039346656037;
259    const PRIME: u64 = 1099511628211;
260    let s = name.to_string();
261    s.bytes()
262        .fold(OFFSET, |h, b| (h ^ b as u64).wrapping_mul(PRIME))
263}
264
265// ─── TLV encoding helpers ─────────────────────────────────────────────────────
266
267/// Encode a `Name` into raw TLV bytes (Name TLV wrapper + components).
268fn encode_name_raw(name: &Name) -> Bytes {
269    let mut w = TlvWriter::new();
270    write_name_tlv(&mut w, name);
271    w.finish()
272}
273
274/// Decode a `Name` from raw TLV bytes.
275fn decode_name_raw(b: &[u8]) -> Option<Name> {
276    // The raw bytes are a Name TLV (type 0x07).
277    if b.is_empty() || b[0] != 0x07 {
278        return None;
279    }
280    use std::str::FromStr;
281    // Re-parse via the wire format: build a minimal Interest-ish context by
282    // using the Name parser directly via its TLV path.
283    // Fallback: parse by reconstructing a canonical URI from the TLV components.
284    let (_, len, hl) = read_tlv_header(b, 0)?;
285    let comps_bytes = &b[hl..hl + len];
286    let mut comps = Vec::new();
287    let mut pos = 0;
288    while pos < comps_bytes.len() {
289        let (typ, clen, chl) = read_tlv_header(comps_bytes, pos)?;
290        let val = comps_bytes[pos + chl..pos + chl + clen].to_vec();
291        comps.push(NameComponent {
292            typ: typ as u64,
293            value: val.into(),
294        });
295        pos += chl + clen;
296    }
297    if comps.is_empty() {
298        return Some(Name::root());
299    }
300    // Reconstruct via canonical URI then parse.
301    let uri = {
302        let mut s = String::new();
303        for comp in &comps {
304            s.push('/');
305            for b in comp.value.iter() {
306                if b.is_ascii_alphanumeric() || b"-.~_".contains(b) {
307                    s.push(*b as char);
308                } else {
309                    s.push_str(&format!("%{:02X}", b));
310                }
311            }
312        }
313        if s.is_empty() { "/".to_string() } else { s }
314    };
315    Name::from_str(&uri).ok()
316}
317
318/// Write a non-negative integer TLV into a `TlvWriter`.
319fn write_nni_to_writer(w: &mut TlvWriter, typ: u32, val: u64) {
320    let bytes = nni_bytes(val);
321    w.write_tlv(typ.into(), &bytes);
322}
323
324fn nni_bytes(val: u64) -> Vec<u8> {
325    if val <= 0xFF {
326        vec![val as u8]
327    } else if val <= 0xFFFF {
328        (val as u16).to_be_bytes().to_vec()
329    } else if val <= 0xFFFF_FFFF {
330        (val as u32).to_be_bytes().to_vec()
331    } else {
332        val.to_be_bytes().to_vec()
333    }
334}
335
336fn read_nni(b: &[u8]) -> Option<u64> {
337    match b.len() {
338        1 => Some(b[0] as u64),
339        2 => Some(u16::from_be_bytes(b.try_into().ok()?) as u64),
340        4 => Some(u32::from_be_bytes(b.try_into().ok()?) as u64),
341        8 => Some(u64::from_be_bytes(b.try_into().ok()?)),
342        _ => None,
343    }
344}
345
346// ─── Minimal TLV reader ───────────────────────────────────────────────────────
347
348/// Read a (type, length, header_len) triple from `b` at `pos`.
349///
350/// Supports NDN TLV variable-length encoding.
351fn read_tlv_header(b: &[u8], pos: usize) -> Option<(u32, usize, usize)> {
352    if pos >= b.len() {
353        return None;
354    }
355    let (typ, t_len) = read_varnumber(b, pos)?;
356    let (len, l_len) = read_varnumber(b, pos + t_len)?;
357    Some((typ as u32, len as usize, t_len + l_len))
358}
359
360fn read_varnumber(b: &[u8], pos: usize) -> Option<(u64, usize)> {
361    let first = *b.get(pos)?;
362    match first {
363        0xFD => {
364            let hi = *b.get(pos + 1)? as u64;
365            let lo = *b.get(pos + 2)? as u64;
366            Some(((hi << 8) | lo, 3))
367        }
368        0xFE => {
369            let v = u32::from_be_bytes(b[pos + 1..pos + 5].try_into().ok()?);
370            Some((v as u64, 5))
371        }
372        0xFF => {
373            let v = u64::from_be_bytes(b[pos + 1..pos + 9].try_into().ok()?);
374            Some((v, 9))
375        }
376        _ => Some((first as u64, 1)),
377    }
378}
379
380// ─── Tests ────────────────────────────────────────────────────────────────────
381
382#[cfg(test)]
383mod tests {
384    use std::str::FromStr;
385
386    use super::*;
387
388    fn n(s: &str) -> Name {
389        Name::from_str(s).unwrap()
390    }
391
392    #[test]
393    fn record_encode_decode_roundtrip() {
394        let rec = ServiceRecord {
395            announced_prefix: n("/ndn/sensor/temp"),
396            node_name: n("/ndn/site/router1"),
397            freshness_ms: 60_000,
398            capabilities: 0x03,
399        };
400        let encoded = rec.encode();
401        let decoded = ServiceRecord::decode(&encoded).unwrap();
402        assert_eq!(decoded.announced_prefix, rec.announced_prefix);
403        assert_eq!(decoded.node_name, rec.node_name);
404        assert_eq!(decoded.freshness_ms, rec.freshness_ms);
405        assert_eq!(decoded.capabilities, rec.capabilities);
406    }
407
408    #[test]
409    fn make_name_under_sd_services() {
410        let rec = ServiceRecord::new(n("/ndn/sensor/temp"), n("/ndn/site/router1"));
411        let name = rec.make_name(1_700_000_000_000);
412        assert!(
413            name.has_prefix(sd_services()),
414            "name should be under sd/services"
415        );
416    }
417
418    #[test]
419    fn data_packet_roundtrip() {
420        let rec = ServiceRecord::new(n("/ndn/edu/ucla/cs"), n("/ndn/site/node42"));
421        let pkt = rec.build_data(42_000);
422        let decoded = ServiceRecord::from_data_packet(&pkt).unwrap();
423        assert_eq!(decoded.announced_prefix, rec.announced_prefix);
424        assert_eq!(decoded.node_name, rec.node_name);
425    }
426
427    #[test]
428    fn fnv1a_hash_is_deterministic() {
429        let h1 = fnv1a_hash_name(&n("/ndn/sensor/temp"));
430        let h2 = fnv1a_hash_name(&n("/ndn/sensor/temp"));
431        assert_eq!(h1, h2);
432    }
433
434    #[test]
435    fn different_prefixes_different_hashes() {
436        let h1 = fnv1a_hash_name(&n("/ndn/sensor/temp"));
437        let h2 = fnv1a_hash_name(&n("/ndn/sensor/pressure"));
438        assert_ne!(h1, h2);
439    }
440
441    #[test]
442    fn browse_interest_has_sd_prefix() {
443        use crate::wire::parse_raw_interest;
444        let pkt = build_browse_interest();
445        let parsed = parse_raw_interest(&pkt).unwrap();
446        assert!(parsed.name.has_prefix(sd_services()));
447    }
448}