Skip to content

Implement CBOR typed arrays per RFC 8746 #27

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6,264 changes: 6,264 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

201 changes: 201 additions & 0 deletions src/cbor/CborDecoder.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {CONST, ERROR, MAJOR} from './constants';
import {CborDecoderBase} from './CborDecoderBase';
import {JsonPackValue} from '../JsonPackValue';
import {TYPED_ARRAY_TAG, ARRAY_TAG} from './constants';
import type {Path} from '../json-pointer';
import type {IReader, IReaderResettable} from '@jsonjoy.com/util/lib/buffers';

Expand Down Expand Up @@ -197,6 +198,206 @@ export class CborDecoder<
this.skipAny();
}

// ----------------------------------------------------------- Tag reading override

public readTagRaw(tag: number): unknown {
// Handle RFC 8746 typed array tags (64-87)
if (tag >= TYPED_ARRAY_TAG.UINT8 && tag <= TYPED_ARRAY_TAG.FLOAT128_LE) {
return this.readTypedArrayTag(tag);
}

// Handle RFC 8746 additional array tags
switch (tag) {
case ARRAY_TAG.MULTI_DIM_ROW_MAJOR:
return this.readMultiDimensionalArray(true);
case ARRAY_TAG.MULTI_DIM_COLUMN_MAJOR:
return this.readMultiDimensionalArray(false);
case ARRAY_TAG.HOMOGENEOUS:
return this.readHomogeneousArray();
}

// Default behavior for other tags
return super.readTagRaw(tag);
}

/**
* Decode a typed array from CBOR tag and byte string
*/
private readTypedArrayTag(tag: number): ArrayBufferView {
const bytes = this.val() as Uint8Array;

switch (tag) {
case TYPED_ARRAY_TAG.UINT8:
return new Uint8Array(bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength));
case TYPED_ARRAY_TAG.UINT8_CLAMPED:
return new Uint8ClampedArray(bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength));
case TYPED_ARRAY_TAG.SINT8:
return new Int8Array(bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength));

// 16-bit arrays
case TYPED_ARRAY_TAG.UINT16_BE:
return this.createUint16Array(bytes, false);
case TYPED_ARRAY_TAG.UINT16_LE:
return this.createUint16Array(bytes, true);
case TYPED_ARRAY_TAG.SINT16_BE:
return this.createInt16Array(bytes, false);
case TYPED_ARRAY_TAG.SINT16_LE:
return this.createInt16Array(bytes, true);

// 32-bit arrays
case TYPED_ARRAY_TAG.UINT32_BE:
return this.createUint32Array(bytes, false);
case TYPED_ARRAY_TAG.UINT32_LE:
return this.createUint32Array(bytes, true);
case TYPED_ARRAY_TAG.SINT32_BE:
return this.createInt32Array(bytes, false);
case TYPED_ARRAY_TAG.SINT32_LE:
return this.createInt32Array(bytes, true);

// 64-bit arrays
case TYPED_ARRAY_TAG.UINT64_BE:
return this.createBigUint64Array(bytes, false);
case TYPED_ARRAY_TAG.UINT64_LE:
return this.createBigUint64Array(bytes, true);
case TYPED_ARRAY_TAG.SINT64_BE:
return this.createBigInt64Array(bytes, false);
case TYPED_ARRAY_TAG.SINT64_LE:
return this.createBigInt64Array(bytes, true);

// Float arrays
case TYPED_ARRAY_TAG.FLOAT32_BE:
return this.createFloat32Array(bytes, false);
case TYPED_ARRAY_TAG.FLOAT32_LE:
return this.createFloat32Array(bytes, true);
case TYPED_ARRAY_TAG.FLOAT64_BE:
return this.createFloat64Array(bytes, false);
case TYPED_ARRAY_TAG.FLOAT64_LE:
return this.createFloat64Array(bytes, true);

// 16-bit and 128-bit floats are not supported by JavaScript
case TYPED_ARRAY_TAG.FLOAT16_BE:
case TYPED_ARRAY_TAG.FLOAT16_LE:
case TYPED_ARRAY_TAG.FLOAT128_BE:
case TYPED_ARRAY_TAG.FLOAT128_LE:
throw new Error(`Unsupported floating point format: tag ${tag}`);

default:
throw new Error(`Unknown typed array tag: ${tag}`);
}
}

private createUint16Array(bytes: Uint8Array, littleEndian: boolean): Uint16Array {
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
const array = new Uint16Array(bytes.byteLength / 2);
for (let i = 0; i < array.length; i++) {
array[i] = view.getUint16(i * 2, littleEndian);
}
return array;
}

private createInt16Array(bytes: Uint8Array, littleEndian: boolean): Int16Array {
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
const array = new Int16Array(bytes.byteLength / 2);
for (let i = 0; i < array.length; i++) {
array[i] = view.getInt16(i * 2, littleEndian);
}
return array;
}

