1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +00:00

feat(Utils.fromBufferToB64): [Platform/PM-26186] Add type safety and ArrayBufferView support + tests (#16609)

* PM-26186 - Utils.ts - fromBufferToB64 - (1) Add type safety (2) Add ArrayBufferView support (3) Add tests

* PM-26186 - Utils.ts - add overloads so that we can specify callers who pass defined buffers will always get a string back so I don't have to modify all call sites to add a null assertion or  "as string"
This commit is contained in:
Jared Snider
2025-09-30 07:53:10 -04:00
committed by GitHub
parent cae01c6e0f
commit 2ccd841f58
2 changed files with 140 additions and 4 deletions

View File

@@ -302,7 +302,7 @@ describe("Utils Service", () => {
expect(b64String).toBe(b64HelloWorldString);
});
runInBothEnvironments("should return an empty string for an empty ArrayBuffer", () => {
runInBothEnvironments("should return empty string for an empty ArrayBuffer", () => {
const buffer = new Uint8Array([]).buffer;
const b64String = Utils.fromBufferToB64(buffer);
expect(b64String).toBe("");
@@ -312,6 +312,81 @@ describe("Utils Service", () => {
const b64String = Utils.fromBufferToB64(null);
expect(b64String).toBeNull();
});
runInBothEnvironments("returns null for undefined input", () => {
const b64 = Utils.fromBufferToB64(undefined as unknown as ArrayBuffer);
expect(b64).toBeNull();
});
runInBothEnvironments("returns empty string for empty input", () => {
const b64 = Utils.fromBufferToB64(new ArrayBuffer(0));
expect(b64).toBe("");
});
runInBothEnvironments("accepts Uint8Array directly", () => {
const u8 = new Uint8Array(asciiHelloWorldArray);
const b64 = Utils.fromBufferToB64(u8);
expect(b64).toBe(b64HelloWorldString);
});
runInBothEnvironments("respects byteOffset/byteLength (view window)", () => {
// [xx, 'hello world', yy] — view should only encode the middle slice
const prefix = [1, 2, 3];
const suffix = [4, 5];
const all = new Uint8Array([...prefix, ...asciiHelloWorldArray, ...suffix]);
const view = new Uint8Array(all.buffer, prefix.length, asciiHelloWorldArray.length);
const b64 = Utils.fromBufferToB64(view);
expect(b64).toBe(b64HelloWorldString);
});
runInBothEnvironments("handles DataView (ArrayBufferView other than Uint8Array)", () => {
const u8 = new Uint8Array(asciiHelloWorldArray);
const dv = new DataView(u8.buffer, 0, u8.byteLength);
const b64 = Utils.fromBufferToB64(dv);
expect(b64).toBe(b64HelloWorldString);
});
runInBothEnvironments("handles DataView with offset/length window", () => {
// Buffer: [xx, 'hello world', yy]
const prefix = [9, 9, 9];
const suffix = [8, 8];
const all = new Uint8Array([...prefix, ...asciiHelloWorldArray, ...suffix]);
// DataView over just the "hello world" window
const dv = new DataView(all.buffer, prefix.length, asciiHelloWorldArray.length);
const b64 = Utils.fromBufferToB64(dv);
expect(b64).toBe(b64HelloWorldString);
});
runInBothEnvironments(
"encodes empty view (offset-length window of zero) as empty string",
() => {
const backing = new Uint8Array([1, 2, 3, 4]);
const emptyView = new Uint8Array(backing.buffer, 2, 0);
const b64 = Utils.fromBufferToB64(emptyView);
expect(b64).toBe("");
},
);
runInBothEnvironments("does not mutate the input", () => {
const original = new Uint8Array(asciiHelloWorldArray);
const copyBefore = new Uint8Array(original); // snapshot
Utils.fromBufferToB64(original);
expect(original).toEqual(copyBefore); // unchanged
});
it("produces the same Base64 in Node vs non-Node mode", () => {
const bytes = new Uint8Array(asciiHelloWorldArray);
Utils.isNode = true;
const nodeB64 = Utils.fromBufferToB64(bytes);
Utils.isNode = false;
const browserB64 = Utils.fromBufferToB64(bytes);
expect(browserB64).toBe(nodeB64);
});
});
describe("fromB64ToArray(...)", () => {

View File

@@ -128,15 +128,52 @@ export class Utils {
return arr;
}
static fromBufferToB64(buffer: ArrayBuffer): string {
/**
* Convert binary data into a Base64 string.
*
* Overloads are provided for two categories of input:
*
* 1. ArrayBuffer
* - A raw, fixed-length chunk of memory (no element semantics).
* - Example: `const buf = new ArrayBuffer(16);`
*
* 2. ArrayBufferView
* - A *view* onto an existing buffer that gives the bytes meaning.
* - Examples: Uint8Array, Int32Array, DataView, etc.
* - Views can expose only a *window* of the underlying buffer via
* `byteOffset` and `byteLength`.
* Example:
* ```ts
* const buf = new ArrayBuffer(8);
* const full = new Uint8Array(buf); // sees all 8 bytes
* const half = new Uint8Array(buf, 4, 4); // sees only last 4 bytes
* ```
*
* Returns:
* - Base64 string for non-empty inputs,
* - null if `buffer` is `null` or `undefined`
* - empty string if `buffer` is empty (0 bytes)
*/
static fromBufferToB64(buffer: null | undefined): null;
static fromBufferToB64(buffer: ArrayBuffer): string;
static fromBufferToB64(buffer: ArrayBufferView): string;
static fromBufferToB64(buffer: ArrayBuffer | ArrayBufferView | null | undefined): string | null {
// Handle null / undefined input
if (buffer == null) {
return null;
}
const bytes: Uint8Array = Utils.normalizeToUint8Array(buffer);
// Handle empty input
if (bytes.length === 0) {
return "";
}
if (Utils.isNode) {
return Buffer.from(buffer).toString("base64");
return Buffer.from(bytes).toString("base64");
} else {
let binary = "";
const bytes = new Uint8Array(buffer);
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
@@ -144,6 +181,30 @@ export class Utils {
}
}
/**
* Normalizes input into a Uint8Array so we always have a uniform,
* byte-level view of the data. This avoids dealing with differences
* between ArrayBuffer (raw memory with no indexing) and other typed
* views (which may have element sizes, offsets, and lengths).
* @param buffer ArrayBuffer or ArrayBufferView (e.g. Uint8Array, DataView, etc.)
*/
private static normalizeToUint8Array(buffer: ArrayBuffer | ArrayBufferView): Uint8Array {
/**
* 1) Uint8Array: already bytes → use directly.
* 2) ArrayBuffer: wrap whole buffer.
* 3) Other ArrayBufferView (e.g., DataView, Int32Array):
* wrap the views window (byteOffset..byteOffset+byteLength).
*/
if (buffer instanceof Uint8Array) {
return buffer;
} else if (buffer instanceof ArrayBuffer) {
return new Uint8Array(buffer);
} else {
const view = buffer as ArrayBufferView;
return new Uint8Array(view.buffer, view.byteOffset, view.byteLength);
}
}
static fromBufferToUrlB64(buffer: ArrayBuffer): string {
return Utils.fromB64toUrlB64(Utils.fromBufferToB64(buffer));
}