UNPKG

@foxglove/cdr

Version:

Common Data Representation serialization and deserialization library

507 lines (438 loc) 15.9 kB
import { EncapsulationKind } from "./EncapsulationKind"; import { getEncapsulationKindInfo } from "./getEncapsulationKindInfo"; import { isBigEndian } from "./isBigEndian"; import { LengthCode, lengthCodeToObjectSizes } from "./lengthCodes"; import { EXTENDED_PID, SENTINEL_PID } from "./reservedPIDs"; interface Indexable { [index: number]: unknown; } interface TypedArrayConstructor<T> { new (length?: number): T; new (buffer: ArrayBufferLike, byteOffset?: number, length?: number): T; BYTES_PER_ELEMENT: number; } type ArrayValueGetter = | "getInt8" | "getUint8" | "getInt16" | "getUint16" | "getInt32" | "getUint32" | "getBigInt64" | "getBigUint64" | "getFloat32" | "getFloat64"; const textDecoder = new TextDecoder("utf8"); export class CdrReader { private view: DataView; private littleEndian: boolean; private hostLittleEndian: boolean; private eightByteAlignment: number; // Alignment for 64-bit values, 4 on CDR2 8 on CDR1 private isCDR2: boolean; /** Origin offset into stream used for alignment */ private origin = 0; // Need to be public for higher level serializers to use readonly usesDelimiterHeader: boolean; readonly usesMemberHeader: boolean; offset: number; get kind(): EncapsulationKind { return this.view.getUint8(1) as EncapsulationKind; } get decodedBytes(): number { return this.offset; } get byteLength(): number { return this.view.byteLength; } constructor(data: ArrayBufferView) { if (data.byteLength < 4) { throw new Error( `Invalid CDR data size ${data.byteLength}, must contain at least a 4-byte header`, ); } this.view = new DataView(data.buffer, data.byteOffset, data.byteLength); const kind = this.kind; const { isCDR2, littleEndian, usesDelimiterHeader, usesMemberHeader } = getEncapsulationKindInfo(kind); this.usesDelimiterHeader = usesDelimiterHeader; this.usesMemberHeader = usesMemberHeader; this.littleEndian = littleEndian; this.hostLittleEndian = !isBigEndian(); this.isCDR2 = isCDR2; this.eightByteAlignment = isCDR2 ? 4 : 8; this.origin = 4; this.offset = 4; } int8(): number { const value = this.view.getInt8(this.offset); this.offset += 1; return value; } uint8(): number { const value = this.view.getUint8(this.offset); this.offset += 1; return value; } int16(): number { this.align(2); const value = this.view.getInt16(this.offset, this.littleEndian); this.offset += 2; return value; } uint16(): number { this.align(2); const value = this.view.getUint16(this.offset, this.littleEndian); this.offset += 2; return value; } int32(): number { this.align(4); const value = this.view.getInt32(this.offset, this.littleEndian); this.offset += 4; return value; } uint32(): number { this.align(4); const value = this.view.getUint32(this.offset, this.littleEndian); this.offset += 4; return value; } int64(): bigint { this.align(this.eightByteAlignment); const value = this.view.getBigInt64(this.offset, this.littleEndian); this.offset += 8; return value; } uint64(): bigint { this.align(this.eightByteAlignment); const value = this.view.getBigUint64(this.offset, this.littleEndian); this.offset += 8; return value; } uint16BE(): number { this.align(2); const value = this.view.getUint16(this.offset, false); this.offset += 2; return value; } uint32BE(): number { this.align(4); const value = this.view.getUint32(this.offset, false); this.offset += 4; return value; } uint64BE(): bigint { this.align(this.eightByteAlignment); const value = this.view.getBigUint64(this.offset, false); this.offset += 8; return value; } float32(): number { this.align(4); const value = this.view.getFloat32(this.offset, this.littleEndian); this.offset += 4; return value; } float64(): number { this.align(this.eightByteAlignment); const value = this.view.getFloat64(this.offset, this.littleEndian); this.offset += 8; return value; } string(prereadLength?: number): string { const length = prereadLength ?? this.uint32(); if (length <= 1) { this.offset += length; return ""; } const data = new Uint8Array(this.view.buffer, this.view.byteOffset + this.offset, length - 1); const value = textDecoder.decode(data); this.offset += length; return value; } /** Reads the delimiter header which contains and returns the object size */ dHeader(): number { const header = this.uint32(); return header; } /** * Reads the member header (EMHEADER) and returns the member ID, mustUnderstand flag, and object size with optional length code * The length code is only present in CDR2 and should prompt objectSize to be used in place of sequence length if applicable. * See Extensible and Dynamic Topic Types (DDS-XTypes) v1.3 @ `7.4.3.4.2` for more info about CDR2 EMHEADER composition. * If a sentinelHeader was read (PL_CDR v1), the readSentinelHeader flag is set to true. */ emHeader(): { mustUnderstand: boolean; id: number; objectSize: number; lengthCode?: LengthCode; readSentinelHeader?: boolean; } { if (this.isCDR2) { return this.memberHeaderV2(); } else { return this.memberHeaderV1(); } } /** XCDR1 PL_CDR encapsulation parameter header*/ private memberHeaderV1(): { id: number; objectSize: number; mustUnderstand: boolean; readSentinelHeader?: boolean; } { // 4-byte header with two 16-bit fields this.align(4); const idHeader = this.uint16(); const mustUnderstandFlag = (idHeader & 0x4000) >> 14 === 1; // indicates that the parameter has a implementation-specific interpretation const implementationSpecificFlag = (idHeader & 0x8000) >> 15 === 1; // Allows the specification of large member ID and/or data length values // requires the reading in of two uint32's for ID and size const extendedPIDFlag = (idHeader & 0x3fff) === EXTENDED_PID; // Indicates the end of the parameter list structure const sentinelPIDFlag = (idHeader & 0x3fff) === SENTINEL_PID; if (sentinelPIDFlag) { // Return that we have read the sentinel header when we expected to read an emHeader. // This can happen for absent optional members at the end of a struct. return { id: SENTINEL_PID, objectSize: 0, mustUnderstand: false, readSentinelHeader: true }; } // Indicates that the ID should be ignored // const ignorePIDFlag = (idHeader & 0x3fff) === 0x3f03; const usesReservedParameterId = (idHeader & 0x3fff) > SENTINEL_PID; // Not trying to support right now if we don't need to if (usesReservedParameterId || implementationSpecificFlag) { throw new Error(`Unsupported parameter ID header ${idHeader.toString(16)}`); } if (extendedPIDFlag) { // Need to consume last part of header (is just an 8 in this case) // Alignment could take care of this, but I want to be explicit this.uint16(); } const id = extendedPIDFlag ? this.uint32() : idHeader & 0x3fff; const objectSize = extendedPIDFlag ? this.uint32() : this.uint16(); this.resetOrigin(); return { id, objectSize, mustUnderstand: mustUnderstandFlag }; } /** Sets the origin to the offset (DDS-XTypes Spec: `PUSH(ORIGIN = 0)`)*/ private resetOrigin(): void { this.origin = this.offset; } /** Reads the PID_SENTINEL value if encapsulation kind supports it (PL_CDR version 1)*/ sentinelHeader(): void { if (!this.isCDR2) { this.align(4); const header = this.uint16(); // Indicates the end of the parameter list structure const sentinelPIDFlag = (header & 0x3fff) === SENTINEL_PID; if (!sentinelPIDFlag) { throw Error( `Expected SENTINEL_PID (${SENTINEL_PID.toString(16)}) flag, but got ${header.toString( 16, )}`, ); } this.uint16(); } } private memberHeaderV2(): { id: number; objectSize: number; mustUnderstand: boolean; lengthCode: LengthCode; } { const header = this.uint32(); // EMHEADER = (M_FLAG<<31) + (LC<<28) + M.id // M is the member of a structure // M_FLAG is the value of the Must Understand option for the member const mustUnderstand = Math.abs((header & 0x80000000) >> 31) === 1; // LC is the value of the Length Code for the member. const lengthCode = ((header & 0x70000000) >> 28) as LengthCode; const id = header & 0x0fffffff; const objectSize = this.emHeaderObjectSize(lengthCode); return { mustUnderstand, id, objectSize, lengthCode }; } /** Uses the length code to derive the member object size in * the EMHEADER, sometimes reading NEXTINT (the next uint32 * following the header) from the buffer */ private emHeaderObjectSize(lengthCode: LengthCode) { // 7.4.3.4.2 Member Header (EMHEADER), Length Code (LC) and NEXTINT switch (lengthCode) { case 0: case 1: case 2: case 3: return lengthCodeToObjectSizes[lengthCode]; // LC > 3 -> NEXTINT exists after header case 4: case 5: // both 4 and 5 just read the next uint32 return this.uint32(); case 6: return 4 * this.uint32(); case 7: return 8 * this.uint32(); default: throw new Error( // eslint-disable-next-line @typescript-eslint/restrict-template-expressions `Invalid length code ${lengthCode} in EMHEADER at offset ${this.offset - 4}`, ); } } sequenceLength(): number { return this.uint32(); } int8Array(count: number = this.sequenceLength()): Int8Array { const array = new Int8Array(this.view.buffer, this.view.byteOffset + this.offset, count); this.offset += count; return array; } uint8Array(count: number = this.sequenceLength()): Uint8Array { const array = new Uint8Array(this.view.buffer, this.view.byteOffset + this.offset, count); this.offset += count; return array; } int16Array(count: number = this.sequenceLength()): Int16Array { return this.typedArray(Int16Array, "getInt16", count); } uint16Array(count: number = this.sequenceLength()): Uint16Array { return this.typedArray(Uint16Array, "getUint16", count); } int32Array(count: number = this.sequenceLength()): Int32Array { return this.typedArray(Int32Array, "getInt32", count); } uint32Array(count: number = this.sequenceLength()): Uint32Array { return this.typedArray(Uint32Array, "getUint32", count); } int64Array(count: number = this.sequenceLength()): BigInt64Array { return this.typedArray(BigInt64Array, "getBigInt64", count, this.eightByteAlignment); } uint64Array(count: number = this.sequenceLength()): BigUint64Array { return this.typedArray(BigUint64Array, "getBigUint64", count, this.eightByteAlignment); } float32Array(count: number = this.sequenceLength()): Float32Array { return this.typedArray(Float32Array, "getFloat32", count); } float64Array(count: number = this.sequenceLength()): Float64Array { return this.typedArray(Float64Array, "getFloat64", count, this.eightByteAlignment); } stringArray(count: number = this.sequenceLength()): string[] { const output: string[] = []; for (let i = 0; i < count; i++) { output.push(this.string()); } return output; } /** * Seek the current read pointer a number of bytes relative to the current position. Note that * seeking before the four-byte header is invalid * @param relativeOffset A positive or negative number of bytes to seek */ seek(relativeOffset: number): void { const newOffset = this.offset + relativeOffset; if (newOffset < 4 || newOffset >= this.view.byteLength) { throw new Error(`seek(${relativeOffset}) failed, ${newOffset} is outside the data range`); } this.offset = newOffset; } /** * Seek to an absolute byte position in the data. Note that seeking before the four-byte header is * invalid * @param offset An absolute byte offset in the range of [4-byteLength) */ seekTo(offset: number): void { if (offset < 4 || offset >= this.view.byteLength) { throw new Error(`seekTo(${offset}) failed, value is outside the data range`); } this.offset = offset; } /** * Duplicate this reader. The underlying buffer is reused and not copied. */ clone(): CdrReader { const clone = new CdrReader(this.view); clone.offset = this.offset; clone.origin = this.origin; return clone; } /** * Limit the reader to a given number of bytes. * @param length The number of bytes to limit the reader to. */ limit(length: number): void { const newByteLength = this.offset + length; if (newByteLength <= this.view.byteLength) { this.view = new DataView(this.view.buffer, this.view.byteOffset, newByteLength); } else { throw new RangeError(`length ${length} exceeds byte length of view`); } } /** * Returns `true` if the reader is at the end of the buffer, or `false` otherwise. */ isAtEnd(): boolean { return this.offset >= this.view.byteLength; } private align(size: number): void { const alignment = (this.offset - this.origin) % size; if (alignment > 0) { this.offset += size - alignment; } } // Reads a given count of numeric values into a typed array. private typedArray<T extends Indexable>( TypedArrayConstructor: TypedArrayConstructor<T>, getter: ArrayValueGetter, count: number, alignment = TypedArrayConstructor.BYTES_PER_ELEMENT, // Expected CDR padding bytes ) { if (count === 0) { return new TypedArrayConstructor(); } this.align(alignment); const totalOffset = this.view.byteOffset + this.offset; if (this.littleEndian !== this.hostLittleEndian) { // Slowest path return this.typedArraySlow(TypedArrayConstructor, getter, count); } else if (totalOffset % TypedArrayConstructor.BYTES_PER_ELEMENT === 0) { // Fastest path const array = new TypedArrayConstructor(this.view.buffer, totalOffset, count); this.offset += TypedArrayConstructor.BYTES_PER_ELEMENT * count; return array; } else { // Slower path return this.typedArrayUnaligned(TypedArrayConstructor, getter, count); } } private typedArrayUnaligned<T extends Indexable>( TypedArrayConstructor: TypedArrayConstructor<T>, getter: ArrayValueGetter, count: number, ) { // Benchmarks indicate for count < ~10 doing each individually is faster than copy if (count < 10) { return this.typedArraySlow(TypedArrayConstructor, getter, count); } // If the length is > 10, then doing a copy of the data to align it is faster // using _set_ is slightly faster than slice on the array buffer according to today's benchmarks const byteLength = TypedArrayConstructor.BYTES_PER_ELEMENT * count; const copy = new Uint8Array(byteLength); copy.set(new Uint8Array(this.view.buffer, this.view.byteOffset + this.offset, byteLength)); this.offset += byteLength; return new TypedArrayConstructor(copy.buffer, copy.byteOffset, count); } private typedArraySlow<T extends Indexable>( TypedArrayConstructor: TypedArrayConstructor<T>, getter: ArrayValueGetter, count: number, ) { const array = new TypedArrayConstructor(count); let offset = this.offset; for (let i = 0; i < count; i++) { array[i] = this.view[getter](offset, this.littleEndian); offset += TypedArrayConstructor.BYTES_PER_ELEMENT; } this.offset = offset; return array; } }