284 lines
9.5 KiB
TypeScript
284 lines
9.5 KiB
TypeScript
// Copyright (C) 2016 Dmitry Chestnykh
|
|
// MIT License. See LICENSE file for details.
|
|
|
|
/**
|
|
* Package base64 implements Base64 encoding and decoding.
|
|
*/
|
|
|
|
// Invalid character used in decoding to indicate
|
|
// that the character to decode is out of range of
|
|
// alphabet and cannot be decoded.
|
|
const INVALID_BYTE = 256;
|
|
|
|
/**
|
|
* Implements standard Base64 encoding.
|
|
*
|
|
* Operates in constant time.
|
|
*/
|
|
export class Coder {
|
|
// TODO(dchest): methods to encode chunk-by-chunk.
|
|
|
|
constructor(private _paddingCharacter = "=") { }
|
|
|
|
encodedLength(length: number): number {
|
|
if (!this._paddingCharacter) {
|
|
return (length * 8 + 5) / 6 | 0;
|
|
}
|
|
return (length + 2) / 3 * 4 | 0;
|
|
}
|
|
|
|
encode(data: Uint8Array): string {
|
|
let out = "";
|
|
|
|
let i = 0;
|
|
for (; i < data.length - 2; i += 3) {
|
|
let c = (data[i] << 16) | (data[i + 1] << 8) | (data[i + 2]);
|
|
out += this._encodeByte((c >>> 3 * 6) & 63);
|
|
out += this._encodeByte((c >>> 2 * 6) & 63);
|
|
out += this._encodeByte((c >>> 1 * 6) & 63);
|
|
out += this._encodeByte((c >>> 0 * 6) & 63);
|
|
}
|
|
|
|
const left = data.length - i;
|
|
if (left > 0) {
|
|
let c = (data[i] << 16) | (left === 2 ? data[i + 1] << 8 : 0);
|
|
out += this._encodeByte((c >>> 3 * 6) & 63);
|
|
out += this._encodeByte((c >>> 2 * 6) & 63);
|
|
if (left === 2) {
|
|
out += this._encodeByte((c >>> 1 * 6) & 63);
|
|
} else {
|
|
out += this._paddingCharacter || "";
|
|
}
|
|
out += this._paddingCharacter || "";
|
|
}
|
|
|
|
return out;
|
|
}
|
|
|
|
maxDecodedLength(length: number): number {
|
|
if (!this._paddingCharacter) {
|
|
return (length * 6 + 7) / 8 | 0;
|
|
}
|
|
return length / 4 * 3 | 0;
|
|
}
|
|
|
|
decodedLength(s: string): number {
|
|
return this.maxDecodedLength(s.length - this._getPaddingLength(s));
|
|
}
|
|
|
|
decode(s: string): Uint8Array {
|
|
if (s.length === 0) {
|
|
return new Uint8Array(0);
|
|
}
|
|
const paddingLength = this._getPaddingLength(s);
|
|
const length = s.length - paddingLength;
|
|
const out = new Uint8Array(this.maxDecodedLength(length));
|
|
let op = 0;
|
|
let i = 0;
|
|
let haveBad = 0;
|
|
let v0 = 0, v1 = 0, v2 = 0, v3 = 0;
|
|
for (; i < length - 4; i += 4) {
|
|
v0 = this._decodeChar(s.charCodeAt(i + 0));
|
|
v1 = this._decodeChar(s.charCodeAt(i + 1));
|
|
v2 = this._decodeChar(s.charCodeAt(i + 2));
|
|
v3 = this._decodeChar(s.charCodeAt(i + 3));
|
|
out[op++] = (v0 << 2) | (v1 >>> 4);
|
|
out[op++] = (v1 << 4) | (v2 >>> 2);
|
|
out[op++] = (v2 << 6) | v3;
|
|
haveBad |= v0 & INVALID_BYTE;
|
|
haveBad |= v1 & INVALID_BYTE;
|
|
haveBad |= v2 & INVALID_BYTE;
|
|
haveBad |= v3 & INVALID_BYTE;
|
|
}
|
|
if (i < length - 1) {
|
|
v0 = this._decodeChar(s.charCodeAt(i));
|
|
v1 = this._decodeChar(s.charCodeAt(i + 1));
|
|
out[op++] = (v0 << 2) | (v1 >>> 4);
|
|
haveBad |= v0 & INVALID_BYTE;
|
|
haveBad |= v1 & INVALID_BYTE;
|
|
}
|
|
if (i < length - 2) {
|
|
v2 = this._decodeChar(s.charCodeAt(i + 2));
|
|
out[op++] = (v1 << 4) | (v2 >>> 2);
|
|
haveBad |= v2 & INVALID_BYTE;
|
|
}
|
|
if (i < length - 3) {
|
|
v3 = this._decodeChar(s.charCodeAt(i + 3));
|
|
out[op++] = (v2 << 6) | v3;
|
|
haveBad |= v3 & INVALID_BYTE;
|
|
}
|
|
if (haveBad !== 0) {
|
|
throw new Error("Base64Coder: incorrect characters for decoding");
|
|
}
|
|
return out;
|
|
}
|
|
|
|
// Standard encoding have the following encoded/decoded ranges,
|
|
// which we need to convert between.
|
|
//
|
|
// ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz 0123456789 + /
|
|
// Index: 0 - 25 26 - 51 52 - 61 62 63
|
|
// ASCII: 65 - 90 97 - 122 48 - 57 43 47
|
|
//
|
|
|
|
// Encode 6 bits in b into a new character.
|
|
protected _encodeByte(b: number): string {
|
|
// Encoding uses constant time operations as follows:
|
|
//
|
|
// 1. Define comparison of A with B using (A - B) >>> 8:
|
|
// if A > B, then result is positive integer
|
|
// if A <= B, then result is 0
|
|
//
|
|
// 2. Define selection of C or 0 using bitwise AND: X & C:
|
|
// if X == 0, then result is 0
|
|
// if X != 0, then result is C
|
|
//
|
|
// 3. Start with the smallest comparison (b >= 0), which is always
|
|
// true, so set the result to the starting ASCII value (65).
|
|
//
|
|
// 4. Continue comparing b to higher ASCII values, and selecting
|
|
// zero if comparison isn't true, otherwise selecting a value
|
|
// to add to result, which:
|
|
//
|
|
// a) undoes the previous addition
|
|
// b) provides new value to add
|
|
//
|
|
let result = b;
|
|
// b >= 0
|
|
result += 65;
|
|
// b > 25
|
|
result += ((25 - b) >>> 8) & ((0 - 65) - 26 + 97);
|
|
// b > 51
|
|
result += ((51 - b) >>> 8) & ((26 - 97) - 52 + 48);
|
|
// b > 61
|
|
result += ((61 - b) >>> 8) & ((52 - 48) - 62 + 43);
|
|
// b > 62
|
|
result += ((62 - b) >>> 8) & ((62 - 43) - 63 + 47);
|
|
|
|
return String.fromCharCode(result);
|
|
}
|
|
|
|
// Decode a character code into a byte.
|
|
// Must return 256 if character is out of alphabet range.
|
|
protected _decodeChar(c: number): number {
|
|
// Decoding works similar to encoding: using the same comparison
|
|
// function, but now it works on ranges: result is always incremented
|
|
// by value, but this value becomes zero if the range is not
|
|
// satisfied.
|
|
//
|
|
// Decoding starts with invalid value, 256, which is then
|
|
// subtracted when the range is satisfied. If none of the ranges
|
|
// apply, the function returns 256, which is then checked by
|
|
// the caller to throw error.
|
|
let result = INVALID_BYTE; // start with invalid character
|
|
|
|
// c == 43 (c > 42 and c < 44)
|
|
result += (((42 - c) & (c - 44)) >>> 8) & (-INVALID_BYTE + c - 43 + 62);
|
|
// c == 47 (c > 46 and c < 48)
|
|
result += (((46 - c) & (c - 48)) >>> 8) & (-INVALID_BYTE + c - 47 + 63);
|
|
// c > 47 and c < 58
|
|
result += (((47 - c) & (c - 58)) >>> 8) & (-INVALID_BYTE + c - 48 + 52);
|
|
// c > 64 and c < 91
|
|
result += (((64 - c) & (c - 91)) >>> 8) & (-INVALID_BYTE + c - 65 + 0);
|
|
// c > 96 and c < 123
|
|
result += (((96 - c) & (c - 123)) >>> 8) & (-INVALID_BYTE + c - 97 + 26);
|
|
|
|
return result;
|
|
}
|
|
|
|
private _getPaddingLength(s: string): number {
|
|
let paddingLength = 0;
|
|
if (this._paddingCharacter) {
|
|
for (let i = s.length - 1; i >= 0; i--) {
|
|
if (s[i] !== this._paddingCharacter) {
|
|
break;
|
|
}
|
|
paddingLength++;
|
|
}
|
|
if (s.length < 4 || paddingLength > 2) {
|
|
throw new Error("Base64Coder: incorrect padding");
|
|
}
|
|
}
|
|
return paddingLength;
|
|
}
|
|
|
|
}
|
|
|
|
const stdCoder = new Coder();
|
|
|
|
export function encode(data: Uint8Array): string {
|
|
return stdCoder.encode(data);
|
|
}
|
|
|
|
export function decode(s: string): Uint8Array {
|
|
return stdCoder.decode(s);
|
|
}
|
|
|
|
/**
|
|
* Implements URL-safe Base64 encoding.
|
|
* (Same as Base64, but '+' is replaced with '-', and '/' with '_').
|
|
*
|
|
* Operates in constant time.
|
|
*/
|
|
export class URLSafeCoder extends Coder {
|
|
// URL-safe encoding have the following encoded/decoded ranges:
|
|
//
|
|
// ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz 0123456789 - _
|
|
// Index: 0 - 25 26 - 51 52 - 61 62 63
|
|
// ASCII: 65 - 90 97 - 122 48 - 57 45 95
|
|
//
|
|
|
|
protected _encodeByte(b: number): string {
|
|
let result = b;
|
|
// b >= 0
|
|
result += 65;
|
|
// b > 25
|
|
result += ((25 - b) >>> 8) & ((0 - 65) - 26 + 97);
|
|
// b > 51
|
|
result += ((51 - b) >>> 8) & ((26 - 97) - 52 + 48);
|
|
// b > 61
|
|
result += ((61 - b) >>> 8) & ((52 - 48) - 62 + 45);
|
|
// b > 62
|
|
result += ((62 - b) >>> 8) & ((62 - 45) - 63 + 95);
|
|
|
|
return String.fromCharCode(result);
|
|
}
|
|
|
|
protected _decodeChar(c: number): number {
|
|
let result = INVALID_BYTE;
|
|
|
|
// c == 45 (c > 44 and c < 46)
|
|
result += (((44 - c) & (c - 46)) >>> 8) & (-INVALID_BYTE + c - 45 + 62);
|
|
// c == 95 (c > 94 and c < 96)
|
|
result += (((94 - c) & (c - 96)) >>> 8) & (-INVALID_BYTE + c - 95 + 63);
|
|
// c > 47 and c < 58
|
|
result += (((47 - c) & (c - 58)) >>> 8) & (-INVALID_BYTE + c - 48 + 52);
|
|
// c > 64 and c < 91
|
|
result += (((64 - c) & (c - 91)) >>> 8) & (-INVALID_BYTE + c - 65 + 0);
|
|
// c > 96 and c < 123
|
|
result += (((96 - c) & (c - 123)) >>> 8) & (-INVALID_BYTE + c - 97 + 26);
|
|
|
|
return result;
|
|
}
|
|
}
|
|
|
|
const urlSafeCoder = new URLSafeCoder();
|
|
|
|
export function encodeURLSafe(data: Uint8Array): string {
|
|
return urlSafeCoder.encode(data);
|
|
}
|
|
|
|
export function decodeURLSafe(s: string): Uint8Array {
|
|
return urlSafeCoder.decode(s);
|
|
}
|
|
|
|
|
|
export const encodedLength = (length: number) =>
|
|
stdCoder.encodedLength(length);
|
|
|
|
export const maxDecodedLength = (length: number) =>
|
|
stdCoder.maxDecodedLength(length);
|
|
|
|
export const decodedLength = (s: string) =>
|
|
stdCoder.decodedLength(s);
|