@ocfl/ocfl 0.3.0 → 1.0.0-beta.1

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.
Files changed (138) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +24 -9
  3. package/dist/constants.d.ts +15 -0
  4. package/dist/constants.d.ts.map +1 -0
  5. package/dist/constants.js +14 -0
  6. package/dist/constants.js.map +1 -0
  7. package/dist/digest.d.ts +58 -0
  8. package/dist/digest.d.ts.map +1 -0
  9. package/dist/digest.js +204 -0
  10. package/dist/digest.js.map +1 -0
  11. package/dist/error.d.ts +5 -0
  12. package/dist/error.d.ts.map +1 -0
  13. package/dist/error.js +9 -0
  14. package/dist/error.js.map +1 -0
  15. package/dist/extension.d.ts +83 -0
  16. package/dist/extension.d.ts.map +1 -0
  17. package/dist/extension.js +170 -0
  18. package/dist/extension.js.map +1 -0
  19. package/dist/extensions/0002-flat-direct-storage-layout.d.ts +12 -0
  20. package/dist/extensions/0002-flat-direct-storage-layout.d.ts.map +1 -0
  21. package/dist/extensions/0002-flat-direct-storage-layout.js +34 -0
  22. package/dist/extensions/0002-flat-direct-storage-layout.js.map +1 -0
  23. package/dist/extensions/0003-hash-and-id-n-tuple-storage-layout.d.ts +15 -0
  24. package/dist/extensions/0003-hash-and-id-n-tuple-storage-layout.d.ts.map +1 -0
  25. package/dist/extensions/0003-hash-and-id-n-tuple-storage-layout.js +49 -0
  26. package/dist/extensions/0003-hash-and-id-n-tuple-storage-layout.js.map +1 -0
  27. package/dist/extensions/0004-hashed-n-tuple-storage-layout.d.ts +17 -0
  28. package/dist/extensions/0004-hashed-n-tuple-storage-layout.d.ts.map +1 -0
  29. package/dist/extensions/0004-hashed-n-tuple-storage-layout.js +54 -0
  30. package/dist/extensions/0004-hashed-n-tuple-storage-layout.js.map +1 -0
  31. package/dist/extensions/0006-flat-omit-prefix-storage-layout.d.ts +6 -0
  32. package/dist/extensions/0006-flat-omit-prefix-storage-layout.d.ts.map +1 -0
  33. package/dist/extensions/0006-flat-omit-prefix-storage-layout.js +8 -0
  34. package/dist/extensions/0006-flat-omit-prefix-storage-layout.js.map +1 -0
  35. package/dist/extensions/0007-n-tuple-omit-prefix-storage-layout.d.ts +6 -0
  36. package/dist/extensions/0007-n-tuple-omit-prefix-storage-layout.d.ts.map +1 -0
  37. package/dist/extensions/0007-n-tuple-omit-prefix-storage-layout.js +8 -0
  38. package/dist/extensions/0007-n-tuple-omit-prefix-storage-layout.js.map +1 -0
  39. package/dist/extensions/0009-digest-algorithms.d.ts +9 -0
  40. package/dist/extensions/0009-digest-algorithms.d.ts.map +1 -0
  41. package/dist/extensions/0009-digest-algorithms.js +11 -0
  42. package/dist/extensions/0009-digest-algorithms.js.map +1 -0
  43. package/dist/extensions/000N-path-direct-storage-layout.d.ts +16 -0
  44. package/dist/extensions/000N-path-direct-storage-layout.d.ts.map +1 -0
  45. package/dist/extensions/000N-path-direct-storage-layout.js +46 -0
  46. package/dist/extensions/000N-path-direct-storage-layout.js.map +1 -0
  47. package/dist/extensions/index.d.ts +23 -0
  48. package/dist/extensions/index.d.ts.map +1 -0
  49. package/dist/extensions/index.js +8 -0
  50. package/dist/extensions/index.js.map +1 -0
  51. package/dist/file.d.ts +87 -0
  52. package/dist/file.d.ts.map +1 -0
  53. package/dist/file.js +118 -0
  54. package/dist/file.js.map +1 -0
  55. package/dist/index.d.ts +61 -0
  56. package/dist/index.d.ts.map +1 -0
  57. package/dist/index.js +73 -0
  58. package/dist/index.js.map +1 -0
  59. package/dist/inventory.d.ts +155 -0
  60. package/dist/inventory.d.ts.map +1 -0
  61. package/dist/inventory.js +591 -0
  62. package/dist/inventory.js.map +1 -0
  63. package/dist/object.d.ts +188 -0
  64. package/dist/object.d.ts.map +1 -0
  65. package/dist/object.js +425 -0
  66. package/dist/object.js.map +1 -0
  67. package/dist/storage.d.ts +94 -0
  68. package/dist/storage.d.ts.map +1 -0
  69. package/dist/storage.js +302 -0
  70. package/dist/storage.js.map +1 -0
  71. package/dist/store.d.ts +100 -0
  72. package/dist/store.d.ts.map +1 -0
  73. package/dist/store.js +157 -0
  74. package/dist/store.js.map +1 -0
  75. package/dist/transaction.d.ts +155 -0
  76. package/dist/transaction.d.ts.map +1 -0
  77. package/dist/transaction.js +395 -0
  78. package/dist/transaction.js.map +1 -0
  79. package/dist/types.d.ts +107 -0
  80. package/dist/types.d.ts.map +1 -0
  81. package/dist/types.js +2 -0
  82. package/dist/types.js.map +1 -0
  83. package/dist/utils.d.ts +24 -0
  84. package/dist/utils.d.ts.map +1 -0
  85. package/dist/utils.js +85 -0
  86. package/dist/utils.js.map +1 -0
  87. package/dist/validation.d.ts +32 -0
  88. package/dist/validation.d.ts.map +1 -0
  89. package/dist/validation.js +31 -0
  90. package/dist/validation.js.map +1 -0
  91. package/package.json +31 -50
  92. package/src/constants.ts +16 -0
  93. package/src/digest.ts +258 -0
  94. package/src/error.ts +9 -0
  95. package/src/extension.ts +206 -0
  96. package/{lib/extensions/0002-flat-direct-storage-layout.js → src/extensions/0002-flat-direct-storage-layout.ts} +15 -22
  97. package/src/extensions/0003-hash-and-id-n-tuple-storage-layout.ts +56 -0
  98. package/src/extensions/0004-hashed-n-tuple-storage-layout.ts +59 -0
  99. package/src/extensions/0006-flat-omit-prefix-storage-layout.ts +9 -0
  100. package/src/extensions/0007-n-tuple-omit-prefix-storage-layout.ts +9 -0
  101. package/src/extensions/0009-digest-algorithms.ts +12 -0
  102. package/src/extensions/000N-path-direct-storage-layout.ts +51 -0
  103. package/src/extensions/index.ts +24 -0
  104. package/src/file.ts +161 -0
  105. package/src/index.ts +92 -0
  106. package/src/inventory.ts +634 -0
  107. package/src/object.ts +473 -0
  108. package/src/storage.ts +342 -0
  109. package/src/store.ts +199 -0
  110. package/src/transaction.ts +456 -0
  111. package/{lib/types.d.ts → src/types.ts} +28 -74
  112. package/src/utils.ts +94 -0
  113. package/src/validation.ts +46 -0
  114. package/index.js +0 -129
  115. package/index.mjs +0 -18
  116. package/lib/constants.js +0 -24
  117. package/lib/digest.js +0 -307
  118. package/lib/enum.js +0 -90
  119. package/lib/error.js +0 -19
  120. package/lib/extension.js +0 -194
  121. package/lib/extensions/0003-hash-and-id-n-tuple-storage-layout.js +0 -60
  122. package/lib/extensions/0004-hashed-n-tuple-storage-layout.js +0 -63
  123. package/lib/extensions/0005-mutable-head.js +0 -0
  124. package/lib/extensions/0006-flat-omit-prefix-storage-layout.js +0 -7
  125. package/lib/extensions/0007-n-tuple-omit-prefix-storage-layout.js +0 -7
  126. package/lib/extensions/0008-schema-registry.js +0 -0
  127. package/lib/extensions/0009-digest-algorithms.js +0 -18
  128. package/lib/extensions/000N-path-direct-storage-layout.js +0 -61
  129. package/lib/extensions/index.js +0 -13
  130. package/lib/file.js +0 -129
  131. package/lib/index.js +0 -13
  132. package/lib/inventory.js +0 -564
  133. package/lib/object.js +0 -451
  134. package/lib/storage.js +0 -338
  135. package/lib/store.js +0 -191
  136. package/lib/transaction.js +0 -448
  137. package/lib/utils.js +0 -123
  138. package/lib/validation.js +0 -174
