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 Object :
44+ return this . writeObj ( value as Record < string , unknown > ) ;
45+ case Array :
46+ return this . writeArr ( value as unknown [ ] ) ;
47+ case Uint8Array :
48+ return this . writeBin ( value as Uint8Array ) ;
49+ default :
50+ return this . writeUnknown ( value ) ;
51+ }
52+ }
53+ case 'bigint' :
54+ return this . writeHyper ( value ) ;
55+ case 'undefined' :
56+ return this . writeVoid ( ) ;
57+ default :
58+ return this . writeUnknown ( value ) ;
59+ }
60+ }
61+
62+ /**
63+ * Writes an XDR void value (no data is actually written).
64+ */
65+ public writeVoid ( ) : void {
66+ // Void values are encoded as no data
67+ }
68+
69+ /**
70+ * Writes an XDR null value (for interface compatibility).
71+ */
72+ public writeNull ( ) : void {
73+ this . writeVoid ( ) ;
74+ }
75+
76+ /**
77+ * Writes an XDR boolean value as a 4-byte integer.
78+ */
79+ public writeBoolean ( bool : boolean ) : void {
80+ this . writeInt ( bool ? 1 : 0 ) ;
81+ }
82+
83+ /**
84+ * Writes an XDR signed 32-bit integer in big-endian format.
85+ */
86+ public writeInt ( int : number ) : void {
87+ const writer = this . writer ;
88+ writer . ensureCapacity ( 4 ) ;
89+ writer . view . setInt32 ( writer . x , Math . trunc ( int ) , false ) ; // big-endian
90+ writer . move ( 4 ) ;
91+ }
92+
93+ /**
94+ * Writes an XDR unsigned 32-bit integer in big-endian format.
95+ */
96+ public writeUnsignedInt ( uint : number ) : void {
97+ const writer = this . writer ;
98+ writer . ensureCapacity ( 4 ) ;
99+ writer . view . setUint32 ( writer . x , Math . trunc ( uint ) >>> 0 , false ) ; // big-endian
100+ writer . move ( 4 ) ;
101+ }
102+
103+ /**
104+ * Writes an XDR signed 64-bit integer (hyper) in big-endian format.
105+ */
106+ public writeHyper ( hyper : number | bigint ) : void {
107+ const writer = this . writer ;
108+ writer . ensureCapacity ( 8 ) ;
109+
110+ if ( typeof hyper === 'bigint' ) {
111+ // Convert bigint to two 32-bit values for big-endian encoding
112+ const high = Number ( ( hyper >> BigInt ( 32 ) ) & BigInt ( 0xFFFFFFFF ) ) ;
113+ const low = Number ( hyper & BigInt ( 0xFFFFFFFF ) ) ;
114+ writer . view . setInt32 ( writer . x , high , false ) ; // high 32 bits
115+ writer . view . setUint32 ( writer . x + 4 , low , false ) ; // low 32 bits
116+ } else {
117+ const truncated = Math . trunc ( hyper ) ;
118+ const high = Math . floor ( truncated / 0x100000000 ) ;
119+ const low = truncated >>> 0 ;
120+ writer . view . setInt32 ( writer . x , high , false ) ; // high 32 bits
121+ writer . view . setUint32 ( writer . x + 4 , low , false ) ; // low 32 bits
122+ }
123+ writer . move ( 8 ) ;
124+ }
125+
126+ /**
127+ * Writes an XDR unsigned 64-bit integer (unsigned hyper) in big-endian format.
128+ */
129+ public writeUnsignedHyper ( uhyper : number | bigint ) : void {
130+ const writer = this . writer ;
131+ writer . ensureCapacity ( 8 ) ;
132+
133+ if ( typeof uhyper === 'bigint' ) {
134+ // Convert bigint to two 32-bit values for big-endian encoding
135+ const high = Number ( ( uhyper >> BigInt ( 32 ) ) & BigInt ( 0xFFFFFFFF ) ) ;
136+ const low = Number ( uhyper & BigInt ( 0xFFFFFFFF ) ) ;
137+ writer . view . setUint32 ( writer . x , high , false ) ; // high 32 bits
138+ writer . view . setUint32 ( writer . x + 4 , low , false ) ; // low 32 bits
139+ } else {
140+ const truncated = Math . trunc ( Math . abs ( uhyper ) ) ;
141+ const high = Math . floor ( truncated / 0x100000000 ) ;
142+ const low = truncated >>> 0 ;
143+ writer . view . setUint32 ( writer . x , high , false ) ; // high 32 bits
144+ writer . view . setUint32 ( writer . x + 4 , low , false ) ; // low 32 bits
145+ }
146+ writer . move ( 8 ) ;
147+ }
148+
149+ /**
150+ * Writes an XDR float value using IEEE 754 single-precision in big-endian format.
151+ */
152+ public writeFloat ( float : number ) : void {
153+ const writer = this . writer ;
154+ writer . ensureCapacity ( 4 ) ;
155+ writer . view . setFloat32 ( writer . x , float , false ) ; // big-endian
156+ writer . move ( 4 ) ;
157+ }
158+
159+ /**
160+ * Writes an XDR double value using IEEE 754 double-precision in big-endian format.
161+ */
162+ public writeDouble ( double : number ) : void {
163+ const writer = this . writer ;
164+ writer . ensureCapacity ( 8 ) ;
165+ writer . view . setFloat64 ( writer . x , double , false ) ; // big-endian
166+ writer . move ( 8 ) ;
167+ }
168+
169+ /**
170+ * Writes an XDR quadruple value (128-bit float).
171+ * Note: JavaScript doesn't have native 128-bit float support, so this is a placeholder.
172+ */
173+ public writeQuadruple ( quad : number ) : void {
174+ // Write as two doubles for now (this is not standard XDR)
175+ this . writeDouble ( quad ) ;
176+ this . writeDouble ( 0 ) ; // padding
177+ }
178+
179+ /**
180+ * Writes XDR opaque data with fixed length.
181+ * Data is padded to 4-byte boundary.
182+ */
183+ public writeOpaque ( data : Uint8Array , size : number ) : void {
184+ if ( data . length !== size ) {
185+ throw new Error ( `Opaque data length ${ data . length } does not match expected size ${ size } ` ) ;
186+ }
187+
188+ const writer = this . writer ;
189+ const paddedSize = this . getPaddedSize ( size ) ;
190+ writer . ensureCapacity ( paddedSize ) ;
191+
192+ // Write data
193+ writer . buf ( data , size ) ;
194+
195+ // Write padding bytes
196+ const padding = paddedSize - size ;
197+ for ( let i = 0 ; i < padding ; i ++ ) {
198+ writer . u8 ( 0 ) ;
199+ }
200+ }
201+
202+ /**
203+ * Writes XDR variable-length opaque data.
204+ * Length is written first, followed by data padded to 4-byte boundary.
205+ */
206+ public writeVarlenOpaque ( data : Uint8Array ) : void {
207+ this . writeUnsignedInt ( data . length ) ;
208+
209+ const writer = this . writer ;
210+ const paddedSize = this . getPaddedSize ( data . length ) ;
211+ writer . ensureCapacity ( paddedSize ) ;
212+
213+ // Write data
214+ writer . buf ( data , data . length ) ;
215+
216+ // Write padding bytes
217+ const padding = paddedSize - data . length ;
218+ for ( let i = 0 ; i < padding ; i ++ ) {
219+ writer . u8 ( 0 ) ;
220+ }
221+ }
222+
223+ /**
224+ * Writes an XDR string with UTF-8 encoding.
225+ * Length is written first, followed by UTF-8 bytes padded to 4-byte boundary.
226+ */
227+ public writeStr ( str : string ) : void {
228+ const writer = this . writer ;
229+ const encoder = new TextEncoder ( ) ;
230+ const utf8Bytes = encoder . encode ( str ) ;
231+
232+ // Write length
233+ this . writeUnsignedInt ( utf8Bytes . length ) ;
234+
235+ // Write string data with padding
236+ const paddedSize = this . getPaddedSize ( utf8Bytes . length ) ;
237+ writer . ensureCapacity ( paddedSize ) ;
238+
239+ // Write UTF-8 bytes
240+ writer . buf ( utf8Bytes , utf8Bytes . length ) ;
241+
242+ // Write padding bytes
243+ const padding = paddedSize - utf8Bytes . length ;
244+ for ( let i = 0 ; i < padding ; i ++ ) {
245+ writer . u8 ( 0 ) ;
246+ }
247+ }
248+
249+ /**
250+ * Writes XDR variable-length array.
251+ * Length is written first, followed by array elements.
252+ */
253+ public writeArr ( arr : unknown [ ] ) : void {
254+ this . writeUnsignedInt ( arr . length ) ;
255+ for ( const item of arr ) {
256+ this . writeAny ( item ) ;
257+ }
258+ }
259+
260+ /**
261+ * Writes XDR structure as a simple mapping (not standard XDR, for compatibility).
262+ */
263+ public writeObj ( obj : Record < string , unknown > ) : void {
264+ const entries = Object . entries ( obj ) ;
265+ this . writeUnsignedInt ( entries . length ) ;
266+ for ( const [ key , value ] of entries ) {
267+ this . writeStr ( key ) ;
268+ this . writeAny ( value ) ;
269+ }
270+ }
271+
272+ // BinaryJsonEncoder interface methods
273+
274+ /**
275+ * Generic number writing - determines type based on value
276+ */
277+ public writeNumber ( num : number ) : void {
278+ if ( Number . isInteger ( num ) ) {
279+ if ( num >= - 2147483648 && num <= 2147483647 ) {
280+ this . writeInt ( num ) ;
281+ } else {
282+ this . writeHyper ( num ) ;
283+ }
284+ } else {
285+ this . writeDouble ( num ) ;
286+ }
287+ }
288+
289+ /**
290+ * Writes an integer value
291+ */
292+ public writeInteger ( int : number ) : void {
293+ this . writeInt ( int ) ;
294+ }
295+
296+ /**
297+ * Writes an unsigned integer value
298+ */
299+ public writeUInteger ( uint : number ) : void {
300+ this . writeUnsignedInt ( uint ) ;
301+ }
302+
303+ /**
304+ * Writes binary data
305+ */
306+ public writeBin ( buf : Uint8Array ) : void {
307+ this . writeVarlenOpaque ( buf ) ;
308+ }
309+
310+ /**
311+ * Writes an ASCII string (same as regular string in XDR)
312+ */
313+ public writeAsciiStr ( str : string ) : void {
314+ this . writeStr ( str ) ;
315+ }
316+
317+ /**
318+ * Calculates the padded size for 4-byte alignment.
319+ */
320+ private getPaddedSize ( size : number ) : number {
321+ return Math . ceil ( size / 4 ) * 4 ;
322+ }
323+ }
0 commit comments