1 // ♓🌟 Piscēs ∷ binary.js
2 // ====================================================================
4 // Copyright © 2020–2023 Lady [@ Lady’s Computer].
6 // This Source Code Form is subject to the terms of the Mozilla Public
7 // License, v. 2.0. If a copy of the MPL was not distributed with this
8 // file, You can obtain one at <https://mozilla.org/MPL/2.0/>.
10 import { fill
, map
, reduce
} from "./collection.js";
11 import { bind
, call
} from "./function.js";
12 import { ceil
, floor
} from "./numeric.js";
13 import { hasOwnProperty
, objectCreate
} from "./object.js";
21 const Buffer
= ArrayBuffer
;
22 const View
= DataView
;
23 const TypedArray
= Object
.getPrototypeOf(Uint8Array
);
24 const { prototype: arrayPrototype
} = Array
;
25 const { prototype: bufferPrototype
} = Buffer
;
26 const { iterator
: iteratorSymbol
} = Symbol
;
27 const { prototype: rePrototype
} = RegExp
;
28 const { prototype: typedArrayPrototype
} = TypedArray
;
29 const { prototype: viewPrototype
} = View
;
31 const { [iteratorSymbol
]: arrayIterator
} = arrayPrototype
;
33 next
: arrayIteratorNext
,
34 } = Object
.getPrototypeOf([][iteratorSymbol
]());
35 const argumentIterablePrototype
= {
40 call(arrayIterator
, this.args
, []),
46 const binaryCodeUnitIterablePrototype
= {
51 call(arrayIterator
, this, []),
58 const getBufferByteLength
=
59 Object
.getOwnPropertyDescriptor(bufferPrototype
, "byteLength").get;
60 const getTypedArrayBuffer
=
61 Object
.getOwnPropertyDescriptor(typedArrayPrototype
, "buffer").get;
63 Object
.getOwnPropertyDescriptor(viewPrototype
, "buffer").get;
64 const { exec
: reExec
} = rePrototype
;
66 getUint8
: viewGetUint8
,
67 setUint8
: viewSetUint8
,
68 setUint16
: viewSetUint16
,
72 * Returns an ArrayBuffer for encoding generated from the provided
75 const bufferFromArgs
= ($, $s
) => {
77 // Try just getting the array buffer associated with the first
78 // argument and returning it if possible.
79 return toArrayBuffer($);
81 // There is no array buffer associated with the first argument.
83 // Construct a string and convert it to an array buffer instead.
89 (result
, ucsCharacter
, index
) => (
90 call(viewSetUint16
, result
, [
92 getCodeUnit(ucsCharacter
, 0),
95 new View(new Buffer(string
.length
* 2)),
101 : hasOwnProperty($, "raw")
104 ...objectCreate(argumentIterablePrototype
, {
114 * Returns the result of decoding the provided base16 string into an
117 * ※ This function is not exposed.
119 const decodeBase16
= (source
) => {
123 const code
= getCodeUnit(ucsCharacter
, 0);
124 const result
= code
>= 0x30 && code
<= 0x39
126 : code
>= 0x41 && code
<= 0x46
128 : code
>= 0x61 && code
<= 0x66
132 throw new RangeError(
133 `Piscēs: Invalid character in Base64: ${ucsCharacter}.`,
140 const { length
} = u4s
;
141 if (length
% 2 == 1) {
142 // The length is such that an entire letter would be dropped during
143 // a forgiving decode.
144 throw new RangeError(
145 `Piscēs: Base16 string has invalid length: ${source}.`,
148 // Every letter contributes at least some bits to the result.
149 const dataView
= new View(new Buffer(floor(length
/ 2)));
150 for (let index
= 0; index
< length
- 1;) {
151 call(viewSetUint8
, dataView
, [
153 (u4s
[index
] << 4) | u4s
[++index
, index
++],
156 return call(getViewBuffer
, dataView
, []);
161 * Returns the result of decoding the provided base32 string into an
164 * If the second argument is truthy, uses Crockford’s encoding rather
165 * than the RFC’s (see <https://www.crockford.com/base32.html>). This
166 * is more human‐friendly and tolerant. Check digits are not supported.
168 * ※ This function is not exposed.
170 const decodeBase32
= (source
, wrmg
) => {
173 ? stringReplace(source
, /-/gu
, "")
174 : source
.length
% 8 == 0
175 ? stringReplace(source
, /(?:=|={3,4}|={6})$/u, "")
178 const code
= getCodeUnit(ucsCharacter
, 0);
180 ? code
>= 0x30 && code
<= 0x39
182 : code
>= 0x41 && code
<= 0x48
186 : code
>= 0x4A && code
<= 0x4B
190 : code
>= 0x4D && code
<= 0x4E
194 : code
>= 0x50 && code
<= 0x54
197 : code
>= 0x56 && code
<= 0x5A
199 : code
>= 0x61 && code
<= 0x68
203 : code
>= 0x6A && code
<= 0x6B
207 : code
>= 0x6D && code
<= 0x6E
211 : code
>= 0x70 && code
<= 0x74
214 : code
>= 0x76 && code
<= 0x7A
217 : code
>= 0x41 && code
<= 0x5A
219 : code
>= 0x61 && code
<= 0x7A
220 ? code
- 97 // same result as above; case insensitive
221 : code
>= 0x32 && code
<= 0x37
222 ? code
- 24 // digits 2–7 map to 26–31
225 throw new RangeError(
226 `Piscēs: Invalid character in Base32: ${ucsCharacter}.`,
233 const { length
} = u5s
;
234 const lengthMod8
= length
% 8;
235 if (lengthMod8
== 1 || lengthMod8
== 3 || lengthMod8
== 6) {
236 // The length is such that an entire letter would be dropped during
237 // a forgiving decode.
238 throw new RangeError(
239 `Piscēs: Base32 string has invalid length: ${source}.`,
242 // Every letter contributes at least some bits to the result.
243 const dataView
= new View(new Buffer(floor(length
* 5 / 8)));
244 for (let index
= 0; index
< length
- 1;) {
245 // The final index is not handled; if the string is not divisible
246 // by 8, some bits might be dropped. This matches the “forgiving
247 // decode” behaviour specified by WhatW·G for base64.
248 const dataIndex
= ceil(index
* 5 / 8);
249 const remainder
= index
% 8;
250 if (remainder
== 0) {
251 call(viewSetUint8
, dataView
, [
253 u5s
[index
] << 3 | u5s
[++index
] >> 2,
255 } else if (remainder
== 1) {
256 call(viewSetUint8
, dataView
, [
258 u5s
[index
] << 6 | u5s
[++index
] << 1 | u5s
[++index
] >> 4,
260 } else if (remainder
== 3) {
261 call(viewSetUint8
, dataView
, [
263 u5s
[index
] << 4 | u5s
[++index
] >> 1,
265 } else if (remainder
== 4) {
266 call(viewSetUint8
, dataView
, [
268 u5s
[index
] << 7 | u5s
[++index
] << 2 | u5s
[++index
] >> 3,
270 } else { // remainder == 6
271 call(viewSetUint8
, dataView
, [
273 u5s
[index
] << 5 | u5s
[++index
, index
++],
277 return call(getViewBuffer
, dataView
, []);
282 * Returns the result of decoding the provided base64 string into an
285 * ※ This function is not exposed.
287 const decodeBase64
= (source
, safe
= false) => {
289 source
.length
% 4 == 0
290 ? stringReplace(source
, /={1,2}$/u, "")
293 const code
= getCodeUnit(ucsCharacter
, 0);
294 const result
= code
>= 0x41 && code
<= 0x5A
296 : code
>= 0x61 && code
<= 0x7A
298 : code
>= 0x30 && code
<= 0x39
300 : code
== (safe
? 0x2D : 0x2B)
302 : code
== (safe
? 0x5F : 0x2F)
306 throw new RangeError(
307 `Piscēs: Invalid character in Base64: ${ucsCharacter}.`,
314 const { length
} = u6s
;
315 if (length
% 4 == 1) {
316 // The length is such that an entire letter would be dropped during
317 // a forgiving decode.
318 throw new RangeError(
319 `Piscēs: Base64 string has invalid length: ${source}.`,
322 // Every letter contributes at least some bits to the result.
323 const dataView
= new View(new Buffer(floor(length
* 3 / 4)));
324 for (let index
= 0; index
< length
- 1;) {
325 // The final index is not handled; if the string is not divisible
326 // by 4, some bits might be dropped. This matches the “forgiving
327 // decode” behaviour specified by WhatW·G for base64.
328 const dataIndex
= ceil(index
* 3 / 4);
329 const remainder
= index
% 4;
330 if (remainder
== 0) {
331 call(viewSetUint8
, dataView
, [
333 u6s
[index
] << 2 | u6s
[++index
] >> 4,
335 } else if (remainder
== 1) {
336 call(viewSetUint8
, dataView
, [
338 u6s
[index
] << 4 | u6s
[++index
] >> 2,
340 } else { // remainder == 2
341 call(viewSetUint8
, dataView
, [
343 u6s
[index
] << 6 | u6s
[++index
, index
++],
347 return call(getViewBuffer
, dataView
, []);
352 * Returns the result of encoding the provided ArrayBuffer into a
355 * ※ This function is not exposed.
357 const encodeBase16
= (buffer
) => {
358 const dataView
= new View(buffer
);
359 const byteLength
= call(getBufferByteLength
, buffer
, []);
360 const minimumLengthOfResults
= byteLength
* 2;
361 const resultingCodeUnits
= fill(
363 binaryCodeUnitIterablePrototype
,
364 { length
: { value
: minimumLengthOfResults
} },
368 for (let index
= 0; index
< byteLength
;) {
369 const codeUnitIndex
= index
* 2;
370 const datum
= call(viewGetUint8
, dataView
, [index
++]);
371 const u4s
= [datum
>> 4, datum
& 0xF];
372 for (let u4i
= 0; u4i
< 2; ++u4i
) {
374 const result
= u4
< 10 ? u4
+ 48 : u4
< 16 ? u4
+ 55 : -1;
376 throw new RangeError(
377 `Piscēs: Unexpected Base16 value: ${u4}.`,
380 resultingCodeUnits
[codeUnitIndex
+ u4i
] = result
;
384 return stringFromCodeUnits(...resultingCodeUnits
);
388 * Returns the result of encoding the provided ArrayBuffer into a
391 * ※ This function is not exposed.
393 const encodeBase32
= (buffer
, wrmg
= false) => {
394 const dataView
= new View(buffer
);
395 const byteLength
= call(getBufferByteLength
, buffer
, []);
396 const minimumLengthOfResults
= ceil(byteLength
* 8 / 5);
397 const fillByte
= wrmg
? 0x2D : 0x3D;
398 const resultingCodeUnits
= fill(
400 binaryCodeUnitIterablePrototype
,
403 value
: minimumLengthOfResults
+
404 (8 - (minimumLengthOfResults
% 8)) % 8,
410 for (let index
= 0; index
< byteLength
;) {
411 const codeUnitIndex
= ceil(index
* 8 / 5);
412 const currentIndex
= codeUnitIndex
+ +(
413 0b01011 & 1 << index
% 5 &&
414 resultingCodeUnits
[codeUnitIndex
] != fillByte
415 ); // bytes 0, 1 & 3 handle two letters; this is for the second
416 const remainder
= currentIndex
% 8;
417 const currentByte
= call(viewGetUint8
, dataView
, [index
]);
419 0b01011010 & 1 << remainder
&& ++index
< byteLength
420 // digits 1, 3, 4 & 6 span multiple bytes
421 ? call(viewGetUint8
, dataView
, [index
])
423 const u5
= remainder
== 0
426 ? (currentByte
& 0b00000111) << 2 | nextByte
>> 6
428 ? (currentByte
& 0b00111111) >> 1
430 ? (currentByte
& 0b00000001) << 4 | nextByte
>> 4
432 ? (currentByte
& 0b00001111) << 1 | nextByte
>> 7
434 ? (currentByte
& 0b01111111) >> 2
436 ? (currentByte
& 0b00000011) << 3 | nextByte
>> 5
437 : (++index
, currentByte
& 0b00011111); // remainder == 7
439 ? u5
< 10 ? u5
+ 48 : u5
< 18
460 throw new RangeError(`Piscēs: Unexpected Base32 value: ${u5}.`);
462 resultingCodeUnits
[currentIndex
] = result
;
465 const answer
= stringFromCodeUnits(...resultingCodeUnits
);
466 return wrmg
? answer
.replace(/-+$/u, "") : answer
;
470 * Returns the result of encoding the provided ArrayBuffer into a
473 * ※ This function is not exposed.
475 const encodeBase64
= (buffer
, safe
= false) => {
476 const dataView
= new View(buffer
);
477 const byteLength
= call(getBufferByteLength
, buffer
, []);
478 const minimumLengthOfResults
= ceil(byteLength
* 4 / 3);
479 const resultingCodeUnits
= fill(
481 binaryCodeUnitIterablePrototype
,
484 value
: minimumLengthOfResults
+
485 (4 - (minimumLengthOfResults
% 4)) % 4,
491 for (let index
= 0; index
< byteLength
;) {
492 const codeUnitIndex
= ceil(index
* 4 / 3);
493 const currentIndex
= codeUnitIndex
+ +(
494 index
% 3 == 0 && resultingCodeUnits
[codeUnitIndex
] != 0x3D
495 ); // every third byte handles two letters; this is for the second
496 const remainder
= currentIndex
% 4;
497 const currentByte
= call(viewGetUint8
, dataView
, [index
]);
498 const nextByte
= remainder
% 3 && ++index
< byteLength
499 // digits 1 & 2 span multiple bytes
500 ? call(viewGetUint8
, dataView
, [index
])
502 const u6
= remainder
== 0
505 ? (currentByte
& 0b00000011) << 4 | nextByte
>> 4
507 ? (currentByte
& 0b00001111) << 2 | nextByte
>> 6
508 : (++index
, currentByte
& 0b00111111); // remainder == 3
509 const result
= u6
< 26
516 ? (safe
? 0x2D : 0x2B)
518 ? (safe
? 0x5F : 0x2F)
521 throw new RangeError(`Piscēs: Unexpected Base64 value: ${u6}.`);
523 resultingCodeUnits
[currentIndex
] = result
;
526 return stringFromCodeUnits(...resultingCodeUnits
);
530 * Returns a source string generated from the arguments passed to a
533 * ※ This function is not exposed.
535 const sourceFromArgs
= ($, $s
) =>
537 typeof $ == "string" ? $ : hasOwnProperty($, "raw")
540 ...objectCreate(argumentIterablePrototype
, {
550 * Returns an ArrayBuffer generated from the provided base16 string.
552 * This function can also be used as a tag for a template literal. The
553 * literal will be interpreted akin to `String.raw`.
555 * ☡ This function throws if the provided string is not a valid base16
558 export const base16Binary
= ($, ...$s
) =>
559 decodeBase16(sourceFromArgs($, $s
));
562 * Returns a (big‐endian) base16 string created from the provided typed
563 * array, buffer, or (16‐bit) string.
565 * This function can also be used as a tag for a template literal. The
566 * literal will be interpreted akin to `String.raw`.
568 export const base16String
= ($, ...$s
) =>
569 encodeBase16(bufferFromArgs($, $s
));
572 * Returns an ArrayBuffer generated from the provided base32 string.
574 * This function can also be used as a tag for a template literal. The
575 * literal will be interpreted akin to `String.raw`.
577 * ☡ This function throws if the provided string is not a valid base32
580 export const base32Binary
= ($, ...$s
) =>
581 decodeBase32(sourceFromArgs($, $s
));
584 * Returns a (big‐endian) base32 string created from the provided typed
585 * array, buffer, or (16‐bit) string.
587 * This function can also be used as a tag for a template literal. The
588 * literal will be interpreted akin to `String.raw`.
590 export const base32String
= ($, ...$s
) =>
591 encodeBase32(bufferFromArgs($, $s
));
594 * Returns an ArrayBuffer generated from the provided base64 string.
596 * This function can also be used as a tag for a template literal. The
597 * literal will be interpreted akin to `String.raw`.
599 * ☡ This function throws if the provided string is not a valid base64
602 export const base64Binary
= ($, ...$s
) =>
603 decodeBase64(sourceFromArgs($, $s
));
606 * Returns a (big‐endian) base64 string created from the provided typed
607 * array, buffer, or (16‐bit) string.
609 * This function can also be used as a tag for a template literal. The
610 * literal will be interpreted akin to `String.raw`.
612 export const base64String
= ($, ...$s
) =>
613 encodeBase64(bufferFromArgs($, $s
));
616 * Returns an ArrayBuffer generated from the provided filename‐safe
619 * This function can also be used as a tag for a template literal. The
620 * literal will be interpreted akin to `String.raw`.
622 * ☡ This function throws if the provided string is not a valid
623 * filename‐safe base64 string.
625 export const filenameSafeBase64Binary
= ($, ...$s
) =>
626 decodeBase64(sourceFromArgs($, $s
), true);
629 * Returns a (big‐endian) filename‐safe base64 string created from the
630 * provided typed array, buffer, or (16‐bit) string.
632 * This function can also be used as a tag for a template literal. The
633 * literal will be interpreted akin to `String.raw`.
635 export const filenameSafeBase64String
= ($, ...$s
) =>
636 encodeBase64(bufferFromArgs($, $s
), true);
639 * Returns whether the provided value is a view on an underlying array
642 * ※ This function returns true for typed arrays and data views.
644 export const { isView
: isArrayBufferView
} = Buffer
;
647 * Returns whether the provided value is a base16 string.
649 * ※ This function returns false if the provided value is not a string
652 export const isBase16
= ($) => {
653 if (typeof $ !== "string") {
656 const source
= stringReplace($, /[\t\n\f\r ]+/gu, "");
657 return source
.length
% 2 != 1 &&
658 call(reExec
, /[^0-9A-F]/iu, [source
]) == null;
663 * Returns whether the provided value is a base32 string.
665 * ※ This function returns false if the provided value is not a string
668 export const isBase32
= ($) => {
669 if (typeof $ !== "string") {
672 const source
= stringReplace($, /[\t\n\f\r ]+/gu, "");
673 const trimmed
= source
.length
% 8 == 0
674 ? stringReplace(source
, /(?:=|={3,4}|={6})$/u, "")
676 return trimmed
.length
% 8 != 1 &&
677 call(reExec
, /[^2-7A-Z/]/iu
, [trimmed
]) == null;
682 * Returns whether the provided value is a Base64 string.
684 * ※ This function returns false if the provided value is not a string
687 export const isBase64
= ($) => {
688 if (typeof $ !== "string") {
691 const source
= stringReplace($, /[\t\n\f\r ]+/gu, "");
692 const trimmed
= source
.length
% 4 == 0
693 ? stringReplace(source
, /={1,2}$/u, "")
695 return trimmed
.length
% 4 != 1 &&
696 call(reExec
, /[^0-9A-Za-z+\/]/u, [trimmed
]) == null;
701 * Returns whether the provided value is a filename‐safe base64 string.
703 * ※ This function returns false if the provided value is not a string
706 export const isFilenameSafeBase64
= ($) => {
707 if (typeof $ !== "string") {
710 const source
= stringReplace($, /[\t\n\f\r ]+/gu, "");
711 const trimmed
= source
.length
% 4 == 0
712 ? stringReplace(source
, /={1,2}$/u, "")
714 return trimmed
.length
% 4 != 1 &&
715 call(reExec
, /[^0-9A-Za-z_-]/u, [trimmed
]) == null;
720 * Returns whether the provided value is a W·R·M·G (Crockford) base32
721 * string. Check digits are not supported.
723 * ※ This function returns false if the provided value is not a string
726 export const isWRMGBase32
= ($) => {
727 if (typeof $ !== "string") {
730 const source
= stringReplace($, /[\t\n\f\r ]+/gu, "");
731 const trimmed
= stringReplace(source
, /-/gu
, "");
732 return trimmed
.length
% 8 != 1 &&
733 call(reExec
, /[^0-9A-TV-Z]/iu, [trimmed
]) == null;
738 * Returns the array buffer associated with the provided object.
740 * ☡ This function throws if the provided object is not a data view or
743 export const toArrayBuffer
= ($) => {
745 // The provided argument has array buffer internal slots.
746 return call(getBufferByteLength
, $, []), $;
749 // The provided argument has typed array internal slots.
750 return call(getTypedArrayBuffer
, $, []);
753 // The provided argument has data view internal slots.
754 return call(getViewBuffer
, $, []);
756 throw new TypeError(`Piscēs: Not an array buffer or view: ${$}.`);
760 * Returns an ArrayBuffer generated from the provided W·R·M·G
761 * (Crockford) base32 string.
763 * This function can also be used as a tag for a template literal. The
764 * literal will be interpreted akin to `String.raw`.
766 * ☡ This function throws if the provided string is not a valid W·R·M·G
769 export const wrmgBase32Binary
= ($, ...$s
) =>
770 decodeBase32(sourceFromArgs($, $s
), true);
773 * Returns a (big‐endian) W·R·M·G (Crockford) base32 string created
774 * from the provided typed array, buffer, or (16‐bit) string.
776 * This function can also be used as a tag for a template literal. The
777 * literal will be interpreted akin to `String.raw`.
779 export const wrmgBase32String
= ($, ...$s
) =>
780 encodeBase32(bufferFromArgs($, $s
), true);