Skip to content

Commit

Permalink
Replaced Buffer with Uint8Array and matching base64 parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
JrMasterModelBuilder committed Sep 23, 2023
1 parent c9910cb commit ef1ebd1
Show file tree
Hide file tree
Showing 4 changed files with 295 additions and 44 deletions.
145 changes: 144 additions & 1 deletion src/util.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {describe, it} from 'node:test';
import {deepStrictEqual, strictEqual} from 'node:assert';

import {stringChunk, xmlDecode} from './util';
import {base64Decode, base64Encode, stringChunk, xmlDecode} from './util';

void describe('util', () => {
void describe('stringChunk', () => {
Expand Down Expand Up @@ -51,4 +51,147 @@ void describe('util', () => {
strictEqual(o.documentElement.toString(), '<xml>a</xml>');
});
});

void describe('base64Encode', () => {
void it('byte combos: 1', () => {
const data = Buffer.alloc(1);
for (let a = 0; a < 256; a++) {
data[0] = a;
deepStrictEqual(
base64Encode(new Uint8Array(data)),
data.toString('base64')
);
}
});

void it('byte combos: 2', () => {
const data = Buffer.alloc(2);
for (let a = 0; a < 256; a++) {
data[0] = a;
for (let b = 0; b < 256; b++) {
data[1] = b;
deepStrictEqual(
base64Encode(new Uint8Array(data)),
data.toString('base64')
);
}
}
});

void it('byte combos: 3', () => {
const data = Buffer.alloc(3);
for (let a = 0; a < 256; a++) {
data[1] = a;
for (let b = 0; b < 256; b++) {
data[2] = b;
deepStrictEqual(
base64Encode(new Uint8Array(data)),
data.toString('base64')
);
}
}
});
});

void describe('base64Decode', () => {
const input = 'ABCDEFGHIJKL';
for (let i = 0; i <= input.length; i++) {
void it(`length: ${i}`, () => {
const s = input.substring(0, i);
const d = base64Decode(Buffer.from(s).toString('base64'));
strictEqual(String.fromCharCode(...d), s);
});
}

void it('bytes: 0x00 * 256', () => {
const bytes = Buffer.alloc(256);
deepStrictEqual(
base64Decode(bytes.toString('base64')),
new Uint8Array(bytes)
);
});

void it('bytes: 0xFF * 256', () => {
const bytes = Buffer.alloc(256);
bytes.fill(0xff);
deepStrictEqual(
base64Decode(bytes.toString('base64')),
new Uint8Array(bytes)
);
});

void it('bytes: 0x00-0xFF', () => {
const bytes = Buffer.alloc(256);
for (let i = 0; i < 256; i++) {
bytes[i] = i;
}
deepStrictEqual(
base64Decode(bytes.toString('base64')),
new Uint8Array(bytes)
);
});

void it('byte combos: 1', () => {
const data = Buffer.alloc(1);
for (let a = 0; a < 256; a++) {
data[0] = a;
deepStrictEqual(
base64Decode(data.toString('base64')),
new Uint8Array(data)
);
}
});

void it('byte combos: 2', () => {
const data = Buffer.alloc(2);
for (let a = 0; a < 256; a++) {
data[0] = a;
for (let b = 0; b < 256; b++) {
data[1] = b;
deepStrictEqual(
base64Decode(data.toString('base64')),
new Uint8Array(data)
);
}
}
});

void it('byte combos: 3', () => {
const data = Buffer.alloc(3);
for (let a = 0; a < 256; a++) {
data[1] = a;
for (let b = 0; b < 256; b++) {
data[2] = b;
deepStrictEqual(
base64Decode(data.toString('base64')),
new Uint8Array(data)
);
}
}
});

for (const [b64, bytes] of [
['', []],
['A', []],
['AB', []],
['AB__', []],
['ABCDA', [0, 16, 131]],
['ABCDAB', [0, 16, 131]],
['ABCDAB__', [0, 16, 131]],
['1\x002\x803\xFF4😀5©6==', [215, 109, 248, 231]],
['AB CD\tEF\rGH\nIJ\0==', [0, 16, 131, 16, 81, 135, 32]],
['ABCD EFGH', [0, 16, 131, 16, 81, 135]],
['\r\nABCD\r\nEFGH\r\n', [0, 16, 131, 16, 81, 135]],
['YWE=YWE=', [97, 97, 97, 97]],
['YW=E', [97, 96, 4]],
['Y=WE', [96, 5, 132]],
['=YWE', [1, 133, 132]],
['====', [0]],
['========', [0, 0]]
] as [string, number[]][]) {
void it(JSON.stringify(b64), () => {
deepStrictEqual(base64Decode(b64), new Uint8Array(bytes));
});
}
});
});
111 changes: 111 additions & 0 deletions src/util.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
import {DOMParser} from '@xmldom/xmldom';

