@push.rocks/smartdb 5.0.5 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,6 +8,7 @@ import {
8
8
  StorageMigrator,
9
9
  } from '../../ts_migration/index.js';
10
10
  import { validateSmartDbManagementOperationOptions } from '../offline-inspection.js';
11
+ import { assertNoLocalSmartDbStorageRootRelocationWorkspace } from '../storage-root-relocation.js';
11
12
  import type {
12
13
  IOpLogEntry,
13
14
  IOpLogResult,
@@ -226,6 +227,8 @@ export class SmartdbServer {
226
227
  checkpoint();
227
228
  // Run storage migration for file-based storage before starting Rust engine
228
229
  if (this.options.storage === 'file' && this.options.storagePath) {
230
+ await assertNoLocalSmartDbStorageRootRelocationWorkspace(this.options.storagePath);
231
+ checkpoint();
229
232
  const migrator = new StorageMigrator(this.options.storagePath);
230
233
  await migrator.run(startupAbortController.signal);
231
234
  checkpoint();
@@ -0,0 +1,277 @@
1
+ import * as plugins from './plugins.js';
2
+
3
+ export const localSmartDbStorageRootRelocationErrorCodes = [
4
+ 'INVALID_REQUEST',
5
+ 'UNSUPPORTED',
6
+ 'BUSY',
7
+ 'NOT_FOUND',
8
+ 'DESTINATION_CONFLICT',
9
+ 'IDENTITY_MISMATCH',
10
+ 'STATE_CORRUPT',
11
+ 'IO',
12
+ 'RECOVERY_REQUIRED',
13
+ ] as const;
14
+
15
+ export type TLocalSmartDbStorageRootRelocationErrorCode =
16
+ (typeof localSmartDbStorageRootRelocationErrorCodes)[number];
17
+
18
+ export interface ILocalSmartDbStoppedStorageRootRelocationInput {
19
+ sourceFolderPath: string;
20
+ destinationFolderPath: string;
21
+ relocationId: string;
22
+ }
23
+
24
+ export interface ILocalSmartDbStoppedStorageRootRelocationReceipt {
25
+ format: 'smartdb.storage-root-relocation.receipt.v1';
26
+ version: 1;
27
+ relocationId: string;
28
+ sourceFolderPath: string;
29
+ destinationFolderPath: string;
30
+ providerRootId: string;
31
+ storageRootDevice: string;
32
+ storageRootInode: string;
33
+ receiptSha256: string;
34
+ sourceReceiptRetained: true;
35
+ }
36
+
37
+ export class LocalSmartDbStorageRootRelocationError extends Error {
38
+ public readonly code: TLocalSmartDbStorageRootRelocationErrorCode;
39
+
40
+ public constructor(codeArg: TLocalSmartDbStorageRootRelocationErrorCode) {
41
+ super(`SmartDB stopped storage-root relocation failed with code ${codeArg}`);
42
+ this.name = 'LocalSmartDbStorageRootRelocationError';
43
+ this.code = codeArg;
44
+ }
45
+ }
46
+
47
+ const relocationInputKeys = [
48
+ 'sourceFolderPath',
49
+ 'destinationFolderPath',
50
+ 'relocationId',
51
+ ] as const;
52
+ const relocationReceiptKeys = [
53
+ 'format',
54
+ 'version',
55
+ 'relocationId',
56
+ 'sourceFolderPath',
57
+ 'destinationFolderPath',
58
+ 'providerRootId',
59
+ 'storageRootDevice',
60
+ 'storageRootInode',
61
+ 'receiptSha256',
62
+ 'sourceReceiptRetained',
63
+ ] as const;
64
+ const maximumFolderPathBytes = 4095;
65
+ const maximumRelocationIdBytes = 256;
66
+ const maximumU64Decimal = '18446744073709551615';
67
+ const receiptDigestDomain = 'smartdb.storage-root-relocation.receipt.v1\0';
68
+
69
+ const relocationError = (
70
+ codeArg: TLocalSmartDbStorageRootRelocationErrorCode,
71
+ ): LocalSmartDbStorageRootRelocationError =>
72
+ new LocalSmartDbStorageRootRelocationError(codeArg);
73
+
74
+ const isPlainExactDataObject = (
75
+ valueArg: unknown,
76
+ keysArg: readonly string[],
77
+ ): valueArg is Record<string, unknown> => {
78
+ if (
79
+ typeof valueArg !== 'object'
80
+ || valueArg === null
81
+ || Array.isArray(valueArg)
82
+ || Object.getPrototypeOf(valueArg) !== Object.prototype
83
+ ) {
84
+ return false;
85
+ }
86
+ const ownKeys = Reflect.ownKeys(valueArg);
87
+ if (
88
+ ownKeys.length !== keysArg.length
89
+ || ownKeys.some((keyArg) => typeof keyArg !== 'string' || !keysArg.includes(keyArg))
90
+ ) {
91
+ return false;
92
+ }
93
+ const descriptors = Object.getOwnPropertyDescriptors(valueArg);
94
+ return keysArg.every((keyArg) => {
95
+ const descriptor = descriptors[keyArg];
96
+ return descriptor !== undefined
97
+ && descriptor.enumerable
98
+ && Object.prototype.hasOwnProperty.call(descriptor, 'value');
99
+ });
100
+ };
101
+
102
+ const isValidUnicodeScalarString = (valueArg: string): boolean => {
103
+ for (let index = 0; index < valueArg.length; index++) {
104
+ const code = valueArg.charCodeAt(index);
105
+ if (code >= 0xd800 && code <= 0xdbff) {
106
+ const next = valueArg.charCodeAt(index + 1);
107
+ if (next < 0xdc00 || next > 0xdfff) return false;
108
+ index++;
109
+ } else if (code >= 0xdc00 && code <= 0xdfff) {
110
+ return false;
111
+ }
112
+ }
113
+ return true;
114
+ };
115
+
116
+ const isNestedPath = (parentArg: string, childArg: string): boolean => {
117
+ const relative = plugins.path.relative(parentArg, childArg);
118
+ return relative !== ''
119
+ && relative !== '..'
120
+ && !relative.startsWith(`..${plugins.path.sep}`)
121
+ && !plugins.path.isAbsolute(relative);
122
+ };
123
+
124
+ const normalizeFolderPath = (valueArg: unknown): string => {
125
+ if (
126
+ typeof valueArg !== 'string'
127
+ || !plugins.path.isAbsolute(valueArg)
128
+ || !isValidUnicodeScalarString(valueArg)
129
+ || [...valueArg].some(characterArg => /\p{Cc}/u.test(characterArg))
130
+ ) {
131
+ throw relocationError('INVALID_REQUEST');
132
+ }
133
+ const initialBytes = plugins.buffer.Buffer.byteLength(valueArg, 'utf8');
134
+ if (initialBytes < 1 || initialBytes > maximumFolderPathBytes) {
135
+ throw relocationError('INVALID_REQUEST');
136
+ }
137
+ const normalized = plugins.path.resolve(valueArg);
138
+ const normalizedBytes = plugins.buffer.Buffer.byteLength(normalized, 'utf8');
139
+ if (
140
+ normalizedBytes < 1
141
+ || normalizedBytes > maximumFolderPathBytes
142
+ || plugins.path.parse(normalized).root === normalized
143
+ || !isValidUnicodeScalarString(normalized)
144
+ || [...normalized].some(characterArg => /\p{Cc}/u.test(characterArg))
145
+ ) {
146
+ throw relocationError('INVALID_REQUEST');
147
+ }
148
+ return normalized;
149
+ };
150
+
151
+ export const normalizeLocalSmartDbStoppedStorageRootRelocationInput = (
152
+ inputArg: ILocalSmartDbStoppedStorageRootRelocationInput,
153
+ ): ILocalSmartDbStoppedStorageRootRelocationInput => {
154
+ if (!isPlainExactDataObject(inputArg, relocationInputKeys)) {
155
+ throw relocationError('INVALID_REQUEST');
156
+ }
157
+ const sourceFolderPath = normalizeFolderPath(inputArg.sourceFolderPath);
158
+ const destinationFolderPath = normalizeFolderPath(inputArg.destinationFolderPath);
159
+ const sourceFolderPathBytes = plugins.buffer.Buffer.byteLength(sourceFolderPath, 'utf8');
160
+ const destinationFolderPathBytes = plugins.buffer.Buffer.byteLength(destinationFolderPath, 'utf8');
161
+ if (
162
+ sourceFolderPath === destinationFolderPath
163
+ || isNestedPath(sourceFolderPath, destinationFolderPath)
164
+ || isNestedPath(destinationFolderPath, sourceFolderPath)
165
+ || destinationFolderPathBytes > sourceFolderPathBytes
166
+ ) {
167
+ throw relocationError('INVALID_REQUEST');
168
+ }
169
+ if (
170
+ typeof inputArg.relocationId !== 'string'
171
+ || plugins.buffer.Buffer.byteLength(inputArg.relocationId, 'utf8') > maximumRelocationIdBytes
172
+ || !/^[A-Za-z0-9][A-Za-z0-9._:@-]*$/.test(inputArg.relocationId)
173
+ ) {
174
+ throw relocationError('INVALID_REQUEST');
175
+ }
176
+ return {
177
+ sourceFolderPath,
178
+ destinationFolderPath,
179
+ relocationId: inputArg.relocationId,
180
+ };
181
+ };
182
+
183
+ const receiptDigest = (
184
+ receiptArg: Omit<ILocalSmartDbStoppedStorageRootRelocationReceipt, 'receiptSha256'>,
185
+ ): string => plugins.crypto
186
+ .createHash('sha256')
187
+ .update(receiptDigestDomain, 'utf8')
188
+ .update(JSON.stringify(receiptArg), 'utf8')
189
+ .digest('hex');
190
+
191
+ const isCanonicalU64Decimal = (valueArg: unknown): valueArg is string =>
192
+ typeof valueArg === 'string'
193
+ && /^(?:0|[1-9][0-9]{0,19})$/.test(valueArg)
194
+ && (valueArg.length < 20 || valueArg <= maximumU64Decimal);
195
+
196
+ const isLowercaseSha256 = (valueArg: unknown): valueArg is string =>
197
+ typeof valueArg === 'string' && /^[0-9a-f]{64}$/.test(valueArg);
198
+
199
+ export const normalizeLocalSmartDbStoppedStorageRootRelocationReceipt = (
200
+ valueArg: unknown,
201
+ inputArg: ILocalSmartDbStoppedStorageRootRelocationInput,
202
+ ): ILocalSmartDbStoppedStorageRootRelocationReceipt => {
203
+ if (!isPlainExactDataObject(valueArg, relocationReceiptKeys)) {
204
+ throw relocationError('RECOVERY_REQUIRED');
205
+ }
206
+ const receipt = valueArg as unknown as ILocalSmartDbStoppedStorageRootRelocationReceipt;
207
+ if (
208
+ receipt.format !== 'smartdb.storage-root-relocation.receipt.v1'
209
+ || receipt.version !== 1
210
+ || receipt.relocationId !== inputArg.relocationId
211
+ || receipt.sourceFolderPath !== inputArg.sourceFolderPath
212
+ || receipt.destinationFolderPath !== inputArg.destinationFolderPath
213
+ || !isLowercaseSha256(receipt.providerRootId)
214
+ || !isCanonicalU64Decimal(receipt.storageRootDevice)
215
+ || !isCanonicalU64Decimal(receipt.storageRootInode)
216
+ || !isLowercaseSha256(receipt.receiptSha256)
217
+ || receipt.sourceReceiptRetained !== true
218
+ ) {
219
+ throw relocationError('RECOVERY_REQUIRED');
220
+ }
221
+ const withoutDigest = {
222
+ format: receipt.format,
223
+ version: receipt.version,
224
+ relocationId: receipt.relocationId,
225
+ sourceFolderPath: receipt.sourceFolderPath,
226
+ destinationFolderPath: receipt.destinationFolderPath,
227
+ providerRootId: receipt.providerRootId,
228
+ storageRootDevice: receipt.storageRootDevice,
229
+ storageRootInode: receipt.storageRootInode,
230
+ sourceReceiptRetained: receipt.sourceReceiptRetained,
231
+ } satisfies Omit<ILocalSmartDbStoppedStorageRootRelocationReceipt, 'receiptSha256'>;
232
+ if (receiptDigest(withoutDigest) !== receipt.receiptSha256) {
233
+ throw relocationError('RECOVERY_REQUIRED');
234
+ }
235
+ return { ...withoutDigest, receiptSha256: receipt.receiptSha256 };
236
+ };
237
+
238
+ export const assertNoLocalSmartDbStorageRootRelocationWorkspace = async (
239
+ storagePathArg: string,
240
+ ): Promise<void> => {
241
+ const storagePath = plugins.path.resolve(storagePathArg);
242
+ const internalPath = plugins.path.join(storagePath, '.__rustdb_internal');
243
+ const workspacePath = plugins.path.join(internalPath, 'storage-root-relocation');
244
+ try {
245
+ const storage = await plugins.fs.stat(storagePath);
246
+ if (!storage.isDirectory()) {
247
+ throw new Error('not a directory');
248
+ }
249
+ } catch (error) {
250
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return;
251
+ throw new Error('SmartDB storage-root relocation workspace inspection is ambiguous');
252
+ }
253
+ try {
254
+ const internal = await plugins.fs.lstat(internalPath);
255
+ if (internal.isSymbolicLink() || !internal.isDirectory()) {
256
+ throw new Error('not a safe directory');
257
+ }
258
+ } catch (error) {
259
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return;
260
+ throw new Error('SmartDB storage-root relocation workspace inspection is ambiguous');
261
+ }
262
+ try {
263
+ await plugins.fs.lstat(workspacePath);
264
+ } catch (error) {
265
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return;
266
+ throw new Error('SmartDB storage-root relocation workspace inspection is ambiguous');
267
+ }
268
+ throw new Error('SmartDB storage-root relocation recovery must complete before startup');
269
+ };
270
+
271
+ export const isLocalSmartDbStorageRootRelocationErrorCode = (
272
+ valueArg: unknown,
273
+ ): valueArg is TLocalSmartDbStorageRootRelocationErrorCode =>
274
+ typeof valueArg === 'string'
275
+ && localSmartDbStorageRootRelocationErrorCodes.includes(
276
+ valueArg as TLocalSmartDbStorageRootRelocationErrorCode,
277
+ );