@foxglove/cdr
Advanced tools
Comparing version 0.1.0 to 0.2.0
@@ -6,2 +6,3 @@ export declare class CdrReader { | ||
private littleEndian; | ||
private hostLittleEndian; | ||
private textDecoder; | ||
@@ -23,4 +24,30 @@ get data(): Uint8Array; | ||
sequenceLength(): number; | ||
int8Array(count?: number): Int8Array; | ||
uint8Array(count?: number): Uint8Array; | ||
int16Array(count?: number): Int16Array; | ||
uint16Array(count?: number): Uint16Array; | ||
int32Array(count?: number): Int32Array; | ||
uint32Array(count?: number): Uint32Array; | ||
int64Array(count?: number): BigInt64Array; | ||
uint64Array(count?: number): BigUint64Array; | ||
float32Array(count?: number): Float32Array; | ||
float64Array(count?: number): Float64Array; | ||
stringArray(count?: number): string[]; | ||
/** | ||
* 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; | ||
/** | ||
* 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; | ||
private align; | ||
private typedArray; | ||
private typedArrayUnaligned; | ||
private typedArraySlow; | ||
} | ||
//# sourceMappingURL=CdrReader.d.ts.map |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.CdrReader = void 0; | ||
const isBigEndian_1 = require("./isBigEndian"); | ||
class CdrReader { | ||
constructor(data) { | ||
this.textDecoder = new TextDecoder("utf8"); | ||
this.hostLittleEndian = !isBigEndian_1.isBigEndian(); | ||
if (data.byteLength < 4) { | ||
@@ -93,2 +95,77 @@ throw new Error(`Invalid CDR data size ${data.byteLength}, must contain at least a 4-byte header`); | ||
} | ||
int8Array(count) { | ||
count ?? (count = this.sequenceLength()); | ||
const array = new Int8Array(this.data.buffer, this.data.byteOffset + this.offset, count); | ||
this.offset += count; | ||
return array; | ||
} | ||
uint8Array(count) { | ||
count ?? (count = this.sequenceLength()); | ||
const array = new Uint8Array(this.data.buffer, this.data.byteOffset + this.offset, count); | ||
this.offset += count; | ||
return array; | ||
} | ||
int16Array(count) { | ||
count ?? (count = this.sequenceLength()); | ||
return this.typedArray(Int16Array, "getInt16", count); | ||
} | ||
uint16Array(count) { | ||
count ?? (count = this.sequenceLength()); | ||
return this.typedArray(Uint16Array, "getUint16", count); | ||
} | ||
int32Array(count) { | ||
count ?? (count = this.sequenceLength()); | ||
return this.typedArray(Int32Array, "getInt32", count); | ||
} | ||
uint32Array(count) { | ||
count ?? (count = this.sequenceLength()); | ||
return this.typedArray(Uint32Array, "getUint32", count); | ||
} | ||
int64Array(count) { | ||
count ?? (count = this.sequenceLength()); | ||
return this.typedArray(BigInt64Array, "getBigInt64", count); | ||
} | ||
uint64Array(count) { | ||
count ?? (count = this.sequenceLength()); | ||
return this.typedArray(BigUint64Array, "getBigUint64", count); | ||
} | ||
float32Array(count) { | ||
count ?? (count = this.sequenceLength()); | ||
return this.typedArray(Float32Array, "getFloat32", count); | ||
} | ||
float64Array(count) { | ||
count ?? (count = this.sequenceLength()); | ||
return this.typedArray(Float64Array, "getFloat64", count); | ||
} | ||
stringArray(count) { | ||
count ?? (count = this.sequenceLength()); | ||
const output = []; | ||
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) { | ||
const newOffset = this.offset + relativeOffset; | ||
if (newOffset < 4 || newOffset >= this.data.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) { | ||
if (offset < 4 || offset >= this.data.byteLength) { | ||
throw new Error(`seekTo(${offset}) failed, value is outside the data range`); | ||
} | ||
this.offset = offset; | ||
} | ||
align(size) { | ||
@@ -100,4 +177,44 @@ const alignment = (this.offset - 4) % size; | ||
} | ||
// Reads a given count of numeric values into a typed array. | ||
typedArray(TypedArrayConstructor, getter, count) { | ||
this.align(TypedArrayConstructor.BYTES_PER_ELEMENT); | ||
const totalOffset = this.data.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 | ||
return new TypedArrayConstructor(this.data.buffer, totalOffset, count); | ||
} | ||
else { | ||
// Slower path | ||
return this.typedArrayUnaligned(TypedArrayConstructor, getter, count); | ||
} | ||
} | ||
typedArrayUnaligned(TypedArrayConstructor, getter, count) { | ||
// 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); | ||
} | ||
typedArraySlow(TypedArrayConstructor, getter, count) { | ||
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; | ||
} | ||
} | ||
exports.CdrReader = CdrReader; | ||
//# sourceMappingURL=CdrReader.js.map |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const CdrReader_1 = require("./CdrReader"); | ||
const CdrWriter_1 = require("./CdrWriter"); | ||
// Example tf2_msgs/TFMessage | ||
const tf2_msg__TFMessage = "0001000001000000cce0d158f08cf9060a000000626173655f6c696e6b000000060000007261646172000000ae47e17a14ae0e4000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f03f"; | ||
describe("CdrReader", () => { | ||
it("parses an example message", () => { | ||
// Example tf2_msgs/TFMessage | ||
const tf2_msg__TFMessage = "0001000001000000cce0d158f08cf9060a000000626173655f6c696e6b000000060000007261646172000000ae47e17a14ae0e4000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f03f"; | ||
const data = Uint8Array.from(Buffer.from(tf2_msg__TFMessage, "hex")); | ||
@@ -29,3 +30,76 @@ const reader = new CdrReader_1.CdrReader(data); | ||
}); | ||
it("seeks to absolute and relative positions", () => { | ||
const data = Uint8Array.from(Buffer.from(tf2_msg__TFMessage, "hex")); | ||
const reader = new CdrReader_1.CdrReader(data); | ||
reader.seekTo(4 + 4 + 4 + 4 + 4 + 10 + 4 + 6); | ||
expect(reader.float64()).toBeCloseTo(3.835); | ||
// This works due to aligned reads | ||
reader.seekTo(4 + 4 + 4 + 4 + 4 + 10 + 4 + 3); | ||
expect(reader.float64()).toBeCloseTo(3.835); | ||
reader.seek(-8); | ||
expect(reader.float64()).toBeCloseTo(3.835); | ||
expect(reader.float64()).toBeCloseTo(0); | ||
}); | ||
it.each([ | ||
["int8Array", "int8", [-128, 127, 3]], | ||
["uint8Array", "uint8", [0, 255, 3]], | ||
["int16Array", "int16", [-32768, 32767, -3]], | ||
["uint16Array", "uint16", [0, 65535, 3]], | ||
["int32Array", "int32", [-2147483648, 2147483647, 3]], | ||
["uint32Array", "uint32", [0, 4294967295, 3]], | ||
])("reads %s", (getter, setter, expected) => { | ||
const writer = new CdrWriter_1.CdrWriter(); | ||
writeArray(writer, setter, expected); | ||
const reader = new CdrReader_1.CdrReader(writer.data); | ||
const array = reader[getter](reader.sequenceLength()); | ||
expect(Array.from(array.values())).toEqual(expected); | ||
}); | ||
it.each([ | ||
["float32Array", "float32", [-3.835, 0, Math.PI], 6], | ||
["float64Array", "float64", [-3.835, 0, Math.PI], 15], | ||
["float64Array", "float64", [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, -0.123456789121212121212], 15], | ||
])("reads %s", (getter, setter, expected, numDigits) => { | ||
const writer = new CdrWriter_1.CdrWriter(); | ||
writeArray(writer, setter, expected); | ||
const reader = new CdrReader_1.CdrReader(writer.data); | ||
const array = reader[getter](reader.sequenceLength()); | ||
expectToBeCloseToArray(Array.from(array.values()), expected, numDigits); | ||
}); | ||
it.each([ | ||
["int64Array", "int64", [-9223372036854775808n, 9223372036854775807n, 3n]], | ||
["uint64Array", "uint64", [0n, 18446744073709551615n, 3n]], | ||
["uint64Array", "uint64", [1n, 2n, 3n, 4n, 5n, 6n, 7n, 8n, 9n, 10n, 11n, 12n]], | ||
])("reads %s", (getter, setter, expected) => { | ||
const writer = new CdrWriter_1.CdrWriter(); | ||
writeBigArray(writer, setter, expected); | ||
const reader = new CdrReader_1.CdrReader(writer.data); | ||
const array = reader[getter](reader.sequenceLength()); | ||
expect(Array.from(array.values())).toEqual(expected); | ||
}); | ||
it("reads stringArray", () => { | ||
const writer = new CdrWriter_1.CdrWriter(); | ||
writer.sequenceLength(3); | ||
writer.string("abc"); | ||
writer.string(""); | ||
writer.string("test string"); | ||
const reader = new CdrReader_1.CdrReader(writer.data); | ||
expect(reader.stringArray(reader.sequenceLength())).toEqual(["abc", "", "test string"]); | ||
}); | ||
}); | ||
function writeArray(writer, setter, array) { | ||
writer.sequenceLength(array.length); | ||
for (const value of array) { | ||
writer[setter](value); | ||
} | ||
} | ||
function writeBigArray(writer, setter, array) { | ||
writer.sequenceLength(array.length); | ||
for (const value of array) { | ||
writer[setter](value); | ||
} | ||
} | ||
function expectToBeCloseToArray(actual, expected, numDigits) { | ||
expect(actual.length).toBe(expected.length); | ||
actual.forEach((x, i) => expect(x).toBeCloseTo(expected[i], numDigits)); | ||
} | ||
//# sourceMappingURL=CdrReader.test.js.map |
@@ -8,3 +8,5 @@ export declare type CdrWriterOpts = { | ||
static DEFAULT_CAPACITY: number; | ||
static BUFFER_COPY_THRESHOLD: number; | ||
private littleEndian; | ||
private hostLittleEndian; | ||
private buffer; | ||
@@ -30,2 +32,12 @@ private array; | ||
sequenceLength(value: number): CdrWriter; | ||
int8Array(value: Int8Array | number[], writeLength?: boolean): CdrWriter; | ||
uint8Array(value: Uint8Array | number[], writeLength?: boolean): CdrWriter; | ||
int16Array(value: Int16Array | number[], writeLength?: boolean): CdrWriter; | ||
uint16Array(value: Uint16Array | number[], writeLength?: boolean): CdrWriter; | ||
int32Array(value: Int32Array | number[], writeLength?: boolean): CdrWriter; | ||
uint32Array(value: Uint32Array | number[], writeLength?: boolean): CdrWriter; | ||
int64Array(value: BigInt64Array | bigint[] | number[], writeLength?: boolean): CdrWriter; | ||
uint64Array(value: BigUint64Array | bigint[] | number[], writeLength?: boolean): CdrWriter; | ||
float32Array(value: Float32Array | number[], writeLength?: boolean): CdrWriter; | ||
float64Array(value: Float64Array | number[], writeLength?: boolean): CdrWriter; | ||
private align; | ||
@@ -32,0 +44,0 @@ private resizeIfNeeded; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.CdrWriter = void 0; | ||
const isBigEndian_1 = require("./isBigEndian"); | ||
class CdrWriter { | ||
@@ -17,2 +18,3 @@ constructor(options = {}) { | ||
this.littleEndian = !(options.bigEndian === true); | ||
this.hostLittleEndian = !isBigEndian_1.isBigEndian(); | ||
this.array = new Uint8Array(this.buffer); | ||
@@ -109,9 +111,172 @@ this.view = new DataView(this.buffer); | ||
} | ||
int8Array(value, writeLength) { | ||
if (writeLength === true) { | ||
this.sequenceLength(value.length); | ||
} | ||
this.resizeIfNeeded(value.length); | ||
this.array.set(value, this.offset); | ||
this.offset += value.length; | ||
return this; | ||
} | ||
uint8Array(value, writeLength) { | ||
if (writeLength === true) { | ||
this.sequenceLength(value.length); | ||
} | ||
this.resizeIfNeeded(value.length); | ||
this.array.set(value, this.offset); | ||
this.offset += value.length; | ||
return this; | ||
} | ||
int16Array(value, writeLength) { | ||
if (writeLength === true) { | ||
this.sequenceLength(value.length); | ||
} | ||
if (value instanceof Int16Array && | ||
this.littleEndian === this.hostLittleEndian && | ||
value.length >= CdrWriter.BUFFER_COPY_THRESHOLD) { | ||
this.align(value.BYTES_PER_ELEMENT, value.byteLength); | ||
this.array.set(new Uint8Array(value.buffer, value.byteOffset, value.byteLength), this.offset); | ||
this.offset += value.byteLength; | ||
} | ||
else { | ||
for (const entry of value) { | ||
this.int16(entry); | ||
} | ||
} | ||
return this; | ||
} | ||
uint16Array(value, writeLength) { | ||
if (writeLength === true) { | ||
this.sequenceLength(value.length); | ||
} | ||
if (value instanceof Uint16Array && | ||
this.littleEndian === this.hostLittleEndian && | ||
value.length >= CdrWriter.BUFFER_COPY_THRESHOLD) { | ||
this.align(value.BYTES_PER_ELEMENT, value.byteLength); | ||
this.array.set(new Uint8Array(value.buffer, value.byteOffset, value.byteLength), this.offset); | ||
this.offset += value.byteLength; | ||
} | ||
else { | ||
for (const entry of value) { | ||
this.uint16(entry); | ||
} | ||
} | ||
return this; | ||
} | ||
int32Array(value, writeLength) { | ||
if (writeLength === true) { | ||
this.sequenceLength(value.length); | ||
} | ||
if (value instanceof Int32Array && | ||
this.littleEndian === this.hostLittleEndian && | ||
value.length >= CdrWriter.BUFFER_COPY_THRESHOLD) { | ||
this.align(value.BYTES_PER_ELEMENT, value.byteLength); | ||
this.array.set(new Uint8Array(value.buffer, value.byteOffset, value.byteLength), this.offset); | ||
this.offset += value.byteLength; | ||
} | ||
else { | ||
for (const entry of value) { | ||
this.int32(entry); | ||
} | ||
} | ||
return this; | ||
} | ||
uint32Array(value, writeLength) { | ||
if (writeLength === true) { | ||
this.sequenceLength(value.length); | ||
} | ||
if (value instanceof Uint32Array && | ||
this.littleEndian === this.hostLittleEndian && | ||
value.length >= CdrWriter.BUFFER_COPY_THRESHOLD) { | ||
this.align(value.BYTES_PER_ELEMENT, value.byteLength); | ||
this.array.set(new Uint8Array(value.buffer, value.byteOffset, value.byteLength), this.offset); | ||
this.offset += value.byteLength; | ||
} | ||
else { | ||
for (const entry of value) { | ||
this.uint32(entry); | ||
} | ||
} | ||
return this; | ||
} | ||
int64Array(value, writeLength) { | ||
if (writeLength === true) { | ||
this.sequenceLength(value.length); | ||
} | ||
if (value instanceof BigInt64Array && | ||
this.littleEndian === this.hostLittleEndian && | ||
value.length >= CdrWriter.BUFFER_COPY_THRESHOLD) { | ||
this.align(value.BYTES_PER_ELEMENT, value.byteLength); | ||
this.array.set(new Uint8Array(value.buffer, value.byteOffset, value.byteLength), this.offset); | ||
this.offset += value.byteLength; | ||
} | ||
else { | ||
for (const entry of value) { | ||
this.int64(BigInt(entry)); | ||
} | ||
} | ||
return this; | ||
} | ||
uint64Array(value, writeLength) { | ||
if (writeLength === true) { | ||
this.sequenceLength(value.length); | ||
} | ||
if (value instanceof BigUint64Array && | ||
this.littleEndian === this.hostLittleEndian && | ||
value.length >= CdrWriter.BUFFER_COPY_THRESHOLD) { | ||
this.align(value.BYTES_PER_ELEMENT, value.byteLength); | ||
this.array.set(new Uint8Array(value.buffer, value.byteOffset, value.byteLength), this.offset); | ||
this.offset += value.byteLength; | ||
} | ||
else { | ||
for (const entry of value) { | ||
this.uint64(BigInt(entry)); | ||
} | ||
} | ||
return this; | ||
} | ||
float32Array(value, writeLength) { | ||
if (writeLength === true) { | ||
this.sequenceLength(value.length); | ||
} | ||
if (value instanceof Float32Array && | ||
this.littleEndian === this.hostLittleEndian && | ||
value.length >= CdrWriter.BUFFER_COPY_THRESHOLD) { | ||
this.align(value.BYTES_PER_ELEMENT, value.byteLength); | ||
this.array.set(new Uint8Array(value.buffer, value.byteOffset, value.byteLength), this.offset); | ||
this.offset += value.byteLength; | ||
} | ||
else { | ||
for (const entry of value) { | ||
this.float32(entry); | ||
} | ||
} | ||
return this; | ||
} | ||
float64Array(value, writeLength) { | ||
if (writeLength === true) { | ||
this.sequenceLength(value.length); | ||
} | ||
if (value instanceof Float64Array && | ||
this.littleEndian === this.hostLittleEndian && | ||
value.length >= CdrWriter.BUFFER_COPY_THRESHOLD) { | ||
this.align(value.BYTES_PER_ELEMENT, value.byteLength); | ||
this.array.set(new Uint8Array(value.buffer, value.byteOffset, value.byteLength), this.offset); | ||
this.offset += value.byteLength; | ||
} | ||
else { | ||
for (const entry of value) { | ||
this.float64(entry); | ||
} | ||
} | ||
return this; | ||
} | ||
// Calculate the capacity needed to hold the given number of aligned bytes, | ||
// resize if needed, and write padding bytes for alignment | ||
align(size) { | ||
align(size, bytesToWrite) { | ||
bytesToWrite ?? (bytesToWrite = size); | ||
// The four byte header is not considered for alignment | ||
const alignment = (this.offset - 4) % size; | ||
const padding = alignment > 0 ? size - alignment : 0; | ||
this.resizeIfNeeded(padding + size); | ||
this.resizeIfNeeded(padding + bytesToWrite); | ||
// Write padding bytes | ||
@@ -143,2 +308,3 @@ this.array.fill(0, this.offset, this.offset + padding); | ||
CdrWriter.DEFAULT_CAPACITY = 16; | ||
CdrWriter.BUFFER_COPY_THRESHOLD = 10; | ||
//# sourceMappingURL=CdrWriter.js.map |
@@ -78,3 +78,17 @@ "use strict"; | ||
}); | ||
it("round trips all array types", () => { | ||
const writer = new CdrWriter_1.CdrWriter(); | ||
writer.int8Array([-128, 127, 3], true); | ||
writer.uint8Array([0, 255, 3], true); | ||
writer.int16Array([-32768, 32767, -3], true); | ||
writer.uint16Array([0, 65535, 3], true); | ||
writer.int32Array([-2147483648, 2147483647, 3], true); | ||
writer.uint32Array([0, 4294967295, 3], true); | ||
writer.int64Array([-9223372036854775808n, 9223372036854775807n, 3n], true); | ||
writer.uint64Array([0n, 18446744073709551615n, 3n], true); | ||
const reader = new CdrReader_1.CdrReader(writer.data); | ||
expect(Array.from(reader.int8Array().values())).toEqual([-128, 127, 3]); | ||
expect(Array.from(reader.uint8Array().values())).toEqual([0, 255, 3]); | ||
}); | ||
}); | ||
//# sourceMappingURL=CdrWriter.test.js.map |
{ | ||
"name": "@foxglove/cdr", | ||
"version": "0.1.0", | ||
"version": "0.2.0", | ||
"description": "Common Data Representation serialization and deserialization library", | ||
@@ -5,0 +5,0 @@ "license": "MIT", |
import { CdrReader } from "./CdrReader"; | ||
import { CdrWriter } from "./CdrWriter"; | ||
type ArrayGetter = | ||
| "int8Array" | ||
| "uint8Array" | ||
| "int16Array" | ||
| "uint16Array" | ||
| "int32Array" | ||
| "uint32Array" | ||
| "float32Array" | ||
| "float64Array"; | ||
type Setter = "int8" | "uint8" | "int16" | "uint16" | "int32" | "uint32" | "float32" | "float64"; | ||
// Example tf2_msgs/TFMessage | ||
const tf2_msg__TFMessage = | ||
"0001000001000000cce0d158f08cf9060a000000626173655f6c696e6b000000060000007261646172000000ae47e17a14ae0e4000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f03f"; | ||
describe("CdrReader", () => { | ||
it("parses an example message", () => { | ||
// Example tf2_msgs/TFMessage | ||
const tf2_msg__TFMessage = | ||
"0001000001000000cce0d158f08cf9060a000000626173655f6c696e6b000000060000007261646172000000ae47e17a14ae0e4000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f03f"; | ||
const data = Uint8Array.from(Buffer.from(tf2_msg__TFMessage, "hex")); | ||
@@ -30,2 +43,90 @@ const reader = new CdrReader(data); | ||
}); | ||
it("seeks to absolute and relative positions", () => { | ||
const data = Uint8Array.from(Buffer.from(tf2_msg__TFMessage, "hex")); | ||
const reader = new CdrReader(data); | ||
reader.seekTo(4 + 4 + 4 + 4 + 4 + 10 + 4 + 6); | ||
expect(reader.float64()).toBeCloseTo(3.835); | ||
// This works due to aligned reads | ||
reader.seekTo(4 + 4 + 4 + 4 + 4 + 10 + 4 + 3); | ||
expect(reader.float64()).toBeCloseTo(3.835); | ||
reader.seek(-8); | ||
expect(reader.float64()).toBeCloseTo(3.835); | ||
expect(reader.float64()).toBeCloseTo(0); | ||
}); | ||
it.each([ | ||
["int8Array", "int8", [-128, 127, 3]], | ||
["uint8Array", "uint8", [0, 255, 3]], | ||
["int16Array", "int16", [-32768, 32767, -3]], | ||
["uint16Array", "uint16", [0, 65535, 3]], | ||
["int32Array", "int32", [-2147483648, 2147483647, 3]], | ||
["uint32Array", "uint32", [0, 4294967295, 3]], | ||
])("reads %s", (getter: string, setter: string, expected: number[]) => { | ||
const writer = new CdrWriter(); | ||
writeArray(writer, setter as Setter, expected); | ||
const reader = new CdrReader(writer.data); | ||
const array = reader[getter as ArrayGetter](reader.sequenceLength()); | ||
expect(Array.from(array.values())).toEqual(expected); | ||
}); | ||
it.each([ | ||
["float32Array", "float32", [-3.835, 0, Math.PI], 6], | ||
["float64Array", "float64", [-3.835, 0, Math.PI], 15], | ||
["float64Array", "float64", [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, -0.123456789121212121212], 15], | ||
])("reads %s", (getter: string, setter: string, expected: number[], numDigits: number) => { | ||
const writer = new CdrWriter(); | ||
writeArray(writer, setter as Setter, expected); | ||
const reader = new CdrReader(writer.data); | ||
const array = reader[getter as ArrayGetter](reader.sequenceLength()); | ||
expectToBeCloseToArray(Array.from(array.values()), expected, numDigits); | ||
}); | ||
it.each([ | ||
["int64Array", "int64", [-9223372036854775808n, 9223372036854775807n, 3n]], | ||
["uint64Array", "uint64", [0n, 18446744073709551615n, 3n]], | ||
["uint64Array", "uint64", [1n, 2n, 3n, 4n, 5n, 6n, 7n, 8n, 9n, 10n, 11n, 12n]], | ||
])("reads %s", (getter: string, setter: string, expected: bigint[]) => { | ||
const writer = new CdrWriter(); | ||
writeBigArray(writer, setter as "int64" | "uint64", expected); | ||
const reader = new CdrReader(writer.data); | ||
const array = reader[getter as "int64Array" | "uint64Array"](reader.sequenceLength()); | ||
expect(Array.from(array.values())).toEqual(expected); | ||
}); | ||
it("reads stringArray", () => { | ||
const writer = new CdrWriter(); | ||
writer.sequenceLength(3); | ||
writer.string("abc"); | ||
writer.string(""); | ||
writer.string("test string"); | ||
const reader = new CdrReader(writer.data); | ||
expect(reader.stringArray(reader.sequenceLength())).toEqual(["abc", "", "test string"]); | ||
}); | ||
}); | ||
function writeArray(writer: CdrWriter, setter: Setter, array: number[]): void { | ||
writer.sequenceLength(array.length); | ||
for (const value of array) { | ||
writer[setter](value); | ||
} | ||
} | ||
function writeBigArray(writer: CdrWriter, setter: "int64" | "uint64", array: bigint[]): void { | ||
writer.sequenceLength(array.length); | ||
for (const value of array) { | ||
writer[setter](value); | ||
} | ||
} | ||
function expectToBeCloseToArray(actual: number[], expected: number[], numDigits: number): void { | ||
expect(actual.length).toBe(expected.length); | ||
actual.forEach((x, i) => expect(x).toBeCloseTo(expected[i]!, numDigits)); | ||
} |
@@ -0,1 +1,25 @@ | ||
import { isBigEndian } from "./isBigEndian"; | ||
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"; | ||
export class CdrReader { | ||
@@ -6,2 +30,3 @@ private array: Uint8Array; | ||
private littleEndian: boolean; | ||
private hostLittleEndian: boolean; | ||
private textDecoder = new TextDecoder("utf8"); | ||
@@ -18,2 +43,4 @@ | ||
constructor(data: Uint8Array) { | ||
this.hostLittleEndian = !isBigEndian(); | ||
if (data.byteLength < 4) { | ||
@@ -114,2 +141,90 @@ throw new Error( | ||
int8Array(count?: number): Int8Array { | ||
count ??= this.sequenceLength(); | ||
const array = new Int8Array(this.data.buffer, this.data.byteOffset + this.offset, count); | ||
this.offset += count; | ||
return array; | ||
} | ||
uint8Array(count?: number): Uint8Array { | ||
count ??= this.sequenceLength(); | ||
const array = new Uint8Array(this.data.buffer, this.data.byteOffset + this.offset, count); | ||
this.offset += count; | ||
return array; | ||
} | ||
int16Array(count?: number): Int16Array { | ||
count ??= this.sequenceLength(); | ||
return this.typedArray(Int16Array, "getInt16", count); | ||
} | ||
uint16Array(count?: number): Uint16Array { | ||
count ??= this.sequenceLength(); | ||
return this.typedArray(Uint16Array, "getUint16", count); | ||
} | ||
int32Array(count?: number): Int32Array { | ||
count ??= this.sequenceLength(); | ||
return this.typedArray(Int32Array, "getInt32", count); | ||
} | ||
uint32Array(count?: number): Uint32Array { | ||
count ??= this.sequenceLength(); | ||
return this.typedArray(Uint32Array, "getUint32", count); | ||
} | ||
int64Array(count?: number): BigInt64Array { | ||
count ??= this.sequenceLength(); | ||
return this.typedArray(BigInt64Array, "getBigInt64", count); | ||
} | ||
uint64Array(count?: number): BigUint64Array { | ||
count ??= this.sequenceLength(); | ||
return this.typedArray(BigUint64Array, "getBigUint64", count); | ||
} | ||
float32Array(count?: number): Float32Array { | ||
count ??= this.sequenceLength(); | ||
return this.typedArray(Float32Array, "getFloat32", count); | ||
} | ||
float64Array(count?: number): Float64Array { | ||
count ??= this.sequenceLength(); | ||
return this.typedArray(Float64Array, "getFloat64", count); | ||
} | ||
stringArray(count?: number): string[] { | ||
count ??= this.sequenceLength(); | ||
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.data.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.data.byteLength) { | ||
throw new Error(`seekTo(${offset}) failed, value is outside the data range`); | ||
} | ||
this.offset = offset; | ||
} | ||
private align(size: number): void { | ||
@@ -121,2 +236,56 @@ const alignment = (this.offset - 4) % size; | ||
} | ||
// Reads a given count of numeric values into a typed array. | ||
private typedArray<T extends Indexable>( | ||
TypedArrayConstructor: TypedArrayConstructor<T>, | ||
getter: ArrayValueGetter, | ||
count: number, | ||
) { | ||
this.align(TypedArrayConstructor.BYTES_PER_ELEMENT); | ||
const totalOffset = this.data.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 | ||
return new TypedArrayConstructor(this.data.buffer, totalOffset, count); | ||
} 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; | ||
} | ||
} |
@@ -84,2 +84,18 @@ import { CdrReader } from "./CdrReader"; | ||
}); | ||
it("round trips all array types", () => { | ||
const writer = new CdrWriter(); | ||
writer.int8Array([-128, 127, 3], true); | ||
writer.uint8Array([0, 255, 3], true); | ||
writer.int16Array([-32768, 32767, -3], true); | ||
writer.uint16Array([0, 65535, 3], true); | ||
writer.int32Array([-2147483648, 2147483647, 3], true); | ||
writer.uint32Array([0, 4294967295, 3], true); | ||
writer.int64Array([-9223372036854775808n, 9223372036854775807n, 3n], true); | ||
writer.uint64Array([0n, 18446744073709551615n, 3n], true); | ||
const reader = new CdrReader(writer.data); | ||
expect(Array.from(reader.int8Array().values())).toEqual([-128, 127, 3]); | ||
expect(Array.from(reader.uint8Array().values())).toEqual([0, 255, 3]); | ||
}); | ||
}); |
@@ -0,1 +1,3 @@ | ||
import { isBigEndian } from "./isBigEndian"; | ||
export type CdrWriterOpts = { | ||
@@ -9,4 +11,6 @@ buffer?: ArrayBuffer; | ||
static DEFAULT_CAPACITY = 16; | ||
static BUFFER_COPY_THRESHOLD = 10; | ||
private littleEndian: boolean; | ||
private hostLittleEndian: boolean; | ||
private buffer: ArrayBuffer; | ||
@@ -36,2 +40,3 @@ private array: Uint8Array; | ||
this.littleEndian = !(options.bigEndian === true); | ||
this.hostLittleEndian = !isBigEndian(); | ||
this.array = new Uint8Array(this.buffer); | ||
@@ -136,9 +141,190 @@ this.view = new DataView(this.buffer); | ||
int8Array(value: Int8Array | number[], writeLength?: boolean): CdrWriter { | ||
if (writeLength === true) { | ||
this.sequenceLength(value.length); | ||
} | ||
this.resizeIfNeeded(value.length); | ||
this.array.set(value, this.offset); | ||
this.offset += value.length; | ||
return this; | ||
} | ||
uint8Array(value: Uint8Array | number[], writeLength?: boolean): CdrWriter { | ||
if (writeLength === true) { | ||
this.sequenceLength(value.length); | ||
} | ||
this.resizeIfNeeded(value.length); | ||
this.array.set(value, this.offset); | ||
this.offset += value.length; | ||
return this; | ||
} | ||
int16Array(value: Int16Array | number[], writeLength?: boolean): CdrWriter { | ||
if (writeLength === true) { | ||
this.sequenceLength(value.length); | ||
} | ||
if ( | ||
value instanceof Int16Array && | ||
this.littleEndian === this.hostLittleEndian && | ||
value.length >= CdrWriter.BUFFER_COPY_THRESHOLD | ||
) { | ||
this.align(value.BYTES_PER_ELEMENT, value.byteLength); | ||
this.array.set(new Uint8Array(value.buffer, value.byteOffset, value.byteLength), this.offset); | ||
this.offset += value.byteLength; | ||
} else { | ||
for (const entry of value) { | ||
this.int16(entry); | ||
} | ||
} | ||
return this; | ||
} | ||
uint16Array(value: Uint16Array | number[], writeLength?: boolean): CdrWriter { | ||
if (writeLength === true) { | ||
this.sequenceLength(value.length); | ||
} | ||
if ( | ||
value instanceof Uint16Array && | ||
this.littleEndian === this.hostLittleEndian && | ||
value.length >= CdrWriter.BUFFER_COPY_THRESHOLD | ||
) { | ||
this.align(value.BYTES_PER_ELEMENT, value.byteLength); | ||
this.array.set(new Uint8Array(value.buffer, value.byteOffset, value.byteLength), this.offset); | ||
this.offset += value.byteLength; | ||
} else { | ||
for (const entry of value) { | ||
this.uint16(entry); | ||
} | ||
} | ||
return this; | ||
} | ||
int32Array(value: Int32Array | number[], writeLength?: boolean): CdrWriter { | ||
if (writeLength === true) { | ||
this.sequenceLength(value.length); | ||
} | ||
if ( | ||
value instanceof Int32Array && | ||
this.littleEndian === this.hostLittleEndian && | ||
value.length >= CdrWriter.BUFFER_COPY_THRESHOLD | ||
) { | ||
this.align(value.BYTES_PER_ELEMENT, value.byteLength); | ||
this.array.set(new Uint8Array(value.buffer, value.byteOffset, value.byteLength), this.offset); | ||
this.offset += value.byteLength; | ||
} else { | ||
for (const entry of value) { | ||
this.int32(entry); | ||
} | ||
} | ||
return this; | ||
} | ||
uint32Array(value: Uint32Array | number[], writeLength?: boolean): CdrWriter { | ||
if (writeLength === true) { | ||
this.sequenceLength(value.length); | ||
} | ||
if ( | ||
value instanceof Uint32Array && | ||
this.littleEndian === this.hostLittleEndian && | ||
value.length >= CdrWriter.BUFFER_COPY_THRESHOLD | ||
) { | ||
this.align(value.BYTES_PER_ELEMENT, value.byteLength); | ||
this.array.set(new Uint8Array(value.buffer, value.byteOffset, value.byteLength), this.offset); | ||
this.offset += value.byteLength; | ||
} else { | ||
for (const entry of value) { | ||
this.uint32(entry); | ||
} | ||
} | ||
return this; | ||
} | ||
int64Array(value: BigInt64Array | bigint[] | number[], writeLength?: boolean): CdrWriter { | ||
if (writeLength === true) { | ||
this.sequenceLength(value.length); | ||
} | ||
if ( | ||
value instanceof BigInt64Array && | ||
this.littleEndian === this.hostLittleEndian && | ||
value.length >= CdrWriter.BUFFER_COPY_THRESHOLD | ||
) { | ||
this.align(value.BYTES_PER_ELEMENT, value.byteLength); | ||
this.array.set(new Uint8Array(value.buffer, value.byteOffset, value.byteLength), this.offset); | ||
this.offset += value.byteLength; | ||
} else { | ||
for (const entry of value) { | ||
this.int64(BigInt(entry)); | ||
} | ||
} | ||
return this; | ||
} | ||
uint64Array(value: BigUint64Array | bigint[] | number[], writeLength?: boolean): CdrWriter { | ||
if (writeLength === true) { | ||
this.sequenceLength(value.length); | ||
} | ||
if ( | ||
value instanceof BigUint64Array && | ||
this.littleEndian === this.hostLittleEndian && | ||
value.length >= CdrWriter.BUFFER_COPY_THRESHOLD | ||
) { | ||
this.align(value.BYTES_PER_ELEMENT, value.byteLength); | ||
this.array.set(new Uint8Array(value.buffer, value.byteOffset, value.byteLength), this.offset); | ||
this.offset += value.byteLength; | ||
} else { | ||
for (const entry of value) { | ||
this.uint64(BigInt(entry)); | ||
} | ||
} | ||
return this; | ||
} | ||
float32Array(value: Float32Array | number[], writeLength?: boolean): CdrWriter { | ||
if (writeLength === true) { | ||
this.sequenceLength(value.length); | ||
} | ||
if ( | ||
value instanceof Float32Array && | ||
this.littleEndian === this.hostLittleEndian && | ||
value.length >= CdrWriter.BUFFER_COPY_THRESHOLD | ||
) { | ||
this.align(value.BYTES_PER_ELEMENT, value.byteLength); | ||
this.array.set(new Uint8Array(value.buffer, value.byteOffset, value.byteLength), this.offset); | ||
this.offset += value.byteLength; | ||
} else { | ||
for (const entry of value) { | ||
this.float32(entry); | ||
} | ||
} | ||
return this; | ||
} | ||
float64Array(value: Float64Array | number[], writeLength?: boolean): CdrWriter { | ||
if (writeLength === true) { | ||
this.sequenceLength(value.length); | ||
} | ||
if ( | ||
value instanceof Float64Array && | ||
this.littleEndian === this.hostLittleEndian && | ||
value.length >= CdrWriter.BUFFER_COPY_THRESHOLD | ||
) { | ||
this.align(value.BYTES_PER_ELEMENT, value.byteLength); | ||
this.array.set(new Uint8Array(value.buffer, value.byteOffset, value.byteLength), this.offset); | ||
this.offset += value.byteLength; | ||
} else { | ||
for (const entry of value) { | ||
this.float64(entry); | ||
} | ||
} | ||
return this; | ||
} | ||
// Calculate the capacity needed to hold the given number of aligned bytes, | ||
// resize if needed, and write padding bytes for alignment | ||
private align(size: number): void { | ||
private align(size: number, bytesToWrite?: number): void { | ||
bytesToWrite ??= size; | ||
// The four byte header is not considered for alignment | ||
const alignment = (this.offset - 4) % size; | ||
const padding = alignment > 0 ? size - alignment : 0; | ||
this.resizeIfNeeded(padding + size); | ||
this.resizeIfNeeded(padding + bytesToWrite); | ||
// Write padding bytes | ||
@@ -145,0 +331,0 @@ this.array.fill(0, this.offset, this.offset + padding); |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
114429
43
1903