From ebaf067ed316b1d06465ef07feb1cacfaa38c4bc Mon Sep 17 00:00:00 2001 From: Junxiao Shi Date: Mon, 23 Sep 2019 22:40:06 -0400 Subject: [PATCH] l3pkt: parameterized and signed Interest ParamsDigest computation is currently a placeholder. --- packages/l3pkt/src/data.ts | 2 +- packages/l3pkt/src/interest.ts | 156 ++++++++++++++++++++++----- packages/l3pkt/src/llsign.ts | 5 - packages/l3pkt/tests/interest.t.ts | 132 ++++++++++++++++++++++- packages/l3pkt/tests/llsign.t.ts | 80 ++++++++++++-- packages/name/src/digest-comp.ts | 28 ++++- packages/name/src/name.ts | 64 ++++++----- packages/name/tests/digest-comp.t.ts | 47 +++++--- packages/name/tests/name.t.ts | 10 ++ packages/tlv/src/encoder.ts | 2 +- 10 files changed, 427 insertions(+), 99 deletions(-) diff --git a/packages/l3pkt/src/data.ts b/packages/l3pkt/src/data.ts index 84852c33..333bcba2 100644 --- a/packages/l3pkt/src/data.ts +++ b/packages/l3pkt/src/data.ts @@ -24,7 +24,7 @@ const EVD = new EvDecoder("Data", TT.Data) .add(TT.DSigInfo, (self, { decoder }) => self.sigInfo = decoder.decode(DSigInfo)) .add(TT.DSigValue, (self, { value, before }) => { self.sigValue = value; - LLVerify.saveSignedPortion(self, before); + self[LLVerify.SIGNED] = before; }); /** Data packet. */ diff --git a/packages/l3pkt/src/interest.ts b/packages/l3pkt/src/interest.ts index 0bafe7f9..b249633d 100644 --- a/packages/l3pkt/src/interest.ts +++ b/packages/l3pkt/src/interest.ts @@ -1,34 +1,59 @@ -import { Name, NameLike } from "@ndn/name"; +import { Name, NameLike, ParamsDigest } from "@ndn/name"; import { Decoder, Encoder, EvDecoder, NNI } from "@ndn/tlv"; import { TT } from "./an"; +import { LLSign, LLVerify } from "./llsign"; +import { ISigInfo } from "./sig-info"; const LIFETIME_DEFAULT = 4000; const HOPLIMIT_MAX = 255; +const FAKE_PARAMS_DIGEST = new Uint8Array((function*() { + for (let i = 0; i < 32; i += 2) { + yield 0xBE; + yield 0xEF; + } +})()); const EVD = new EvDecoder("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; }) +.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 +.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)) +.add(TT.AppParameters, (self, { value, tlv }) => { + if (ParamsDigest.findIn(self.name) < 0) { + throw new Error("ParamsDigest missing in parameterized Interest"); + } + self.appParameters = value; + self[LLVerify.SIGNED] = tlv; +}) +.add(TT.ISigInfo, (self, { decoder }) => self.sigInfo = decoder.decode(ISigInfo)) +.add(TT.ISigValue, (self, { value, tlv }) => { + if (!ParamsDigest.match(self.name.at(-1))) { + throw new Error("ParamsDigest missing or out of place in signed Interest"); + } + const appParametersTlv = self[LLVerify.SIGNED]; + if (typeof appParametersTlv === "undefined") { + throw new Error("AppParameters missing in signed Interest"); + } + if (typeof self.sigInfo === "undefined") { + throw new Error("ISigInfo missing in signed Interest"); + } + + self.sigValue = value; + self[LLVerify.SIGNED] = Encoder.encode([ + self.name.getPrefix(-1).valueOnly, + new Uint8Array(appParametersTlv.buffer, appParametersTlv.byteOffset, + tlv.byteOffset - appParametersTlv.byteOffset), + ]); +}); /** Interest packet. */ export class Interest { - public get name() { return this.name_; } - public set name(v) { this.name_ = v; } - - public get canBePrefix() { return this.canBePrefix_; } - public set canBePrefix(v) { this.canBePrefix_ = v; } - - public get mustBeFresh() { return this.mustBeFresh_; } - public set mustBeFresh(v) { this.mustBeFresh_ = v; } - public get nonce() { return this.nonce_; } - public set nonce(v) { this.nonce_ = v; } + public set nonce(v) { this.nonce_ = v && NNI.constrain(v, "Nonce", 0xFFFFFFFF); } public get lifetime() { return this.lifetime_; } public set lifetime(v) { this.lifetime_ = NNI.constrain(v, "InterestLifetime"); } @@ -37,14 +62,18 @@ export class Interest { public set hopLimit(v) { this.hopLimit_ = NNI.constrain(v, "HopLimit", HOPLIMIT_MAX); } public static decodeFrom(decoder: Decoder): Interest { - const self = new Interest(); - EVD.decode(self, decoder); - return self; + return EVD.decode(new Interest(), decoder); } - private name_: Name = new Name(); - private canBePrefix_: boolean = false; - private mustBeFresh_: boolean = false; + public name: Name = new Name(); + public canBePrefix: boolean = false; + public mustBeFresh: boolean = false; + public appParameters?: Uint8Array; + public sigInfo?: ISigInfo; + public sigValue?: Uint8Array; + public [LLSign.PENDING]?: LLSign; + public [LLVerify.SIGNED]?: Uint8Array; + private nonce_: number|undefined; private lifetime_: number = LIFETIME_DEFAULT; private hopLimit_: number = HOPLIMIT_MAX; @@ -60,6 +89,7 @@ export class Interest { * - Interest.Nonce(v) * - Interest.Lifetime(v) * - Interest.HopLimit(v) + * - Uint8Array as AppParameters */ constructor(...args: Array) { args.forEach((arg) => { @@ -75,6 +105,8 @@ export class Interest { this.lifetime = arg.v; } else if (arg instanceof HopLimitTag) { this.hopLimit = arg.v; + } else if (arg instanceof Uint8Array) { + this.appParameters = arg; } else if (arg instanceof Interest) { Object.assign(this, arg); } else { @@ -84,9 +116,8 @@ export class Interest { } public encodeTo(encoder: Encoder) { - if (this.name.size < 1) { - throw new Error("Interest name is empty"); - } + this.insertParamsDigest(); + LLSign.encodeErrorIfPending(this); const nonce = typeof this.nonce === "undefined" ? Math.random() * 0x100000000 : this.nonce; @@ -100,8 +131,77 @@ export class Interest { [TT.InterestLifetime, NNI(this.lifetime)] : undefined, this.hopLimit !== HOPLIMIT_MAX ? [TT.HopLimit, NNI(this.hopLimit, 1)] : undefined, + this.appParameters ? + [TT.AppParameters, this.appParameters] : undefined, + this.sigInfo, + this.sigValue ? + [TT.ISigValue, this.sigValue] : undefined, ); } + + public [LLSign.PROCESS](): Promise { + this.insertParamsDigest(); + return LLSign.processImpl(this, + () => Encoder.encode([ + this.name.getPrefix(-1).valueOnly, + [TT.AppParameters, this.appParameters], + this.sigInfo, + ]), + (sig) => { + this.sigValue = this.sigInfo ? sig : undefined; + return this.updateParamsDigest(); + }); + } + + public [LLVerify.VERIFY](verify: LLVerify): Promise { + if (!this.sigValue) { + return Promise.resolve(); + } + return LLVerify.verifyImpl(this, this.sigValue, verify); + } + + private insertParamsDigest() { + if (this.name.size < 1) { + throw new Error("Interest name is empty"); + } + + let pdIndex = ParamsDigest.findIn(this.name); + let pdAppendPlaceholder = false; + if (this.sigInfo) { + if (pdIndex < 0) { + pdAppendPlaceholder = true; + } else if (pdIndex !== this.name.size - 1) { + throw new Error("ParamsDigest out of place for signed Interest"); + } + + if (!this.appParameters) { + this.appParameters = new Uint8Array(); + } + } else if (this.appParameters) { + if (pdIndex < 0) { + pdAppendPlaceholder = true; + } + } else if (pdIndex >= 0) { + this.appParameters = new Uint8Array(); + } else { + return; // not a parameterized or signed Interest + } + + if (pdAppendPlaceholder) { + pdIndex = this.name.size; + this.name = this.name.append(ParamsDigest.PLACEHOLDER); + } + + if (ParamsDigest.isPlaceholder(this.name.at(pdIndex)) && !this[LLSign.PENDING]) { + this[LLSign.PENDING] = () => Promise.resolve(new Uint8Array()); + } + } + + private async updateParamsDigest(): Promise { + const pdIndex = ParamsDigest.findIn(this.name); + const newDigest = FAKE_PARAMS_DIGEST; // TODO compute digest + this.name = this.name.replaceAt(pdIndex, ParamsDigest.create(newDigest)); + } } class NonceTag { @@ -136,5 +236,5 @@ export namespace Interest { } export type CtorArg = NameLike | typeof CanBePrefix | typeof MustBeFresh | - LifetimeTag | HopLimitTag; + LifetimeTag | HopLimitTag | Uint8Array; } diff --git a/packages/l3pkt/src/llsign.ts b/packages/l3pkt/src/llsign.ts index c7f73396..429e318d 100644 --- a/packages/l3pkt/src/llsign.ts +++ b/packages/l3pkt/src/llsign.ts @@ -64,11 +64,6 @@ export namespace LLVerify { [VERIFY](verify: LLVerify): Promise; } - /** Store signed portion during decoding. */ - export function saveSignedPortion(obj: Verifiable, signed: Uint8Array) { - obj[SIGNED] = signed; - } - /** Perform verification. */ export function verifyImpl(obj: Verifiable, sig: Uint8Array, verify: LLVerify): Promise { const signed = obj[LLVerify.SIGNED]; diff --git a/packages/l3pkt/tests/interest.t.ts b/packages/l3pkt/tests/interest.t.ts index 58a0ec37..87c3de0f 100644 --- a/packages/l3pkt/tests/interest.t.ts +++ b/packages/l3pkt/tests/interest.t.ts @@ -1,8 +1,8 @@ -import { Name } from "@ndn/name"; +import { Name, ParamsDigest } from "@ndn/name"; import { Decoder, Encoder } from "@ndn/tlv"; import "@ndn/tlv/test-fixture"; -import { Interest } from "../src"; +import { Interest, ISigInfo, LLSign, LLVerify, SigType, TT } from "../src"; test("encode", () => { expect(() => new Interest({} as any)).toThrow(); @@ -76,7 +76,6 @@ test("decode", () => { 0x0A, 0x04, 0xA0, 0xA1, 0xA2, 0xA3, 0x0C, 0x02, 0x76, 0xA1, 0x22, 0x01, 0xDC, - // TODO AppParameters, ISigInfo, ISigValue ])); interest = decoder.decode(Interest); expect(interest.name.toString()).toBe("/A"); @@ -85,4 +84,131 @@ test("decode", () => { expect(interest.nonce).toBe(0xA0A1A2A3); expect(interest.lifetime).toBe(30369); expect(interest.hopLimit).toBe(220); + expect(interest.appParameters).toBeUndefined(); + expect(interest.sigInfo).toBeUndefined(); + expect(interest.sigValue).toBeUndefined(); +}); + +async function encodeWithLLSign(interest: Interest): Promise { + await interest[LLSign.PROCESS](); + return Encoder.encode(interest); +} + +test("encode parameterized", async () => { + // insert empty AppParameters + let interest = new Interest(new Name("/A").append(ParamsDigest.PLACEHOLDER).append("C")); + await expect(encodeWithLLSign(interest)).resolves.toEncodeAs(({ value }) => { + expect(value).toMatchTlv( + ({ decoder }) => { + const name = decoder.decode(Name); + expect(name.size).toBe(3); + expect(name.at(1).is(ParamsDigest)).toBeTruthy(); + }, + ({ type }) => expect(type).toBe(TT.Nonce), + ({ type, length }) => { + expect(type).toBe(TT.AppParameters); + expect(length).toBe(0); + }, + ); + }); + + // append ParamsDigest + interest = new Interest(new Name("/A"), new Uint8Array([0xC0, 0xC1])); + await expect(encodeWithLLSign(interest)).resolves.toEncodeAs(({ value }) => { + expect(value).toMatchTlv( + ({ decoder }) => { + const name = decoder.decode(Name); + expect(name.size).toBe(2); + expect(name.at(1).is(ParamsDigest)).toBeTruthy(); + }, + ({ type }) => expect(type).toBe(TT.Nonce), + ({ type, value }) => { + expect(type).toBe(TT.AppParameters); + expect(value).toEqualUint8Array([0xC0, 0xC1]); + }, + ); + }); +}); + +test("decode parameterized", async () => { + let decoder = new Decoder(Encoder.encode([ + TT.Interest, + new Name("/A"), + [TT.AppParameters, new Uint8Array([0xC0, 0xC1])], + ])); + expect(() => decoder.decode(Interest)).toThrow(/missing/); + + decoder = new Decoder(Encoder.encode([ + TT.Interest, + new Name("/A").append(ParamsDigest, new Uint8Array(32)), + [TT.AppParameters, new Uint8Array([0xC0, 0xC1])], + ])); + let interest = decoder.decode(Interest); + expect(interest.name.size).toBe(2); + expect(interest.appParameters).not.toBeUndefined(); + + decoder = new Decoder(Encoder.encode([ + TT.Interest, + new Name("/A").append(ParamsDigest, new Uint8Array(32)).append("C"), + [TT.AppParameters, new Uint8Array([0xC0, 0xC1])], + ])); + interest = decoder.decode(Interest); + expect(interest.name.size).toBe(3); + expect(interest.appParameters).not.toBeUndefined(); + + const verify = jest.fn(); + await expect(interest[LLVerify.VERIFY](verify)).resolves.toBeUndefined(); + expect(verify).not.toHaveBeenCalled(); +}); + +test("encode signed", async () => { + // error on out of place ParamsDigest + const interest = new Interest(new Name("/A").append(ParamsDigest.PLACEHOLDER).append("C")); + interest.sigInfo = new ISigInfo(); + interest.sigInfo.type = SigType.Sha256; + await expect(encodeWithLLSign(interest)).rejects.toThrow(/out of place/); + + // other tests in llsign.t.ts +}); + +test("decode signed", () => { + let decoder = new Decoder(Encoder.encode([ + TT.Interest, + new Name("/A").append(ParamsDigest, new Uint8Array(32)), + [TT.ISigValue, new Uint8Array(4)], + ])); + expect(() => decoder.decode(Interest)).toThrow(/missing/); + + decoder = new Decoder(Encoder.encode([ + TT.Interest, + new Name("/A").append(ParamsDigest, new Uint8Array(32)), + [TT.AppParameters, new Uint8Array([0xC0, 0xC1])], + [TT.ISigValue, new Uint8Array(4)], + ])); + expect(() => decoder.decode(Interest)).toThrow(/missing/); + + const si = new ISigInfo(); + si.type = SigType.Sha256; + + decoder = new Decoder(Encoder.encode([ + TT.Interest, + new Name("/A").append(ParamsDigest, new Uint8Array(32)).append("C"), + [TT.AppParameters, new Uint8Array([0xC0, 0xC1])], + si, + [TT.ISigValue, new Uint8Array(4)], + ])); + expect(() => decoder.decode(Interest)).toThrow(/out of place/); + + decoder = new Decoder(Encoder.encode([ + TT.Interest, + new Name("/A").append(ParamsDigest, new Uint8Array(32)), + [TT.AppParameters, new Uint8Array([0xC0, 0xC1])], + si, + [TT.ISigValue, new Uint8Array(4)], + ])); + const interest = decoder.decode(Interest); + expect(interest.name.size).toBe(2); + expect(interest.appParameters).not.toBeUndefined(); + expect(interest.sigInfo).not.toBeUndefined(); + expect(interest.sigValue).not.toBeUndefined(); }); diff --git a/packages/l3pkt/tests/llsign.t.ts b/packages/l3pkt/tests/llsign.t.ts index 734547f8..b8a24a77 100644 --- a/packages/l3pkt/tests/llsign.t.ts +++ b/packages/l3pkt/tests/llsign.t.ts @@ -3,7 +3,9 @@ import delay from "delay"; import { Name } from "@ndn/name"; import { Decodable, Decoder, Encodable, Encoder } from "@ndn/tlv"; -import { Data, LLSign, LLVerify } from "../src"; +import "@ndn/tlv/test-fixture"; + +import { Data, DSigInfo, Interest, ISigInfo, KeyDigest, LLSign, LLVerify, SigInfo, SigType, TT } from "../src"; class TestAlgo { constructor(private key: string, private wantSignError: boolean = false) { @@ -27,7 +29,7 @@ class TestAlgo { private computeSignature(input: Uint8Array): Uint8Array { // warning: this is insecure hashing algorithm, for test case only - const hash = crypto.createHmac("sha1", this.key); + const hash = crypto.createHmac("sha256", this.key); hash.update(input); return hash.digest(); } @@ -36,18 +38,71 @@ class TestAlgo { const ALGO0 = new TestAlgo("0"); const ALGO1 = new TestAlgo("1", true); -type Pkt = LLSign.Signable & LLVerify.Verifiable & Encodable; +function makeSigInfo(cls: (new() => T)): T { + const sigInfo = new cls(); + sigInfo.type = SigType.HmacWithSha256; + sigInfo.keyLocator = new KeyDigest(new Uint8Array([0xA0, 0xA1])); + return sigInfo; +} + +type Pkt = LLSign.Signable & LLVerify.Verifiable & Encodable & {sigInfo: SigInfo}; interface Row { - cls: (new(name?: Name) => Pkt) & Decodable; + cls: (new(name: Name) => Pkt) & Decodable; + si: SigInfo; + checkWire(tlv: Decoder.Tlv); } const TABLE = [ - { cls: Data }, + { + cls: Interest, + si: makeSigInfo(ISigInfo), + checkWire({ type, value }) { + expect(type).toBe(TT.Interest); + expect(value).toMatchTlv( + ({ type, value }) => { + expect(type).toBe(TT.Name); + expect(value).toMatchTlv( + ({ type }) => expect(type).toBe(TT.GenericNameComponent), + ({ type, length }) => { + expect(type).toBe(TT.ParametersSha256DigestComponent); + expect(length).toBe(32); + }, + ); + }, + ({ type }) => expect(type).toBe(TT.Nonce), + ({ type, length }) => { + expect(type).toBe(TT.AppParameters); + expect(length).toBe(0); + }, + ({ type }) => expect(type).toBe(TT.ISigInfo), + ({ type, length }) => { + expect(type).toBe(TT.ISigValue); + expect(length).toBe(32); + }, + ); + }, + }, + { + cls: Data, + si: makeSigInfo(DSigInfo), + checkWire({ type, value }) { + expect(type).toBe(TT.Data); + expect(value).toMatchTlv( + ({ decoder }) => expect(decoder.decode(Name).size).toBe(1), + ({ type }) => expect(type).toBe(TT.DSigInfo), + ({ type, length }) => { + expect(type).toBe(TT.DSigValue); + expect(length).toBe(32); + }, + ); + }, + }, ] as Row[]; -test.each(TABLE)("sign %#", async ({ cls }) => { - const obj = new cls(); +test.each(TABLE)("sign %#", async ({ cls, si }) => { + const obj = new cls(new Name("/A")); + obj.sigInfo = si; await expect(obj[LLSign.PROCESS]()).resolves.toBeUndefined(); // noop obj[LLSign.PENDING] = ALGO1.sign; @@ -61,16 +116,19 @@ test.each(TABLE)("sign %#", async ({ cls }) => { expect(Encoder.encode(obj)).not.toBeUndefined(); }); -test.each(TABLE)("verify %#", async ({ cls }) => { +test.each(TABLE)("verify %#", async ({ cls, si, checkWire }) => { const src = new cls(new Name("/A")); + src.sigInfo = si; src[LLSign.PENDING] = ALGO0.sign; await src[LLSign.PROCESS](); + const wire = Encoder.encode(src); + expect(wire).toMatchTlv(checkWire); - const obj = new Decoder(Encoder.encode(src)).decode(cls); + const obj = new Decoder(wire).decode(cls); expect(obj[LLVerify.SIGNED]).not.toBeUndefined(); await expect(obj[LLVerify.VERIFY](ALGO0.verify)).resolves.toBeUndefined(); await expect(obj[LLVerify.VERIFY](ALGO1.verify)).rejects.toThrow(/incorrect/); - const empty = new cls(); // no signed portion - await expect(empty[LLVerify.VERIFY](ALGO0.verify)).rejects.toThrow(/empty/); + obj[LLVerify.SIGNED] = undefined; + await expect(obj[LLVerify.VERIFY](ALGO0.verify)).rejects.toThrow(/empty/); }); diff --git a/packages/name/src/digest-comp.ts b/packages/name/src/digest-comp.ts index 5b514f23..3a35facc 100644 --- a/packages/name/src/digest-comp.ts +++ b/packages/name/src/digest-comp.ts @@ -1,6 +1,7 @@ import { TT } from "./an"; import { Component } from "./component"; import { NamingConvention } from "./convention"; +import { Name } from "./name"; const DIGEST_LENGTH = 32; @@ -27,5 +28,30 @@ class DigestComp implements NamingConvention { /** ImplicitSha256DigestComponent */ export const ImplicitDigest = new DigestComp(TT.ImplicitSha256DigestComponent); +const PARAMS_PLACEHOLDER_TAG = Symbol("ParametersSha256DigestComponent.placeholder"); + +class ParamsDigestComp extends DigestComp { + /** ParamsDigest placeholder during Interest encoding. */ + public readonly PLACEHOLDER: Component; + + constructor() { + super(TT.ParametersSha256DigestComponent); + this.PLACEHOLDER = Object.assign( + new Component(TT.ParametersSha256DigestComponent), + {[PARAMS_PLACEHOLDER_TAG]: true}); + } + + /** Determine if comp is a ParamsDigest placeholder. */ + public isPlaceholder(comp: Component): boolean { + return !!comp[PARAMS_PLACEHOLDER_TAG]; + } + + /** Find ParamsDigest or placeholder in name. */ + public findIn(name: Name, matchPlaceholder: boolean = true): number { + return name.comps.findIndex((comp) => this.match(comp) || + (matchPlaceholder && this.isPlaceholder(comp))); + } +} + /** ParametersSha256DigestComponent */ -export const ParamsDigest = new DigestComp(TT.ParametersSha256DigestComponent); +export const ParamsDigest = new ParamsDigestComp(); diff --git a/packages/name/src/name.ts b/packages/name/src/name.ts index dedada18..0e1d0abc 100644 --- a/packages/name/src/name.ts +++ b/packages/name/src/name.ts @@ -1,4 +1,4 @@ -import { Decoder, Encoder } from "@ndn/tlv"; +import { Decoder, Encodable, Encoder } from "@ndn/tlv"; import { TT } from "./an"; import { Component, ComponentLike } from "./component"; @@ -20,16 +20,23 @@ export class Name { return self; } + public get comps() { return this.comps_; } + + /** Obtain an Encodable for TLV-VALUE only. */ + public get valueOnly(): Encodable { + return { + encodeTo: (encoder: Encoder) => { + encoder.prependValue(...this.comps_); + }, + }; + } + private comps_: Component[]; - /** - * Create empty name, or copy from other name, or parse from URI. - */ + /** Create empty name, or copy from other name, or parse from URI. */ constructor(input?: NameLike); - /** - * Create from components. - */ + /** Create from components. */ constructor(comps: ComponentLike[]); constructor(arg1?) { @@ -47,9 +54,7 @@ export class Name { return this.comps_.length; } - /** - * Retrieve i-th component. - */ + /** Retrieve i-th component. */ public get(i: number): Component|undefined { i = i < 0 ? i + this.size : i; return this.comps_[i]; @@ -67,35 +72,25 @@ export class Name { return comp; } - /** - * Get URI string. - */ + /** Get URI string. */ public toString(): string { return "/" + this.comps_.map((comp) => comp.toString()).join("/"); } - /** - * Get sub name [begin, end). - */ + /** Get sub name [begin, end). */ public slice(begin?: number, end?: number): Name { return new Name(this.comps_.slice(begin, end)); } - /** - * Get prefix of n components. - */ + /** Get prefix of n components. */ public getPrefix(n: number): Name { return this.slice(0, n); } - /** - * Append a component from naming convention. - */ + /** Append a component from naming convention. */ public append(convention: NamingConvention, v: T): Name; - /** - * Append suffix with one or more components. - */ + /** Append suffix with one or more components. */ public append(...suffix: ComponentLike[]): Name; public append(...args) { @@ -107,9 +102,14 @@ export class Name { return new Name(this.comps_.concat(suffix.map(Component.from))); } - /** - * Compare with other name. - */ + /** Return a copy of Name with a component replaced. */ + public replaceAt(i: number, comp: ComponentLike): Name { + const copy = new Name(this); + copy.comps.splice(i, 1, Component.from(comp)); + return copy; + } + + /** Compare with other name. */ public compare(other: NameLike): Name.CompareResult { const rhs = new Name(other); const commonSize = Math.min(this.size, rhs.size); @@ -128,16 +128,12 @@ export class Name { return Name.CompareResult.EQUAL; } - /** - * Determine if this name equals other. - */ + /** Determine if this name equals other. */ public equals(other: NameLike): boolean { return this.compare(other) === Name.CompareResult.EQUAL; } - /** - * Determine if this name is a prefix of other. - */ + /** Determine if this name is a prefix of other. */ public isPrefixOf(other: NameLike): boolean { const cmp = this.compare(other); return cmp === Name.CompareResult.EQUAL || cmp === Name.CompareResult.LPREFIX; diff --git a/packages/name/tests/digest-comp.t.ts b/packages/name/tests/digest-comp.t.ts index 235ccc1c..e74f41e8 100644 --- a/packages/name/tests/digest-comp.t.ts +++ b/packages/name/tests/digest-comp.t.ts @@ -1,22 +1,39 @@ -import { ImplicitDigest, Name, ParamsDigest } from "../src"; +import { ImplicitDigest, Name, NamingConvention, ParamsDigest, TT } from "../src"; import "../test-fixture"; -test("ImplicitDigest", () => { +interface Row { + compType: NamingConvention; + tt: number; +} + +const TABLE = [ + { compType: ImplicitDigest, tt: TT.ImplicitSha256DigestComponent }, + { compType: ParamsDigest, tt: TT.ParametersSha256DigestComponent }, +] as Row[]; + +test.each(TABLE)("DigestComp %#", ({ compType, tt }) => { const digest = new Uint8Array(32); digest[1] = 0xAA; - const name = new Name().append(ImplicitDigest, digest); - expect(name.at(0)).toEqualComponent("1=%00%aa" + "%00".repeat(30)); - expect(name.at(0).is(ImplicitDigest)).toBeTruthy(); - expect(ImplicitDigest.parse(name.at(0))).toEqual(digest); - expect(() => ImplicitDigest.create(new Uint8Array(7))).toThrow(); + const name = new Name().append(compType, digest); + expect(name.at(0)).toEqualComponent(`${tt}=%00%aa${"%00".repeat(30)}`); + expect(name.at(0).is(compType)).toBeTruthy(); + expect(compType.parse(name.at(0))).toEqual(digest); + expect(() => compType.create(new Uint8Array(7))).toThrow(); }); -test("ParamsDigest", () => { - const digest = new Uint8Array(32); - digest[1] = 0xAA; - const name = new Name().append(ParamsDigest, digest); - expect(name.at(0)).toEqualComponent("2=%00%aa" + "%00".repeat(30)); - expect(name.at(0).is(ParamsDigest)).toBeTruthy(); - expect(ParamsDigest.parse(name.at(0))).toEqual(digest); - expect(() => ParamsDigest.create(new Uint8Array(7))).toThrow(); +test("ParamsDigest placeholder", () => { + let name = new Name("/A/B/C"); + expect(ParamsDigest.findIn(name)).toBeLessThan(0); + expect(ParamsDigest.findIn(name, false)).toBeLessThan(0); + expect(ParamsDigest.isPlaceholder(name.at(1))).toBeFalsy(); + + name = name.replaceAt(1, ParamsDigest.PLACEHOLDER); + expect(ParamsDigest.findIn(name)).toBe(1); + expect(ParamsDigest.findIn(name, false)).toBeLessThan(0); + expect(ParamsDigest.isPlaceholder(name.at(1))).toBeTruthy(); + + name = name.replaceAt(1, ParamsDigest.create(new Uint8Array(32))); + expect(ParamsDigest.findIn(name)).toBe(1); + expect(ParamsDigest.findIn(name, false)).toBe(1); + expect(ParamsDigest.isPlaceholder(name.at(1))).toBeFalsy(); }); diff --git a/packages/name/tests/name.t.ts b/packages/name/tests/name.t.ts index 098ee22d..723c1f7a 100644 --- a/packages/name/tests/name.t.ts +++ b/packages/name/tests/name.t.ts @@ -46,6 +46,8 @@ test("modify", () => { expect(name.slice(1).toString()).toBe("/B/C"); expect(name.getPrefix(-1).toString()).toBe("/A/B"); expect(name.append("D", "E").toString()).toBe("/A/B/C/D/E"); + expect(name.replaceAt(1, "BB").toString()).toBe("/A/BB/C"); + expect(name.replaceAt(-1, "CC").toString()).toBe("/A/B/CC"); expect(name.toString()).toBe("/A/B/C"); // immutable }); @@ -79,6 +81,14 @@ test("encode", () => { ]); }); +test("encode valueOnly", () => { + const name = new Name("/A/B"); + expect(name.valueOnly).toEncodeAs([ + 0x08, 0x01, 0x41, + 0x08, 0x01, 0x42, + ]); +}); + test("NameLike", () => { expect(Name.isNameLike(new Name())).toBeTruthy(); expect(Name.isNameLike("/")).toBeTruthy(); diff --git a/packages/tlv/src/encoder.ts b/packages/tlv/src/encoder.ts index 76d22f62..dcc5f89c 100644 --- a/packages/tlv/src/encoder.ts +++ b/packages/tlv/src/encoder.ts @@ -12,7 +12,7 @@ export interface EncodableObj { * Optional second item could be OmitEmpty to omit the TLV if TLV-VALUE is empty. * Subsequent items are Encodables for TLV-VALUE. */ -type EncodableTlv = [number, ...any[]]; +export type EncodableTlv = [number, ...any[]]; /** * An object acceptable to Encoder.encode().