@lumjs/encode 1.2.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [2.1.0]
10
+ ### Changed
11
+ - The `base64` library now supports the `Uint8Array.fromBase64` static method,
12
+ and the corresponding `toBase64()` instance method. As those are rather new
13
+ APIs and aren't supported by every JS runtime yet, it will check for them and
14
+ use them if found, and fall back to the prior implementation otherwise.
15
+ - Updated this changelog, as I forgot to after 2.0 was released last year.
16
+
17
+ ## [2.0.0] - 2024-08-14
18
+ ### Changed
19
+ - A major overhaul, removing the dependency on `crypto-js` library.
20
+ - Rewrote `base64` library to use `atob`, `btoa`, `TextEncoder`, and `TextDecoder`.
21
+ - Rewrote `hash` library using the modern `SubtleCrypto` APIs.
22
+ - Moved `urlize()` and `deurlize()` functions into `base64` library.
23
+ ### Removed
24
+ - The `crypto` sub-module which was designed as a wrapper for `crypto-js`.
25
+ - The `safe64` library has been moved into a standalone [safe64-data] package.
26
+ - Dependencies on `php-serialize` and `ubjson` which were only used by `safe64`.
27
+
9
28
  ## [1.2.0] - 2023-01-06
10
29
  ### Changed
11
30
  - Bumped `@lumjs/core` to `1.8.0`.
@@ -23,7 +42,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
23
42
  ### Added
24
43
  - Initial release.
25
44
 
26
- [Unreleased]: https://github.com/supernovus/lum.encode.js/compare/v1.2.0...HEAD
45
+ [Unreleased]: https://github.com/supernovus/lum.encode.js/compare/v2.1.0...HEAD
46
+ [2.1.0]: https://github.com/supernovus/lum.encode.js/compare/v2.0.0...v2.1.0
47
+ [2.0.0]: https://github.com/supernovus/lum.encode.js/compare/v1.2.0...v2.0.0
27
48
  [1.2.0]: https://github.com/supernovus/lum.encode.js/compare/v1.1.0...v1.2.0
28
49
  [1.1.0]: https://github.com/supernovus/lum.encode.js/compare/v1.0.0...v1.1.0
29
50
  [1.0.0]: https://github.com/supernovus/lum.encode.js/releases/tag/v1.0.0
51
+ [safe64-data]: https://github.com/supernovus/lum.safe64-data.js
52
+
package/README.md CHANGED
@@ -2,9 +2,20 @@
2
2
 
3
3
  A bunch of encoding libraries.
4
4
 
