Skip to content

Commit e8a68dc

Browse files
authored
Merge pull request #56 from jsonjoy-com/copilot/fix-55
Implement XDR decoder classes for RFC 4506 compliant binary decoding
2 parents c4eda46 + 5bcc10d commit e8a68dc

File tree

5 files changed

+1241
-0
lines changed

5 files changed

+1241
-0
lines changed

src/xdr/XdrDecoder.ts

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
import {Reader} from '@jsonjoy.com/buffers/lib/Reader';
2+
import type {IReader, IReaderResettable} from '@jsonjoy.com/buffers/lib';
3+
import type {BinaryJsonDecoder} from '../types';
4+
5+
/**
6+
* XDR (External Data Representation) binary decoder for basic value decoding.
7+
* Implements XDR binary decoding according to RFC 4506.
8+
*
9+
* Key XDR decoding principles:
10+
* - All data types are aligned to 4-byte boundaries
11+
* - Multi-byte quantities are transmitted in big-endian byte order
12+
* - Strings and opaque data are padded to 4-byte boundaries
13+
* - Variable-length arrays and strings are preceded by their length
14+
*/
15+
export class XdrDecoder<R extends IReader & IReaderResettable = IReader & IReaderResettable>
16+
implements BinaryJsonDecoder
17+
{
18+
public constructor(public reader: R = new Reader() as any) {}
19+
20+
public read(uint8: Uint8Array): unknown {
21+
this.reader.reset(uint8);
22+
return this.readAny();
23+
}
24+
25+
public decode(uint8: Uint8Array): unknown {
26+
this.reader.reset(uint8);
27+
return this.readAny();
28+
}
29+
30+
public readAny(): unknown {
31+
// Basic implementation - in practice this would need schema info
32+
// For now, we'll throw as this should be used with schema decoder
33+
throw new Error('XdrDecoder.readAny() requires explicit type methods or use XdrSchemaDecoder');
34+
}
35+
36+
/**
37+
* Reads an XDR void value (no data is actually read).
38+
*/
39+
public readVoid(): void {
40+
// Void values have no representation in XDR
41+
}
42+
43+
/**
44+
* Reads an XDR boolean value as a 4-byte integer.
45+
* Returns true for non-zero values, false for zero.
46+
*/
47+
public readBoolean(): boolean {
48+
return this.readInt() !== 0;
49+
}
50+
51+
/**
52+
* Reads an XDR signed 32-bit integer in big-endian format.
53+
*/
54+
public readInt(): number {
55+
const reader = this.reader;
56+
const value = reader.view.getInt32(reader.x, false); // false = big-endian
57+
reader.x += 4;
58+
return value;
59+
}
60+
61+
/**
62+
* Reads an XDR unsigned 32-bit integer in big-endian format.
63+
*/
64+
public readUnsignedInt(): number {
65+
const reader = this.reader;
66+
const value = reader.view.getUint32(reader.x, false); // false = big-endian
67+
reader.x += 4;
68+
return value;
69+
}
70+
71+
/**
72+
* Reads an XDR signed 64-bit integer (hyper) in big-endian format.
73+
*/
74+
public readHyper(): bigint {
75+
const reader = this.reader;
76+
const value = reader.view.getBigInt64(reader.x, false); // false = big-endian
77+
reader.x += 8;
78+
return value;
79+
}
80+
81+
/**
82+
* Reads an XDR unsigned 64-bit integer (unsigned hyper) in big-endian format.
83+
*/
84+
public readUnsignedHyper(): bigint {
85+
const reader = this.reader;
86+
const value = reader.view.getBigUint64(reader.x, false); // false = big-endian
87+
reader.x += 8;
88+
return value;
89+
}
90+
91+
/**
92+
* Reads an XDR float value using IEEE 754 single-precision in big-endian format.
93+
*/
94+
public readFloat(): number {
95+
const reader = this.reader;
96+
const value = reader.view.getFloat32(reader.x, false); // false = big-endian
97+
reader.x += 4;
98+
return value;
99+
}
100+
101+
/**
102+
* Reads an XDR double value using IEEE 754 double-precision in big-endian format.
103+
*/
104+
public readDouble(): number {
105+
const reader = this.reader;
106+
const value = reader.view.getFloat64(reader.x, false); // false = big-endian
107+
reader.x += 8;
108+
return value;
109+
}
110+
111+
/**
112+
* Reads an XDR quadruple value (128-bit float).
113+
* Note: JavaScript doesn't have native 128-bit float support.
114+
*/
115+
public readQuadruple(): number {
116+
throw new Error('not implemented');
117+
}
118+
119+
/**
120+
* Reads XDR opaque data with known fixed length.
121+
* Data is padded to 4-byte boundary but only the actual data is returned.
122+
*/
123+
public readOpaque(size: number): Uint8Array {
124+
const reader = this.reader;
125+
const data = new Uint8Array(size);
126+
127+
// Read actual data
128+
for (let i = 0; i < size; i++) {
129+
data[i] = reader.u8();
130+
}
131+
132+
// Skip padding bytes to reach 4-byte boundary
133+
const paddedSize = Math.ceil(size / 4) * 4;
134+
const padding = paddedSize - size;
135+
reader.skip(padding);
136+
137+
return data;
138+
}
139+
140+
/**
141+
* Reads XDR variable-length opaque data.
142+
* Length is read first, followed by data padded to 4-byte boundary.
143+
*/
144+
public readVarlenOpaque(): Uint8Array {
145+
const size = this.readUnsignedInt();
146+
return this.readOpaque(size);
147+
}
148+
149+
/**
150+
* Reads an XDR string with UTF-8 encoding.
151+
* Length is read first, followed by UTF-8 bytes padded to 4-byte boundary.
152+
*/
153+
public readString(): string {
154+
const size = this.readUnsignedInt();
155+
const reader = this.reader;
156+
157+
// Read UTF-8 bytes
158+
const utf8Bytes = new Uint8Array(size);
159+
for (let i = 0; i < size; i++) {
160+
utf8Bytes[i] = reader.u8();
161+
}
162+
163+
// Skip padding bytes to reach 4-byte boundary
164+
const paddedSize = Math.ceil(size / 4) * 4;
165+
const padding = paddedSize - size;
166+
reader.skip(padding);
167+
168+
// Decode UTF-8 to string
169+
return new TextDecoder('utf-8').decode(utf8Bytes);
170+
}
171+
172+
/**
173+
* Reads an XDR enum value as an unsigned integer.
174+
*/
175+
public readEnum(): number {
176+
return this.readInt();
177+
}
178+
179+
/**
180+
* Reads a fixed-size array of elements.
181+
* Caller must provide the decode function for each element.
182+
*/
183+
public readArray<T>(size: number, elementReader: () => T): T[] {
184+
const array: T[] = [];
185+
for (let i = 0; i < size; i++) {
186+
array.push(elementReader());
187+
}
188+
return array;
189+
}
190+
191+
/**
192+
* Reads a variable-length array of elements.
193+
* Length is read first, followed by elements.
194+
*/
195+
public readVarlenArray<T>(elementReader: () => T): T[] {
196+
const size = this.readUnsignedInt();
197+
return this.readArray(size, elementReader);
198+
}
199+
}

