@ocfl/ocfl 0.2.0 → 0.3.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,138 +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
144
+ * Create a hasher instance for calculating multiple types of digest in one go.
145
+ * @param {string|string[]} algorithms
68
146
  */
69
- function createStreamThrough(algorithm, options) {
70
- const hash = algorithms[algorithm || 'sha512']();
71
- hash.setEncoding('hex');
72
- 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
+ });
73
167
  }
74
168
 
75
169
  /**
76
- *
77
- * @param {string} algorithm
78
- * @param {Buffer | string | Array<Buffer | string>} input
79
- * @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>}
80
174
  */
81
- function digestSync(algorithm, input) {
82
- const hash = algorithms[algorithm || 'sha512']();
83
- let inputs = Array.isArray(input) ? input : [input];
84
- for (let chunk of inputs) {
85
- 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);
86
194
  }
87
- return hash.digest('hex');
195
+ return ws;
88
196
  }
89
197
 
198
+ /** @typedef {TransformStream & { digest: IMultiDigest }} StreamThroughHash */
90
199
  /**
91
- *
92
- * @param {string} algorithm
93
- * @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>}
94
204
  */
95
- async function digestAsync(algorithm, input) {
96
- 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;
97
221
  }
98
222
 
99
223
  /**
100
224
  *
225
+ * @overload
226
+ * @param {string[]} algorithm
227
+ * @param {IDataType | ReadableStream} input
228
+ * @return {Promise<{[key:string]: string}>}
229
+ */
230
+ /**
231
+ * @overload
101
232
  * @param {string} algorithm
102
- * @param {NodeJS.ReadableStream} input
233
+ * @param {IDataType | ReadableStream} input
234
+ * @return {Promise<string>}
103
235
  */
104
- async function digestFromStream(algorithm, input) {
105
- let hash = algorithms[algorithm]();
106
- await stream.pipeline(input, hash);
107
- 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
+ }
108
255
  }
109
256
 
110
257
  /**
111
- *
258
+ * For this sync function to work correctly, it requires a call to initSync() to be awaited for each algorithm before calling this function.
112
259
  * @param {string} algorithm
113
- * @param {string} filePath
260
+ * @param {IDataType} input
114
261
  */
115
- async function digestFromFile(algorithm, filePath) {
116
- 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;
117
284
  }
118
285
 
119
286
  const OcflDigest = {
120
287
  CONTENT,
121
288
  FIXITY,
122
- algorithms,
123
- createStream, createStreamThrough,
124
- digestAsync, digestSync,
125
- digestFromStream, digestFromFile
289
+ // algorithms,
290
+ createStream, createStreamThrough, digest,
291
+ registerHasherFactory, hasAlgorithm, getHexDigestLength,
292
+ digestSync, initSync
126
293
  };
127
294
 
128
295
  module.exports = {
129
- OcflDigest
296
+ OcflDigest,
297
+ [testSymbol]: {
298
+ hasherCache
299
+ }
130
300
  // DIGEST,
131
301
  // DIGEST_FIXITY,
132
302
  // digestSync,
133
303
  // digestAsync,
134
304
  // digestFromStream,
135
- // digestFromFile,
136
305
  // digestStream,
137
306
  // HashThrough
138
307
  };