From: Lady Date: Sat, 23 Jul 2022 08:17:12 +0000 (-0700) Subject: Refactor base64; remove runtime dependencies X-Git-Tag: 0.1.0~1 X-Git-Url: https://git.ladys.computer/Pisces/commitdiff_plain/337761c5caf8a56367c331e579dfc4d1d2003caf?ds=sidebyside;hp=f753ef493338c40b6b2941d77e54510fa3c1691c Refactor base64; remove runtime dependencies --- diff --git a/base64.js b/base64.js index ff58328..e9661b7 100644 --- a/base64.js +++ b/base64.js @@ -7,7 +7,55 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at . -import { getPrototype, hasOwnProperty } from "./object.js"; +import { fill, map, reduce } from "./collection.js"; +import { bind, call } from "./function.js"; +import { ceil, floor } from "./numeric.js"; +import { hasOwnProperty, objectCreate } from "./object.js"; +import { + getCodeUnit, + rawString, + stringFromCodeUnits, + stringReplace, +} from "./string.js"; + +const Buffer = ArrayBuffer; +const View = DataView; +const TypedArray = Object.getPrototypeOf(Uint8Array); +const { prototype: arrayPrototype } = Array; +const { prototype: bufferPrototype } = Buffer; +const { iterator: iteratorSymbol } = Symbol; +const { prototype: rePrototype } = RegExp; +const { prototype: typedArrayPrototype } = TypedArray; +const { prototype: viewPrototype } = View; + +const { [iteratorSymbol]: arrayIterator } = arrayPrototype; +const { + next: arrayIteratorNext, +} = Object.getPrototypeOf([][iteratorSymbol]()); +const getBufferByteLength = + Object.getOwnPropertyDescriptor(bufferPrototype, "byteLength").get; +const getTypedArrayBuffer = + Object.getOwnPropertyDescriptor(typedArrayPrototype, "buffer").get; +const getViewBuffer = + Object.getOwnPropertyDescriptor(viewPrototype, "buffer").get; +const { exec: reExec } = rePrototype; +const { + getUint8: viewGetUint8, + setUint8: viewSetUint8, + setUint16: viewSetUint16, +} = viewPrototype; + +const base64CodeUnitIterablePrototype = { + [iteratorSymbol]() { + return { + next: bind( + arrayIteratorNext, + call(arrayIterator, this, []), + [], + ), + }; + }, +}; /** * Returns an ArrayBuffer generated from the provided Base64. @@ -16,17 +64,21 @@ import { getPrototype, hasOwnProperty } from "./object.js"; * literal will be interpreted akin to `String.raw`. */ export const a2b = ($, ...$s) => { - const source = ( + const source = stringReplace( typeof $ == "string" ? $ : hasOwnProperty($, "raw") - ? String.raw($, ...$s) - : `${$}` - ).replace(/[\t\n\f\r ]+/gu, ""); - const u6s = Array.prototype.map.call( - source.length % 4 == 0 ? source.replace(/={1,2}$/u, "") : source, - (character) => { - const code = character.charCodeAt(0); + ? rawString($, ...$s) + : `${$}`, + /[\t\n\f\r ]+/gu, + "", + ); + const u6s = map( + source.length % 4 == 0 + ? stringReplace(source, /={1,2}$/u, "") + : source, + (ucsCharacter) => { + const code = getCodeUnit(ucsCharacter, 0); const result = code >= 0x41 && code <= 0x5A ? code - 65 : code >= 0x61 && code <= 0x7A @@ -48,30 +100,28 @@ export const a2b = ($, ...$s) => { }, ); const { length } = u6s; - const dataView = new DataView( - new ArrayBuffer(Math.floor(length * 3 / 4)), - ); + const dataView = new View(new Buffer(floor(length * 3 / 4))); for (let index = 0; index < length - 1;) { - const dataIndex = Math.ceil(index * 3 / 4); + const dataIndex = ceil(index * 3 / 4); const remainder = index % 3; if (remainder == 0) { - dataView.setUint8( + call(viewSetUint8, dataView, [ dataIndex, (u6s[index] << 2) + (u6s[++index] >> 4), - ); + ]); } else if (remainder == 1) { - dataView.setUint8( + call(viewSetUint8, dataView, [ dataIndex, ((u6s[index] & 0xF) << 4) + (u6s[++index] >> 2), - ); + ]); } else { - dataView.setUint8( + call(viewSetUint8, dataView, [ dataIndex, ((u6s[index] & 0x3) << 6) + u6s[++index], - ); + ]); } } - return dataView.buffer; + return call(getViewBuffer, dataView, []); }; /** @@ -82,45 +132,67 @@ export const a2b = ($, ...$s) => { * literal will be interpreted akin to `String.raw`. */ export const b2a = ($, ...$s) => { - const buffer = $ instanceof ArrayBuffer + const buffer = $ instanceof Buffer ? $ - : $ instanceof DataView || $ instanceof getPrototype(Uint8Array) - ? $.buffer + : $ instanceof View + ? call(getViewBuffer, $, []) + : $ instanceof TypedArray + ? call(getTypedArrayBuffer, $, []) : ((string) => - Array.prototype.reduce.call( - string, - (result, code·point, index) => ( - result.setUint16(index * 2, code·point.charCodeAt(0)), result + call( + getViewBuffer, + reduce( + string, + (result, ucsCharacter, index) => ( + call(viewSetUint16, result, [ + index * 2, + getCodeUnit(ucsCharacter, 0), + ]), result + ), + new View(new Buffer(string.length * 2)), ), - new DataView(new ArrayBuffer(string.length * 2)), - ).buffer)( + [], + ))( typeof $ == "string" ? $ - : Object.hasOwn($, "raw") - ? String.raw($, ...$s) + : hasOwnProperty($, "raw") + ? rawString($, ...$s) : `${$}`, ); - const dataView = new DataView(buffer); - const { byteLength } = buffer; - const minimumLengthOfResults = Math.ceil(byteLength * 4 / 3); - const resultingCode·points = new Array( - minimumLengthOfResults + (4 - (minimumLengthOfResults % 4)) % 4, - ).fill(0x3D); + const dataView = new View(buffer); + const byteLength = call(getBufferByteLength, buffer, []); + const minimumLengthOfResults = ceil(byteLength * 4 / 3); + const resultingCodeUnits = fill( + objectCreate( + base64CodeUnitIterablePrototype, + { + length: { + value: minimumLengthOfResults + + (4 - (minimumLengthOfResults % 4)) % 4, + }, + }, + ), + 0x3D, + ); for (let index = 0; index < byteLength;) { - const code·pointIndex = Math.ceil(index * 4 / 3); - const currentIndex = code·pointIndex + +( - index % 3 == 0 && resultingCode·points[code·pointIndex] != 0x3D + const codeUnitIndex = ceil(index * 4 / 3); + const currentIndex = codeUnitIndex + +( + index % 3 == 0 && resultingCodeUnits[codeUnitIndex] != 0x3D ); const remainder = currentIndex % 4; const u6 = remainder == 0 - ? dataView.getUint8(index) >> 2 + ? call(viewGetUint8, dataView, [index]) >> 2 : remainder == 1 - ? ((dataView.getUint8(index++) & 0x3) << 4) + - (index < byteLength ? dataView.getUint8(index) >> 4 : 0) + ? ((call(viewGetUint8, dataView, [index++]) & 0x3) << 4) + + (index < byteLength + ? call(viewGetUint8, dataView, [index]) >> 4 + : 0) : remainder == 2 - ? ((dataView.getUint8(index++) & 0xF) << 2) + - (index < byteLength ? dataView.getUint8(index) >> 6 : 0) - : dataView.getUint8(index++) & 0x3F; + ? ((call(viewGetUint8, dataView, [index++]) & 0xF) << 2) + + (index < byteLength + ? call(viewGetUint8, dataView, [index]) >> 6 + : 0) + : call(viewGetUint8, dataView, [index++]) & 0x3F; const result = u6 < 26 ? u6 + 65 : u6 < 52 @@ -135,10 +207,10 @@ export const b2a = ($, ...$s) => { if (result < 0) { throw new RangeError(`Piscēs: Unexpected Base64 value: ${u6}.`); } else { - resultingCode·points[currentIndex] = result; + resultingCodeUnits[currentIndex] = result; } } - return String.fromCodePoint(...resultingCode·points); + return stringFromCodeUnits(...resultingCodeUnits); }; /** @@ -147,11 +219,14 @@ export const b2a = ($, ...$s) => { * Returns false if the provided value is not a string primitive. */ export const isBase64 = ($) => { - if (typeof $ != "string") return false; - const source = $.replace(/[\t\n\f\r ]+/gu, ""); - const trimmed = source.length % 4 == 0 - ? source.replace(/={1,2}$/u, "") - : source; - return trimmed.length % 4 != 1 && - !/[^0-9A-Za-z+\/]/u.test(trimmed); + if (typeof $ !== "string") { + return false; + } else { + const source = stringReplace($, /[\t\n\f\r ]+/gu, ""); + const trimmed = source.length % 4 == 0 + ? stringReplace(source, /={1,2}$/u, "") + : source; + return trimmed.length % 4 != 1 && + call(reExec, /[^0-9A-Za-z+\/]/u, [trimmed]) == null; + } }; diff --git a/base64.test.js b/base64.test.js index e6361a4..84d9057 100755 --- a/base64.test.js +++ b/base64.test.js @@ -7,13 +7,15 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at . -import { a2b, b2a, isBase64 } from "./base64.js"; import { assert, assertEquals, assertStrictEquals, assertThrows, + describe, + it, } from "./dev-deps.js"; +import { a2b, b2a, isBase64 } from "./base64.js"; const base64s = { "AGIAYQBzAGUANgA0": "base64", @@ -24,17 +26,8 @@ const base64s = { ), }; -Deno.test({ - name: "Calling b2a returns the correct string.", - fn: () => - Object.entries(base64s).forEach(([key, value]) => - assertStrictEquals(b2a(value), key) - ), -}); - -Deno.test({ - name: "Calling a2b returns the correct data.", - fn: () => { +describe("a2b", () => { + it("[[Call]] returns the correct data", () => { assertEquals( a2b("AGIAYQBzAGUANgA0"), new Uint8Array( @@ -43,50 +36,53 @@ Deno.test({ ($) => $.charCodeAt(0), ), ).buffer, + "AGIAYQBzAGUANgA0", ); assertEquals( a2b("R/Q="), new Uint16Array([62535]).buffer, + "R/Q=", ); assertEquals( a2b("S0lCSQ=="), new Uint8ClampedArray([75, 73, 66, 73]).buffer, + "S0lCSQ==", ); assertEquals( a2b("YmFzZTY0"), new Uint8Array([98, 97, 115, 101, 54, 52]).buffer, + "YmFzZTY0", ); - }, -}); + }); -Deno.test({ - name: "Calling a2b throws when provided with an invalid character.", - fn: () => assertThrows(() => a2b("abc_")), -}); + it("[[Call]] throws when provided with an invalid character", () => { + assertThrows(() => a2b("abc_")); + }); -Deno.test({ - name: "Calling a2b throws when provided with an invalid equals.", - fn: () => assertThrows(() => a2b("abc==")), -}); + it("[[Call]] throws when provided with an invalid equals", () => { + assertThrows(() => a2b("abc==")); + }); -Deno.test({ - name: - "Calling a2b throws when provided with a length with a remainder of 1 when divided by 4.", - fn: () => { + it("[[Call]] throws when provided with a length with a remainder of 1 when divided by 4", () => { assertThrows(() => a2b("abcde")); assertThrows(() => a2b("abcde===")); - }, + }); }); -Deno.test({ - name: "isBase64 returns true for base64 strings.", - fn: () => - Object.keys(base64s).forEach((key) => assert(isBase64(key))), +describe("b2a", () => { + it("[[Call]] returns the correct string", () => { + Object.entries(base64s).forEach(([key, value]) => + assertStrictEquals(b2a(value), key) + ); + }); }); -Deno.test({ - name: "isBase64 returns false for others.", - fn: () => +describe("isBase64", () => { + it("[[Call]] returns true for base64 strings", () => { + Object.keys(base64s).forEach((key) => assert(isBase64(key))); + }); + + it("[[Call]] returns false for others", () => { [ undefined, null, @@ -101,5 +97,6 @@ Deno.test({ "abc_", "a", "abc==", - ].forEach((value) => assert(!isBase64(value))), + ].forEach((value) => assert(!isBase64(value))); + }); });