UNPKG

@aokiapp/tlv

Version:

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

120 lines (119 loc) 4.78 kB
import { TagClass } from "../common/types.js"; export class BasicTLVParser { /** * Parse a buffer containing a single TLV structure. * @param buffer - The TLV data buffer to parse. * @returns The parsed result including tag, length, and value. */ static parse(buffer) { const view = new DataView(buffer); let offset = 0; const tagInfo = this.readTagInfo(view, offset); offset = tagInfo.newOffset; const lengthInfo = this.readLength(view, offset); offset = lengthInfo.newOffset; const valueInfo = this.readValue(buffer, offset, lengthInfo.length); offset = valueInfo.newOffset; return { tag: tagInfo.tag, length: lengthInfo.length, value: valueInfo.value, endOffset: offset, }; } /** * Read the tag portion from the DataView and update the offset. * @param view - The DataView representing the TLV buffer. * @param offset - The current read position within the buffer. * @returns An object containing the parsed tag information and the new offset. */ static readTagInfo(view, offset) { const firstByte = view.getUint8(offset++); const tagClassBits = (firstByte & 0xc0) >> 6; const tagClass = this.getTagClass(tagClassBits); const isConstructed = !!(firstByte & 0x20); let tagNumber = firstByte & 0x1f; if (tagNumber === 0x1f) { tagNumber = 0; let b; const MAX_SAFE = Number.MAX_SAFE_INTEGER; do { b = view.getUint8(offset++); const base7 = b & 0x7f; const potential = tagNumber * 128 + base7; if (potential > MAX_SAFE) { throw new Error(`Long-form tag number exceeds JavaScript MAX_SAFE_INTEGER at offset ${offset - 1} (partial=${tagNumber}, next=${base7})`); } tagNumber = potential; } while (b & 0x80); } return { tag: { tagClass, constructed: isConstructed, tagNumber }, newOffset: offset, }; } /** * Convert tag class bits into a TagClass enum value. * @param {number} bits - The bits extracted from the tag byte. * @returns {TagClass} The corresponding TagClass. */ static getTagClass(bits) { switch (bits) { case 0: return TagClass.Universal; case 1: return TagClass.Application; case 2: return TagClass.ContextSpecific; case 3: return TagClass.Private; } throw new Error(`Invalid tag class bits: ${bits} (expected 0..3)`); } /** * Read the length portion from the DataView and update the offset. * @param view - The DataView representing the TLV buffer. * @param offset - The current read position within the buffer. * @returns An object containing the parsed length and the new offset. */ static readLength(view, offset) { const first = view.getUint8(offset++); // DER forbids indefinite length (0x80) if (first === 0x80) { throw new Error(`Indefinite length (0x80) at offset ${offset - 1} is not allowed (DER)`); } let length; if (first & 0x80) { const numBytes = first & 0x7f; length = 0; const MAX_SAFE = Number.MAX_SAFE_INTEGER; for (let i = 0; i < numBytes; i++) { const byte = view.getUint8(offset++); // Use safe arithmetic (no 32-bit bitwise truncation) and guard overflow. if (length > Math.floor(MAX_SAFE / 256)) { throw new Error(`Long-form length exceeds JavaScript MAX_SAFE_INTEGER at offset ${offset - 1}`); } length = length * 256 + byte; } } else { length = first; } return { length, newOffset: offset }; } /** * Read the value portion from the buffer based on the specified length. * @param buffer - The original TLV data buffer. * @param offset - The current read position within the buffer. * @param length - The length of the TLV value. * @returns An object containing the raw value slice and the new offset. */ static readValue(buffer, offset, length) { const end = offset + length; if (end > buffer.byteLength) { throw new Error(`Declared length ${length} at offset ${offset} exceeds available bytes (buffer length ${buffer.byteLength})`); } const value = buffer.slice(offset, end); return { value, newOffset: end }; } }