@lumjs/encode 1.1.0 → 2.0.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/lib/hash.js CHANGED
@@ -1,18 +1,50 @@
1
1
  const {S,F,isObj,isNil} = require('@lumjs/core/types');
2
2
 
3
3
  const util = require('./util');
4
- const Base91 = require('./base91');
5
- const Safe64 = require('./safe64');
6
- const Crypto = require('./crypto');
7
- const cload = Crypto.load;
4
+ const base64 = require('./base64');
5
+ const base91 = require('./base91');
8
6
 
9
- const DEFALGO = 'SHA256';
7
+ const D_ALGO = 'SHA-256';
8
+ const D_ADD_ENC = 'base64';
9
+ const D_TIMEOUT = 10000;
10
+ const D_TRY_TIME = 100;
11
+
12
+ const ALGO_ALIASES =
13
+ {
14
+ SHA1: 'SHA-1',
15
+ SHA256: 'SHA-256',
16
+ SHA384: 'SHA-384',
17
+ SHA512: 'SHA-512',
18
+ }
19
+
20
+ const ALGO_INFO =
21
+ {
22
+ 'SHA-1': {length: 160, block: 512},
23
+ 'SHA-256': {length: 256, block: 512},
24
+ 'SHA-384': {length: 384, block: 1024},
25
+ 'SHA-512': {length: 512, block: 1024},
26
+ }
27
+
28
+ const DATA_ENCODERS =
29
+ {
30
+ base64, base91,
31
+ };
32
+
33
+ /**
34
+ * Algorithm info object
35
+ * @typedef {object} module:@lumjs/encode/hash~Algo
36
+ * @property {string} id - The formal id/name of the algorithm
37
+ * @property {number} length - The output length (in bits)
38
+ * @property {number} block - The block size (in bits)
39
+ */
10
40
 
11
41
  /**
12
- * Hashifier
13
- *
14
42
  * A simple yet flexible class for building cryptographic hashes.
15
43
  *
44
+ * @property {module:@lumjs/encode/hash~Algo} algo - Digest algorithm in use
45
+ * @property {object} current - Progressive digest generation data (internal)
46
+ * @property {object} defaults - Default options for various methods.
47
+ *
16
48
  * @exports module:@lumjs/encode/hash
17
49
  */
18
50
  module.exports = class
@@ -24,23 +56,69 @@ module.exports = class
24
56
  *
25
57
  * If this is a `string`, it's assumed to be the `options.algo` option.
26
58
  *
27
- * @param {string} [options.algo="SHA256"] Hashing algorithm
59
+ * @param {string} [options.algo="SHA-256"] Digest (hash) algorithm
28
60
  *
29
- * By default we use "SHA256" for backwards compatibility with my
30
- * older libraries and apps. You can set it to any *hashing algorithm*
31
- * from the `crypto-js` library (we use our own
32
- * [crypto]{@link module:@lumjs/encode/crypto} helper.)
61
+ * By default we use `SHA-256` for backwards compatibility with my
62
+ * older libraries and apps. You can set it to any *digest algorithm*
63
+ * supported by the `SubtleCrypto` API.
33
64
  *
