/** * @license * Copyright 2022-2026 Matter.js Authors * SPDX-License-Identifier: Apache-2.0 */ import { Diagnostic, UnexpectedDataError } from "@matter/general"; import { ValidationDatatypeMismatchError } from "../common/ValidationError.js"; import { TlvCodec, TlvTag, TlvType, TlvTypeLength } from "./TlvCodec.js"; import { TlvArrayReader, TlvElement, TlvReader, TlvSchema, TlvStream, TlvWriter } from "./TlvSchema.js"; export class AnySchema extends TlvSchema { getEncodedByteLength(tlvStream: TlvStream): number { let byteLength = 0; tlvStream.forEach(({ tag, typeLength, value }) => { switch (typeLength.type) { case TlvType.Null: case TlvType.Boolean: case TlvType.Array: case TlvType.Structure: case TlvType.List: case TlvType.EndOfContainer: byteLength += TlvCodec.getTagByteLength(tag); break; case TlvType.UnsignedInt: case TlvType.SignedInt: case TlvType.Float: case TlvType.Utf8String: case TlvType.ByteString: byteLength += TlvCodec.getTagByteLength(tag); byteLength += TlvCodec.getPrimitiveByteLength(typeLength, value); break; } }); return byteLength; } override encodeTlvInternal(writer: TlvWriter, tlvStream: TlvStream, tagAssigned?: TlvTag): void { tlvStream.forEach(({ tag, typeLength, value }) => { if (tagAssigned !== undefined) { // Assign the tag to the 1st TLV element in the stream tag = tagAssigned; tagAssigned = undefined; } switch (typeLength.type) { case TlvType.Null: case TlvType.Boolean: case TlvType.Array: case TlvType.Structure: case TlvType.List: case TlvType.EndOfContainer: writer.writeTag(typeLength, tagAssigned ?? tag); break; case TlvType.UnsignedInt: case TlvType.SignedInt: case TlvType.Float: case TlvType.Utf8String: case TlvType.ByteString: writer.writeTag(typeLength, tagAssigned ?? tag); writer.writePrimitive(typeLength, value); break; } }); } override decodeTlvInternalValue(reader: TlvReader, typeLength: TlvTypeLength): TlvStream { return this.decodeTlvValueRec(reader, typeLength, new Array>()); } decodeTlvValueRec(reader: TlvReader, typeLength: TlvTypeLength, tlvStream: TlvStream, tag?: TlvTag) { switch (typeLength.type) { case TlvType.Null: tlvStream.push({ tag, typeLength, value: null }); break; case TlvType.Boolean: case TlvType.UnsignedInt: case TlvType.SignedInt: case TlvType.Float: case TlvType.Utf8String: case TlvType.ByteString: tlvStream.push({ tag, typeLength, value: reader.readPrimitive(typeLength) }); break; case TlvType.Array: case TlvType.Structure: case TlvType.List: tlvStream.push({ tag, typeLength }); while (true) { const { tag: elementTag, typeLength: typeLengthElement } = reader.readTagType(); this.decodeTlvValueRec(reader, typeLengthElement, tlvStream, elementTag); if (typeLengthElement.type === TlvType.EndOfContainer) break; } break; case TlvType.EndOfContainer: tlvStream.push({ tag, typeLength }); break; } return tlvStream; } override validate(tlvStream: TlvStream): void { if (!Array.isArray(tlvStream)) throw new ValidationDatatypeMismatchError(`Expected TlvStream, got ${typeof tlvStream}.`); tlvStream.forEach(({ typeLength }) => { if (!typeLength || typeof typeLength !== "object") throw new ValidationDatatypeMismatchError( `Expected typeLength properties in TlvStream, got ${typeof typeLength}.`, ); if (typeof typeLength.type !== "number") throw new ValidationDatatypeMismatchError( `Expected typeLength.type as number in TlvStream, got ${typeof typeLength.type}.`, ); }); } decodeAnyTlvStream(encoded: TlvStream) { if (encoded.length === 0) { return undefined; } const reader = new TlvArrayReader(encoded); const result = this.decodeGenericElement(reader); const nextElement = reader.readTagType(); if (nextElement !== undefined) { throw new UnexpectedDataError( `Unexpected data left after parsing all data: ${Diagnostic.json(nextElement)}`, ); } return result; } decodeGenericElement(reader: TlvArrayReader, preReadElement?: TlvElement, allowTag = false) { const element = preReadElement ?? reader.readTagType(); const { tag, typeLength: { type }, } = element; switch (type) { case TlvType.Null: case TlvType.Boolean: case TlvType.UnsignedInt: case TlvType.SignedInt: case TlvType.Float: case TlvType.Utf8String: case TlvType.ByteString: if (tag !== undefined && !allowTag) { throw new UnexpectedDataError(`Tag detected for a native type: ${Diagnostic.json(element)}`); } return reader.readPrimitive(element.typeLength); case TlvType.Array: case TlvType.List: return this.decodeGenericArrayOrList(reader, type === TlvType.List); case TlvType.Structure: return this.decodeGenericStructure(reader); default: throw new UnexpectedDataError(`Unknown type: ${type}`); } } decodeGenericArrayOrList(reader: TlvArrayReader, allowTag = false) { const result = new Array(); while (true) { const element = reader.readTagType(); const { tag, typeLength: { type }, } = element; if (type === TlvType.EndOfContainer) break; if (tag !== undefined && !allowTag) { throw new UnexpectedDataError(`Tag detected : ${Diagnostic.json(element)}`); } result.push(this.decodeGenericElement(reader, element, allowTag)); } return result; } decodeGenericStructure(reader: TlvArrayReader) { const result: { [key: string]: any } = {}; while (true) { const element = reader.readTagType(); const { tag, typeLength: { type }, } = element; if (type === TlvType.EndOfContainer) break; if (tag === undefined || tag.id === undefined) { throw new UnexpectedDataError(`Tag missing for a structure: ${Diagnostic.json(element)}`); } result[tag.id] = this.decodeGenericElement(reader, element, true); } return result; } } export const TlvAny = new AnySchema();