private createUint32Array(bytes: Uint8Array, littleEndian: boolean): Uint32Array {
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
const array = new Uint32Array(bytes.byteLength / 4);
for (let i = 0; i < array.length; i++) {
array[i] = view.getUint32(i * 4, littleEndian);
}
return array;
}

private createInt32Array(bytes: Uint8Array, littleEndian: boolean): Int32Array {
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
const array = new Int32Array(bytes.byteLength / 4);
for (let i = 0; i < array.length; i++) {
array[i] = view.getInt32(i * 4, littleEndian);
}
return array;
}

private createBigUint64Array(bytes: Uint8Array, littleEndian: boolean): BigUint64Array {
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
const array = new BigUint64Array(bytes.byteLength / 8);
for (let i = 0; i < array.length; i++) {
array[i] = view.getBigUint64(i * 8, littleEndian);
}
return array;
}

private createBigInt64Array(bytes: Uint8Array, littleEndian: boolean): BigInt64Array {
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
const array = new BigInt64Array(bytes.byteLength / 8);
for (let i = 0; i < array.length; i++) {
array[i] = view.getBigInt64(i * 8, littleEndian);
}
return array;
}

private createFloat32Array(bytes: Uint8Array, littleEndian: boolean): Float32Array {
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
const array = new Float32Array(bytes.byteLength / 4);
for (let i = 0; i < array.length; i++) {
array[i] = view.getFloat32(i * 4, littleEndian);
}
return array;
}

private createFloat64Array(bytes: Uint8Array, littleEndian: boolean): Float64Array {
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
const array = new Float64Array(bytes.byteLength / 8);
for (let i = 0; i < array.length; i++) {
array[i] = view.getFloat64(i * 8, littleEndian);
}
return array;
}

/**
* Read a multi-dimensional array (tag 40 or 1040)
*/
private readMultiDimensionalArray(rowMajor: boolean): unknown {
const data = this.val() as unknown[];
if (!Array.isArray(data) || data.length !== 2) {
throw new Error('Multi-dimensional array must contain exactly 2 elements');
}

const dimensions = data[0] as number[];
const elements = data[1];

if (!Array.isArray(dimensions) || dimensions.some(d => typeof d !== 'number' || d <= 0)) {
throw new Error('Multi-dimensional array dimensions must be positive integers');
}

// For now, we return the original structure as-is
// In a more sophisticated implementation, you might want to reshape the data
return {
dimensions,
elements,
order: rowMajor ? 'row-major' : 'column-major'
};
}

/**
* Read a homogeneous array (tag 41)
*/
private readHomogeneousArray(): unknown[] {
const array = this.val();
if (!Array.isArray(array)) {
throw new Error('Homogeneous array must contain an array');
}

// For now, we just return the array as-is
// In a more sophisticated implementation, you might want to validate homogeneity
// or create a specialized data structure
return array;
}

// ----------------------------------------------------------- Token skipping

