X-Git-Url: https://git.ladys.computer/Pisces/blobdiff_plain/cb5a5ceca492121141a12d921afc78dbde7fbfa1..3d2a5e40eaf384aee6b9699bc71af015fd2cb2e9:/binary.js diff --git a/binary.js b/binary.js new file mode 100644 index 0000000..dc4b612 --- /dev/null +++ b/binary.js @@ -0,0 +1,280 @@ +// ♓🌟 Piscēs ∷ binary.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 { 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 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( + arrayIteratorNext, + call(arrayIterator, this, []), + [], + ), + }; + }, +}; + +const decodeBase64 = (source, safe = false) => { + 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 + ? code - 71 + : code >= 0x30 && code <= 0x39 + ? code + 4 + : code == (safe ? 0x2D : 0x2B) + ? 62 + : code == (safe ? 0x5F : 0x2F) + ? 63 + : -1; + if (result < 0) { + throw new RangeError( + `Piscēs: Invalid character in Base64: ${character}.`, + ); + } else { + return result; + } + }, + ); + const { length } = u6s; + const dataView = new View(new Buffer(floor(length * 3 / 4))); + for (let index = 0; index < length - 1;) { + const dataIndex = ceil(index * 3 / 4); + const remainder = index % 3; + if (remainder == 0) { + call(viewSetUint8, dataView, [ + dataIndex, + (u6s[index] << 2) + (u6s[++index] >> 4), + ]); + } else if (remainder == 1) { + call(viewSetUint8, dataView, [ + dataIndex, + ((u6s[index] & 0xF) << 4) + (u6s[++index] >> 2), + ]); + } else { + call(viewSetUint8, dataView, [ + dataIndex, + ((u6s[index] & 0x3) << 6) + u6s[++index], + ]); + } + } + return call(getViewBuffer, dataView, []); +}; + +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( + binaryCodeUnitIterablePrototype, + { + length: { + value: minimumLengthOfResults + + (4 - (minimumLengthOfResults % 4)) % 4, + }, + }, + ), + 0x3D, + ); + for (let index = 0; index < byteLength;) { + const codeUnitIndex = ceil(index * 4 / 3); + const currentIndex = codeUnitIndex + +( + index % 3 == 0 && resultingCodeUnits[codeUnitIndex] != 0x3D + ); + const remainder = currentIndex % 4; + const u6 = remainder == 0 + ? call(viewGetUint8, dataView, [index]) >> 2 + : remainder == 1 + ? ((call(viewGetUint8, dataView, [index++]) & 0x3) << 4) + + (index < byteLength + ? call(viewGetUint8, dataView, [index]) >> 4 + : 0) + : remainder == 2 + ? ((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 + ? u6 + 71 + : u6 < 62 + ? u6 - 4 + : u6 < 63 + ? (safe ? 0x2D : 0x2B) + : u6 < 64 + ? (safe ? 0x5F : 0x2F) + : -1; + if (result < 0) { + throw new RangeError(`Piscēs: Unexpected Base64 value: ${u6}.`); + } else { + resultingCodeUnits[currentIndex] = result; + } + } + 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. + * + * Returns false if the provided value is not a string primitive. + */ +export const isBase64 = ($) => { + 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; + } +}; + +/** + * 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; + } +};