Skip to content

Commit

Permalink
fix: don't reference globalThis.Buffer when env=browser (#967) (#999)
Browse files Browse the repository at this point in the history
The generated bytesFromBase64 and base64FromBytes functions now only include code that's required for the specified env:
For env=node, the functions now exclusively use globalThis.Buffer to de-/encode to and from JSON
For env=browser, globalThis.btoa/atob is used
For env=both, the two functions use either the node or browser implementations depending on whether globalThis.Buffer exists
  • Loading branch information
cmd-johnson authored Feb 15, 2024
1 parent 061eaf8 commit 0d34612
Show file tree
Hide file tree
Showing 13 changed files with 271 additions and 57 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Message } from './message';

describe('bytes-as-base64', () => {
type TestData = [string, Uint8Array];
const testData: TestData[] = [
['3q2+7w==', Uint8Array.from([0xDE, 0xAD, 0xBE, 0xEF])],
['AAAAAAAAAAAAAAAAAAAAAA==', new Uint8Array(16).fill(0x00)],
['/////////////////////w==', new Uint8Array(16).fill(0xFF)],
['AAECAwQFBgcICQoLDA0ODw==', new Uint8Array(16).map((_, i) => i)],
];

it('fromJSON can decode bytes from base64', () => {
for (const entry of testData) {
const message = Message.fromJSON({ data: entry[0] });
expect(message).toEqual({ data: entry[1] });
}
});

it('toJSON can encode bytes as base64', () => {
for (const entry of testData) {
const message = Message.toJSON({ data: entry[1] });
expect(message).toEqual({ data: entry[0] });
}
});

it('fromJSON and toJSON can handle "large" bytes fields', () => {
const LENGTH = 1000000; // 1 MB
const messageA = { data: new Uint8Array(LENGTH).fill(0xFF) };
const json = Message.toJSON(messageA);
expect(json).toHaveProperty('data');
const messageB = Message.fromJSON(json);
expect(messageA).toEqual(messageB);
});
});
Binary file added integration/bytes-as-base64-browser/message.bin
Binary file not shown.
5 changes: 5 additions & 0 deletions integration/bytes-as-base64-browser/message.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
syntax = "proto3";

message Message {
bytes data = 1;
}
67 changes: 67 additions & 0 deletions integration/bytes-as-base64-browser/message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/* eslint-disable */

export const protobufPackage = "";

export interface Message {
data: Uint8Array;
}

function createBaseMessage(): Message {
return { data: new Uint8Array(0) };
}

export const Message = {
fromJSON(object: any): Message {
return { data: isSet(object.data) ? bytesFromBase64(object.data) : new Uint8Array(0) };
},

toJSON(message: Message): unknown {
const obj: any = {};
if (message.data.length !== 0) {
obj.data = base64FromBytes(message.data);
}
return obj;
},

create<I extends Exact<DeepPartial<Message>, I>>(base?: I): Message {
return Message.fromPartial(base ?? ({} as any));
},
fromPartial<I extends Exact<DeepPartial<Message>, I>>(object: I): Message {
const message = createBaseMessage();
message.data = object.data ?? new Uint8Array(0);
return message;
},
};

function bytesFromBase64(b64: string): Uint8Array {
const bin = globalThis.atob(b64);
const arr = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; ++i) {
arr[i] = bin.charCodeAt(i);
}
return arr;
}

function base64FromBytes(arr: Uint8Array): string {
const bin: string[] = [];
arr.forEach((byte) => {
bin.push(globalThis.String.fromCharCode(byte));
});
return globalThis.btoa(bin.join(""));
}

type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;

export type DeepPartial<T> = T extends Builtin ? T
: T extends globalThis.Array<infer U> ? globalThis.Array<DeepPartial<U>>
: T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>
: T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> }
: Partial<T>;

type KeysOfUnion<T> = T extends T ? keyof T : never;
export type Exact<P, I extends P> = P extends Builtin ? P
: P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never };

function isSet(value: any): boolean {
return value !== null && value !== undefined;
}
1 change: 1 addition & 0 deletions integration/bytes-as-base64-browser/parameters.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
outputEncodeMethods=false,outputJsonMethods=true,env=browser
34 changes: 34 additions & 0 deletions integration/bytes-as-base64-node/bytes-as-base64-node-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Message } from './message';

