Static read-through audit of crates/dpp-digital-link. Five verified findings. No unsafe, no crypto, no reachable panics outside test code.
1. Trailing unpaired AI code is silently dropped
Location: src/digital_link/link.rs:78
Issue: while i + 1 < ai_segments.len() silently drops a dangling AI code with no paired value instead of erroring.
Failure scenario: https://id.odal-node.io/01/09506000134352/21 (odd segment count) parses successfully with serial: None instead of returning an error for the truncated/malformed URI.
2. Duplicate primary-key AI creates parser-differential ambiguity
Location: src/digital_link/link.rs:87-90
Issue: A second 01 segment later in the path unconditionally overwrites gtin with no duplicate/position check.
Failure scenario: .../01/09506000134352/21/SN1/01/00000000000001 parses; resolver_base's path prefix is computed from the first 01 occurrence while the final gtin field reflects the last one. Two consumers of this struct can disagree about which GTIN the URI refers to.
3. GS1-mandated length limits are declared but never enforced
Location: src/digital_link/ai.rs:15, src/digital_link/link.rs::parse
Issue: max_len is defined per-AI but never read in parse. Values decoded from an untrusted URI (percent_decode, line 84) have no length cap.
Failure scenario: A crafted Digital Link URI with an arbitrarily long serial/batch/variant value is accepted and propagated downstream into AAS submodels/JSON-LD payloads unbounded.
4. Non-object JSON silently discards the entire passport payload
Location: src/jsonld/context.rs:33-41
Issue: frame_passport returns a bare @context envelope with no error if passport isn't a JSON object.
Failure scenario: Any caller passing a non-object Value (e.g. from unvalidated deserialized input) gets a "successful" result containing no data, silently.
5. Dead code branch
Location: src/digital_link/link.rs:111-113
Issue: The AiRole::DataAttribute match arm is unreachable — no descriptor in AI_TABLE currently has that role.
Static read-through audit of
crates/dpp-digital-link. Five verified findings. Nounsafe, no crypto, no reachable panics outside test code.1. Trailing unpaired AI code is silently dropped
Location:
src/digital_link/link.rs:78Issue:
while i + 1 < ai_segments.len()silently drops a dangling AI code with no paired value instead of erroring.Failure scenario:
https://id.odal-node.io/01/09506000134352/21(odd segment count) parses successfully withserial: Noneinstead of returning an error for the truncated/malformed URI.2. Duplicate primary-key AI creates parser-differential ambiguity
Location:
src/digital_link/link.rs:87-90Issue: A second
01segment later in the path unconditionally overwritesgtinwith no duplicate/position check.Failure scenario:
.../01/09506000134352/21/SN1/01/00000000000001parses;resolver_base's path prefix is computed from the first01occurrence while the finalgtinfield reflects the last one. Two consumers of this struct can disagree about which GTIN the URI refers to.3. GS1-mandated length limits are declared but never enforced
Location:
src/digital_link/ai.rs:15,src/digital_link/link.rs::parseIssue:
max_lenis defined per-AI but never read inparse. Values decoded from an untrusted URI (percent_decode, line 84) have no length cap.Failure scenario: A crafted Digital Link URI with an arbitrarily long serial/batch/variant value is accepted and propagated downstream into AAS submodels/JSON-LD payloads unbounded.
4. Non-object JSON silently discards the entire passport payload
Location:
src/jsonld/context.rs:33-41Issue:
frame_passportreturns a bare@contextenvelope with no error ifpassportisn't a JSON object.Failure scenario: Any caller passing a non-object
Value(e.g. from unvalidated deserialized input) gets a "successful" result containing no data, silently.5. Dead code branch
Location:
src/digital_link/link.rs:111-113Issue: The
AiRole::DataAttributematch arm is unreachable — no descriptor inAI_TABLEcurrently has that role.