package/src/digest.ts ADDED
@@ -0,0 +1,258 @@
1
+ import crypto from 'node:crypto';
2
+ import zlib from 'node:zlib';
3
+ import type { CommonHasher, IDataType, MultiHasher } from './types.ts';
4
+
5
+ const CONTENT = ['sha256', 'sha512'] as const;
6
+ type ContentAlgorithm = (typeof CONTENT)[number];
7
+
8
+ /** OCFL algorithm name to node:crypto name */
9
+ const NATIVE = {
10
+ md5: 'md5',
11
+ sha1: 'sha1',
12
+ sha256: 'sha256',
13
+ sha512: 'sha512',
14
+ 'sha512/256': 'sha512-256',
15
+ 'blake2b-512': 'blake2b512',
16
+ };
17
+
18
+ type Digest = CommonHasher['digest'];
19
+ type MultiDigest = MultiHasher['digest'];
20
+ type WritableStreamHasher = WritableStream & { update(data: IDataType): void; digest: Digest };
21
+ type WritableStreamMultiHasher = WritableStream & { update(data: IDataType): void; digest: MultiDigest };
22
+ type StreamThroughHash = TransformStream & { digest: MultiDigest };
23
+ type StreamThroughOptions = { writableStrategy?: QueuingStrategy; readableStrategy?: QueuingStrategy };
24
+
25
+ function toBytes(data: IDataType): string | Uint8Array {
26
+ if (typeof data === 'string' || data instanceof Uint8Array) {
27
+ return data;
28
+ }
29
+ if (ArrayBuffer.isView(data)) {
30
+ return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
31
+ }
32
+ return new Uint8Array(data);
33
+ }
34
+
35
+ interface HashState {
36
+ update(data: string | Uint8Array): void;
37
+ digest(): Uint8Array;
38
+ hex(): string;
39
+ }
40
+
41
+ function hasher(digestSize: number, start: () => HashState): CommonHasher {
42
+ let state = start();
43
+ return {
44
+ digestSize,
45
+ init() {
46
+ state = start();
47
+ return this;
48
+ },
49
+ update(data) {
50
+ state.update(toBytes(data));
51
+ return this;
52
+ },
53
+ digest: ((encoding?: 'binary' | 'hex') => (encoding === 'binary' ? state.digest() : state.hex())) as Digest,
54
+ };
55
+ }
56
+
57
+ const hasherFactory: { [key: string]: () => Promise<CommonHasher> } = {};
58
+ const hasherSyncCache: { [key: string]: CommonHasher } = {};
59
+
60
+ /**
61
+ * Register a hash algorithm and it's factory function.
62
+ */
63
+ function registerHasherFactory(name: string, factory: () => Promise<CommonHasher>) {
64
+ if (typeof factory === 'function') {
65
+ hasherFactory[name] = factory;
66
+ }
67
+ }
68
+
69
+ function hasAlgorithm(name: string) {
70
+ return name in hasherFactory;
71
+ }
72
+
73
+ for (const [algo, nativeName] of Object.entries(NATIVE)) {
74
+ const digestSize = crypto.createHash(nativeName).digest().length;
75
+ registerHasherFactory(algo, async () =>
76
+ hasher(digestSize, () => {
77
+ const h = crypto.createHash(nativeName);
78
+ let result: Buffer | undefined;
79
+ const final = () => {
80
+ result ??= h.digest();
81
+ return result;
82
+ };
83
+ return { update: (d) => h.update(d), digest: () => new Uint8Array(final()), hex: () => final().toString('hex') };
84
+ }),
85
+ );
86
+ }
87
+
88
+ registerHasherFactory('crc32', async () =>
89
+ hasher(4, () => {
90
+ let crc = 0;
91
+ return {
92
+ update: (d) => {
93
+ crc = zlib.crc32(d, crc);
94
+ },
95
+ digest: () => {
96
+ const b = new Uint8Array(4);
97
+ new DataView(b.buffer).setUint32(0, crc);
98
+ return b;
99
+ },
100
+ hex: () => crc.toString(16).padStart(8, '0'),
101
+ };
102
+ }),
103
+ );
104
+
105
+ registerHasherFactory('size', async () =>
106
+ hasher(8, () => {
107
+ let size = 0;
108
+ return {
109
+ update: (d) => {
110
+ size += typeof d === 'string' ? Buffer.byteLength(d) : d.byteLength;
111
+ },
112
+ digest: () => new Uint8Array(new BigUint64Array([BigInt(size)]).buffer),
113
+ hex: () => `${size}`,
114
+ };
115
+ }),
116
+ );
117
+
118
+ /**
119
+ * Create a hasher instance for the given algorithm.
120
+ */
121
+ async function createHasher(algorithm: string): Promise<CommonHasher> {
122
+ const factory = hasherFactory[algorithm];
123
+ if (!factory) {
124
+ throw new Error(`Unsupported digest algorithm: ${algorithm}`);
125
+ }
126
+ return (await factory()).init();
127
+ }
128
+
129
+ /**
130
+ * Create a hasher instance for calculating multiple types of digest in one go.
131
+ */
132
+ async function createMultiHasher(algorithms: string | string[]): Promise<MultiHasher> {
133
+ const algoNames = Array.isArray(algorithms) ? (algorithms.length ? [...new Set(algorithms)] : ['sha512']) : [algorithms];
134
+ const hashers = await Promise.all(algoNames.map(async (name) => [name, await createHasher(name)] as const));
135
+ const multiHasher: MultiHasher = {
136
+ multi: true,
137
+ update(data) {
138
+ for (const [, hash] of hashers) {
139
+ hash.update(data);
140
+ }
141
+ return this;
142
+ },
143
+ digest: ((encoding?: 'binary' | 'hex') => {
144
+ const result: { [key: string]: string | Uint8Array } = {};
145
+ for (const [name, hash] of hashers) {
146
+ result[name] = hash.digest(encoding as 'hex');
147
+ }
148
+ return result;
149
+ }) as MultiDigest,
150
+ };
151
+ return multiHasher;
152
+ }
153
+
154
+ function createAnyHasher(algorithm: string | string[]): Promise<CommonHasher | MultiHasher> {
155
+ return Array.isArray(algorithm) ? createMultiHasher(algorithm) : createHasher(algorithm);
156
+ }
157
+
158
+ /**
159
+ * Create a hash instance that implements WritableStream.
160
+ * @param algorithm One or more digest algorithm names
161
+ */
162
+ async function createStream(algorithm?: string): Promise<WritableStreamHasher>;
163
+ async function createStream(algorithm: string[]): Promise<WritableStreamMultiHasher>;
164
+ async function createStream(algorithm: string | string[] = 'sha512'): Promise<WritableStreamHasher | WritableStreamMultiHasher> {
165
+ const hash = await createAnyHasher(algorithm);
166
+ const ws = new WritableStream({
167
+ write(chunk) {
168
+ hash.update(chunk);
169
+ },
170
+ }) as WritableStreamHasher & WritableStreamMultiHasher;
171
+ ws.update = (data) => {
172
+ hash.update(data);
173
+ };
174
+ ws.digest = ((encoding?: 'binary' | 'hex') => hash.digest(encoding as 'hex')) as Digest & MultiDigest;
175
+ return ws;
176
+ }
177
+
178
+ /**
179
+ * Create a pass through stream that pass data as it is and calculate the digest as data pass through.
180
+ */
181
+ async function createStreamThrough(algorithm: string | string[] = 'sha512', options?: StreamThroughOptions): Promise<StreamThroughHash> {
182
+ const hash = await createAnyHasher(algorithm);
183
+ const { writableStrategy, readableStrategy } = options || {};
184
+ const ts = new TransformStream(
185
+ {
186
+ transform(chunk, controller) {
187
+ controller.enqueue(chunk);
188
+ hash.update(chunk);
189
+ },
190
+ },
191
+ writableStrategy,
192
+ readableStrategy,
193
+ ) as StreamThroughHash;
194
+ ts.digest = ((encoding?: 'binary' | 'hex') => hash.digest(encoding as 'hex')) as MultiDigest;
195
+ return ts;
196
+ }
197
+
198
+ async function digest(algorithm: string[], input: IDataType | ReadableStream): Promise<{ [key: string]: string }>;
199
+ async function digest(algorithm: string, input: IDataType | ReadableStream): Promise<string>;
200
+ async function digest(algorithm: string | string[], input: IDataType | ReadableStream): Promise<string | { [key: string]: string }> {
201
+ if (input instanceof ReadableStream) {
202
+ const hash = await createStream(algorithm as string);
203
+ await input.pipeTo(hash);
204
+ return hash.digest();
205
+ }
206
+ const hash = await createAnyHasher(algorithm);
207
+ return hash.update(input).digest();
208
+ }
209
+
210
+ async function initSync(algorithm: string) {
211
+ if (!hasherSyncCache[algorithm]) {
212
+ const factory = hasherFactory[algorithm];
213
+ if (!factory) {
214
+ throw new Error(`Unsupported digest algorithm: ${algorithm}`);
215
+ }
216
+ hasherSyncCache[algorithm] = await factory();
217
+ }
218
+ }
219
+
220
+ /**
221
+ * For this sync function to work correctly, it requires a call to initSync() to be awaited for each algorithm before calling this function.
222
+ */
223
+ function digestSync(algorithm: string, input: IDataType): string {
224
+ const hasher = hasherSyncCache[algorithm];
225
+ if (!hasher) {
226
+ throw new Error(`Hasher for algorithm ${algorithm} is not initialized. Please call and await initSync('${algorithm}') before calling this function.`);
227
+ }
228
+ return hasher.init().update(input).digest();
229
+ }
230
+
231
+ const HEX_DIGEST_LENGTH: { [algorithm: string]: number } = {
232
+ md5: 32,
233
+ sha1: 40,
234
+ sha256: 64,
235
+ 'sha512/256': 64,
236
+ sha512: 128,
237
+ 'blake2b-512': 128,
238
+ crc32: 8,
239
+ size: 1,
240
+ };
241
+
242
+ function getHexDigestLength(algorithm: string) {
243
+ return HEX_DIGEST_LENGTH[algorithm] || 0;
244
+ }
245
+
246
+ const OcflDigest = {
247
+ CONTENT,
248
+ createStream,
249
+ createStreamThrough,
250
+ digest,
251
+ registerHasherFactory,
252
+ hasAlgorithm,
253
+ getHexDigestLength,
254
+ digestSync,
255
+ initSync,
256
+ };
257
+
258
+ export { type ContentAlgorithm, OcflDigest };
package/src/error.ts ADDED
@@ -0,0 +1,9 @@
1
+ class NotImplementedError extends Error {
2
+ constructor(methodName?: string) {
3
+ const msg = methodName ? `Method ${methodName} has not been implemented` : '';
4
+ super(msg);
5
+ this.name = 'NotImplementedError';
6
+ }
7
+ }
8
+
9
+ export { NotImplementedError };
@@ -0,0 +1,206 @@
1
+ import { NotImplementedError } from './error.ts';
2
+ import type { StorageLayout } from './extensions/index.ts';
3
+ import type { OcflExtensionConfig } from './types.ts';
4
+
5
+ /** Index by Registered extension name */
6
+ const extensionByName: { [name: string]: typeof OcflExtension } = {};
7
+ /** Index by extension class name */
8
+ const extensionByClassName: { [name: string]: typeof OcflExtension } = {};
9
+
10
+ const storageLayout: { [className: string]: typeof OcflStorageLayout } = {};
11
+
12
+ type ExtensionConstructor<T> = new (config?: OcflExtensionConfig) => T;
13
+
14
+ class OcflExtension<C extends object = object> {
15
+ static #object: { [name: string]: typeof OcflExtension } = {};
16
+ static #storage: { [name: string]: typeof OcflExtension } = {};
17
+
18
+ parameters: C;
19
+
20
+ /**
21
+ * Get an object-level extension
22
+ * @param name - A registered extension name
23
+ */
24
+ static object(name: string) {
25
+ return OcflExtension.#object[name];
26
+ }
27
+
28
+ /**
29
+ * Get an storage-level extension
30
+ * @param name - A registered extension name
31
+ */
32
+ static storage(name: string) {
33
+ return OcflExtension.#storage[name];
34
+ }
35
+
36
+ /** List all registered extension classes */
37
+ static get classes(): (typeof OcflExtension)[] {
38
+ return Object.values(extensionByName);
39
+ }
40
+
41
+ /**
42
+ * Find a registered extension with the specified name
43
+ * @param name - extension class name or registered extension name
44
+ * @returns the extension class
45
+ */
46
+ static class(name: string): typeof OcflExtension | undefined {
47
+ return extensionByName[name] || extensionByClassName[name];
48
+ }
49
+
50
+ /**
51
+ * Register an extension
52
+ */
53
+ // biome-ignore lint/complexity/noThisInStatic: defaults to the subclass register() is called on
54
+ static register(extensionClass: typeof OcflExtension = this) {
55
+ const name = extensionClass.NAME;
56
+ extensionByName[name] = extensionClass;
57
+ extensionByClassName[extensionClass.name] = extensionClass;
58
+ import('./index.ts').then((ocfl) => extensionClass.setup(ocfl));
59
+ }
60
+
61
+ /**
62
+ * Create an instance of the extension
63
+ */
64
+ static create<T extends OcflExtension>(this: ExtensionConstructor<T>, config?: OcflExtensionConfig): T {
65
+ const name = config?.extensionName;
66
+ // biome-ignore lint/complexity/noThisInStatic: must be the subclass create() is called on, or it recurses
67
+ if (!name || (this as unknown as typeof OcflExtension).NAME === name) {
68
+ return new this(config);
69
+ }
70
+ return createByName(OcflExtension, 'Extension', name, config) as T;
71
+ }
72
+
73
+ static get forObject() {
74
+ return false;
75
+ }
76
+ static get forStorage() {
77
+ return false;
78
+ }
79
+ static get NAME() {
80
+ return '0000-example-extension';
81
+ }
82
+ static get DESCRIPTION() {
83
+ return 'Example extension';
84
+ }
85
+ static async setup(_ocfl: typeof import('./index.ts')): Promise<void> {}
86
+
87
+ constructor(config?: OcflExtensionConfig, defaultConfig?: C) {
88
+ this.parameters = typeof defaultConfig === 'object' ? Object.create(defaultConfig) : {};
89
+ if (typeof config === 'object' && defaultConfig) {
90
+ for (const k in config) {
91
+ if (k in defaultConfig) {
92
+ (this.parameters as OcflExtensionConfig)[k] = config[k];
93
+ }
94
+ }
95
+ }
96
+ }
97
+
98
+ /** Return all the parameters in which the value has been set to non-default value */
99
+ get config(): OcflExtensionConfig | undefined {
100
+ const paramNames = Object.keys(this.parameters);
101
+ if (paramNames.length === 0) {
102
+ return undefined;
103
+ }
104
+ const params: OcflExtensionConfig = { extensionName: this.name };
105
+ for (const name of paramNames) {
106
+ params[name] = this.get(name);
107
+ }
108
+ return params;
109
+ }
110
+
111
+ get p() {
112
+ return this.parameters;
113
+ }
114
+
115
+ /** Extension registered name */
116
+ get name(): string {
117
+ return (this.constructor as typeof OcflExtension).NAME;
118
+ }
119
+ get description(): string {
120
+ return (this.constructor as typeof OcflExtension).DESCRIPTION;
121
+ }
122
+ get minOcflVersion() {
123
+ return '1.0';
124
+ }
125
+ get version() {
126
+ return '1.0';
127
+ }
128
+
129
+ /**
130
+ * Get the specified parameter value
131
+ * @param param - The extension parameter name
132
+ */
133
+ // biome-ignore lint/suspicious/noExplicitAny: parameter values are extension specific
134
+ get(param: string): any {
135
+ return (this.parameters as OcflExtensionConfig)[param];
136
+ }
137
+
138
+ /**
139
+ * Set the specified parameter value
140
+ * @param param - The extension parameter name
141
+ * @param value - The extension parameter value
142
+ */
143
+ set(param: string, value: unknown) {
144
+ if (param in this.parameters) {
145
+ (this.parameters as OcflExtensionConfig)[param] = value;
146
+ }
147
+ }
148
+ }
149
+
150
+ class OcflStorageLayout<C extends object = object> extends OcflExtension<C> {
151
+ static get layout(): StorageLayout {
152
+ return storageLayout as unknown as StorageLayout;
153
+ }
154
+
155
+ static class(name: string): typeof OcflStorageLayout | undefined {
156
+ const c = OcflExtension.class(name);
157
+ if (c && c.prototype instanceof OcflStorageLayout) {
158
+ return c as typeof OcflStorageLayout;
159
+ }
160
+ }
161
+
162
+ static get classes(): (typeof OcflStorageLayout)[] {
163
+ return Object.values(storageLayout);
164
+ }
165
+
166
+ /**
167
+ * Register a storage layout extension
168
+ */
169
+ // biome-ignore lint/complexity/noThisInStatic: defaults to the subclass register() is called on
170
+ static register(extensionClass: typeof OcflStorageLayout = this) {
171
+ OcflExtension.register(extensionClass);
172
+ storageLayout[extensionClass.name] = extensionClass;
173
+ }
174
+
175
+ static get forStorage() {
176
+ return true;
177
+ }
178
+
179
+ async validate() {}
180
+
181
+ /**
182
+ * Map an object identifier to a path
183
+ * @param id - The identifier of the OCFL Object
184
+ */
185
+ map(_id: string): string {
186
+ throw new NotImplementedError();
187
+ }
188
+ }
189
+
190
+ type ExtensionRegistry<T> = { class(name: string): { create(config?: OcflExtensionConfig): T } | undefined };
191
+
192
+ /** Create an instance of the extension registered as `name`, or throw if `registry` has none */
193
+ function createByName<T extends OcflExtension>(
194
+ registry: ExtensionRegistry<T>,
195
+ kind: 'Extension' | 'Layout',
196
+ name: string | undefined,
197
+ config?: OcflExtensionConfig,
198
+ ): T {
199
+ const cls = name ? registry.class(name) : undefined;
200
+ if (!cls) {
201
+ throw new Error(`${kind} "${name}" is not supported.`);
202
+ }
203
+ return cls.create(config);
204
+ }
205
+
206
+ export { createByName, OcflExtension, OcflStorageLayout };
@@ -1,35 +1,28 @@
1
- const path = require("path");
2
- const { OcflStorageLayout } = require("../extension");
3
- const { EXTENSIONS_DIR } = require("../constants").OcflConstants;
1
+ import path from 'node:path';
2
+ import { OcflConstants } from '../constants.ts';
3
+ import { OcflStorageLayout } from '../extension.ts';
4
4
 