src/xdr/XdrSchemaDecoder.ts

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
import {Reader} from '@jsonjoy.com/buffers/lib/Reader';
2+
import {XdrDecoder} from './XdrDecoder';
3+
import {XdrUnion} from './XdrUnion';
4+
import type {IReader, IReaderResettable} from '@jsonjoy.com/buffers/lib';
5+
import type {
6+
XdrSchema,
7+
XdrPrimitiveSchema,
8+
XdrWidePrimitiveSchema,
9+
XdrCompositeSchema,
10+
XdrEnumSchema,
11+
XdrOpaqueSchema,
12+
XdrVarlenOpaqueSchema,
13+
XdrStringSchema,
14+
XdrArraySchema,
15+
XdrVarlenArraySchema,
16+
XdrStructSchema,
17+
XdrUnionSchema,
18+
} from './types';
19+
20+
/**
21+
* XDR (External Data Representation) schema-aware decoder.
22+
* Decodes values according to provided XDR schemas with proper validation.
23+
* Based on RFC 4506 specification.
24+
*/
25+
export class XdrSchemaDecoder {
26+
private decoder: XdrDecoder;
27+
28+
constructor(public readonly reader: IReader & IReaderResettable = new Reader()) {
29+
this.decoder = new XdrDecoder(reader);
30+
}
31+
32+
/**
33+
* Decodes a value according to the provided schema.
34+
*/
35+
public decode(data: Uint8Array, schema: XdrSchema): unknown {
36+
this.reader.reset(data);
37+
return this.readValue(schema);
38+
}
39+
40+
/**
41+
* Reads a value according to its schema.
42+
*/
43+
private readValue(schema: XdrSchema): unknown {
44+
switch (schema.type) {
45+
// Primitive types
46+
case 'void':
47+
return this.decoder.readVoid();
48+
case 'int':
49+
return this.decoder.readInt();
50+
case 'unsigned_int':
51+
return this.decoder.readUnsignedInt();
52+
case 'boolean':
53+
return this.decoder.readBoolean();
54+
case 'hyper':
55+
return this.decoder.readHyper();
56+
case 'unsigned_hyper':
57+
return this.decoder.readUnsignedHyper();
58+
case 'float':
59+
return this.decoder.readFloat();
60+
case 'double':
61+
return this.decoder.readDouble();
62+
case 'quadruple':
63+
return this.decoder.readQuadruple();
64+
case 'enum':
65+
return this.readEnum(schema as XdrEnumSchema);
66+
67+
// Wide primitive types
68+
case 'opaque':
69+
return this.readOpaque(schema as XdrOpaqueSchema);
70+
case 'vopaque':
71+
return this.readVarlenOpaque(schema as XdrVarlenOpaqueSchema);
72+
case 'string':
73+
return this.readString(schema as XdrStringSchema);
74+
75+
// Composite types
76+
case 'array':
77+
return this.readArray(schema as XdrArraySchema);
78+
case 'varray':
79+
return this.readVarlenArray(schema as XdrVarlenArraySchema);
80+
case 'struct':
81+
return this.readStruct(schema as XdrStructSchema);
82+
case 'union':
83+
return this.readUnion(schema as XdrUnionSchema);
84+
85+
default:
86+
throw new Error(`Unknown schema type: ${(schema as any).type}`);
87+
}
88+
}
89+
90+
/**
91+
* Reads an enum value according to the enum schema.
92+
*/
93+
private readEnum(schema: XdrEnumSchema): string | number {
94+
const value = this.decoder.readEnum();
95+
96+
// Find the enum name for this value
97+
for (const [name, enumValue] of Object.entries(schema.values)) {
98+
if (enumValue === value) {
99+
return name;
100+
}
101+
}
102+
103+
// If no matching name found, return the numeric value
104+
return value;
105+
}
106+
107+
/**
108+
* Reads opaque data according to the opaque schema.
109+
*/
110+
private readOpaque(schema: XdrOpaqueSchema): Uint8Array {
111+
return this.decoder.readOpaque(schema.size);
112+
}
113+
114+
/**
115+
* Reads variable-length opaque data according to the schema.
116+
*/
117+
private readVarlenOpaque(schema: XdrVarlenOpaqueSchema): Uint8Array {
118+
const data = this.decoder.readVarlenOpaque();
119+
120+
// Check size constraint if specified
121+
if (schema.size !== undefined && data.length > schema.size) {
122+
throw new Error(`Variable-length opaque data size ${data.length} exceeds maximum ${schema.size}`);
123+
}
124+
125+
return data;
126+
}
127+
128+
/**
129+
* Reads a string according to the string schema.
130+
*/
131+
private readString(schema: XdrStringSchema): string {
132+
const str = this.decoder.readString();
133+
134+
// Check size constraint if specified
135+
if (schema.size !== undefined && str.length > schema.size) {
136+
throw new Error(`String length ${str.length} exceeds maximum ${schema.size}`);
137+
}
138+
139+
return str;
140+
}
141+
142+
/**
143+
* Reads a fixed-size array according to the array schema.
144+
*/
145+
private readArray(schema: XdrArraySchema): unknown[] {
146+
return this.decoder.readArray(schema.size, () => this.readValue(schema.elements));
147+
}
148+
149+
/**
150+
* Reads a variable-length array according to the schema.
151+
*/
152+
private readVarlenArray(schema: XdrVarlenArraySchema): unknown[] {
153+
const array = this.decoder.readVarlenArray(() => this.readValue(schema.elements));
154+
155+
// Check size constraint if specified
156+
if (schema.size !== undefined && array.length > schema.size) {
157+
throw new Error(`Variable-length array size ${array.length} exceeds maximum ${schema.size}`);
158+
}
159+
160+
return array;
161+
}
162+
163+
/**
164+
* Reads a struct according to the struct schema.
165+
*/
166+
private readStruct(schema: XdrStructSchema): Record<string, unknown> {
167+
const struct: Record<string, unknown> = {};
168+
169+
for (const [fieldSchema, fieldName] of schema.fields) {
170+
struct[fieldName] = this.readValue(fieldSchema);
171+
}
172+
173+
return struct;
174+
}
175+
176+
/**
177+
* Reads a union according to the union schema.
178+
*/
179+
private readUnion(schema: XdrUnionSchema): XdrUnion {
180+
// Read discriminant
181+
const discriminant = this.decoder.readInt();
182+
183+
// Find matching arm
184+
for (const [armDiscriminant, armSchema] of schema.arms) {
185+
if (armDiscriminant === discriminant) {
186+
const value = this.readValue(armSchema);
187+
return new XdrUnion(discriminant, value);
188+
}
189+
}
190+
191+
// If no matching arm found, try default
192+
if (schema.default) {
193+
const value = this.readValue(schema.default);
194+
return new XdrUnion(discriminant, value);
195+
}
196+
197+
throw new Error(`No matching union arm for discriminant: ${discriminant}`);
198+
}
199+
}

0 commit comments

Comments
 (0)