From: Lady Date: Sun, 25 Sep 2022 07:00:10 +0000 (-0700) Subject: Rename base64.js to binary.js; reorganize contents X-Git-Tag: 0.3.0~8 X-Git-Url: https://git.ladys.computer/Pisces/commitdiff_plain/3d2a5e40eaf384aee6b9699bc71af015fd2cb2e9?ds=inline;hp=cb5a5ceca492121141a12d921afc78dbde7fbfa1 Rename base64.js to binary.js; reorganize contents `atob` and `btoa` now have the much clearer names `base64Binary` and `base64String`; filename‐safe equivalents have been added. The intention is to add the base16 and base32 formats from R·F·C 3548 as well, although that work is still forthcoming. --- diff --git a/base64.test.js b/base64.test.js deleted file mode 100755 index 84d9057..0000000 --- a/base64.test.js +++ /dev/null @@ -1,102 +0,0 @@ -// ♓🌟 Piscēs ∷ base64.test.js -// ==================================================================== -// -// Copyright © 2020–2022 Lady [@ Lady’s Computer]. -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at . - -import { - assert, - assertEquals, - assertStrictEquals, - assertThrows, - describe, - it, -} from "./dev-deps.js"; -import { a2b, b2a, isBase64 } from "./base64.js"; - -const base64s = { - "AGIAYQBzAGUANgA0": "base64", - "R/Q=": new Uint16Array([62535]), - "S0lCSQ==": new Uint8ClampedArray([75, 73, 66, 73]).buffer, - YmFzZTY0: new DataView( - new Uint8Array([98, 97, 115, 101, 54, 52]).buffer, - ), -}; - -describe("a2b", () => { - it("[[Call]] returns the correct data", () => { - assertEquals( - a2b("AGIAYQBzAGUANgA0"), - new Uint8Array( - Array.prototype.map.call( - "\u{0}b\u{0}a\u{0}s\u{0}e\u{0}6\u{0}4", - ($) => $.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", - ); - }); - - it("[[Call]] throws when provided with an invalid character", () => { - assertThrows(() => a2b("abc_")); - }); - - it("[[Call]] throws when provided with an invalid equals", () => { - assertThrows(() => a2b("abc==")); - }); - - it("[[Call]] throws when provided with a length with a remainder of 1 when divided by 4", () => { - assertThrows(() => a2b("abcde")); - assertThrows(() => a2b("abcde===")); - }); -}); - -describe("b2a", () => { - it("[[Call]] returns the correct string", () => { - Object.entries(base64s).forEach(([key, value]) => - assertStrictEquals(b2a(value), key) - ); - }); -}); - -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, - true, - Symbol(), - 27, - 98n, - {}, - [], - () => {}, - new Proxy({}, {}), - "abc_", - "a", - "abc==", - ].forEach((value) => assert(!isBase64(value))); - }); -}); diff --git a/base64.js b/binary.js similarity index 77% rename from base64.js rename to binary.js index e9661b7..dc4b612 100644 --- a/base64.js +++ b/binary.js @@ -1,4 +1,4 @@ -// ♓🌟 Piscēs ∷ base64.js +// ♓🌟 Piscēs ∷ binary.js // ==================================================================== // // Copyright © 2020–2022 Lady [@ Lady’s Computer]. @@ -45,7 +45,36 @@ const { setUint16: viewSetUint16, } = viewPrototype; -const base64CodeUnitIterablePrototype = { +const bufferFromArgs = ($, $s) => + $ instanceof Buffer + ? $ + : $ instanceof View + ? call(getViewBuffer, $, []) + : $ instanceof TypedArray + ? call(getTypedArrayBuffer, $, []) + : ((string) => + call( + getViewBuffer, + reduce( + string, + (result, ucsCharacter, index) => ( + call(viewSetUint16, result, [ + index * 2, + getCodeUnit(ucsCharacter, 0), + ]), result + ), + new View(new Buffer(string.length * 2)), + ), + [], + ))( + typeof $ == "string" + ? $ + : hasOwnProperty($, "raw") + ? rawString($, ...$s) + : `${$}`, + ); + +const binaryCodeUnitIterablePrototype = { [iteratorSymbol]() { return { next: bind( @@ -57,22 +86,7 @@ const base64CodeUnitIterablePrototype = { }, }; -/** - * Returns an ArrayBuffer generated from the provided Base64. - * - * This function can also be used as a tag for a template literal. The - * literal will be interpreted akin to `String.raw`. - */ -export const a2b = ($, ...$s) => { - const source = stringReplace( - typeof $ == "string" - ? $ - : hasOwnProperty($, "raw") - ? rawString($, ...$s) - : `${$}`, - /[\t\n\f\r ]+/gu, - "", - ); +const decodeBase64 = (source, safe = false) => { const u6s = map( source.length % 4 == 0 ? stringReplace(source, /={1,2}$/u, "") @@ -85,9 +99,9 @@ export const a2b = ($, ...$s) => { ? code - 71 : code >= 0x30 && code <= 0x39 ? code + 4 - : code == 0x2B + : code == (safe ? 0x2D : 0x2B) ? 62 - : code == 0x2F + : code == (safe ? 0x5F : 0x2F) ? 63 : -1; if (result < 0) { @@ -124,47 +138,13 @@ export const a2b = ($, ...$s) => { return call(getViewBuffer, dataView, []); }; -/** - * Returns a (big‐endian) base64 string created from a typed array, - * buffer, or string. - * - * This function can also be used as a tag for a template literal. The - * literal will be interpreted akin to `String.raw`. - */ -export const b2a = ($, ...$s) => { - const buffer = $ instanceof Buffer - ? $ - : $ instanceof View - ? call(getViewBuffer, $, []) - : $ instanceof TypedArray - ? call(getTypedArrayBuffer, $, []) - : ((string) => - call( - getViewBuffer, - reduce( - string, - (result, ucsCharacter, index) => ( - call(viewSetUint16, result, [ - index * 2, - getCodeUnit(ucsCharacter, 0), - ]), result - ), - new View(new Buffer(string.length * 2)), - ), - [], - ))( - typeof $ == "string" - ? $ - : hasOwnProperty($, "raw") - ? rawString($, ...$s) - : `${$}`, - ); +const encodeBase64 = (buffer, safe = false) => { const dataView = new View(buffer); const byteLength = call(getBufferByteLength, buffer, []); const minimumLengthOfResults = ceil(byteLength * 4 / 3); const resultingCodeUnits = fill( objectCreate( - base64CodeUnitIterablePrototype, + binaryCodeUnitIterablePrototype, { length: { value: minimumLengthOfResults + @@ -200,9 +180,9 @@ export const b2a = ($, ...$s) => { : u6 < 62 ? u6 - 4 : u6 < 63 - ? 43 + ? (safe ? 0x2D : 0x2B) : u6 < 64 - ? 47 + ? (safe ? 0x5F : 0x2F) : -1; if (result < 0) { throw new RangeError(`Piscēs: Unexpected Base64 value: ${u6}.`); @@ -213,6 +193,56 @@ export const b2a = ($, ...$s) => { return stringFromCodeUnits(...resultingCodeUnits); }; +const sourceFromArgs = ($, $s) => + stringReplace( + typeof $ == "string" + ? $ + : hasOwnProperty($, "raw") + ? rawString($, ...$s) + : `${$}`, + /[\t\n\f\r ]+/gu, + "", + ); + +/** + * Returns an ArrayBuffer generated from the provided Base64. + * + * This function can also be used as a tag for a template literal. The + * literal will be interpreted akin to `String.raw`. + */ +export const base64Binary = ($, ...$s) => + decodeBase64(sourceFromArgs($, $s)); + +/** + * Returns a (big‐endian) base64 string created from a typed array, + * buffer, or (16‐bit) string. + * + * This function can also be used as a tag for a template literal. The + * literal will be interpreted akin to `String.raw`. + */ +export const base64String = ($, ...$s) => + encodeBase64(bufferFromArgs($, $s)); + +/** + * Returns an ArrayBuffer generated from the provided filename‐safe + * Base64. + * + * This function can also be used as a tag for a template literal. The + * literal will be interpreted akin to `String.raw`. + */ +export const filenameSafeBase64Binary = ($, ...$s) => + decodeBase64(sourceFromArgs($, $s), true); + +/** + * Returns a (big‐endian) filename‐safe base64 string created from a + * typed array, buffer, or (16‐bit) string. + * + * This function can also be used as a tag for a template literal. The + * literal will be interpreted akin to `String.raw`. + */ +export const filenameSafeBase64String = ($, ...$s) => + encodeBase64(bufferFromArgs($, $s), true); + /** * Returns whether the provided value is a Base64 string. * @@ -230,3 +260,21 @@ export const isBase64 = ($) => { call(reExec, /[^0-9A-Za-z+\/]/u, [trimmed]) == null; } }; + +/** + * Returns whether the provided value is a filename‐safe base64 string. + * + * Returns false if the provided value is not a string primitive. + */ +export const isFilenameSafeBase64 = ($) => { + 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/binary.test.js b/binary.test.js new file mode 100755 index 0000000..161f8db --- /dev/null +++ b/binary.test.js @@ -0,0 +1,191 @@ +// ♓🌟 Piscēs ∷ binary.test.js +// ==================================================================== +// +// Copyright © 2020–2022 Lady [@ Lady’s Computer]. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at . + +import { + assert, + assertEquals, + assertStrictEquals, + assertThrows, + describe, + it, +} from "./dev-deps.js"; +import { + base64Binary, + base64String, + filenameSafeBase64Binary, + filenameSafeBase64String, + isBase64, + isFilenameSafeBase64, +} from "./binary.js"; + +const base64s = { + "AGIAYQBzAGUANgA0": "base64", + "R/Q=": new Uint16Array([62535]), + "S0lCSQ==": new Uint8ClampedArray([75, 73, 66, 73]).buffer, + YmFzZTY0: new DataView( + new Uint8Array([98, 97, 115, 101, 54, 52]).buffer, + ), +}; + +describe("base64Binary", () => { + it("[[Call]] returns the correct data", () => { + assertEquals( + base64Binary("AGIAYQBzAGUANgA0"), + new Uint8Array( + Array.prototype.map.call( + "\u{0}b\u{0}a\u{0}s\u{0}e\u{0}6\u{0}4", + ($) => $.charCodeAt(0), + ), + ).buffer, + "AGIAYQBzAGUANgA0", + ); + assertEquals( + base64Binary("R/Q="), + new Uint16Array([62535]).buffer, + "R/Q=", + ); + assertEquals( + base64Binary("S0lCSQ=="), + new Uint8ClampedArray([75, 73, 66, 73]).buffer, + "S0lCSQ==", + ); + assertEquals( + base64Binary("YmFzZTY0"), + new Uint8Array([98, 97, 115, 101, 54, 52]).buffer, + "YmFzZTY0", + ); + }); + + it("[[Call]] throws when provided with an invalid character", () => { + assertThrows(() => base64Binary("abc_")); + }); + + it("[[Call]] throws when provided with an invalid equals", () => { + assertThrows(() => base64Binary("abc==")); + }); + + it("[[Call]] throws when provided with a length with a remainder of 1 when divided by 4", () => { + assertThrows(() => base64Binary("abcde")); + assertThrows(() => base64Binary("abcde===")); + }); +}); + +describe("base64String", () => { + it("[[Call]] returns the correct string", () => { + Object.entries(base64s).forEach(([key, value]) => + assertStrictEquals(base64String(value), key) + ); + }); +}); + +describe("filenameSafeBase64Binary", () => { + it("[[Call]] returns the correct data", () => { + assertEquals( + filenameSafeBase64Binary("AGIAYQBzAGUANgA0"), + new Uint8Array( + Array.prototype.map.call( + "\u{0}b\u{0}a\u{0}s\u{0}e\u{0}6\u{0}4", + ($) => $.charCodeAt(0), + ), + ).buffer, + "AGIAYQBzAGUANgA0", + ); + assertEquals( + filenameSafeBase64Binary("R_Q="), + new Uint16Array([62535]).buffer, + "R/Q=", + ); + assertEquals( + filenameSafeBase64Binary("S0lCSQ=="), + new Uint8ClampedArray([75, 73, 66, 73]).buffer, + "S0lCSQ==", + ); + assertEquals( + filenameSafeBase64Binary("YmFzZTY0"), + new Uint8Array([98, 97, 115, 101, 54, 52]).buffer, + "YmFzZTY0", + ); + }); + + it("[[Call]] throws when provided with an invalid character", () => { + assertThrows(() => filenameSafeBase64Binary("abc/")); + }); + + it("[[Call]] throws when provided with an invalid equals", () => { + assertThrows(() => filenameSafeBase64Binary("abc==")); + }); + + it("[[Call]] throws when provided with a length with a remainder of 1 when divided by 4", () => { + assertThrows(() => filenameSafeBase64Binary("abcde")); + assertThrows(() => filenameSafeBase64Binary("abcde===")); + }); +}); + +describe("filenameSafeBase64String", () => { + it("[[Call]] returns the correct string", () => { + Object.entries(base64s).forEach(([key, value]) => + assertStrictEquals( + filenameSafeBase64String(value), + key.replace("+", "-").replace("/", "_"), + ) + ); + }); +}); + +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, + true, + Symbol(), + 27, + 98n, + {}, + [], + () => {}, + new Proxy({}, {}), + "abc_", + "a", + "abc==", + ].forEach((value) => assert(!isBase64(value))); + }); +}); + +describe("isFilenameSafeBase64", () => { + it("[[Call]] returns true for filename‐safe base64 strings", () => { + Object.keys(base64s).forEach((key) => + assert( + isFilenameSafeBase64(key.replace("+", "-").replace("/", "_")), + ) + ); + }); + + it("[[Call]] returns false for others", () => { + [ + undefined, + null, + true, + Symbol(), + 27, + 98n, + {}, + [], + () => {}, + new Proxy({}, {}), + "abc/", + "a", + "abc==", + ].forEach((value) => assert(!isFilenameSafeBase64(value))); + }); +}); diff --git a/mod.js b/mod.js index 42b421c..13840c9 100644 --- a/mod.js +++ b/mod.js @@ -7,7 +7,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at . -export * from "./base64.js"; +export * from "./binary.js"; export * from "./collection.js"; export * from "./function.js"; export * from "./iri.js";