diff --git a/pkg/lvs/package.json b/pkg/lvs/package.json index b30fd8c7..682537f8 100644 --- a/pkg/lvs/package.json +++ b/pkg/lvs/package.json @@ -28,6 +28,7 @@ "dependencies": { "@ndn/packet": "workspace:*", "@ndn/tlv": "workspace:^", + "@ndn/trust-schema": "workspace:^", "@ndn/util": "workspace:*", "tslib": "^2.8.0" } diff --git a/pkg/lvs/src/mod.ts b/pkg/lvs/src/mod.ts index 88a39133..1cd29cfe 100644 --- a/pkg/lvs/src/mod.ts +++ b/pkg/lvs/src/mod.ts @@ -1 +1,2 @@ export * as lvstlv from "./tlv"; +export * from "./translate"; diff --git a/pkg/lvs/src/translate.ts b/pkg/lvs/src/translate.ts new file mode 100644 index 00000000..51dbaf0f --- /dev/null +++ b/pkg/lvs/src/translate.ts @@ -0,0 +1,128 @@ +import { type Component, Name } from "@ndn/packet"; +import { pattern as P, TrustSchemaPolicy } from "@ndn/trust-schema"; +import { assert } from "@ndn/util"; + +import { type ConsOption, type Constraint, type LvsModel, type Node, type PatternEdge, ValueEdge } from "./tlv"; + +export function toPolicy(model: LvsModel): TrustSchemaPolicy { + return new Translator(model).translate(); +} + +class Translator { + constructor(private readonly model: LvsModel) {} + + private readonly policy = new TrustSchemaPolicy(); + private readonly tagSymbols = new Map(); + private readonly wantedNodes = new Set(); + private lastAutoId = 0; + + public translate(): TrustSchemaPolicy { + this.gatherTagSymbols(); + this.gatherNodes(); + this.processPatterns(); + this.processRules(); + return this.policy; + } + + private gatherTagSymbols(): void { + this.tagSymbols.clear(); + for (const { tag, identifier } of this.model.tagSymbols) { + this.tagSymbols.set(tag, identifier); + } + } + + private nameTag(tag: number): string { + // TODO + const prefix = this.tagSymbols.get(tag) ?? `_TAG_${tag}`; + const suffix = tag > this.model.namedPatternCnt ? `_${++this.lastAutoId}` : ""; + return `${prefix}${suffix}`; + } + + private gatherNodes(): void { + this.wantedNodes.clear(); + for (const node of this.model.nodes) { + if (node.ruleNames.length > 0 || node.signConstraints.length > 0) { + this.wantedNodes.add(node.id); + } + for (const sc of node.signConstraints) { + this.wantedNodes.add(sc); + } + } + } + + private processPatterns(): void { + for (const id of this.wantedNodes) { + const node = this.model.nodes[id]!; + const pattern = this.trPattern(node).simplify(); + for (const name of this.namePattern(node)) { + this.policy.addPattern(name, pattern); + } + } + } + + private namePattern(node: Node): string[] { + if (node.ruleNames.length === 0) { + return [`_NODE_${node.id}`]; + } + return node.ruleNames; + } + + private trPattern(node: Node): P.Pattern { + const parts: P.Pattern[] = []; + while (node.parent !== undefined) { + const parent = this.model.nodes[node.parent]!; + const edge = parent.findEdgeTo(node.id)!; + parts.unshift(this.trEdge(edge)); + node = parent; + } + return new P.ConcatPattern(parts); + } + + private trEdge(edge: ValueEdge | PatternEdge): P.Pattern { + if (edge instanceof ValueEdge) { + return this.trValue(edge.value); + } + + // TODO + assert(edge.constraints.length <= 1, "PatternEdge with multiple Constraints is unimplemented"); + const inner = edge.constraints[0] && this.trConstraint(edge.constraints[0]); + return new P.VariablePattern(this.nameTag(edge.tag), { inner }); + } + + private trConstraint(cons: Constraint): P.Pattern { + return new P.AlternatePattern( + Array.from(cons.options, (co) => this.trConsOption(co)), + ); + } + + private trConsOption(co: ConsOption): P.Pattern { + if (co.value) { + return this.trValue(co.value); + } + if (co.tag) { + return new P.VariablePattern(this.nameTag(co.tag)); + } + assert(co.call); + // TODO + assert(false, "UserFnCall is unimplemented"); + } + + private trValue(value: Component): P.Pattern { + return new P.ConstPattern(new Name([value])); + } + + private processRules(): void { + for (const node of this.model.nodes) { + const packetIds = this.namePattern(node); + for (const sc of node.signConstraints) { + const signerIds = this.namePattern(this.model.nodes[sc]!); + + for (const packetId of packetIds) { + for (const signerId of signerIds) { + this.policy.addRule(packetId, signerId); + } + } + } + } + } +} diff --git a/pkg/lvs/test-fixture/lvstlv.ts b/pkg/lvs/test-fixture/lvstlv.ts index 29970e6e..6a123c4e 100644 --- a/pkg/lvs/test-fixture/lvstlv.ts +++ b/pkg/lvs/test-fixture/lvstlv.ts @@ -1,22 +1,22 @@ -import fs from "node:fs/promises"; +import fs from "node:fs"; import path from "node:path"; import { Decoder } from "@ndn/tlv"; import { lvstlv } from ".."; -async function loadLVSTLV(filename: string): Promise { - return Decoder.decode( - await fs.readFile(path.join(import.meta.dirname, filename)), +function loadLVSTLV(filename: string): () => lvstlv.LvsModel { + return () => Decoder.decode( + fs.readFileSync(path.join(import.meta.dirname, filename)), lvstlv.LvsModel, ); } /** python-ndn LVS model sample "quick example". */ -export const pyndn0 = await loadLVSTLV("pyndn0.tlv"); +export const pyndn0 = loadLVSTLV("pyndn0.tlv"); /** python-ndn LVS model sample "signing key suggesting". */ -export const pyndn1 = await loadLVSTLV("pyndn1.tlv"); +export const pyndn1 = loadLVSTLV("pyndn1.tlv"); /** python-ndn LVS model sample "compiler and checker demonstration". */ -export const pyndn2 = await loadLVSTLV("pyndn2.tlv"); +export const pyndn2 = loadLVSTLV("pyndn2.tlv"); diff --git a/pkg/lvs/tests/lvstlv.t.ts b/pkg/lvs/tests/lvstlv.t.ts deleted file mode 100644 index 37a17f15..00000000 --- a/pkg/lvs/tests/lvstlv.t.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { console } from "@ndn/util"; -import { expect, test } from "vitest"; - -import { pyndn0, pyndn1, pyndn2 } from "../test-fixture/lvstlv"; - -test("pyndn0", () => { - console.log(pyndn0.toString()); -}); - -test("pyndn1", () => { - console.log(pyndn1.toString()); - expect(pyndn1.nodes).toHaveLength(26); -}); - -test("pyndn2", () => { - console.log(pyndn2.toString()); -}); diff --git a/pkg/lvs/tests/pyndn.t.ts b/pkg/lvs/tests/pyndn.t.ts new file mode 100644 index 00000000..b8ad89ae --- /dev/null +++ b/pkg/lvs/tests/pyndn.t.ts @@ -0,0 +1,39 @@ +import { Name } from "@ndn/packet"; +// import { printESM, versec } from "@ndn/trust-schema"; +import { console } from "@ndn/util"; +import { expect, test } from "vitest"; + +import { toPolicy } from ".."; +import { pyndn0, pyndn1, pyndn2 } from "../test-fixture/lvstlv"; + +test("pyndn0", () => { + const model = pyndn0(); + // console.log(model.toString()); + + const policy = toPolicy(model); + // console.log(printESM(policy)); + // console.log(versec.print(policy)); + expect(policy.canSign( + new Name("/a/blog/article/math/2022/03"), + new Name("/a/blog/author/xinyu/KEY/1/admin/1"), + )).toBeTruthy(); + expect(policy.canSign( + new Name("/a/blog/author/xinyu/KEY/1/admin/1"), + new Name("/a/blog/admin/admin/KEY/1/root/1"), + )).toBeTruthy(); + expect(policy.canSign( + new Name("/a/blog/author/xinyu/KEY/1/admin/1"), + new Name("/a/blog/KEY/1/self/1"), + )).toBeFalsy(); +}); + +test("pyndn1", () => { + const model = pyndn1(); + console.log(model.toString()); + expect(model.nodes).toHaveLength(26); +}); + +test("pyndn2", () => { + const model = pyndn2(); + console.log(model.toString()); +}); diff --git a/pkg/tlv/src/impl-field.ts b/pkg/tlv/src/impl-field.ts index bf23f4a1..162bf5e9 100644 --- a/pkg/tlv/src/impl-field.ts +++ b/pkg/tlv/src/impl-field.ts @@ -10,23 +10,14 @@ export interface Field extends Required { asString: (v: T) => Iterable; } -interface Options extends EvDecoder.RuleOptions { - required?: boolean; - repeat?: boolean; -} - export function makeField( tt: number, key: string, type: StructFieldType, - opts: Options, + opts: EvDecoder.RuleOptions, evd: EvDecoder, ): Field | Field { - const fo = { ...opts, ...(evd ? evd.applyDefaultsToRuleOptions(opts) : { - order: tt, - required: false, - repeat: false, - }) }; + const fo = evd.applyDefaultsToRuleOptions(opts); evd.add( tt, fo.repeat ? diff --git a/pkg/trust-schema/src/schema/pattern.ts b/pkg/trust-schema/src/schema/pattern.ts index 997beb3e..f9d01bea 100644 --- a/pkg/trust-schema/src/schema/pattern.ts +++ b/pkg/trust-schema/src/schema/pattern.ts @@ -209,6 +209,17 @@ export class VariablePattern extends Pattern { public readonly inner?: Pattern; public readonly filter?: VariablePattern.Filter; + public override simplify(): Pattern { + const inner = this.inner?.simplify(); + if (inner === this.inner) { + return this; + } + return new VariablePattern(this.id, { + ...this, + inner, + }); + } + private *innerMatch(value: Name, input?: Map): Iterable { if (!this.inner) { yield new Map();