@ndn/tlv
Version:
NDNts: TLV
145 lines (144 loc) • 5.38 kB
JavaScript
import { assert } from "@ndn/util";
import {} from "./decoder_browser.js";
import {} from "./encoder_browser.js";
import { EvDecoder } from "./ev-decoder_browser.js";
import { encodeFields, makeField, sortFields } from "./impl-field_browser.js";
/**
* Helper to build a base class that represents a TLV structure.
*
* @remarks
* StructBuilder allows you to define the typing, constructor, encoder, and decoder, while writing
* each field only once. To be compatible with StructBuilder, the TLV structure being described
* shall contain a sequence of sub-TLV elements with distinct TLV-TYPE numbers, where each
* sub-TLV-TYPE appears zero, one, or multiple times.
*
* To use StructBuilder, calling code should follow these steps:
* 1. Invoke `.add()` method successively to define sub-TLV elements.
* 2. Obtain a base class via `.baseClass()` method, which contains one field for each sub-TLV-TYPE
* as defined, along with constructor, encoding, and decoding functions.
* 3. Declare a subclass deriving from this base class, to add more functionality.
* 4. Assign the subclass constructor to `.subclass` property of the builder.
*/
export class StructBuilder {
typeName;
topTT;
/**
* Constructor.
* @param typeName - Type name, used in error messages.
* @param topTT - If specified, encode as complete TLV; otherwise, encode as TLV-VALUE only.
*/
constructor(typeName, topTT) {
this.typeName = typeName;
this.topTT = topTT;
this.EVD = new EvDecoder(typeName, topTT);
}
/**
* Subclass constructor.
* This must be assigned, otherwise decoding function will not work.
*/
subclass;
fields = [];
flagBits = [];
EVD;
/** Access EvDecoder for certain customizations. */
static evdOf(sb) {
return sb.EVD;
}
/** Retrieve field names. */
static keysOf(sb) {
return sb.fields.map(({ key }) => key);
}
/**
* Add a field.
* @param tt - TLV-TYPE number.
* @param key - Field name on the base class.
* @param type - Field type.
* @param opts - Field options.
* @returns StructBuilder annotated with field typing.
*/
add(tt, key, type, opts = {}) {
const field = makeField(tt, key, type, opts, this.EVD);
const { flagPrefix = key, flagBits } = opts;
if (flagBits) {
field.asString = function* (v) {
if (typeof v !== "number") {
return;
}
yield ` ${key}=0x${v.toString(16).toUpperCase()}(`;
let delim = "";
for (const [str, bit] of Object.entries(flagBits)) {
if ((v & bit) !== 0) {
yield `${delim}${str}`;
delim = "|";
}
}
yield ")";
};
for (const [str, bit] of Object.entries(flagBits)) {
const prop = flagPrefix + str.slice(0, 1).toUpperCase() + str.slice(1);
this.flagBits.push({ key, prop, bit });
}
}
this.fields.push(field);
return this;
}
/** Change IsCritical on the EvDecoder. */
setIsCritical(cb) {
this.EVD.setIsCritical(cb);
return this;
}
/**
* Obtain a base class for the TLV structure class.
* @typeParam S - Subclass type.
*/
baseClass() {
sortFields(this.fields);
const b = this; // eslint-disable-line unicorn/no-this-assignment, @typescript-eslint/no-this-alias
return class {
constructor() {
for (const { key, newValue: construct } of b.fields) {
this[key] = construct();
}
for (const { key, prop, bit } of b.flagBits) {
Object.defineProperty(this, prop, {
configurable: true,
enumerable: false,
get() {
return (((this)[key] ?? 0) & bit) !== 0;
},
set(v) {
(this)[key] ??= 0;
if (v) {
(this)[key] |= bit;
}
else {
(this)[key] &= ~bit;
}
},
});
}
}
encodeTo(encoder) {
const elements = encodeFields(b.fields, this);
if (b.topTT === undefined) {
encoder.encode(elements);
}
else {
encoder.prependTlv(b.topTT, ...elements);
}
}
static decodeFrom(decoder) {
assert(b.subclass, `StructBuilder(${b.typeName}).subclass is unset`);
const t = new b.subclass();
return b.EVD[b.topTT === undefined ? "decodeValue" : "decode"](t, decoder);
}
toString() {
const tokens = [b.typeName];
for (const { key, asString } of b.fields) {
tokens.push(...asString(this[key]));
}
return tokens.join("");
}
};
}
}