UNPKG

@aokiapp/tlv

Version:

Tag-Length-Value (TLV) parser and builder library with schema support. Provides both parsing and building APIs as submodules.

157 lines (156 loc) 5.76 kB
import { inferIsSetFromTag, TagClass } from "../common/index"; import { BasicTLVBuilder } from "./basic-builder.js"; /** * A TLV builder that encodes data according to a provided schema. * Strict mode enforces presence of all required fields and type expectations. */ export class SchemaBuilder { schema; strict; constructor(schema, options) { this.schema = schema; this.strict = options?.strict ?? true; } // Encodes the supplied data into TLV using the schema rules. build(data) { return this.encodeTopLevel(this.schema, data); } encodeTopLevel(schema, data) { if (this.isConstructed(schema)) { return this.encodeConstructed(schema, (data ?? {})); } if (this.isRepeated(schema)) { // Top-level repeated has no tag to wrap items; disallow to keep TLV well-formed. throw new Error(`Top-level repeated schema '${schema.name}' is not supported. Wrap it in a constructed container.`); } return this.encodePrimitive(schema, data); } encodePrimitive(schema, data) { const { tagNumber, tagClass } = schema; const value = schema.encode(data); return BasicTLVBuilder.build({ tag: { tagClass, constructed: false, tagNumber }, length: value.byteLength, value, endOffset: 0, }); } encodeConstructed(schema, data) { const { tagNumber, tagClass } = schema; const childBuffers = []; for (const field of schema.fields) { const fieldName = field.name; const v = data[fieldName]; // Missing property handling if (v === undefined) { if (field.optional) { continue; } throw new Error(`Missing required property '${fieldName}' in constructed '${schema.name}'`); } if (this.isRepeated(field)) { if (!Array.isArray(v)) { throw new Error(`Repeated field '${fieldName}' expects an array`); } const items = v; for (const item of items) { const itemTLV = this.encodeTopLevel(field.item, item); childBuffers.push(itemTLV); } continue; } if (this.isConstructed(field)) { const childTLV = this.encodeConstructed(field, v); childBuffers.push(childTLV); continue; } // Primitive child const primTLV = this.encodePrimitive(field, v); childBuffers.push(primTLV); } // For SET, enforce DER canonical ordering when strict=true; preserve input order when strict=false if (schema.isSet === true && this.strict === true) { childBuffers.sort((a, b) => this.compareUnsignedLex(a, b)); } const inner = this.concatBuffers(childBuffers); return BasicTLVBuilder.build({ tag: { tagClass, constructed: true, tagNumber }, length: inner.byteLength, value: inner, endOffset: 0, }); } isConstructed(schema) { return Object.prototype.hasOwnProperty.call(schema, "fields"); } isRepeated(schema) { return Object.prototype.hasOwnProperty.call(schema, "item"); } concatBuffers(buffers) { const total = buffers.reduce((sum, b) => sum + b.byteLength, 0); const out = new Uint8Array(total); let off = 0; for (const b of buffers) { out.set(new Uint8Array(b), off); off += b.byteLength; } return out.buffer; } // Unsigned lexicographic comparator for raw DER bytes (a < b => negative, a > b => positive) compareUnsignedLex(a, b) { const ua = new Uint8Array(a); const ub = new Uint8Array(b); const len = Math.min(ua.length, ub.length); for (let i = 0; i < len; i++) { if (ua[i] !== ub[i]) return ua[i] - ub[i]; } return ua.length - ub.length; } } /** * Utility class for creating new TLV schemas (identical to parser schemas). */ // Convenience factory for constructing schema descriptors used by the builder. export class Schema { static primitive(name, options, encode) { const tagNumber = options.tagNumber; if (typeof tagNumber !== "number") { throw new Error(`Primitive schema '${name}' requires tagNumber`); } const obj = { name, encode, tagClass: options?.tagClass ?? TagClass.Universal, tagNumber, optional: options?.optional ? true : false, }; return obj; } static constructed(name, options, fields) { const tagClassNormalized = options?.tagClass ?? TagClass.Universal; const inferredIsSet = options?.isSet !== undefined ? options.isSet : inferIsSetFromTag(tagClassNormalized, options?.tagNumber); const inferredTagNumber = inferredIsSet ? 17 : 16; const obj = { name, fields, tagClass: tagClassNormalized, tagNumber: options?.tagNumber ?? inferredTagNumber, optional: options?.optional ? true : false, isSet: inferredIsSet, }; return obj; } static repeated(name, options, item) { const obj = { name, item, tagClass: options?.tagClass ?? TagClass.Universal, tagNumber: options?.tagNumber, optional: options?.optional ? true : false, }; return obj; } }