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