]> Lady’s Gitweb - Pisces/commitdiff
Add base16 support to binary.js
authorLady <redacted>
Sat, 20 May 2023 01:05:02 +0000 (18:05 -0700)
committerLady <redacted>
Sat, 20 May 2023 01:05:02 +0000 (18:05 -0700)
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.

binary.js
binary.test.js

index 362161bb8e83bc4268d7cfcf654ccb0600dc15f0..25fd05586ee86a497b2efb662e433ec9055f0b31 100644 (file)
--- 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.
  *
index be6c6a3682a04b11fbaaaca1611717c020e79cf7..9dbce5753e7ee3970093973517a82fc6e595d6d1 100755 (executable)
@@ -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([]),
+      "<empty>",
+    );
+    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()) {
This page took 0.079828 seconds and 4 git commands to generate.