From ef1ebd1f4f6ba5c773458a70f6b1ac9c17d5348d Mon Sep 17 00:00:00 2001 From: JrMasterModelBuilder Date: Sat, 23 Sep 2023 03:48:46 -0400 Subject: [PATCH] Replaced Buffer with Uint8Array and matching base64 parsing --- src/util.test.ts | 145 ++++++++++++++++++++++++++++++++++++++++- src/util.ts | 111 +++++++++++++++++++++++++++++++ src/value/data.test.ts | 59 ++++++++--------- src/value/data.ts | 24 ++++--- 4 files changed, 295 insertions(+), 44 deletions(-) diff --git a/src/util.test.ts b/src/util.test.ts index 1e51ca0..840dcea 100644 --- a/src/util.test.ts +++ b/src/util.test.ts @@ -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', () => { @@ -51,4 +51,147 @@ void describe('util', () => { strictEqual(o.documentElement.toString(), 'a'); }); }); + + 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)); + }); + } + }); }); diff --git a/src/util.ts b/src/util.ts index 6ae2202..378304b 100644 --- a/src/util.ts +++ b/src/util.ts @@ -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; } @@ -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); +} diff --git a/src/value/data.test.ts b/src/value/data.test.ts index d1952b5..a72edd0 100644 --- a/src/value/data.test.ts +++ b/src/value/data.test.ts @@ -1,6 +1,6 @@ /* eslint-disable max-nested-callbacks */ import {describe, it} from 'node:test'; -import {strictEqual, throws} from 'node:assert'; +import {deepStrictEqual, strictEqual, throws} from 'node:assert'; import {ValueData} from './data'; @@ -9,11 +9,11 @@ void describe('value/data', () => { void describe('constructor', () => { void it('length: 0', () => { const el = new ValueData(); - strictEqual(el.value.toString('base64'), ''); + deepStrictEqual(el.value, new Uint8Array(0)); }); void it('length: 10', () => { - const b = Buffer.alloc(10); + const b = new Uint8Array(10); const el = new ValueData(b); strictEqual(el.value, b); }); @@ -40,7 +40,7 @@ void describe('value/data', () => { }); void it('length: 10', () => { - const b = Buffer.alloc(10); + const b = new Uint8Array(10); const el = new ValueData(b); strictEqual(el.toXml(), '\nAAAAAAAAAAAAAA==\n'); strictEqual( @@ -50,7 +50,7 @@ void describe('value/data', () => { }); void it('length: 10 wrapped', () => { - const b = Buffer.alloc(10); + const b = new Uint8Array(10); const el = new ValueData(b); strictEqual( el.toXml({ @@ -72,66 +72,66 @@ void describe('value/data', () => { void describe('fromXml', () => { void it('length: 0', () => { - const b = Buffer.alloc(0); + const b = new Uint8Array(0); const el = new ValueData(b); const xml = el.toXml(); - el.value = Buffer.alloc(1); + el.value = new Uint8Array(1); el.fromXml(xml); - strictEqual(el.value.toString('base64'), b.toString('base64')); + deepStrictEqual(el.value, b); }); void it('length: 1', () => { - const b = Buffer.alloc(1); + const b = new Uint8Array(1); const el = new ValueData(b); const xml = el.toXml(); - el.value = Buffer.alloc(0); + el.value = new Uint8Array(0); el.fromXml(xml); - strictEqual(el.value.toString('base64'), b.toString('base64')); + deepStrictEqual(el.value, b); }); void it('length: 2', () => { - const b = Buffer.alloc(2); + const b = new Uint8Array(2); const el = new ValueData(b); const xml = el.toXml(); - el.value = Buffer.alloc(0); + el.value = new Uint8Array(0); el.fromXml(xml); - strictEqual(el.value.toString('base64'), b.toString('base64')); + deepStrictEqual(el.value, b); }); void it('length: 3', () => { - const b = Buffer.alloc(3); + const b = new Uint8Array(3); const el = new ValueData(b); const xml = el.toXml(); - el.value = Buffer.alloc(0); + el.value = new Uint8Array(0); el.fromXml(xml); - strictEqual(el.value.toString('base64'), b.toString('base64')); + deepStrictEqual(el.value, b); }); void it('length: 10', () => { - const b = Buffer.alloc(10); + const b = new Uint8Array(10); const el = new ValueData(b); const xml = el.toXml(); - el.value = Buffer.alloc(0); + el.value = new Uint8Array(0); el.fromXml(xml); - strictEqual(el.value.toString('base64'), b.toString('base64')); + deepStrictEqual(el.value, b); }); void it('length: 100', () => { - const b = Buffer.alloc(100); + const b = new Uint8Array(100); const el = new ValueData(b); const xml = el.toXml(); - el.value = Buffer.alloc(0); + el.value = new Uint8Array(0); el.fromXml(xml); - strictEqual(el.value.toString('base64'), b.toString('base64')); + deepStrictEqual(el.value, b); }); void it('length: 100', () => { - const b = Buffer.alloc(100); + const b = new Uint8Array(100); const el = new ValueData(b); const xml = el.toXml(); - el.value = Buffer.alloc(0); + el.value = new Uint8Array(0); el.fromXml(xml); - strictEqual(el.value.toString('base64'), b.toString('base64')); + deepStrictEqual(el.value, b); }); void it('charset', () => { @@ -148,13 +148,6 @@ void describe('value/data', () => { strictEqual(el.value.length, 48); }); - void it('baddata', () => { - const el = new ValueData(); - throws(() => { - el.fromXml('-'); - }); - }); - void it('children', () => { const el = new ValueData(); throws(() => { diff --git a/src/value/data.ts b/src/value/data.ts index 1f1c3ae..2e27d41 100644 --- a/src/value/data.ts +++ b/src/value/data.ts @@ -4,7 +4,14 @@ import { IToXmlOptions, NEWLINE_STRING } from '../options'; -import {IElement, assertXmlTagName, stringChunk, xmlElementText} from '../util'; +import { + IElement, + assertXmlTagName, + base64Decode, + base64Encode, + stringChunk, + xmlElementText +} from '../util'; import {Value} from '../value'; /** @@ -24,14 +31,14 @@ export class ValueData extends Value { /** * Value value. */ - public value = Buffer.alloc(0); + public value: Uint8Array; /** * ValueData constructor. * * @param value The value. */ - constructor(value = Buffer.alloc(0)) { + constructor(value = new Uint8Array(0)) { super(); this.value = value; @@ -42,11 +49,8 @@ export class ValueData extends Value { */ public fromXmlElement(element: Readonly) { assertXmlTagName(element, 'data'); - const b64 = xmlElementText(element)?.nodeValue || ''; - if (!/^[0-9a-z+/\s]*[=]?\s*[=]?\s*$/i.test(b64)) { - throw new Error(`Invalid base64 data: ${b64}`); - } - this.value = Buffer.from(b64, 'base64'); + const text = xmlElementText(element)?.nodeValue || ''; + this.value = base64Decode(text); } /** @@ -59,11 +63,11 @@ export class ValueData extends Value { const p = indentString.repeat(depth); const r = [`${p}`]; if (c > 0) { - for (const s of stringChunk(this.value.toString('base64'), c)) { + for (const s of stringChunk(base64Encode(this.value), c)) { r.push(`${p}${s}`); } } else { - r.push(`${p}${this.value.toString('base64')}`); + r.push(`${p}${base64Encode(this.value)}`); } r.push(`${p}`); return r.join(newlineString);