Skip to content

Commit 3a6ae9f

Browse files
authored
Merge pull request #12 from jeskew/feature/base64-support
Feature/base64 support
2 parents 885bf1b + a1ff71d commit 3a6ae9f

File tree

17 files changed

+493
-0
lines changed

17 files changed

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

packages/util-base64-browser/index.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
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(
76+
chunkOffset + k,
77+
(bits & (255 << offset)) >> offset
78+
);
79+
}
80+
}
81+
82+
return new Uint8Array(out);
83+
}
84+
85+
/**
86+
* Converts a Uint8Array of binary data to a base-64 encoded string.
87+
*
88+
* @param input The binary data to encode
89+
*
90+
* @see https://tools.ietf.org/html/rfc4648#section-4
91+
*/
92+
export function toBase64(input: Uint8Array): string {
93+
let str = '';
94+
for (let i = 0; i < input.length; i += 3) {
95+
let bits = 0;
96+
let bitLength = 0;
97+
for (let j = i, limit = Math.min(i + 3, input.length); j < limit; j++) {
98+
bits |= input[j] << (limit - j - 1) * bitsPerByte;
99+
bitLength += bitsPerByte;
100+
}
101+
102+
const bitClusterCount = Math.ceil(bitLength / bitsPerLetter);
103+
bits <<= bitClusterCount * bitsPerLetter - bitLength;
104+
for (let k = 1; k <= bitClusterCount; k++) {
105+
const offset = (bitClusterCount - k) * bitsPerLetter;
106+
str += alphabetByValue[(bits & (maxLetterValue << offset)) >> offset];
107+
}
108+
109+
str += '=='.slice(0, 4 - bitClusterCount);
110+
}
111+
112+
return str;
113+
}
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: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import {
2+
fromBase64,
3+
toBase64,
4+
} from "../";
5+
6+
const doublePadded = new Uint8Array([0xde, 0xad, 0xbe, 0xef]);
7+
const b64DoublePadded = '3q2+7w==';
8+
9+
const singlePadded = new Uint8Array([0xde, 0xad, 0xbe, 0xef, 0xfa]);
10+
const b64SinglePadded = '3q2+7/o=';
11+
12+
const unpadded = new Uint8Array([0xde, 0xad, 0xbe, 0xef, 0xfa, 0xce]);
13+
const b64Unpadded = '3q2+7/rO';
14+
15+
describe('toBase64', () => {
16+
it(
17+
'should convert Uint8Arrays with byte lengths divisible by 3 to unpadded base64 strings',
18+
() => {
19+
expect(toBase64(unpadded)).toBe(b64Unpadded);
20+
}
21+
);
22+
23+
it(
24+
'should convert Uint8Arrays whose byte lengths mod 3 === 2 to single-padded base64 strings',
25+
() => {
26+
expect(toBase64(singlePadded)).toBe(b64SinglePadded);
27+
}
28+
);
29+
30+
it(
31+
'should convert Uint8Arrays whose byte lengths mod 3 === 1 to double-padded base64 strings',
32+
() => {
33+
expect(toBase64(doublePadded)).toBe(b64DoublePadded);
34+
}
35+
);
36+
37+
it('should throw when given a number', () => {
38+
expect(() => toBase64(0xdeadbeefface as any)).toThrow();
39+
});
40+
});
41+
42+
describe('fromBase64', () => {
43+
it('should convert unpadded base64 strings to Uint8Arrays', () => {
44+
expect(fromBase64(b64Unpadded)).toEqual(unpadded);
45+
});
46+
47+
it('should convert single padded base64 strings to Uint8Arrays', () => {
48+
expect(fromBase64(b64SinglePadded)).toEqual(singlePadded);
49+
});
50+
51+
it('should convert double padded base64 strings to Uint8Arrays', () => {
52+
expect(fromBase64(b64DoublePadded)).toEqual(doublePadded);
53+
});
54+
55+
it('should throw when given a number', () => {
56+
expect(() => fromBase64(0xdeadbeefface as any)).toThrow();
57+
});
58+
});

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)