5
- /** @type {FlatDirectStorageLayout} */
6
- var instance;
5
+ const { EXTENSIONS_DIR } = OcflConstants;
6
+
7
+ let instance: FlatDirectStorageLayout;
7
8
 
8
9
  /**
9
10
  * OCFL object identifiers are mapped directly to directory names that are direct children of the OCFL storage root directory.
10
11
  */
11
- // @ts-ignore
12
12
  class FlatDirectStorageLayout extends OcflStorageLayout {
13
- static get NAME() { return '0002-flat-direct-storage-layout' }
13
+ static get NAME() {
14
+ return '0002-flat-direct-storage-layout';
15
+ }
14
16
  static get DESCRIPTION() {
15
- return "OCFL object identifiers are mapped directly to directory names" +
16
- " that are direct children of the OCFL storage root.";
17
+ return 'OCFL object identifiers are mapped directly to directory names' + ' that are direct children of the OCFL storage root.';
17
18
  }
18
19
 
19
- static create() {
20
- if (!instance) instance = new FlatDirectStorageLayout();
21
- return instance;
22
- }
23
-
24
- constructor() {
25
- super()
20
+ static create<T>(): T {
21
+ instance ??= new FlatDirectStorageLayout();
22
+ return instance as T;
26
23
  }
27
24
 
28
- /**
29
- * @param {string} id
30
- * @return {string}
31
- */
32
- map(id) {
25
+ map(id: string): string {
33
26
  if (path.basename(id) !== id) {
34
27
  throw new Error(`The object id <${id}> is incompatible with layout extension ${this.name} because it contains the path separator character.`);
35
28
  } else if (id === EXTENSIONS_DIR) {
@@ -43,4 +36,4 @@ class FlatDirectStorageLayout extends OcflStorageLayout {
43
36
  }
44
37
  }
45
38
 
46
- module.exports = { FlatDirectStorageLayout };
39
+ export { FlatDirectStorageLayout };
@@ -0,0 +1,56 @@
1
+ import path from 'node:path';
2
+ import { OcflDigest } from '../digest.ts';
3
+ import { OcflStorageLayout } from '../extension.ts';
4
+
5
+ function encodeIdentifier(id: string) {
6
+ return encodeURIComponent(id).toLowerCase().replace(/\./g, '%2e');
7
+ }
8
+
9
+ const DefaultConfig = {
10
+ extensionName: '0003-hash-and-id-n-tuple-storage-layout',
11
+ digestAlgorithm: 'sha256',
12
+ tupleSize: 3,
13
+ numberOfTuples: 3,
14
+ };
15
+
16
+ class HashAndIdNTupleStorageLayout extends OcflStorageLayout<typeof DefaultConfig> {
17
+ static get NAME() {
18
+ return DefaultConfig.extensionName;
19
+ }
20
+ static get DESCRIPTION() {
21
+ return (
22
+ 'OCFL object identifiers are hashed and encoded as lowercase hex strings.' +
23
+ ' These digests are then divided into N n-tuple segments,' +
24
+ ' which are used to create nested paths under the OCFL storage root.' +
25
+ ' Finally, the OCFL object identifier is percent-encoded to create a directory name for the OCFL object root.'
26
+ );
27
+ }
28
+
29
+ constructor(config?: Partial<typeof DefaultConfig>) {
30
+ super(config, DefaultConfig);
31
+ const c = this.parameters;
32
+ const digestLength = OcflDigest.getHexDigestLength(c.digestAlgorithm);
33
+ if (!digestLength) {
34
+ throw new Error('Invalid digestAlgorithm');
35
+ }
36
+ const p = c.numberOfTuples * c.tupleSize;
37
+ if (p > digestLength) {
38
+ throw new Error('Product of numberOfTuples and tupleSize is greater than the number of characters in the hex encoded digest.');
39
+ }
40
+ }
41
+
42
+ map(id: string): string {
43
+ const digest = OcflDigest.digestSync(this.parameters.digestAlgorithm, id);
44
+ const segments = [];
45
+ const s = this.parameters.tupleSize;
46
+ const n = this.parameters.numberOfTuples;
47
+ let i: number;
48
+ for (i = 0; i < n; ++i) {
49
+ segments.push(digest.slice(s * i, s * i + s));
50
+ }
51
+ segments.push(encodeIdentifier(id));
52
+ return segments.join(path.sep);
53
+ }
54
+ }
55
+
56
+ export { HashAndIdNTupleStorageLayout };
@@ -0,0 +1,59 @@
1
+ import path from 'node:path';
2
+ import { OcflDigest } from '../digest.ts';
3
+ import { OcflStorageLayout } from '../extension.ts';
4
+
5
+ const DefaultConfig = {
6
+ extensionName: '0004-hashed-n-tuple-storage-layout',
7
+ digestAlgorithm: 'sha256',
8
+ tupleSize: 3,
9
+ numberOfTuples: 3,
10
+ shortObjectRoot: false,
11
+ };
12
+ export type HashedNTupleStorageLayoutConfig = Partial<typeof DefaultConfig>;
13
+
14
+ class HashedNTupleStorageLayout extends OcflStorageLayout<typeof DefaultConfig> {
15
+ static get NAME() {
16
+ return DefaultConfig.extensionName;
17
+ }
18
+ static get DESCRIPTION() {
19
+ return (
20
+ 'OCFL object identifiers are hashed and encoded as lowercase hex strings.' +
21
+ ' These digests are then divided into N n-tuple segments,' +
22
+ ' which are used to create nested paths under the OCFL storage root.'
23
+ );
24
+ }
25
+
26
+ constructor(config?: HashedNTupleStorageLayoutConfig) {
27
+ super(config, DefaultConfig);
28
+ const c = this.parameters;
29
+ const digestLength = OcflDigest.getHexDigestLength(c.digestAlgorithm);
30
+ if (!digestLength) {
31
+ throw new Error('Invalid digestAlgorithm');
32
+ }
33
+ const p = c.numberOfTuples * c.tupleSize;
34
+ if (p > digestLength) {
35
+ throw new Error('Product of numberOfTuples and tupleSize is greater than the number of characters in the hex encoded digest.');
36
+ } else if (p === digestLength && c.shortObjectRoot) {
37
+ throw new Error('shortObjectRoot cannot be set to true');
38
+ }
39
+ }
40
+
41
+ map(id: string): string {
42
+ const digest = OcflDigest.digestSync(this.parameters.digestAlgorithm, id);
43
+ const segments = [];
44
+ const s = this.parameters.tupleSize;
45
+ const n = this.parameters.numberOfTuples;
46
+ let i: number;
47
+ for (i = 0; i < n; ++i) {
48
+ segments.push(digest.slice(s * i, s * i + s));
49
+ }
50
+ if (this.parameters.shortObjectRoot) {
51
+ segments.push(digest.slice(s * i));
52
+ } else {
53
+ segments.push(digest);
54
+ }
55
+ return segments.join(path.sep);
56
+ }
57
+ }
58
+
59
+ export { HashedNTupleStorageLayout };
@@ -0,0 +1,9 @@
1
+ import { OcflStorageLayout } from '../extension.ts';
2
+
3
+ class FlatOmitPrefixStorageLayout extends OcflStorageLayout {
4
+ static get NAME() {
5
+ return '0006-flat-omit-prefix-storage-layout';
6
+ }
7
+ }
8
+
9
+ export { FlatOmitPrefixStorageLayout };
@@ -0,0 +1,9 @@
1
+ import { OcflStorageLayout } from '../extension.ts';
2
+
3
+ class NTupleOmitPrefixStorageLayout extends OcflStorageLayout {
4
+ static get NAME() {
5
+ return '0007-n-tuple-omit-prefix-storage-layout';
6
+ }
7
+ }
8
+
9
+ export { NTupleOmitPrefixStorageLayout };
@@ -0,0 +1,12 @@
1
+ import { OcflExtension } from '../extension.ts';
2
+
3
+ /**
4
+ * Notes: this implementation does nothing because the 0009 extension is implemented directly in the OcflDigest class.
5
+ */
6
+ class DigestAlgorithm extends OcflExtension {
7
+ static get NAME() {
8
+ return '0009-digest-algorithms';
9
+ }
10
+ }
11
+
12
+ export { DigestAlgorithm };