| import {ByteBuffer} from './byte-buffer.js'; | |
| import { | |
| FILE_IDENTIFIER_LENGTH, | |
| SIZE_PREFIX_LENGTH, | |
| SIZEOF_INT, | |
| SIZEOF_SHORT, | |
| } from './constants.js'; | |
| import {IGeneratedObject, Offset} from './types.js'; | |
| export class Builder { | |
| private bb: ByteBuffer; | |
| /** Remaining space in the ByteBuffer. */ | |
| private space: number; | |
| /** Minimum alignment encountered so far. */ | |
| private minalign = 1; | |
| /** The vtable for the current table. */ | |
| private vtable: number[] | null = null; | |
| /** The amount of fields we're actually using. */ | |
| private vtable_in_use = 0; | |
| /** Whether we are currently serializing a table. */ | |
| private isNested = false; | |
| /** Starting offset of the current struct/table. */ | |
| private object_start = 0; | |
| /** List of offsets of all vtables. */ | |
| private vtables: number[] = []; | |
| /** For the current vector being built. */ | |
| private vector_num_elems = 0; | |
| /** False omits default values from the serialized data */ | |
| private force_defaults = false; | |
| private string_maps: Map<string | Uint8Array, number> | null = null; | |
| private text_encoder = new TextEncoder(); | |
| /** | |
| * Create a FlatBufferBuilder. | |
| */ | |
| constructor(opt_initial_size?: number) { | |
| let initial_size: number; | |
| if (!opt_initial_size) { | |
| initial_size = 1024; | |
| } else { | |
| initial_size = opt_initial_size; | |
| } | |
| /** | |
| * @type {ByteBuffer} | |
| * @private | |
| */ | |
| this.bb = ByteBuffer.allocate(initial_size); | |
| this.space = initial_size; | |
| } | |
| clear(): void { | |
| this.bb.clear(); | |
| this.space = this.bb.capacity(); | |
| this.minalign = 1; | |
| this.vtable = null; | |
| this.vtable_in_use = 0; | |
| this.isNested = false; | |
| this.object_start = 0; | |
| this.vtables = []; | |
| this.vector_num_elems = 0; | |
| this.force_defaults = false; | |
| this.string_maps = null; | |
| } | |
| /** | |
| * In order to save space, fields that are set to their default value | |
| * don't get serialized into the buffer. Forcing defaults provides a | |
| * way to manually disable this optimization. | |
| * | |
| * @param forceDefaults true always serializes default values | |
| */ | |
| forceDefaults(forceDefaults: boolean): void { | |
| this.force_defaults = forceDefaults; | |
| } | |
| /** | |
| * Get the ByteBuffer representing the FlatBuffer. Only call this after you've | |
| * called finish(). The actual data starts at the ByteBuffer's current position, | |
| * not necessarily at 0. | |
| */ | |
| dataBuffer(): ByteBuffer { | |
| return this.bb; | |
| } | |
| /** | |
| * Get the bytes representing the FlatBuffer. Only call this after you've | |
| * called finish(). | |
| */ | |
| asUint8Array(): Uint8Array { | |
| return this.bb | |
| .bytes() | |
| .subarray(this.bb.position(), this.bb.position() + this.offset()); | |
| } | |
| /** | |
| * Prepare to write an element of `size` after `additional_bytes` have been | |
| * written, e.g. if you write a string, you need to align such the int length | |
| * field is aligned to 4 bytes, and the string data follows it directly. If all | |
| * you need to do is alignment, `additional_bytes` will be 0. | |
| * | |
| * @param size This is the of the new element to write | |
| * @param additional_bytes The padding size | |
| */ | |
| prep(size: number, additional_bytes: number): void { | |
| // Track the biggest thing we've ever aligned to. | |
| if (size > this.minalign) { | |
| this.minalign = size; | |
| } | |
| // Find the amount of alignment needed such that `size` is properly | |
| // aligned after `additional_bytes` | |
| const align_size = | |
| (~(this.bb.capacity() - this.space + additional_bytes) + 1) & (size - 1); | |
| // Reallocate the buffer if needed. | |
| while (this.space < align_size + size + additional_bytes) { | |
| const old_buf_size = this.bb.capacity(); | |
| this.bb = Builder.growByteBuffer(this.bb); | |
| this.space += this.bb.capacity() - old_buf_size; | |
| } | |
| this.pad(align_size); | |
| } | |
| pad(byte_size: number): void { | |
| for (let i = 0; i < byte_size; i++) { | |
| this.bb.writeInt8(--this.space, 0); | |
| } | |
| } | |
| writeInt8(value: number): void { | |
| this.bb.writeInt8((this.space -= 1), value); | |
| } | |
| writeInt16(value: number): void { | |
| this.bb.writeInt16((this.space -= 2), value); | |
| } | |
| writeInt32(value: number): void { | |
| this.bb.writeInt32((this.space -= 4), value); | |
| } | |
| writeInt64(value: bigint): void { | |
| this.bb.writeInt64((this.space -= 8), value); | |
| } | |
| writeFloat32(value: number): void { | |
| this.bb.writeFloat32((this.space -= 4), value); | |
| } | |
| writeFloat64(value: number): void { | |
| this.bb.writeFloat64((this.space -= 8), value); | |
| } | |
| /** | |
| * Add an `int8` to the buffer, properly aligned, and grows the buffer (if necessary). | |
| * @param value The `int8` to add the buffer. | |
| */ | |
| addInt8(value: number): void { | |
| this.prep(1, 0); | |
| this.writeInt8(value); | |
| } | |
| /** | |
| * Add an `int16` to the buffer, properly aligned, and grows the buffer (if necessary). | |
| * @param value The `int16` to add the buffer. | |
| */ | |
| addInt16(value: number): void { | |
| this.prep(2, 0); | |
| this.writeInt16(value); | |
| } | |
| /** | |
| * Add an `int32` to the buffer, properly aligned, and grows the buffer (if necessary). | |
| * @param value The `int32` to add the buffer. | |
| */ | |
| addInt32(value: number): void { | |
| this.prep(4, 0); | |
| this.writeInt32(value); | |
| } | |
| /** | |
| * Add an `int64` to the buffer, properly aligned, and grows the buffer (if necessary). | |
| * @param value The `int64` to add the buffer. | |
| */ | |
| addInt64(value: bigint): void { | |
| this.prep(8, 0); | |
| this.writeInt64(value); | |
| } | |
| /** | |
| * Add a `float32` to the buffer, properly aligned, and grows the buffer (if necessary). | |
| * @param value The `float32` to add the buffer. | |
| */ | |
| addFloat32(value: number): void { | |
| this.prep(4, 0); | |
| this.writeFloat32(value); | |
| } | |
| /** | |
| * Add a `float64` to the buffer, properly aligned, and grows the buffer (if necessary). | |
| * @param value The `float64` to add the buffer. | |
| */ | |
| addFloat64(value: number): void { | |
| this.prep(8, 0); | |
| this.writeFloat64(value); | |
| } | |
| addFieldInt8( | |
| voffset: number, | |
| value: number, | |
| defaultValue: number | null | undefined, | |
| ): void { | |
| if (this.force_defaults || value != defaultValue) { | |
| this.addInt8(value); | |
| this.slot(voffset); | |
| } | |
| } | |
| addFieldInt16( | |
| voffset: number, | |
| value: number, | |
| defaultValue: number | null | undefined, | |
| ): void { | |
| if (this.force_defaults || value != defaultValue) { | |
| this.addInt16(value); | |
| this.slot(voffset); | |
| } | |
| } | |
| addFieldInt32( | |
| voffset: number, | |
| value: number, | |
| defaultValue: number | null | undefined, | |
| ): void { | |
| if (this.force_defaults || value != defaultValue) { | |
| this.addInt32(value); | |
| this.slot(voffset); | |
| } | |
| } | |
| addFieldInt64( | |
| voffset: number, | |
| value: bigint, | |
| defaultValue: bigint | null | undefined, | |
| ): void { | |
| if (this.force_defaults || value !== defaultValue) { | |
| this.addInt64(value); | |
| this.slot(voffset); | |
| } | |
| } | |
| addFieldFloat32( | |
| voffset: number, | |
| value: number, | |
| defaultValue: number | null | undefined, | |
| ): void { | |
| if (this.force_defaults || value != defaultValue) { | |
| this.addFloat32(value); | |
| this.slot(voffset); | |
| } | |
| } | |
| addFieldFloat64( | |
| voffset: number, | |
| value: number, | |
| defaultValue: number | null | undefined, | |
| ): void { | |
| if (this.force_defaults || value != defaultValue) { | |
| this.addFloat64(value); | |
| this.slot(voffset); | |
| } | |
| } | |
| addFieldOffset(voffset: number, value: Offset, defaultValue: Offset): void { | |
| if (this.force_defaults || value != defaultValue) { | |
| this.addOffset(value); | |
| this.slot(voffset); | |
| } | |
| } | |
| /** | |
| * Structs are stored inline, so nothing additional is being added. `d` is always 0. | |
| */ | |
| addFieldStruct(voffset: number, value: Offset, defaultValue: Offset): void { | |
| if (value != defaultValue) { | |
| this.nested(value); | |
| this.slot(voffset); | |
| } | |
| } | |
| /** | |
| * Structures are always stored inline, they need to be created right | |
| * where they're used. You'll get this assertion failure if you | |
| * created it elsewhere. | |
| */ | |
| nested(obj: Offset): void { | |
| if (obj != this.offset()) { | |
| throw new TypeError('FlatBuffers: struct must be serialized inline.'); | |
| } | |
| } | |
| /** | |
| * Should not be creating any other object, string or vector | |
| * while an object is being constructed | |
| */ | |
| notNested(): void { | |
| if (this.isNested) { | |
| throw new TypeError( | |
| 'FlatBuffers: object serialization must not be nested.', | |
| ); | |
| } | |
| } | |
| /** | |
| * Set the current vtable at `voffset` to the current location in the buffer. | |
| */ | |
| slot(voffset: number): void { | |
| if (this.vtable !== null) this.vtable[voffset] = this.offset(); | |
| } | |
| /** | |
| * @returns Offset relative to the end of the buffer. | |
| */ | |
| offset(): Offset { | |
| return this.bb.capacity() - this.space; | |
| } | |
| /** | |
| * Doubles the size of the backing ByteBuffer and copies the old data towards | |
| * the end of the new buffer (since we build the buffer backwards). | |
| * | |
| * @param bb The current buffer with the existing data | |
| * @returns A new byte buffer with the old data copied | |
| * to it. The data is located at the end of the buffer. | |
| * | |
| * uint8Array.set() formally takes {Array<number>|ArrayBufferView}, so to pass | |
| * it a uint8Array we need to suppress the type check: | |
| * @suppress {checkTypes} | |
| */ | |
| static growByteBuffer(bb: ByteBuffer): ByteBuffer { | |
| const old_buf_size = bb.capacity(); | |
| // Ensure we don't grow beyond what fits in an int. | |
| if (old_buf_size & 0xc0000000) { | |
| throw new Error('FlatBuffers: cannot grow buffer beyond 2 gigabytes.'); | |
| } | |
| const new_buf_size = old_buf_size << 1; | |
| const nbb = ByteBuffer.allocate(new_buf_size); | |
| nbb.setPosition(new_buf_size - old_buf_size); | |
| nbb.bytes().set(bb.bytes(), new_buf_size - old_buf_size); | |
| return nbb; | |
| } | |
| /** | |
| * Adds on offset, relative to where it will be written. | |
| * | |
| * @param offset The offset to add. | |
| */ | |
| addOffset(offset: Offset): void { | |
| this.prep(SIZEOF_INT, 0); // Ensure alignment is already done. | |
| this.writeInt32(this.offset() - offset + SIZEOF_INT); | |
| } | |
| /** | |
| * Start encoding a new object in the buffer. Users will not usually need to | |
| * call this directly. The FlatBuffers compiler will generate helper methods | |
| * that call this method internally. | |
| */ | |
| startObject(numfields: number): void { | |
| this.notNested(); | |
| if (this.vtable == null) { | |
| this.vtable = []; | |
| } | |
| this.vtable_in_use = numfields; | |
| for (let i = 0; i < numfields; i++) { | |
| this.vtable[i] = 0; // This will push additional elements as needed | |
| } | |
| this.isNested = true; | |
| this.object_start = this.offset(); | |
| } | |
| /** | |
| * Finish off writing the object that is under construction. | |
| * | |
| * @returns The offset to the object inside `dataBuffer` | |
| */ | |
| endObject(): Offset { | |
| if (this.vtable == null || !this.isNested) { | |
| throw new Error('FlatBuffers: endObject called without startObject'); | |
| } | |
| this.addInt32(0); | |
| const vtableloc = this.offset(); | |
| // Trim trailing zeroes. | |
| let i = this.vtable_in_use - 1; | |
| // eslint-disable-next-line no-empty | |
| for (; i >= 0 && this.vtable[i] == 0; i--) {} | |
| const trimmed_size = i + 1; | |
| // Write out the current vtable. | |
| for (; i >= 0; i--) { | |
| // Offset relative to the start of the table. | |
| this.addInt16(this.vtable[i] != 0 ? vtableloc - this.vtable[i] : 0); | |
| } | |
| const standard_fields = 2; // The fields below: | |
| this.addInt16(vtableloc - this.object_start); | |
| const len = (trimmed_size + standard_fields) * SIZEOF_SHORT; | |
| this.addInt16(len); | |
| // Search for an existing vtable that matches the current one. | |
| let existing_vtable = 0; | |
| const vt1 = this.space; | |
| outer_loop: for (i = 0; i < this.vtables.length; i++) { | |
| const vt2 = this.bb.capacity() - this.vtables[i]; | |
| if (len == this.bb.readInt16(vt2)) { | |
| for (let j = SIZEOF_SHORT; j < len; j += SIZEOF_SHORT) { | |
| if (this.bb.readInt16(vt1 + j) != this.bb.readInt16(vt2 + j)) { | |
| continue outer_loop; | |
| } | |
| } | |
| existing_vtable = this.vtables[i]; | |
| break; | |
| } | |
| } | |
| if (existing_vtable) { | |
| // Found a match: | |
| // Remove the current vtable. | |
| this.space = this.bb.capacity() - vtableloc; | |
| // Point table to existing vtable. | |
| this.bb.writeInt32(this.space, existing_vtable - vtableloc); | |
| } else { | |
| // No match: | |
| // Add the location of the current vtable to the list of vtables. | |
| this.vtables.push(this.offset()); | |
| // Point table to current vtable. | |
| this.bb.writeInt32( | |
| this.bb.capacity() - vtableloc, | |
| this.offset() - vtableloc, | |
| ); | |
| } | |
| this.isNested = false; | |
| return vtableloc as Offset; | |
| } | |
| /** | |
| * Finalize a buffer, poiting to the given `root_table`. | |
| */ | |
| finish( | |
| root_table: Offset, | |
| opt_file_identifier?: string, | |
| opt_size_prefix?: boolean, | |
| ): void { | |
| const size_prefix = opt_size_prefix ? SIZE_PREFIX_LENGTH : 0; | |
| if (opt_file_identifier) { | |
| const file_identifier = opt_file_identifier; | |
| this.prep( | |
| this.minalign, | |
| SIZEOF_INT + FILE_IDENTIFIER_LENGTH + size_prefix, | |
| ); | |
| if (file_identifier.length != FILE_IDENTIFIER_LENGTH) { | |
| throw new TypeError( | |
| 'FlatBuffers: file identifier must be length ' + | |
| FILE_IDENTIFIER_LENGTH, | |
| ); | |
| } | |
| for (let i = FILE_IDENTIFIER_LENGTH - 1; i >= 0; i--) { | |
| this.writeInt8(file_identifier.charCodeAt(i)); | |
| } | |
| } | |
| this.prep(this.minalign, SIZEOF_INT + size_prefix); | |
| this.addOffset(root_table); | |
| if (size_prefix) { | |
| this.addInt32(this.bb.capacity() - this.space); | |
| } | |
| this.bb.setPosition(this.space); | |
| } | |
| /** | |
| * Finalize a size prefixed buffer, pointing to the given `root_table`. | |
| */ | |
| finishSizePrefixed( | |
| this: Builder, | |
| root_table: Offset, | |
| opt_file_identifier?: string, | |
| ): void { | |
| this.finish(root_table, opt_file_identifier, true); | |
| } | |
| /** | |
| * This checks a required field has been set in a given table that has | |
| * just been constructed. | |
| */ | |
| requiredField(table: Offset, field: number): void { | |
| const table_start = this.bb.capacity() - table; | |
| const vtable_start = table_start - this.bb.readInt32(table_start); | |
| const ok = | |
| field < this.bb.readInt16(vtable_start) && | |
| this.bb.readInt16(vtable_start + field) != 0; | |
| // If this fails, the caller will show what field needs to be set. | |
| if (!ok) { | |
| throw new TypeError('FlatBuffers: field ' + field + ' must be set'); | |
| } | |
| } | |
| /** | |
| * Start a new array/vector of objects. Users usually will not call | |
| * this directly. The FlatBuffers compiler will create a start/end | |
| * method for vector types in generated code. | |
| * | |
| * @param elem_size The size of each element in the array | |
| * @param num_elems The number of elements in the array | |
| * @param alignment The alignment of the array | |
| */ | |
| startVector(elem_size: number, num_elems: number, alignment: number): void { | |
| this.notNested(); | |
| this.vector_num_elems = num_elems; | |
| this.prep(SIZEOF_INT, elem_size * num_elems); | |
| this.prep(alignment, elem_size * num_elems); // Just in case alignment > int. | |
| } | |
| /** | |
| * Finish off the creation of an array and all its elements. The array must be | |
| * created with `startVector`. | |
| * | |
| * @returns The offset at which the newly created array | |
| * starts. | |
| */ | |
| endVector(): Offset { | |
| this.writeInt32(this.vector_num_elems); | |
| return this.offset(); | |
| } | |
| /** | |
| * Encode the string `s` in the buffer using UTF-8. If the string passed has | |
| * already been seen, we return the offset of the already written string | |
| * | |
| * @param s The string to encode | |
| * @return The offset in the buffer where the encoded string starts | |
| */ | |
| createSharedString(s: string | Uint8Array): Offset { | |
| if (!s) { | |
| return 0; | |
| } | |
| if (!this.string_maps) { | |
| this.string_maps = new Map(); | |
| } | |
| if (this.string_maps.has(s)) { | |
| return this.string_maps.get(s) as Offset; | |
| } | |
| const offset = this.createString(s); | |
| this.string_maps.set(s, offset); | |
| return offset; | |
| } | |
| /** | |
| * Encode the string `s` in the buffer using UTF-8. If a Uint8Array is passed | |
| * instead of a string, it is assumed to contain valid UTF-8 encoded data. | |
| * | |
| * @param s The string to encode | |
| * @return The offset in the buffer where the encoded string starts | |
| */ | |
| createString(s: string | Uint8Array | null | undefined): Offset { | |
| if (s === null || s === undefined) { | |
| return 0; | |
| } | |
| let utf8: string | Uint8Array | number[]; | |
| if (s instanceof Uint8Array) { | |
| utf8 = s; | |
| } else { | |
| utf8 = this.text_encoder.encode(s); | |
| } | |
| this.addInt8(0); | |
| this.startVector(1, utf8.length, 1); | |
| this.bb.setPosition((this.space -= utf8.length)); | |
| this.bb.bytes().set(utf8, this.space); | |
| return this.endVector(); | |
| } | |
| /** | |
| * Create a byte vector. | |
| * | |
| * @param v The bytes to add | |
| * @returns The offset in the buffer where the byte vector starts | |
| */ | |
| createByteVector(v: Uint8Array | null | undefined): Offset { | |
| if (v === null || v === undefined) { | |
| return 0; | |
| } | |
| this.startVector(1, v.length, 1); | |
| this.bb.setPosition((this.space -= v.length)); | |
| this.bb.bytes().set(v, this.space); | |
| return this.endVector(); | |
| } | |
| /** | |
| * A helper function to pack an object | |
| * | |
| * @returns offset of obj | |
| */ | |
| createObjectOffset(obj: string | IGeneratedObject | null | undefined): Offset { | |
| if (obj === null || obj === undefined) { | |
| return 0; | |
| } | |
| if (typeof obj === 'string') { | |
| return this.createString(obj); | |
| } else { | |
| return obj.pack(this); | |
| } | |
| } | |
| /** | |
| * A helper function to pack a list of object | |
| * | |
| * @returns list of offsets of each non null/undefined object | |
| */ | |
| createObjectOffsetList(list: (string | IGeneratedObject)[]): Offset[] { | |
| const ret: number[] = []; | |
| for (let i = 0; i < list.length; ++i) { | |
| const val = list[i]; | |
| if (val !== null && val !== undefined) { | |
| ret.push(this.createObjectOffset(val)); | |
| } else { | |
| throw new TypeError( | |
| 'FlatBuffers: Argument for createObjectOffsetList cannot contain null or undefined.', | |
| ); | |
| } | |
| } | |
| return ret; | |
| } | |
| createStructOffsetList( | |
| list: (string | IGeneratedObject)[], | |
| startFunc: (builder: Builder, length: number) => void, | |
| ): Offset { | |
| startFunc(this, list.length); | |
| this.createObjectOffsetList(list.slice().reverse()); | |
| return this.endVector(); | |
| } | |
| } |