Skip to content

Commit ae087c8

Browse files
authored
Merge pull request #12 from jeskew/feature/base64-support
Feature/base64 support
2 parents 93fad60 + 8a45ba2 commit ae087c8

File tree

17 files changed

+466
-0
lines changed

17 files changed

+466
-0
lines changed

packages/types/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ export * from "./http";
33
export * from "./crypto";
44
export * from "./protocol";
55
export * from "./response";
6+
export * from "./util";

packages/types/util.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/**
2+
* A function that, given a TypedArray of bytes, can produce a string
3+
* representation thereof.
4+
*
5+
* @example An encoder function that converts bytes to hexadecimal
6+
* representation would return `'deadbeef'` when given `new
7+
* Uint8Array([0xde, 0xad, 0xbe, 0xef])`.
8+
*/
9+
export interface Encoder {
10+
(input: Uint8Array): string;
11+
}
12+
13+
/**
14+
* A function that, given a string, can derive the bytes represented by that
15+
* string.
16+
*
17+
* @example A decoder function that converts bytes to hexadecimal
18+
* representation would return `new Uint8Array([0xde, 0xad, 0xbe, 0xef])` when
19+
* given the string `'deadbeef'`.
20+
*/
21+
export interface Decoder {
22+
(input: string): Uint8Array;
23+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/node_modules/
2+
*.js
3+
*.js.map
4+
*.d.ts
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { fromBase64, toBase64 } from "../";
2+
3+
const doublePadded = new Uint8Array([0xde, 0xad, 0xbe, 0xef]);
4+
const b64DoublePadded = "3q2+7w==";
5+
6+
const singlePadded = new Uint8Array([0xde, 0xad, 0xbe, 0xef, 0xfa]);
7+
const b64SinglePadded = "3q2+7/o=";
8+
9+
const unpadded = new Uint8Array([0xde, 0xad, 0xbe, 0xef, 0xfa, 0xce]);
10+
const b64Unpadded = "3q2+7/rO";
11+
12+
describe("toBase64", () => {
13+
it("should convert Uint8Arrays with byte lengths divisible by 3 to unpadded base64 strings", () => {
14+
expect(toBase64(unpadded)).toBe(b64Unpadded);
15+
});
16+
17+
it("should convert Uint8Arrays whose byte lengths mod 3 === 2 to single-padded base64 strings", () => {
18+
expect(toBase64(singlePadded)).toBe(b64SinglePadded);
19+
});
20+
21+
it("should convert Uint8Arrays whose byte lengths mod 3 === 1 to double-padded base64 strings", () => {
22+
expect(toBase64(doublePadded)).toBe(b64DoublePadded);
23+
});
24+
});
25+
26+
describe("fromBase64", () => {
27+
it("should convert unpadded base64 strings to Uint8Arrays", () => {
28+
expect(fromBase64(b64Unpadded)).toEqual(unpadded);
29+
});
30+
31+
it("should convert single padded base64 strings to Uint8Arrays", () => {
32+
expect(fromBase64(b64SinglePadded)).toEqual(singlePadded);
33+
});
34+
35+
it("should convert double padded base64 strings to Uint8Arrays", () => {
36+
expect(fromBase64(b64DoublePadded)).toEqual(doublePadded);
37+
});
38+
});

packages/util-base64-browser/index.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
const alphabetByEncoding: { [key: string]: number } = {};
2+
const alphabetByValue: Array<string> = new Array(64);
3+
4+
for (
5+
let i = 0, start = "A".charCodeAt(0), limit = "Z".charCodeAt(0);
6+
i + start <= limit;
7+
i++
8+
) {
9+
const char = String.fromCharCode(i + start);
10+
alphabetByEncoding[char] = i;
11+
alphabetByValue[i] = char;
12+
}
13+
14+
for (
15+
let i = 0, start = "a".charCodeAt(0), limit = "z".charCodeAt(0);
16+
i + start <= limit;
17+
i++
18+
) {
19+
const char = String.fromCharCode(i + start);
20+
const index = i + 26;
21+
alphabetByEncoding[char] = index;
22+
alphabetByValue[index] = char;
23+
}
24+
25+
for (let i = 0; i < 10; i++) {
26+
alphabetByEncoding[i.toString(10)] = i + 52;
27+
const char = i.toString(10);
28+
const index = i + 52;
29+
alphabetByEncoding[char] = index;
30+
alphabetByValue[index] = char;
31+
}
32+
33+
alphabetByEncoding["+"] = 62;
34+
alphabetByValue[62] = "+";
35+
alphabetByEncoding["/"] = 63;
36+
alphabetByValue[63] = "/";
37+
38+
const bitsPerLetter = 6;
39+
const bitsPerByte = 8;
40+
const maxLetterValue = 0b111111;
41+
42+
/**
43+
* Converts a base-64 encoded string to a Uint8Array of bytes.
44+
*
45+
* @param input The base-64 encoded string
46+
*
47+
* @see https://tools.ietf.org/html/rfc4648#section-4
48+
*/
49+
export function fromBase64(input: string): Uint8Array {
50+
let totalByteLength = (input.length / 4) * 3;
51+
if (input.substr(-2) === "==") {
52+
totalByteLength -= 2;
53+
} else if (input.substr(-1) === "=") {
54+
totalByteLength--;
55+
}
56+
const out = new ArrayBuffer(totalByteLength);
57+
const dataView = new DataView(out);
58+
for (let i = 0; i < input.length; i += 4) {
59+
let bits = 0;
60+
let bitLength = 0;
61+
for (let j = i, limit = i + 3; j <= limit; j++) {
62+
if (input[j] !== "=") {
63+
bits |= alphabetByEncoding[input[j]] << ((limit - j) * bitsPerLetter);
64+
bitLength += bitsPerLetter;
65+
} else {
66+
bits >>= bitsPerLetter;
67+
}
68+
}
69+
70+
const chunkOffset = (i / 4) * 3;
71+
bits >>= bitLength % bitsPerByte;
72+
const byteLength = Math.floor(bitLength / bitsPerByte);
73+
for (let k = 0; k < byteLength; k++) {
74+
const offset = (byteLength - k - 1) * bitsPerByte;
75+
dataView.setUint8(chunkOffset + k, (bits & (255 << offset)) >> offset);
76+
}
77+
}
78+
79+
return new Uint8Array(out);
80+
}
81+
82+
/**
83+
* Converts a Uint8Array of binary data to a base-64 encoded string.
84+
*
85+
* @param input The binary data to encode
86+
*
87+
* @see https://tools.ietf.org/html/rfc4648#section-4
88+
*/
89+
export function toBase64(input: Uint8Array): string {
90+
let str = "";
91+
for (let i = 0; i < input.length; i += 3) {
92+
let bits = 0;
93+
let bitLength = 0;
94+
for (let j = i, limit = Math.min(i + 3, input.length); j < limit; j++) {
95+
bits |= input[j] << ((limit - j - 1) * bitsPerByte);
96+
bitLength += bitsPerByte;
97+
}
98+
99+
const bitClusterCount = Math.ceil(bitLength / bitsPerLetter);
100+
bits <<= bitClusterCount * bitsPerLetter - bitLength;
101+
for (let k = 1; k <= bitClusterCount; k++) {
102+
const offset = (bitClusterCount - k) * bitsPerLetter;
103+
str += alphabetByValue[(bits & (maxLetterValue << offset)) >> offset];
104+
}
105+
106+
str += "==".slice(0, 4 - bitClusterCount);
107+
}
108+
109+
return str;
110+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"name": "@aws/util-base64-browser",
3+
"private": true,
4+
"version": "0.0.1",
5+
"description": "A pure JS Base64 <-> UInt8Array converter",
6+
"main": "index.js",
7+
"scripts": {
8+
"prepublishOnly": "tsc",
9+
"pretest": "tsc",
10+
"test": "jest"
11+
},
12+
"author": "[email protected]",
13+
"license": "UNLICENSED",
14+
"devDependencies": {
15+
"@types/jest": "^19.2.2",
16+
"@types/node": "^7.0.12",
17+
"jest": "^19.0.2",
18+
"typescript": "^2.3"
19+
}
20+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"compilerOptions": {
3+
"target": "es5",
4+
"module": "commonjs",
5+
"declaration": true,
6+
"sourceMap": true,
7+
"strict": true
8+
}
9+
}

packages/util-base64-node/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/node_modules/
2+
*.js
3+
*.js.map
4+
*.d.ts
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { fromBase64, toBase64 } from "../";
2+
3+
const doublePadded = new Uint8Array([0xde, 0xad, 0xbe, 0xef]);
4+
const b64DoublePadded = "3q2+7w==";
5+
6+
const singlePadded = new Uint8Array([0xde, 0xad, 0xbe, 0xef, 0xfa]);
7+
const b64SinglePadded = "3q2+7/o=";
8+
9+
const unpadded = new Uint8Array([0xde, 0xad, 0xbe, 0xef, 0xfa, 0xce]);
10+
const b64Unpadded = "3q2+7/rO";
11+
12+
describe("toBase64", () => {
13+
it("should convert Uint8Arrays with byte lengths divisible by 3 to unpadded base64 strings", () => {
14+
expect(toBase64(unpadded)).toBe(b64Unpadded);
15+
});
16+
17+
it("should convert Uint8Arrays whose byte lengths mod 3 === 2 to single-padded base64 strings", () => {
18+
expect(toBase64(singlePadded)).toBe(b64SinglePadded);
19+
});
20+
21+
it("should convert Uint8Arrays whose byte lengths mod 3 === 1 to double-padded base64 strings", () => {
22+
expect(toBase64(doublePadded)).toBe(b64DoublePadded);
23+
});
24+
25+
it("should throw when given a number", () => {
26+
expect(() => toBase64(0xdeadbeefface as any)).toThrow();
27+
});
28+
});
29+
30+
describe("fromBase64", () => {
31+
it("should convert unpadded base64 strings to Uint8Arrays", () => {
32+
expect(fromBase64(b64Unpadded)).toEqual(unpadded);
33+
});
34+
35+
it("should convert single padded base64 strings to Uint8Arrays", () => {
36+
expect(fromBase64(b64SinglePadded)).toEqual(singlePadded);
37+
});
38+
39+
it("should convert double padded base64 strings to Uint8Arrays", () => {
40+
expect(fromBase64(b64DoublePadded)).toEqual(doublePadded);
41+
});
42+
43+
it("should throw when given a number", () => {
44+
expect(() => fromBase64(0xdeadbeefface as any)).toThrow();
45+
});
46+
});

packages/util-base64-node/index.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { Buffer } from "buffer";
2+
3+
/**
4+
* Converts a base-64 encoded string to a Uint8Array of bytes using Node.JS's
5+
* `buffer` module.
6+
*
7+
* @param input The base-64 encoded string
8+
*/
9+
export function fromBase64(input: string): Uint8Array {
10+
if (typeof input === "number") {
11+
throw new Error("Cannot base64 decode a number");
12+
}
13+
14+
let buf: Buffer;
15+
if (typeof Buffer.from === "function" && Buffer.from !== Uint8Array.from) {
16+
buf = Buffer.from(input, "base64");
17+
} else {
18+
buf = new Buffer(input, "base64");
19+
}
20+
21+
return new Uint8Array(
22+
buf.buffer,
23+
buf.byteOffset,
24+
buf.byteLength / Uint8Array.BYTES_PER_ELEMENT
25+
);
26+
}
27+
28+
/**
29+
* Converts a Uint8Array of binary data to a base-64 encoded string using
30+
* Node.JS's `buffer` module.
31+
*
32+
* @param input The binary data to encode
33+
*/
34+
export function toBase64(input: Uint8Array): string {
35+
if (typeof input === "number") {
36+
throw new Error("Cannot base64 encode a number");
37+
}
38+
39+
let buf: Buffer;
40+
if (typeof Buffer.from === "function" && Buffer.from !== Uint8Array.from) {
41+
buf = Buffer.from(input.buffer);
42+
} else {
43+
buf = new Buffer(input.buffer);
44+
}
45+
46+
return buf.toString("base64");
47+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"name": "@aws/util-base64-node",
3+
"private": true,
4+
"version": "0.0.1",
5+
"description": "A Node.JS Base64 <-> UInt8Array converter",
6+
"main": "index.js",
7+
"scripts": {
8+
"prepublishOnly": "tsc",
9+
"pretest": "tsc",
10+
"test": "jest"
11+
},
12+
"author": "[email protected]",
13+
"license": "UNLICENSED",
14+
"devDependencies": {
15+
"@types/jest": "^19.2.2",
16+
"@types/node": "^7.0.12",
17+
"jest": "^19.0.2",
18+
"typescript": "^2.3"
19+
}
20+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"compilerOptions": {
3+
"target": "es5",
4+
"module": "commonjs",
5+
"declaration": true,
6+
"sourceMap": true,
7+
"strict": true
8+
}
9+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/node_modules/
2+
*.js
3+
*.js.map
4+
*.d.ts

0 commit comments

Comments
 (0)