Skip to content

Commit

Permalink
tlv: shorten EvDecoder construction syntax
Browse files Browse the repository at this point in the history
  • Loading branch information
yoursunny committed Sep 22, 2019
1 parent 65c3adf commit 8a2b6a8
Show file tree
Hide file tree
Showing 4 changed files with 185 additions and 101 deletions.
22 changes: 11 additions & 11 deletions packages/l3pkt/src/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,17 @@ const FAKESIG = new Uint8Array([
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
]);

const EVD = new EvDecoder<Data>(TT.Data, [
{ tt: TT.Name, cb: (self, { decoder }) => { self.name = decoder.decode(Name); } },
{ tt: TT.MetaInfo, cb: EvDecoder.Nest(new EvDecoder<Data>(TT.MetaInfo, [
{ tt: TT.ContentType, cb: (self, { value }) => { self.contentType = NNI.decode(value); } },
{ tt: TT.FreshnessPeriod, cb: (self, { value }) => { self.freshnessPeriod = NNI.decode(value); } },
{ tt: TT.FinalBlockId, cb: (self, { vd }) => { self.finalBlockId = Component.decodeFrom(vd); } },
])) },
{ tt: TT.Content, cb: (self, { value }) => { self.content = value; } },
{ tt: TT.DSigInfo, cb: () => undefined },
{ tt: TT.DSigValue, cb: () => undefined },
]);
const EVD = new EvDecoder<Data>("Data", TT.Data)
.add(TT.Name, (self, { decoder }) => { self.name = decoder.decode(Name); })
.add(TT.MetaInfo,
new EvDecoder<Data>("MetaInfo")
.add(TT.ContentType, (self, { value }) => { self.contentType = NNI.decode(value); })
.add(TT.FreshnessPeriod, (self, { value }) => { self.freshnessPeriod = NNI.decode(value); })
.add(TT.FinalBlockId, (self, { vd }) => { self.finalBlockId = Component.decodeFrom(vd); }),
)
.add(TT.Content, (self, { value }) => { self.content = value; })
.add(TT.DSigInfo, () => undefined)
.add(TT.DSigValue, () => undefined);

