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
) =>
79 ? call(getViewBuffer
, $, [])
80 : $ instanceof TypedArray
81 ? call(getTypedArrayBuffer
, $, [])
87 (result
, ucsCharacter
, index
) => (
88 call(viewSetUint16
, result
, [
90 getCodeUnit(ucsCharacter
, 0),
93 new View(new Buffer(string
.length
* 2)),
99 : hasOwnProperty($, "raw")
102 ...objectCreate(argumentIterablePrototype
, {
110 * Returns the result of decoding the provided base16 string into an
113 * ※ This function is not exposed.
115 const decodeBase16
= (source
) => {
119 const code
= getCodeUnit(ucsCharacter
, 0);
120 const result
= code
>= 0x30 && code
<= 0x39
122 : code
>= 0x41 && code
<= 0x46
124 : code
>= 0x61 && code
<= 0x66
128 throw new RangeError(
129 `Piscēs: Invalid character in Base64: ${ucsCharacter}.`,
136 const { length
} = u4s
;
137 if (length
% 2 == 1) {
138 throw new RangeError(
139 `Piscēs: Base16 string has invalid length: ${source}.`,
142 const dataView
= new View(new Buffer(floor(length
/ 2)));
143 for (let index
= 0; index
< length
- 1;) {
144 call(viewSetUint8
, dataView
, [
146 (u4s
[index
] << 4) | u4s
[++index
, index
++],
149 return call(getViewBuffer
, dataView
, []);
154 * Returns the result of decoding the provided base64 string into an
157 * ※ This function is not exposed.
159 const decodeBase64
= (source
, safe
= false) => {
161 source
.length
% 4 == 0
162 ? stringReplace(source
, /={1,2}$/u, "")
165 const code
= getCodeUnit(ucsCharacter
, 0);
166 const result
= code
>= 0x41 && code
<= 0x5A
168 : code
>= 0x61 && code
<= 0x7A
170 : code
>= 0x30 && code
<= 0x39
172 : code
== (safe
? 0x2D : 0x2B)
174 : code
== (safe
? 0x5F : 0x2F)
178 throw new RangeError(
179 `Piscēs: Invalid character in Base64: ${ucsCharacter}.`,
186 const { length
} = u6s
;
187 if (length
% 4 == 1) {
188 throw new RangeError(
189 `Piscēs: Base64 string has invalid length: ${source}.`,
192 const dataView
= new View(new Buffer(floor(length
* 3 / 4)));
193 for (let index
= 0; index
< length
- 1;) {
194 // The final index is not handled; if the string is not divisible
195 // by 4, some bits might be dropped. This matches the “forgiving
196 // decode” behaviour specified by WhatW·G for base64.
197 const dataIndex
= ceil(index
* 3 / 4);
198 const remainder
= index
% 4;
199 if (remainder
== 0) {
200 call(viewSetUint8
, dataView
, [
202 u6s
[index
] << 2 | u6s
[++index
] >> 4,
204 } else if (remainder
== 1) {
205 call(viewSetUint8
, dataView
, [
207 u6s
[index
] << 4 | u6s
[++index
] >> 2,
209 } else { // remainder == 2
210 call(viewSetUint8
, dataView
, [
212 u6s
[index
] << 6 | u6s
[++index
, index
++],
216 return call(getViewBuffer
, dataView
, []);
221 * Returns the result of encoding the provided ArrayBuffer into a
224 * ※ This function is not exposed.
226 const encodeBase16
= (buffer
) => {
227 const dataView
= new View(buffer
);
228 const byteLength
= call(getBufferByteLength
, buffer
, []);
229 const minimumLengthOfResults
= byteLength
* 2;
230 const resultingCodeUnits
= fill(
232 binaryCodeUnitIterablePrototype
,
233 { length
: { value
: minimumLengthOfResults
} },
237 for (let index
= 0; index
< byteLength
;) {
238 const codeUnitIndex
= index
* 2;
239 const datum
= call(viewGetUint8
, dataView
, [index
++]);
240 const u4s
= [datum
>> 4, datum
& 0xF];
241 for (let u4i
= 0; u4i
< 2; ++u4i
) {
243 const result
= u4
< 10 ? u4
+ 48 : u4
< 16 ? u4
+ 55 : -1;
245 throw new RangeError(
246 `Piscēs: Unexpected Base16 value: ${u4}.`,
249 resultingCodeUnits
[codeUnitIndex
+ u4i
] = result
;
253 return stringFromCodeUnits(...resultingCodeUnits
);
257 * Returns the result of encoding the provided ArrayBuffer into a
260 * ※ This function is not exposed.
262 const encodeBase64
= (buffer
, safe
= false) => {
263 const dataView
= new View(buffer
);
264 const byteLength
= call(getBufferByteLength
, buffer
, []);
265 const minimumLengthOfResults
= ceil(byteLength
* 4 / 3);
266 const resultingCodeUnits
= fill(
268 binaryCodeUnitIterablePrototype
,
271 value
: minimumLengthOfResults
+
272 (4 - (minimumLengthOfResults
% 4)) % 4,
278 for (let index
= 0; index
< byteLength
;) {
279 const codeUnitIndex
= ceil(index
* 4 / 3);
280 const currentIndex
= codeUnitIndex
+ +(
281 index
% 3 == 0 && resultingCodeUnits
[codeUnitIndex
] != 0x3D
282 ); // every third byte handles two letters; this is for the second
283 const remainder
= currentIndex
% 4;
284 const currentByte
= call(viewGetUint8
, dataView
, [index
]);
285 const nextByte
= remainder
% 3 && ++index
< byteLength
286 // digits 1 & 2 span multiple bytes
287 ? call(viewGetUint8
, dataView
, [index
])
289 const u6
= remainder
== 0
292 ? (currentByte
& 0b00000011) << 4 | nextByte
>> 4
294 ? (currentByte
& 0b00001111) << 2 | nextByte
>> 6
295 : (++index
, currentByte
& 0b00111111); // remainder == 3
296 const result
= u6
< 26
303 ? (safe
? 0x2D : 0x2B)
305 ? (safe
? 0x5F : 0x2F)
308 throw new RangeError(`Piscēs: Unexpected Base64 value: ${u6}.`);
310 resultingCodeUnits
[currentIndex
] = result
;
313 return stringFromCodeUnits(...resultingCodeUnits
);
317 * Returns a source string generated from the arguments passed to a
320 * ※ This function is not exposed.
322 const sourceFromArgs
= ($, $s
) =>
324 typeof $ == "string" ? $ : hasOwnProperty($, "raw")
327 ...objectCreate(argumentIterablePrototype
, {
337 * Returns an ArrayBuffer generated from the provided base16 string.
339 * This function can also be used as a tag for a template literal. The
340 * literal will be interpreted akin to `String.raw`.
342 * ☡ This function throws if the provided string is not a valid base16
345 export const base16Binary
= ($, ...$s
) =>
346 decodeBase16(sourceFromArgs($, $s
));
349 * Returns a (big‐endian) base16 string created from the provided typed
350 * array, buffer, or (16‐bit) string.
352 * This function can also be used as a tag for a template literal. The
353 * literal will be interpreted akin to `String.raw`.
355 export const base16String
= ($, ...$s
) =>
356 encodeBase16(bufferFromArgs($, $s
));
359 * Returns an ArrayBuffer generated from the provided base64 string.
361 * This function can also be used as a tag for a template literal. The
362 * literal will be interpreted akin to `String.raw`.
364 * ☡ This function throws if the provided string is not a valid base64
367 export const base64Binary
= ($, ...$s
) =>
368 decodeBase64(sourceFromArgs($, $s
));
371 * Returns a (big‐endian) base64 string created from the provided typed
372 * array, buffer, or (16‐bit) string.
374 * This function can also be used as a tag for a template literal. The
375 * literal will be interpreted akin to `String.raw`.
377 export const base64String
= ($, ...$s
) =>
378 encodeBase64(bufferFromArgs($, $s
));
381 * Returns an ArrayBuffer generated from the provided filename‐safe
384 * This function can also be used as a tag for a template literal. The
385 * literal will be interpreted akin to `String.raw`.
387 * ☡ This function throws if the provided string is not a valid
388 * filename‐safe base64 string.
390 export const filenameSafeBase64Binary
= ($, ...$s
) =>
391 decodeBase64(sourceFromArgs($, $s
), true);
394 * Returns a (big‐endian) filename‐safe base64 string created from the
395 * provided typed array, buffer, or (16‐bit) string.
397 * This function can also be used as a tag for a template literal. The
398 * literal will be interpreted akin to `String.raw`.
400 export const filenameSafeBase64String
= ($, ...$s
) =>
401 encodeBase64(bufferFromArgs($, $s
), true);
404 * Returns whether the provided value is a base16 string.
406 * ※ This function returns false if the provided value is not a string
409 export const isBase16
= ($) => {
410 if (typeof $ !== "string") {
413 const source
= stringReplace($, /[\t\n\f\r ]+/gu, "");
414 return source
.length
% 2 != 1 &&
415 call(reExec
, /[^0-9A-F]/iu, [source
]) == null;
420 * Returns whether the provided value is a Base64 string.
422 * ※ This function returns false if the provided value is not a string
425 export const isBase64
= ($) => {
426 if (typeof $ !== "string") {
429 const source
= stringReplace($, /[\t\n\f\r ]+/gu, "");
430 const trimmed
= source
.length
% 4 == 0
431 ? stringReplace(source
, /={1,2}$/u, "")
433 return trimmed
.length
% 4 != 1 &&
434 call(reExec
, /[^0-9A-Za-z+\/]/u, [trimmed
]) == null;
439 * Returns whether the provided value is a filename‐safe base64 string.
441 * ※ This function returns false if the provided value is not a string
444 export const isFilenameSafeBase64
= ($) => {
445 if (typeof $ !== "string") {
448 const source
= stringReplace($, /[\t\n\f\r ]+/gu, "");
449 const trimmed
= source
.length
% 4 == 0
450 ? stringReplace(source
, /={1,2}$/u, "")
452 return trimmed
.length
% 4 != 1 &&
453 call(reExec
, /[^0-9A-Za-z_-]/u, [trimmed
]) == null;