public skipTkn(minor: number): void {
Expand Down
112 changes: 112 additions & 0 deletions src/cbor/CborEncoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,22 @@ import {isFloat32} from '@jsonjoy.com/util/lib/buffers/isFloat32';
import {JsonPackExtension} from '../JsonPackExtension';
import {CborEncoderFast} from './CborEncoderFast';
import {JsonPackValue} from '../JsonPackValue';
import {TYPED_ARRAY_TAG, ARRAY_TAG} from './constants';
import {isLittleEndian} from './shared';
import type {IWriter, IWriterGrowable} from '@jsonjoy.com/util/lib/buffers';

export interface CborMultiDimensionalArray {
__cbor_multi_dim__: true;
dimensions: number[];
elements: unknown;
rowMajor?: boolean;
}

export interface CborHomogeneousArray {
__cbor_homogeneous__: true;
elements: unknown[];
}

export class CborEncoder<W extends IWriter & IWriterGrowable = IWriter & IWriterGrowable> extends CborEncoderFast<W> {
/**
* Called when the encoder encounters a value that it does not know how to encode.
Expand All @@ -27,11 +41,34 @@ export class CborEncoder<W extends IWriter & IWriterGrowable = IWriter & IWriter
const constructor = value.constructor;
switch (constructor) {
case Object:
// Check for special CBOR array types first
if (this.isCborMultiDimensionalArray(value)) return this.writeMultiDimensionalArray(value);
if (this.isCborHomogeneousArray(value)) return this.writeHomogeneousArray(value);
return this.writeObj(value as Record<string, unknown>);
case Array:
return this.writeArr(value as unknown[]);
case Uint8Array:
return this.writeBin(value as Uint8Array);
case Int8Array:
return this.writeTypedArray(value as Int8Array);
case Uint8ClampedArray:
return this.writeTypedArray(value as Uint8ClampedArray);
case Int16Array:
return this.writeTypedArray(value as Int16Array);
case Uint16Array:
return this.writeTypedArray(value as Uint16Array);
case Int32Array:
return this.writeTypedArray(value as Int32Array);
case Uint32Array:
return this.writeTypedArray(value as Uint32Array);
case Float32Array:
return this.writeTypedArray(value as Float32Array);
case Float64Array:
return this.writeTypedArray(value as Float64Array);
case BigInt64Array:
return this.writeTypedArray(value as BigInt64Array);
case BigUint64Array:
return this.writeTypedArray(value as BigUint64Array);
case Map:
return this.writeMap(value as Map<unknown, unknown>);
case JsonPackExtension:
Expand All @@ -43,6 +80,8 @@ export class CborEncoder<W extends IWriter & IWriterGrowable = IWriter & IWriter
if (value instanceof Uint8Array) return this.writeBin(value);
if (Array.isArray(value)) return this.writeArr(value);
if (value instanceof Map) return this.writeMap(value);
if (this.isCborMultiDimensionalArray(value)) return this.writeMultiDimensionalArray(value);
if (this.isCborHomogeneousArray(value)) return this.writeHomogeneousArray(value);
return this.writeUnknown(value);
}
}
Expand Down Expand Up @@ -71,4 +110,77 @@ export class CborEncoder<W extends IWriter & IWriterGrowable = IWriter & IWriter
public writeUndef(): void {
this.writer.u8(0xf7);
}

/**
* Write a typed array using RFC 8746 CBOR typed array tags
*/
public writeTypedArray(value: ArrayBufferView): void {
const tag = this.getTypedArrayTag(value);
const buffer = new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
this.writeTag(tag, buffer);
}

/**
* Determine the appropriate CBOR tag for a typed array
*/
private getTypedArrayTag(value: ArrayBufferView): number {
const constructor = value.constructor;

switch (constructor) {
case Int8Array:
return TYPED_ARRAY_TAG.SINT8;
case Uint8ClampedArray:
return TYPED_ARRAY_TAG.UINT8_CLAMPED;
case Int16Array:
return isLittleEndian ? TYPED_ARRAY_TAG.SINT16_LE : TYPED_ARRAY_TAG.SINT16_BE;
case Uint16Array:
return isLittleEndian ? TYPED_ARRAY_TAG.UINT16_LE : TYPED_ARRAY_TAG.UINT16_BE;
case Int32Array:
return isLittleEndian ? TYPED_ARRAY_TAG.SINT32_LE : TYPED_ARRAY_TAG.SINT32_BE;
case Uint32Array:
return isLittleEndian ? TYPED_ARRAY_TAG.UINT32_LE : TYPED_ARRAY_TAG.UINT32_BE;
case Float32Array:
return isLittleEndian ? TYPED_ARRAY_TAG.FLOAT32_LE : TYPED_ARRAY_TAG.FLOAT32_BE;
case Float64Array:
return isLittleEndian ? TYPED_ARRAY_TAG.FLOAT64_LE : TYPED_ARRAY_TAG.FLOAT64_BE;
case BigInt64Array:
return isLittleEndian ? TYPED_ARRAY_TAG.SINT64_LE : TYPED_ARRAY_TAG.SINT64_BE;
case BigUint64Array:
return isLittleEndian ? TYPED_ARRAY_TAG.UINT64_LE : TYPED_ARRAY_TAG.UINT64_BE;
default:
throw new Error(`Unsupported typed array type: ${constructor.name}`);
}
}

/**
* Check if value is a CBOR multi-dimensional array
*/
private isCborMultiDimensionalArray(value: unknown): value is CborMultiDimensionalArray {
return typeof value === 'object' && value !== null &&
'__cbor_multi_dim__' in value && (value as any).__cbor_multi_dim__ === true;
}

/**
* Check if value is a CBOR homogeneous array
*/
private isCborHomogeneousArray(value: unknown): value is CborHomogeneousArray {
return typeof value === 'object' && value !== null &&
'__cbor_homogeneous__' in value && (value as any).__cbor_homogeneous__ === true;
}

/**
* Write a multi-dimensional array using RFC 8746 tags
*/
private writeMultiDimensionalArray(value: CborMultiDimensionalArray): void {
const tag = value.rowMajor !== false ? ARRAY_TAG.MULTI_DIM_ROW_MAJOR : ARRAY_TAG.MULTI_DIM_COLUMN_MAJOR;
const data = [value.dimensions, value.elements];
this.writeTag(tag, data);
}

/**
* Write a homogeneous array using RFC 8746 tag
*/
private writeHomogeneousArray(value: CborHomogeneousArray): void {
this.writeTag(ARRAY_TAG.HOMOGENEOUS, value.elements);
}
}
Loading