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:
@@ -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(...)", () => {
|
||||
|
||||
@@ -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 view’s 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));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user