1// Copyright (C) 2022 The Android Open Source Project 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15import {length as utf8Len, write as utf8Write} from '@protobufjs/utf8'; 16import {assertTrue} from '../base/logging'; 17import {isString} from '../base/object_utils'; 18 19// A token that can be appended to an `ArrayBufferBuilder`. 20export type ArrayBufferToken = string | number | Uint8Array; 21 22// Return the length, in bytes, of a token to be inserted. 23function tokenLength(token: ArrayBufferToken): number { 24 if (isString(token)) { 25 return utf8Len(token); 26 } else if (token instanceof Uint8Array) { 27 return token.byteLength; 28 } else { 29 assertTrue(token >= 0 && token <= 0xffffffff); 30 // 32-bit integers take 4 bytes 31 return 4; 32 } 33} 34 35// Insert a token into the buffer, at position `byteOffset`. 36// 37// @param dataView A DataView into the buffer to write into. 38// @param typedArray A Uint8Array view into the buffer to write into. 39// @param byteOffset Position to write at, in the buffer. 40// @param token Token to insert into the buffer. 41function insertToken( 42 dataView: DataView, 43 typedArray: Uint8Array, 44 byteOffset: number, 45 token: ArrayBufferToken, 46): void { 47 if (isString(token)) { 48 // Encode the string in UTF-8 49 const written = utf8Write(token, typedArray, byteOffset); 50 assertTrue(written === utf8Len(token)); 51 } else if (token instanceof Uint8Array) { 52 // Copy the bytes from the other array 53 typedArray.set(token, byteOffset); 54 } else { 55 assertTrue(token >= 0 && token <= 0xffffffff); 56 // 32-bit little-endian value 57 dataView.setUint32(byteOffset, token, true); 58 } 59} 60 61// Like a string builder, but for an ArrayBuffer instead of a string. This 62// allows us to assemble messages to send/receive over the wire. Data can be 63// appended to the buffer using `append()`. The data we append can be of the 64// following types: 65// 66// - string: the ASCII string is appended. Throws an error if there are 67// non-ASCII characters. 68// - number: the number is appended as a 32-bit little-endian integer. 69// - Uint8Array: the bytes are appended as-is to the buffer. 70export class ArrayBufferBuilder { 71 private readonly tokens: ArrayBufferToken[] = []; 72 73 // Return an `ArrayBuffer` that is the concatenation of all the tokens. 74 toArrayBuffer(): ArrayBuffer { 75 // Calculate the size of the buffer we need. 76 let byteLength = 0; 77 for (const token of this.tokens) { 78 byteLength += tokenLength(token); 79 } 80 // Allocate the buffer. 81 const buffer = new ArrayBuffer(byteLength); 82 const dataView = new DataView(buffer); 83 const typedArray = new Uint8Array(buffer); 84 // Fill the buffer with the tokens. 85 let byteOffset = 0; 86 for (const token of this.tokens) { 87 insertToken(dataView, typedArray, byteOffset, token); 88 byteOffset += tokenLength(token); 89 } 90 assertTrue(byteOffset === byteLength); 91 // Return the values. 92 return buffer; 93 } 94 95 // Add one or more tokens to the value of this object. 96 append(token: ArrayBufferToken): void { 97 this.tokens.push(token); 98 } 99} 100