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|null): 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|null): 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|null): 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|null): 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|null): 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|null): 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 this.bb.bytes().set(utf8, this.space); 543 return this.endVector(); 544 } 545 546 /** 547 * Create a byte vector. 548 * 549 * @param v The bytes to add 550 * @returns The offset in the buffer where the byte vector starts 551 */ 552 createByteVector(v: Uint8Array | null | undefined): Offset { 553 if (v === null || v === undefined) { 554 return 0; 555 } 556 557 this.startVector(1, v.length, 1); 558 this.bb.setPosition(this.space -= v.length); 559 this.bb.bytes().set(v, this.space); 560 return this.endVector(); 561 } 562 563 /** 564 * A helper function to pack an object 565 * 566 * @returns offset of obj 567 */ 568 createObjectOffset(obj: string | IGeneratedObject | null): Offset { 569 if(obj === null) { 570 return 0 571 } 572 573 if(typeof obj === 'string') { 574 return this.createString(obj); 575 } else { 576 return obj.pack(this); 577 } 578 } 579 580 /** 581 * A helper function to pack a list of object 582 * 583 * @returns list of offsets of each non null object 584 */ 585 createObjectOffsetList(list: (string | IGeneratedObject)[]): Offset[] { 586 const ret: number[] = []; 587 588 for(let i = 0; i < list.length; ++i) { 589 const val = list[i]; 590 591 if(val !== null) { 592 ret.push(this.createObjectOffset(val)); 593 } else { 594 throw new TypeError( 595 'FlatBuffers: Argument for createObjectOffsetList cannot contain null.'); 596 } 597 } 598 599 return ret; 600 } 601 602 createStructOffsetList(list: (string | IGeneratedObject)[], startFunc: (builder: Builder, length: number) => void): Offset { 603 startFunc(this, list.length); 604 this.createObjectOffsetList(list.slice().reverse()); 605 return this.endVector(); 606 } 607 } 608