const B6 = 0x3f;
const B8 = 0xff;
const C64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
const C64M = [
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, 62, -1, -1, -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60,
61, -1, -1, -1, 64, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12,
13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, -1,
26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44,
45, 46, 47, 48, 49, 50, 51
];

export interface IText {
nodeValue: string | null;
}
Expand Down Expand Up @@ -186,3 +199,101 @@ export function stringChunk(str: string, len: number) {
}
return r;
}

/**
* Base64 encode function mirroring decode function.
*
* @param data Byte array.
* @returns Base64 string.
*/
export function base64Encode(data: Uint8Array) {
const l = data.length;
let r = '';
for (let i = 0; i < l; ) {
const a = data[i++];
const b = i < l ? data[i++] : null;
const c = i < l ? data[i++] : null;
// eslint-disable-next-line no-bitwise
const o = (a << 16) | ((b || 0) << 8) | (c || 0);
r +=
// eslint-disable-next-line no-bitwise
C64[o >> 18] +
// eslint-disable-next-line no-bitwise
C64[(o >> 12) & B6] +
// eslint-disable-next-line no-bitwise
C64[b === null ? 64 : (o >> 6) & B6] +
// eslint-disable-next-line no-bitwise
C64[c === null ? 64 : o & B6];
}
return r;
}

/**
* Base64 decode function that matches plist parsing behavior.
*
* @param base64 Base64 string.
* @returns Byte array.
*/
export function base64Decode(base64: string) {
const l = base64.length;
const r = [];
OUTER: for (let a, b, c, d, m, z, i = 0; i < l; ) {
for (;;) {
if ((m = C64M[base64.charCodeAt(i++)]) >= 0) {
a = m;
break;
}
if (i >= l) {
break OUTER;
}
}
for (;;) {
if ((m = C64M[base64.charCodeAt(i++)]) >= 0) {
b = m;
break;
}
if (i >= l) {
break OUTER;
}
}
for (;;) {
if ((m = C64M[base64.charCodeAt(i++)]) >= 0) {
c = m;
break;
}
if (i >= l) {
break OUTER;
}
}
for (;;) {
if ((m = C64M[base64.charCodeAt(i++)]) >= 0) {
d = m;
break;
}
if (i >= l) {
break OUTER;
}
}
// eslint-disable-next-line no-bitwise
z = ((a & B6) << 18) | ((b & B6) << 12) | ((c & B6) << 6) | (d & B6);
// eslint-disable-next-line default-case, no-nested-ternary
switch (c > B6 ? (d > B6 ? 2 : 0) : d > B6 ? 1 : 0) {
case 0: {
// eslint-disable-next-line no-bitwise
r.push((z >> 16) & B8, (z >> 8) & B8, z & B8);
break;
}
case 1: {
// eslint-disable-next-line no-bitwise
r.push((z >> 16) & B8, (z >> 8) & B8);
break;
}
case 2: {
// eslint-disable-next-line no-bitwise
r.push((z >> 16) & B8);
break;
}
}
}
return new Uint8Array(r);
}
Loading

0 comments on commit ef1ebd1

Please sign in to comment.