From: Lady Date: Sat, 20 May 2023 01:05:02 +0000 (-0700) Subject: Add base16 support to binary.js X-Git-Tag: 0.3.0~3 X-Git-Url: https://git.ladys.computer/Pisces/commitdiff_plain/295c32f3bca325fc406aeb9fa20adb643c8bf429?ds=sidebyside;hp=8d2beebdeea4000740fc3914f5d568f473629951 Add base16 support to binary.js Just iterating over the bytes and doing `$.toString(16).toUpperCase()` or `parseInt($, 16)` might actually be faster, but I kind of like using an actual algorithm to ensure parity with base64 ⁊·c processing. --- diff --git a/binary.js b/binary.js index 362161b..25fd055 100644 --- a/binary.js +++ b/binary.js @@ -106,6 +106,50 @@ const bufferFromArgs = ($, $s) => : `${$}`, ); +/** + * Returns the result of decoding the provided base16 string into an + * ArrayBuffer. + * + * ※ This function is not exposed. + */ +const decodeBase16 = (source) => { + const u4s = map( + source, + (ucsCharacter) => { + const code = getCodeUnit(ucsCharacter, 0); + const result = code >= 0x30 && code <= 0x39 + ? code - 48 + : code >= 0x41 && code <= 0x46 + ? code - 55 + : code >= 0x61 && code <= 0x66 + ? code - 87 + : -1; + if (result < 0) { + throw new RangeError( + `Piscēs: Invalid character in Base64: ${ucsCharacter}.`, + ); + } else { + return result; + } + }, + ); + const { length } = u4s; + if (length % 2 == 1) { + throw new RangeError( + `Piscēs: Base16 string has invalid length: ${source}.`, + ); + } else { + const dataView = new View(new Buffer(floor(length / 2))); + for (let index = 0; index < length - 1;) { + call(viewSetUint8, dataView, [ + floor(index / 2), + (u4s[index] << 4) | u4s[++index, index++], + ]); + } + return call(getViewBuffer, dataView, []); + } +}; + /** * Returns the result of decoding the provided base64 string into an * ArrayBuffer. @@ -173,6 +217,42 @@ const decodeBase64 = (source, safe = false) => { } }; +/** + * Returns the result of encoding the provided ArrayBuffer into a + * base16 string. + * + * ※ This function is not exposed. + */ +const encodeBase16 = (buffer) => { + const dataView = new View(buffer); + const byteLength = call(getBufferByteLength, buffer, []); + const minimumLengthOfResults = byteLength * 2; + const resultingCodeUnits = fill( + objectCreate( + binaryCodeUnitIterablePrototype, + { length: { value: minimumLengthOfResults } }, + ), + 0x3D, + ); + for (let index = 0; index < byteLength;) { + const codeUnitIndex = index * 2; + const datum = call(viewGetUint8, dataView, [index++]); + const u4s = [datum >> 4, datum & 0xF]; + for (let u4i = 0; u4i < 2; ++u4i) { + const u4 = u4s[u4i]; + const result = u4 < 10 ? u4 + 48 : u4 < 16 ? u4 + 55 : -1; + if (result < 0) { + throw new RangeError( + `Piscēs: Unexpected Base16 value: ${u4}.`, + ); + } else { + resultingCodeUnits[codeUnitIndex + u4i] = result; + } + } + } + return stringFromCodeUnits(...resultingCodeUnits); +}; + /** * Returns the result of encoding the provided ArrayBuffer into a * base64 string. @@ -253,6 +333,28 @@ const sourceFromArgs = ($, $s) => "", ); +/** + * Returns an ArrayBuffer generated from the provided base16 string. + * + * This function can also be used as a tag for a template literal. The + * literal will be interpreted akin to `String.raw`. + * + * ☡ This function throws if the provided string is not a valid base16 + * string. + */ +export const base16Binary = ($, ...$s) => + decodeBase16(sourceFromArgs($, $s)); + +/** + * Returns a (big‐endian) base16 string created from the provided 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 base16String = ($, ...$s) => + encodeBase16(bufferFromArgs($, $s)); + /** * Returns an ArrayBuffer generated from the provided base64 string. * @@ -298,6 +400,22 @@ export const filenameSafeBase64Binary = ($, ...$s) => export const filenameSafeBase64String = ($, ...$s) => encodeBase64(bufferFromArgs($, $s), true); +/** + * Returns whether the provided value is a base16 string. + * + * ※ This function returns false if the provided value is not a string + * primitive. + */ +export const isBase16 = ($) => { + if (typeof $ !== "string") { + return false; + } else { + const source = stringReplace($, /[\t\n\f\r ]+/gu, ""); + return source.length % 2 != 1 && + call(reExec, /[^0-9A-F]/iu, [source]) == null; + } +}; + /** * Returns whether the provided value is a Base64 string. * diff --git a/binary.test.js b/binary.test.js index be6c6a3..9dbce57 100755 --- a/binary.test.js +++ b/binary.test.js @@ -16,10 +16,13 @@ import { it, } from "./dev-deps.js"; import { + base16Binary, + base16String, base64Binary, base64String, filenameSafeBase64Binary, filenameSafeBase64String, + isBase16, isBase64, isFilenameSafeBase64, } from "./binary.js"; @@ -27,59 +30,188 @@ import { // These tests assume a LITTLE‐ENDIAN environment. const data = new Map([ ["", { + base16: "", base64: "", }], ["base64", { + base16: "006200610073006500360034", base64: "AGIAYQBzAGUANgA0", }], [new Uint16Array([62535]), { + base16: "47F4", base64: "R/Q=", }], [new Uint8ClampedArray([75, 73, 66, 73]).buffer, { + base16: "4B494249", base64: "S0lCSQ==", }], [new DataView(new Uint8Array([98, 97, 115, 101, 54, 52]).buffer), { + base16: "626173653634", base64: "YmFzZTY0", }], // The following three examples are from RFC 3548. [new Uint8Array([0x14, 0xFB, 0x9C, 0x03, 0xD9, 0x7E]), { + base16: "14FB9C03D97E", base64: "FPucA9l+", }], [new Uint8Array([0x14, 0xFB, 0x9C, 0x03, 0xD9]), { + base16: "14FB9C03D9", base64: "FPucA9k=", }], [new Uint8Array([0x14, 0xFB, 0x9C, 0x03]), { + base16: "14FB9C03", base64: "FPucAw==", }], // The following examples are from the Ruby base32 gem. [new Uint8Array([0x28]), { + base16: "28", base64: "KA==", }], [new Uint8Array([0xD6]), { + base16: "D6", base64: "1g==", }], [new Uint16Array([0xF8D6]), { + base16: "D6F8", base64: "1vg=", }], [new Uint8Array([0xD6, 0xF8, 0x00]), { + base16: "D6F800", base64: "1vgA", }], [new Uint8Array([0xD6, 0xF8, 0x10]), { + base16: "D6F810", base64: "1vgQ", }], [new Uint32Array([0x0C11F8D6]), { + base16: "D6F8110C", base64: "1vgRDA==", }], [new Uint8Array([0xD6, 0xF8, 0x11, 0x0C, 0x80]), { + base16: "D6F8110C80", base64: "1vgRDIA=", }], [new Uint16Array([0xF8D6, 0x0C11, 0x3085]), { + base16: "D6F8110C8530", base64: "1vgRDIUw", }], ]); +describe("base16Binary", () => { + it("[[Call]] returns the correct data", () => { + assertEquals( + new Uint8Array(base16Binary("")), + new Uint8Array([]), + "", + ); + assertEquals( + new Uint8Array(base16Binary("006200610073006500360034")), + Uint8Array.from( + "\u{0}b\u{0}a\u{0}s\u{0}e\u{0}6\u{0}4", + ($) => $.charCodeAt(0), + ), + "006200610073006500360034", + ); + assertEquals( + new Uint16Array(base16Binary("47F4")), + new Uint16Array([62535]), + "47F4", + ); + assertEquals( + new Uint8ClampedArray(base16Binary("4B494249")), + new Uint8ClampedArray([75, 73, 66, 73]), + "4B494249", + ); + assertEquals( + new Uint8Array(base16Binary("626173653634")), + new Uint8Array([98, 97, 115, 101, 54, 52]), + "626173653634", + ); + assertEquals( + new Uint8Array(base16Binary("14FB9C03D97E")), + new Uint8Array([0x14, 0xFB, 0x9C, 0x03, 0xD9, 0x7E]), + "14FB9C03D97E", + ); + assertEquals( + new Uint8Array(base16Binary("14FB9C03D9")), + new Uint8Array([0x14, 0xFB, 0x9C, 0x03, 0xD9]), + "14FB9C03D9", + ); + assertEquals( + new Uint8Array(base16Binary("14FB9C03")), + new Uint8Array([0x14, 0xFB, 0x9C, 0x03]), + "14FB9C03", + ); + assertEquals( + new Uint8Array(base16Binary("28")), + new Uint8Array([0x28]), + "28", + ); + assertEquals( + new Uint8Array(base16Binary("D6")), + new Uint8Array([0xD6]), + "D6", + ); + assertEquals( + new Uint16Array(base16Binary("D6F8")), + new Uint16Array([0xF8D6]), + "D6F8", + ); + assertEquals( + new Uint8Array(base16Binary("D6F800")), + new Uint8Array([0xD6, 0xF8, 0x00]), + "D6F800", + ); + assertEquals( + new Uint8Array(base16Binary("D6F810")), + new Uint8Array([0xD6, 0xF8, 0x10]), + "D6F810", + ); + assertEquals( + new Uint32Array(base16Binary("D6F8110C")), + new Uint32Array([0x0C11F8D6]), + "D6F8110C", + ); + assertEquals( + new Uint8Array(base16Binary("D6F8110C80")), + new Uint8Array([0xD6, 0xF8, 0x11, 0x0C, 0x80]), + "D6F8110C80", + ); + assertEquals( + new Uint16Array(base16Binary("D6F8110C8530")), + new Uint16Array([0xF8D6, 0x0C11, 0x3085]), + "D6F8110C8530", + ); + }); + + it("[[Call]] is case‐insensitive", () => { + assertEquals( + new Uint8Array(base16Binary("d6f8110C80")), + new Uint8Array([0xD6, 0xF8, 0x11, 0x0C, 0x80]), + "d6f8110C80", + ); + }); + + it("[[Call]] throws when provided with an invalid character", () => { + assertThrows(() => base16Binary("ABCG")); + }); + + it("[[Call]] throws when provided with a length with a remainder of 1 when divided by 2", () => { + assertThrows(() => base16Binary("A")); + assertThrows(() => base16Binary("ABC")); + }); +}); + +describe("base16String", () => { + it("[[Call]] returns the correct string", () => { + for (const [source, { base16 }] of data) { + assertStrictEquals(base16String(source), base16); + } + }); +}); + describe("base64Binary", () => { it("[[Call]] returns the correct data", () => { assertEquals( @@ -286,6 +418,34 @@ describe("filenameSafeBase64String", () => { }); }); +describe("isBase16", () => { + it("[[Call]] returns true for base64 strings", () => { + for (const { base16 } of data.values()) { + assert(isBase16(base16)); + assert(isBase16(base16.toLowerCase())); + } + }); + + it("[[Call]] returns false for others", () => { + [ + undefined, + null, + true, + Symbol(), + 27, + 98n, + {}, + [], + () => {}, + new Proxy({}, {}), + "abc_", + "a", + "abc", + "abcg", + ].forEach((value) => assert(!isBase16(value))); + }); +}); + describe("isBase64", () => { it("[[Call]] returns true for base64 strings", () => { for (const { base64 } of data.values()) {