@ocfl/ocfl 0.2.2 → 0.4.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/index.js CHANGED
@@ -3,22 +3,11 @@
3
3
  */
4
4
 
5
5
  const { OcflConstants, OcflDigest, OcflExtension, OcflStorageLayout, OcflStore,
6
- OcflObject, OcflObjectImpl,
7
- OcflObjectInventory, OcflObjectTransactionImpl,
6
+ OcflObject, OcflObjectInventory, OcflObjectTransactionImpl,
8
7
  OcflStorage, OcflStorageImpl,
9
- createObjectProxy, createTransactionProxy,
10
8
  extensions
11
9
  } = require('./lib/index.js');
12
10
 
13
- // const { FlatDirectStorageLayout,
14
- // FlatOmitPrefixStorageLayout,
15
- // HashAndIdNTupleStorageLayout,
16
- // HashedNTupleStorageLayout,
17
- // NTupleOmitPrefixStorageLayout } = extensions;
18
-
19
- /** @typedef {import('./lib/object.js').OcflObjectConfig} OcflObjectConfig */
20
- /** @typedef {import('./lib/storage.js').OcflStorageConfig} OcflStorageConfig */
21
-
22
11
  /**
23
12
  * @typedef {Object} StorageLayout
24
13
  * @property {typeof extensions.FlatDirectStorageLayout} FlatDirectStorageLayout
@@ -28,7 +17,10 @@ const { OcflConstants, OcflDigest, OcflExtension, OcflStorageLayout, OcflStore,
28
17
  * @property {typeof extensions.NTupleOmitPrefixStorageLayout} NTupleOmitPrefixStorageLayout
29
18
  * @property {typeof extensions.PathDirectStorageLayout} PathDirectStorageLayout
30
19
  */
31
- /** @typedef {import('./lib/extension.js').OcflExtensionConfig} OcflExtensionConfig */
20
+
21
+ /** @typedef {import('./lib/types').OcflExtensionConfig} OcflExtensionConfig */
22
+ /** @typedef {import('./lib/types').OcflObjectConfig} OcflObjectConfig */
23
+ /** @typedef {import('./lib/types').OcflStorageConfig} OcflStorageConfig */
32
24
 
33
25
  /**
34
26
  * Create a storage layout from extension name or config
@@ -46,106 +38,89 @@ function layout(config) {
46
38
  }
47
39
  let storageLayout = Object.assign(layout, /** @type {StorageLayout} */(OcflStorageLayout.layout))
48
40
 
49
-
50
41
  /**
51
- * Top level OCFL API interface that provides storage and object creation.
52
- * Any backend store implementation module must return the instance of this class.
42
+ * Creates a new OCFL core API with a specific backend store.
43
+ * Any backend store implementation module must return the result of this method.
44
+ * @param {typeof OcflStore} store
45
+ * @param {SO} defaultOptions
53
46
  * @template SO
47
+ * @returns Top level OCFL API interface that provides storage and object creation.
54
48
  */