5
- Among other things, it offers a pure JS implementation of my
6
- [Safe64](https://github.com/supernovus/lum.encode.php)
7
- data serialization and encoding format.
5
+ ## 2.x Release Notes
6
+
7
+ Version `2.0` is pretty much a complete rewrite, and as the major version
8
+ change indicates, is not 100% backwards compatible. I've tried to make the
9
+ migration process as painless as possible, but some changes will be required.
10
+
11
+ - Split the [Safe64] libraries into their own package.
12
+ - Rewrote the `base64` and `hash` libraries to use modern `ES2015+` APIs.
13
+ - Dropped the dependency on the legacy `crypto-js` package.
14
+ - The `base64` libraries now have additional `async` functions for working
15
+ with arbitrary data in addition to the synchronous ones for Unicode text.
16
+ - The `hash` library now uses `async` methods due to the `SubtleCrypto` API
17
+ that we're now using for generating the hashes.
18
+ - Minor cleanups in `base91` and `utils` modules.
8
19
 
9
20
  ## Official URLs
10
21
 
@@ -20,3 +31,7 @@ Timothy Totten <2010@totten.ca>
20
31
  ## License
21
32
 
22
33
  [MIT](https://spdx.org/licenses/MIT.html)
34
+
35
+ ---
36
+
37
+ [Safe64]: https://github.com/supernovus/lum.safe64-data.js
package/lib/base64.js CHANGED
@@ -1,55 +1,430 @@
1
1
 
2
- const {S,isObj} = require('@lumjs/core/types');
3
- const Base64 = require('crypto-js/enc-base64');
4
- const Utf8 = require('crypto-js/enc-utf8');
2
+ const {S,B,F,isObj} = require('@lumjs/core/types');
3
+
4
+ const D_MIME = 'application/octet-stream';
5
+ const D_ENC = 'utf-8';
6
+ const P_DATA = 'data:';
7
+ const P_B64 = ';base64,';
8
+ const R_PRE = /^data\:(.*?);base64,/;
9
+ const B_64U = 'base64url';
10
+
11
+ const UI8_FB64 = (typeof Uint8Array.fromBase64 === F);
12
+ const URL_CHARS = /_-/;
5
13
 
6
14
  /**
7
15
  * Base64 functions.
8
16
  *
9
- * Provides friendlier wrappers around the `crypto-js` libraries.
17
+ * Several functions based on code from MDN guides:
18
+ * https://developer.mozilla.org/en-US/docs/Glossary/Base64
10
19
  *
11
20
  * @module @lumjs/encode/base64
12
21
  */
13
22
 
14
23
  /**
15
- * Encode data as a `Base64` string.
16
- *
17
- * @param {(string|WordArray)} rawdata - The data we want to encode.
24
+ * Make a Base64 string URL-safe.
25
+ *
26
+ * Converts `+` to `-` and `/` to `_`.
27
+ * By default it also strips `=` padding characters.
18
28
  *
19
- * If this is a `string` we'll convert it into a `WordArray` using
20
- * the `stringFormat` object.
29
+ * @param {string} string - A Base64-encoded string
30
+ * @param {object} [options] Options
31
+ * @param {boolean} [options.useTildes=false] Use tildes?
21
32
  *
22
- * @param {object} [stringFormat=Utf8] The string format.
33
+ * Replaces `=` with `~` characters instead of stripping them.
34
+ * This option is for backwards-compatibility with old code only,
35
+ * and there's no reason to use it these days.
36
+ *
37
+ * @returns {string}
38
+ */
39
+ function urlize(string, options={})
40
+ {
41
+ string = string.replaceAll('+', '-');
42
+ string = string.replaceAll('/', '_');
43
+ string = string.replaceAll('=', options.useTildes ? '~' : '');
44
+ return string;
45
+ }
46
+
47
+ /**
48
+ * Undoes the effects of `urlize()`
49
+ *
50
+ * Doesn't matter if the string has actually been passed to `urlize()`,
51
+ * nor if the obsolete `options.useTildes` option was used when encoding.
52
+ *
53
+ * @param {string} string
54
+ * @returns {string}
55
+ */
56
+ function deurlize(string)
57
+ {
58
+ string = string.replaceAll('-', '+');
59
+ string = string.replaceAll('_', '/');
60
+ string = string.replaceAll('~', '=');
61
+ string += "===".substring((string.length+3)%4);
62
+ return string;
63
+ }
64
+
65
+ /**
66
+ * Convert a Base64-encoded string into a Uint8Array.
23
67
  *
24
- * Can be any encoding module from the `crypto-js` library.
25
- * Default is `CryptoJS.enc.Utf8`
68
+ * This is a low-level function with no options.
69
+ * See `decodeText()` for a more full-featured function.
26
70
  *
27
- * @return {string} The encoded string.
71
+ * This now checks to see if `Uint8Array.fromBase64()` exists,
72
+ * and if it does, will use it. Otherwise it uses the original
73
+ * encoding algorithm.
74
+ *
75
+ * @param {string} base64 - Base64 encoded-string
76
+ * @param {object} [options] Passed to fromBase64() if it exists
77
+ * @returns {Uint8Array}
28
78
  */
29
- exports.encode = function(rawdata, stringFormat=Utf8)
79
+ function toBytes(base64, options={})
30
80
  {
31
- const data = typeof rawdata === S ? stringFormat.parse(rawdata) : rawdata;
32
- return Base64.stringify(data);
81
+ if (UI8_FB64)
82
+ {
83
+ return Uint8Array.fromBase64(base64, options);
84
+ }
85
+
86
+ const binString = atob(base64);
87
+ return Uint8Array.from(binString, (m) => m.codePointAt(0));
33
88
  }
34
89
 
35
90
  /**
36
- * Decode a `Base64` string back into raw data.
91
+ * Convert a Uint8Array into Base64-encoded string.
92
+ *
93
+ * This is a low-level function with no options.
94
+ * See `encodeText()` for a more full-featured function.
37
95
  *
38
- * @param {string} string - The Base64 string to decode.
96
+ * This now checks to see if `bytes.toBase64()` exists,
97
+ * and if it does, will use it. Otherwise it uses the original
98
+ * decoding algorithm.
99
+ *
100
+ * @param {Uint8Array} bytes - Byte array to convert
101
+ * @param {object} [options] Passed to toBase64() if it exists
102
+ * @returns {string}
103
+ */
104
+ function fromBytes(bytes, options={})
105
+ {
106
+ if (typeof bytes.toBase64 === F)
107
+ {
108
+ return bytes.toBase64(options);
109
+ }
110
+
111
+ const binString = Array.from(bytes, (byte) =>
112
+ String.fromCodePoint(byte),
113
+ ).join("");
114
+ return btoa(binString);
115
+ }
116
+
117
+ /**
118
+ * Encode a string to Base64.
119
+ *
120
+ * Uses the `TextEncoder` API and `fromBytes()`.
121
+ * May optionally pass the output through `urlize()`.
122
+ *
123
+ * @param {string} data - Any valid (Unicode) string
124
+ * @param {(object|boolean)} [options] Options
125
+ *
126
+ * - If `boolean`, used as `options.url`
127
+ * - Passed to `urlize()` if `options.url` is `true`
128
+ *
129
+ * @param {boolean} [options.url=false] Urlize the output?
130
+ * If true, converts `+`, `/`, and `=` to URL-friendly alternatives.
131
+ *
132
+ * @returns {string} A Base64-encoded string
133
+ */
134
+ function encodeText(data, options={})
135
+ {
136
+ if (typeof options === B)
137
+ { // Assume the 'url' option.
138
+ options = {url: options};
139
+ }
140
+
141
+ const encoder = new TextEncoder();
142
+ data = encoder.encode(data);
143
+ const toB64 = (typeof data.toBase64 === F);
144
+
145
+ if (toB64 && options.url)
146
+ {
147
+ if (typeof options.alphabet !== S)
148
+ {
149
+ options.alphabet = B_64U;
150
+ }
151
+ if (!options.useTildes && typeof options.omitPadding !== B)
152
+ {
153
+ options.omitPadding = true;
154
+ }
155
+ }
156
+
157
+ const base64 = fromBytes(data, options);
158
+ return (options.url && !toB64) ? urlize(base64, options) : base64;
159
+ }
160
+
161
+ /**
162
+ * Decode a Base64 string into a Unicode string.
163
+ *
164
+ * Uses `toBytes()` and the `TextDecoder` API.
165
+ * Will pass input through `deurlize()` by default.
166
+ *
167
+ * @param {string} base64 - A Base64 string to decode
168
+ * @param {(object|boolean)} [options] Options
169
+ *
170
+ * - If `boolean`, used as `options.url`
171
+ * - Passed to `new TextDecoder()`
172
+ * - Passed to `decoder.decode()`
173
+ *
174
+ * @param {boolean} [options.url=true] Deurlize the output?
175
+ *
176
+ * Unless this is explicitly set as `false`, the `base64`
177
+ * string will be passed to `deurlize()` before being
178
+ * processed further.
179
+ *
180
+ * @returns {string} A Unicode string
181
+ */
182
+ function decodeText(base64, options={})
183
+ {
184
+ if (typeof options === B)
185
+ { // Assume the 'url' option.
186
+ options = {url: options};
187
+ }
188
+
189
+ if (options.url !== false)
190
+ { // Unless explicitly disabled, use deurlize() first.
191
+ if (UI8_FB64)
192
+ {
193
+ if (typeof options.alphabet !== S)
194
+ {
195
+ if (URL_CHARS.test(base64))
196
+ {
197
+ options.alphabet = B_64U;
198
+ }
199
+ }
200
+ }
201
+ else
202
+ {
203
+ base64 = deurlize(base64);
204
+ }
205
+ }
206
+
207
+ const encoding = options.encoding ?? D_ENC;
208
+ const decoder = new TextDecoder(encoding, options);
209
+ return decoder.decode(toBytes(base64, options), options);
210
+ }
211
+
212
+ /**
213
+ * Encode binary data into a Base64-encoded Data URL.
214
+ *
215
+ * @param {(File|Blob|Array|TypedArray|ArrayBuffer)} data - Data to encode
216
+ *
217
+ * If this is not a `Blob` or `File`, it will be converted into one.
218
+ *
219
+ * @param {object} [options] Options
220
+ *
221
+ * @param {object} [options.blob] Options for Blob instances.
222
+ *
223
+ * If specified, this will be passed to the `Blob()` constructor.
224
+ *
225
+ * Only used if `data` is not already a `Blob` or `File` instance,
226
+ * and `options.file` was not specified or set to `false`.
39
227
  *
40
- * @param {(object|false)} [stringFormat=Utf8] The string format.
228
+ * @param {(object|boolean)} [options.file] Options for File instances.
41
229
  *
42
- * Can be any encoder library from the `crypto-js` library.
43
- * Default is `CryptoJS.enc.Utf8`
230
+ * If this is any non-false value, and `data` is not already a `Blob`,
231
+ * then we will convert `data` into a `File` instance instead of a `Blob`.
44
232
  *
45
- * If this is `false`, we'll return a `WordArray` object.
233
+ * If this is an `object`, it will be passed to the `File()` constructor.
234
+ *
235
+ * @param {string} [options.file.name] Filename for the `File` instance.
236
+ *
237
+ * This is likely never needed, but is kept for completion sake.
238
+ *
239
+ * @returns {Promise<string>} Resolves to the Data URL
240
+ */
241
+ async function toDataUrl(data, options={})
242
+ {
243
+ if (!(data instanceof Blob))
244
+ { // Build a Blob or File instance out of the passed data.
245
+ if (!Array.isArray(data))
246
+ { // Wrap the data in an Array.
247
+ data = [data];
248
+ }
249
+
250
+ // Sources for our Blob/File options.
251
+ const optsrc = [{type: D_MIME}, options];
252
+
253
+ if (options.file)
254
+ { // Let's build a File.
255
+ if (isObj(options.file))
256
+ {
257
+ optsrc.push(options.file);
258
+ }
259
+ const fopts = Object.assign(...optsrc);
260
+ const fname = fopts.filename ?? fopts.name ?? '';
261
+ data = new File(data, fname, fopts);
262
+ }
263
+ else
264
+ { // Let's build a Blob.
265
+ if (isObj(options.blob))
266
+ {
267
+ optsrc.push(options.blob);
268
+ }
269
+ const bopts = Object.assign(...optsrc);
270
+ data = new Blob(data, bopts);
271
+ }
272
+ } // Ensure sufficient Blobiness.
273
+
274
+ return await new Promise((resolve, reject) =>
275
+ {
276
+ const reader = Object.assign(new FileReader(),
277
+ {
278
+ onload: () => resolve(reader.result),
279
+ onerror: () => reject(reader.error),
280
+ });
281
+ reader.readAsDataURL(data);
282
+ });
283
+
284
+ } // toDataUrl()
285
+
286
+ /**
287
+ * Decode a Data URL into arbitrary binary data.
288
+ *
289
+ * @param {string} dataUrl - A valid Data URL
290
+ * @param {object} [options] Options
291
+ * @param {boolean} [options.response=false] Return `Response`
292
+ * @param {boolean} [options.buffer=false] Return `ArrayBuffer`
46
293
  *
47
- * @return {(string|WordArray)} The decoded output.
294
+ * @returns {Promise<(Uint8Array|ArrayBuffer|Response)>} Promise of data
295
+ *
296
+ * By default this resolves to a `Uint8Array` instance.
297
+ *
298
+ * See `options.response` and `options.buffer` for alternative values that
299
+ * this may resolve to if requested.
300
+ *
301
+ */
302
+ async function fromDataUrl(dataUrl, options={})
303
+ {
304
+ const res = await fetch(dataUrl);
305
+ if (options.response) return res;
306
+ const buf = await res.arrayBuffer();
307
+ if (options.buffer) return buf;
308
+ return new Uint8Array(buf);
309
+ }
310
+
311
+ /**
312
+ * A wrapper around `toDataUrl()` that strips the Data URL header,
313
+ * leaving just the Base64 string, and can emit URL-safe strings.
314
+ *
315
+ * @param {mixed} data - See `toDataUrl()` for valid values
316
+ * @param {object} [options] Options
317
+ *
318
+ * - Passed to `toDataUrl()`
319
+ * - Passed to `urlize()` if `options.url` is `true`
320
+ *
321
+ * @param {boolean} [options.url=false] Use `urlize()` on encoded string?
322
+ *
323
+ * @returns {Promise<string>} Resolves to a Base64 string
324
+ */
325
+ async function encodeData(data, options={})
326
+ {
327
+ let base64 = await toDataUrl(data, options).replace(R_PRE, '');
328
+ return options.url ? urlize(base64, options) : base64;
329
+ }
330
+
331
+ /**
332
+ * A wrapper around `fromDataUrl()` that adds a Data URL header
333
+ * if necessary, and can handle URL-safe Base64 strings.
334
+ *
335
+ * @param {*} base64
336
+ * @param {*} options
337
+ *
338
+ * - Passed to `fromDataUrl()`
339
+ * - Passed to `deurlize()` if `options.url` is NOT set to `false`
340
+ *
341
+ * @param {boolean} [options.url=true] Use `deurlize()` on decoded string?
342
+ *
343
+ * @returns {Promise} See `fromDataUrl()` for more details
344
+ */
345
+ async function decodeData(base64, options={})
346
+ {
347
+ if (!R_PRE.test(base64))
348
+ { // Assume a raw base64 string.
349
+ if (options.url !== false)
350
+ { // Unless explicitly disabled, use deurlize() first.
351
+ base64 = deurlize(base64);
352
+ }
353
+ const type = (typeof options.type === S) ? options.type : D_MIME;
354
+ base64 = P_DATA+type+P_B64+base64;
355
+ }
356
+
357
+ return fromDataUrl(base64, options);
358
+ }
359
+
360
+ /**
361
+ * Encode data into a base64 string
362
+ *
363
+ * Uses `encodeText()` unless the `data` or `options` have specific
364
+ * values that indicate `encodeData()` should be used instead.
365
+ *
366
+ * @param {*} data - Data to encode
367
+ *
368
+ * If this is anything other than a `string`, `encodeData()` will be used.
369
+ *
370
+ * @param {object} [options] Options
371
+ *
372
+ * If either `options.blob` or `options.file` are specified,
373
+ * `encodeData()` will be used.
374
+ *
375
+ * @returns {(string|Promise<string>)}
376
+ * See `encodeText()` and `encodeData()` for details.
48
377
  */
49
- exports.decode = function(string, stringFormat=Utf8)
378
+ function encode(data, options={})
379
+ {
380
+ if (options.blob || options.file || (typeof data !== S))
381
+ {
382
+ return encodeData(data, options);
383
+ }
384
+ else
385
+ {
386
+ return encodeText(data, options);
387
+ }
388
+ }
389
+
390
+ /**
391
+ * Decode a base64 string into data
392
+ *
393
+ * Uses `decodeText()` unless the `base64` or `options` have specific
394
+ * values that indicate `decodeData()` should be used.
395
+ *
396
+ * @param {string} base64 - Base64-encoded string (or a Data URL).
397
+ *
398
+ * If this begins with a Data URL header, `decodeData()` will be used.
399
+ *
400
+ * @param {object} [options] Options
401
+ *
402
+ * If either `options.response` or `options.buffer` are true,
403
+ * `decodeData()` will be used.
404
+ *
405
+ * @returns {mixed} See `decodeText()` and `decodeData()` for details;
406
+ * will always be a `Promise` if `decodeData()` was used.
407
+ */
408
+ function decode(base64, options={})
409
+ {
410
+ if (options.response || options.buffer || R_PRE.test(base64))
411
+ {
412
+ return decodeData(base64, options);
413
+ }
414
+ else
415
+ {
416
+ return decodeText(base64, options);
417
+ }
418
+ }
419
+
420
+ module.exports =
50
421
  {
51
- const data = Base64.parse(string);
52
- return (isObj(stringFormat) ? data.toString(stringFormat) : data);
422
+ urlize, deurlize,
423
+ toBytes, fromBytes,
424
+ encodeText, decodeText,
425
+ toDataUrl, fromDataUrl,
426
+ encodeData, decodeData,
427
+ encode, decode,
428
+ NATIVE_BASE64: UI8_FB64,
53
429
  }
54
430
 
55
- exports.Utf8 = Utf8;
package/lib/base91.js CHANGED
@@ -1,4 +1,4 @@
1
- const {S,U,B} = require('@lumjs/core/types');
1
+ const {S,B} = require('@lumjs/core/types');
2
2
 
3
3
  /**
4
4
  * A pure-Javascript base91 library.
@@ -169,17 +169,17 @@ exports.decode = function(data, opts={})
169
169
  opts = {string: opts};
170
170
  }
171
171
 
172
- if (opts.string && typeof Uint8Array !== U && typeof TextDecoder !== U)
172
+ if (opts.string)
173
173
  {
174
174
  const uint = Uint8Array.from(output);
175
175
  const td = new TextDecoder();
176
176
  return td.decode(uint);
177
177
  }
178
- if (opts.uint && typeof Uint8Array !== U)
178
+ if (opts.uint)
179
179
  {
180
180
  return Uint8Array.from(output);
181
181
  }
182
- else if (opts.buffer && typeof Buffer !== U)
182
+ else if (opts.buffer)
183
183
  {
184
184
  return new Buffer.from(output);
185
185
  }