• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1import { ByteBuffer } from "./byte-buffer"
2import { SIZEOF_SHORT, SIZE_PREFIX_LENGTH, SIZEOF_INT, FILE_IDENTIFIER_LENGTH } from "./constants"
3import { Offset, IGeneratedObject } from "./types"
4import { Long } from "./long"
5
6export class Builder {
7    private bb: ByteBuffer
8    /** Remaining space in the ByteBuffer. */
9    private space: number
10    /** Minimum alignment encountered so far. */
11    private minalign = 1
12    /** The vtable for the current table. */
13    private vtable: number[] | null = null
14    /** The amount of fields we're actually using. */
15    private vtable_in_use = 0
16    /** Whether we are currently serializing a table. */
17    private isNested = false;
18    /** Starting offset of the current struct/table. */
19    private object_start = 0
20    /** List of offsets of all vtables. */
21    private vtables: number[] = []
22    /** For the current vector being built. */
23    private vector_num_elems = 0
24    /** False omits default values from the serialized data */
25    private force_defaults = false;
26
27    private string_maps: Map<string | Uint8Array, number> | null = null;
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: Long): 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 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 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 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 the buffer.
181     */
182    addInt64(value: Long): 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 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 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: Long, defaultValue: Long): void {
227      if (this.force_defaults || !value.equals(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 Error('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 Error('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 Error('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 = this.bb.readInt16(vtable_start + field) != 0;
462
463      // If this fails, the caller will show what field needs to be set.
464      if (!ok) {
465        throw new Error('FlatBuffers: field ' + field + ' must be set');
466      }
467    }
468
469    /**
470     * Start a new array/vector of objects.  Users usually will not call
471     * this directly. The FlatBuffers compiler will create a start/end
472     * method for vector types in generated code.
473     *
474     * @param elem_size The size of each element in the array
475     * @param num_elems The number of elements in the array
476     * @param alignment The alignment of the array
477     */
478    startVector(elem_size: number, num_elems: number, alignment: number): void {
479      this.notNested();
480      this.vector_num_elems = num_elems;
481      this.prep(SIZEOF_INT, elem_size * num_elems);
482      this.prep(alignment, elem_size * num_elems); // Just in case alignment > int.
483    }
484
485    /**
486     * Finish off the creation of an array and all its elements. The array must be
487     * created with `startVector`.
488     *
489     * @returns The offset at which the newly created array
490     * starts.
491     */
492    endVector(): Offset {
493      this.writeInt32(this.vector_num_elems);
494      return this.offset();
495    }
496
497    /**
498     * Encode the string `s` in the buffer using UTF-8. If the string passed has
499     * already been seen, we return the offset of the already written string
500     *
501     * @param s The string to encode
502     * @return The offset in the buffer where the encoded string starts
503     */
504    createSharedString(s: string | Uint8Array): Offset {
505      if (!s) { return 0 }
506
507      if (!this.string_maps) {
508        this.string_maps = new Map();
509      }
510
511      if (this.string_maps.has(s)) {
512        return this.string_maps.get(s) as Offset
513      }
514      const offset = this.createString(s)
515      this.string_maps.set(s, offset)
516      return offset
517    }
518
519    /**
520     * Encode the string `s` in the buffer using UTF-8. If a Uint8Array is passed
521     * instead of a string, it is assumed to contain valid UTF-8 encoded data.
522     *
523     * @param s The string to encode
524     * @return The offset in the buffer where the encoded string starts
525     */
526    createString(s: string | Uint8Array): Offset {
527      if (!s) { return 0 }
528      let utf8: string | Uint8Array | number[];
529      if (s instanceof Uint8Array) {
530        utf8 = s;
531      } else {
532        utf8 = [];
533        let i = 0;
534
535        while (i < s.length) {
536          let codePoint;
537
538          // Decode UTF-16
539          const a = s.charCodeAt(i++);
540          if (a < 0xD800 || a >= 0xDC00) {
541            codePoint = a;
542          } else {
543            const b = s.charCodeAt(i++);
544            codePoint = (a << 10) + b + (0x10000 - (0xD800 << 10) - 0xDC00);
545          }
546
547          // Encode UTF-8
548          if (codePoint < 0x80) {
549            utf8.push(codePoint);
550          } else {
551            if (codePoint < 0x800) {
552              utf8.push(((codePoint >> 6) & 0x1F) | 0xC0);
553            } else {
554              if (codePoint < 0x10000) {
555                utf8.push(((codePoint >> 12) & 0x0F) | 0xE0);
556              } else {
557                utf8.push(
558                  ((codePoint >> 18) & 0x07) | 0xF0,
559                  ((codePoint >> 12) & 0x3F) | 0x80);
560              }
561              utf8.push(((codePoint >> 6) & 0x3F) | 0x80);
562            }
563            utf8.push((codePoint & 0x3F) | 0x80);
564          }
565        }
566      }
567
568      this.addInt8(0);
569      this.startVector(1, utf8.length, 1);
570      this.bb.setPosition(this.space -= utf8.length);
571      for (let i = 0, offset = this.space, bytes = this.bb.bytes(); i < utf8.length; i++) {
572        bytes[offset++] = utf8[i];
573      }
574      return this.endVector();
575    }
576
577    /**
578     * A helper function to avoid generated code depending on this file directly.
579     */
580    createLong(low: number, high: number): Long {
581      return Long.create(low, high);
582    }
583
584    /**
585     * A helper function to pack an object
586     *
587     * @returns offset of obj
588     */
589    createObjectOffset(obj: string | any): Offset {
590      if(obj === null) {
591        return 0
592      }
593
594      if(typeof obj === 'string') {
595        return this.createString(obj);
596      } else {
597        return obj.pack(this);
598      }
599    }
600
601    /**
602     * A helper function to pack a list of object
603     *
604     * @returns list of offsets of each non null object
605     */
606    createObjectOffsetList(list: string[] | any[]): Offset[] {
607      const ret: number[] = [];
608
609      for(let i = 0; i < list.length; ++i) {
610        const val = list[i];
611
612        if(val !== null) {
613          ret.push(this.createObjectOffset(val));
614        } else {
615          throw new Error(
616            'FlatBuffers: Argument for createObjectOffsetList cannot contain null.');
617        }
618      }
619
620      return ret;
621    }
622
623    createStructOffsetList(list: string[] | any[], startFunc: (builder: Builder, length: number) => void): Offset {
624      startFunc(this, list.length);
625      this.createObjectOffsetList(list);
626      return this.endVector();
627    }
628  }
629