Skip to content
Merged
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
199 changes: 199 additions & 0 deletions src/xdr/XdrDecoder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import {Reader} from '@jsonjoy.com/buffers/lib/Reader';
import type {IReader, IReaderResettable} from '@jsonjoy.com/buffers/lib';
import type {BinaryJsonDecoder} from '../types';

/**
* XDR (External Data Representation) binary decoder for basic value decoding.
* Implements XDR binary decoding according to RFC 4506.
*
* Key XDR decoding principles:
* - All data types are aligned to 4-byte boundaries
* - Multi-byte quantities are transmitted in big-endian byte order
* - Strings and opaque data are padded to 4-byte boundaries
* - Variable-length arrays and strings are preceded by their length
*/
export class XdrDecoder<R extends IReader & IReaderResettable = IReader & IReaderResettable>
implements BinaryJsonDecoder
{
public constructor(public reader: R = new Reader() as any) {}

public read(uint8: Uint8Array): unknown {
this.reader.reset(uint8);
return this.readAny();
}

public decode(uint8: Uint8Array): unknown {
this.reader.reset(uint8);
return this.readAny();
}

public readAny(): unknown {
// Basic implementation - in practice this would need schema info
// For now, we'll throw as this should be used with schema decoder
throw new Error('XdrDecoder.readAny() requires explicit type methods or use XdrSchemaDecoder');
}

/**
* Reads an XDR void value (no data is actually read).
*/
public readVoid(): void {
// Void values have no representation in XDR
}

/**
* Reads an XDR boolean value as a 4-byte integer.
* Returns true for non-zero values, false for zero.
*/
public readBoolean(): boolean {
return this.readInt() !== 0;
}

/**
* Reads an XDR signed 32-bit integer in big-endian format.
*/
public readInt(): number {
const reader = this.reader;
const value = reader.view.getInt32(reader.x, false); // false = big-endian
reader.x += 4;
return value;
}

/**
* Reads an XDR unsigned 32-bit integer in big-endian format.
*/
public readUnsignedInt(): number {
const reader = this.reader;
const value = reader.view.getUint32(reader.x, false); // false = big-endian
reader.x += 4;
return value;
}

/**
* Reads an XDR signed 64-bit integer (hyper) in big-endian format.
*/
public readHyper(): bigint {
const reader = this.reader;
const value = reader.view.getBigInt64(reader.x, false); // false = big-endian
reader.x += 8;
return value;
}

/**
* Reads an XDR unsigned 64-bit integer (unsigned hyper) in big-endian format.
*/
public readUnsignedHyper(): bigint {
const reader = this.reader;
const value = reader.view.getBigUint64(reader.x, false); // false = big-endian
reader.x += 8;
return value;
}

/**
* Reads an XDR float value using IEEE 754 single-precision in big-endian format.
*/
public readFloat(): number {
const reader = this.reader;
const value = reader.view.getFloat32(reader.x, false); // false = big-endian
reader.x += 4;
return value;
}

/**
* Reads an XDR double value using IEEE 754 double-precision in big-endian format.
*/
public readDouble(): number {
const reader = this.reader;
const value = reader.view.getFloat64(reader.x, false); // false = big-endian
reader.x += 8;
return value;
}

/**
* Reads an XDR quadruple value (128-bit float).
* Note: JavaScript doesn't have native 128-bit float support.
*/
public readQuadruple(): number {
throw new Error('not implemented');
}

/**
* Reads XDR opaque data with known fixed length.
* Data is padded to 4-byte boundary but only the actual data is returned.
*/
public readOpaque(size: number): Uint8Array {
const reader = this.reader;
const data = new Uint8Array(size);

// Read actual data
for (let i = 0; i < size; i++) {
data[i] = reader.u8();
}

// Skip padding bytes to reach 4-byte boundary
const paddedSize = Math.ceil(size / 4) * 4;
const padding = paddedSize - size;
reader.skip(padding);

return data;
}

/**
* Reads XDR variable-length opaque data.
* Length is read first, followed by data padded to 4-byte boundary.
*/
public readVarlenOpaque(): Uint8Array {
const size = this.readUnsignedInt();
return this.readOpaque(size);
}

/**
* Reads an XDR string with UTF-8 encoding.
* Length is read first, followed by UTF-8 bytes padded to 4-byte boundary.
*/
public readString(): string {
const size = this.readUnsignedInt();
const reader = this.reader;

// Read UTF-8 bytes
const utf8Bytes = new Uint8Array(size);
for (let i = 0; i < size; i++) {
utf8Bytes[i] = reader.u8();
}

// Skip padding bytes to reach 4-byte boundary
const paddedSize = Math.ceil(size / 4) * 4;
const padding = paddedSize - size;
reader.skip(padding);

// Decode UTF-8 to string
return new TextDecoder('utf-8').decode(utf8Bytes);
}

/**
* Reads an XDR enum value as an unsigned integer.
*/
public readEnum(): number {
return this.readInt();
}

/**
* Reads a fixed-size array of elements.
* Caller must provide the decode function for each element.
*/
public readArray<T>(size: number, elementReader: () => T): T[] {
const array: T[] = [];
for (let i = 0; i < size; i++) {
array.push(elementReader());
}
return array;
}

/**
* Reads a variable-length array of elements.
* Length is read first, followed by elements.
*/
public readVarlenArray<T>(elementReader: () => T): T[] {
const size = this.readUnsignedInt();
return this.readArray(size, elementReader);
}
}
199 changes: 199 additions & 0 deletions src/xdr/XdrSchemaDecoder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import {Reader} from '@jsonjoy.com/buffers/lib/Reader';
import {XdrDecoder} from './XdrDecoder';
import {XdrUnion} from './XdrUnion';
import type {IReader, IReaderResettable} from '@jsonjoy.com/buffers/lib';
import type {
XdrSchema,
XdrPrimitiveSchema,
XdrWidePrimitiveSchema,
XdrCompositeSchema,
XdrEnumSchema,
XdrOpaqueSchema,
XdrVarlenOpaqueSchema,
XdrStringSchema,
XdrArraySchema,
XdrVarlenArraySchema,
XdrStructSchema,
XdrUnionSchema,
} from './types';

