ndn_security/
trust_schema.rs

1use ndn_packet::{Name, NameComponent};
2use std::collections::HashMap;
3use std::sync::Arc;
4
5use crate::lvs::{LvsError, LvsModel};
6
7/// Error returned when a pattern or rule string cannot be parsed.
8#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
9pub enum PatternParseError {
10    #[error("empty pattern string")]
11    Empty,
12    #[error("unclosed capture variable (missing '>')")]
13    UnclosedCapture,
14    #[error("MultiCapture ('**') must be the last component")]
15    MultiCaptureNotLast,
16    #[error("rule must have exactly one '=>' separator")]
17    BadRuleSeparator,
18}
19
20/// A single component in a name pattern.
21#[derive(Clone, Debug, PartialEq, Eq)]
22pub enum PatternComponent {
23    /// Must match this exact component.
24    Literal(NameComponent),
25    /// Binds one component to a named variable.
26    Capture(Arc<str>),
27    /// Binds one or more trailing components to a named variable.
28    MultiCapture(Arc<str>),
29}
30
31/// A name pattern with named capture groups.
32///
33/// Used by the trust schema to express rules like:
34/// "Data under `/sensor/<node>/<type>` must be signed by `/sensor/<node>/KEY/<id>`"
35/// where `<node>` must match in both patterns.
36///
37/// # Text format
38///
39/// Patterns can be parsed from and serialized to a human-readable string:
40///
41/// - `/literal` → [`PatternComponent::Literal`]
42/// - `/<var>` → [`PatternComponent::Capture`] — matches one name component
43/// - `/<**var>` → [`PatternComponent::MultiCapture`] — matches all remaining components (must be last)
44///
45/// Example: `/sensor/<node>/KEY/<id>` parses to
46/// `[Literal("sensor"), Capture("node"), Literal("KEY"), Capture("id")]`.
47#[derive(Clone, Debug, PartialEq, Eq)]
48pub struct NamePattern(pub Vec<PatternComponent>);
49
50impl NamePattern {
51    /// Parse a pattern from a text string.
52    ///
53    /// Components are `/`-separated. An empty leading `/` is ignored.
54    /// `<var>` is a single-component capture; `<**var>` is a multi-component
55    /// capture (must be the last component in the pattern).
56    ///
57    /// # Examples
58    ///
59    /// ```
60    /// use ndn_security::trust_schema::NamePattern;
61    ///
62    /// let p = NamePattern::parse("/sensor/<node>/KEY/<id>").unwrap();
63    /// ```
64    pub fn parse(s: &str) -> Result<Self, PatternParseError> {
65        let s = s.trim();
66        if s.is_empty() {
67            return Err(PatternParseError::Empty);
68        }
69        // Strip optional leading slash.
70        let s = s.strip_prefix('/').unwrap_or(s);
71        if s.is_empty() {
72            // Just a lone "/" — empty pattern (matches root).
73            return Ok(Self(vec![]));
74        }
75
76        let mut components = Vec::new();
77        let parts: Vec<&str> = s.split('/').collect();
78        let last_idx = parts.len().saturating_sub(1);
79
80        for (i, part) in parts.iter().enumerate() {
81            if let Some(inner) = part.strip_prefix('<') {
82                let var = inner
83                    .strip_suffix('>')
84                    .ok_or(PatternParseError::UnclosedCapture)?;
85                if let Some(multi_var) = var.strip_prefix("**") {
86                    if i != last_idx {
87                        return Err(PatternParseError::MultiCaptureNotLast);
88                    }
89                    components.push(PatternComponent::MultiCapture(Arc::from(multi_var)));
90                } else {
91                    components.push(PatternComponent::Capture(Arc::from(var)));
92                }
93            } else {
94                let comp = NameComponent::generic(bytes::Bytes::copy_from_slice(part.as_bytes()));
95                components.push(PatternComponent::Literal(comp));
96            }
97        }
98
99        Ok(Self(components))
100    }
101
102    /// Attempt to match `name` against this pattern, extending `bindings`.
103    /// Returns `true` if the match succeeds.
104    pub fn matches(&self, name: &Name, bindings: &mut HashMap<Arc<str>, NameComponent>) -> bool {
105        let components = name.components();
106        let mut name_idx = 0;
107
108        for pat in &self.0 {
109            match pat {
110                PatternComponent::Literal(c) => {
111                    if name_idx >= components.len() || &components[name_idx] != c {
112                        return false;
113                    }
114                    name_idx += 1;
115                }
116                PatternComponent::Capture(var) => {
117                    if name_idx >= components.len() {
118                        return false;
119                    }
120                    let comp = components[name_idx].clone();
121                    if let Some(existing) = bindings.get(var) {
122                        if existing != &comp {
123                            return false; // variable must be consistent
124                        }
125                    } else {
126                        bindings.insert(Arc::clone(var), comp);
127                    }
128                    name_idx += 1;
129                }
130                PatternComponent::MultiCapture(_var) => {
131                    // Greedily consume all remaining components.
132                    name_idx = components.len();
133                }
134            }
135        }
136        name_idx == components.len()
137    }
138}
139
140/// A single trust schema rule: Data matching `data_pattern` must be signed
141/// by a key matching `key_pattern`, with captured variables consistent between
142/// both patterns.
143///
144/// # Text format
145///
146/// A rule is serialized as `"<data_pattern> => <key_pattern>"`, e.g.:
147///
148/// ```text
149/// /sensor/<node>/<type> => /sensor/<node>/KEY/<id>
150/// ```
151#[derive(Clone, Debug, PartialEq, Eq)]
152pub struct SchemaRule {
153    pub data_pattern: NamePattern,
154    pub key_pattern: NamePattern,
155}
156
157impl SchemaRule {
158    /// Parse a rule from its text representation (`"data_pattern => key_pattern"`).
159    pub fn parse(s: &str) -> Result<Self, PatternParseError> {
160        let parts: Vec<&str> = s.splitn(2, "=>").collect();
161        if parts.len() != 2 {
162            return Err(PatternParseError::BadRuleSeparator);
163        }
164        let data_pattern = NamePattern::parse(parts[0].trim())?;
165        let key_pattern = NamePattern::parse(parts[1].trim())?;
166        Ok(Self {
167            data_pattern,
168            key_pattern,
169        })
170    }
171
172    /// Check whether `data_name` and `key_name` satisfy this rule.
173    pub fn check(&self, data_name: &Name, key_name: &Name) -> bool {
174        let mut bindings = HashMap::new();
175        self.data_pattern.matches(data_name, &mut bindings)
176            && self.key_pattern.matches(key_name, &mut bindings)
177    }
178}
179
180impl std::fmt::Display for NamePattern {
181    /// Serialize a pattern to its text form, e.g. `/sensor/<node>/KEY/<id>`.
182    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
183        if self.0.is_empty() {
184            return f.write_str("/");
185        }
186        for comp in &self.0 {
187            f.write_str("/")?;
188            match comp {
189                PatternComponent::Literal(nc) => {
190                    f.write_str(&String::from_utf8_lossy(&nc.value))?;
191                }
192                PatternComponent::Capture(var) => {
193                    write!(f, "<{var}>")?;
194                }
195                PatternComponent::MultiCapture(var) => {
196                    write!(f, "<**{var}>")?;
197                }
198            }
199        }
200        Ok(())
201    }
202}
203
204impl std::fmt::Display for SchemaRule {
205    /// Serialize a rule to its text form, e.g. `/sensor/<node> => /sensor/<node>/KEY/<id>`.
206    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
207        write!(f, "{} => {}", self.data_pattern, self.key_pattern)
208    }
209}
210
211/// A collection of trust schema rules, optionally backed by an imported
212/// LightVerSec model.
213///
214/// # Backing stores
215///
216/// A `TrustSchema` combines two independent rule sources, OR'd together:
217///
218/// 1. A vector of native [`SchemaRule`]s authored in ndn-rs's own text
219///    grammar (`data_pattern => key_pattern`). Convenient for simple
220///    hand-written policies.
221/// 2. An optional compiled [`LvsModel`] imported via
222///    [`TrustSchema::from_lvs_binary`] from the binary TLV format used by
223///    python-ndn, NDNts, and ndnd. Lets ndn-rs consume trust schemas
224///    authored in the wider NDN community's tooling.
225///
226/// [`TrustSchema::allows`] returns `true` if **either** source permits the
227/// `(data_name, key_name)` pair — you can mix a hand-written rule with an
228/// imported LVS model and both will be consulted.
229#[derive(Clone, Debug, Default)]
230pub struct TrustSchema {
231    rules: Vec<SchemaRule>,
232    lvs: Option<Arc<LvsModel>>,
233}
234
235impl TrustSchema {
236    pub fn new() -> Self {
237        Self {
238            rules: Vec::new(),
239            lvs: None,
240        }
241    }
242
243    pub fn add_rule(&mut self, rule: SchemaRule) {
244        self.rules.push(rule);
245    }
246
247    /// Construct a trust schema backed by a compiled LightVerSec model in
248    /// its TLV binary format.
249    ///
250    /// The binary format is defined at
251    /// <https://python-ndn.readthedocs.io/en/latest/src/lvs/binary-format.html>
252    /// and produced by python-ndn's LVS compiler, NDNts `@ndn/lvs`, and
253    /// ndnd. See the [`crate::lvs`] module docs for the supported feature
254    /// subset — notably, user functions (`$eq`, `$regex`, …) are parsed
255    /// but not dispatched in v0.1.0, so any rule that depends on one will
256    /// never match a packet. Inspect [`LvsModel::uses_user_functions`] on
257    /// the result of [`TrustSchema::lvs_model`] if you need to refuse
258    /// such schemas.
259    ///
260    /// The resulting schema has no native `SchemaRule`s — add them with
261    /// [`TrustSchema::add_rule`] if you want to mix the two sources.
262    pub fn from_lvs_binary(wire: &[u8]) -> Result<Self, LvsError> {
263        let model = LvsModel::decode(wire)?;
264        Ok(Self {
265            rules: Vec::new(),
266            lvs: Some(Arc::new(model)),
267        })
268    }
269
270    /// Return the imported LVS model, if this schema was constructed from
271    /// one. Use this to inspect [`LvsModel::uses_user_functions`] or walk
272    /// the node graph for diagnostics.
273    pub fn lvs_model(&self) -> Option<&LvsModel> {
274        self.lvs.as_deref()
275    }
276
277    /// Returns `true` if at least one source permits this
278    /// `(data_name, key_name)` pair. Checks native rules first (cheap), then
279    /// falls through to the LVS model if present.
280    pub fn allows(&self, data_name: &Name, key_name: &Name) -> bool {
281        if self.rules.iter().any(|r| r.check(data_name, key_name)) {
282            return true;
283        }
284        if let Some(lvs) = self.lvs.as_deref() {
285            return lvs.check(data_name, key_name);
286        }
287        false
288    }
289
290    /// Return an immutable slice of all native rules in this schema.
291    /// Does not include rules inside an imported LVS model.
292    pub fn rules(&self) -> &[SchemaRule] {
293        &self.rules
294    }
295
296    /// Remove the rule at `index`, returning it.
297    ///
298    /// Panics if `index` is out of bounds.
299    pub fn remove_rule(&mut self, index: usize) -> SchemaRule {
300        self.rules.remove(index)
301    }
302
303    /// Remove all rules, returning the schema to its empty (reject-all) state.
304    /// Also clears any imported LVS model.
305    pub fn clear(&mut self) {
306        self.rules.clear();
307        self.lvs = None;
308    }
309
310    /// Accept any signed packet regardless of name relationship.
311    ///
312    /// Useful for the `AcceptSigned` security profile and for tests.
313    pub fn accept_all() -> Self {
314        let mut schema = Self::new();
315        schema.add_rule(SchemaRule {
316            data_pattern: NamePattern(vec![PatternComponent::MultiCapture("_".into())]),
317            key_pattern: NamePattern(vec![PatternComponent::MultiCapture("_".into())]),
318        });
319        schema
320    }
321
322    /// Hierarchical trust: data and key must share a common first component.
323    ///
324    /// Rule: `/<org>/**` must be signed by `/<org>/**`. The actual hierarchy
325    /// is enforced by the certificate chain walk — a key can only be trusted
326    /// if its cert was issued by a parent key, all the way up to a trust anchor.
327    /// The schema just ensures the top-level namespace matches.
328    pub fn hierarchical() -> Self {
329        let mut schema = Self::new();
330        schema.add_rule(SchemaRule {
331            data_pattern: NamePattern(vec![
332                PatternComponent::Capture("org".into()),
333                PatternComponent::MultiCapture("_data".into()),
334            ]),
335            key_pattern: NamePattern(vec![
336                PatternComponent::Capture("org".into()),
337                PatternComponent::MultiCapture("_key".into()),
338            ]),
339        });
340        schema
341    }
342}
343
344#[cfg(test)]
345mod tests {
346    use super::*;
347    use bytes::Bytes;
348    use ndn_packet::NameComponent;
349
350    fn comp(s: &'static str) -> NameComponent {
351        NameComponent::generic(Bytes::from_static(s.as_bytes()))
352    }
353    fn name(components: &[&'static str]) -> Name {
354        Name::from_components(components.iter().map(|s| comp(s)))
355    }
356
357    #[test]
358    fn literal_matches_exact() {
359        let pat = NamePattern(vec![PatternComponent::Literal(comp("sensor"))]);
360        assert!(pat.matches(&name(&["sensor"]), &mut HashMap::new()));
361    }
362
363    #[test]
364    fn literal_rejects_wrong_component() {
365        let pat = NamePattern(vec![PatternComponent::Literal(comp("sensor"))]);
366        assert!(!pat.matches(&name(&["actuator"]), &mut HashMap::new()));
367    }
368
369    #[test]
370    fn literal_rejects_extra_components() {
371        let pat = NamePattern(vec![PatternComponent::Literal(comp("a"))]);
372        assert!(!pat.matches(&name(&["a", "b"]), &mut HashMap::new()));
373    }
374
375    #[test]
376    fn capture_binds_variable() {
377        let pat = NamePattern(vec![
378            PatternComponent::Literal(comp("sensor")),
379            PatternComponent::Capture(Arc::from("node")),
380        ]);
381        let mut bindings = HashMap::new();
382        assert!(pat.matches(&name(&["sensor", "node1"]), &mut bindings));
383        assert_eq!(bindings[&Arc::from("node")], comp("node1"));
384    }
385
386    #[test]
387    fn capture_enforces_consistency() {
388        let var: Arc<str> = Arc::from("node");
389        let data_pat = NamePattern(vec![PatternComponent::Capture(Arc::clone(&var))]);
390        let key_pat = NamePattern(vec![PatternComponent::Capture(Arc::clone(&var))]);
391        let mut bindings = HashMap::new();
392        // Bind node = "n1" via data pattern
393        assert!(data_pat.matches(&name(&["n1"]), &mut bindings));
394        // Key pattern with same value succeeds
395        assert!(key_pat.matches(&name(&["n1"]), &mut bindings.clone()));
396        // Key pattern with different value fails
397        assert!(!key_pat.matches(&name(&["n2"]), &mut bindings));
398    }
399
400    #[test]
401    fn multi_capture_consumes_remaining() {
402        let pat = NamePattern(vec![
403            PatternComponent::Literal(comp("prefix")),
404            PatternComponent::MultiCapture(Arc::from("rest")),
405        ]);
406        assert!(pat.matches(&name(&["prefix", "a", "b", "c"]), &mut HashMap::new()));
407    }
408
409    #[test]
410    fn schema_rule_allows_matching_pair() {
411        let rule = SchemaRule {
412            data_pattern: NamePattern(vec![PatternComponent::Literal(comp("data"))]),
413            key_pattern: NamePattern(vec![PatternComponent::Literal(comp("key"))]),
414        };
415        assert!(rule.check(&name(&["data"]), &name(&["key"])));
416        assert!(!rule.check(&name(&["data"]), &name(&["wrong"])));
417    }
418
419    #[test]
420    fn trust_schema_allows_via_any_rule() {
421        let mut schema = TrustSchema::new();
422        schema.add_rule(SchemaRule {
423            data_pattern: NamePattern(vec![PatternComponent::Literal(comp("data"))]),
424            key_pattern: NamePattern(vec![PatternComponent::Literal(comp("key"))]),
425        });
426        assert!(schema.allows(&name(&["data"]), &name(&["key"])));
427        assert!(!schema.allows(&name(&["data"]), &name(&["wrong"])));
428    }
429
430    #[test]
431    fn empty_schema_rejects_everything() {
432        let schema = TrustSchema::new();
433        assert!(!schema.allows(&name(&["a"]), &name(&["b"])));
434    }
435
436    #[test]
437    fn accept_all_allows_any_pair() {
438        let schema = TrustSchema::accept_all();
439        assert!(schema.allows(&name(&["a", "b"]), &name(&["x", "y", "z"])));
440        assert!(schema.allows(&name(&["data"]), &name(&["key"])));
441    }
442
443    #[test]
444    fn pattern_parse_literal() {
445        let p = NamePattern::parse("/sensor/temp").unwrap();
446        assert_eq!(p.0.len(), 2);
447        assert!(matches!(&p.0[0], PatternComponent::Literal(nc) if nc.value.as_ref() == b"sensor"));
448        assert!(matches!(&p.0[1], PatternComponent::Literal(nc) if nc.value.as_ref() == b"temp"));
449    }
450
451    #[test]
452    fn pattern_parse_captures() {
453        let p = NamePattern::parse("/sensor/<node>/KEY/<id>").unwrap();
454        assert_eq!(p.0.len(), 4);
455        assert!(matches!(&p.0[0], PatternComponent::Literal(_)));
456        assert!(matches!(&p.0[1], PatternComponent::Capture(v) if v.as_ref() == "node"));
457        assert!(matches!(&p.0[2], PatternComponent::Literal(_)));
458        assert!(matches!(&p.0[3], PatternComponent::Capture(v) if v.as_ref() == "id"));
459    }
460
461    #[test]
462    fn pattern_parse_multi_capture_at_end() {
463        let p = NamePattern::parse("/org/<**rest>").unwrap();
464        assert_eq!(p.0.len(), 2);
465        assert!(matches!(&p.0[1], PatternComponent::MultiCapture(v) if v.as_ref() == "rest"));
466    }
467
468    #[test]
469    fn pattern_parse_multi_capture_not_last_errors() {
470        assert!(matches!(
471            NamePattern::parse("/org/<**rest>/extra"),
472            Err(PatternParseError::MultiCaptureNotLast)
473        ));
474    }
475
476    #[test]
477    fn pattern_parse_unclosed_capture_errors() {
478        assert!(matches!(
479            NamePattern::parse("/sensor/<node"),
480            Err(PatternParseError::UnclosedCapture)
481        ));
482    }
483
484    #[test]
485    fn pattern_roundtrip_text() {
486        let s = "/sensor/<node>/KEY/<id>";
487        let p = NamePattern::parse(s).unwrap();
488        assert_eq!(p.to_string(), s);
489    }
490
491    #[test]
492    fn pattern_roundtrip_multi() {
493        let s = "/org/<**rest>";
494        let p = NamePattern::parse(s).unwrap();
495        assert_eq!(p.to_string(), s);
496    }
497
498    #[test]
499    fn rule_parse_roundtrip() {
500        let s = "/sensor/<node>/<type> => /sensor/<node>/KEY/<id>";
501        let r = SchemaRule::parse(s).unwrap();
502        assert_eq!(r.to_string(), s);
503    }
504
505    #[test]
506    fn rule_parse_bad_separator_errors() {
507        assert!(matches!(
508            SchemaRule::parse("/a /b"),
509            Err(PatternParseError::BadRuleSeparator)
510        ));
511    }
512
513    #[test]
514    fn schema_remove_rule() {
515        let mut schema = TrustSchema::new();
516        schema.add_rule(SchemaRule {
517            data_pattern: NamePattern(vec![PatternComponent::Literal(comp("data"))]),
518            key_pattern: NamePattern(vec![PatternComponent::Literal(comp("key"))]),
519        });
520        assert!(schema.allows(&name(&["data"]), &name(&["key"])));
521        schema.remove_rule(0);
522        assert!(!schema.allows(&name(&["data"]), &name(&["key"])));
523    }
524
525    #[test]
526    fn schema_rules_returns_slice() {
527        let mut schema = TrustSchema::new();
528        schema.add_rule(SchemaRule {
529            data_pattern: NamePattern(vec![PatternComponent::Literal(comp("d"))]),
530            key_pattern: NamePattern(vec![PatternComponent::Literal(comp("k"))]),
531        });
532        assert_eq!(schema.rules().len(), 1);
533    }
534
535    // ── LVS binary import integration ─────────────────────────────────────
536
537    /// Build a minimal LVS binary fixture equivalent to the native rule
538    /// `"/app => /key"` — root has two ValueEdges, and the "app" node's
539    /// SignConstraint points at the "key" node.
540    fn lvs_hierarchical_fixture() -> Vec<u8> {
541        use crate::lvs::type_number as tn;
542        use bytes::BytesMut;
543        use ndn_tlv::TlvWriter;
544
545        fn write_tlv(buf: &mut BytesMut, t: u64, v: &[u8]) {
546            let mut w = TlvWriter::new();
547            w.write_tlv(t, v);
548            buf.extend_from_slice(&w.finish());
549        }
550        fn uint_tlv(buf: &mut BytesMut, t: u64, v: u64) {
551            let be = if v <= u8::MAX as u64 {
552                vec![v as u8]
553            } else {
554                (v as u32).to_be_bytes().to_vec()
555            };
556            write_tlv(buf, t, &be);
557        }
558        /// Write COMPONENT_VALUE TLV wrapping a GenericNameComponent.
559        fn write_cv(buf: &mut BytesMut, bytes: &[u8]) {
560            let mut nc = Vec::with_capacity(2 + bytes.len());
561            nc.push(0x08);
562            nc.push(bytes.len() as u8);
563            nc.extend_from_slice(bytes);
564            write_tlv(buf, tn::COMPONENT_VALUE, &nc);
565        }
566
567        let mut out = BytesMut::new();
568        uint_tlv(&mut out, tn::VERSION, crate::lvs::LVS_VERSION);
569        uint_tlv(&mut out, tn::NODE_ID, 0);
570        uint_tlv(&mut out, tn::NAMED_PATTERN_NUM, 0);
571
572        // Node 0 (root) with two ValueEdges
573        {
574            let mut node = BytesMut::new();
575            uint_tlv(&mut node, tn::NODE_ID, 0);
576            {
577                let mut ve = BytesMut::new();
578                uint_tlv(&mut ve, tn::NODE_ID, 1);
579                write_cv(&mut ve, b"app");
580                write_tlv(&mut node, tn::VALUE_EDGE, &ve);
581            }
582            {
583                let mut ve = BytesMut::new();
584                uint_tlv(&mut ve, tn::NODE_ID, 2);
585                write_cv(&mut ve, b"key");
586                write_tlv(&mut node, tn::VALUE_EDGE, &ve);
587            }
588            write_tlv(&mut out, tn::NODE, &node);
589        }
590        // Node 1 (app data endpoint) — sign_cons = [2]
591        {
592            let mut node = BytesMut::new();
593            uint_tlv(&mut node, tn::NODE_ID, 1);
594            uint_tlv(&mut node, tn::PARENT_ID, 0);
595            uint_tlv(&mut node, tn::KEY_NODE_ID, 2);
596            write_tlv(&mut out, tn::NODE, &node);
597        }
598        // Node 2 (key leaf / trust anchor)
599        {
600            let mut node = BytesMut::new();
601            uint_tlv(&mut node, tn::NODE_ID, 2);
602            uint_tlv(&mut node, tn::PARENT_ID, 0);
603            write_tlv(&mut out, tn::NODE, &node);
604        }
605        out.to_vec()
606    }
607
608    #[test]
609    fn trust_schema_from_lvs_binary_roundtrip() {
610        let schema = TrustSchema::from_lvs_binary(&lvs_hierarchical_fixture()).expect("LVS import");
611        assert!(schema.lvs_model().is_some());
612        assert!(schema.allows(&name(&["app"]), &name(&["key"])));
613        assert!(!schema.allows(&name(&["app"]), &name(&["wrong"])));
614        assert!(!schema.allows(&name(&["stranger"]), &name(&["key"])));
615    }
616
617    #[test]
618    fn trust_schema_mixes_native_rules_with_lvs_model() {
619        let mut schema = TrustSchema::from_lvs_binary(&lvs_hierarchical_fixture()).unwrap();
620        // Native rule allows an entirely different namespace the LVS model
621        // knows nothing about. Both must work in the same schema.
622        schema.add_rule(SchemaRule::parse("/native => /native/KEY").unwrap());
623
624        // LVS-side check.
625        assert!(schema.allows(&name(&["app"]), &name(&["key"])));
626        // Native-side check.
627        assert!(schema.allows(&name(&["native"]), &name(&["native", "KEY"])));
628        // Still rejects things neither source covers.
629        assert!(!schema.allows(&name(&["foo"]), &name(&["bar"])));
630    }
631
632    #[test]
633    fn trust_schema_lvs_model_accessor_returns_parsed_model() {
634        let schema = TrustSchema::from_lvs_binary(&lvs_hierarchical_fixture()).unwrap();
635        let model = schema.lvs_model().expect("lvs model set");
636        assert_eq!(model.nodes.len(), 3);
637        assert!(!model.uses_user_functions());
638    }
639
640    #[test]
641    fn trust_schema_from_lvs_binary_bad_version_errors() {
642        use crate::lvs::LvsError;
643        let mut bad = lvs_hierarchical_fixture();
644        // Overwrite version TLV value byte (offset depends on the fixture
645        // layout; we know it's the first TLV with VERSION type 0x61 and a
646        // 4-byte payload). Easier: build a fresh fixture with a bad version.
647        bad.clear();
648        use crate::lvs::type_number as tn;
649        use bytes::BytesMut;
650        use ndn_tlv::TlvWriter;
651        let mut out = BytesMut::new();
652        {
653            let mut w = TlvWriter::new();
654            w.write_tlv(tn::VERSION, &0xDEADBEEFu32.to_be_bytes());
655            out.extend_from_slice(&w.finish());
656            let mut w = TlvWriter::new();
657            w.write_tlv(tn::NODE_ID, &[0u8]);
658            out.extend_from_slice(&w.finish());
659            let mut w = TlvWriter::new();
660            w.write_tlv(tn::NAMED_PATTERN_NUM, &[0u8]);
661            out.extend_from_slice(&w.finish());
662        }
663        let err = TrustSchema::from_lvs_binary(&out).unwrap_err();
664        assert!(matches!(err, LvsError::UnsupportedVersion { .. }));
665    }
666
667    // ── Original hierarchical test ─────────────────────────────────────────
668
669    #[test]
670    fn hierarchical_requires_matching_first_component() {
671        let schema = TrustSchema::hierarchical();
672        // Same org: allowed
673        assert!(schema.allows(&name(&["org", "data"]), &name(&["org", "KEY", "k1"])));
674        // Different org: rejected
675        assert!(!schema.allows(&name(&["orgA", "data"]), &name(&["orgB", "KEY", "k1"])));
676        // Same org, deeper hierarchy: allowed
677        assert!(schema.allows(
678            &name(&["org", "dept", "sensor", "temp"]),
679            &name(&["org", "dept", "KEY", "k1"])
680        ));
681    }
682}