X-Git-Url: https://git.ladys.computer/Pisces/blobdiff_plain/b353e413a079066e14c779fc1e203e27ffcd92dd..refs/heads/current:/binary.js diff --git a/binary.js b/binary.js index 272775f..c149daf 100644 --- a/binary.js +++ b/binary.js @@ -1,7 +1,7 @@ // ♓🌟 Piscēs ∷ binary.js // ==================================================================== // -// Copyright © 2020–2022 Lady [@ Lady’s Computer]. +// Copyright © 2020–2023 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 @@ -17,23 +17,24 @@ import { stringFromCodeUnits, stringReplace, } from "./string.js"; +import { ITERATOR } from "./value.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: sharedBufferPrototype } = SharedArrayBuffer; const { prototype: rePrototype } = RegExp; const { prototype: typedArrayPrototype } = TypedArray; const { prototype: viewPrototype } = View; -const { [iteratorSymbol]: arrayIterator } = arrayPrototype; +const { [ITERATOR]: arrayIterator } = arrayPrototype; const { next: arrayIteratorNext, -} = Object.getPrototypeOf([][iteratorSymbol]()); +} = Object.getPrototypeOf([][ITERATOR]()); const argumentIterablePrototype = { - [iteratorSymbol]() { + [ITERATOR]() { return { next: bind( arrayIteratorNext, @@ -44,7 +45,7 @@ const argumentIterablePrototype = { }, }; const binaryCodeUnitIterablePrototype = { - [iteratorSymbol]() { + [ITERATOR]() { return { next: bind( arrayIteratorNext, @@ -55,27 +56,61 @@ const binaryCodeUnitIterablePrototype = { }, }; +const { exec: reExec } = rePrototype; +const { slice: bufferSlice } = bufferPrototype; const getBufferByteLength = Object.getOwnPropertyDescriptor(bufferPrototype, "byteLength").get; +const { slice: sharedBufferSlice } = sharedBufferPrototype; +const getSharedBufferByteLength = + Object.getOwnPropertyDescriptor(sharedBufferPrototype, "byteLength") + .get; const getTypedArrayBuffer = Object.getOwnPropertyDescriptor(typedArrayPrototype, "buffer").get; +const getTypedArrayByteLength = + Object.getOwnPropertyDescriptor(typedArrayPrototype, "byteLength") + .get; +const getTypedArrayByteOffset = + Object.getOwnPropertyDescriptor(typedArrayPrototype, "byteOffset") + .get; const getViewBuffer = Object.getOwnPropertyDescriptor(viewPrototype, "buffer").get; -const { exec: reExec } = rePrototype; +const getViewByteLength = + Object.getOwnPropertyDescriptor(viewPrototype, "byteLength").get; +const getViewByteOffset = + Object.getOwnPropertyDescriptor(viewPrototype, "byteOffset").get; const { + getBigInt64: viewGetInt64, + getBigUint64: viewGetUint64, + getFloat32: viewGetFloat32, + getFloat64: viewGetFloat64, + getInt8: viewGetInt8, + getInt16: viewGetInt16, + getInt32: viewGetInt32, getUint8: viewGetUint8, + getUint16: viewGetUint16, + getUint32: viewGetUint32, + setFloat32: viewSetFloat32, + setFloat64: viewSetFloat64, setUint8: viewSetUint8, setUint16: viewSetUint16, + setUint32: viewSetUint32, + setBigUint64: viewSetUint64, } = viewPrototype; -const bufferFromArgs = ($, $s) => - $ instanceof Buffer - ? $ - : $ instanceof View - ? call(getViewBuffer, $, []) - : $ instanceof TypedArray - ? call(getTypedArrayBuffer, $, []) - : ((string) => +/** + * Returns an ArrayBuffer for encoding generated from the provided + * arguments. + */ +const bufferFromArgs = ($, $s) => { + try { + // Try just getting the array buffer associated with the first + // argument and returning it if possible. + return toArrayBuffer($); + } catch { + // There is no array buffer associated with the first argument. + // + // Construct a string and convert it to an array buffer instead. + return ((string) => call( getViewBuffer, reduce( @@ -90,7 +125,7 @@ const bufferFromArgs = ($, $s) => ), [], ))( - typeof $ == "string" + typeof $ === "string" ? $ : hasOwnProperty($, "raw") ? rawString( @@ -101,6 +136,173 @@ 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) { + // The source contains an invalid character. + throw new RangeError( + `Piscēs: Invalid character in Base64: ${ucsCharacter}.`, + ); + } else { + // The source contains a valid character with a recognized + // mapping. + return result; + } + }, + ); + const { length } = u4s; + if (length % 2 === 1) { + // The length is such that an entire letter would be dropped during + // a forgiving decode. + throw new RangeError( + `Piscēs: Base16 string has invalid length: ${source}.`, + ); + } else { + // Every letter contributes at least some bits to the result. + const dataView = new View(new Buffer(floor(length / 2))); + for (let index = 0; index < length - 1;) { + // Iterate over the characters and assign their bits to the + // buffer. + call(viewSetUint8, dataView, [ + floor(index / 2), + (u4s[index] << 4) | u4s[++index, index++], + ]); + } + return call(getViewBuffer, dataView, []); + } +}; + +/** + * Returns the result of decoding the provided base32 string into an + * ArrayBuffer. + * + * If the second argument is truthy, uses Crockford’s encoding rather + * than the RFC’s (see ). This + * is more human‐friendly and tolerant. Check digits are not supported. + * + * ※ This function is not exposed. + */ +const decodeBase32 = (source, wrmg) => { + const u5s = map( + wrmg + ? stringReplace(source, /-/gu, "") + : source.length % 8 === 0 + ? stringReplace(source, /(?:=|={3,4}|={6})$/u, "") + : source, + (ucsCharacter) => { + const code = getCodeUnit(ucsCharacter, 0); + const result = wrmg + ? code >= 0x30 && code <= 0x39 + ? code - 48 + : code >= 0x41 && code <= 0x48 + ? code - 55 + : code === 0x49 + ? 1 // I + : code >= 0x4A && code <= 0x4B + ? code - 56 + : code === 0x4C + ? 1 // L + : code >= 0x4D && code <= 0x4E + ? code - 57 + : code === 0x4F + ? 0 // O + : code >= 0x50 && code <= 0x54 + ? code - 58 + // U is skipped + : code >= 0x56 && code <= 0x5A + ? code - 59 + : code >= 0x61 && code <= 0x68 + ? code - 87 + : code === 0x69 + ? 1 // i + : code >= 0x6A && code <= 0x6B + ? code - 88 + : code === 0x6C + ? 1 // l + : code >= 0x6D && code <= 0x6E + ? code - 89 + : code === 0x6F + ? 0 // o + : code >= 0x70 && code <= 0x74 + ? code - 90 + // u is skipped + : code >= 0x76 && code <= 0x7A + ? code - 91 + : -1 + : code >= 0x41 && code <= 0x5A + ? code - 65 + : code >= 0x61 && code <= 0x7A + ? code - 97 // same result as above; case insensitive + : code >= 0x32 && code <= 0x37 + ? code - 24 // digits 2–7 map to 26–31 + : -1; + if (result < 0) { + // The source contains an invalid character. + throw new RangeError( + `Piscēs: Invalid character in Base32: ${ucsCharacter}.`, + ); + } else { + // The source contains a valid character with a recognized + // mapping. + return result; + } + }, + ); + const { length } = u5s; + const lengthMod8 = length % 8; + if (lengthMod8 === 1 || lengthMod8 === 3 || lengthMod8 === 6) { + // The length is such that an entire letter would be dropped during + // a forgiving decode. + throw new RangeError( + `Piscēs: Base32 string has invalid length: ${source}.`, + ); + } else { + // Every letter contributes at least some bits to the result. + const dataView = new View(new Buffer(floor(length * 5 / 8))); + for (let index = 0; index < length - 1;) { + // Iterate over the characters and assign their bits to the + // buffer. + // + // The final index is not handled; if the string is not divisible + // by 8, some bits might be dropped. This matches the “forgiving + // decode” behaviour specified by WhatW·G for base64. + const dataIndex = ceil(index * 5 / 8); + const remainder = index % 8; + call(viewSetUint8, dataView, [ + dataIndex, + remainder === 0 + ? u5s[index] << 3 | u5s[++index] >> 2 + : remainder === 1 + ? u5s[index] << 6 | u5s[++index] << 1 | u5s[++index] >> 4 + : remainder === 3 + ? u5s[index] << 4 | u5s[++index] >> 1 + : remainder === 4 + ? u5s[index] << 7 | u5s[++index] << 2 | u5s[++index] >> 3 + : u5s[index] << 5 | u5s[++index, index++], // remainder === 6 + ]); + } + return call(getViewBuffer, dataView, []); + } +}; /** * Returns the result of decoding the provided base64 string into an @@ -110,7 +312,7 @@ const bufferFromArgs = ($, $s) => */ const decodeBase64 = (source, safe = false) => { const u6s = map( - source.length % 4 == 0 + source.length % 4 === 0 ? stringReplace(source, /={1,2}$/u, "") : source, (ucsCharacter) => { @@ -121,43 +323,182 @@ const decodeBase64 = (source, safe = false) => { ? code - 71 : code >= 0x30 && code <= 0x39 ? code + 4 - : code == (safe ? 0x2D : 0x2B) + : code === (safe ? 0x2D : 0x2B) ? 62 - : code == (safe ? 0x5F : 0x2F) + : code === (safe ? 0x5F : 0x2F) ? 63 : -1; if (result < 0) { + // The source contains an invalid character. throw new RangeError( - `Piscēs: Invalid character in Base64: ${character}.`, + `Piscēs: Invalid character in Base64: ${ucsCharacter}.`, ); } else { + // The source contains a valid character with a recognized + // mapping. 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) { + if (length % 4 === 1) { + // The length is such that an entire letter would be dropped during + // a forgiving decode. + throw new RangeError( + `Piscēs: Base64 string has invalid length: ${source}.`, + ); + } else { + // Every letter contributes at least some bits to the result. + const dataView = new View(new Buffer(floor(length * 3 / 4))); + for (let index = 0; index < length - 1;) { + // Iterate over the characters and assign their bits to the + // buffer. + // + // The final index is not handled; if the string is not divisible + // by 4, some bits might be dropped. This matches the “forgiving + // decode” behaviour specified by WhatW·G for base64. + const dataIndex = ceil(index * 3 / 4); + const remainder = index % 4; call(viewSetUint8, dataView, [ dataIndex, - ((u6s[index] & 0xF) << 4) + (u6s[++index] >> 2), + remainder === 0 + ? u6s[index] << 2 | u6s[++index] >> 4 + : remainder === 1 + ? u6s[index] << 4 | u6s[++index] >> 2 + : u6s[index] << 6 | u6s[++index, index++], // remainder === 2 ]); + } + return call(getViewBuffer, dataView, []); + } +}; + +/** + * 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;) { + // Iterate over the bytes and generate code units for them. + const codeUnitIndex = index * 2; + const datum = call(viewGetUint8, dataView, [index++]); + const u4s = [datum >> 4, datum & 0xF]; + for (let u4i = 0; u4i < 2; ++u4i) { + // Handle the high four bits, then the low four bits. + const u4 = u4s[u4i]; + const result = u4 < 10 ? u4 + 48 : u4 < 16 ? u4 + 55 : -1; + if (result < 0) { + // No mapping exists for these four bits. + // + // ※ This shouldn’t be possible! + throw new RangeError( + `Piscēs: Unexpected Base16 value: ${u4}.`, + ); + } else { + // A mapping exists for the bits. + resultingCodeUnits[codeUnitIndex + u4i] = result; + } + } + } + return stringFromCodeUnits(...resultingCodeUnits); +}; + +/** + * Returns the result of encoding the provided ArrayBuffer into a + * base32 string. + * + * ※ This function is not exposed. + */ +const encodeBase32 = (buffer, wrmg = false) => { + const dataView = new View(buffer); + const byteLength = call(getBufferByteLength, buffer, []); + const minimumLengthOfResults = ceil(byteLength * 8 / 5); + const fillByte = wrmg ? 0x2D : 0x3D; + const resultingCodeUnits = fill( + objectCreate( + binaryCodeUnitIterablePrototype, + { + length: { + value: minimumLengthOfResults + + (8 - (minimumLengthOfResults % 8)) % 8, + }, + }, + ), + fillByte, + ); + for (let index = 0; index < byteLength;) { + // Iterate over the bytes and generate code units for them. + const codeUnitIndex = ceil(index * 8 / 5); + const currentIndex = codeUnitIndex + +( + 0b01011 & 1 << index % 5 && + resultingCodeUnits[codeUnitIndex] != fillByte + ); // bytes 0, 1 & 3 handle two letters; this is for the second + const remainder = currentIndex % 8; + const currentByte = call(viewGetUint8, dataView, [index]); + const nextByte = + 0b01011010 & 1 << remainder && ++index < byteLength + // digits 1, 3, 4 & 6 span multiple bytes + ? call(viewGetUint8, dataView, [index]) + : 0; + const u5 = remainder === 0 + ? currentByte >> 3 + : remainder === 1 + ? (currentByte & 0b00000111) << 2 | nextByte >> 6 + : remainder === 2 + ? (currentByte & 0b00111111) >> 1 + : remainder === 3 + ? (currentByte & 0b00000001) << 4 | nextByte >> 4 + : remainder === 4 + ? (currentByte & 0b00001111) << 1 | nextByte >> 7 + : remainder === 5 + ? (currentByte & 0b01111111) >> 2 + : remainder === 6 + ? (currentByte & 0b00000011) << 3 | nextByte >> 5 + : (++index, currentByte & 0b00011111); // remainder === 7 + const result = wrmg + ? u5 < 10 ? u5 + 48 : u5 < 18 + ? u5 + 55 + // skip I + : u5 < 20 + ? u5 + 56 + // skip L + : u5 < 22 + ? u5 + 57 + // skip O + : u5 < 27 + ? u5 + 58 + // skip U + : u5 < 32 + ? u5 + 59 + : -1 + : u5 < 26 + ? u5 + 65 + : u5 < 32 + ? u5 + 24 + : -1; + if (result < 0) { + // No mapping exists for these five bits. + // + // ※ This shouldn’t be possible! + throw new RangeError(`Piscēs: Unexpected Base32 value: ${u5}.`); } else { - call(viewSetUint8, dataView, [ - dataIndex, - ((u6s[index] & 0x3) << 6) + u6s[++index], - ]); + // A mapping exists for the bits. + resultingCodeUnits[currentIndex] = result; } } - return call(getViewBuffer, dataView, []); + const answer = stringFromCodeUnits(...resultingCodeUnits); + return wrmg ? answer.replace(/-+$/u, "") : answer; }; /** @@ -183,24 +524,24 @@ const encodeBase64 = (buffer, safe = false) => { 0x3D, ); for (let index = 0; index < byteLength;) { + // Iterate over the bytes and generate code units for them. const codeUnitIndex = ceil(index * 4 / 3); const currentIndex = codeUnitIndex + +( - index % 3 == 0 && resultingCodeUnits[codeUnitIndex] != 0x3D - ); + index % 3 === 0 && resultingCodeUnits[codeUnitIndex] != 0x3D + ); // every third byte handles two letters; this is for the second 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 currentByte = call(viewGetUint8, dataView, [index]); + const nextByte = remainder % 3 && ++index < byteLength + // digits 1 & 2 span multiple bytes + ? call(viewGetUint8, dataView, [index]) + : 0; + const u6 = remainder === 0 + ? currentByte >> 2 + : remainder === 1 + ? (currentByte & 0b00000011) << 4 | nextByte >> 4 + : remainder === 2 + ? (currentByte & 0b00001111) << 2 | nextByte >> 6 + : (++index, currentByte & 0b00111111); // remainder === 3 const result = u6 < 26 ? u6 + 65 : u6 < 52 @@ -213,8 +554,12 @@ const encodeBase64 = (buffer, safe = false) => { ? (safe ? 0x5F : 0x2F) : -1; if (result < 0) { + // No mapping exists for these six bits. + // + // ※ This shouldn’t be possible! throw new RangeError(`Piscēs: Unexpected Base64 value: ${u6}.`); } else { + // A mapping exists for the bits. resultingCodeUnits[currentIndex] = result; } } @@ -229,7 +574,7 @@ const encodeBase64 = (buffer, safe = false) => { */ const sourceFromArgs = ($, $s) => stringReplace( - typeof $ == "string" ? $ : hasOwnProperty($, "raw") + typeof $ === "string" ? $ : hasOwnProperty($, "raw") ? rawString( $, ...objectCreate(argumentIterablePrototype, { @@ -241,11 +586,78 @@ const sourceFromArgs = ($, $s) => "", ); +/** + * Returns a slice of the provided value according to the algorithm of + * `ArrayBuffer::slice` (or `SharedArrayBuffer::slice`). + * + * ☡ This function throws if the provided value is not an array buffer. + */ +export const arrayBufferSlice = ($, start, end, ...args) => + call( + isSharedArrayBuffer($) ? sharedBufferSlice : bufferSlice, + $, + [ + start, + end, + ...objectCreate( + argumentIterablePrototype, + { args: { value: args } }, + ), + ], + ); + +/** + * 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 base32 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 base32 + * string. + */ +export const base32Binary = ($, ...$s) => + decodeBase32(sourceFromArgs($, $s)); + +/** + * Returns a (big‐endian) base32 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 base32String = ($, ...$s) => + encodeBase32(bufferFromArgs($, $s)); + /** * Returns an ArrayBuffer generated from the provided base64 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 base64 + * string. */ export const base64Binary = ($, ...$s) => decodeBase64(sourceFromArgs($, $s)); @@ -266,6 +678,9 @@ export const base64String = ($, ...$s) => * * 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 + * filename‐safe base64 string. */ export const filenameSafeBase64Binary = ($, ...$s) => decodeBase64(sourceFromArgs($, $s), true); @@ -280,6 +695,587 @@ export const filenameSafeBase64Binary = ($, ...$s) => export const filenameSafeBase64String = ($, ...$s) => encodeBase64(bufferFromArgs($, $s), true); +export const { + /** + * Returns the signed 8‐bit integral value in the provided array + * buffer or array buffer view at the provided byte offset. + * + * ※ The retrieved value will be big·endian unless a third argument + * is specified and truthy. + * + * ※ This is similar to `DataView::getInt8`, but works on all array + * buffers and array buffer views and returns a big·int. + * + * ☡ This function throws if the first argument is not an array + * buffer, data view, or typed array. + */ + get8BitSignedIntegralItem, + + /** + * Returns the unsigned 8‐bit integral value in the provided array + * buffer or array buffer view at the provided byte offset. + * + * ※ The retrieved value will be big·endian unless a third argument + * is specified and truthy. + * + * ※ This is similar to `DataView::getUint8`, but works on all array + * buffers and array buffer views and returns a big·int. + * + * ☡ This function throws if the first argument is not an array + * buffer, data view, or typed array. + */ + get8BitUnsignedIntegralItem, + + /** + * Returns the signed 16‐bit integral value in the provided array + * buffer or array buffer view at the provided byte offset. + * + * ※ The retrieved value will be big·endian unless a third argument + * is specified and truthy. + * + * ※ This is similar to `DataView::getInt16`, but works on all array + * buffers and array buffer views and returns a big·int. + * + * ☡ This function throws if the first argument is not an array + * buffer, data view, or typed array. + */ + get16BitSignedIntegralItem, + + /** + * Returns the unsigned 16‐bit integral value in the provided array + * buffer or array buffer view at the provided byte offset. + * + * ※ The retrieved value will be big·endian unless a third argument + * is specified and truthy. + * + * ※ This is similar to `DataView::getUint16`, but works on all + * array buffers and array buffer views and returns a big·int. + * + * ☡ This function throws if the first argument is not an array + * buffer, data view, or typed array. + */ + get16BitUnsignedIntegralItem, + + /** + * Returns the 32‐bit floating point value in the provided array + * buffer or array buffer view at the provided byte offset. + * + * ※ The retrieved value will be big·endian unless a third argument + * is specified and truthy. + * + * ※ This is similar to `DataView::getFloat32`, but works on all + * array buffers and array buffer views. + * + * ☡ This function throws if the first argument is not an array + * buffer, data view, or typed array. + */ + get32BitFloatingPointItem, + + /** + * Returns the signed 32‐bit integral value in the provided array + * buffer or array buffer view at the provided byte offset. + * + * ※ The retrieved value will be big·endian unless a third argument + * is specified and truthy. + * + * ※ This is similar to `DataView::getInt32`, but works on all array + * buffers and array buffer views and returns a big·int. + * + * ☡ This function throws if the first argument is not an array + * buffer, data view, or typed array. + */ + get32BitSignedIntegralItem, + + /** + * Returns the unsigned 32‐bit integral value in the provided array + * buffer or array buffer view at the provided byte offset. + * + * ※ The retrieved value will be big·endian unless a third argument + * is specified and truthy. + * + * ※ This is similar to `DataView::getUint32`, but works on all + * array buffers and array buffer views and returns a big·int. + * + * ☡ This function throws if the first argument is not an array + * buffer, data view, or typed array. + */ + get32BitUnsignedIntegralItem, + + /** + * Returns the 64‐bit floating point value in the provided array + * buffer or array buffer view at the provided byte offset. + * + * ※ The retrieved value will be big·endian unless a third argument + * is specified and truthy. + * + * ※ This is similar to `DataView::getFloat64`, but works on all + * array buffers and array buffer views. + * + * ☡ This function throws if the first argument is not an array + * buffer, data view, or typed array. + */ + get64BitFloatingPointItem, + + /** + * Returns the signed 64‐bit integral value in the provided array + * buffer or array buffer view at the provided byte offset. + * + * ※ The retrieved value will be big·endian unless a third argument + * is specified and truthy. + * + * ※ This is similar to `DataView::getBigInt64`, but works on all + * array buffers and array buffer views. + * + * ☡ This function throws if the first argument is not an array + * buffer, data view, or typed array. + */ + get64BitSignedIntegralItem, + + /** + * Returns the unsigned 64‐bit integral value in the provided array + * buffer or array buffer view at the provided byte offset. + * + * ※ The retrieved value will be big·endian unless a third argument + * is specified and truthy. + * + * ※ This is similar to `DataView::getBigUint64`, but works on all + * array buffers and array buffer views. + * + * ☡ This function throws if the first argument is not an array + * buffer, data view, or typed array. + */ + get64BitUnsignedIntegralItem, + + /** + * Sets the 8‐bit integral value in the provided array buffer or + * array buffer view at the provided byte offset to the provided + * value. + * + * ※ The value will be set as big·endian unless a fourth argument is + * specified and truthy. + * + * ※ This is similar to `DataView::setInt8`, but works on all array + * buffers and array buffer views and accepts both numeric and + * big·int values. + * + * ※ It doesn’t matter whether the provided value is signed or + * unsigned, as the algorithm will cast one to the other. + * + * ☡ This function throws if the first argument is not an array + * buffer, data view, or typed array. + */ + set8BitIntegralItem, + + /** + * Sets the 16‐bit integral value in the provided array buffer or + * array buffer view at the provided byte offset to the provided + * value. + * + * ※ The value will be set as big·endian unless a fourth argument is + * specified and truthy. + * + * ※ This is similar to `DataView::setInt16`, but works on all array + * buffers and array buffer views and accepts both numeric and + * big·int values. + * + * ※ It doesn’t matter whether the provided value is signed or + * unsigned, as the algorithm will cast one to the other. + * + * ☡ This function throws if the first argument is not an array + * buffer, data view, or typed array. + */ + set16BitIntegralItem, + + /** + * Sets the 32‐bit floating point value in the provided array buffer + * or array buffer view at the provided byte offset to the provided + * value. + * + * ※ The value will be set as big·endian unless a fourth argument is + * specified and truthy. + * + * ※ This is similar to `DataView::setFloat32`, but works on all + * array buffers and array buffer views. + * + * ☡ This function throws if the first argument is not an array + * buffer, data view, or typed array. + */ + set32BitFloatingPointItem, + + /** + * Sets the 32‐bit integral value in the provided array buffer or + * array buffer view at the provided byte offset to the provided + * value. + * + * ※ The value will be set as big·endian unless a fourth argument is + * specified and truthy. + * + * ※ This is similar to `DataView::setInt32`, but works on all array + * buffers and array buffer views and accepts both numeric and + * big·int values. + * + * ※ It doesn’t matter whether the provided value is signed or + * unsigned, as the algorithm will cast one to the other. + * + * ☡ This function throws if the first argument is not an array + * buffer, data view, or typed array. + */ + set32BitIntegralItem, + + /** + * Sets the 64‐bit floating point value in the provided array buffer + * or array buffer view at the provided byte offset to the provided + * value. + * + * ※ The value will be set as big·endian unless a fourth argument is + * specified and truthy. + * + * ※ This is similar to `DataView::setFloat64`, but works on all + * array buffers and array buffer views. + * + * ☡ This function throws if the first argument is not an array + * buffer, data view, or typed array. + */ + set64BitFloatingPointItem, + + /** + * Sets the 64‐bit integral value in the provided array buffer or + * array buffer view at the provided byte offset to the provided + * value. + * + * ※ The value will be set as big·endian unless a fourth argument is + * specified and truthy. + * + * ※ This is similar to `DataView::setInt32`, but works on all array + * buffers and array buffer views. + * + * ※ It doesn’t matter whether the provided value is signed or + * unsigned, as the algorithm will cast one to the other. + * + * ☡ This function throws if the first argument is not an array + * buffer, data view, or typed array, or if the third argument is not + * a big·int. + */ + set64BitIntegralItem, +} = (() => { + const makeBigInt = BigInt; + const { asUintN } = BigInt; + const makeNumber = Number; + + const viewMap = new WeakMap(); + const view = ($) => { + const buffer = toArrayBuffer($); + if (viewMap.has(buffer)) { + // A view has already been allocated for this buffer; use it. + return viewMap.get(buffer); + } else { + // No view has been created for this buffer yet. + const result = new View(buffer); + viewMap.set(buffer, result); + return result; + } + }; + + return { + get8BitSignedIntegralItem: ($, byteOffset, ...args) => + makeBigInt( + call(viewGetInt8, view($), [ + getByteOffset($) + byteOffset, + ...objectCreate( + argumentIterablePrototype, + { args: { value: args } }, + ), + ]), + ), + get8BitUnsignedIntegralItem: ($, byteOffset, ...args) => + makeBigInt( + call(viewGetUint8, view($), [ + getByteOffset($) + byteOffset, + ...objectCreate( + argumentIterablePrototype, + { args: { value: args } }, + ), + ]), + ), + get16BitSignedIntegralItem: ($, byteOffset, ...args) => + makeBigInt( + call(viewGetInt16, view($), [ + getByteOffset($) + byteOffset, + ...objectCreate( + argumentIterablePrototype, + { args: { value: args } }, + ), + ]), + ), + get16BitUnsignedIntegralItem: ($, byteOffset, ...args) => + makeBigInt( + call(viewGetUint16, view($), [ + getByteOffset($) + byteOffset, + ...objectCreate( + argumentIterablePrototype, + { args: { value: args } }, + ), + ]), + ), + get32BitFloatingPointItem: ($, byteOffset, ...args) => + call(viewGetFloat32, view($), [ + getByteOffset($) + byteOffset, + ...objectCreate( + argumentIterablePrototype, + { args: { value: args } }, + ), + ]), + get32BitSignedIntegralItem: ($, byteOffset, ...args) => + makeBigInt( + call(viewGetInt32, view($), [ + getByteOffset($) + byteOffset, + ...objectCreate( + argumentIterablePrototype, + { args: { value: args } }, + ), + ]), + ), + get32BitUnsignedIntegralItem: ($, byteOffset, ...args) => + makeBigInt( + call(viewGetUint32, view($), [ + getByteOffset($) + byteOffset, + ...objectCreate( + argumentIterablePrototype, + { args: { value: args } }, + ), + ]), + ), + get64BitFloatingPointItem: ($, byteOffset, ...args) => + call(viewGetFloat64, view($), [ + getByteOffset($) + byteOffset, + ...objectCreate( + argumentIterablePrototype, + { args: { value: args } }, + ), + ]), + get64BitSignedIntegralItem: ($, byteOffset, ...args) => + call(viewGetInt64, view($), [ + getByteOffset($) + byteOffset, + ...objectCreate( + argumentIterablePrototype, + { args: { value: args } }, + ), + ]), + get64BitUnsignedIntegralItem: ($, byteOffset, ...args) => + call(viewGetUint64, view($), [ + getByteOffset($) + byteOffset, + ...objectCreate( + argumentIterablePrototype, + { args: { value: args } }, + ), + ]), + set8BitIntegralItem: ($, byteOffset, value, ...args) => + call(viewSetUint8, view($), [ + getByteOffset($) + byteOffset, + makeNumber(value), + ...objectCreate( + argumentIterablePrototype, + { args: { value: args } }, + ), + ]), + set16BitIntegralItem: ($, byteOffset, value, ...args) => + call(viewSetUint16, view($), [ + getByteOffset($) + byteOffset, + makeNumber(value), + ...objectCreate( + argumentIterablePrototype, + { args: { value: args } }, + ), + ]), + set32BitFloatingPointItem: ($, byteOffset, value, ...args) => + call(viewSetFloat32, view($), [ + getByteOffset($) + byteOffset, + value, + ...objectCreate( + argumentIterablePrototype, + { args: { value: args } }, + ), + ]), + set32BitIntegralItem: ($, byteOffset, value, ...args) => + call(viewSetUint32, view($), [ + getByteOffset($) + byteOffset, + makeNumber(value), + ...objectCreate( + argumentIterablePrototype, + { args: { value: args } }, + ), + ]), + set64BitFloatingPointItem: ($, byteOffset, value, ...args) => + call(viewSetFloat64, view($), [ + getByteOffset($) + byteOffset, + value, + ...objectCreate( + argumentIterablePrototype, + { args: { value: args } }, + ), + ]), + set64BitIntegralItem: ($, byteOffset, value, ...args) => + call(viewSetUint64, view($), [ + getByteOffset($) + byteOffset, + asUintN(64, value), + ...objectCreate( + argumentIterablePrototype, + { args: { value: args } }, + ), + ]), + }; +})(); + +/** + * Returns the byte length for the provided array buffer or array + * buffer view. + * + * ☡ This function throws if the provided value is not an array buffer, + * data view, or typed array. + */ +export const getByteLength = ($) => { + try { + // Attempt to get the byte length from the provided value as an + // `ArrayBuffer`. + return call(getBufferByteLength, $, []); + } catch { + // The provided value is not an `ArrayBuffer`. + /* do nothing */ + } + try { + // Attempt to get the byte length from the provided value as a + // `SharedArrayBuffer`. + return call(getSharedBufferByteLength, $, []); + } catch { + // The provided value is not a `SharedArrayBuffer`. + /* do nothing */ + } + try { + // Attempt to get the byte length from the provided value as a + // data view. + return call(getViewByteLength, $, []); + } catch { + // The provided value is not a data view. + /* do nothing */ + } + try { + // Attempt to get the byte length from the provided value as a + // typed array. + return call(getTypedArrayByteLength, $, []); + } catch { + // The provided value is not a typed array. + /* do nothing */ + } + throw new TypeError(`Piscēs: Not an array buffer or view: ${$}.`); +}; + +/** + * Returns the byte offset for the provided array buffer or array + * buffer view. + * + * ※ This function always returns `0` for array buffers. + * + * ☡ This function throws if the provided value is not an array buffer, + * data view, or typed array. + */ +export const getByteOffset = ($) => { + if (isArrayBuffer($)) { + // The provided value is an array buffer. + return 0; + } else { + try { + // Attempt to get the byte offset from the provided value as a + // data view. + return call(getViewByteOffset, $, []); + } catch { + // The provided value is not a data view. + /* do nothing */ + } + try { + // Attempt to get the byte offset from the provided value as a + // typed array. + return call(getTypedArrayByteOffset, $, []); + } catch { + // The provided value is not a typed array. + /* do nothing */ + } + throw new TypeError(`Piscēs: Not an array buffer or view: ${$}.`); + } +}; + +/** + * Returns whether the provided value is a view on an underlying array + * buffer. + * + * ※ This function returns true for typed arrays and data views. + */ +export const { isView: isArrayBufferView } = Buffer; + +/** + * Returns whether the provided value is an array buffer. + * + * ※ This function returns true for both `ArrayBuffer`s and + * `SharedArrayBuffer`s. + */ +export const isArrayBuffer = ($) => { + try { + // Try to see if the provided argument has array buffer internal + // slots and return true if so. + return call(getBufferByteLength, $, []), true; + } catch { + // The provided argument does not have array buffer internal slots. + /* do nothing */ + } + try { + // Try to see if the provided argument has array buffer internal + // slots and return true if so. + return call(getSharedBufferByteLength, $, []), true; + } catch { + // The provided argument does not have array buffer internal slots. + /* do nothing */ + } + return false; +}; + +/** + * 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") { + // The provided value is not a string. + return false; + } else { + // The provided value is a string. + 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 base32 string. + * + * ※ This function returns false if the provided value is not a string + * primitive. + */ +export const isBase32 = ($) => { + if (typeof $ !== "string") { + // The provided value is not a string. + return false; + } else { + // The provided value is a string. + const source = stringReplace($, /[\t\n\f\r ]+/gu, ""); + const trimmed = source.length % 8 === 0 + ? stringReplace(source, /(?:=|={3,4}|={6})$/u, "") + : source; + return trimmed.length % 8 !== 1 && + call(reExec, /[^2-7A-Z/]/iu, [trimmed]) === null; + } +}; + /** * Returns whether the provided value is a Base64 string. * @@ -288,14 +1284,16 @@ export const filenameSafeBase64String = ($, ...$s) => */ export const isBase64 = ($) => { if (typeof $ !== "string") { + // The provided value is not a string. return false; } else { + // The provided value is a string. const source = stringReplace($, /[\t\n\f\r ]+/gu, ""); - const trimmed = source.length % 4 == 0 + 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; + return trimmed.length % 4 !== 1 && + call(reExec, /[^0-9A-Za-z+\/]/u, [trimmed]) === null; } }; @@ -307,13 +1305,110 @@ export const isBase64 = ($) => { */ export const isFilenameSafeBase64 = ($) => { if (typeof $ !== "string") { + // The provided value is not a string. return false; } else { + // The provided value is a string. const source = stringReplace($, /[\t\n\f\r ]+/gu, ""); - const trimmed = source.length % 4 == 0 + 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; + return trimmed.length % 4 !== 1 && + call(reExec, /[^0-9A-Za-z_-]/u, [trimmed]) === null; } }; + +/** Returns whether the provided value is a shared array buffer. */ +export const isSharedArrayBuffer = ($) => { + try { + // Try to see if the provided argument has shared array buffer + // internal slots and return true if so. + return call(getSharedBufferByteLength, $, []), true; + } catch { + // The provided argument does not have data view internal slots. + return false; + } +}; + +/** Returns whether the provided value is a typed array. */ +export const isTypedArray = ($) => { + try { + // Try to see if the provided argument has typed array internal + // slots and return true if so. + return call(getTypedArrayBuffer, $, []), true; + } catch { + // The provided argument does not have typed array internal slots. + return false; + } +}; + +/** + * Returns whether the provided value is a W·R·M·G (Crockford) base32 + * string. Check digits are not supported. + * + * ※ This function returns false if the provided value is not a string + * primitive. + */ +export const isWRMGBase32 = ($) => { + if (typeof $ !== "string") { + // The provided value is not a string. + return false; + } else { + // The provided value is a string. + const source = stringReplace($, /[\t\n\f\r ]+/gu, ""); + const trimmed = stringReplace(source, /-/gu, ""); + return trimmed.length % 8 !== 1 && + call(reExec, /[^0-9A-TV-Z]/iu, [trimmed]) === null; + } +}; + +/** + * Returns the array buffer associated with the provided object. + * + * ☡ This function throws if the provided object is not a data view or + * typed array. + */ +export const toArrayBuffer = ($) => { + if (isArrayBuffer($)) { + // The provided argument has array buffer internal slots. + return $; + } else { + // The provided argument does not have array buffer internal slots. + try { + // The provided argument has typed array internal slots. + return call(getTypedArrayBuffer, $, []); + } catch { + /* do nothing */ + } + try { + // The provided argument has data view internal slots. + return call(getViewBuffer, $, []); + } catch { + /* do nothing */ + } + throw new TypeError(`Piscēs: Not an array buffer or view: ${$}.`); + } +}; + +/** + * Returns an ArrayBuffer generated from the provided W·R·M·G + * (Crockford) base32 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 W·R·M·G + * base32 string. + */ +export const wrmgBase32Binary = ($, ...$s) => + decodeBase32(sourceFromArgs($, $s), true); + +/** + * Returns a (big‐endian) W·R·M·G (Crockford) base32 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 wrmgBase32String = ($, ...$s) => + encodeBase32(bufferFromArgs($, $s), true);