ndn_security/
lvs.rs

1//! LightVerSec (LVS) binary trust schema parser and evaluator.
2//!
3//! This module imports pre-compiled LVS trust schemas in the TLV binary
4//! format defined by python-ndn
5//! (<https://python-ndn.readthedocs.io/en/latest/src/lvs/binary-format.html>)
6//! and interoperable with NDNts `@ndn/lvs` and ndnd's
7//! `std/security/trust_schema` packages. It exists so ndn-rs users can
8//! consume trust schemas authored in the tooling the wider NDN community
9//! already uses, rather than re-expressing them in ndn-rs's native
10//! `SchemaRule` vocabulary.
11//!
12//! # Supported subset
13//!
14//! ndn-rs v0.1.0 supports:
15//!
16//! - Full TLV parse of `LvsModel`, `Node`, `ValueEdge`, `PatternEdge`,
17//!   `Constraint`, `ConstraintOption`, `TagSymbol` (every type number in
18//!   the binary format spec).
19//! - Tree-walk evaluation of `(data_name, key_name)` pairs against the
20//!   LVS graph, checking `ValueEdge` literal matches first, then
21//!   `PatternEdge` pattern matches (per the spec's dispatch order).
22//! - `ConstraintOption::Value` (literal) and `ConstraintOption::Tag`
23//!   (equals a previously-bound pattern variable).
24//! - `SignConstraint`: the signing-key name is walked from the start node
25//!   and must reach one of the node IDs listed on the matched data node.
26//! - `NamedPatternCnt` handling: temporary (`_`) vs. named edges are
27//!   treated uniformly during matching, per the spec note that a checker
28//!   concerned only with signature validity does not need to distinguish
29//!   them.
30//!
31//! # Not supported in v0.1.0
32//!
33//! - **`ConstraintOption::UserFnCall`** — user functions (e.g. `$eq`,
34//!   `$regex`) are not yet dispatched. A PatternEdge whose constraints
35//!   contain a `UserFnCall` option cannot be satisfied; if no other
36//!   option on that constraint succeeds, the edge fails to match.
37//!   Attempting to load a schema that contains user functions is allowed
38//!   — the schema parses fine — but any rule that depends on a user
39//!   function will never match a packet. This mirrors python-ndn's
40//!   documented fallback where unknown functions cause verification to
41//!   fail, and is loudly marked by a [`LvsModel::uses_user_functions`]
42//!   flag so callers can refuse to load such schemas when interop
43//!   parity matters.
44//! - Sanity checks beyond the mandatory set from the spec. Unreachable
45//!   nodes are not pruned; trust-anchor-reachability is not verified.
46//!   This matches python-ndn's behaviour.
47//! - Roundtripping back to the binary format (`from_lvs_binary` is import
48//!   only).
49//!
50//! # Version compatibility
51//!
52//! Only LVS binary version `0x00011000` (the python-ndn current stable
53//! version) is accepted. Loading any other version returns
54//! [`LvsError::UnsupportedVersion`].
55//!
56//! # Cross-reference
57//!
58//! The parser was written against two upstream references:
59//!
60//! - Binary format spec: `docs/src/lvs/binary-format.rst` in python-ndn.
61//! - Reference parser:
62//!   `src/ndn/app_support/light_versec/binary.py` in python-ndn.
63//!
64//! Every TLV type number in [`type_number`] matches the python-ndn
65//! `TypeNumber` class verbatim.
66
67use std::collections::HashMap;
68
69use bytes::Bytes;
70use ndn_packet::{Name, NameComponent};
71
72/// LVS binary format version supported by this parser.
73///
74/// Must match python-ndn's `binary.VERSION`. See the module docs for
75/// version-compatibility rules.
76pub const LVS_VERSION: u64 = 0x0001_1000;
77
78/// LVS TLV type numbers (mirrors python-ndn's `TypeNumber`).
79pub mod type_number {
80    pub const COMPONENT_VALUE: u64 = 0x21;
81    pub const PATTERN_TAG: u64 = 0x23;
82    pub const NODE_ID: u64 = 0x25;
83    pub const USER_FN_ID: u64 = 0x27;
84    pub const IDENTIFIER: u64 = 0x29;
85    pub const USER_FN_CALL: u64 = 0x31;
86    pub const FN_ARGS: u64 = 0x33;
87    pub const CONS_OPTION: u64 = 0x41;
88    pub const CONSTRAINT: u64 = 0x43;
89    pub const VALUE_EDGE: u64 = 0x51;
90    pub const PATTERN_EDGE: u64 = 0x53;
91    pub const KEY_NODE_ID: u64 = 0x55;
92    pub const PARENT_ID: u64 = 0x57;
93    pub const VERSION: u64 = 0x61;
94    pub const NODE: u64 = 0x63;
95    pub const TAG_SYMBOL: u64 = 0x67;
96    pub const NAMED_PATTERN_NUM: u64 = 0x69;
97}
98
99/// Errors raised while parsing or checking an LVS binary model.
100#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
101pub enum LvsError {
102    #[error("truncated TLV: {0}")]
103    Truncated(&'static str),
104    #[error("TLV length mismatch at offset {offset}: claimed {claimed}, available {available}")]
105    LengthMismatch {
106        offset: usize,
107        claimed: usize,
108        available: usize,
109    },
110    #[error("unexpected TLV type 0x{actual:x}, expected 0x{expected:x}")]
111    UnexpectedType { actual: u64, expected: u64 },
112    #[error("unsupported LVS binary version 0x{actual:x} (expected 0x{expected:x})")]
113    UnsupportedVersion { actual: u64, expected: u64 },
114    #[error("node id {node_id} out of range (model has {n} nodes)")]
115    NodeIdOutOfRange { node_id: u64, n: usize },
116    #[error("node at index {idx} has id {id} (must equal its index)")]
117    NodeIdMismatch { idx: usize, id: u64 },
118    #[error("ConstraintOption must have exactly one of Value/Tag/UserFn")]
119    MalformedConstraintOption,
120    #[error("invalid UTF-8 in identifier")]
121    BadIdentifier,
122}
123
124/// A parsed LVS trust schema.
125///
126/// See the module docs for which LVS features are supported. Construct via
127/// [`LvsModel::decode`] (typically via
128/// [`crate::TrustSchema::from_lvs_binary`]).
129#[derive(Debug, Clone)]
130pub struct LvsModel {
131    pub version: u64,
132    pub start_id: u64,
133    pub named_pattern_cnt: u64,
134    pub nodes: Vec<LvsNode>,
135    pub tag_symbols: Vec<LvsTagSymbol>,
136    /// True if any `PatternEdge` constraint references a `UserFnCall`.
137    /// Callers that require exact parity with python-ndn should refuse
138    /// to load such schemas until user-function dispatch lands.
139    uses_user_functions: bool,
140}
141
142#[derive(Debug, Clone)]
143pub struct LvsNode {
144    pub id: u64,
145    pub parent: Option<u64>,
146    pub rule_names: Vec<String>,
147    pub value_edges: Vec<LvsValueEdge>,
148    pub pattern_edges: Vec<LvsPatternEdge>,
149    pub sign_constraints: Vec<u64>,
150}
151
152#[derive(Debug, Clone)]
153pub struct LvsValueEdge {
154    pub dest: u64,
155    /// The exact `NameComponent` this edge matches, parsed from the
156    /// pre-encoded `COMPONENT_VALUE` TLV field in the binary format. The
157    /// wire layout is `COMPONENT_VALUE[length][NameComponentTLV]`, so the
158    /// value bytes themselves are a full `t+l+v` NameComponent.
159    pub value: NameComponent,
160}
161
162#[derive(Debug, Clone)]
163pub struct LvsPatternEdge {
164    pub dest: u64,
165    pub tag: u64,
166    pub constraints: Vec<LvsConstraint>,
167}
168
169/// A disjunction of options; the edge matches only if every constraint's
170/// option-set is satisfied (CNF: AND of ORs).
171#[derive(Debug, Clone)]
172pub struct LvsConstraint {
173    pub options: Vec<LvsConstraintOption>,
174}
175
176#[derive(Debug, Clone)]
177pub enum LvsConstraintOption {
178    /// Name component must equal this literal value. Parsed from the
179    /// pre-encoded `COMPONENT_VALUE` TLV in the binary format — see the
180    /// note on [`LvsValueEdge::value`].
181    Value(NameComponent),
182    /// Name component must equal the component previously bound to this
183    /// pattern-edge tag id.
184    Tag(u64),
185    /// User function call — not supported in v0.1.0. Stored so the parser
186    /// can accept the schema and flag `uses_user_functions`, but never
187    /// matches anything at check time.
188    UserFn(LvsUserFnCall),
189}
190
191#[derive(Debug, Clone)]
192pub struct LvsUserFnCall {
193    pub fn_id: String,
194    pub args: Vec<LvsUserFnArg>,
195}
196
197#[derive(Debug, Clone)]
198pub enum LvsUserFnArg {
199    Value(NameComponent),
200    Tag(u64),
201}
202
203#[derive(Debug, Clone)]
204pub struct LvsTagSymbol {
205    pub tag: u64,
206    pub ident: String,
207}
208
209impl LvsModel {
210    /// Parse an LVS binary model from its TLV wire bytes.
211    ///
212    /// The top-level `LvsModel` has no outer TLV wrapper — the input
213    /// buffer is a sequence of top-level TLV fields (Version, StartId,
214    /// NamedPatternCnt, *Node, *TagSymbol).
215    pub fn decode(input: &[u8]) -> Result<Self, LvsError> {
216        let mut cursor = input;
217        let mut version: Option<u64> = None;
218        let mut start_id: Option<u64> = None;
219        let mut named_pattern_cnt: Option<u64> = None;
220        let mut nodes: Vec<LvsNode> = Vec::new();
221        let mut tag_symbols: Vec<LvsTagSymbol> = Vec::new();
222        let mut uses_user_functions = false;
223
224        while !cursor.is_empty() {
225            let (t, value, rest) = read_tlv(cursor)?;
226            cursor = rest;
227            match t {
228                type_number::VERSION => {
229                    version = Some(read_uint(value)?);
230                }
231                type_number::NODE_ID => {
232                    start_id = Some(read_uint(value)?);
233                }
234                type_number::NAMED_PATTERN_NUM => {
235                    named_pattern_cnt = Some(read_uint(value)?);
236                }
237                type_number::NODE => {
238                    let (node, node_uses_fn) = LvsNode::decode(value)?;
239                    uses_user_functions |= node_uses_fn;
240                    nodes.push(node);
241                }
242                type_number::TAG_SYMBOL => {
243                    tag_symbols.push(LvsTagSymbol::decode(value)?);
244                }
245                // Unknown top-level type — skip per TLV forward-compat convention.
246                _ => {}
247            }
248        }
249
250        let version = version.ok_or(LvsError::Truncated("missing Version"))?;
251        if version != LVS_VERSION {
252            return Err(LvsError::UnsupportedVersion {
253                actual: version,
254                expected: LVS_VERSION,
255            });
256        }
257        let start_id = start_id.ok_or(LvsError::Truncated("missing StartId"))?;
258        let named_pattern_cnt =
259            named_pattern_cnt.ok_or(LvsError::Truncated("missing NamedPatternCnt"))?;
260
261        // Sanity: every node's id equals its index.
262        for (idx, n) in nodes.iter().enumerate() {
263            if n.id as usize != idx {
264                return Err(LvsError::NodeIdMismatch { idx, id: n.id });
265            }
266        }
267        // Sanity: every edge and sign constraint refers to an existing node.
268        let n_nodes = nodes.len();
269        for node in &nodes {
270            for e in &node.value_edges {
271                if e.dest as usize >= n_nodes {
272                    return Err(LvsError::NodeIdOutOfRange {
273                        node_id: e.dest,
274                        n: n_nodes,
275                    });
276                }
277            }
278            for e in &node.pattern_edges {
279                if e.dest as usize >= n_nodes {
280                    return Err(LvsError::NodeIdOutOfRange {
281                        node_id: e.dest,
282                        n: n_nodes,
283                    });
284                }
285            }
286            for &sc in &node.sign_constraints {
287                if sc as usize >= n_nodes {
288                    return Err(LvsError::NodeIdOutOfRange {
289                        node_id: sc,
290                        n: n_nodes,
291                    });
292                }
293            }
294        }
295        if (start_id as usize) >= n_nodes && n_nodes > 0 {
296            return Err(LvsError::NodeIdOutOfRange {
297                node_id: start_id,
298                n: n_nodes,
299            });
300        }
301
302        Ok(Self {
303            version,
304            start_id,
305            named_pattern_cnt,
306            nodes,
307            tag_symbols,
308            uses_user_functions,
309        })
310    }
311
312    /// Returns `true` if the loaded schema references any user functions.
313    ///
314    /// Because v0.1.0 does not dispatch user functions, any rule that
315    /// depends on one will never match a packet. Callers that need bit-exact
316    /// parity with python-ndn's evaluation can inspect this flag and refuse
317    /// to use the schema.
318    pub fn uses_user_functions(&self) -> bool {
319        self.uses_user_functions
320    }
321
322    /// Walk the LVS graph for `name`, collecting the set of reachable
323    /// `(node_id, bindings)` pairs where the walk has consumed all of
324    /// `name`'s components. Multiple endings are possible because different
325    /// pattern-edge choices can lead to different terminal nodes.
326    fn walk(&self, name: &Name) -> Vec<(u64, HashMap<u64, NameComponent>)> {
327        let mut out = Vec::new();
328        if self.nodes.is_empty() {
329            return out;
330        }
331        let start = self.start_id;
332        let bindings: HashMap<u64, NameComponent> = HashMap::new();
333        self.walk_inner(start, name.components(), 0, bindings, &mut out);
334        out
335    }
336
337    fn walk_inner(
338        &self,
339        node_id: u64,
340        comps: &[NameComponent],
341        depth: usize,
342        bindings: HashMap<u64, NameComponent>,
343        out: &mut Vec<(u64, HashMap<u64, NameComponent>)>,
344    ) {
345        if depth == comps.len() {
346            out.push((node_id, bindings));
347            return;
348        }
349        let Some(node) = self.nodes.get(node_id as usize) else {
350            return;
351        };
352        let comp = &comps[depth];
353
354        // Per the spec: check ValueEdges for exact matches first.
355        for ve in &node.value_edges {
356            if &ve.value == comp {
357                self.walk_inner(ve.dest, comps, depth + 1, bindings.clone(), out);
358            }
359        }
360
361        // Then PatternEdges. Per the spec: when multiple PatternEdges can
362        // match, the first one in the file should hit. We explore all
363        // matching edges so that alternative paths for the key-name walk
364        // still get considered, but we stop at the first successful
365        // terminal — matching the "first occurring" semantics is done by
366        // the caller (which picks out[0] if out is non-empty).
367        for pe in &node.pattern_edges {
368            if self.pattern_edge_matches(pe, comp, &bindings) {
369                let mut new_bindings = bindings.clone();
370                new_bindings.insert(pe.tag, comp.clone());
371                self.walk_inner(pe.dest, comps, depth + 1, new_bindings, out);
372            }
373        }
374    }
375
376    fn pattern_edge_matches(
377        &self,
378        edge: &LvsPatternEdge,
379        comp: &NameComponent,
380        bindings: &HashMap<u64, NameComponent>,
381    ) -> bool {
382        // Every constraint must be satisfied (AND). If there are no
383        // constraints, the edge always matches.
384        edge.constraints.iter().all(|c| {
385            c.options
386                .iter()
387                .any(|opt| self.option_matches(opt, comp, bindings))
388        })
389    }
390
391    fn option_matches(
392        &self,
393        opt: &LvsConstraintOption,
394        comp: &NameComponent,
395        bindings: &HashMap<u64, NameComponent>,
396    ) -> bool {
397        match opt {
398            LvsConstraintOption::Value(v) => v == comp,
399            LvsConstraintOption::Tag(t) => bindings.get(t).is_some_and(|prev| prev == comp),
400            // User functions are not dispatched in v0.1.0 — they never match.
401            LvsConstraintOption::UserFn(_) => false,
402        }
403    }
404
405    /// Check whether `data_name` is allowed to be signed by `key_name`
406    /// under this LVS schema. Returns `true` if:
407    ///
408    /// 1. `data_name` reaches some node `D` in the graph, and
409    /// 2. `D` has at least one `SignConstraint`, and
410    /// 3. `key_name` reaches a node whose id is listed in `D.sign_constraints`.
411    pub fn check(&self, data_name: &Name, key_name: &Name) -> bool {
412        let data_endings = self.walk(data_name);
413        if data_endings.is_empty() {
414            return false;
415        }
416        let key_endings = self.walk(key_name);
417        if key_endings.is_empty() {
418            return false;
419        }
420        for (data_node_id, _data_bindings) in &data_endings {
421            let Some(node) = self.nodes.get(*data_node_id as usize) else {
422                continue;
423            };
424            if node.sign_constraints.is_empty() {
425                continue;
426            }
427            for (key_node_id, _) in &key_endings {
428                if node.sign_constraints.contains(key_node_id) {
429                    return true;
430                }
431            }
432        }
433        false
434    }
435}
436
437impl LvsNode {
438    fn decode(input: &[u8]) -> Result<(Self, bool), LvsError> {
439        let mut cursor = input;
440        let mut id: Option<u64> = None;
441        let mut parent: Option<u64> = None;
442        let mut rule_names = Vec::new();
443        let mut value_edges = Vec::new();
444        let mut pattern_edges = Vec::new();
445        let mut sign_constraints = Vec::new();
446        let mut uses_user_functions = false;
447
448        while !cursor.is_empty() {
449            let (t, v, rest) = read_tlv(cursor)?;
450            cursor = rest;
451            match t {
452                type_number::NODE_ID => id = Some(read_uint(v)?),
453                type_number::PARENT_ID => parent = Some(read_uint(v)?),
454                type_number::IDENTIFIER => rule_names.push(read_string(v)?),
455                type_number::VALUE_EDGE => value_edges.push(LvsValueEdge::decode(v)?),
456                type_number::PATTERN_EDGE => {
457                    let (edge, uses_fn) = LvsPatternEdge::decode(v)?;
458                    uses_user_functions |= uses_fn;
459                    pattern_edges.push(edge);
460                }
461                type_number::KEY_NODE_ID => sign_constraints.push(read_uint(v)?),
462                _ => {}
463            }
464        }
465
466        let id = id.ok_or(LvsError::Truncated("Node missing NodeId"))?;
467        Ok((
468            Self {
469                id,
470                parent,
471                rule_names,
472                value_edges,
473                pattern_edges,
474                sign_constraints,
475            },
476            uses_user_functions,
477        ))
478    }
479}
480
481impl LvsValueEdge {
482    fn decode(input: &[u8]) -> Result<Self, LvsError> {
483        let mut cursor = input;
484        let mut dest: Option<u64> = None;
485        let mut value: Option<NameComponent> = None;
486        while !cursor.is_empty() {
487            let (t, v, rest) = read_tlv(cursor)?;
488            cursor = rest;
489            match t {
490                type_number::NODE_ID => dest = Some(read_uint(v)?),
491                type_number::COMPONENT_VALUE => value = Some(parse_name_component(v)?),
492                _ => {}
493            }
494        }
495        Ok(Self {
496            dest: dest.ok_or(LvsError::Truncated("ValueEdge missing dest"))?,
497            value: value.ok_or(LvsError::Truncated("ValueEdge missing value"))?,
498        })
499    }
500}
501
502impl LvsPatternEdge {
503    fn decode(input: &[u8]) -> Result<(Self, bool), LvsError> {
504        let mut cursor = input;
505        let mut dest: Option<u64> = None;
506        let mut tag: Option<u64> = None;
507        let mut constraints = Vec::new();
508        let mut uses_user_functions = false;
509        while !cursor.is_empty() {
510            let (t, v, rest) = read_tlv(cursor)?;
511            cursor = rest;
512            match t {
513                type_number::NODE_ID => dest = Some(read_uint(v)?),
514                type_number::PATTERN_TAG => tag = Some(read_uint(v)?),
515                type_number::CONSTRAINT => {
516                    let (c, uses_fn) = LvsConstraint::decode(v)?;
517                    uses_user_functions |= uses_fn;
518                    constraints.push(c);
519                }
520                _ => {}
521            }
522        }
523        Ok((
524            Self {
525                dest: dest.ok_or(LvsError::Truncated("PatternEdge missing dest"))?,
526                tag: tag.ok_or(LvsError::Truncated("PatternEdge missing tag"))?,
527                constraints,
528            },
529            uses_user_functions,
530        ))
531    }
532}
533
534impl LvsConstraint {
535    fn decode(input: &[u8]) -> Result<(Self, bool), LvsError> {
536        let mut cursor = input;
537        let mut options = Vec::new();
538        let mut uses_user_functions = false;
539        while !cursor.is_empty() {
540            let (t, v, rest) = read_tlv(cursor)?;
541            cursor = rest;
542            if t == type_number::CONS_OPTION {
543                let (opt, uses_fn) = LvsConstraintOption::decode(v)?;
544                uses_user_functions |= uses_fn;
545                options.push(opt);
546            }
547        }
548        Ok((Self { options }, uses_user_functions))
549    }
550}
551
552impl LvsConstraintOption {
553    fn decode(input: &[u8]) -> Result<(Self, bool), LvsError> {
554        let mut cursor = input;
555        let mut value: Option<NameComponent> = None;
556        let mut tag: Option<u64> = None;
557        let mut fn_call: Option<LvsUserFnCall> = None;
558
559        while !cursor.is_empty() {
560            let (t, v, rest) = read_tlv(cursor)?;
561            cursor = rest;
562            match t {
563                type_number::COMPONENT_VALUE => value = Some(parse_name_component(v)?),
564                type_number::PATTERN_TAG => tag = Some(read_uint(v)?),
565                type_number::USER_FN_CALL => fn_call = Some(LvsUserFnCall::decode(v)?),
566                _ => {}
567            }
568        }
569
570        let set_count = value.is_some() as u8 + tag.is_some() as u8 + fn_call.is_some() as u8;
571        if set_count != 1 {
572            return Err(LvsError::MalformedConstraintOption);
573        }
574
575        if let Some(v) = value {
576            Ok((Self::Value(v), false))
577        } else if let Some(t) = tag {
578            Ok((Self::Tag(t), false))
579        } else {
580            Ok((Self::UserFn(fn_call.unwrap()), true))
581        }
582    }
583}
584
585impl LvsUserFnCall {
586    fn decode(input: &[u8]) -> Result<Self, LvsError> {
587        let mut cursor = input;
588        let mut fn_id: Option<String> = None;
589        let mut args: Vec<LvsUserFnArg> = Vec::new();
590        while !cursor.is_empty() {
591            let (t, v, rest) = read_tlv(cursor)?;
592            cursor = rest;
593            match t {
594                type_number::USER_FN_ID => fn_id = Some(read_string(v)?),
595                type_number::FN_ARGS => args.push(LvsUserFnArg::decode(v)?),
596                _ => {}
597            }
598        }
599        Ok(Self {
600            fn_id: fn_id.ok_or(LvsError::Truncated("UserFnCall missing FnId"))?,
601            args,
602        })
603    }
604}
605
606impl LvsUserFnArg {
607    fn decode(input: &[u8]) -> Result<Self, LvsError> {
608        let mut cursor = input;
609        let mut value: Option<NameComponent> = None;
610        let mut tag: Option<u64> = None;
611        while !cursor.is_empty() {
612            let (t, v, rest) = read_tlv(cursor)?;
613            cursor = rest;
614            match t {
615                type_number::COMPONENT_VALUE => value = Some(parse_name_component(v)?),
616                type_number::PATTERN_TAG => tag = Some(read_uint(v)?),
617                _ => {}
618            }
619        }
620        if let Some(v) = value {
621            Ok(Self::Value(v))
622        } else if let Some(t) = tag {
623            Ok(Self::Tag(t))
624        } else {
625            Err(LvsError::Truncated("UserFnArg empty"))
626        }
627    }
628}
629
630impl LvsTagSymbol {
631    fn decode(input: &[u8]) -> Result<Self, LvsError> {
632        let mut cursor = input;
633        let mut tag: Option<u64> = None;
634        let mut ident: Option<String> = None;
635        while !cursor.is_empty() {
636            let (t, v, rest) = read_tlv(cursor)?;
637            cursor = rest;
638            match t {
639                type_number::PATTERN_TAG => tag = Some(read_uint(v)?),
640                type_number::IDENTIFIER => ident = Some(read_string(v)?),
641                _ => {}
642            }
643        }
644        Ok(Self {
645            tag: tag.ok_or(LvsError::Truncated("TagSymbol missing tag"))?,
646            ident: ident.ok_or(LvsError::Truncated("TagSymbol missing ident"))?,
647        })
648    }
649}
650
651// ── TLV primitive helpers ─────────────────────────────────────────────────────
652
653/// Read a TLV from the start of `input`, returning (type, value_slice, rest).
654fn read_tlv(input: &[u8]) -> Result<(u64, &[u8], &[u8]), LvsError> {
655    let (t, tn) = ndn_tlv::read_varu64(input).map_err(|_| LvsError::Truncated("TLV type"))?;
656    let (l, ln) =
657        ndn_tlv::read_varu64(&input[tn..]).map_err(|_| LvsError::Truncated("TLV length"))?;
658    let header_len = tn + ln;
659    let total = header_len
660        .checked_add(l as usize)
661        .ok_or(LvsError::Truncated("TLV length overflow"))?;
662    if total > input.len() {
663        return Err(LvsError::LengthMismatch {
664            offset: 0,
665            claimed: total,
666            available: input.len(),
667        });
668    }
669    Ok((t, &input[header_len..total], &input[total..]))
670}
671
672/// Read a non-negative integer from a TLV value (1/2/4/8 bytes big-endian).
673fn read_uint(v: &[u8]) -> Result<u64, LvsError> {
674    match v.len() {
675        1 => Ok(v[0] as u64),
676        2 => Ok(u16::from_be_bytes(v.try_into().unwrap()) as u64),
677        4 => Ok(u32::from_be_bytes(v.try_into().unwrap()) as u64),
678        8 => Ok(u64::from_be_bytes(v.try_into().unwrap())),
679        _ => Err(LvsError::Truncated("uint: unexpected length")),
680    }
681}
682
683fn read_string(v: &[u8]) -> Result<String, LvsError> {
684    std::str::from_utf8(v)
685        .map(|s| s.to_owned())
686        .map_err(|_| LvsError::BadIdentifier)
687}
688
689/// Parse a `COMPONENT_VALUE` field (which itself contains a full
690/// NameComponent TLV, per the LVS binary format) into a `NameComponent`.
691///
692/// The LVS spec line `Value = COMPONENT-VALUE-TYPE TLV-LENGTH NameComponent`
693/// is easy to misread as "the value is raw bytes" — python-ndn's
694/// `BytesField` declaration is the source of that confusion. In reality the
695/// bytes *are* a full NameComponent with its own type+length header, so
696/// e.g. a Generic "KEY" literal is stored as `08 03 4b 45 59`, not `4b 45
697/// 59`. We parse the inner TLV here so that matching compares the full
698/// component (including type) against the walked Name's components.
699fn parse_name_component(value: &[u8]) -> Result<NameComponent, LvsError> {
700    let (t, tn) =
701        ndn_tlv::read_varu64(value).map_err(|_| LvsError::Truncated("NameComponent type"))?;
702    let (l, ln) = ndn_tlv::read_varu64(&value[tn..])
703        .map_err(|_| LvsError::Truncated("NameComponent length"))?;
704    let start = tn + ln;
705    let end = start
706        .checked_add(l as usize)
707        .ok_or(LvsError::Truncated("NameComponent length overflow"))?;
708    if end > value.len() {
709        return Err(LvsError::LengthMismatch {
710            offset: 0,
711            claimed: end,
712            available: value.len(),
713        });
714    }
715    Ok(NameComponent::new(
716        t,
717        Bytes::copy_from_slice(&value[start..end]),
718    ))
719}
720
721#[cfg(test)]
722mod tests {
723    use super::*;
724    use bytes::BytesMut;
725
726    // ── Hand-built fixtures ────────────────────────────────────────────────
727    //
728    // Each helper writes a TLV (type, value) pair in the same wire format
729    // python-ndn produces. Because python-ndn's IntField writes the minimum
730    // number of bytes (1/2/4/8) that fit the value, we do the same here.
731
732    fn write_tlv(buf: &mut BytesMut, t: u64, value: &[u8]) {
733        use ndn_tlv::TlvWriter;
734        let mut w = TlvWriter::new();
735        w.write_tlv(t, value);
736        buf.extend_from_slice(&w.finish());
737    }
738
739    /// Encode `bytes` as a GenericNameComponent TLV (type 0x08) — the form
740    /// the LVS binary compiler uses for literal values.
741    fn encode_generic_nc(bytes: &[u8]) -> Vec<u8> {
742        let mut out = Vec::with_capacity(2 + bytes.len());
743        out.push(0x08);
744        out.push(bytes.len() as u8);
745        out.extend_from_slice(bytes);
746        out
747    }
748
749    /// Write a COMPONENT_VALUE TLV whose value is a pre-encoded
750    /// GenericNameComponent wrapping `bytes`, matching the python-ndn LVS
751    /// compiler output.
752    fn write_component_value_tlv(buf: &mut BytesMut, bytes: &[u8]) {
753        let nc = encode_generic_nc(bytes);
754        write_tlv(buf, type_number::COMPONENT_VALUE, &nc);
755    }
756
757    fn uint_be(value: u64) -> Vec<u8> {
758        if value <= u8::MAX as u64 {
759            vec![value as u8]
760        } else if value <= u16::MAX as u64 {
761            (value as u16).to_be_bytes().to_vec()
762        } else if value <= u32::MAX as u64 {
763            (value as u32).to_be_bytes().to_vec()
764        } else {
765            value.to_be_bytes().to_vec()
766        }
767    }
768
769    fn write_uint_tlv(buf: &mut BytesMut, t: u64, value: u64) {
770        let be = uint_be(value);
771        write_tlv(buf, t, &be);
772    }
773
774    // ── Basic fixture: hierarchical trust ─────────────────────────────────
775    //
776    // Three nodes:
777    //   0 (root)
778    //     --value("app")--> 1 (data)        sign_cons = [2]
779    //     --value("key")--> 2 (key, leaf)
780    //
781    // The "app" data name can be signed by the "key" key name.
782    //
783    // Named-pattern count is 0 (no pattern edges).
784    fn build_hierarchical_fixture() -> Vec<u8> {
785        let mut out = BytesMut::new();
786        // LvsModel envelope fields (flat, no outer wrapper).
787        write_uint_tlv(&mut out, type_number::VERSION, LVS_VERSION);
788        write_uint_tlv(&mut out, type_number::NODE_ID, 0); // start_id = 0
789        write_uint_tlv(&mut out, type_number::NAMED_PATTERN_NUM, 0);
790
791        // Node 0 (root).
792        {
793            let mut node = BytesMut::new();
794            write_uint_tlv(&mut node, type_number::NODE_ID, 0);
795            // ValueEdge → node 1 on "app"
796            {
797                let mut ve = BytesMut::new();
798                write_uint_tlv(&mut ve, type_number::NODE_ID, 1);
799                write_component_value_tlv(&mut ve, b"app");
800                write_tlv(&mut node, type_number::VALUE_EDGE, &ve);
801            }
802            // ValueEdge → node 2 on "key"
803            {
804                let mut ve = BytesMut::new();
805                write_uint_tlv(&mut ve, type_number::NODE_ID, 2);
806                write_component_value_tlv(&mut ve, b"key");
807                write_tlv(&mut node, type_number::VALUE_EDGE, &ve);
808            }
809            write_tlv(&mut out, type_number::NODE, &node);
810        }
811
812        // Node 1 (app data) — sign_cons = [2].
813        {
814            let mut node = BytesMut::new();
815            write_uint_tlv(&mut node, type_number::NODE_ID, 1);
816            write_uint_tlv(&mut node, type_number::PARENT_ID, 0);
817            write_uint_tlv(&mut node, type_number::KEY_NODE_ID, 2);
818            write_tlv(&mut out, type_number::NODE, &node);
819        }
820
821        // Node 2 (key, leaf, trust anchor).
822        {
823            let mut node = BytesMut::new();
824            write_uint_tlv(&mut node, type_number::NODE_ID, 2);
825            write_uint_tlv(&mut node, type_number::PARENT_ID, 0);
826            write_tlv(&mut out, type_number::NODE, &node);
827        }
828
829        out.to_vec()
830    }
831
832    fn comp(s: &'static str) -> NameComponent {
833        NameComponent::generic(Bytes::from_static(s.as_bytes()))
834    }
835
836    fn name(parts: &[&'static str]) -> Name {
837        Name::from_components(parts.iter().map(|p| comp(p)))
838    }
839
840    #[test]
841    fn decode_hierarchical_fixture() {
842        let wire = build_hierarchical_fixture();
843        let model = LvsModel::decode(&wire).expect("decode");
844        assert_eq!(model.version, LVS_VERSION);
845        assert_eq!(model.start_id, 0);
846        assert_eq!(model.named_pattern_cnt, 0);
847        assert_eq!(model.nodes.len(), 3);
848        assert_eq!(model.nodes[0].value_edges.len(), 2);
849        assert_eq!(model.nodes[1].sign_constraints, vec![2]);
850        assert!(!model.uses_user_functions());
851    }
852
853    #[test]
854    fn hierarchical_allows_app_signed_by_key() {
855        let model = LvsModel::decode(&build_hierarchical_fixture()).unwrap();
856        assert!(model.check(&name(&["app"]), &name(&["key"])));
857    }
858
859    #[test]
860    fn hierarchical_rejects_wrong_key_name() {
861        let model = LvsModel::decode(&build_hierarchical_fixture()).unwrap();
862        assert!(!model.check(&name(&["app"]), &name(&["other"])));
863    }
864
865    #[test]
866    fn hierarchical_rejects_unknown_data_name() {
867        let model = LvsModel::decode(&build_hierarchical_fixture()).unwrap();
868        assert!(!model.check(&name(&["stranger"]), &name(&["key"])));
869    }
870
871    // ── Pattern-edge fixture with capture variable ────────────────────────
872    //
873    // Models the schema:
874    //     /sensor/<node> => /sensor/<node>/KEY
875    //
876    // where <node> is a pattern variable that must be consistent between
877    // the data and key walks.
878    //
879    // Layout:
880    //   0 root
881    //     --value("sensor")--> 1
882    //   1 "sensor"
883    //     --pattern(tag=1, no constraints)--> 2  // data endpoint
884    //     --pattern(tag=1, no constraints)--> 3  // key endpoint prefix
885    //   2 (data, sign_cons=[4])
886    //   3 intermediate "key"
887    //     --value("KEY")--> 4
888    //   4 (key, leaf)
889    //
890    // This is a simplification — python-ndn's compiler would produce a
891    // more complex graph. But the parser and checker must handle the
892    // primitives used here correctly.
893    fn build_pattern_fixture() -> Vec<u8> {
894        let mut out = BytesMut::new();
895        write_uint_tlv(&mut out, type_number::VERSION, LVS_VERSION);
896        write_uint_tlv(&mut out, type_number::NODE_ID, 0);
897        write_uint_tlv(&mut out, type_number::NAMED_PATTERN_NUM, 1);
898
899        // Node 0 (root) — ValueEdge "sensor" → 1.
900        {
901            let mut node = BytesMut::new();
902            write_uint_tlv(&mut node, type_number::NODE_ID, 0);
903            let mut ve = BytesMut::new();
904            write_uint_tlv(&mut ve, type_number::NODE_ID, 1);
905            write_component_value_tlv(&mut ve, b"sensor");
906            write_tlv(&mut node, type_number::VALUE_EDGE, &ve);
907            write_tlv(&mut out, type_number::NODE, &node);
908        }
909
910        // Node 1 — two PatternEdges (tag=1, no constraints).
911        {
912            let mut node = BytesMut::new();
913            write_uint_tlv(&mut node, type_number::NODE_ID, 1);
914            write_uint_tlv(&mut node, type_number::PARENT_ID, 0);
915
916            // PatternEdge → 2
917            {
918                let mut pe = BytesMut::new();
919                write_uint_tlv(&mut pe, type_number::NODE_ID, 2);
920                write_uint_tlv(&mut pe, type_number::PATTERN_TAG, 1);
921                write_tlv(&mut node, type_number::PATTERN_EDGE, &pe);
922            }
923            // PatternEdge → 3 with constraint: tag == 1 (consistency)
924            // Here we want the key-path pattern edge to BIND a new variable
925            // that must equal tag 1 from the data path. But bindings don't
926            // cross walks — the check is per-walk. Since ndn-rs evaluates
927            // data and key separately, consistency across walks is only
928            // possible via sign-constraint graph structure, which this
929            // fixture emulates by tying the two endpoints through sign_cons.
930            //
931            // For a meaningful test of the Tag constraint option, we add a
932            // constraint that says "this pattern edge must equal tag 1 from
933            // the SAME walk". That can't actually fire during a single walk
934            // without a preceding pattern-edge on the same path, so we add
935            // a pattern edge first on the key path (node 3 → 4).
936            {
937                let mut pe = BytesMut::new();
938                write_uint_tlv(&mut pe, type_number::NODE_ID, 3);
939                write_uint_tlv(&mut pe, type_number::PATTERN_TAG, 1);
940                write_tlv(&mut node, type_number::PATTERN_EDGE, &pe);
941            }
942            write_tlv(&mut out, type_number::NODE, &node);
943        }
944
945        // Node 2 — data endpoint, sign_cons = [4].
946        {
947            let mut node = BytesMut::new();
948            write_uint_tlv(&mut node, type_number::NODE_ID, 2);
949            write_uint_tlv(&mut node, type_number::PARENT_ID, 1);
950            write_uint_tlv(&mut node, type_number::KEY_NODE_ID, 4);
951            write_tlv(&mut out, type_number::NODE, &node);
952        }
953
954        // Node 3 — intermediate on key path, ValueEdge "KEY" → 4.
955        {
956            let mut node = BytesMut::new();
957            write_uint_tlv(&mut node, type_number::NODE_ID, 3);
958            write_uint_tlv(&mut node, type_number::PARENT_ID, 1);
959            let mut ve = BytesMut::new();
960            write_uint_tlv(&mut ve, type_number::NODE_ID, 4);
961            write_component_value_tlv(&mut ve, b"KEY");
962            write_tlv(&mut node, type_number::VALUE_EDGE, &ve);
963            write_tlv(&mut out, type_number::NODE, &node);
964        }
965
966        // Node 4 — key leaf.
967        {
968            let mut node = BytesMut::new();
969            write_uint_tlv(&mut node, type_number::NODE_ID, 4);
970            write_uint_tlv(&mut node, type_number::PARENT_ID, 3);
971            write_tlv(&mut out, type_number::NODE, &node);
972        }
973
974        out.to_vec()
975    }
976
977    #[test]
978    fn pattern_fixture_allows_data_signed_by_key() {
979        let model = LvsModel::decode(&build_pattern_fixture()).unwrap();
980        // /sensor/temp signed by /sensor/temp/KEY
981        assert!(model.check(
982            &name(&["sensor", "temp"]),
983            &name(&["sensor", "temp", "KEY"])
984        ));
985    }
986
987    #[test]
988    fn pattern_fixture_rejects_shorter_key() {
989        let model = LvsModel::decode(&build_pattern_fixture()).unwrap();
990        assert!(!model.check(&name(&["sensor", "temp"]), &name(&["sensor", "temp"])));
991    }
992
993    #[test]
994    fn pattern_fixture_rejects_wrong_root() {
995        let model = LvsModel::decode(&build_pattern_fixture()).unwrap();
996        assert!(!model.check(&name(&["other", "temp"]), &name(&["sensor", "temp", "KEY"])));
997    }
998
999    // ── Version check ──────────────────────────────────────────────────────
1000
1001    #[test]
1002    fn unsupported_version_errors() {
1003        let mut out = BytesMut::new();
1004        write_uint_tlv(&mut out, type_number::VERSION, 0xDEADBEEF);
1005        write_uint_tlv(&mut out, type_number::NODE_ID, 0);
1006        write_uint_tlv(&mut out, type_number::NAMED_PATTERN_NUM, 0);
1007        assert!(matches!(
1008            LvsModel::decode(&out),
1009            Err(LvsError::UnsupportedVersion { .. })
1010        ));
1011    }
1012
1013    // ── User function detection ────────────────────────────────────────────
1014
1015    #[test]
1016    fn user_function_schema_parses_and_flags() {
1017        let mut out = BytesMut::new();
1018        write_uint_tlv(&mut out, type_number::VERSION, LVS_VERSION);
1019        write_uint_tlv(&mut out, type_number::NODE_ID, 0);
1020        write_uint_tlv(&mut out, type_number::NAMED_PATTERN_NUM, 1);
1021
1022        // Node 0 with a PatternEdge → 1 whose constraint uses a user fn.
1023        {
1024            let mut node = BytesMut::new();
1025            write_uint_tlv(&mut node, type_number::NODE_ID, 0);
1026            let mut pe = BytesMut::new();
1027            write_uint_tlv(&mut pe, type_number::NODE_ID, 1);
1028            write_uint_tlv(&mut pe, type_number::PATTERN_TAG, 1);
1029            {
1030                let mut cons = BytesMut::new();
1031                {
1032                    let mut opt = BytesMut::new();
1033                    let mut call = BytesMut::new();
1034                    write_tlv(&mut call, type_number::USER_FN_ID, b"$regex");
1035                    {
1036                        let mut arg = BytesMut::new();
1037                        write_component_value_tlv(&mut arg, b"^[0-9]+$");
1038                        write_tlv(&mut call, type_number::FN_ARGS, &arg);
1039                    }
1040                    write_tlv(&mut opt, type_number::USER_FN_CALL, &call);
1041                    write_tlv(&mut cons, type_number::CONS_OPTION, &opt);
1042                }
1043                write_tlv(&mut pe, type_number::CONSTRAINT, &cons);
1044            }
1045            write_tlv(&mut node, type_number::PATTERN_EDGE, &pe);
1046            write_tlv(&mut out, type_number::NODE, &node);
1047        }
1048        // Node 1 leaf.
1049        {
1050            let mut node = BytesMut::new();
1051            write_uint_tlv(&mut node, type_number::NODE_ID, 1);
1052            write_uint_tlv(&mut node, type_number::PARENT_ID, 0);
1053            write_tlv(&mut out, type_number::NODE, &node);
1054        }
1055
1056        let model = LvsModel::decode(&out).expect("decode");
1057        assert!(
1058            model.uses_user_functions(),
1059            "user-fn schema must flag uses_user_functions"
1060        );
1061        // The user-fn-gated edge never matches, so the packet is rejected.
1062        assert!(!model.check(&name(&["123"]), &name(&["123"])));
1063    }
1064}