/** Data packet. */
export class Data {
Expand Down
19 changes: 9 additions & 10 deletions packages/l3pkt/src/interest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,15 @@ import { TT } from "./an";
const LIFETIME_DEFAULT = 4000;
const HOPLIMIT_MAX = 255;

const EVD = new EvDecoder<Interest>(TT.Interest, [
{ tt: TT.Name, cb: (self, { decoder }) => { self.name = decoder.decode(Name); } },
{ tt: TT.CanBePrefix, cb: (self) => { self.canBePrefix = true; } },
{ tt: TT.MustBeFresh, cb: (self) => { self.mustBeFresh = true; } },
// TODO ForwardingHint
{ tt: TT.Nonce, cb: (self, { value }) => { self.nonce = NNI.decode(value, 4); } },
{ tt: TT.InterestLifetime, cb: (self, { value }) => { self.lifetime = NNI.decode(value); } },
{ tt: TT.HopLimit, cb: (self, { value }) => { self.hopLimit = NNI.decode(value, 1); } },
// TODO AppParameters, ISigInfo, ISigValue
]);
const EVD = new EvDecoder<Interest>("Interest", TT.Interest)
.add(TT.Name, (self, { decoder }) => { self.name = decoder.decode(Name); })
.add(TT.CanBePrefix, (self) => { self.canBePrefix = true; })
.add(TT.MustBeFresh, (self) => { self.mustBeFresh = true; })
// TODO ForwardingHint
.add(TT.Nonce, (self, { value }) => { self.nonce = NNI.decode(value, 4); })
.add(TT.InterestLifetime, (self, { value }) => { self.lifetime = NNI.decode(value); })
.add(TT.HopLimit, (self, { value }) => { self.hopLimit = NNI.decode(value, 1); });
// TODO AppParameters, ISigInfo, ISigValue

/** Interest packet. */
export class Interest {
Expand Down
132 changes: 83 additions & 49 deletions packages/tlv/src/ev-decoder.ts
Original file line number Diff line number Diff line change
@@ -1,54 +1,110 @@
import { Decoder } from "./decoder";
import { printTT } from "./string";

/** Invoked when a matching TLV element is found. */
type ElementCallback<T> = (target: T, tlv: Decoder.Tlv) => any;

interface Rule<T> {
cb: ElementCallback<T>;

/**
* Expected order of appearance.
* Default to the order in which rules were added to EvDecoder.
*/
order: number;

/** Whether TLV element may appear more than once. */
repeat: boolean;
}

type RuleOptions<T> = Partial<Omit<Rule<T>, "cb">>;

const AUTO_ORDER_SKIP = 100;

/**
* Invoked when a TLV element does not match any rule.
* 'order' denotes the order number of last recognized TLV element.
* Return true if this TLV element is accepted, or false to follow evolvability guidelines.
*/
type UnknownElementCallback<T> = (target: T, tlv: Decoder.Tlv, order: number) => boolean;

function nest<T>(evd: EvDecoder<T>): ElementCallback<T> {
return (target, { decoder }) => { evd.decode(target, decoder); };
}

function isCritical(tt: number): boolean {
return tt <= 0x1F || tt % 2 === 1;
}

/** TLV-VALUE decoder that understands Packet Format v0.3 evolvability rules. */
/** TLV-VALUE decoder that understands Packet Format v0.3 evolvability guidelines. */
export class EvDecoder<T> {
private ttIndex: {[tt: number]: number};
private topTT: number[];
private rules = {} as Record<number, Rule<T>>;
private nextOrder = AUTO_ORDER_SKIP;

/**
* Constructor
* @param typeName type name, used in error messages.
* @param topTT if specified, check top-level TLV-TYPE to be in this list.
*/
constructor(private typeName: string, topTT?: number|number[]) {
this.topTT = !topTT ? [] : Array.isArray(topTT) ? topTT : [topTT];
}

/**
* Constructor.
* @param tlvType top-level TLV-TYPE.
* @param rules rules to decode TLV-VALUE elements, in the order of expected appearance.
* Add a decoding rule.
* @param tt TLV-TYPE to match this rule.
* @param cb callback to handle element TLV.
* @param options additional rule options.
*/
constructor(private tlvType: number, private rules: Array<EvDecoder.Rule<T>>) {
this.ttIndex = Object.fromEntries(this.rules.map((rule, i) => [rule.tt, i]));
public add(tt: number, cb: ElementCallback<T>|EvDecoder<T>,
options?: RuleOptions<T>): EvDecoder<T> {
if (cb instanceof EvDecoder) {
cb = nest(cb);
}
this.rules[tt] = Object.assign({ cb, order: this.nextOrder, repeat: false } as Rule<T>, options);
this.nextOrder += AUTO_ORDER_SKIP;
return this;
}

/** Set callback to handle unknown elements. */
public setUnknown(cb: UnknownElementCallback<T>): EvDecoder<T> {
this.unknownCb = cb;
return this;
}

/** Decode TLV element. */
/** Decode to target object. */
public decode(target: T, decoder: Decoder): T {
const { type, vd } = decoder.read();
if (type !== this.tlvType) {
throw new Error(`want TLV-TYPE ${printTT(this.tlvType)} but got ${printTT(type)}`);
if (this.topTT.length && !this.topTT.includes(type)) {
throw new Error(`TLV-TYPE ${printTT(type)} is not ${this.typeName}`);
}

let currentIndex = 0;
let currentOccurs = 0;
let currentOrder = 0;
let currentCount = 0;
while (!vd.eof) {
const tlv = vd.read();
const tt = tlv.type;
const i: number|undefined = this.ttIndex[tt];
if (typeof i === "undefined") {
this.handleUnrecognized(tt, "unknown");
const rule: Rule<T>|undefined = this.rules[tt];
if (typeof rule === "undefined") {
if (!this.unknownCb(target, tlv, currentOrder)) {
this.handleUnrecognized(tt, "unknown");
}
continue;
}
const rule = this.rules[i];

if (currentIndex > i) {
if (currentOrder > rule.order) {
this.handleUnrecognized(tt, "out of order");
continue;
}

if (currentIndex < i) {
currentIndex = i;
currentOccurs = 0;
if (currentOrder < rule.order) {
currentOrder = rule.order;
currentCount = 0;
}
++currentOccurs;
if (!rule.repeatable && currentOccurs > 1) {
throw new Error(`TLV-TYPE ${printTT(tt)} cannot repeat in ${printTT(this.tlvType)}`);
++currentCount;
if (!rule.repeat && currentCount > 1) {
throw new Error(`TLV-TYPE ${printTT(tt)} cannot repeat in ${this.typeName}`);
}

rule.cb(target, tlv);
Expand All @@ -57,33 +113,11 @@ export class EvDecoder<T> {
return target;
}

private unknownCb: UnknownElementCallback<T> = () => false;

private handleUnrecognized(tt: number, reason: string) {
if (!isCritical(tt)) {
return;
if (isCritical(tt)) {
throw new Error(`TLV-TYPE ${printTT(tt)} is ${reason} in ${this.typeName}`);
}
throw new Error(`TLV-TYPE ${printTT(tt)} is ${reason} in ${printTT(this.tlvType)}`);
}
}

type ElementCallback<T> = (target: T, tlv: Decoder.Tlv) => any;

export namespace EvDecoder {
/** TLV element decoding rule. */
export interface Rule<T> {
/** TLV-TYPE number. */
tt: number;
/** Callback to record TLV element. */
cb: ElementCallback<T>;
/** Whether this TLV-TYPE may appear more than once, default is false. */
repeatable?: boolean;
}

/**
* Use a nested EvDecoder as Rule.cb().
*
* Generally, T would be same as the target type of top level EvDecoder.
*/
export function Nest<T>(evd: EvDecoder<T>): ElementCallback<T> {
return (target, { decoder }) => { evd.decode(target, decoder); };
}
}
113 changes: 82 additions & 31 deletions packages/tlv/tests/ev-decoder.t.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ class EvdTestTarget {
public a6 = 0;
public a9 = 0;
public c1 = 0;
public c2 = 0;

public sum(): number {
return this.a1 * 1000 + this.a4 * 100 + this.a6 * 10 + this.a9;
Expand All @@ -23,18 +24,18 @@ class A1 {
}
}

const EVD = new EvDecoder<EvdTestTarget>(0xA0, [
{ tt: 0xA1, cb: (self, { decoder }) => { ++self.a1; decoder.decode(A1); } },
{ tt: 0xA4, cb: (self) => { ++self.a4; } },
{ tt: 0xA6, cb: (self) => { ++self.a6; }, repeatable: true },
{ tt: 0xA9, cb: (self) => { ++self.a9; } },
{ tt: 0xC0, cb: EvDecoder.Nest(new EvDecoder<EvdTestTarget>(0xC0, [
{ tt: 0xC1, cb: (self, { value }) => { self.c1 = NNI.decode(value); } },
])) },
]);

test("simple", () => {
let decoder = new Decoder(new Uint8Array([
const EVD = new EvDecoder<EvdTestTarget>("A0", 0xA0)
.add(0xA1, (self, { decoder }) => { ++self.a1; decoder.decode(A1); })
.add(0xA4, (self) => { ++self.a4; })
.add(0xA6, (self) => { ++self.a6; }, { repeat: true })
.add(0xA9, (self) => { ++self.a9; })
.add(0xC0,
new EvDecoder<EvdTestTarget>("C0")
.add(0xC1, (self, { value }) => { self.c1 = NNI.decode(value); }),
);

test("decode normal", () => {
const decoder = new Decoder(new Uint8Array([
0xA0, 0x11,
0xA1, 0x01, 0x10,
0xA4, 0x00,
Expand All @@ -43,60 +44,110 @@ test("simple", () => {
0xA9, 0x00,
0xC0, 0x04, 0xC1, 0x02, 0x01, 0x04,
]));
let target = new EvdTestTarget();
const target = new EvdTestTarget();
EVD.decode(target, decoder);
expect(target.sum()).toBe(1121);
expect(target.c1).toBe(0x0104);
});

decoder = new Decoder(new Uint8Array([
test("decode unknown non-critical", () => {
const decoder = new Decoder(new Uint8Array([
0xA0, 0x02,
0xA2, 0x00, // non-critical
]));
target = new EvdTestTarget();
const target = new EvdTestTarget();
EVD.decode(target, decoder);
expect(target.sum()).toBe(0);
});

decoder = new Decoder(new Uint8Array([
test("decode unknown critical", () => {
const decoder = new Decoder(new Uint8Array([
0xA0, 0x02,
0xA3, 0x00, // critical
0xA3, 0x00,
]));
target = new EvdTestTarget();
const target = new EvdTestTarget();
expect(() => EVD.decode(target, decoder)).toThrow();
});

decoder = new Decoder(new Uint8Array([
test("decode unknown critical in grandfathered range", () => {
const decoder = new Decoder(new Uint8Array([
0xA0, 0x02,
0x10, 0x00, // critical
0x10, 0x00,
]));
target = new EvdTestTarget();
const target = new EvdTestTarget();
expect(() => EVD.decode(target, decoder)).toThrow();
});

decoder = new Decoder(new Uint8Array([
test("decode non-repeatable", () => {
const decoder = new Decoder(new Uint8Array([
0xA0, 0x05,
0xA1, 0x01, 0x10,
0xA1, 0x00, // cannot repeat
]));
target = new EvdTestTarget();
const target = new EvdTestTarget();
expect(() => EVD.decode(target, decoder)).toThrow();
});

decoder = new Decoder(new Uint8Array([
test("decode out-of-order critical", () => {
const decoder = new Decoder(new Uint8Array([
0xA0, 0x04,
0xA4, 0x00,
0xA1, 0x00, // out of order, critical
0xA1, 0x00,
]));
target = new EvdTestTarget();
const target = new EvdTestTarget();
expect(() => EVD.decode(target, decoder)).toThrow();
});

decoder = new Decoder(new Uint8Array([
test("decode out-of-order non-critical", () => {
const decoder = new Decoder(new Uint8Array([
0xA0, 0x06,
0xA6, 0x00,
0xA9, 0x00,
0xA6, 0x00, // out of order, non-critical
0xA6, 0x00,
]));
target = new EvdTestTarget();
const target = new EvdTestTarget();
EVD.decode(target, decoder);
expect(target.sum()).toBe(11);
});

decoder = new Decoder(new Uint8Array([0xAF, 0x00]));
target = new EvdTestTarget();
test("decode bad TLV-TYPE", () => {
const decoder = new Decoder(new Uint8Array([0xAF, 0x00]));
const target = new EvdTestTarget();
expect(() => EVD.decode(target, decoder)).toThrow();
});

test("setUnknown", () => {
const cb = jest.fn<boolean, [EvdTestTarget, Decoder.Tlv, number]>(
(self, { type }, order) => {
if (type === 0xA1) {
++self.a1;
return true;
}
return false;
});

const evd = new EvDecoder<EvdTestTarget>("A0AA", [0xA0, 0xAA])
.add(0xA4, (self) => { ++self.a4; }, { order: 7 })
.setUnknown(cb);

const decoder = new Decoder(new Uint8Array([
0xAA, 0x0A,
0xA2, 0x00, // ignored
0xA1, 0x00, // handled by cb
0xA4, 0x00, // handled by rule
0xA1, 0x00, // handled by cb
0xA6, 0x00, // ignored
]));
const target = evd.decode(new EvdTestTarget(), decoder);
expect(target.sum()).toBe(2100);

expect(cb).toHaveBeenCalledTimes(4);
expect(cb.mock.calls[0][1].type).toBe(0xA2);
expect(cb.mock.calls[0][2]).toBe(0);
expect(cb.mock.calls[1][1].type).toBe(0xA1);
expect(cb.mock.calls[1][2]).toBe(0);
expect(cb.mock.calls[2][1].type).toBe(0xA1);
expect(cb.mock.calls[2][2]).toBe(7);
expect(cb.mock.calls[3][1].type).toBe(0xA6);
expect(cb.mock.calls[3][2]).toBe(7);
});

0 comments on commit 8a2b6a8

Please sign in to comment.