1import { ByteBuffer } from "./byte-buffer.js"
2import { SIZEOF_SHORT, SIZE_PREFIX_LENGTH, SIZEOF_INT, FILE_IDENTIFIER_LENGTH } from "./constants.js"
3import { Offset, IGeneratedObject } from "./types.js"
4
5export class Builder {
6 private bb: ByteBuffer
7 /** Remaining space in the ByteBuffer. */
8 private space: number
9 /** Minimum alignment encountered so far. */
10 private minalign = 1
11 /** The vtable for the current table. */
12 private vtable: number[] | null = null
13 /** The amount of fields we're actually using. */
14 private vtable_in_use = 0
15 /** Whether we are currently serializing a table. */
16 private isNested = false;
17 /** Starting offset of the current struct/table. */
18 private object_start = 0
19 /** List of offsets of all vtables. */
20 private vtables: number[] = []
21 /** For the current vector being built. */
22 private vector_num_elems = 0
23 /** False omits default values from the serialized data */
24 private force_defaults = false;
25
26 private string_maps: Map<string | Uint8Array, number> | null = null;
27 private text_encoder = new TextEncoder();
28
29 /**
30 * Create a FlatBufferBuilder.
31 */
32 constructor(opt_initial_size?: number) {
33 let initial_size: number;
34
35 if (!opt_initial_size) {
36 initial_size = 1024;
37 } else {
38 initial_size = opt_initial_size;
39 }
40
41 /**
42 * @type {ByteBuffer}
43 * @private
44 */
45 this.bb = ByteBuffer.allocate(initial_size);
46 this.space = initial_size;
47 }
48
49
50 clear(): void {
51 this.bb.clear();
52 this.space = this.bb.capacity();
53 this.minalign = 1;
54 this.vtable = null;
55 this.vtable_in_use = 0;
56 this.isNested = false;
57 this.object_start = 0;
58 this.vtables = [];
59 this.vector_num_elems = 0;
60 this.force_defaults = false;
61 this.string_maps = null;
62 }
63
64 /**
65 * In order to save space, fields that are set to their default value
66 * don't get serialized into the buffer. Forcing defaults provides a
67 * way to manually disable this optimization.
68 *
69 * @param forceDefaults true always serializes default values
70 */
71 forceDefaults(forceDefaults: boolean): void {
72 this.force_defaults = forceDefaults;
73 }
74
75 /**
76 * Get the ByteBuffer representing the FlatBuffer. Only call this after you've
77 * called finish(). The actual data starts at the ByteBuffer's current position,
78 * not necessarily at 0.
79 */
80 dataBuffer(): ByteBuffer {
81 return this.bb;
82 }
83
84 /**
85 * Get the bytes representing the FlatBuffer. Only call this after you've
86 * called finish().
87 */
88 asUint8Array(): Uint8Array {
89 return this.bb.bytes().subarray(this.bb.position(), this.bb.position() + this.offset());
90 }
91
92 /**
93 * Prepare to write an element of `size` after `additional_bytes` have been
94 * written, e.g. if you write a string, you need to align such the int length
95 * field is aligned to 4 bytes, and the string data follows it directly. If all
96 * you need to do is alignment, `additional_bytes` will be 0.
97 *
98 * @param size This is the of the new element to write
99 * @param additional_bytes The padding size
100 */
101 prep(size: number, additional_bytes: number): void {
102 // Track the biggest thing we've ever aligned to.
103 if (size > this.minalign) {
104 this.minalign = size;
105 }
106
107 // Find the amount of alignment needed such that `size` is properly
108 // aligned after `additional_bytes`
109 const align_size = ((~(this.bb.capacity() - this.space + additional_bytes)) + 1) & (size - 1);
110
111 // Reallocate the buffer if needed.
112 while (this.space < align_size + size + additional_bytes) {
113 const old_buf_size = this.bb.capacity();
114 this.bb = Builder.growByteBuffer(this.bb);
115 this.space += this.bb.capacity() - old_buf_size;
116 }
117
118 this.pad(align_size);
119 }
120
121 pad(byte_size: number): void {
122 for (let i = 0; i < byte_size; i++) {
123 this.bb.writeInt8(--this.space, 0);
124 }
125 }
126
127 writeInt8(value: number): void {
128 this.bb.writeInt8(this.space -= 1, value);
129 }
130
131 writeInt16(value: number): void {
132 this.bb.writeInt16(this.space -= 2, value);
133 }
134
135 writeInt32(value: number): void {
136 this.bb.writeInt32(this.space -= 4, value);
137 }
138
139 writeInt64(value: bigint): void {
140 this.bb.writeInt64(this.space -= 8, value);
141 }
142
143 writeFloat32(value: number): void {
144 this.bb.writeFloat32(this.space -= 4, value);
145 }
146
147 writeFloat64(value: number): void {
148 this.bb.writeFloat64(this.space -= 8, value);
149 }
150
151 /**
152 * Add an `int8` to the buffer, properly aligned, and grows the buffer (if necessary).
153 * @param value The `int8` to add the buffer.
154 */
155 addInt8(value: number): void {
156 this.prep(1, 0);
157 this.writeInt8(value);
158 }
159
160 /**
161 * Add an `int16` to the buffer, properly aligned, and grows the buffer (if necessary).
162 * @param value The `int16` to add the buffer.
163 */
164 addInt16(value: number): void {
165 this.prep(2, 0);
166 this.writeInt16(value);
167 }
168
169 /**
170 * Add an `int32` to the buffer, properly aligned, and grows the buffer (if necessary).
171 * @param value The `int32` to add the buffer.
172 */
173 addInt32(value: number): void {
174 this.prep(4, 0);
175 this.writeInt32(value);
176 }
177
178 /**
179 * Add an `int64` to the buffer, properly aligned, and grows the buffer (if necessary).
180 * @param value The `int64` to add the buffer.
181 */
182 addInt64(value: bigint): void {
183 this.prep(8, 0);
184 this.writeInt64(value);
185 }
186
187 /**
188 * Add a `float32` to the buffer, properly aligned, and grows the buffer (if necessary).
189 * @param value The `float32` to add the buffer.
190 */
191 addFloat32(value: number): void {
192 this.prep(4, 0);
193 this.writeFloat32(value);
194 }
195
196 /**
197 * Add a `float64` to the buffer, properly aligned, and grows the buffer (if necessary).
198 * @param value The `float64` to add the buffer.
199 */
200 addFloat64(value: number): void {
201 this.prep(8, 0);
202 this.writeFloat64(value);
203 }
204
205 addFieldInt8(voffset: number, value: number, defaultValue: number): void {
206 if (this.force_defaults || value != defaultValue) {
207 this.addInt8(value);
208 this.slot(voffset);
209 }
210 }
211
212 addFieldInt16(voffset: number, value: number, defaultValue: number): void {
213 if (this.force_defaults || value != defaultValue) {
214 this.addInt16(value);
215 this.slot(voffset);
216 }
217 }
218
219 addFieldInt32(voffset: number, value: number, defaultValue: number): void {
220 if (this.force_defaults || value != defaultValue) {
221 this.addInt32(value);
222 this.slot(voffset);
223 }
224 }
225
226 addFieldInt64(voffset: number, value: bigint, defaultValue: bigint): void {
227 if (this.force_defaults || value !== defaultValue) {
228 this.addInt64(value);
229 this.slot(voffset);
230 }
231 }
232
233 addFieldFloat32(voffset: number, value: number, defaultValue: number): void {
234 if (this.force_defaults || value != defaultValue) {
235 this.addFloat32(value);
236 this.slot(voffset);
237 }
238 }
239
240 addFieldFloat64(voffset: number, value: number, defaultValue: number): void {
241 if (this.force_defaults || value != defaultValue) {
242 this.addFloat64(value);
243 this.slot(voffset);
244 }
245 }
246
247 addFieldOffset(voffset: number, value: Offset, defaultValue: Offset): void {
248 if (this.force_defaults || value != defaultValue) {
249 this.addOffset(value);
250 this.slot(voffset);
251 }
252 }
253
254 /**
255 * Structs are stored inline, so nothing additional is being added. `d` is always 0.
256 */
257 addFieldStruct(voffset: number, value: Offset, defaultValue: Offset): void {
258 if (value != defaultValue) {
259 this.nested(value);
260 this.slot(voffset);
261 }
262 }
263
264 /**
265 * Structures are always stored inline, they need to be created right
266 * where they're used. You'll get this assertion failure if you
267 * created it elsewhere.
268 */
269 nested(obj: Offset): void {
270 if (obj != this.offset()) {
271 throw new TypeError('FlatBuffers: struct must be serialized inline.');
272 }
273 }
274
275 /**
276 * Should not be creating any other object, string or vector
277 * while an object is being constructed
278 */
279 notNested(): void {
280 if (this.isNested) {
281 throw new TypeError('FlatBuffers: object serialization must not be nested.');
282 }
283 }
284
285 /**
286 * Set the current vtable at `voffset` to the current location in the buffer.
287 */
288 slot(voffset: number): void {
289 if (this.vtable !== null)
290 this.vtable[voffset] = this.offset();
291 }
292
293 /**
294 * @returns Offset relative to the end of the buffer.
295 */
296 offset(): Offset {
297 return this.bb.capacity() - this.space;
298 }
299
300 /**
301 * Doubles the size of the backing ByteBuffer and copies the old data towards
302 * the end of the new buffer (since we build the buffer backwards).
303 *
304 * @param bb The current buffer with the existing data
305 * @returns A new byte buffer with the old data copied
306 * to it. The data is located at the end of the buffer.
307 *
308 * uint8Array.set() formally takes {Array<number>|ArrayBufferView}, so to pass
309 * it a uint8Array we need to suppress the type check:
310 * @suppress {checkTypes}
311 */
312 static growByteBuffer(bb: ByteBuffer): ByteBuffer {
313 const old_buf_size = bb.capacity();
314
315 // Ensure we don't grow beyond what fits in an int.
316 if (old_buf_size & 0xC0000000) {
317 throw new Error('FlatBuffers: cannot grow buffer beyond 2 gigabytes.');
318 }
319
320 const new_buf_size = old_buf_size << 1;
321 const nbb = ByteBuffer.allocate(new_buf_size);
322 nbb.setPosition(new_buf_size - old_buf_size);
323 nbb.bytes().set(bb.bytes(), new_buf_size - old_buf_size);
324 return nbb;
325 }
326
327 /**
328 * Adds on offset, relative to where it will be written.
329 *
330 * @param offset The offset to add.
331 */
332 addOffset(offset: Offset): void {
333 this.prep(SIZEOF_INT, 0); // Ensure alignment is already done.
334 this.writeInt32(this.offset() - offset + SIZEOF_INT);
335 }
336
337 /**
338 * Start encoding a new object in the buffer. Users will not usually need to
339 * call this directly. The FlatBuffers compiler will generate helper methods
340 * that call this method internally.
341 */
342 startObject(numfields: number): void {
343 this.notNested();
344 if (this.vtable == null) {
345 this.vtable = [];
346 }
347 this.vtable_in_use = numfields;
348 for (let i = 0; i < numfields; i++) {
349 this.vtable[i] = 0; // This will push additional elements as needed
350 }
351 this.isNested = true;
352 this.object_start = this.offset();
353 }
354
355 /**
356 * Finish off writing the object that is under construction.
357 *
358 * @returns The offset to the object inside `dataBuffer`
359 */
360 endObject(): Offset {
361 if (this.vtable == null || !this.isNested) {
362 throw new Error('FlatBuffers: endObject called without startObject');
363 }
364
365 this.addInt32(0);
366 const vtableloc = this.offset();
367
368 // Trim trailing zeroes.
369 let i = this.vtable_in_use - 1;
370 // eslint-disable-next-line no-empty
371 for (; i >= 0 && this.vtable[i] == 0; i--) {}
372 const trimmed_size = i + 1;
373
374 // Write out the current vtable.
375 for (; i >= 0; i--) {
376 // Offset relative to the start of the table.
377 this.addInt16(this.vtable[i] != 0 ? vtableloc - this.vtable[i] : 0);
378 }
379
380 const standard_fields = 2; // The fields below:
381 this.addInt16(vtableloc - this.object_start);
382 const len = (trimmed_size + standard_fields) * SIZEOF_SHORT;
383 this.addInt16(len);
384
385 // Search for an existing vtable that matches the current one.
386 let existing_vtable = 0;
387 const vt1 = this.space;
388 outer_loop:
389 for (i = 0; i < this.vtables.length; i++) {
390 const vt2 = this.bb.capacity() - this.vtables[i];
391 if (len == this.bb.readInt16(vt2)) {
392 for (let j = SIZEOF_SHORT; j < len; j += SIZEOF_SHORT) {
393 if (this.bb.readInt16(vt1 + j) != this.bb.readInt16(vt2 + j)) {
394 continue outer_loop;
395 }
396 }
397 existing_vtable = this.vtables[i];
398 break;
399 }
400 }
401
402 if (existing_vtable) {
403 // Found a match:
404 // Remove the current vtable.
405 this.space = this.bb.capacity() - vtableloc;
406
407 // Point table to existing vtable.
408 this.bb.writeInt32(this.space, existing_vtable - vtableloc);
409 } else {
410 // No match:
411 // Add the location of the current vtable to the list of vtables.
412 this.vtables.push(this.offset());
413
414 // Point table to current vtable.
415 this.bb.writeInt32(this.bb.capacity() - vtableloc, this.offset() - vtableloc);
416 }
417
418 this.isNested = false;
419 return vtableloc as Offset;
420 }
421
422 /**
423 * Finalize a buffer, poiting to the given `root_table`.
424 */
425 finish(root_table: Offset, opt_file_identifier?: string, opt_size_prefix?: boolean): void {
426 const size_prefix = opt_size_prefix ? SIZE_PREFIX_LENGTH : 0;
427 if (opt_file_identifier) {
428 const file_identifier = opt_file_identifier;
429 this.prep(this.minalign, SIZEOF_INT +
430 FILE_IDENTIFIER_LENGTH + size_prefix);
431 if (file_identifier.length != FILE_IDENTIFIER_LENGTH) {
432 throw new TypeError('FlatBuffers: file identifier must be length ' +
433 FILE_IDENTIFIER_LENGTH);
434 }
435 for (let i = FILE_IDENTIFIER_LENGTH - 1; i >= 0; i--) {
436 this.writeInt8(file_identifier.charCodeAt(i));
437 }
438 }
439 this.prep(this.minalign, SIZEOF_INT + size_prefix);
440 this.addOffset(root_table);
441 if (size_prefix) {
442 this.addInt32(this.bb.capacity() - this.space);
443 }
444 this.bb.setPosition(this.space);
445 }
446
447 /**
448 * Finalize a size prefixed buffer, pointing to the given `root_table`.
449 */
450 finishSizePrefixed(this: Builder, root_table: Offset, opt_file_identifier?: string): void {
451 this.finish(root_table, opt_file_identifier, true);
452 }
453
454 /**
455 * This checks a required field has been set in a given table that has
456 * just been constructed.
457 */
458 requiredField(table: Offset, field: number): void {
459 const table_start = this.bb.capacity() - table;
460 const vtable_start = table_start - this.bb.readInt32(table_start);
461 const ok = field < this.bb.readInt16(vtable_start) &&
462 this.bb.readInt16(vtable_start + field) != 0;
463
464 // If this fails, the caller will show what field needs to be set.
465 if (!ok) {
466 throw new TypeError('FlatBuffers: field ' + field + ' must be set');
467 }
468 }
469
470 /**
471 * Start a new array/vector of objects. Users usually will not call
472 * this directly. The FlatBuffers compiler will create a start/end
473 * method for vector types in generated code.
474 *
475 * @param elem_size The size of each element in the array
476 * @param num_elems The number of elements in the array
477 * @param alignment The alignment of the array
478 */
479 startVector(elem_size: number, num_elems: number, alignment: number): void {
480 this.notNested();
481 this.vector_num_elems = num_elems;
482 this.prep(SIZEOF_INT, elem_size * num_elems);
483 this.prep(alignment, elem_size * num_elems); // Just in case alignment > int.
484 }
485
486 /**
487 * Finish off the creation of an array and all its elements. The array must be
488 * created with `startVector`.
489 *
490 * @returns The offset at which the newly created array
491 * starts.
492 */
493 endVector(): Offset {
494 this.writeInt32(this.vector_num_elems);
495 return this.offset();
496 }
497
498 /**
499 * Encode the string `s` in the buffer using UTF-8. If the string passed has
500 * already been seen, we return the offset of the already written string
501 *
502 * @param s The string to encode
503 * @return The offset in the buffer where the encoded string starts
504 */
505 createSharedString(s: string | Uint8Array): Offset {
506 if (!s) { return 0 }
507
508 if (!this.string_maps) {
509 this.string_maps = new Map();
510 }
511
512 if (this.string_maps.has(s)) {
513 return this.string_maps.get(s) as Offset
514 }
515 const offset = this.createString(s)
516 this.string_maps.set(s, offset)
517 return offset
518 }
519
520 /**
521 * Encode the string `s` in the buffer using UTF-8. If a Uint8Array is passed
522 * instead of a string, it is assumed to contain valid UTF-8 encoded data.
523 *
524 * @param s The string to encode
525 * @return The offset in the buffer where the encoded string starts
526 */
527 createString(s: string | Uint8Array | null | undefined): Offset {
528 if (s === null || s === undefined) {
529 return 0;
530 }
531
532 let utf8: string | Uint8Array | number[];
533 if (s instanceof Uint8Array) {
534 utf8 = s;
535 } else {
536 utf8 = this.text_encoder.encode(s);
537 }
538
539 this.addInt8(0);
540 this.startVector(1, utf8.length, 1);
541 this.bb.setPosition(this.space -= utf8.length);
542 for (let i = 0, offset = this.space, bytes = this.bb.bytes(); i < utf8.length; i++) {
543 bytes[offset++] = utf8[i];
544 }
545 return this.endVector();
546 }
547
548 /**
549 * A helper function to pack an object
550 *
551 * @returns offset of obj
552 */
553 createObjectOffset(obj: string | IGeneratedObject | null): Offset {
554 if(obj === null) {
555 return 0
556 }
557
558 if(typeof obj === 'string') {
559 return this.createString(obj);
560 } else {
561 return obj.pack(this);
562 }
563 }
564
565 /**
566 * A helper function to pack a list of object
567 *
568 * @returns list of offsets of each non null object
569 */
570 createObjectOffsetList(list: (string | IGeneratedObject)[]): Offset[] {
571 const ret: number[] = [];
572
573 for(let i = 0; i < list.length; ++i) {
574 const val = list[i];
575
576 if(val !== null) {
577 ret.push(this.createObjectOffset(val));
578 } else {
579 throw new TypeError(
580 'FlatBuffers: Argument for createObjectOffsetList cannot contain null.');
581 }
582 }
583
584 return ret;
585 }
586
587 createStructOffsetList(list: (string | IGeneratedObject)[], startFunc: (builder: Builder, length: number) => void): Offset {
588 startFunc(this, list.length);
589 this.createObjectOffsetList(list.slice().reverse());
590 return this.endVector();
591 }
592 }
View as plain text