34
- * @param {object} [options.safe64] Default options for `safe64()`
65
+ * We look up the algorithm with
66
+ * [getAlgorithm()]{@link module:@lumjs/encode/hash#getAlgorithm},
67
+ * and so support hyphenless aliases and case-insensitive ids.
68
+ *
69
+ * For more information on supported digest algorithms, see:
70
+ * https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest
71
+ *
72
+ * @param {object} [options.base64] Default options for `base64()`
35
73
  *
36
74
  * If specified, this will become the default options for the
37
- * [safe64()]{@link module:@lumjs/encode/hash#safe64} method.
75
+ * [base64()]{@link module:@lumjs/encode/hash#base64} method.
38
76
  *
39
77
  * @param {object} [options.base91] Default options for `base91()`
40
78
  *
41
79
  * If specified, this will become the default options for the
42
- * [base91()]{@link module:@lumjs/encode/hash#base9164} method.
80
+ * [base91()]{@link module:@lumjs/encode/hash#base91} method.
81
+ *
82
+ * @param {(string|function|object)} [options.addUsing="base64"]
83
+ * Encoder for `add()` to use when non-string values are passed.
84
+ *
85
+ * If this is a `string`, it may be either `"base64"` or `"base91"`.
86
+ *
87
+ * If it is a `function`, it will be sent the data value and must
88
+ * return an encoded string representation of that value.
89
+ *
90
+ * If this is an `object`, it must have a method called `encode()`
91
+ * that works the same way as if you'd passed a `function`.
43
92
  *
93
+ * @param {(string|function)} [options.joinWith=""]
94
+ * What `hash()` will use to join progressive data values.
95
+ *
96
+ * If this is a `string` the array of data values will be joined
97
+ * using the `array.join()` method.
98
+ *
99
+ * If it is a `function`, it will be sent the array of values,
100
+ * and must return an encoded representation of those values
101
+ * having been joined together in some fashion.
102
+ *
103
+ * The default value is an empty string, which means the data
104
+ * values are simply concatenated together with no separator.
105
+ *
106
+ * @param {number} [options.timeout=10000] Timeout for async encoding
107
+ *
108
+ * This is how long `hash()` will wait for async encoding to finish
109
+ * before throwing an error indicating something went wrong.
110
+ *
111
+ * Value is in milliseconds.
112
+ *
113
+ * @param {number} [options.tryEvery=100] Interval to test async encoding
114
+ *
115
+ * If async encoding is ongoing, this is the interval `hash()` will
116
+ * test to see if encoding has finished.
117
+ *
118
+ * Value is in milliseconds.
119
+ *
120
+ * @throws {Error} If `options.algo` was an invalid algorithm.
121
+ *
44
122
  */
45
123
  constructor(options={})
46
124
  {
@@ -49,22 +127,67 @@ module.exports = class
49
127
  options = {algo: options};
50
128
  }
51
129
 
52
- const algo = options.algo ?? DEFALGO;
130
+ this.options = options;
53
131
 
54
- this.algoLib = cload.algo(algo);
55
- this.hashFunction = cload.hash(algo);
132
+ this.current =
133
+ {
134
+ queue: 0,
135
+ hash: [],
136
+ encoder: options.addUsing ?? D_ADD_ENC,
137
+ joiner: options.joinWith ?? '',
138
+ }
56
139
 
57
- //console.debug("<hash>", {hash: this, algo, options});
140
+ this.algo = this.getAlgorithm(
141
+ (typeof options.algo === S)
142
+ ? options.algo
143
+ : D_ALGO);
58
144
 
59
- if (!isObj(this.algoLib) || typeof this.hashFunction !== F)
145
+ if (this.algo === null)
60
146
  {
61
- throw new Error("Invalid hashing algorithm");
147
+ throw new Error("Invalid 'algo' specified: "+options.algo);
62
148
  }
63
149
 
64
- this.safe64Options = options.safe64 ?? {};
65
- this.base91Options = options.base91 ?? {};
150
+ this.defaults =
151
+ {
152
+ base64: options.base64 ?? {},
153
+ base91: options.base91 ?? {},
154
+ }
66
155
 
67
- } // construct()
156
+ this.timeout = options.timeout ?? D_TIMEOUT;
157
+ this.tryEvery = options.tryEvery ?? D_TRY_TIME;
158
+
159
+ } // construct
160
+
161
+ /**
162
+ * Lookup a hashing algorithm and return details about it
163
+ *
164
+ * @param {string} id - The name of the algorithm.
165
+ *
166
+ * It's case-insensitive (will be forced to uppercase),
167
+ * and you may omit the hyphen in the algorithm name.
168
+ * Thus `sha256` is the same as `SHA-256`.
169
+ *
170
+ * @returns {?module:@lumjs/encode/hash~Algo}
171
+ * Will be an Algo info object if the `id` was valid,
172
+ * or `null` otherwise.
173
+ */
174
+ getAlgorithm(id)
175
+ {
176
+ id = id.toString().toUpperCase();
177
+
178
+ if (id in ALGO_ALIASES)
179
+ {
180
+ id = ALGO_ALIASES[id];
181
+ }
182
+
183
+ if (id in ALGO_INFO)
184
+ {
185
+ return Object.assign({id}, ALGO_INFO[id]);
186
+ }
187
+
188
+ // Invalid algorithm id, nothing to return.
189
+ return null;
190
+ }
68
191
 
69
192
  /**
70
193
  * Get a cryptographic *hash*.
@@ -74,120 +197,118 @@ module.exports = class
74
197
  * Otherwise, if there is a current *progressive hash* in the process of
75
198
  * being built, it will be *finalized* and returned.
76
199
  *
77
- * This will return `undefined` if there is no valid data to be hashed.
78
- *
79
- * @param {(string|WordArray)} [input] Input to hash immediately.
200
+ * @param {(string|object)} [input] Input to hash immediately
80
201
  *
81
- * @return {WordArray|undefined}
202
+ * @return {Promise<ArrayBuffer>} See `SubtleCrypto.digest()` for details.
203
+ *
204
+ * @throws {TypeError} If the `options.joinWith` value is invalid,
205
+ * and no `input` was passed.
206
+ *
82
207
  */
83
- hash(input)
208
+ async hash(input)
84
209
  {
85
210
  if (isNil(input))
86
211
  { // No input, let's see if we have a progrssive hash being built.
87
- if (typeof this.currentHash === 'object')
88
- { // Let's return the finalized hash.
89
- let value = this.currentHash.finalize();
90
- delete this.currentHash;
91
- return value;
212
+ if (this.current.queue !== 0)
213
+ { // We need to wait for some asynchronous encoding to finish.
214
+ return new Promise((resolve, reject) =>
215
+ {
216
+ const timeout = setTimeout(() =>
217
+ {
218
+ clearInterval(test);
219
+ clearTimeout(timeout);
220
+ reject(new Error("Timed out waiting for async encoding"));
221
+ }, this.timeout);
222
+
223
+ const test = setInterval(() =>
224
+ {
225
+ if (this.current.queue === 0)
226
+ {
227
+ clearInterval(test);
228
+ clearTimeout(timeout);
229
+ resolve(this.hash(input));
230
+ }
231
+ }, this.tryEvery);
232
+ });
233
+ }
234
+
235
+ const joiner = this.current.joiner;
236
+
237
+ if (typeof joiner === F)
238
+ {
239
+ input = joiner.call(this, this.current.hash);
92
240
  }
241
+ else if (typeof joiner === S)
242
+ {
243
+ input = this.current.hash.join(joiner);
244
+ }
245
+ else
246
+ {
247
+ console.error({joiner, input, hashifier: this});
248
+ throw new TypeError("Invalid 'joinWith' value");
249
+ }
250
+
251
+ // Now clear the current hash values.
252
+ this.current.hash.length = 0;
93
253
  }
94
- else if (typeof input === 'string' || this.valid(hash))
95
- { // Input data was passed, let's hash it now.
96
- return this.hashFunction(input);
254
+
255
+ if (typeof input === S)
256
+ { // This is super simple.
257
+ const encoder = new TextEncoder();
258
+ input = encoder.encode(input);
97
259
  }
98
- }
260
+ else if (input instanceof Blob)
261
+ { // As is this.
262
+ input = await input.arrayBuffer();
263
+ }
264
+
265
+ //console.debug({algo: this.algo, input});
99
266
 
100
- // Not really a useful public method.
101
- valid(hash)
102
- { // WordArray is not a standard object and doesn't work with instanceof.
103
- return (isObj(hash) && typeof hash.toString === 'function');
267
+ return crypto.subtle.digest(this.algo.id, input);
104
268
  }
105
269
 
106
270
  /**
107
271
  * Get hash as a Hex string.
108
272
  *
109
- * @param {(string|WordArray)} [input]
110
- * See [hash()]{@link module:@lumjs/encode/hash#hash}
111
- * @returns {string}
273
+ * @param {(string|object|null)} [input]
274
+ * See [hash()]{@link module:@lumjs/encode/hash#hash} for details.
112
275
  *
113
- * This uses the default `Hex` encoder from `crypto-js` to perform
114
- * the hashing, so output is directly as if you'd used that library.
276
+ * @returns {string}
115
277
  */
116
- hex(input)
278
+ async hex(input)
117
279
  {
118
- const hash = this.hash(input);
119
- if (this.valid(hash)) return hash.toString();
280
+ const hashBuf = await this.hash(input);
281
+ const hashArr = Array.from(new Uint8Array(hashBuf));
282
+ return (hashArr.map((b)=>b.toString(16).padStart(2, "0")).join(""));
120
283
  }
121
284
 
122
285
  /**
123
286
  * Get hash as a Base64-encoded string.
124
287
  *
125
- * @param {(string|WordArray)} [input]
126
- * See [hash()]{@link module:@lumjs/encode/hash#hash}
127
- * @returns {string}
288
+ * @param {(string|object|null)} [input]
289
+ * See [hash()]{@link module:@lumjs/encode/hash#hash} for details.
128
290
  *
129
- * This uses the direct `Base64` encoder from `crypto-js` to perform
130
- * the hashing, so output is directly as if you'd used that library.
131
- */
132
- base64(input)
133
- {
134
- const hash = this.hash(input);
135
- const base64 = Crypto.enc.Base64;
136
- if (this.valid(hash)) return hash.toString(base64);
137
- }
138
-
139
- /**
140
- * Get hash as a Safe64-encoded string.
141
- *
142
- * Calls [base64()]{@link module:@lumjs/encode/hash#base64},
143
- * then passes the output from that to
144
- * [urlize()]{@link module:@lumjs/encode/safe64.urlize} to
145
- * convert into the *Safe64* format.
146
- *
147
- * @param {(string|WordArray)} [input]
148
- * See [hash()]{@link module:@lumjs/encode/hash#hash}
149
- * @param {object} [opts]
150
- * Options for `urlize()`.
151
- * @returns {string}
291
+ * @param {object} [options] Options for Base64 encoding
152
292
  *
153
- * By default it's a raw *Safe64* string with **no** header,
154
- * and **no** tildes. The `opts` passed can change the format
155
- * by enabling *tildes* or adding a *header*.
293
+ * @param {boolean} [options.url=false] Use URL-safe variant?
156
294
  *
295
+ * @returns {string}
157
296
  */
158
- safe64(input, opts={})
297
+ async base64(input, opts=this.defaults.base64)
159
298
  {
160
- let base64 = this.base64(input);
161
- if (base64)
162
- return Safe64.urlize(base64, opts);
299
+ const hash = await this.hash(input);
300
+ const b64str = base64.fromBytes(new Uint8Array(hash));
301
+ return opts.url ? base64.urlize(b64str) : b64str;
163
302
  }
164
303
 
165
304
  /**
166
305
  * Get hash as a Base91-encoded string.
167
306
  *
168
- * The `crypto-js` library set currently does not natively support
169
- * the `base91` encoding format.
170
- *
171
- * So this offers a few different ways to encode `crypto-js` hashes
172
- * in `base91` format using our own
173
- * [base91]{@link module:@lumjs/encode/base91} module.
174
- *
175
307
  * @param {(string|WordArray)} [input]
176
308
  * See [hash()]{@link module:@lumjs/encode/hash#hash}
177
309
  * @param {object} [opts] Options for how to encode the hash.
178
- * @param {boolean} [opts.words=false] Use `hash.words`?
179
- *
180
- * If this is `true`, we will pass the `hash.words` array to
181
- * the `base91.encode()` method to encode them.
182
310
  *
183
- * If this is true, then `opts.enc` and `opts.nba` are ignored.
184
- *
185
- * @param {object} [opts.enc] The `crypto-js` encoder.
186
- *
187
- * That can be any of the `crypto.enc.*` plugins.
188
- * If not specified, the default `Hex` encoding format will be used.
189
- *
190
- * @param {(boolean|object)} [opts.nba=true] Use `numByteArray()` ?
311
+ * @param {(boolean|object)} [opts.nba=false] Use `numByteArray()` ?
191
312
  *
192
313
  * If this is `true` the *hash string* will be passed to `numByteArray()`
193
314
  * with the *default options* and the output from that will be passed to
@@ -196,42 +317,90 @@ module.exports = class
196
317
  * If this is an `object`, then the same logic as `true` applies, except this
197
318
  * will be used as the *explicit options* for the `numByteArray()` method.
198
319
  *
199
- * If this is `false` the *hash string* itself will be passed directly to
200
- * `base91.encode()` without further encoding.
201
- *
202
- * The default value of `true` is provided for backwards compatibility with
203
- * the previous version of this library which did not have as many options.
320
+ * If this is `false` the `ArrayBffer` will be converted into a `Uint8Array`,
321
+ * and that will be passed to `base91.encode()`.
204
322
  *
205
323
  * @returns {string}
206
324
  */
207
- base91(input, opts={})
325
+ async base91(input, opts=this.defaults.base91)
208
326
  {
209
- let hash = this.hash(input);
210
- if (this.valid(hash))
211
- return Base91.encode(util.numByteArray(hash.toString()))
327
+ const nba = isObj(opts.nba) ? opts.nba : (opts.nba === true ? {} : null);
328
+
329
+ let hash = await (nba ? this.hex(input) : this.hash(input));
330
+
331
+ if (nba)
332
+ {
333
+ hash = util.numByteArray(hash);
334
+ }
335
+
336
+ return base91.encode(new Uint8Array(hash));
212
337
  }
213
338
 
214
339
  /**
215
340
  * Add input to a progressive hash.
216
341
  *
217
- * If there is not already a progressive hash being built, we will
218
- * create one automatically. Use the `hash()` method with no parameter
219
- * to retrieve the finalized hash as a `WordArray` object, or one of
220
- * the other convenience methods to return it in a specific string
221
- * format.
222
- *
223
- * @param {string|WordArray} input A value to add to the hash.
342
+ * @param {(string|object)} input - A value to add to the hash.
343
+ *
344
+ * If it is an `object` then it will be processed with the `addWith`
345
+ * handler. See the constructor for details on supported formats.
346
+ *
347
+ * String values are simply added _as-is_.
224
348
  *
225
- * @return Lum.Hashifier The current object.
349
+ * @return {object} `this`
226
350
  */
227
- add(input)
351
+ add(input, opts={})
228
352
  {
229
- if (!isObj(this.currentHash))
230
- { // We need to start a new hash.
231
- this.currentHash = this.algoLib.create();
353
+ if (typeof input !== S)
354
+ {
355
+ let enc = this.current.encoder,
356
+ eopts = null;
357
+
358
+ if (typeof enc === S)
359
+ {
360
+ enc = DATA_ENCODERS[enc];
361
+ eopts = this.defaults[enc];
362
+ }
363
+
364
+ // Compile the options for the encoder.
365
+ eopts = Object.assign({hashifier: this}, eopts, opts);
366
+
367
+ if (typeof enc === F)
368
+ { // Call the function, using the hashifier instance as `this`.
369
+ input = enc.call(this, input, eopts);
370
+ }
371
+ else if (isObj(enc) && typeof enc.encode === F)
372
+ { // Using an encoder object/instance.
373
+ input = enc.encode(input, eopts);
374
+ }
375
+ else
376
+ {
377
+ console.error({enc, input, opts, hashifier: this});
378
+ throw new TypeError("Invalid 'addUsing' value");
379
+ }
232
380
  }
233
381
 
234
- this.currentHash.update(input);
382
+ let pos = this.current.hash.length + this.current.queue;
383
+
384
+ if (input instanceof Promise)
385
+ {
386
+ this.current.queue++;
387
+
388
+ input.then((data) =>
389
+ {
390
+ this.current.hash[pos] = data;
391
+ this.current.queue--;
392
+ }).catch((err) =>
393
+ {
394
+ console.error("An error occurred encoding data", err);
395
+ this.current.hash[pos] = '';
396
+ this.current.queue--;
397
+ });
398
+
399
+ }
400
+ else
401
+ {
402
+ this.current.hash[pos] = input;
403
+ }
235
404
 
236
405
  return this;
237
406
  }
package/lib/index.js CHANGED
@@ -3,47 +3,39 @@
3
3
  * @module @lumjs/encode
4
4
  */
5
5
 
6
- const {can,from} = require('@lumjs/core').buildModule(module);
6
+ const {def,lazy} = require('@lumjs/core/types');
7
+
8
+ const E = def.e;
9
+
10
+ const util = require('./util');
7
11
 
8
12
  /**
9
- * @name module:@lumjs/encode.ord
10
- * @function
13
+ * @alias module:@lumjs/encode.ord
11
14
  * @see {@link module:@lumjs/encode/util.ord}
12
15
  */
16
+ def(exports, 'ord', util.ord, E);
13
17
 
14
18
  /**
15
19
  * @name module:@lumjs/encode.numByteArray
16
20
  * @function
17
21
  * @see {@link module:@lumjs/encode/util.numByteArray}
18
22
  */
19
- from('./util', 'ord', 'numByteArray');
23
+ def(exports, 'numByteArray', util.numByteArray, E);
20
24
 
21
25
  /**
22
26
  * @name module:@lumjs/encode.Base64
23
27
  * @see {@link module:@lumjs/encode/base64}
24
28
  */
25
- can('Base64', true);
29
+ lazy(exports, 'Base64', () => require('./base64'), E);
26
30
 
27
31
  /**
28
32
  * @name module:@lumjs/encode.Base91
29
33
  * @see {@link module:@lumjs/encode/base91}
30
34
  */
31
- can('Base91', true);
32
-
33
- /**
34
- * @name module:@lumjs/encode.Safe64
35
- * @see {@link module:@lumjs/encode/safe64}
36
- */
37
- can('Safe64', true);
35
+ lazy(exports, 'Base91', () => require('./base91'), E);
38
36
 
39
37
  /**
40
38
  * @name module:@lumjs/encode.Hash
41
39
  * @see {@link module:@lumjs/encode/hash}
42
40
  */
43
- can('Hash', true);
44
-
45
- /**
46
- * @name module:@lumjs/encode.Crypto
47
- * @see {@link module:@lumjs/encode/crypto}
48
- */
49
- can('Crypto', true);
41
+ lazy(exports, 'Hash', () => require('./hash'), E);
package/lib/util.js CHANGED
@@ -69,6 +69,7 @@ exports.ord = function (string)
69
69
  *
70
70
  * @param {number} [options.base=16] The numeric base for the string.
71
71
  *
72
+ * The valid range is between `2` and `36`.
72
73
  * This defaults to `16` which is Hexadecimal.
73
74
  *
74
75
  * @param {boolean} [options.strict=false] Do not allow indivisible strings
package/package.json CHANGED
@@ -1,17 +1,13 @@
1
1
  {
2
2
  "name": "@lumjs/encode",
3
- "version": "1.1.0",
3
+ "version": "2.0.0",
4
4
  "main": "lib/index.js",
5
5
  "exports":
6
6
  {
7
7
  ".": "./lib/index.js",
8
8
  "./base64": "./lib/base64.js",
9
9
  "./base91": "./lib/base91.js",
10
- "./crypto": "./lib/crypto/index.js",
11
- "./crypto/load": "./lib/crypto/load.js",
12
10
  "./hash": "./lib/hash.js",
13
- "./safe64": "./lib/safe64/index.js",
14
- "./safe64/header": "./lib/safe64/header.js",
15
11
  "./util": "./lib/util.js",
16
12
  "./package.json": "./package.json"
17
13
  },
@@ -21,15 +17,13 @@
21
17
  "type": "git",
22
18
  "url": "https://github.com/supernovus/lum.encode.js.git"
23
19
  },
24
- "dependencies": {
25
- "@lumjs/core": "^1.7.1",
26
- "crypto-js": "^4.1.1",
27
- "php-serialize": "^4.0.2",
28
- "@shelacek/ubjson": "^1.1.1"
20
+ "dependencies":
21
+ {
22
+ "@lumjs/core": "^1.26.0"
29
23
  },
30
24
  "devDependencies":
31
25
  {
32
- "@lumjs/tests": "^1.7.0"
26
+ "@lumjs/tests": "^2.0.0"
33
27
  },
34
28
  "scripts":
35
29
  {
package/TODO.md DELETED
@@ -1,3 +0,0 @@
1
- # TODO
2
-
3
- - Nothing on the current list.