diff --git a/integration/bytes-as-base64-browser/bytes-as-base64-browser-test.ts b/integration/bytes-as-base64-browser/bytes-as-base64-browser-test.ts new file mode 100644 index 000000000..a344cf423 --- /dev/null +++ b/integration/bytes-as-base64-browser/bytes-as-base64-browser-test.ts @@ -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); + }); +}); diff --git a/integration/bytes-as-base64-browser/message.bin b/integration/bytes-as-base64-browser/message.bin new file mode 100644 index 000000000..b035d9e09 Binary files /dev/null and b/integration/bytes-as-base64-browser/message.bin differ diff --git a/integration/bytes-as-base64-browser/message.proto b/integration/bytes-as-base64-browser/message.proto new file mode 100644 index 000000000..31f490b2e --- /dev/null +++ b/integration/bytes-as-base64-browser/message.proto @@ -0,0 +1,5 @@ +syntax = "proto3"; + +message Message { + bytes data = 1; +} diff --git a/integration/bytes-as-base64-browser/message.ts b/integration/bytes-as-base64-browser/message.ts new file mode 100644 index 000000000..87632f9e7 --- /dev/null +++ b/integration/bytes-as-base64-browser/message.ts @@ -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>>(base?: I): Message { + return Message.fromPartial(base ?? ({} as any)); + }, + fromPartial, 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 extends Builtin ? T + : T extends globalThis.Array ? globalThis.Array> + : T extends ReadonlyArray ? ReadonlyArray> + : T extends {} ? { [K in keyof T]?: DeepPartial } + : Partial; + +type KeysOfUnion = T extends T ? keyof T : never; +export type Exact = P extends Builtin ? P + : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; + +function isSet(value: any): boolean { + return value !== null && value !== undefined; +} diff --git a/integration/bytes-as-base64-browser/parameters.txt b/integration/bytes-as-base64-browser/parameters.txt new file mode 100644 index 000000000..e64126ab8 --- /dev/null +++ b/integration/bytes-as-base64-browser/parameters.txt @@ -0,0 +1 @@ +outputEncodeMethods=false,outputJsonMethods=true,env=browser diff --git a/integration/bytes-as-base64-node/bytes-as-base64-node-test.ts b/integration/bytes-as-base64-node/bytes-as-base64-node-test.ts new file mode 100644 index 000000000..a238c8059 --- /dev/null +++ b/integration/bytes-as-base64-node/bytes-as-base64-node-test.ts @@ -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); + }); +}); diff --git a/integration/bytes-as-base64-node/message.bin b/integration/bytes-as-base64-node/message.bin new file mode 100644 index 000000000..b035d9e09 Binary files /dev/null and b/integration/bytes-as-base64-node/message.bin differ diff --git a/integration/bytes-as-base64-node/message.proto b/integration/bytes-as-base64-node/message.proto new file mode 100644 index 000000000..31f490b2e --- /dev/null +++ b/integration/bytes-as-base64-node/message.proto @@ -0,0 +1,5 @@ +syntax = "proto3"; + +message Message { + bytes data = 1; +} diff --git a/integration/bytes-as-base64-node/message.ts b/integration/bytes-as-base64-node/message.ts new file mode 100644 index 000000000..ec2a9bee2 --- /dev/null +++ b/integration/bytes-as-base64-node/message.ts @@ -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>>(base?: I): Message { + return Message.fromPartial(base ?? ({} as any)); + }, + fromPartial, 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 extends Builtin ? T + : T extends globalThis.Array ? globalThis.Array> + : T extends ReadonlyArray ? ReadonlyArray> + : T extends {} ? { [K in keyof T]?: DeepPartial } + : Partial; + +type KeysOfUnion = T extends T ? keyof T : never; +export type Exact = P extends Builtin ? P + : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; + +function isSet(value: any): boolean { + return value !== null && value !== undefined; +} diff --git a/integration/bytes-as-base64-node/parameters.txt b/integration/bytes-as-base64-node/parameters.txt new file mode 100644 index 000000000..08e1ed94e --- /dev/null +++ b/integration/bytes-as-base64-node/parameters.txt @@ -0,0 +1 @@ +outputEncodeMethods=false,outputJsonMethods=true,env=node diff --git a/integration/bytes-node/google/protobuf/wrappers.ts b/integration/bytes-node/google/protobuf/wrappers.ts index 79a5e79e4..a7a984eb8 100644 --- a/integration/bytes-node/google/protobuf/wrappers.ts +++ b/integration/bytes-node/google/protobuf/wrappers.ts @@ -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; diff --git a/integration/bytes-node/point.ts b/integration/bytes-node/point.ts index 8bb3222a0..d2bce9d09 100644 --- a/integration/bytes-node/point.ts +++ b/integration/bytes-node/point.ts @@ -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; diff --git a/src/main.ts b/src/main.ts index 36f998e7e..c4f3e1eb2 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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()} } `, );