describe('bytes-as-base64', () => {
type TestData = [string, Buffer];
const testData: TestData[] = [
['3q2+7w==', Buffer.from([0xDE, 0xAD, 0xBE, 0xEF])],
['AAAAAAAAAAAAAAAAAAAAAA==', Buffer.alloc(16).fill(0x00)],
['/////////////////////w==', Buffer.alloc(16).fill(0xFF)],
['AAECAwQFBgcICQoLDA0ODw==', Buffer.from(Array.from({length: 16}).map((_, i) => i))],
];

it('fromJSON can decode bytes from base64', () => {
for (const entry of testData) {
const message = Message.fromJSON({ data: entry[0] });
expect(message).toEqual({ data: entry[1] });
}
});

it('toJSON can encode bytes as base64', () => {
for (const entry of testData) {
const message = Message.toJSON({ data: entry[1] });
expect(message).toEqual({ data: entry[0] });
}
});

it('fromJSON and toJSON can handle "large" bytes fields', () => {
const LENGTH = 1000000; // 1 MB
const messageA = { data: Buffer.alloc(LENGTH).fill(0xFF) };
const json = Message.toJSON(messageA);
expect(json).toHaveProperty('data');
const messageB = Message.fromJSON(json);
expect(messageA).toEqual(messageB);
});
});
Binary file added integration/bytes-as-base64-node/message.bin
Binary file not shown.
5 changes: 5 additions & 0 deletions integration/bytes-as-base64-node/message.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
syntax = "proto3";

message Message {
bytes data = 1;
}
58 changes: 58 additions & 0 deletions integration/bytes-as-base64-node/message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/* eslint-disable */

export const protobufPackage = "";

export interface Message {
data: Buffer;
}

function createBaseMessage(): Message {
return { data: Buffer.alloc(0) };
}

export const Message = {
fromJSON(object: any): Message {
return { data: isSet(object.data) ? Buffer.from(bytesFromBase64(object.data)) : Buffer.alloc(0) };
},

toJSON(message: Message): unknown {
const obj: any = {};
if (message.data.length !== 0) {
obj.data = base64FromBytes(message.data);
}
return obj;
},

create<I extends Exact<DeepPartial<Message>, I>>(base?: I): Message {
return Message.fromPartial(base ?? ({} as any));
},
fromPartial<I extends Exact<DeepPartial<Message>, I>>(object: I): Message {
const message = createBaseMessage();
message.data = object.data ?? Buffer.alloc(0);
return message;
},
};

function bytesFromBase64(b64: string): Uint8Array {
return Uint8Array.from(globalThis.Buffer.from(b64, "base64"));
}

function base64FromBytes(arr: Uint8Array): string {
return globalThis.Buffer.from(arr).toString("base64");
}

type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;

export type DeepPartial<T> = T extends Builtin ? T
: T extends globalThis.Array<infer U> ? globalThis.Array<DeepPartial<U>>
: T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>
: T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> }
: Partial<T>;

type KeysOfUnion<T> = T extends T ? keyof T : never;
export type Exact<P, I extends P> = P extends Builtin ? P
: P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never };

function isSet(value: any): boolean {
return value !== null && value !== undefined;
}
1 change: 1 addition & 0 deletions integration/bytes-as-base64-node/parameters.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
outputEncodeMethods=false,outputJsonMethods=true,env=node
21 changes: 2 additions & 19 deletions integration/bytes-node/google/protobuf/wrappers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -608,28 +608,11 @@ export const BytesValue = {
};

function bytesFromBase64(b64: string): Uint8Array {
if (globalThis.Buffer) {
return Uint8Array.from(globalThis.Buffer.from(b64, "base64"));
} else {
const bin = globalThis.atob(b64);
const arr = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; ++i) {
arr[i] = bin.charCodeAt(i);
}
return arr;
}
return Uint8Array.from(globalThis.Buffer.from(b64, "base64"));
}

