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