]> Lady’s Gitweb - Pisces/blob - binary.js
Fix base64 implementation
[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 base64 string into an
111 * ArrayBuffer.
112 *
113 * ※ This function is not exposed.
114 */
115 const decodeBase64 = (source, safe = false) => {
116 const u6s = map(
117 source.length % 4 == 0
118 ? stringReplace(source, /={1,2}$/u, "")
119 : source,
120 (ucsCharacter) => {
121 const code = getCodeUnit(ucsCharacter, 0);
122 const result = code >= 0x41 && code <= 0x5A
123 ? code - 65
124 : code >= 0x61 && code <= 0x7A
125 ? code - 71
126 : code >= 0x30 && code <= 0x39
127 ? code + 4
128 : code == (safe ? 0x2D : 0x2B)
129 ? 62
130 : code == (safe ? 0x5F : 0x2F)
131 ? 63
132 : -1;
133 if (result < 0) {
134 throw new RangeError(
135 `Piscēs: Invalid character in Base64: ${ucsCharacter}.`,
136 );
137 } else {
138 return result;
139 }
140 },
141 );
142 const { length } = u6s;
143 if (length % 4 == 1) {
144 throw new RangeError(
145 `Piscēs: Base64 string has invalid length: ${source}.`,
146 );
147 } else {
148 const dataView = new View(new Buffer(floor(length * 3 / 4)));
149 for (let index = 0; index < length - 1;) {
150 // The final index is not handled; if the string is not divisible
151 // by 4, some bits might be dropped. This matches the “forgiving
152 // decode” behaviour specified by WhatW·G for base64.
153 const dataIndex = ceil(index * 3 / 4);
154 const remainder = index % 4;
155 if (remainder == 0) {
156 call(viewSetUint8, dataView, [
157 dataIndex,
158 u6s[index] << 2 | u6s[++index] >> 4,
159 ]);
160 } else if (remainder == 1) {
161 call(viewSetUint8, dataView, [
162 dataIndex,
163 u6s[index] << 4 | u6s[++index] >> 2,
164 ]);
165 } else { // remainder == 2
166 call(viewSetUint8, dataView, [
167 dataIndex,
168 u6s[index] << 6 | u6s[++index, index++],
169 ]);
170 }
171 }
172 return call(getViewBuffer, dataView, []);
173 }
174 };
175
176 /**
177 * Returns the result of encoding the provided ArrayBuffer into a
178 * base64 string.
179 *
180 * ※ This function is not exposed.
181 */
182 const encodeBase64 = (buffer, safe = false) => {
183 const dataView = new View(buffer);
184 const byteLength = call(getBufferByteLength, buffer, []);
185 const minimumLengthOfResults = ceil(byteLength * 4 / 3);
186 const resultingCodeUnits = fill(
187 objectCreate(
188 binaryCodeUnitIterablePrototype,
189 {
190 length: {
191 value: minimumLengthOfResults +
192 (4 - (minimumLengthOfResults % 4)) % 4,
193 },
194 },
195 ),
196 0x3D,
197 );
198 for (let index = 0; index < byteLength;) {
199 const codeUnitIndex = ceil(index * 4 / 3);
200 const currentIndex = codeUnitIndex + +(
201 index % 3 == 0 && resultingCodeUnits[codeUnitIndex] != 0x3D
202 ); // every third byte handles two letters; this is for the second
203 const remainder = currentIndex % 4;
204 const currentByte = call(viewGetUint8, dataView, [index]);
205 const nextByte = remainder % 3 && ++index < byteLength
206 // digits 1 & 2 span multiple bytes
207 ? call(viewGetUint8, dataView, [index])
208 : 0;
209 const u6 = remainder == 0
210 ? currentByte >> 2
211 : remainder == 1
212 ? (currentByte & 0b00000011) << 4 | nextByte >> 4
213 : remainder == 2
214 ? (currentByte & 0b00001111) << 2 | nextByte >> 6
215 : (++index, currentByte & 0b00111111); // remainder == 3
216 const result = u6 < 26
217 ? u6 + 65
218 : u6 < 52
219 ? u6 + 71
220 : u6 < 62
221 ? u6 - 4
222 : u6 < 63
223 ? (safe ? 0x2D : 0x2B)
224 : u6 < 64
225 ? (safe ? 0x5F : 0x2F)
226 : -1;
227 if (result < 0) {
228 throw new RangeError(`Piscēs: Unexpected Base64 value: ${u6}.`);
229 } else {
230 resultingCodeUnits[currentIndex] = result;
231 }
232 }
233 return stringFromCodeUnits(...resultingCodeUnits);
234 };
235
236 /**
237 * Returns a source string generated from the arguments passed to a
238 * tag function.
239 *
240 * ※ This function is not exposed.
241 */
242 const sourceFromArgs = ($, $s) =>
243 stringReplace(
244 typeof $ == "string" ? $ : hasOwnProperty($, "raw")
245 ? rawString(
246 $,
247 ...objectCreate(argumentIterablePrototype, {
248 args: { value: $s },
249 }),
250 )
251 : `${$}`,
252 /[\t\n\f\r ]+/gu,
253 "",
254 );
255
256 /**
257 * Returns an ArrayBuffer generated from the provided base64 string.
258 *
259 * This function can also be used as a tag for a template literal. The
260 * literal will be interpreted akin to `String.raw`.
261 *
262 * ☡ This function throws if the provided string is not a valid base64
263 * string.
264 */
265 export const base64Binary = ($, ...$s) =>
266 decodeBase64(sourceFromArgs($, $s));
267
268 /**
269 * Returns a (big‐endian) base64 string created from the provided typed
270 * array, buffer, or (16‐bit) string.
271 *
272 * This function can also be used as a tag for a template literal. The
273 * literal will be interpreted akin to `String.raw`.
274 */
275 export const base64String = ($, ...$s) =>
276 encodeBase64(bufferFromArgs($, $s));
277
278 /**
279 * Returns an ArrayBuffer generated from the provided filename‐safe
280 * base64 string.
281 *
282 * This function can also be used as a tag for a template literal. The
283 * literal will be interpreted akin to `String.raw`.
284 *
285 * ☡ This function throws if the provided string is not a valid
286 * filename‐safe base64 string.
287 */
288 export const filenameSafeBase64Binary = ($, ...$s) =>
289 decodeBase64(sourceFromArgs($, $s), true);
290
291 /**
292 * Returns a (big‐endian) filename‐safe base64 string created from the
293 * provided typed array, buffer, or (16‐bit) string.
294 *
295 * This function can also be used as a tag for a template literal. The
296 * literal will be interpreted akin to `String.raw`.
297 */
298 export const filenameSafeBase64String = ($, ...$s) =>
299 encodeBase64(bufferFromArgs($, $s), true);
300
301 /**
302 * Returns whether the provided value is a Base64 string.
303 *
304 * ※ This function returns false if the provided value is not a string
305 * primitive.
306 */
307 export const isBase64 = ($) => {
308 if (typeof $ !== "string") {
309 return false;
310 } else {
311 const source = stringReplace($, /[\t\n\f\r ]+/gu, "");
312 const trimmed = source.length % 4 == 0
313 ? stringReplace(source, /={1,2}$/u, "")
314 : source;
315 return trimmed.length % 4 != 1 &&
316 call(reExec, /[^0-9A-Za-z+\/]/u, [trimmed]) == null;
317 }
318 };
319
320 /**
321 * Returns whether the provided value is a filename‐safe base64 string.
322 *
323 * ※ This function returns false if the provided value is not a string
324 * primitive.
325 */
326 export const isFilenameSafeBase64 = ($) => {
327 if (typeof $ !== "string") {
328 return false;
329 } else {
330 const source = stringReplace($, /[\t\n\f\r ]+/gu, "");
331 const trimmed = source.length % 4 == 0
332 ? stringReplace(source, /={1,2}$/u, "")
333 : source;
334 return trimmed.length % 4 != 1 &&
335 call(reExec, /[^0-9A-Za-z_-]/u, [trimmed]) == null;
336 }
337 };
This page took 0.076368 seconds and 5 git commands to generate.