/** * Base64 Utility Functions in TypeScript */ export { encode, decode } /** * Encode an array of bytes as a Uint8Array in base64 notation. */ function encode (bytes: Uint8Array) : string { // slice the array // length of head is a multiple of 3 // treat the tail as a special case let {head, tail, tail_len} = slice3k(bytes); let head_str : string = encode_head(head); let tail_str : string = encode_tail(tail, tail_len); return head_str + tail_str; } type slice3k = {head : Uint8Array, tail : Uint8Array, tail_len : number}; /** * Take a Uint8Array, take the first 3k (k >= 0) bytes, put them in head, and * the remaining 0,1, or 2 bytes, put them in tail * * @internal */ function slice3k (bytes: Uint8Array) : slice3k { let len : number = bytes.length; // too lazy to look up how to do integer division in js so this will do let tail_len : number = len % 3; let head_len : number = len - tail_len; // for slice: // first argument is the 0-index of the start // second - first is the length of the slice let head : Uint8Array = bytes.slice(0, head_len); // empty second argument means go to the end let tail : Uint8Array = bytes.slice(head_len); return {head : head, tail : tail, tail_len : tail_len}; } /** * Encode a Uint8Array whose length is known to be a multiple of 3 * * @internal */ function encode_head (head_bytes: Uint8Array) : string { // can assume length of bytes is a multiple of 3 // start index at 0 // increment by 3 let head_bytes_len : number = head_bytes.length; let max_idx0 : number = head_bytes_len - 1; let head_str_acc : string = ''; for(let this_3slice_start_idx0 = 0; this_3slice_start_idx0 <= max_idx0; this_3slice_start_idx0 += 3) { let this_3slice_bytes : Uint8Array = head_bytes.slice(this_3slice_start_idx0, this_3slice_start_idx0 + 3); let this_3slice_str : string = encode3(this_3slice_bytes); head_str_acc += this_3slice_str; } return head_str_acc; } /** * Encode a 3 bytes into base64 notation * * @internal */ function encode3 (bytes: Uint8Array) : string { let b0 : number = bytes[0]; let b1 : number = bytes[1]; let b2 : number = bytes[2]; // ABCDEFGH 12345678 abcdefgh // b0 b1 b2 // ABCDEF GH1234 5678ab cdefgh // n0 n1 n2 n3 let n0 : number = b0 >> 2; // b0 = ABCDEFGH // 4 = _____1__ // b0 % 4 = ______GH // (b0 % 4) << 4 = __GH____ // b1 = 12345678 // b1 >> 4 = ____1234 // n1 = __GH1234 let n1 : number = ((b0 % 4) << 4) + (b1 >> 4); // b1 = 12345678 // 16 = ___1____ // b1 % 16 = ____5678 // (b1 % 16) << 2 = __5678__ // b2 = abcdefgh // b2 >> 6 = ______ab // n2 = __5678ab let n2 : number = ((b1 % 16) << 2) + (b2 >> 6); // b2 = abcdefgh // 64 = _1______ // n3 = __cdefgh let n3 : number = b2 % 64; // convert to chars let s0 : string = int2char(n0); let s1 : string = int2char(n1); let s2 : string = int2char(n2); let s3 : string = int2char(n3); // retrvn return s0 + s1 + s2 + s3; } /** * Encode the final 0, 1, or 2 bytes * * @internal */ function encode_tail (tail_bytes : Uint8Array, tail_len : number) : string { switch(tail_len) { case 0: return ''; case 1: return encode1(tail_bytes); case 2: return encode2(tail_bytes); default: throw new Error('encode_tail with tail_len = ' + tail_len); } } /** * Encode a single byte * * @internal */ function encode1 (bytes: Uint8Array) : string { let b0 : number = bytes[0]; // n0 = __ABCDEF // b0 = ABCDEFGH // b0 >> 2 = __ABCDEF let n0 : number = b0 >> 2; // n1 = __GH____ // b0 = ABCDEFGH // 4 = _____1__ // b0 % 4 = ______GH // (b0 % 4) << 4 = __GH____ let n1 : number = (b0 % 4) << 4; return int2char(n0) + int2char(n1) + '=='; } /** * Encode two bytes * * @internal */ function encode2 (bytes: Uint8Array) : string { let b0 : number = bytes[0]; let b1 : number = bytes[1]; // ABCDEFGH 12345678 // b0 b1 // ABCDEF GH1234 5678__ // n0 n1 n2 let n0 : number = b0 >> 2; // b0 = ABCDEFGH // 4 = _____1__ // b0 % 4 = ______GH // (b0 % 4) << 4 = __GH____ // b1 = 12345678 // b1 >> 4 = ____1234 // n1 = __GH1234 let n1 : number = ((b0 % 4) << 4) + (b1 >> 4); // b1 = 12345678 // 16 = ___1____ // b1 % 16 = ____5678 // (b1 % 16) << 2 = __5678__ // n2 = __5678__ let n2 : number = (b1 % 16) << 2; // convert to chars let s0 : string = int2char(n0); let s1 : string = int2char(n1); let s2 : string = int2char(n2); // retrvn return s0 + s1 + s2 + '='; } /** * Decode a base64-encoded string */ function decode (base64_str : string) : Uint8Array { // length of the string is guaranteed to be a multiple of 4 // if the string is empty, return the empty array let len = base64_str.length; // this branching contains the implicit assertion that the length is a // multiple of 4. If this is not true, the bottom branch is triggered. // general case goes first because speeeeeed if ( (4 < len) && (0 === (len % 4))) { // split the head and tail let tail_start_idx0 : number = len - 4; let head_s : string = base64_str.slice(0, tail_start_idx0); let tail_s : string = base64_str.slice(tail_start_idx0); // Using arrays because Uint8Arrays don't have a concat operation let head_arr : Array = decode_head(head_s); let tail_arr : Array = decode_tail(tail_s); // silly to put these in variables but this is exactly the type of // situation where JS type insanity shows up // // see: i forgot // > [1,2,3] + [4,5,6] // '1,2,34,5,6' // // Originally, I used + like some sort of moron who codes in a sane // language // // seriously what is this language // // this is some clown behavior let total_arr : Array = head_arr.concat(tail_arr); return new Uint8Array(total_arr); } // special case if the length is exactly 4 else if (4 === len) { // it's just a tail return new Uint8Array(decode_tail(base64_str)); } // empty string else if (0 === len) { return new Uint8Array([]); } else { throw new Error('base64 decode: invalid string length: ' + len); } } /** * Decode a string known to not have any padding * * @internal */ function decode_head (s: string) : Array { // go 4 characters at a time let max_i0 : number = s.length - 1; let decoded_acc : Array = []; for(let i0 = 0; i0 <= max_i0; i0 += 4) { let this_slice_s : string = s.slice(i0, i0 + 4); let this_slice_arr : Array = decode3(this_slice_s); // update accumulator decoded_acc = decoded_acc.concat(this_slice_arr); } return decoded_acc; } /** * Decode 4 characters that correspond to either 3 bytes, 2, bytes, or 1 byte * * @internal */ function decode_tail (s: string) : Array { // all that matters right now is the last 2 chars // s0, s1, s2, s3 // 0 based indexing is so annoying let s2 = s[2]; let s3 = s[3]; // braaaaaaaaaaaaaaaaaench // two equals signs means 1 byte if (('=' === s3) && ('=' === s2)) { return decode1(s); } // one equals sign means 2 bytes else if (('=' === s3)) { return decode2(s); } // 0 equals signs means 3 bytes else { return decode3(s); } } /** * Decode a 4-character long base64 string corresponding to 3 bytes * * @internal */ function decode3 (s: string) : Array { // pull out strings let s0 : string = s[0]; let s1 : string = s[1]; let s2 : string = s[2]; let s3 : string = s[3]; // convert to numbers let n0 : number = char2int(s0); let n1 : number = char2int(s1); let n2 : number = char2int(s2); let n3 : number = char2int(s3); // abcdef gh1234 5678ab cdefgh // n0 n1 n2 n3 // abcdefgh 12345678 abcdefgh // b0 b1 b2 // n0 = __abcdef // n1 = __gh1234 // n0 << 2 = abcdef__ // n1 >> 4 = ______gh // b0 = abcdefgh let b0 : number = (n0 << 2) + (n1 >> 4); // n1 = __gh1234 // 16 = ___1____ // n1 % 16 = ____1234 // (n1 % 16) << 4 = 1234____ // n2 = __5678ab // n2 >> 2 = ____5678 // b1 = 12345678 let b1 : number = ((n1 % 16) << 4) + (n2 >> 2); // n2 = __5678ab // 4 = _____1__ // n2 % 4 = ______ab // (n2 % 4) << 6 = ab______ // n3 = __cdefgh let b2 : number = ((n2 % 4) << 6) + n3; return [b0, b1, b2]; } /** * Decode a 4-character long base64 string corresponding to 2 bytes * * @internal */ function decode2 (s: string) : Array { // xyz= // pull out strings let s0 : string = s[0]; let s1 : string = s[1]; let s2 : string = s[2]; // convert to numbers let n0 : number = char2int(s0); let n1 : number = char2int(s1); let n2 : number = char2int(s2); // abcdef gh1234 5678__ // n0 n1 n2 // abcdefgh 12345678 // b0 b1 // n0 = __abcdef // n1 = __gh1234 // n0 << 2 = abcdef__ // n1 >> 4 = ______gh // b0 = abcdefgh let b0 : number = (n0 << 2) + (n1 >> 4); // n1 = __gh1234 // 16 = ___1____ // n1 % 16 = ____1234 // (n1 % 16) << 4 = 1234____ // n2 = __5678__ // n2 >> 2 = ____5678 // b1 = 12345678 let b1 : number = ((n1 % 16) << 4) + (n2 >> 2); return [b0, b1]; } /** * Decode a 4-character long base64 string corresponding to 2 bytes * * @internal */ function decode1 (s: string) : Array { // xy== // pull out strings let s0 : string = s[0]; let s1 : string = s[1]; // convert to numbers let n0 : number = char2int(s0); let n1 : number = char2int(s1); // abcdef gh____ // n0 n1 // abcdefgh // b0 // n0 = __abcdef // n1 = __gh____ // n0 << 2 = abcdef__ // n1 >> 4 = ______gh // b0 = abcdefgh let b0 : number = (n0 << 2) + (n1 >> 4); return [b0]; } // FIXME: these tables would *probably* be faster if they were made into objects /** * Conversion table for base64 encode * * @internal */ function int2char (n: number) : string { switch(n) { case 0: return 'A'; case 1: return 'B'; case 2: return 'C'; case 3: return 'D'; case 4: return 'E'; case 5: return 'F'; case 6: return 'G'; case 7: return 'H'; case 8: return 'I'; case 9: return 'J'; case 10: return 'K'; case 11: return 'L'; case 12: return 'M'; case 13: return 'N'; case 14: return 'O'; case 15: return 'P'; case 16: return 'Q'; case 17: return 'R'; case 18: return 'S'; case 19: return 'T'; case 20: return 'U'; case 21: return 'V'; case 22: return 'W'; case 23: return 'X'; case 24: return 'Y'; case 25: return 'Z'; case 26: return 'a'; case 27: return 'b'; case 28: return 'c'; case 29: return 'd'; case 30: return 'e'; case 31: return 'f'; case 32: return 'g'; case 33: return 'h'; case 34: return 'i'; case 35: return 'j'; case 36: return 'k'; case 37: return 'l'; case 38: return 'm'; case 39: return 'n'; case 40: return 'o'; case 41: return 'p'; case 42: return 'q'; case 43: return 'r'; case 44: return 's'; case 45: return 't'; case 46: return 'u'; case 47: return 'v'; case 48: return 'w'; case 49: return 'x'; case 50: return 'y'; case 51: return 'z'; case 52: return '0'; case 53: return '1'; case 54: return '2'; case 55: return '3'; case 56: return '4'; case 57: return '5'; case 58: return '6'; case 59: return '7'; case 60: return '8'; case 61: return '9'; case 62: return '+'; case 63: return '/'; default: throw new Error("invalid base64 encode byte: " + n); } } /** * Conversion table for base64 decode * * @internal */ function char2int (s: string) : number { switch(s) { case 'A': return 0; case 'B': return 1; case 'C': return 2; case 'D': return 3; case 'E': return 4; case 'F': return 5; case 'G': return 6; case 'H': return 7; case 'I': return 8; case 'J': return 9; case 'K': return 10; case 'L': return 11; case 'M': return 12; case 'N': return 13; case 'O': return 14; case 'P': return 15; case 'Q': return 16; case 'R': return 17; case 'S': return 18; case 'T': return 19; case 'U': return 20; case 'V': return 21; case 'W': return 22; case 'X': return 23; case 'Y': return 24; case 'Z': return 25; case 'a': return 26; case 'b': return 27; case 'c': return 28; case 'd': return 29; case 'e': return 30; case 'f': return 31; case 'g': return 32; case 'h': return 33; case 'i': return 34; case 'j': return 35; case 'k': return 36; case 'l': return 37; case 'm': return 38; case 'n': return 39; case 'o': return 40; case 'p': return 41; case 'q': return 42; case 'r': return 43; case 's': return 44; case 't': return 45; case 'u': return 46; case 'v': return 47; case 'w': return 48; case 'x': return 49; case 'y': return 50; case 'z': return 51; case '0': return 52; case '1': return 53; case '2': return 54; case '3': return 55; case '4': return 56; case '5': return 57; case '6': return 58; case '7': return 59; case '8': return 60; case '9': return 61; case '+': return 62; case '/': return 63; default: throw new Error("invalid base64 character: " + s); } }