X-Git-Url: https://git.ladys.computer/Pisces/blobdiff_plain/295c32f3bca325fc406aeb9fa20adb643c8bf429..6e6d4e3261c1c943fe44fa9e381bcf8bf1441fd6:/binary.js diff --git a/binary.js b/binary.js index 25fd055..5ee2bda 100644 --- a/binary.js +++ b/binary.js @@ -135,10 +135,13 @@ const decodeBase16 = (source) => { ); 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;) { call(viewSetUint8, dataView, [ @@ -150,6 +153,127 @@ const decodeBase16 = (source) => { } }; +/** + * 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) { + throw new RangeError( + `Piscēs: Invalid character in Base32: ${ucsCharacter}.`, + ); + } else { + 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;) { + // 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; + if (remainder == 0) { + call(viewSetUint8, dataView, [ + dataIndex, + u5s[index] << 3 | u5s[++index] >> 2, + ]); + } else if (remainder == 1) { + call(viewSetUint8, dataView, [ + dataIndex, + u5s[index] << 6 | u5s[++index] << 1 | u5s[++index] >> 4, + ]); + } else if (remainder == 3) { + call(viewSetUint8, dataView, [ + dataIndex, + u5s[index] << 4 | u5s[++index] >> 1, + ]); + } else if (remainder == 4) { + call(viewSetUint8, dataView, [ + dataIndex, + u5s[index] << 7 | u5s[++index] << 2 | u5s[++index] >> 3, + ]); + } else { // remainder == 6 + call(viewSetUint8, dataView, [ + dataIndex, + u5s[index] << 5 | u5s[++index, index++], + ]); + } + } + return call(getViewBuffer, dataView, []); + } +}; + /** * Returns the result of decoding the provided base64 string into an * ArrayBuffer. @@ -185,10 +309,13 @@ const decodeBase64 = (source, safe = false) => { ); const { length } = u6s; 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;) { // The final index is not handled; if the string is not divisible @@ -253,6 +380,88 @@ const encodeBase16 = (buffer) => { 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;) { + 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) { + throw new RangeError(`Piscēs: Unexpected Base32 value: ${u5}.`); + } else { + resultingCodeUnits[currentIndex] = result; + } + } + const answer = stringFromCodeUnits(...resultingCodeUnits); + return wrmg ? answer.replace(/-+$/u, "") : answer; +}; + /** * Returns the result of encoding the provided ArrayBuffer into a * base64 string. @@ -355,6 +564,28 @@ export const base16Binary = ($, ...$s) => 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. * @@ -416,6 +647,25 @@ export const isBase16 = ($) => { } }; +/** + * 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") { + return false; + } else { + 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. * @@ -453,3 +703,44 @@ export const isFilenameSafeBase64 = ($) => { call(reExec, /[^0-9A-Za-z_-]/u, [trimmed]) == null; } }; + +/** + * 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") { + return false; + } else { + 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 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);