@ndn/tlv
Version:
NDNts: TLV
133 lines (132 loc) • 4.51 kB
JavaScript
import { assert } from "@ndn/util";
import { printTT } from "./string_node.js";
const AUTO_ORDER_SKIP = 100;
function nest(evd) {
return (target, { decoder }) => { evd.decode(target, decoder); };
}
function isCritical(tt) {
return tt <= 0x1F || tt % 2 === 1;
}
/**
* TLV-VALUE decoder that understands Packet Format v0.3 evolvability guidelines.
* @typeParam T - Target type being decoded.
*/
export class EvDecoder {
typeName;
topTT;
rules = new Map();
requiredTT = new Set();
nextOrder = AUTO_ORDER_SKIP;
isCritical = isCritical;
unknownHandler;
/** Callbacks before decoding TLV-VALUE. */
beforeObservers = [];
/** Callbacks after decoding TLV-VALUE. */
afterObservers = [];
/**
* Constructor.
* @param typeName - type name, used in error messages.
* @param topTT - If specified, the top-level TLV-TYPE will be checked to be in this list.
*/
constructor(typeName, topTT = []) {
this.typeName = typeName;
this.topTT = typeof topTT === "number" ? [topTT] : topTT;
}
applyDefaultsToRuleOptions({ order = (this.nextOrder += AUTO_ORDER_SKIP), required = false, repeat = false, } = {}) {
return { order, required, repeat };
}
/**
* Add a decoding rule.
* @param tt - TLV-TYPE to match this rule.
* @param cb - Callback or nested EvDecoder to handle element TLV.
* @param opts - Additional rule options.
*/
add(tt, cb, opts = {}) {
const ro = this.applyDefaultsToRuleOptions(opts);
assert(!this.rules.has(tt), "duplicate rule for same TLV-TYPE");
this.rules.set(tt, {
...ro,
cb: cb instanceof EvDecoder ? nest(cb) : cb,
});
if (ro.required) {
this.requiredTT.add(tt);
}
return this;
}
/** Set callback to determine whether TLV-TYPE is critical. */
setIsCritical(cb) {
this.isCritical = cb;
return this;
}
/** Set callback to handle unknown elements. */
setUnknown(cb) {
this.unknownHandler = cb;
return this;
}
/** Decode TLV to target object. */
decode(target, decoder) {
const topTlv = decoder.read();
if (this.topTT.length > 0 && !this.topTT.includes(topTlv.type)) {
throw new Error(`TLV-TYPE ${printTT(topTlv.type)} is not ${this.typeName}`);
}
return this.decodeV(target, topTlv.vd, topTlv);
}
/** Decode TLV-VALUE to target object. */
decodeValue(target, vd) {
return this.decodeV(target, vd);
}
decodeV(target, vd, topTlv) {
for (const cb of this.beforeObservers) {
cb(target, topTlv);
}
let currentOrder = 0;
const foundTT = new Set();
const missingTT = new Set(this.requiredTT);
while (!vd.eof) {
const tlv = vd.read();
const tt = tlv.type;
const rule = this.rules.get(tt);
if (!rule) {
if (!this.unknownHandler?.(target, tlv, currentOrder)) {
this.handleUnrecognized(tt, "unknown");
}
continue;
}
if (currentOrder > rule.order) {
this.handleUnrecognized(tt, "out of order");
continue;
}
currentOrder = rule.order;
if (!rule.repeat && foundTT.has(tt)) {
throw new Error(`TLV-TYPE ${printTT(tt)} cannot repeat in ${this.typeName}`);
}
foundTT.add(tt);
missingTT.delete(tt);
rule.cb(target, tlv);
}
if (missingTT.size > 0) {
throw new Error(`TLV-TYPE ${Array.from(missingTT, printTT).join(",")} missing in ${this.typeName}`);
}
for (const cb of this.afterObservers) {
cb(target, topTlv);
}
return target;
}
handleUnrecognized(tt, reason) {
if (this.isCritical(tt)) {
throw new Error(`TLV-TYPE ${printTT(tt)} is ${reason} in ${this.typeName}`);
}
}
}
(function (EvDecoder) {
/**
* IsCritical callback that always returns `false`.
* Any unrecognized or out-of-order TLV elements would be ignored.
*/
EvDecoder.neverCritical = () => false;
/**
* IsCritical callback that always returns `true`.
* Any unrecognized or out-of-order TLV elements would cause an error.
*/
EvDecoder.alwaysCritical = () => true;
})(EvDecoder || (EvDecoder = {}));