/**
* XDR (External Data Representation) schema-aware decoder.
* Decodes values according to provided XDR schemas with proper validation.
* Based on RFC 4506 specification.
*/
export class XdrSchemaDecoder {
private decoder: XdrDecoder;

constructor(public readonly reader: IReader & IReaderResettable = new Reader()) {
this.decoder = new XdrDecoder(reader);
}

/**
* Decodes a value according to the provided schema.
*/
public decode(data: Uint8Array, schema: XdrSchema): unknown {
this.reader.reset(data);
return this.readValue(schema);
}

/**
* Reads a value according to its schema.
*/
private readValue(schema: XdrSchema): unknown {
switch (schema.type) {
// Primitive types
case 'void':
return this.decoder.readVoid();
case 'int':
return this.decoder.readInt();
case 'unsigned_int':
return this.decoder.readUnsignedInt();
case 'boolean':
return this.decoder.readBoolean();
case 'hyper':
return this.decoder.readHyper();
case 'unsigned_hyper':
return this.decoder.readUnsignedHyper();
case 'float':
return this.decoder.readFloat();
case 'double':
return this.decoder.readDouble();
case 'quadruple':
return this.decoder.readQuadruple();
case 'enum':
return this.readEnum(schema as XdrEnumSchema);

// Wide primitive types
case 'opaque':
return this.readOpaque(schema as XdrOpaqueSchema);
case 'vopaque':
return this.readVarlenOpaque(schema as XdrVarlenOpaqueSchema);
case 'string':
return this.readString(schema as XdrStringSchema);

// Composite types
case 'array':
return this.readArray(schema as XdrArraySchema);
case 'varray':
return this.readVarlenArray(schema as XdrVarlenArraySchema);
case 'struct':
return this.readStruct(schema as XdrStructSchema);
case 'union':
return this.readUnion(schema as XdrUnionSchema);

default:
throw new Error(`Unknown schema type: ${(schema as any).type}`);
}
}

/**
* Reads an enum value according to the enum schema.
*/
private readEnum(schema: XdrEnumSchema): string | number {
const value = this.decoder.readEnum();

// Find the enum name for this value
for (const [name, enumValue] of Object.entries(schema.values)) {
if (enumValue === value) {
return name;
}
}

// If no matching name found, return the numeric value
return value;
}

/**
* Reads opaque data according to the opaque schema.
*/
private readOpaque(schema: XdrOpaqueSchema): Uint8Array {
return this.decoder.readOpaque(schema.size);
}

/**
* Reads variable-length opaque data according to the schema.
*/
private readVarlenOpaque(schema: XdrVarlenOpaqueSchema): Uint8Array {
const data = this.decoder.readVarlenOpaque();

// Check size constraint if specified
if (schema.size !== undefined && data.length > schema.size) {
throw new Error(`Variable-length opaque data size ${data.length} exceeds maximum ${schema.size}`);
}

return data;
}

/**
* Reads a string according to the string schema.
*/
private readString(schema: XdrStringSchema): string {
const str = this.decoder.readString();

// Check size constraint if specified
if (schema.size !== undefined && str.length > schema.size) {
throw new Error(`String length ${str.length} exceeds maximum ${schema.size}`);
}

return str;
}

/**
* Reads a fixed-size array according to the array schema.
*/
private readArray(schema: XdrArraySchema): unknown[] {
return this.decoder.readArray(schema.size, () => this.readValue(schema.elements));
}

/**
* Reads a variable-length array according to the schema.
*/
private readVarlenArray(schema: XdrVarlenArraySchema): unknown[] {
const array = this.decoder.readVarlenArray(() => this.readValue(schema.elements));

// Check size constraint if specified
if (schema.size !== undefined && array.length > schema.size) {
throw new Error(`Variable-length array size ${array.length} exceeds maximum ${schema.size}`);
}

return array;
}

/**
* Reads a struct according to the struct schema.
*/
private readStruct(schema: XdrStructSchema): Record<string, unknown> {
const struct: Record<string, unknown> = {};

for (const [fieldSchema, fieldName] of schema.fields) {
struct[fieldName] = this.readValue(fieldSchema);
}

return struct;
}

/**
* Reads a union according to the union schema.
*/
private readUnion(schema: XdrUnionSchema): XdrUnion {
// Read discriminant
const discriminant = this.decoder.readInt();

// Find matching arm
for (const [armDiscriminant, armSchema] of schema.arms) {
if (armDiscriminant === discriminant) {
const value = this.readValue(armSchema);
return new XdrUnion(discriminant, value);
}
}

// If no matching arm found, try default
if (schema.default) {
const value = this.readValue(schema.default);
return new XdrUnion(discriminant, value);
}

throw new Error(`No matching union arm for discriminant: ${discriminant}`);
}
}
Loading