]> Lady’s Gitweb - Pisces/blob - binary.js
Add base16 support to binary.js
[Pisces] / binary.js
1 // ♓🌟 Piscēs ∷ binary.js
2 // ====================================================================
3 //
4 // Copyright © 2020–2023 Lady [@ Lady’s Computer].
5 //
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/>.
9
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";
14 import {
15 getCodeUnit,
16 rawString,
17 stringFromCodeUnits,
18 stringReplace,
19 } from "./string.js";
20
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;
30
31 const { [iteratorSymbol]: arrayIterator } = arrayPrototype;
32 const {
33 next: arrayIteratorNext,
34 } = Object.getPrototypeOf([][iteratorSymbol]());
35 const argumentIterablePrototype = {
36 [iteratorSymbol]() {
37 return {
38 next: bind(
39 arrayIteratorNext,
40 call(arrayIterator, this.args, []),
41 [],
42 ),
43 };
44 },
45 };
46 const binaryCodeUnitIterablePrototype = {
47 [iteratorSymbol]() {
48 return {
49 next: bind(
50 arrayIteratorNext,
51 call(arrayIterator, this, []),
52 [],
53 ),
54 };
55 },
56 };
57
58 const getBufferByteLength =
59 Object.getOwnPropertyDescriptor(bufferPrototype, "byteLength").get;
60 const getTypedArrayBuffer =
61 Object.getOwnPropertyDescriptor(typedArrayPrototype, "buffer").get;
62 const getViewBuffer =
63 Object.getOwnPropertyDescriptor(viewPrototype, "buffer").get;
64 const { exec: reExec } = rePrototype;
65 const {
66 getUint8: viewGetUint8,
67 setUint8: viewSetUint8,
68 setUint16: viewSetUint16,
69 } = viewPrototype;
70
71 /**
72 * Returns an ArrayBuffer for encoding generated from the provided
73 * arguments.
74 */
75 const bufferFromArgs = ($, $s) =>
76 $ instanceof Buffer
77 ? $
78 : $ instanceof View
79 ? call(getViewBuffer, $, [])
80 : $ instanceof TypedArray
81 ? call(getTypedArrayBuffer, $, [])
82 : ((string) =>
83 call(
84 getViewBuffer,
85 reduce(
86 string,
87 (result, ucsCharacter, index) => (
88 call(viewSetUint16, result, [
89 index * 2,
90 getCodeUnit(ucsCharacter, 0),
91 ]), result
92 ),
93 new View(new Buffer(string.length * 2)),
94 ),
95 [],
96 ))(
97 typeof $ == "string"
98 ? $
99 : hasOwnProperty($, "raw")
100 ? rawString(
101 $,
102 ...objectCreate(argumentIterablePrototype, {
103 args: { value: $s },
104 }),
105 )
106 : `${$}`,
107 );
108
109 /**
110 * Returns the result of decoding the provided base16 string into an
111 * ArrayBuffer.
112 *
113 * ※ This function is not exposed.
114 */
115 const decodeBase16 = (source) => {
116 const u4s = map(
117 source,
118 (ucsCharacter) => {
119 const code = getCodeUnit(ucsCharacter, 0);
120 const result = code >= 0x30 && code <= 0x39
121 ? code - 48
122 : code >= 0x41 && code <= 0x46
123 ? code - 55
124 : code >= 0x61 && code <= 0x66
125 ? code - 87
126 : -1;
127 if (result < 0) {
128 throw new RangeError(
129 `Piscēs: Invalid character in Base64: ${ucsCharacter}.`,
130 );
131 } else {
132 return result;
133 }
134 },
135 );
136 const { length } = u4s;
137 if (length % 2 == 1) {
138 throw new RangeError(
139 `Piscēs: Base16 string has invalid length: ${source}.`,
140 );
141 } else {
142 const dataView = new View(new Buffer(floor(length / 2)));
143 for (let index = 0; index < length - 1;) {
144 call(viewSetUint8, dataView, [
145 floor(index / 2),
146 (u4s[index] << 4) | u4s[++index, index++],
147 ]);
148 }
149 return call(getViewBuffer, dataView, []);
150 }
151 };
152
153 /**
154 * Returns the result of decoding the provided base64 string into an
155 * ArrayBuffer.
156 *
157 * ※ This function is not exposed.
158 */
159 const decodeBase64 = (source, safe = false) => {
160 const u6s = map(
161 source.length % 4 == 0
162 ? stringReplace(source, /={1,2}$/u, "")
163 : source,
164 (ucsCharacter) => {
165 const code = getCodeUnit(ucsCharacter, 0);
166 const result = code >= 0x41 && code <= 0x5A
167 ? code - 65
168 : code >= 0x61 && code <= 0x7A
169 ? code - 71
170 : code >= 0x30 && code <= 0x39
171 ? code + 4
172 : code == (safe ? 0x2D : 0x2B)
173 ? 62
174 : code == (safe ? 0x5F : 0x2F)
175 ? 63
176 : -1;
177 if (result < 0) {
178 throw new RangeError(
179 `Piscēs: Invalid character in Base64: ${ucsCharacter}.`,
180 );
181 } else {
182 return result;
183 }
184 },
185 );
186 const { length } = u6s;
187 if (length % 4 == 1) {
188 throw new RangeError(
189 `Piscēs: Base64 string has invalid length: ${source}.`,
190 );
191 } else {
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, [
201 dataIndex,
202 u6s[index] << 2 | u6s[++index] >> 4,
203 ]);
204 } else if (remainder == 1) {
205 call(viewSetUint8, dataView, [
206 dataIndex,
207 u6s[index] << 4 | u6s[++index] >> 2,
208 ]);
209 } else { // remainder == 2
210 call(viewSetUint8, dataView, [
211 dataIndex,
212 u6s[index] << 6 | u6s[++index, index++],
213 ]);
214 }
215 }
216 return call(getViewBuffer, dataView, []);
217 }
218 };
219
220 /**
221 * Returns the result of encoding the provided ArrayBuffer into a
222 * base16 string.
223 *
224 * ※ This function is not exposed.
225 */
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(
231 objectCreate(
232 binaryCodeUnitIterablePrototype,
233 { length: { value: minimumLengthOfResults } },
234 ),
235 0x3D,
236 );
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) {
242 const u4 = u4s[u4i];
243 const result = u4 < 10 ? u4 + 48 : u4 < 16 ? u4 + 55 : -1;
244 if (result < 0) {
245 throw new RangeError(
246 `Piscēs: Unexpected Base16 value: ${u4}.`,
247 );
248 } else {
249 resultingCodeUnits[codeUnitIndex + u4i] = result;
250 }
251 }
252 }
253 return stringFromCodeUnits(...resultingCodeUnits);
254 };
255
256 /**
257 * Returns the result of encoding the provided ArrayBuffer into a
258 * base64 string.
259 *
260 * ※ This function is not exposed.
261 */
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(
267 objectCreate(
268 binaryCodeUnitIterablePrototype,
269 {
270 length: {
271 value: minimumLengthOfResults +
272 (4 - (minimumLengthOfResults % 4)) % 4,
273 },
274 },
275 ),
276 0x3D,
277 );
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])
288 : 0;
289 const u6 = remainder == 0
290 ? currentByte >> 2
291 : remainder == 1
292 ? (currentByte & 0b00000011) << 4 | nextByte >> 4
293 : remainder == 2
294 ? (currentByte & 0b00001111) << 2 | nextByte >> 6
295 : (++index, currentByte & 0b00111111); // remainder == 3
296 const result = u6 < 26
297 ? u6 + 65
298 : u6 < 52
299 ? u6 + 71
300 : u6 < 62
301 ? u6 - 4
302 : u6 < 63
303 ? (safe ? 0x2D : 0x2B)
304 : u6 < 64
305 ? (safe ? 0x5F : 0x2F)
306 : -1;
307 if (result < 0) {
308 throw new RangeError(`Piscēs: Unexpected Base64 value: ${u6}.`);
309 } else {
310 resultingCodeUnits[currentIndex] = result;
311 }
312 }
313 return stringFromCodeUnits(...resultingCodeUnits);
314 };
315
316 /**
317 * Returns a source string generated from the arguments passed to a
318 * tag function.
319 *
320 * ※ This function is not exposed.
321 */
322 const sourceFromArgs = ($, $s) =>
323 stringReplace(
324 typeof $ == "string" ? $ : hasOwnProperty($, "raw")
325 ? rawString(
326 $,
327 ...objectCreate(argumentIterablePrototype, {
328 args: { value: $s },
329 }),
330 )
331 : `${$}`,
332 /[\t\n\f\r ]+/gu,
333 "",
334 );
335
336 /**
337 * Returns an ArrayBuffer generated from the provided base16 string.
338 *
339 * This function can also be used as a tag for a template literal. The
340 * literal will be interpreted akin to `String.raw`.
341 *
342 * ☡ This function throws if the provided string is not a valid base16
343 * string.
344 */
345 export const base16Binary = ($, ...$s) =>
346 decodeBase16(sourceFromArgs($, $s));
347
348 /**
349 * Returns a (big‐endian) base16 string created from the provided typed
350 * array, buffer, or (16‐bit) string.
351 *
352 * This function can also be used as a tag for a template literal. The
353 * literal will be interpreted akin to `String.raw`.
354 */
355 export const base16String = ($, ...$s) =>
356 encodeBase16(bufferFromArgs($, $s));
357
358 /**
359 * Returns an ArrayBuffer generated from the provided base64 string.
360 *
361 * This function can also be used as a tag for a template literal. The
362 * literal will be interpreted akin to `String.raw`.
363 *
364 * ☡ This function throws if the provided string is not a valid base64
365 * string.
366 */
367 export const base64Binary = ($, ...$s) =>
368 decodeBase64(sourceFromArgs($, $s));
369
370 /**
371 * Returns a (big‐endian) base64 string created from the provided typed
372 * array, buffer, or (16‐bit) string.
373 *
374 * This function can also be used as a tag for a template literal. The
375 * literal will be interpreted akin to `String.raw`.
376 */
377 export const base64String = ($, ...$s) =>
378 encodeBase64(bufferFromArgs($, $s));
379
380 /**
381 * Returns an ArrayBuffer generated from the provided filename‐safe
382 * base64 string.
383 *
384 * This function can also be used as a tag for a template literal. The
385 * literal will be interpreted akin to `String.raw`.
386 *
387 * ☡ This function throws if the provided string is not a valid
388 * filename‐safe base64 string.
389 */
390 export const filenameSafeBase64Binary = ($, ...$s) =>
391 decodeBase64(sourceFromArgs($, $s), true);
392
393 /**
394 * Returns a (big‐endian) filename‐safe base64 string created from the
395 * provided typed array, buffer, or (16‐bit) string.
396 *
397 * This function can also be used as a tag for a template literal. The
398 * literal will be interpreted akin to `String.raw`.
399 */
400 export const filenameSafeBase64String = ($, ...$s) =>
401 encodeBase64(bufferFromArgs($, $s), true);
402
403 /**
404 * Returns whether the provided value is a base16 string.
405 *
406 * ※ This function returns false if the provided value is not a string
407 * primitive.
408 */
409 export const isBase16 = ($) => {
410 if (typeof $ !== "string") {
411 return false;
412 } else {
413 const source = stringReplace($, /[\t\n\f\r ]+/gu, "");
414 return source.length % 2 != 1 &&
415 call(reExec, /[^0-9A-F]/iu, [source]) == null;
416 }
417 };
418
419 /**
420 * Returns whether the provided value is a Base64 string.
421 *
422 * ※ This function returns false if the provided value is not a string
423 * primitive.
424 */
425 export const isBase64 = ($) => {
426 if (typeof $ !== "string") {
427 return false;
428 } else {
429 const source = stringReplace($, /[\t\n\f\r ]+/gu, "");
430 const trimmed = source.length % 4 == 0
431 ? stringReplace(source, /={1,2}$/u, "")
432 : source;
433 return trimmed.length % 4 != 1 &&
434 call(reExec, /[^0-9A-Za-z+\/]/u, [trimmed]) == null;
435 }
436 };
437
438 /**
439 * Returns whether the provided value is a filename‐safe base64 string.
440 *
441 * ※ This function returns false if the provided value is not a string
442 * primitive.
443 */
444 export const isFilenameSafeBase64 = ($) => {
445 if (typeof $ !== "string") {
446 return false;
447 } else {
448 const source = stringReplace($, /[\t\n\f\r ]+/gu, "");
449 const trimmed = source.length % 4 == 0
450 ? stringReplace(source, /={1,2}$/u, "")
451 : source;
452 return trimmed.length % 4 != 1 &&
453 call(reExec, /[^0-9A-Za-z_-]/u, [trimmed]) == null;
454 }
455 };
This page took 0.196385 seconds and 5 git commands to generate.