function base64FromBytes(arr: Uint8Array): string {
if (globalThis.Buffer) {
return globalThis.Buffer.from(arr).toString("base64");
} else {
const bin: string[] = [];
arr.forEach((byte) => {
bin.push(globalThis.String.fromCharCode(byte));
});
return globalThis.btoa(bin.join(""));
}
return globalThis.Buffer.from(arr).toString("base64");
}

type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;
Expand Down
21 changes: 2 additions & 19 deletions integration/bytes-node/point.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,28 +84,11 @@ export const Point = {
};

function bytesFromBase64(b64: string): Uint8Array {
if (globalThis.Buffer) {
return Uint8Array.from(globalThis.Buffer.from(b64, "base64"));
} else {
const bin = globalThis.atob(b64);
const arr = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; ++i) {
arr[i] = bin.charCodeAt(i);
}
return arr;
}
return Uint8Array.from(globalThis.Buffer.from(b64, "base64"));
}

function base64FromBytes(arr: Uint8Array): string {
if (globalThis.Buffer) {
return globalThis.Buffer.from(arr).toString("base64");
} else {
const bin: string[] = [];
arr.forEach((byte) => {
bin.push(globalThis.String.fromCharCode(byte));
});
return globalThis.btoa(bin.join(""));
}
return globalThis.Buffer.from(arr).toString("base64");
}

type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;
Expand Down
81 changes: 62 additions & 19 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -550,36 +550,79 @@ function makeByteUtils(options: Options) {
);
const globalThis = options.globalThisPolyfill ? globalThisPolyfill : conditionalOutput("globalThis", code``);

function getBytesFromBase64Snippet() {
const bytesFromBase64NodeSnippet = code`
return Uint8Array.from(${globalThis}.Buffer.from(b64, 'base64'));
`;

const bytesFromBase64BrowserSnippet = code`
const bin = ${globalThis}.atob(b64);
const arr = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; ++i) {
arr[i] = bin.charCodeAt(i);
}
return arr;
`;

switch (options.env) {
case EnvOption.NODE:
return bytesFromBase64NodeSnippet;
case EnvOption.BROWSER:
return bytesFromBase64BrowserSnippet;
default:
return code`
if (${globalThis}.Buffer) {
${bytesFromBase64NodeSnippet}
} else {
${bytesFromBase64BrowserSnippet}
}
`;
}
}

const bytesFromBase64 = conditionalOutput(
"bytesFromBase64",
code`
function bytesFromBase64(b64: string): Uint8Array {
if (${globalThis}.Buffer) {
return Uint8Array.from(${globalThis}.Buffer.from(b64, 'base64'));
} else {
const bin = ${globalThis}.atob(b64);
const arr = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; ++i) {
arr[i] = bin.charCodeAt(i);
}
return arr;
}
${getBytesFromBase64Snippet()}
}
`,
);

function getBase64FromBytesSnippet() {
const base64FromBytesNodeSnippet = code`
return ${globalThis}.Buffer.from(arr).toString('base64');
`;

const base64FromBytesBrowserSnippet = code`
const bin: string[] = [];
arr.forEach((byte) => {
bin.push(${globalThis}.String.fromCharCode(byte));
});
return ${globalThis}.btoa(bin.join(''));
`;

switch (options.env) {
case EnvOption.NODE:
return base64FromBytesNodeSnippet;
case EnvOption.BROWSER:
return base64FromBytesBrowserSnippet;
default:
return code`
if (${globalThis}.Buffer) {
${base64FromBytesNodeSnippet}
} else {
${base64FromBytesBrowserSnippet}
}
`;
}
}

const base64FromBytes = conditionalOutput(
"base64FromBytes",
code`
function base64FromBytes(arr: Uint8Array): string {
if (${globalThis}.Buffer) {
return ${globalThis}.Buffer.from(arr).toString('base64')
} else {
const bin: string[] = [];
arr.forEach((byte) => {
bin.push(${globalThis}.String.fromCharCode(byte));
});
return ${globalThis}.btoa(bin.join(''));
}
${getBase64FromBytesSnippet()}
}
`,
);
Expand Down

0 comments on commit 0d34612

Please sign in to comment.