Skip to content

Commit 18a4051

Browse files
committed
chore(util-base64): add browser implementation
1 parent 36a3e1a commit 18a4051

File tree

7 files changed

+151
-0
lines changed

7 files changed

+151
-0
lines changed

packages/util-base64/jest.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ const base = require("../../jest.config.base.js");
22

33
module.exports = {
44
...base,
5+
testMatch: ["**/*.spec.ts"],
56
};

packages/util-base64/package.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,14 @@
4646
"files": [
4747
"dist-*"
4848
],
49+
"browser": {
50+
"./dist-es/fromBase64": "./dist-es/fromBase64.browser",
51+
"./dist-es/toBase64": "./dist-es/toBase64.browser"
52+
},
53+
"react-native": {
54+
"./dist-es/fromBase64": "./dist-es/fromBase64.browser",
55+
"./dist-es/toBase64": "./dist-es/toBase64.browser"
56+
},
4957
"homepage": "https://github.com/aws/aws-sdk-js-v3/tree/main/packages/util-base64",
5058
"repository": {
5159
"type": "git",
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
const alphabetByEncoding: Record<string, number> = {};
2+
const alphabetByValue: Array<string> = new Array(64);
3+
4+
for (let i = 0, start = "A".charCodeAt(0), limit = "Z".charCodeAt(0); i + start <= limit; i++) {
5+
const char = String.fromCharCode(i + start);
6+
alphabetByEncoding[char] = i;
7+
alphabetByValue[i] = char;
8+
}
9+
10+
for (let i = 0, start = "a".charCodeAt(0), limit = "z".charCodeAt(0); i + start <= limit; i++) {
11+
const char = String.fromCharCode(i + start);
12+
const index = i + 26;
13+
alphabetByEncoding[char] = index;
14+
alphabetByValue[index] = char;
15+
}
16+
17+
for (let i = 0; i < 10; i++) {
18+
alphabetByEncoding[i.toString(10)] = i + 52;
19+
const char = i.toString(10);
20+
const index = i + 52;
21+
alphabetByEncoding[char] = index;
22+
alphabetByValue[index] = char;
23+
}
24+
25+
alphabetByEncoding["+"] = 62;
26+
alphabetByValue[62] = "+";
27+
alphabetByEncoding["/"] = 63;
28+
alphabetByValue[63] = "/";
29+
30+
const bitsPerLetter = 6;
31+
const bitsPerByte = 8;
32+
const maxLetterValue = 0b111111;
33+
34+
export { alphabetByEncoding, alphabetByValue, bitsPerLetter, bitsPerByte, maxLetterValue };
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* @jest-environment jsdom
3+
*/
4+
import testCases from "./__mocks__/testCases.json";
5+
import { fromBase64 } from "./fromBase64.browser";
6+
7+
describe(fromBase64.name, () => {
8+
it.each(testCases as Array<[string, string, number[]]>)("%s", (desc, encoded, decoded) => {
9+
expect(fromBase64(encoded)).toEqual(new Uint8Array(decoded));
10+
});
11+
12+
it("should throw when given a number", () => {
13+
expect(() => fromBase64(0xdeadbeefface as any)).toThrow();
14+
});
15+
16+
describe("should reject invalid base64 strings", () => {
17+
it.each(["Rg", "Rg=", "[][]", "-_=="])("rejects '%s'", (value) => {
18+
expect(() => fromBase64(value)).toThrowError();
19+
});
20+
});
21+
});
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { alphabetByEncoding, bitsPerByte, bitsPerLetter } from "./constants.browser";
2+
3+
/**
4+
* Converts a base-64 encoded string to a Uint8Array of bytes.
5+
*
6+
* @param input The base-64 encoded string
7+
*
8+
* @see https://tools.ietf.org/html/rfc4648#section-4
9+
*/
10+
export const fromBase64 = (input: string): Uint8Array => {
11+
let totalByteLength = (input.length / 4) * 3;
12+
if (input.slice(-2) === "==") {
13+
totalByteLength -= 2;
14+
} else if (input.slice(-1) === "=") {
15+
totalByteLength--;
16+
}
17+
const out = new ArrayBuffer(totalByteLength);
18+
const dataView = new DataView(out);
19+
for (let i = 0; i < input.length; i += 4) {
20+
let bits = 0;
21+
let bitLength = 0;
22+
for (let j = i, limit = i + 3; j <= limit; j++) {
23+
if (input[j] !== "=") {
24+
// If we don't check for this, we'll end up using undefined in a bitwise
25+
// operation, in which it will be treated as 0.
26+
if (!(input[j] in alphabetByEncoding)) {
27+
throw new TypeError(`Invalid character ${input[j]} in base64 string.`);
28+
}
29+
bits |= alphabetByEncoding[input[j]] << ((limit - j) * bitsPerLetter);
30+
bitLength += bitsPerLetter;
31+
} else {
32+
bits >>= bitsPerLetter;
33+
}
34+
}
35+
36+
const chunkOffset = (i / 4) * 3;
37+
bits >>= bitLength % bitsPerByte;
38+
const byteLength = Math.floor(bitLength / bitsPerByte);
39+
for (let k = 0; k < byteLength; k++) {
40+
const offset = (byteLength - k - 1) * bitsPerByte;
41+
dataView.setUint8(chunkOffset + k, (bits & (255 << offset)) >> offset);
42+
}
43+
}
44+
45+
return new Uint8Array(out);
46+
};
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* @jest-environment jsdom
3+
*/
4+
import testCases from "./__mocks__/testCases.json";
5+
import { toBase64 } from "./toBase64.browser";
6+
7+
describe(toBase64.name, () => {
8+
it.each(testCases as Array<[string, string, number[]]>)("%s", (desc, encoded, decoded) => {
9+
expect(toBase64(new Uint8Array(decoded))).toEqual(encoded);
10+
});
11+
});
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { alphabetByValue, bitsPerByte, bitsPerLetter, maxLetterValue } from "./constants.browser";
2+
/**
3+
* Converts a Uint8Array of binary data to a base-64 encoded string.
4+
*
5+
* @param input The binary data to encode
6+
*
7+
* @see https://tools.ietf.org/html/rfc4648#section-4
8+
*/
9+
export function toBase64(input: Uint8Array): string {
10+
let str = "";
11+
for (let i = 0; i < input.length; i += 3) {
12+
let bits = 0;
13+
let bitLength = 0;
14+
for (let j = i, limit = Math.min(i + 3, input.length); j < limit; j++) {
15+
bits |= input[j] << ((limit - j - 1) * bitsPerByte);
16+
bitLength += bitsPerByte;
17+
}
18+
19+
const bitClusterCount = Math.ceil(bitLength / bitsPerLetter);
20+
bits <<= bitClusterCount * bitsPerLetter - bitLength;
21+
for (let k = 1; k <= bitClusterCount; k++) {
22+
const offset = (bitClusterCount - k) * bitsPerLetter;
23+
str += alphabetByValue[(bits & (maxLetterValue << offset)) >> offset];
24+
}
25+
26+
str += "==".slice(0, 4 - bitClusterCount);
27+
}
28+
29+
return str;
30+
}

0 commit comments

Comments
 (0)