UNPKG

@aokiapp/tlv

Version:

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

281 lines (280 loc) 10.8 kB
import { BasicTLVBuilder } from "./basic-builder.js"; import { TagClass } from "../common/types.js"; /** * Checks if a given schema is a constructed schema. * @param schema - A TLV schema object. * @returns True if the schema has fields; false otherwise. */ function isConstructedSchema(schema) { return ("fields" in schema && Array.isArray(schema.fields)); } /** * A builder that builds TLV data based on a given schema (synchronous or asynchronous). * @template S - The schema type. */ export class SchemaBuilder { schema; strict; /** * Constructs a SchemaBuilder for the specified schema. * @param schema - The TLV schema to use. */ constructor(schema, options) { this.schema = schema; this.strict = options?.strict ?? true; } /** * Builds data either in synchronous or asynchronous mode. * @param data - The input data matching the schema structure. * @param options - If { async: true }, builds asynchronously; otherwise synchronously. * @returns Either a built ArrayBuffer or a Promise of a built ArrayBuffer. */ build(data, options) { const prevStrict = this.strict; if (options?.strict !== undefined) { this.strict = options.strict; } try { if (options?.async) { return this.buildAsync(data); } else { return this.buildSync(data); } } finally { this.strict = prevStrict; } } /** * Builds data in synchronous mode. * @param data - The input data. * @returns Built TLV result. */ buildSync(data) { return this.buildWithSchemaSync(this.schema, data); } /** * Builds data in asynchronous mode. * @param data - The input data. * @returns A Promise of built TLV result. */ async buildAsync(data) { return await this.buildWithSchemaAsync(this.schema, data); } /** * Recursively builds data in synchronous mode. * @param schema - The schema to build with. * @param data - The data to build. * @returns Built result. */ buildWithSchemaSync(schema, data) { if (isConstructedSchema(schema)) { let fieldsToProcess = [...schema.fields]; // For SET, sort fields by tag as required by DER strict mode if (schema.tagNumber === 17 && (schema.tagClass === TagClass.Universal || schema.tagClass === undefined) && this.strict) { fieldsToProcess = fieldsToProcess.slice().sort((a, b) => { // Encode tag bytes for comparison const encodeTag = (field) => { const tagClass = field.tagClass ?? TagClass.Universal; const tagNumber = field.tagNumber ?? 0; const constructed = isConstructedSchema(field) ? 0x20 : 0x00; const bytes = []; let firstByte = (tagClass << 6) | constructed; if (tagNumber < 31) { firstByte |= tagNumber; bytes.push(firstByte); } else { firstByte |= 0x1f; bytes.push(firstByte); let num = tagNumber; const tagNumBytes = []; do { tagNumBytes.unshift(num % 128); num = Math.floor(num / 128); } while (num > 0); for (let i = 0; i < tagNumBytes.length - 1; i++) { bytes.push(tagNumBytes[i] | 0x80); } bytes.push(tagNumBytes[tagNumBytes.length - 1]); } return new Uint8Array(bytes); }; return compareUint8Arrays(encodeTag(a), encodeTag(b)); }); /** * Compare two Uint8Arrays lexicographically. * Returns -1 if a < b, 1 if a > b, 0 if equal. */ function compareUint8Arrays(a, b) { const len = Math.min(a.length, b.length); for (let i = 0; i < len; i++) { if (a[i] !== b[i]) return a[i] < b[i] ? -1 : 1; } if (a.length !== b.length) return a.length < b.length ? -1 : 1; return 0; } } const childrenBuffers = fieldsToProcess.map((fieldSchema) => { const fieldName = fieldSchema.name; const fieldData = data[fieldName]; if (fieldData === undefined) { throw new Error(`Missing required field: ${fieldName}`); } return this.buildWithSchemaSync(fieldSchema, fieldData); }); // Avoid unnecessary ArrayBuffer copies const totalLength = childrenBuffers.reduce((sum, buf) => sum + buf.byteLength, 0); const childrenData = new Uint8Array(totalLength); let offset = 0; for (const buffer of childrenBuffers) { const bufView = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer); childrenData.set(bufView, offset); offset += bufView.byteLength; } return BasicTLVBuilder.build({ tag: { tagClass: schema.tagClass ?? TagClass.Universal, tagNumber: schema.tagNumber ?? 16, // Default to SEQUENCE for constructed constructed: true, }, length: childrenData.byteLength, value: childrenData.buffer, endOffset: 0, }); } else { // PrimitiveTLVSchema let value; if (schema.encode) { const encoded = schema.encode(data); if (encoded instanceof Promise) { throw new Error(`Asynchronous encoder used in synchronous build for field: ${schema.name}`); } value = encoded; } else { if (!(data instanceof ArrayBuffer)) { throw new Error(`Field '${schema.name}' requires an ArrayBuffer, but received other type.`); } value = data; } return BasicTLVBuilder.build({ tag: { tagClass: schema.tagClass ?? TagClass.Universal, tagNumber: schema.tagNumber ?? 0, constructed: false, }, length: value.byteLength, value: value, endOffset: 0, }); } } /** * Recursively builds data in asynchronous mode. * @param schema - The schema to build with. * @param data - The data to build. * @returns A Promise of the built result. */ async buildWithSchemaAsync(schema, data) { if (isConstructedSchema(schema)) { const fieldsToProcess = [...schema.fields]; // For SET, sort fields by tag as required by DER if ((schema.tagNumber === 17 && schema.tagClass === TagClass.Universal) || (schema.tagNumber === 17 && schema.tagClass === undefined)) { fieldsToProcess.sort((a, b) => { const tagA = a.tagNumber ?? 0; const tagB = b.tagNumber ?? 0; return tagA - tagB; }); } const childBuffers = await Promise.all(fieldsToProcess.map((fieldSchema) => { const fieldName = fieldSchema.name; const fieldData = data[fieldName]; if (fieldData === undefined) { throw new Error(`Missing required field: ${fieldName}`); } return this.buildWithSchemaAsync(fieldSchema, fieldData); })); const totalLength = childBuffers.reduce((sum, buf) => sum + buf.byteLength, 0); const childrenData = new Uint8Array(totalLength); let offset = 0; for (const buffer of childBuffers) { childrenData.set(new Uint8Array(buffer), offset); offset += buffer.byteLength; } return BasicTLVBuilder.build({ tag: { tagClass: schema.tagClass ?? TagClass.Universal, tagNumber: schema.tagNumber ?? 16, // Default to SEQUENCE for constructed constructed: true, }, length: childrenData.byteLength, value: childrenData.buffer, endOffset: 0, }); } else { // PrimitiveTLVSchema let value; if (schema.encode) { value = await Promise.resolve(schema.encode(data)); } else { if (!(data instanceof ArrayBuffer)) { throw new Error(`Field '${schema.name}' requires an ArrayBuffer, but received other type.`); } value = data; } return BasicTLVBuilder.build({ tag: { tagClass: schema.tagClass ?? TagClass.Universal, tagNumber: schema.tagNumber ?? 0, constructed: false, }, length: value.byteLength, value: value, endOffset: 0, }); } } } /** * Utility class for creating new TLV schemas (identical to parser schemas). */ export class Schema { // 実装 static primitive(name, encode, options) { const { tagClass, tagNumber } = options ?? {}; return { name, encode, tagClass, tagNumber, }; } /** * Creates a constructed TLV schema definition. * @param name - The name of the field. * @param fields - An array of TLV schema definitions. * @param options - Optional tag class and tag number. * @returns A constructed TLV schema object. */ static constructed(name, fields, options) { const { tagClass, tagNumber } = options ?? {}; return { name, fields, tagClass, tagNumber, }; } }