55
- class Ocfl {
56
- #store;
57
- #defaultOptions;
58
-
59
- /**
60
- * @param {typeof OcflStore} store
61
- * @param {SO} defaultOptions
62
- */
63
- constructor(store, defaultOptions) {
64
- this.#store = store;
65
- this.#defaultOptions = defaultOptions;
66
- }
67
-
68
- get OCFL_VERSIONS() { return OcflConstants.OCFL_VERSIONS; }
69
-
70
- /**
71
- * Built-in storage layout classes
72
- * @return {StorageLayout}
73
- */
74
- get StorageLayout() { return OcflStorageLayout.layout }
75
-
76
- /**
77
- * Instantiate a storage layout from extension name or config
78
- * @param {string|OcflExtensionConfig} config - Layout name or config
79
- */
80
- storageLayout(config) {
81
- let name, c;
82
- if (typeof config === 'string') {
83
- name = config;
84
- } else {
85
- name = config.extensionName;
86
- c = config;
49
+ function implementOcfl(store, defaultOptions) {
50
+ return {
51
+ OCFL_VERSIONS: OcflConstants.OCFL_VERSIONS,
52
+
53
+ /** Built-in storage layout classes */
54
+ StorageLayout: OcflStorageLayout.layout,
55
+
56
+ /**
57
+ * Instantiate a storage layout from extension name or config
58
+ * @param {string | OcflExtensionConfig} config - Layout name or config
59
+ */
60
+ storageLayout(config) {
61
+ let name, c;
62
+ if (typeof config === 'string') {
63
+ name = config;
64
+ } else {
65
+ name = config.extensionName;
66
+ c = config;
67
+ }
68
+ let cls = OcflStorageLayout.class(name)
69
+ if (!cls) throw new Error(`Layout "${name}" is not supported.`);
70
+ return cls.create(c);
71
+ },
72
+
73
+ /**
74
+ * Instantiate an OCFL Object
75
+ * @param {OcflObjectConfig} config
76
+ * @param {SO} [storeOptions]
77
+ */
78
+ object(config, storeOptions = defaultOptions) {
79
+ return new OcflObject(config, store.getInstance(storeOptions));
80
+ },
81
+
82
+ /**
83
+ * Instantiate an OCFL Storage root
84
+ * @param {OcflStorageConfig} config
85
+ * @param {SO} [storeOptions]
86
+ * @return {OcflStorage}
87
+ */
88
+ storage(config, storeOptions = defaultOptions) {
89
+ return new OcflStorageImpl(config, store.getInstance(storeOptions));
90
+ },
91
+
92
+ /**
93
+ * Create a new OCFL Storage
94
+ * @param {OcflStorageConfig} config
95
+ * @param {SO} [storeOptions]
96
+ * @return {Promise<OcflStorage>}
97
+ */
98
+ async createStorage(config, storeOptions = defaultOptions) {
99
+ return (new OcflStorageImpl(config, store.getInstance(storeOptions))).create();
100
+ },
101
+
102
+ /**
103
+ * Load an existing OCFL Storage
104
+ * @param {OcflStorageConfig} config
105
+ * @param {SO} [storeOptions]
106
+ * @return {Promise<OcflStorage>}
107
+ */
108
+ async loadStorage(config, storeOptions = defaultOptions) {
109
+ return (new OcflStorageImpl(config, store.getInstance(storeOptions))).load();
87
110
  }
88
- let cls = OcflStorageLayout.class(name)
89
- if (!cls) throw new Error(`Layout "${name}" is not supported.`);
90
- return cls.create(c);
91
- }
92
-
93
- /**
94
- * Instantiate an OCFL Object object
95
- * @param {OcflObjectConfig} config
96
- * @param {SO} [storeOptions]
97
- * @return {OcflObject}
98
- */
99
- object(config, storeOptions = this.#defaultOptions) {
100
- return new OcflObjectImpl(config, this.#store.getInstance(storeOptions));
101
- }
102
-
103
- /**
104
- * Instantiate an OCFL Storage object
105
- * @param {OcflStorageConfig} config
106
- * @param {SO} [storeOptions]
107
- * @return {OcflStorage}
108
- */
109
- storage(config, storeOptions = this.#defaultOptions) {
110
- return new OcflStorageImpl(config, this.#store.getInstance(storeOptions));
111
- }
112
-
113
- /**
114
- * Create a new OCFL Storage
115
- * @param {OcflStorageConfig} config
116
- * @param {SO} [storeOptions]
117
- * @return {Promise<OcflStorage>}
118
- */
119
- async createStorage(config, storeOptions = this.#defaultOptions) {
120
- return (new OcflStorageImpl(config, this.#store.getInstance(storeOptions))).create();
121
- }
122
-
123
- /**
124
- * Load an existing OCFL Storage
125
- * @param {OcflStorageConfig} config
126
- * @param {SO} [storeOptions]
127
- * @return {Promise<OcflStorage>}
128
- */
129
- async loadStorage(config, storeOptions = this.#defaultOptions) {
130
- return (new OcflStorageImpl(config, this.#store.getInstance(storeOptions))).load();
131
111
  }
132
112
  }
133
113
 
134
-
135
-
136
114
  module.exports = {
137
115
  OcflConstants,
138
116
  OcflDigest,
139
117
  OcflExtension, OcflStorageLayout,
140
118
  OcflStore,
141
119
  OcflObject,
142
- OcflObjectImpl,
143
120
  OcflObjectInventory,
144
121
  OcflObjectTransactionImpl,
145
- createObjectProxy,
146
- createTransactionProxy,
147
122
  OcflStorage, OcflStorageImpl,
148
- Ocfl,
123
+ implementOcfl,
149
124
  extensions
150
125
  };
151
126
 
package/index.mjs CHANGED
@@ -6,12 +6,12 @@
6
6
  //export * from './lib/constants.js';
7
7
 
8
8
  export * from './index.js';
9
- import ocfl from './index.js';
9
+ import * as ocfl from './index.js';
10
10
  export default ocfl;
11
11
  //export const { DIGEST, DIGEST_FIXITY, INVENTORY_NAME, NAMASTE_PREFIX_OBJECT, NAMASTE_PREFIX_STORAGE, OCFL_VERSION } = ocfl
12
- // export const { OcflObject, OcflObjectImpl, OcflObjectInventory, OcflObjectTransaction, OcflObjectTransactionImpl,
12
+ // export const { OcflObject, OcflObjectInventory, OcflObjectTransaction, OcflObjectTransactionImpl,
13
13
  // OcflStorage, OcflStorageImpl,
14
- // createProxy, digestFrom, digestFromFile, digestFromStream,
14
+ // createProxy, digestFrom, digestFromStream,
15
15
  // DIGEST, DIGEST_FIXITY, INVENTORY_NAME, NAMASTE_PREFIX_OBJECT, NAMASTE_PREFIX_STORAGE, OCFL_VERSION } = ocfl;
16
16
  //export const {} = ocfl;
17
17
  //export { default as ocfl } from './index.js';
package/lib/digest.js CHANGED
@@ -1,139 +1,307 @@
1
1
  //@ts-check
2
2
 
3
- //const hasha = require("hasha");
4
- const crypto = require("crypto");
5
- const fs = require("fs");
6
- const stream = require("stream/promises");
3
+ const hashWasm = require('hash-wasm');
4
+ const { sha512_256 } = require('js-sha512'); // fallback for sha512/256 algorithm, which is not included in hash-wasm
7
5
  //const worker = require('node:worker_threads');
8
- const { Enum } = require('./enum.js');
9
- const { Transform } = require('stream');
6
+ const { enumeration } = require('./enum.js');
7
+ const { testSymbol } = require('./utils.js');
8
+
9
+ const CONTENT = enumeration(['sha256', 'sha512']);
10
+ const FIXITY = enumeration(['sha256', 'sha512', 'md5', 'sha1', 'blake2b-512', 'blake2b-160', 'blake2b-256', 'blake2b-384', 'sha512/256', 'size', 'crc32']);
10
11
 
11
12
  /**
13
+ * @typedef {((outputType: "binary") => Uint8Array) & ((outputType?: "hex") => string)} IDigest
14
+ * @typedef {((outputType: "binary") => {[key:string]: Uint8Array}) & ((outputType?: "hex") => {[key:string]: string})} IMultiDigest
15
+ * @typedef {WritableStream & { update(data: IDataType): void, digest: IDigest }} WritableStreamHasher
16
+ * @typedef {WritableStream & { update(data: IDataType): void, digest: IMultiDigest }} WritableStreamMultiHasher
12
17
  */
13
- class CONTENT extends Enum {
14
- static sha256 = new this('sha256');
15
- static sha512 = new this('sha512');
16
- };
17
18
 
18
- class FIXITY extends Enum {
19
- static sha256 = new this('sha256');
20
- static sha512 = new this('sha512');
21
- static md5 = new this('md5');
22
- static sha1 = new this('sha1');
23
- static blake2b512 = new this('blake2b-512');
24
- };
19
+ /**
20
+ * @type { Object.<string, CommonHasher[]> }
21
+ */
22
+ const hasherCache = {};
23
+ /**
24
+ * @type { Object.<string, CommonHasher> }
25
+ */
26
+ const hasherSyncCache = {};
27
+ // for (let algo of FIXITY) {
28
+ // algorithms[algo] = function () { return crypto.createHash(algo); }
29
+ // }
30
+
31
+ /** @type { WeakMap<CommonHasher, boolean>} */
32
+ const hasherPending = new WeakMap();
33
+
34
+ /** @type { {[key: string]: () => Promise<CommonHasher>} } */
35
+ const hasherFactory = {};
25
36
 
26
37
  /**
27
- * A pass through stream that pass data as it is and calculate the digest as data pass through.
38
+ * Register a hash algorithm and it's factory function.
39
+ * @param {string} name
40
+ * @param {() => Promise<CommonHasher>} factory
28
41
  */
29
- class HashThrough extends Transform {
30
- constructor(hash, streamOptions) {
31
- super(streamOptions);
32
- this.hash = hash;
42
+ function registerHasherFactory(name, factory) {
43
+ if (typeof factory === 'function') {
44
+ hasherFactory[name] = factory;
33
45
  }
34
- _transform(chunk, encoding, cb) {
35
- try {
36
- this.hash.update(chunk, encoding);
37
- cb(null, chunk);
38
- } catch (err) {
39
- cb(err)
46
+ }
47
+
48
+ function hasAlgorithm(name) {
49
+ return name in hasherFactory;
50
+ }
51
+
52
+ (function init() {
53
+ for (const algo of FIXITY) {
54
+ if (algo.startsWith('blake2b')) {
55
+ const bits = parseInt(algo.split('-')[1]);
56
+ registerHasherFactory(algo, async () => hashWasm.createBLAKE2b(bits));
57
+ } else if (algo === 'sha512/256') {
58
+ registerHasherFactory(algo, async () => {
59
+ let h = sha512_256.create();
60
+ /** @type {CommonHasher} */
61
+ const hasher = {
62
+ digestSize: 32,
63
+ init() { h = sha512_256.create(); return this; },
64
+ update(data) { h.update(/** @type {any} */(data)); return this },
65
+ // @ts-ignore
66
+ digest(encoding) {
67
+ if (encoding === 'binary') return new Uint8Array(h.digest());
68
+ else return h.hex();
69
+ }
70
+ };
71
+ return hasher;
72
+ });
73
+ } else if (algo === 'size') {
74
+ registerHasherFactory(algo, async () => {
75
+ let encoder;
76
+ let size = 0;
77
+ /** @type {CommonHasher} */
78
+ const hasher = {
79
+ digestSize: 1,
80
+ init() {
81
+ size = 0;
82
+ encoder = new TextEncoder();
83
+ return this;
84
+ },
85
+ update(data) {
86
+ if (data) {
87
+ if (typeof data === 'string') size += encoder.encode(data).length;
88
+ else size += data.byteLength;
89
+ }
90
+ return this;
91
+ },
92
+ // @ts-ignore
93
+ digest(encoding) {
94
+ if (encoding === 'binary') return new Uint8Array(new BigUint64Array([BigInt(size)]).buffer);
95
+ else return '' + size;
96
+ }
97
+ };
98
+ return hasher;
99
+ });
100
+ } else {
101
+ registerHasherFactory(algo, hashWasm['create' + algo.toUpperCase()]);
40
102
  }
41
103
  }
42
- digest(encoding) {
43
- return this.hash.digest(encoding || 'hex');
44
- }
45
- }
104
+ })();
105
+ // (async () => {
106
+ // for (const name in hasherFactory) {
107
+ // const factory = hasherFactory[name];
108
+ // const h = await factory();
109
+ // console.log(name);
110
+ // console.log(h);
111
+ // console.log(h.init().update('test').digest().length)
112
+ // }
113
+ // })();
46
114
 
47
115
  /**
48
- * @type { Object.<string, function():crypto.Hash>}
116
+ *
117
+ * @param {*} hasher
118
+ * @returns {hasher is MultiHasher}
49
119
  */
50
- let algorithms = {};
51
- for (let algo of FIXITY) {
52
- algorithms[algo.value] = function () { return crypto.createHash(algo.name); }
120
+ function isMulti(hasher) {
121
+ return hasher.multi;
53
122
  }
54
123
 
55
124
  /**
56
- *
125
+ * Create a hasher instance for the given algorithm or get it from cache if available.
57
126
  * @param {string} algorithm
58
127
  */
59
- function createStream(algorithm) {
60
- const hash = algorithms[algorithm || 'sha512']();
61
- //hash.setEncoding('hex');
128
+ async function createHasher(algorithm) {
129
+ const cacheArray = hasherCache[algorithm];
130
+ let hash = (cacheArray ?? []).find(hasher => !hasherPending.get(hasher));
131
+ if (!hash) {
132
+ const factory = hasherFactory[algorithm];
133
+ if (!factory) throw new Error(`Unsupported digest algorithm: ${algorithm}`);
134
+ hash = await factory();
135
+ if (cacheArray) cacheArray.push(hash);
136
+ else hasherCache[algorithm] = [hash];
137
+ }
138
+ hasherPending.set(hash, true);
139
+ hash.init();
62
140
  return hash;
63
141
  }
64
142
 
65
143
  /**
66
- *
67
- * @param {string} [algorithm]
68
- * @param {object} [options]
144
+ * Create a hasher instance for calculating multiple types of digest in one go.
145
+ * @param {string|string[]} algorithms
69
146
  */
70
- function createStreamThrough(algorithm = 'sha512', options) {
71
- const hash = algorithms[algorithm]();
72
- //hash.setEncoding('hex');
73
- return new HashThrough(hash, options);
147
+ async function createMultiHasher(algorithms) {
148
+ const algoNames = Array.isArray(algorithms) ? (algorithms.length ? [...(new Set(algorithms))] : ['sha512']) : [algorithms];
149
+ const hashers = await Promise.all(algoNames.map(createHasher));
150
+ return /** @type { MultiHasher } */({
151
+ multi: true,
152
+ update(data) {
153
+ for (const hash of hashers) {
154
+ hash.update(data);
155
+ }
156
+ return this;
157
+ },
158
+ digest(encoding) {
159
+ const result = {};
160
+ for (let i = 0; i < hashers.length; i++) {
161
+ hasherPending.set(hashers[i], false);
162
+ result[algoNames[i]] = hashers[i].digest(encoding);
163
+ }
164
+ return result;
165
+ }
166
+ });
74
167
  }
75
168
 
76
169
  /**
77
- *
78
- * @param {string} algorithm
79
- * @param {Buffer | string | Array<Buffer | string>} input
80
- * @return {string}
170
+ * Create a hash instance that implements WritableStream.
171
+ * @overload
172
+ * @param {string} algorithm One or more digest algorithm names
173
+ * @return {Promise<WritableStreamHasher>}
81
174
  */
82
- function digestSync(algorithm, input) {
83
- const hash = algorithms[algorithm || 'sha512']();
84
- let inputs = Array.isArray(input) ? input : [input];
85
- for (let chunk of inputs) {
86
- hash.update(chunk);
175
+ /**
176
+ * @overload
177
+ * @param {string[]} algorithm One or more digest algorithm names
178
+ * @return {Promise<WritableStreamMultiHasher>}
179
+ */
180
+ async function createStream(algorithm = 'sha512') {
181
+ const hash = await (Array.isArray(algorithm) ? createMultiHasher(algorithm) : createHasher(algorithm));
182
+ /** @type {any} */
183
+ const ws = new WritableStream({
184
+ write(chunk) {
185
+ hash.update(chunk);
186
+ }
187
+ });
188
+ ws.update = function (data) {
189
+ hash.update(data);
190
+ };
191
+ ws.digest = function (encoding) {
192
+ if (!isMulti(hash)) hasherPending.set(hash, false);
193
+ return hash.digest(encoding);
87
194
  }
88
- return hash.digest('hex');
195
+ return ws;
89
196
  }
90
197
 
198
+ /** @typedef {TransformStream & { digest: IMultiDigest }} StreamThroughHash */
91
199
  /**
92
- *
93
- * @param {string} algorithm
94
- * @param {Buffer | string | Array<Buffer | string>} input
200
+ * Create a pass through stream that pass data as it is and calculate the digest as data pass through.
201
+ * @param {string|string[]} [algorithm]
202
+ * @param {object} [options]
203
+ * @return {Promise<StreamThroughHash>}
95
204
  */
96
- async function digestAsync(algorithm, input) {
97
- return digestSync(algorithm, input);
205
+ async function createStreamThrough(algorithm = 'sha512', options) {
206
+ const hash = await (Array.isArray(algorithm) ? createMultiHasher(algorithm) : createHasher(algorithm));
207
+ const { writableStrategy, readableStrategy } = options || {};
208
+ //hash.setEncoding('hex');
209
+ /** @type {any} */
210
+ const ts = new TransformStream({
211
+ transform(chunk, controller) {
212
+ controller.enqueue(chunk);
213
+ hash.update(chunk);
214
+ }
215
+ }, writableStrategy, readableStrategy);
216
+ ts.digest = function (encoding) {
217
+ if (!isMulti(hash)) hasherPending.set(hash, false);
218
+ return hash.digest(encoding);
219
+ }
220
+ return ts;
98
221
  }
99
222
 
100
223
  /**
101
224
  *
225
+ * @overload
226
+ * @param {string[]} algorithm
227
+ * @param {IDataType | ReadableStream} input
228
+ * @return {Promise<{[key:string]: string}>}
229
+ */
230
+ /**
231
+ * @overload
102
232
  * @param {string} algorithm
103
- * @param {NodeJS.ReadableStream} input
233
+ * @param {IDataType | ReadableStream} input
234
+ * @return {Promise<string>}
104
235
  */
105
- async function digestFromStream(algorithm, input) {
106
- let hash = algorithms[algorithm]();
107
- await stream.pipeline(input, hash);
108
- return hash.digest('hex');
236
+ async function digest(algorithm, input) {
237
+ if (input instanceof ReadableStream) {
238
+ const hash = await createStream(algorithm);
239
+ await input.pipeTo(hash);
240
+ return hash.digest();
241
+ } else {
242
+ const hash = await (Array.isArray(algorithm) ? createMultiHasher(algorithm) : createHasher(algorithm));
243
+ if (!isMulti(hash)) hasherPending.set(hash, false);
244
+ return hash.update(input).digest();
245
+ }
246
+ }
247
+
248
+ async function initSync(algorithm) {
249
+ if (!hasherSyncCache[algorithm]) {
250
+ const factory = hasherFactory[algorithm];
251
+ if (!factory) throw new Error(`Unsupported digest algorithm: ${algorithm}`);
252
+ const hasher = await factory();
253
+ hasherSyncCache[algorithm] = hasher;
254
+ }
109
255
  }
110
256
 
111
257
  /**
112
- *
258
+ * For this sync function to work correctly, it requires a call to initSync() to be awaited for each algorithm before calling this function.
113
259
  * @param {string} algorithm
114
- * @param {string} filePath
260
+ * @param {IDataType} input
115
261
  */
116
- async function digestFromFile(algorithm, filePath) {
117
- return await digestFromStream(algorithm, fs.createReadStream(filePath));
262
+ function digestSync(algorithm, input) {
263
+ const hasher = hasherSyncCache[algorithm];
264
+ if (!hasher) throw new Error(`Hasher for algorithm ${algorithm} is not initialized. Please call and await initSync('${algorithm}') before calling this function.`);
265
+ return hasher.init().update(input).digest();
266
+ }
267
+
268
+ const HEX_DIGEST_LENGTH = {
269
+ 'md5': 32,
270
+ 'sha1': 40,
271
+ 'sha256': 64,
272
+ 'sha512/256': 64,
273
+ 'sha512': 128,
274
+ 'blake2b-160': 40,
275
+ 'blake2b-256': 64,
276
+ 'blake2b-384': 96,
277
+ 'blake2b-512': 128,
278
+ 'crc32': 8,
279
+ 'size': 1
280
+ };
281
+
282
+ function getHexDigestLength(algorithm) {
283
+ return HEX_DIGEST_LENGTH[algorithm] || 0;
118
284
  }
119
285
 
120
286
  const OcflDigest = {
121
287
  CONTENT,
122
288
  FIXITY,
123
- algorithms,
124
- createStream, createStreamThrough,
125
- digestAsync, digestSync,
126
- digestFromStream, digestFromFile
289
+ // algorithms,
290
+ createStream, createStreamThrough, digest,
291
+ registerHasherFactory, hasAlgorithm, getHexDigestLength,
292
+ digestSync, initSync
127
293
  };
128
294
 
129
295
  module.exports = {
130
- OcflDigest
296
+ OcflDigest,
297
+ [testSymbol]: {
298
+ hasherCache
299
+ }
131
300
  // DIGEST,
132
301
  // DIGEST_FIXITY,
133
302
  // digestSync,
134
303
  // digestAsync,
135
304
  // digestFromStream,
136
- // digestFromFile,
137
305
  // digestStream,
138
306
  // HashThrough
139
307
  };