xref: /aosp_15_r20/external/flatbuffers/ts/flexbuffers/builder.ts (revision 890232f25432b36107d06881e0a25aaa6b473652)
1import { BitWidth } from './bit-width.js'
2import { paddingSize, iwidth, uwidth, fwidth, toByteWidth, fromByteWidth } from './bit-width-util.js'
3import { toUTF8Array } from './flexbuffers-util.js'
4import { ValueType } from './value-type.js'
5import { isNumber, isTypedVectorElement, toTypedVector } from './value-type-util.js'
6import { StackValue } from './stack-value.js'
7
8interface StackPointer {
9  stackPosition: number,
10  isVector: boolean
11  presorted?: boolean
12}
13
14export class Builder {
15  buffer: ArrayBuffer
16  view: DataView
17
18  readonly stack: Array<StackValue> = [];
19  readonly stackPointers: Array<StackPointer> = [];
20  offset = 0;
21  finished = false;
22  readonly stringLookup: Record<string, StackValue> = {};
23  readonly keyLookup: Record<string, StackValue> = {};
24  readonly keyVectorLookup: Record<string, StackValue> = {};
25  readonly indirectIntLookup: Record<number, StackValue> = {};
26  readonly indirectUIntLookup: Record<number, StackValue> = {};
27  readonly indirectFloatLookup: Record<number, StackValue> = {};
28
29  constructor(size = 2048, private dedupStrings = true, private dedupKeys = true, private dedupKeyVectors = true) {
30    this.buffer = new ArrayBuffer(size > 0 ? size : 2048);
31    this.view = new DataView(this.buffer);
32  }
33
34  private align(width: BitWidth) {
35    const byteWidth = toByteWidth(width);
36    this.offset += paddingSize(this.offset, byteWidth);
37    return byteWidth;
38  }
39
40  computeOffset(newValueSize: number): number {
41    const targetOffset = this.offset + newValueSize;
42    let size = this.buffer.byteLength;
43    const prevSize = size;
44    while (size < targetOffset) {
45      size <<= 1;
46    }
47    if (prevSize < size) {
48      const prevBuffer = this.buffer;
49      this.buffer = new ArrayBuffer(size);
50      this.view = new DataView(this.buffer);
51      new Uint8Array(this.buffer).set(new Uint8Array(prevBuffer), 0);
52    }
53    return targetOffset;
54  }
55
56  pushInt(value: number, width: BitWidth): void {
57    if (width === BitWidth.WIDTH8) {
58      this.view.setInt8(this.offset, value);
59    } else if (width === BitWidth.WIDTH16) {
60      this.view.setInt16(this.offset, value, true);
61    } else if (width === BitWidth.WIDTH32) {
62      this.view.setInt32(this.offset, value, true);
63    } else if (width === BitWidth.WIDTH64) {
64      this.view.setBigInt64(this.offset, BigInt(value), true);
65    } else {
66      throw `Unexpected width: ${width} for value: ${value}`;
67    }
68  }
69
70  pushUInt(value: number, width: BitWidth): void {
71    if (width === BitWidth.WIDTH8) {
72      this.view.setUint8(this.offset, value);
73    } else if (width === BitWidth.WIDTH16) {
74      this.view.setUint16(this.offset, value, true);
75    } else if (width === BitWidth.WIDTH32) {
76      this.view.setUint32(this.offset, value, true);
77    } else if (width === BitWidth.WIDTH64) {
78      this.view.setBigUint64(this.offset, BigInt(value), true);
79    } else {
80      throw `Unexpected width: ${width} for value: ${value}`;
81    }
82  }
83
84  private writeInt(value: number, byteWidth: number) {
85    const newOffset = this.computeOffset(byteWidth);
86    this.pushInt(value, fromByteWidth(byteWidth));
87    this.offset = newOffset;
88  }
89
90  private writeUInt(value: number, byteWidth: number) {
91    const newOffset = this.computeOffset(byteWidth);
92    this.pushUInt(value, fromByteWidth(byteWidth));
93    this.offset = newOffset;
94  }
95
96  private writeBlob(arrayBuffer: ArrayBuffer) {
97    const length = arrayBuffer.byteLength;
98    const bitWidth = uwidth(length);
99    const byteWidth = this.align(bitWidth);
100    this.writeUInt(length, byteWidth);
101    const blobOffset = this.offset;
102    const newOffset = this.computeOffset(length);
103    new Uint8Array(this.buffer).set(new Uint8Array(arrayBuffer), blobOffset);
104    this.stack.push(this.offsetStackValue(blobOffset, ValueType.BLOB, bitWidth));
105    this.offset = newOffset;
106  }
107
108  private writeString(str: string): void {
109    if (this.dedupStrings && Object.prototype.hasOwnProperty.call(this.stringLookup, str)) {
110      this.stack.push(this.stringLookup[str]);
111      return;
112    }
113    const utf8 = toUTF8Array(str);
114    const length = utf8.length;
115    const bitWidth = uwidth(length);
116    const byteWidth = this.align(bitWidth);
117    this.writeUInt(length, byteWidth);
118    const stringOffset = this.offset;
119    const newOffset = this.computeOffset(length + 1);
120    new Uint8Array(this.buffer).set(utf8, stringOffset);
121    const stackValue = this.offsetStackValue(stringOffset, ValueType.STRING, bitWidth);
122    this.stack.push(stackValue);
123    if (this.dedupStrings) {
124      this.stringLookup[str] = stackValue;
125    }
126    this.offset = newOffset;
127  }
128
129  private writeKey(str: string): void {
130    if (this.dedupKeys && Object.prototype.hasOwnProperty.call(this.keyLookup, str)) {
131      this.stack.push(this.keyLookup[str]);
132      return;
133    }
134    const utf8 = toUTF8Array(str);
135    const length = utf8.length;
136    const newOffset = this.computeOffset(length + 1);
137    new Uint8Array(this.buffer).set(utf8, this.offset);
138    const stackValue = this.offsetStackValue(this.offset, ValueType.KEY, BitWidth.WIDTH8);
139    this.stack.push(stackValue);
140    if (this.dedupKeys) {
141      this.keyLookup[str] = stackValue;
142    }
143    this.offset = newOffset;
144  }
145
146  private writeStackValue(value: StackValue, byteWidth: number): void {
147    const newOffset = this.computeOffset(byteWidth);
148    if (value.isOffset()) {
149      const relativeOffset = this.offset - value.offset;
150      if (byteWidth === 8 || BigInt(relativeOffset) < (BigInt(1) << BigInt(byteWidth * 8))) {
151        this.writeUInt(relativeOffset, byteWidth);
152      } else {
153        throw `Unexpected size ${byteWidth}. This might be a bug. Please create an issue https://github.com/google/flatbuffers/issues/new`
154      }
155    } else {
156      value.writeToBuffer(byteWidth);
157    }
158    this.offset = newOffset;
159  }
160
161  private integrityCheckOnValueAddition() {
162    if (this.finished) {
163      throw "Adding values after finish is prohibited";
164    }
165    if (this.stackPointers.length !== 0 && this.stackPointers[this.stackPointers.length - 1].isVector === false) {
166      if (this.stack[this.stack.length - 1].type !== ValueType.KEY) {
167        throw "Adding value to a map before adding a key is prohibited";
168      }
169    }
170  }
171
172  private integrityCheckOnKeyAddition() {
173    if (this.finished) {
174      throw "Adding values after finish is prohibited";
175    }
176    if (this.stackPointers.length === 0 || this.stackPointers[this.stackPointers.length - 1].isVector) {
177      throw "Adding key before starting a map is prohibited";
178    }
179  }
180
181  startVector(): void {
182    this.stackPointers.push({ stackPosition: this.stack.length, isVector: true });
183  }
184
185  startMap(presorted = false): void {
186    this.stackPointers.push({ stackPosition: this.stack.length, isVector: false, presorted: presorted });
187  }
188
189  private endVector(stackPointer: StackPointer) {
190    const vecLength = this.stack.length - stackPointer.stackPosition;
191    const vec = this.createVector(stackPointer.stackPosition, vecLength, 1);
192    this.stack.splice(stackPointer.stackPosition, vecLength);
193    this.stack.push(vec);
194  }
195
196  private endMap(stackPointer: StackPointer) {
197    if (!stackPointer.presorted) {
198      this.sort(stackPointer);
199    }
200    let keyVectorHash = "";
201    for (let i = stackPointer.stackPosition; i < this.stack.length; i += 2) {
202      keyVectorHash += `,${this.stack[i].offset}`;
203    }
204    const vecLength = (this.stack.length - stackPointer.stackPosition) >> 1;
205
206    if (this.dedupKeyVectors && !Object.prototype.hasOwnProperty.call(this.keyVectorLookup, keyVectorHash)) {
207      this.keyVectorLookup[keyVectorHash] = this.createVector(stackPointer.stackPosition, vecLength, 2);
208    }
209    const keysStackValue = this.dedupKeyVectors ? this.keyVectorLookup[keyVectorHash] : this.createVector(stackPointer.stackPosition, vecLength, 2);
210    const valuesStackValue = this.createVector(stackPointer.stackPosition + 1, vecLength, 2, keysStackValue);
211    this.stack.splice(stackPointer.stackPosition, vecLength << 1);
212    this.stack.push(valuesStackValue);
213  }
214
215  private sort(stackPointer: StackPointer) {
216    const view = this.view
217    const stack = this.stack
218
219    function shouldFlip(v1: StackValue, v2: StackValue) {
220      if (v1.type !== ValueType.KEY || v2.type !== ValueType.KEY) {
221        throw `Stack values are not keys ${v1} | ${v2}. Check if you combined [addKey] with add... method calls properly.`
222      }
223      let c1, c2;
224      let index = 0;
225      do {
226        c1 = view.getUint8(v1.offset + index);
227        c2 = view.getUint8(v2.offset + index);
228        if (c2 < c1) return true;
229        if (c1 < c2) return false;
230        index += 1;
231      } while (c1 !== 0 && c2 !== 0);
232      return false;
233    }
234
235    function swap(stack: Array<StackValue>, flipIndex: number, i: number) {
236      if (flipIndex === i) return;
237      const k = stack[flipIndex];
238      const v = stack[flipIndex + 1];
239      stack[flipIndex] = stack[i];
240      stack[flipIndex + 1] = stack[i + 1];
241      stack[i] = k;
242      stack[i + 1] = v;
243    }
244
245    function selectionSort() {
246      for (let i = stackPointer.stackPosition; i < stack.length; i += 2) {
247        let flipIndex = i;
248        for (let j = i + 2; j < stack.length; j += 2) {
249          if (shouldFlip(stack[flipIndex], stack[j])) {
250            flipIndex = j;
251          }
252        }
253        if (flipIndex !== i) {
254          swap(stack, flipIndex, i);
255        }
256      }
257    }
258
259    function smaller(v1: StackValue, v2: StackValue) {
260      if (v1.type !== ValueType.KEY || v2.type !== ValueType.KEY) {
261        throw `Stack values are not keys ${v1} | ${v2}. Check if you combined [addKey] with add... method calls properly.`
262      }
263      if (v1.offset === v2.offset) {
264        return false;
265      }
266      let c1, c2;
267      let index = 0;
268      do {
269        c1 = view.getUint8(v1.offset + index);
270        c2 = view.getUint8(v2.offset + index);
271        if (c1 < c2) return true;
272        if (c2 < c1) return false;
273        index += 1;
274      } while (c1 !== 0 && c2 !== 0);
275      return false;
276    }
277
278    function quickSort(left: number, right: number) {
279
280      if (left < right) {
281        const mid = left + (((right - left) >> 2)) * 2;
282        const pivot = stack[mid];
283        let left_new = left;
284        let right_new = right;
285
286        do {
287          while (smaller(stack[left_new], pivot)) {
288            left_new += 2;
289          }
290          while (smaller(pivot, stack[right_new])) {
291            right_new -= 2;
292          }
293          if (left_new <= right_new) {
294            swap(stack, left_new, right_new);
295            left_new += 2;
296            right_new -= 2;
297          }
298        } while (left_new <= right_new);
299
300        quickSort(left, right_new);
301        quickSort(left_new, right);
302
303      }
304    }
305
306    let sorted = true;
307    for (let i = stackPointer.stackPosition; i < this.stack.length - 2; i += 2) {
308      if (shouldFlip(this.stack[i], this.stack[i + 2])) {
309        sorted = false;
310        break;
311      }
312    }
313
314    if (!sorted) {
315      if (this.stack.length - stackPointer.stackPosition > 40) {
316        quickSort(stackPointer.stackPosition, this.stack.length - 2);
317      } else {
318        selectionSort();
319      }
320    }
321  }
322
323  end(): void {
324    if (this.stackPointers.length < 1) return;
325    const pointer = this.stackPointers.pop() as StackPointer;
326    if (pointer.isVector) {
327      this.endVector(pointer);
328    } else {
329      this.endMap(pointer);
330    }
331  }
332
333  private createVector(start: number, vecLength: number, step: number, keys: StackValue | null = null) {
334    let bitWidth = uwidth(vecLength);
335    let prefixElements = 1;
336    if (keys !== null) {
337      const elementWidth = keys.elementWidth(this.offset, 0);
338      if (elementWidth > bitWidth) {
339        bitWidth = elementWidth;
340      }
341      prefixElements += 2;
342    }
343    let vectorType = ValueType.KEY;
344    let typed = keys === null;
345    for (let i = start; i < this.stack.length; i += step) {
346      const elementWidth = this.stack[i].elementWidth(this.offset, i + prefixElements);
347      if (elementWidth > bitWidth) {
348        bitWidth = elementWidth;
349      }
350      if (i === start) {
351        vectorType = this.stack[i].type;
352        typed = typed && isTypedVectorElement(vectorType);
353      } else {
354        if (vectorType !== this.stack[i].type) {
355          typed = false;
356        }
357      }
358    }
359    const byteWidth = this.align(bitWidth);
360    const fix = typed && isNumber(vectorType) && vecLength >= 2 && vecLength <= 4;
361    if (keys !== null) {
362      this.writeStackValue(keys, byteWidth);
363      this.writeUInt(1 << keys.width, byteWidth);
364    }
365    if (!fix) {
366      this.writeUInt(vecLength, byteWidth);
367    }
368    const vecOffset = this.offset;
369    for (let i = start; i < this.stack.length; i += step) {
370      this.writeStackValue(this.stack[i], byteWidth);
371    }
372    if (!typed) {
373      for (let i = start; i < this.stack.length; i += step) {
374        this.writeUInt(this.stack[i].storedPackedType(), 1);
375      }
376    }
377    if (keys !== null) {
378      return this.offsetStackValue(vecOffset, ValueType.MAP, bitWidth);
379    }
380    if (typed) {
381      const vType = toTypedVector(vectorType, fix ? vecLength : 0);
382      return this.offsetStackValue(vecOffset, vType, bitWidth);
383    }
384    return this.offsetStackValue(vecOffset, ValueType.VECTOR, bitWidth);
385  }
386
387  private nullStackValue() {
388    return new StackValue(this, ValueType.NULL, BitWidth.WIDTH8);
389  }
390
391  private boolStackValue(value: boolean) {
392    return new StackValue(this, ValueType.BOOL, BitWidth.WIDTH8, value);
393  }
394
395  private intStackValue(value: number | bigint) {
396    return new StackValue(this, ValueType.INT, iwidth(value), value as number);
397  }
398
399  private uintStackValue(value: number) {
400    return new StackValue(this, ValueType.UINT, uwidth(value), value);
401  }
402
403  private floatStackValue(value: number) {
404    return new StackValue(this, ValueType.FLOAT, fwidth(value), value);
405  }
406
407  private offsetStackValue(offset: number, valueType: ValueType, bitWidth: BitWidth): StackValue {
408    return new StackValue(this, valueType, bitWidth, null, offset);
409  }
410
411  private finishBuffer() {
412    if (this.stack.length !== 1) {
413      throw `Stack has to be exactly 1, but it is ${this.stack.length}. You have to end all started vectors and maps before calling [finish]`;
414    }
415    const value = this.stack[0];
416    const byteWidth = this.align(value.elementWidth(this.offset, 0));
417    this.writeStackValue(value, byteWidth);
418    this.writeUInt(value.storedPackedType(), 1);
419    this.writeUInt(byteWidth, 1);
420    this.finished = true;
421  }
422
423  add(value: undefined | null | boolean | bigint | number | DataView | string | Array<unknown> | Record<string, unknown> | unknown): void {
424    this.integrityCheckOnValueAddition();
425    if (typeof value === 'undefined') {
426      throw "You need to provide a value";
427    }
428    if (value === null) {
429      this.stack.push(this.nullStackValue());
430    } else if (typeof value === "boolean") {
431      this.stack.push(this.boolStackValue(value));
432    } else if (typeof value === "bigint") {
433      this.stack.push(this.intStackValue(value));
434    } else if (typeof value == 'number') {
435      if (Number.isInteger(value)) {
436        this.stack.push(this.intStackValue(value));
437      } else {
438        this.stack.push(this.floatStackValue(value));
439      }
440    } else if (ArrayBuffer.isView(value)) {
441      this.writeBlob(value.buffer);
442    } else if (typeof value === 'string' || value instanceof String) {
443      this.writeString(value as string);
444    } else if (Array.isArray(value)) {
445      this.startVector();
446      for (let i = 0; i < value.length; i++) {
447        this.add(value[i]);
448      }
449      this.end();
450    } else if (typeof value === 'object') {
451      const properties = Object.getOwnPropertyNames(value).sort();
452      this.startMap(true);
453      for (let i = 0; i < properties.length; i++) {
454        const key = properties[i];
455        this.addKey(key);
456        this.add((value as Record<string, unknown>)[key]);
457      }
458      this.end();
459    } else {
460      throw `Unexpected value input ${value}`;
461    }
462  }
463
464  finish(): Uint8Array {
465    if (!this.finished) {
466      this.finishBuffer();
467    }
468    const result = this.buffer.slice(0, this.offset);
469    return new Uint8Array(result);
470  }
471
472  isFinished(): boolean {
473    return this.finished;
474  }
475
476  addKey(key: string): void {
477    this.integrityCheckOnKeyAddition();
478    this.writeKey(key);
479  }
480
481  addInt(value: number, indirect = false, deduplicate = false): void {
482    this.integrityCheckOnValueAddition();
483    if (!indirect) {
484      this.stack.push(this.intStackValue(value));
485      return;
486    }
487    if (deduplicate && Object.prototype.hasOwnProperty.call(this.indirectIntLookup, value)) {
488      this.stack.push(this.indirectIntLookup[value]);
489      return;
490    }
491    const stackValue = this.intStackValue(value);
492    const byteWidth = this.align(stackValue.width);
493    const newOffset = this.computeOffset(byteWidth);
494    const valueOffset = this.offset;
495    stackValue.writeToBuffer(byteWidth);
496    const stackOffset = this.offsetStackValue(valueOffset, ValueType.INDIRECT_INT, stackValue.width);
497    this.stack.push(stackOffset);
498    this.offset = newOffset;
499    if (deduplicate) {
500      this.indirectIntLookup[value] = stackOffset;
501    }
502  }
503
504  addUInt(value: number, indirect = false, deduplicate = false): void {
505    this.integrityCheckOnValueAddition();
506    if (!indirect) {
507      this.stack.push(this.uintStackValue(value));
508      return;
509    }
510    if (deduplicate && Object.prototype.hasOwnProperty.call(this.indirectUIntLookup, value)) {
511      this.stack.push(this.indirectUIntLookup[value]);
512      return;
513    }
514    const stackValue = this.uintStackValue(value);
515    const byteWidth = this.align(stackValue.width);
516    const newOffset = this.computeOffset(byteWidth);
517    const valueOffset = this.offset;
518    stackValue.writeToBuffer(byteWidth);
519    const stackOffset = this.offsetStackValue(valueOffset, ValueType.INDIRECT_UINT, stackValue.width);
520    this.stack.push(stackOffset);
521    this.offset = newOffset;
522    if (deduplicate) {
523      this.indirectUIntLookup[value] = stackOffset;
524    }
525  }
526
527  addFloat(value: number, indirect = false, deduplicate = false): void {
528    this.integrityCheckOnValueAddition();
529    if (!indirect) {
530      this.stack.push(this.floatStackValue(value));
531      return;
532    }
533    if (deduplicate && Object.prototype.hasOwnProperty.call(this.indirectFloatLookup, value)) {
534      this.stack.push(this.indirectFloatLookup[value]);
535      return;
536    }
537    const stackValue = this.floatStackValue(value);
538    const byteWidth = this.align(stackValue.width);
539    const newOffset = this.computeOffset(byteWidth);
540    const valueOffset = this.offset;
541    stackValue.writeToBuffer(byteWidth);
542    const stackOffset = this.offsetStackValue(valueOffset, ValueType.INDIRECT_FLOAT, stackValue.width);
543    this.stack.push(stackOffset);
544    this.offset = newOffset;
545    if (deduplicate) {
546      this.indirectFloatLookup[value] = stackOffset;
547    }
548  }
549}
550