/** * @license * Copyright 2022-2026 Matter.js Authors * SPDX-License-Identifier: Apache-2.0 */ import { TlvNumericSchema } from "#tlv/TlvNumber.js"; import { ArraySchema } from "./TlvArray.js"; import { TlvTag, TlvType, TlvTypeLength } from "./TlvCodec.js"; import { TlvEncodingOptions, TlvReader, TlvSchema, TlvWriter } from "./TlvSchema.js"; import { StringSchema } from "./TlvString.js"; /** * Schema to encode a nullable value in TLV. * * @see {@link MatterSpecification.v10.Core} § A.11.6 */ export class NullableSchema extends TlvSchema { readonly schema: TlvSchema; constructor(schema: TlvSchema) { super(); // According to Matter specification, nullable numeric types cannot use the full range of the base type. // They use one value to represent null in some cases. // That's why adjust the max value accordingly if needed. if (schema instanceof TlvNumericSchema && schema.type !== TlvType.Float) { // Unsigned integers use the max value of the base type to represent null if (schema.baseTypeMin === 0 && schema.max === schema.baseTypeMax) { if (typeof schema.baseTypeMax === "number") { schema = schema.bound({ min: schema.min, max: schema.baseTypeMax - 1 }); } else { schema = schema.bound({ min: schema.min, max: schema.baseTypeMax - BigInt(1) }); } } else if (schema.baseTypeMin < 0 && schema.min === schema.baseTypeMin) { // Signed integers use the min value of the base type to represent null if (typeof schema.baseTypeMin === "number") { schema = schema.bound({ min: schema.baseTypeMin + 1, max: schema.max }); } else { schema = schema.bound({ min: schema.baseTypeMin + BigInt(1), max: schema.max }); } } } this.schema = schema; } /** @deprecated Part of old ClusterType() compat layer. */ override get element(): TlvSchema.Element | undefined { const inner = this.schema.element; if (inner === undefined) { return undefined; } const quality = inner.quality === undefined ? "X" : typeof inner.quality === "string" ? inner.quality.includes("X") ? inner.quality : `${inner.quality} X` : inner.quality; return { ...inner, quality }; } override encodeTlvInternal(writer: TlvWriter, value: T | null, tag?: TlvTag, options?: TlvEncodingOptions): void { if (value === null) { writer.writeTag({ type: TlvType.Null }, tag); } else { this.schema.encodeTlvInternal(writer, value, tag, options); } } override decodeTlvInternalValue(reader: TlvReader, typeLength: TlvTypeLength): T | null { if (typeLength.type === TlvType.Null) return null; const value = this.schema.decodeTlvInternalValue(reader, typeLength); // The Matter standard allows to send an empty string or Array for nullable elements that have a length. // This should be handled like null, so make sure to convert that correctly when decoding. // @see {@link MatterSpecification.v12.Core} § 7.17.1 if ( value !== null && (this.schema instanceof ArraySchema || this.schema instanceof StringSchema) && (value as any).length === 0 ) { // But because of Spec vs SDK interpretation issues we only map empty data where the min length is >0 as null // and leave the handling of null vs "" (and only for empty strings and arrays) to the implementation. // see https://github.com/CHIP-Specifications/connectedhomeip-spec/issues/11387 // Empty UInt8Arrays are mapped to null as before for convenience until this also makes issues in the future. if ( (this.schema instanceof StringSchema && this.schema.type === TlvType.ByteString) || (this.schema.minLength !== undefined && this.schema.minLength > 0) ) { return null; } } return value; } override validate(value: T | null): void { if (value !== null) this.schema.validate(value); } override injectField(value: T, fieldId: number, fieldValue: any, injectChecker: (fieldValue: any) => boolean): T { if (value !== null) { return this.schema.injectField(value, fieldId, fieldValue, injectChecker); } return value; } override removeField(value: T, fieldId: number, removeChecker: (fieldValue: any) => boolean): T { if (value !== null) { return this.schema.removeField(value, fieldId, removeChecker); } return value; } } const nullableCache = new WeakMap, NullableSchema>(); /** Nullable TLV schema. */ export const TlvNullable = (schema: TlvSchema): NullableSchema => { let result = nullableCache.get(schema) as NullableSchema | undefined; if (result === undefined) { result = new NullableSchema(schema); nullableCache.set(schema, result); } return result; };