mirror of
https://github.com/bitwarden/browser
synced 2025-12-12 06:13:38 +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);
|
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 buffer = new Uint8Array([]).buffer;
|
||||||
const b64String = Utils.fromBufferToB64(buffer);
|
const b64String = Utils.fromBufferToB64(buffer);
|
||||||
expect(b64String).toBe("");
|
expect(b64String).toBe("");
|
||||||
@@ -312,6 +312,81 @@ describe("Utils Service", () => {
|
|||||||
const b64String = Utils.fromBufferToB64(null);
|
const b64String = Utils.fromBufferToB64(null);
|
||||||
expect(b64String).toBeNull();
|
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(...)", () => {
|
describe("fromB64ToArray(...)", () => {
|
||||||
|
|||||||
@@ -128,15 +128,52 @@ export class Utils {
|
|||||||
return arr;
|
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) {
|
if (buffer == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const bytes: Uint8Array = Utils.normalizeToUint8Array(buffer);
|
||||||
|
|
||||||
|
// Handle empty input
|
||||||
|
if (bytes.length === 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
if (Utils.isNode) {
|
if (Utils.isNode) {
|
||||||
return Buffer.from(buffer).toString("base64");
|
return Buffer.from(bytes).toString("base64");
|
||||||
} else {
|
} else {
|
||||||
let binary = "";
|
let binary = "";
|
||||||
const bytes = new Uint8Array(buffer);
|
|
||||||
for (let i = 0; i < bytes.byteLength; i++) {
|
for (let i = 0; i < bytes.byteLength; i++) {
|
||||||
binary += String.fromCharCode(bytes[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 {
|
static fromBufferToUrlB64(buffer: ArrayBuffer): string {
|
||||||
return Utils.fromB64toUrlB64(Utils.fromBufferToB64(buffer));
|
return Utils.fromB64toUrlB64(Utils.fromBufferToB64(buffer));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user