Skip to content

Commit c4eda46

Browse files
authored
Merge pull request #54 from jsonjoy-com/copilot/fix-53
Implement XDR encoder with separated schema validation and comprehensive tests
2 parents bb24f85 + d10904e commit c4eda46

File tree

8 files changed

+2613
-2
lines changed

8 files changed

+2613
-2
lines changed

src/xdr/XdrEncoder.ts

Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
import type {IWriter, IWriterGrowable} from '@jsonjoy.com/buffers/lib';
2+
import type {BinaryJsonEncoder} from '../types';
3+
4+
/**
5+
* XDR (External Data Representation) binary encoder for basic value encoding.
6+
* Implements XDR binary encoding according to RFC 4506.
7+
*
8+
* Key XDR encoding principles:
9+
* - All data types are aligned to 4-byte boundaries
10+
* - Multi-byte quantities are transmitted in big-endian byte order
11+
* - Strings and opaque data are padded to 4-byte boundaries
12+
* - Variable-length arrays and strings are preceded by their length
13+
*/
14+
export class XdrEncoder implements BinaryJsonEncoder {
15+
constructor(public readonly writer: IWriter & IWriterGrowable) {}
16+
17+
public encode(value: unknown): Uint8Array {
18+
const writer = this.writer;
19+
writer.reset();
20+
this.writeAny(value);
21+
return writer.flush();
22+
}
23+
24+
/**
25+
* Called when the encoder encounters a value that it does not know how to encode.
26+
*/
27+
public writeUnknown(value: unknown): void {
28+
this.writeVoid();
29+
}
30+
31+
public writeAny(value: unknown): void {
32+
switch (typeof value) {
33+
case 'boolean':
34+
return this.writeBoolean(value);
35+
case 'number':
36+
return this.writeNumber(value);
37+
case 'string':
38+
return this.writeStr(value);
39+
case 'object': {
40+
if (value === null) return this.writeVoid();
41+
const constructor = value.constructor;
42+
switch (constructor) {
43+
case Uint8Array:
44+
return this.writeBin(value as Uint8Array);
45+
default:
46+
return this.writeUnknown(value);
47+
}
48+
}
49+
case 'bigint':
50+
return this.writeHyper(value);
51+
case 'undefined':
52+
return this.writeVoid();
53+
default:
54+
return this.writeUnknown(value);
55+
}
56+
}
57+
58+
/**
59+
* Writes an XDR void value (no data is actually written).
60+
*/
61+
public writeVoid(): void {
62+
// Void values are encoded as no data
63+
}
64+
65+
/**
66+
* Writes an XDR null value (for interface compatibility).
67+
*/
68+
public writeNull(): void {
69+
this.writeVoid();
70+
}
71+
72+
/**
73+
* Writes an XDR boolean value as a 4-byte integer.
74+
*/
75+
public writeBoolean(bool: boolean): void {
76+
this.writeInt(bool ? 1 : 0);
77+
}
78+
79+
/**
80+
* Writes an XDR signed 32-bit integer in big-endian format.
81+
*/
82+
public writeInt(int: number): void {
83+
const writer = this.writer;
84+
writer.ensureCapacity(4);
85+
writer.view.setInt32(writer.x, Math.trunc(int), false); // big-endian
86+
writer.move(4);
87+
}
88+
89+
/**
90+
* Writes an XDR unsigned 32-bit integer in big-endian format.
91+
*/
92+
public writeUnsignedInt(uint: number): void {
93+
const writer = this.writer;
94+
writer.ensureCapacity(4);
95+
writer.view.setUint32(writer.x, Math.trunc(uint) >>> 0, false); // big-endian
96+
writer.move(4);
97+
}
98+
99+
/**
100+
* Writes an XDR signed 64-bit integer (hyper) in big-endian format.
101+
*/
102+
public writeHyper(hyper: number | bigint): void {
103+
const writer = this.writer;
104+
writer.ensureCapacity(8);
105+
106+
if (typeof hyper === 'bigint') {
107+
writer.view.setBigInt64(writer.x, hyper, false); // big-endian
108+
} else {
109+
const truncated = Math.trunc(hyper);
110+
const high = Math.floor(truncated / 0x100000000);
111+
const low = truncated >>> 0;
112+
writer.view.setInt32(writer.x, high, false); // high 32 bits
113+
writer.view.setUint32(writer.x + 4, low, false); // low 32 bits
114+
}
115+
writer.move(8);
116+
}
117+
118+
/**
119+
* Writes an XDR unsigned 64-bit integer (unsigned hyper) in big-endian format.
120+
*/
121+
public writeUnsignedHyper(uhyper: number | bigint): void {
122+
const writer = this.writer;
123+
writer.ensureCapacity(8);
124+
125+
if (typeof uhyper === 'bigint') {
126+
writer.view.setBigUint64(writer.x, uhyper, false); // big-endian
127+
} else {
128+
const truncated = Math.trunc(Math.abs(uhyper));
129+
const high = Math.floor(truncated / 0x100000000);
130+
const low = truncated >>> 0;
131+
writer.view.setUint32(writer.x, high, false); // high 32 bits
132+
writer.view.setUint32(writer.x + 4, low, false); // low 32 bits
133+
}
134+
writer.move(8);
135+
}
136+
137+
/**
138+
* Writes an XDR float value using IEEE 754 single-precision in big-endian format.
139+
*/
140+
public writeFloat(float: number): void {
141+
const writer = this.writer;
142+
writer.ensureCapacity(4);
143+
writer.view.setFloat32(writer.x, float, false); // big-endian
144+
writer.move(4);
145+
}
146+
147+
/**
148+
* Writes an XDR double value using IEEE 754 double-precision in big-endian format.
149+
*/
150+
public writeDouble(double: number): void {
151+
const writer = this.writer;
152+
writer.ensureCapacity(8);
153+
writer.view.setFloat64(writer.x, double, false); // big-endian
154+
writer.move(8);
155+
}
156+
157+
/**
158+
* Writes an XDR quadruple value (128-bit float).
159+
* Note: JavaScript doesn't have native 128-bit float support.
160+
*/
161+
public writeQuadruple(quad: number): void {
162+
throw new Error('not implemented');
163+
}
164+
165+
/**
166+
* Writes XDR opaque data with fixed length.
167+
* Data is padded to 4-byte boundary.
168+
*/
169+
public writeOpaque(data: Uint8Array): void {
170+
const size = data.length;
171+
const writer = this.writer;
172+
const paddedSize = Math.ceil(size / 4) * 4;
173+
writer.ensureCapacity(paddedSize);
174+
175+
// Write data
176+
writer.buf(data, size);
177+
178+
// Write padding bytes
179+
const padding = paddedSize - size;
180+
for (let i = 0; i < padding; i++) {
181+
writer.u8(0);
182+
}
183+
}
184+
185+
/**
186+
* Writes XDR variable-length opaque data.
187+
* Length is written first, followed by data padded to 4-byte boundary.
188+
*/
189+
public writeVarlenOpaque(data: Uint8Array): void {
190+
this.writeUnsignedInt(data.length);
191+
this.writeOpaque(data);
192+
}
193+
194+
/**
195+
* Writes an XDR string with UTF-8 encoding.
196+
* Length is written first, followed by UTF-8 bytes padded to 4-byte boundary.
197+
*/
198+
public writeStr(str: string): void {
199+
const writer = this.writer;
200+
201+
// Write string using writer's UTF-8 method and get actual byte count
202+
const lengthOffset = writer.x;
203+
writer.x += 4; // Reserve space for length
204+
const bytesWritten = writer.utf8(str);
205+
206+
// Calculate and write padding
207+
const paddedSize = Math.ceil(bytesWritten / 4) * 4;
208+
const padding = paddedSize - bytesWritten;
209+
for (let i = 0; i < padding; i++) {
210+
writer.u8(0);
211+
}
212+
213+
// Go back and write the actual byte length
214+
const currentPos = writer.x;
215+
writer.x = lengthOffset;
216+
this.writeUnsignedInt(bytesWritten);
217+
writer.x = currentPos;
218+
}
219+
220+
public writeArr(arr: unknown[]): void {
221+
throw new Error('not implemented');
222+
}
223+
224+
public writeObj(obj: Record<string, unknown>): void {
225+
throw new Error('not implemented');
226+
}
227+
228+
// BinaryJsonEncoder interface methods
229+
230+
/**
231+
* Generic number writing - determines type based on value
232+
*/
233+
public writeNumber(num: number): void {
234+
if (Number.isInteger(num)) {
235+
if (num >= -2147483648 && num <= 2147483647) {
236+
this.writeInt(num);
237+
} else {
238+
this.writeHyper(num);
239+
}
240+
} else {
241+
this.writeDouble(num);
242+
}
243+
}
244+
245+
/**
246+
* Writes an integer value
247+
*/
248+
public writeInteger(int: number): void {
249+
this.writeInt(int);
250+
}
251+
252+
/**
253+
* Writes an unsigned integer value
254+
*/
255+
public writeUInteger(uint: number): void {
256+
this.writeUnsignedInt(uint);
257+
}
258+
259+
/**
260+
* Writes binary data
261+
*/
262+
public writeBin(buf: Uint8Array): void {
263+
this.writeVarlenOpaque(buf);
264+
}
265+
266+
/**
267+
* Writes an ASCII string (same as regular string in XDR)
268+
*/
269+
public writeAsciiStr(str: string): void {
270+
this.writeStr(str);
271+
}
272+
}

0 commit comments

Comments
 (0)