@zenfs/core 2.4.2 → 2.4.4
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/COPYING.md +14 -8
- package/dist/backends/backend.d.ts +1 -1
- package/dist/backends/single_buffer.d.ts +1 -1
- package/dist/backends/single_buffer.js +26 -23
- package/dist/context.d.ts +1 -1
- package/dist/context.js +10 -19
- package/dist/internal/contexts.d.ts +24 -4
- package/dist/internal/contexts.js +40 -0
- package/dist/internal/inode.js +3 -3
- package/dist/node/sync.js +1 -1
- package/dist/path.js +3 -3
- package/dist/utils.d.ts +3 -1
- package/dist/utils.js +2 -1
- package/dist/vfs/acl.js +3 -3
- package/dist/vfs/async.js +4 -3
- package/dist/vfs/file.js +4 -4
- package/dist/vfs/shared.js +5 -4
- package/dist/vfs/sync.js +4 -3
- package/dist/vfs/watchers.d.ts +1 -1
- package/dist/vfs/watchers.js +8 -5
- package/package.json +1 -1
- package/tests/backend/single-buffer.test.ts +48 -1
- package/tests/common/context.test.ts +2 -4
- package/tests/fs/links.test.ts +1 -2
- package/tests/fs/read.test.ts +1 -2
package/COPYING.md
CHANGED
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
_This document is supplemental to the license._
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Feel free to reach out if you have any concerns regarding licensing. You can create a new discussion [here](https://github.com/orgs/zen-fs/discussions/new?category=q-a).
|
|
4
|
+
|
|
5
|
+
Regarding section 4(b) of version 3 of the LGPL, you are permitted to link to a copy of the LGPL and this document rather than including the full text of both.
|
|
6
|
+
|
|
7
|
+
#### Exception for web applications
|
|
5
8
|
|
|
6
|
-
|
|
9
|
+
You are permitted to convey the Combined Work without meeting the requirements of section 4(d) of version 3 of the LGPL if
|
|
7
10
|
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
|
|
11
|
+
- a) the Combined Work is only accessed by users from other computers over a network (i.e. a web application or website), and
|
|
12
|
+
- b) you also provide the [Corresponding Source](https://www.gnu.org/licenses/gpl-3.0.html#:~:text=“Corresponding%20Source”) of The Library or an offer thereof.
|
|
13
|
+
|
|
14
|
+
This exception does not excuse or waive any other requirement of the LGPL.
|
|
15
|
+
|
|
16
|
+
The practical goal of this exception is to allow ZenFS to be used in modern web apps. For the avoidance of doubt, you are permitted to bundle and minify ZenFS in a web application. Linking to ZenFS' GitHub or npm page is sufficient to satisfy the above section (b) provided you do not modify ZenFS.
|
|
11
17
|
|
|
12
18
|
For example, in a list of libraries you could have:
|
|
13
19
|
|
|
14
|
-
> - [ZenFS](https://github.com/zen-fs/core), LGPL-3.0-
|
|
20
|
+
> - [ZenFS](https://github.com/zen-fs/core), Licensed under the [LGPL 3.0 or later](https://www.gnu.org/licenses/lgpl-3.0.html) and [COPYING.md](https://github.com/zen-fs/core/blob/main/COPYING.md), Copyright © James Prevett and other ZenFS contributors
|
|
15
21
|
|
|
16
|
-
|
|
22
|
+
**This is a very easy requirement, please respect it.**
|
|
17
23
|
|
|
18
24
|
Before v2.4.0, ZenFS was licensed under the MIT. This still means you need to include the copyright notice and license.
|
|
@@ -9,7 +9,7 @@ type OptionType = 'string' | 'number' | 'bigint' | 'boolean' | 'symbol' | 'undef
|
|
|
9
9
|
* @category Backends and Configuration
|
|
10
10
|
*/
|
|
11
11
|
export type OptionsConfig<T> = {
|
|
12
|
-
[K in keyof T]: {
|
|
12
|
+
[K in Exclude<keyof T, keyof SharedConfig>]: {
|
|
13
13
|
/**
|
|
14
14
|
* The type of the option. Can be a:
|
|
15
15
|
* - string given by `typeof`
|
|
@@ -26,6 +26,7 @@ declare const MetadataBlock_base: import("memium/decorators").StructFromTypedArr
|
|
|
26
26
|
*/
|
|
27
27
|
export declare class MetadataBlock extends MetadataBlock_base<ArrayBufferLike> {
|
|
28
28
|
readonly ['constructor']: typeof MetadataBlock;
|
|
29
|
+
private static readonly lockIndex;
|
|
29
30
|
/**
|
|
30
31
|
* The crc32c checksum for the metadata block.
|
|
31
32
|
* @privateRemarks Keep this first!
|
|
@@ -37,7 +38,6 @@ export declare class MetadataBlock extends MetadataBlock_base<ArrayBufferLike> {
|
|
|
37
38
|
accessor previous_offset: number;
|
|
38
39
|
protected _previous?: MetadataBlock;
|
|
39
40
|
get previous(): MetadataBlock | undefined;
|
|
40
|
-
protected get offsetHex(): string;
|
|
41
41
|
/** Metadata entries. */
|
|
42
42
|
accessor items: ArrayOf<MetadataEntry>;
|
|
43
43
|
toString(long?: boolean): string;
|
|
@@ -86,7 +86,7 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
|
|
|
86
86
|
});
|
|
87
87
|
// SPDX-License-Identifier: LGPL-3.0-or-later
|
|
88
88
|
import { withErrno } from 'kerium';
|
|
89
|
-
import { alert, crit, err, warn } from 'kerium/log';
|
|
89
|
+
import { alert, crit, debug, err, warn } from 'kerium/log';
|
|
90
90
|
import { array, offsetof, sizeof } from 'memium';
|
|
91
91
|
import { $from, field, struct, types as t } from 'memium/decorators';
|
|
92
92
|
import { BufferView } from 'utilium/buffer.js';
|
|
@@ -95,6 +95,7 @@ import { decodeUUID, encodeUUID } from 'utilium/string.js';
|
|
|
95
95
|
import { _inode_version, Inode } from '../internal/inode.js';
|
|
96
96
|
import { StoreFS } from './store/fs.js';
|
|
97
97
|
import { SyncMapTransaction } from './store/map.js';
|
|
98
|
+
const hex = (value) => '0x' + value.toString(16).padStart(8, '0');
|
|
98
99
|
// eslint-disable-next-line @typescript-eslint/unbound-method
|
|
99
100
|
const { format } = new Intl.NumberFormat('en-US', {
|
|
100
101
|
notation: 'compact',
|
|
@@ -155,7 +156,7 @@ let MetadataEntry = (() => {
|
|
|
155
156
|
get size() { return this.#size_accessor_storage; }
|
|
156
157
|
set size(value) { this.#size_accessor_storage = value; }
|
|
157
158
|
toString() {
|
|
158
|
-
return `<MetadataEntry @
|
|
159
|
+
return `<MetadataEntry @ ${hex(this.byteOffset)}>`;
|
|
159
160
|
}
|
|
160
161
|
constructor() {
|
|
161
162
|
super(...arguments);
|
|
@@ -216,8 +217,8 @@ let MetadataBlock = (() => {
|
|
|
216
217
|
__esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
|
|
217
218
|
MetadataBlock = _classThis = _classDescriptor.value;
|
|
218
219
|
if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
219
|
-
__runInitializers(_classThis, _classExtraInitializers);
|
|
220
220
|
}
|
|
221
|
+
static lockIndex = offsetof(MetadataBlock, 'locked') / Int32Array.BYTES_PER_ELEMENT;
|
|
221
222
|
#checksum_accessor_storage = __runInitializers(this, _checksum_initializers, void 0);
|
|
222
223
|
/**
|
|
223
224
|
* The crc32c checksum for the metadata block.
|
|
@@ -240,27 +241,24 @@ let MetadataBlock = (() => {
|
|
|
240
241
|
this._previous ??= new MetadataBlock(this.buffer, this.previous_offset);
|
|
241
242
|
return this._previous;
|
|
242
243
|
}
|
|
243
|
-
get offsetHex() {
|
|
244
|
-
return '0x' + this.byteOffset.toString(16).padStart(8, '0');
|
|
245
|
-
}
|
|
246
244
|
#items_accessor_storage = __runInitializers(this, _items_initializers, void 0);
|
|
247
245
|
/** Metadata entries. */
|
|
248
246
|
get items() { return this.#items_accessor_storage; }
|
|
249
247
|
set items(value) { this.#items_accessor_storage = value; }
|
|
250
248
|
toString(long = false) {
|
|
251
249
|
if (!long)
|
|
252
|
-
return `<MetadataBlock @ ${this.
|
|
250
|
+
return `<MetadataBlock @ ${hex(this.byteOffset)}>`;
|
|
253
251
|
let text = [
|
|
254
|
-
`---- Metadata block at ${this.
|
|
255
|
-
`Checksum:
|
|
252
|
+
`---- Metadata block at ${hex(this.byteOffset)} ----`,
|
|
253
|
+
`Checksum: ${hex(this.checksum)}`,
|
|
256
254
|
`Last updated: ${new Date(Number(this.timestamp)).toLocaleString()}`,
|
|
257
|
-
`Previous block:
|
|
255
|
+
`Previous block: ${hex(this.previous_offset)}`,
|
|
258
256
|
'Entries:',
|
|
259
257
|
].join('\n');
|
|
260
258
|
for (const entry of this.items) {
|
|
261
259
|
if (!entry.offset)
|
|
262
260
|
continue;
|
|
263
|
-
text += `\n\
|
|
261
|
+
text += `\n\t${hex(entry.id)}: ${format(entry.size).padStart(5)} at ${hex(entry.offset)}`;
|
|
264
262
|
}
|
|
265
263
|
return text;
|
|
266
264
|
}
|
|
@@ -276,28 +274,26 @@ let MetadataBlock = (() => {
|
|
|
276
274
|
*/
|
|
277
275
|
waitUnlocked(depth = 0) {
|
|
278
276
|
if (depth > max_lock_attempts)
|
|
279
|
-
throw crit(withErrno('EBUSY', `sbfs: exceeded max attempts waiting for metadata block at ${this.
|
|
280
|
-
|
|
281
|
-
if (!Atomics.load(this, i))
|
|
277
|
+
throw crit(withErrno('EBUSY', `sbfs: exceeded max attempts waiting for metadata block at ${hex(this.byteOffset)} to be unlocked`));
|
|
278
|
+
if (!Atomics.load(this, MetadataBlock.lockIndex))
|
|
282
279
|
return;
|
|
283
|
-
switch (Atomics.wait(this,
|
|
280
|
+
switch (Atomics.wait(this, MetadataBlock.lockIndex, 1)) {
|
|
284
281
|
case 'ok':
|
|
285
282
|
break;
|
|
286
283
|
case 'not-equal':
|
|
287
284
|
depth++;
|
|
288
|
-
err(`sbfs: waiting for metadata block at ${this.
|
|
285
|
+
err(`sbfs: waiting for metadata block at ${hex(this.byteOffset)} to be unlocked (${depth}/${max_lock_attempts})`);
|
|
289
286
|
return this.waitUnlocked(depth);
|
|
290
287
|
case 'timed-out':
|
|
291
|
-
throw crit(withErrno('EBUSY', `sbfs: timed out waiting for metadata block at ${this.
|
|
288
|
+
throw crit(withErrno('EBUSY', `sbfs: timed out waiting for metadata block at ${hex(this.byteOffset)} to be unlocked`));
|
|
292
289
|
}
|
|
293
290
|
}
|
|
294
291
|
lock() {
|
|
295
292
|
this.waitUnlocked();
|
|
296
|
-
|
|
297
|
-
Atomics.store(this, i, 1);
|
|
293
|
+
Atomics.store(this, MetadataBlock.lockIndex, 1);
|
|
298
294
|
const release = () => {
|
|
299
|
-
Atomics.store(this,
|
|
300
|
-
Atomics.notify(this,
|
|
295
|
+
Atomics.store(this, MetadataBlock.lockIndex, 0);
|
|
296
|
+
Atomics.notify(this, MetadataBlock.lockIndex, 1);
|
|
301
297
|
};
|
|
302
298
|
release[Symbol.dispose] = release;
|
|
303
299
|
return release;
|
|
@@ -306,6 +302,9 @@ let MetadataBlock = (() => {
|
|
|
306
302
|
super(...arguments);
|
|
307
303
|
__runInitializers(this, _locked_extraInitializers);
|
|
308
304
|
}
|
|
305
|
+
static {
|
|
306
|
+
__runInitializers(_classThis, _classExtraInitializers);
|
|
307
|
+
}
|
|
309
308
|
};
|
|
310
309
|
return MetadataBlock = _classThis;
|
|
311
310
|
})();
|
|
@@ -425,7 +424,7 @@ let SuperBlock = (() => {
|
|
|
425
424
|
throw crit(withErrno('EIO', 'sbfs: checksum mismatch for super block'));
|
|
426
425
|
this.metadata = new MetadataBlock(this.buffer, this.metadata_offset);
|
|
427
426
|
if (this.metadata.checksum !== checksum(this.metadata))
|
|
428
|
-
throw crit(withErrno('EIO', `sbfs: checksum mismatch for metadata block (saved ${this.metadata.checksum
|
|
427
|
+
throw crit(withErrno('EIO', `sbfs: checksum mismatch for metadata block (saved ${hex(this.metadata.checksum)}, computed ${hex(checksum(this.metadata))})`));
|
|
429
428
|
if (this.inode_format != _inode_version)
|
|
430
429
|
throw crit(withErrno('EIO', 'sbfs: inode format mismatch'));
|
|
431
430
|
if (this.metadata_block_size != sizeof(MetadataBlock))
|
|
@@ -506,6 +505,7 @@ let SuperBlock = (() => {
|
|
|
506
505
|
this.metadata_offset = metadata.byteOffset;
|
|
507
506
|
_update(metadata);
|
|
508
507
|
_update(this);
|
|
508
|
+
debug(`sbfs: rotated metadata block at ${hex(metadata.previous_offset)} with new block at ${hex(offset)}`);
|
|
509
509
|
return metadata;
|
|
510
510
|
}
|
|
511
511
|
/**
|
|
@@ -541,7 +541,10 @@ export { SuperBlock };
|
|
|
541
541
|
* Note we don't include the checksum when computing a new one.
|
|
542
542
|
*/
|
|
543
543
|
function checksum(value) {
|
|
544
|
-
|
|
544
|
+
let length = sizeof(value) - 4;
|
|
545
|
+
if (value instanceof MetadataBlock)
|
|
546
|
+
length -= Int32Array.BYTES_PER_ELEMENT;
|
|
547
|
+
return crc32c(new Uint8Array(value.buffer, value.byteOffset + 4, length));
|
|
545
548
|
}
|
|
546
549
|
/**
|
|
547
550
|
* Update a block's checksum and timestamp.
|
package/dist/context.d.ts
CHANGED
|
@@ -11,4 +11,4 @@ export declare const boundContexts: Map<number, BoundContext>;
|
|
|
11
11
|
* Note that the default credentials of a bound context are copied from the global credentials.
|
|
12
12
|
* @category Contexts
|
|
13
13
|
*/
|
|
14
|
-
export declare function bindContext(this:
|
|
14
|
+
export declare function bindContext(this: V_Context, init?: ContextInit): BoundContext;
|
package/dist/context.js
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
// SPDX-License-Identifier: LGPL-3.0-or-later
|
|
2
|
+
import { UV } from 'kerium';
|
|
2
3
|
import { bindFunctions } from 'utilium';
|
|
3
|
-
import {
|
|
4
|
-
import { createCredentials } from './internal/credentials.js';
|
|
5
|
-
import * as path from './path.js';
|
|
4
|
+
import { contextOf, createChildContext } from './internal/contexts.js';
|
|
6
5
|
import * as fs from './node/index.js';
|
|
6
|
+
import * as path from './path.js';
|
|
7
7
|
import * as xattr from './vfs/xattr.js';
|
|
8
|
-
// 0 is reserved for the global/default context
|
|
9
|
-
let _nextId = 1;
|
|
10
8
|
/**
|
|
11
9
|
* A map of all contexts.
|
|
12
10
|
* @internal
|
|
@@ -18,19 +16,12 @@ export const boundContexts = new Map();
|
|
|
18
16
|
* Note that the default credentials of a bound context are copied from the global credentials.
|
|
19
17
|
* @category Contexts
|
|
20
18
|
*/
|
|
21
|
-
export function bindContext(
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
credentials: createCredentials(credentials),
|
|
28
|
-
descriptors: new Map(),
|
|
29
|
-
parent,
|
|
30
|
-
children: [],
|
|
31
|
-
};
|
|
32
|
-
const bound = {
|
|
33
|
-
...ctx,
|
|
19
|
+
export function bindContext(init = {}) {
|
|
20
|
+
const $ = contextOf(this);
|
|
21
|
+
if (!fs.statSync.call(this, $.root).isDirectory())
|
|
22
|
+
throw UV('ENOTDIR', { syscall: 'chroot', path: $.root });
|
|
23
|
+
const ctx = createChildContext($, init);
|
|
24
|
+
const bound = Object.assign(ctx, {
|
|
34
25
|
fs: {
|
|
35
26
|
...bindFunctions(fs, ctx),
|
|
36
27
|
promises: bindFunctions(fs.promises, ctx),
|
|
@@ -42,7 +33,7 @@ export function bindContext({ root = this?.root || '/', pwd = this?.pwd || '/',
|
|
|
42
33
|
ctx.children.push(child);
|
|
43
34
|
return child;
|
|
44
35
|
},
|
|
45
|
-
};
|
|
36
|
+
});
|
|
46
37
|
boundContexts.set(ctx.id, bound);
|
|
47
38
|
return bound;
|
|
48
39
|
}
|
|
@@ -4,11 +4,18 @@ import type * as path from '../path.js';
|
|
|
4
4
|
import type { Handle } from '../vfs/file.js';
|
|
5
5
|
import type * as xattr from '../vfs/xattr.js';
|
|
6
6
|
import type { Credentials, CredentialsInit } from './credentials.js';
|
|
7
|
+
/**
|
|
8
|
+
* Symbol used for context branding
|
|
9
|
+
* @internal @hidden
|
|
10
|
+
*/
|
|
11
|
+
declare const kIsContext: unique symbol;
|
|
7
12
|
/**
|
|
8
13
|
* A context used for FS operations
|
|
9
14
|
* @category Contexts
|
|
10
15
|
*/
|
|
11
16
|
export interface FSContext {
|
|
17
|
+
/** The unique ID of the context */
|
|
18
|
+
readonly [kIsContext]: boolean;
|
|
12
19
|
/** The unique ID of the context */
|
|
13
20
|
readonly id: number;
|
|
14
21
|
/**
|
|
@@ -22,16 +29,16 @@ export interface FSContext {
|
|
|
22
29
|
/** The credentials of the context, used for access checks */
|
|
23
30
|
readonly credentials: Credentials;
|
|
24
31
|
/** A map of open file descriptors to their handles */
|
|
25
|
-
descriptors: Map<number, Handle>;
|
|
32
|
+
readonly descriptors: Map<number, Handle>;
|
|
26
33
|
/** The parent context, if any. */
|
|
27
|
-
parent:
|
|
34
|
+
readonly parent: FSContext | null;
|
|
28
35
|
/** The child contexts */
|
|
29
|
-
children: FSContext[];
|
|
36
|
+
readonly children: FSContext[];
|
|
30
37
|
}
|
|
31
38
|
/**
|
|
32
39
|
* maybe an FS context
|
|
33
40
|
*/
|
|
34
|
-
export type V_Context =
|
|
41
|
+
export type V_Context = unknown;
|
|
35
42
|
/**
|
|
36
43
|
* Allows you to restrict operations to a specific root path and set of credentials.
|
|
37
44
|
* @category Contexts
|
|
@@ -62,3 +69,16 @@ export interface ContextInit {
|
|
|
62
69
|
* @category Contexts
|
|
63
70
|
*/
|
|
64
71
|
export declare const defaultContext: FSContext;
|
|
72
|
+
export declare function contextOf($: unknown): FSContext;
|
|
73
|
+
/**
|
|
74
|
+
* Create a blank FS Context
|
|
75
|
+
* @internal
|
|
76
|
+
* @category Contexts
|
|
77
|
+
* @todo Make sure parent root can't be escaped
|
|
78
|
+
*
|
|
79
|
+
* This exists so that `kIsContext` is not exported and to make sure the context is "secure".
|
|
80
|
+
*/
|
|
81
|
+
export declare function createChildContext(parent: FSContext, init?: ContextInit): FSContext & {
|
|
82
|
+
parent: FSContext;
|
|
83
|
+
};
|
|
84
|
+
export {};
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import { createCredentials } from './credentials.js';
|
|
2
|
+
/**
|
|
3
|
+
* Symbol used for context branding
|
|
4
|
+
* @internal @hidden
|
|
5
|
+
*/
|
|
6
|
+
const kIsContext = Symbol('ZenFSContext');
|
|
2
7
|
/**
|
|
3
8
|
* The default/global context.
|
|
4
9
|
* @internal @hidden
|
|
5
10
|
* @category Contexts
|
|
6
11
|
*/
|
|
7
12
|
export const defaultContext = {
|
|
13
|
+
[kIsContext]: true,
|
|
8
14
|
id: 0,
|
|
9
15
|
root: '/',
|
|
10
16
|
pwd: '/',
|
|
@@ -13,3 +19,37 @@ export const defaultContext = {
|
|
|
13
19
|
parent: null,
|
|
14
20
|
children: [],
|
|
15
21
|
};
|
|
22
|
+
export function contextOf($) {
|
|
23
|
+
return typeof $ === 'object' && $ !== null && kIsContext in $ ? $ : defaultContext;
|
|
24
|
+
}
|
|
25
|
+
// 0 is reserved for the global/default context
|
|
26
|
+
let _nextId = 1;
|
|
27
|
+
/**
|
|
28
|
+
* Create a blank FS Context
|
|
29
|
+
* @internal
|
|
30
|
+
* @category Contexts
|
|
31
|
+
* @todo Make sure parent root can't be escaped
|
|
32
|
+
*
|
|
33
|
+
* This exists so that `kIsContext` is not exported and to make sure the context is "secure".
|
|
34
|
+
*/
|
|
35
|
+
export function createChildContext(parent, init = {}) {
|
|
36
|
+
const { root = parent.root, pwd = parent.pwd, credentials = structuredClone(parent.credentials) } = init;
|
|
37
|
+
const ctx = {
|
|
38
|
+
[kIsContext]: true,
|
|
39
|
+
id: _nextId++,
|
|
40
|
+
root,
|
|
41
|
+
pwd,
|
|
42
|
+
credentials: createCredentials(credentials),
|
|
43
|
+
descriptors: new Map(),
|
|
44
|
+
parent: parent,
|
|
45
|
+
children: [],
|
|
46
|
+
};
|
|
47
|
+
Object.defineProperties(ctx, {
|
|
48
|
+
id: { configurable: false, writable: false },
|
|
49
|
+
credentials: { configurable: false, writable: false },
|
|
50
|
+
descriptors: { configurable: false, writable: false },
|
|
51
|
+
parent: { configurable: false, writable: false },
|
|
52
|
+
children: { configurable: false, writable: false },
|
|
53
|
+
});
|
|
54
|
+
return ctx;
|
|
55
|
+
}
|
package/dist/internal/inode.js
CHANGED
|
@@ -39,9 +39,9 @@ import { sizeof } from 'memium';
|
|
|
39
39
|
import { $from, field, struct, types as t } from 'memium/decorators';
|
|
40
40
|
import { decodeUTF8, encodeUTF8, pick } from 'utilium';
|
|
41
41
|
import { BufferView } from 'utilium/buffer.js';
|
|
42
|
-
import { Stats } from '../node/stats.js';
|
|
43
42
|
import * as c from '../constants.js';
|
|
44
|
-
import {
|
|
43
|
+
import { Stats } from '../node/stats.js';
|
|
44
|
+
import { contextOf } from './contexts.js';
|
|
45
45
|
/**
|
|
46
46
|
* Root inode
|
|
47
47
|
* @hidden
|
|
@@ -620,7 +620,7 @@ export function isFIFO(metadata) {
|
|
|
620
620
|
* @internal
|
|
621
621
|
*/
|
|
622
622
|
export function hasAccess($, inode, access) {
|
|
623
|
-
const credentials =
|
|
623
|
+
const { credentials } = contextOf($);
|
|
624
624
|
if (isSymbolicLink(inode) || credentials.euid === 0 || credentials.egid === 0)
|
|
625
625
|
return true;
|
|
626
626
|
let perm = 0;
|
package/dist/node/sync.js
CHANGED
|
@@ -467,7 +467,7 @@ export function lutimesSync(path, atime, mtime) {
|
|
|
467
467
|
lutimesSync;
|
|
468
468
|
export function realpathSync(path, options) {
|
|
469
469
|
const encoding = typeof options == 'string' ? options : (options?.encoding ?? 'utf8');
|
|
470
|
-
path = normalizePath(path);
|
|
470
|
+
path = normalizePath(path, true);
|
|
471
471
|
const { fullPath } = _sync.resolve(this, path);
|
|
472
472
|
if (encoding == 'utf8' || encoding == 'utf-8')
|
|
473
473
|
return fullPath;
|
package/dist/path.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// SPDX-License-Identifier: LGPL-3.0-or-later
|
|
2
|
-
import {
|
|
2
|
+
import { contextOf } from './internal/contexts.js';
|
|
3
3
|
import { globToRegex } from './utils.js';
|
|
4
4
|
export const sep = '/';
|
|
5
5
|
function validateObject(str, name) {
|
|
@@ -81,7 +81,7 @@ export function formatExt(ext) {
|
|
|
81
81
|
}
|
|
82
82
|
export function resolve(...parts) {
|
|
83
83
|
let resolved = '';
|
|
84
|
-
for (const part of [...parts.reverse(), this
|
|
84
|
+
for (const part of [...parts.reverse(), contextOf(this).pwd]) {
|
|
85
85
|
if (!part?.length)
|
|
86
86
|
continue;
|
|
87
87
|
resolved = `${part}/${resolved}`;
|
|
@@ -334,7 +334,7 @@ export function format(pathObject) {
|
|
|
334
334
|
return dir === pathObject.root ? `${dir}${base}` : `${dir}/${base}`;
|
|
335
335
|
}
|
|
336
336
|
export function parse(path) {
|
|
337
|
-
const isAbsolute = path
|
|
337
|
+
const isAbsolute = path[0] === '/';
|
|
338
338
|
const ret = { root: isAbsolute ? '/' : '', dir: '', base: '', ext: '', name: '' };
|
|
339
339
|
if (path.length === 0)
|
|
340
340
|
return ret;
|
package/dist/utils.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { type Exception } from 'kerium';
|
|
|
2
2
|
import type * as fs from 'node:fs';
|
|
3
3
|
import type { Worker as NodeWorker } from 'node:worker_threads';
|
|
4
4
|
import { type OptionalTuple } from 'utilium';
|
|
5
|
+
import type { V_Context } from './internal/contexts.js';
|
|
5
6
|
declare global {
|
|
6
7
|
function atob(data: string): string;
|
|
7
8
|
function btoa(data: string): string;
|
|
@@ -31,8 +32,9 @@ export declare function normalizeTime(time: fs.TimeLike): number;
|
|
|
31
32
|
/**
|
|
32
33
|
* Normalizes a path
|
|
33
34
|
* @internal
|
|
35
|
+
* @todo clean this up and make it so `path.resolve` is only called when an explicit context is passed (i.e. `normalizePath(..., $)` to use `path.resolve`)
|
|
34
36
|
*/
|
|
35
|
-
export declare function normalizePath(p: fs.PathLike, noResolve?: boolean): string;
|
|
37
|
+
export declare function normalizePath(this: V_Context, p: fs.PathLike, noResolve?: boolean): string;
|
|
36
38
|
/**
|
|
37
39
|
* Normalizes options
|
|
38
40
|
* @param options options to normalize
|
package/dist/utils.js
CHANGED
|
@@ -51,6 +51,7 @@ export function normalizeTime(time) {
|
|
|
51
51
|
/**
|
|
52
52
|
* Normalizes a path
|
|
53
53
|
* @internal
|
|
54
|
+
* @todo clean this up and make it so `path.resolve` is only called when an explicit context is passed (i.e. `normalizePath(..., $)` to use `path.resolve`)
|
|
54
55
|
*/
|
|
55
56
|
export function normalizePath(p, noResolve = false) {
|
|
56
57
|
if (p instanceof URL) {
|
|
@@ -67,7 +68,7 @@ export function normalizePath(p, noResolve = false) {
|
|
|
67
68
|
throw withErrno('EINVAL', 'Path can not be empty');
|
|
68
69
|
p = p.replaceAll(/[/\\]+/g, '/');
|
|
69
70
|
// Note: PWD is not resolved here, it is resolved later.
|
|
70
|
-
return noResolve ? p : resolve(p);
|
|
71
|
+
return noResolve ? p : resolve.call(this, p);
|
|
71
72
|
}
|
|
72
73
|
/**
|
|
73
74
|
* Normalizes options
|
package/dist/vfs/acl.js
CHANGED
|
@@ -44,9 +44,9 @@ import { err } from 'kerium/log';
|
|
|
44
44
|
import { packed, sizeof } from 'memium';
|
|
45
45
|
import { $from, struct, types as t } from 'memium/decorators';
|
|
46
46
|
import { BufferView } from 'utilium/buffer.js';
|
|
47
|
-
import { defaultContext } from '../internal/contexts.js';
|
|
48
|
-
import { Attributes } from '../internal/inode.js';
|
|
49
47
|
import { R_OK, S_IRWXG, S_IRWXO, S_IRWXU, W_OK, X_OK } from '../constants.js';
|
|
48
|
+
import { contextOf } from '../internal/contexts.js';
|
|
49
|
+
import { Attributes } from '../internal/inode.js';
|
|
50
50
|
import * as xattr from './xattr.js';
|
|
51
51
|
const version = 2;
|
|
52
52
|
export var Type;
|
|
@@ -206,7 +206,7 @@ export function check($, inode, access) {
|
|
|
206
206
|
if (!shouldCheck)
|
|
207
207
|
return true;
|
|
208
208
|
inode.attributes ??= new Attributes();
|
|
209
|
-
const { euid, egid } =
|
|
209
|
+
const { euid, egid } = contextOf($).credentials;
|
|
210
210
|
const data = inode.attributes.get('system.posix_acl_access');
|
|
211
211
|
if (!data)
|
|
212
212
|
return true;
|
package/dist/vfs/async.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { setUVMessage, UV } from 'kerium';
|
|
2
2
|
import { decodeUTF8 } from 'utilium';
|
|
3
3
|
import * as constants from '../constants.js';
|
|
4
|
-
import {
|
|
4
|
+
import { contextOf } from '../internal/contexts.js';
|
|
5
5
|
import { hasAccess, isDirectory, isSymbolicLink } from '../internal/inode.js';
|
|
6
6
|
import { basename, dirname, join, parse, resolve as resolvePath } from '../path.js';
|
|
7
7
|
import { normalizeMode, normalizePath } from '../utils.js';
|
|
@@ -17,6 +17,7 @@ import { emitChange } from './watchers.js';
|
|
|
17
17
|
* @internal @hidden
|
|
18
18
|
*/
|
|
19
19
|
export async function resolve($, path, preserveSymlinks, extra) {
|
|
20
|
+
path = resolvePath.call($, path);
|
|
20
21
|
if (preserveSymlinks) {
|
|
21
22
|
const resolved = resolveMount(path, $, extra);
|
|
22
23
|
const stats = await resolved.fs.stat(resolved.path).catch(() => undefined);
|
|
@@ -74,7 +75,7 @@ export async function open($, path, opt) {
|
|
|
74
75
|
throw UV('ENOTDIR', 'open', dirname(path));
|
|
75
76
|
if (!opt.allowDirectory && mode & constants.S_IFDIR)
|
|
76
77
|
throw UV('EISDIR', 'open', path);
|
|
77
|
-
const { euid: uid, egid: gid } =
|
|
78
|
+
const { euid: uid, egid: gid } = contextOf($).credentials;
|
|
78
79
|
const inode = await fs.createFile(resolved, {
|
|
79
80
|
mode,
|
|
80
81
|
uid: parentStats.mode & constants.S_ISUID ? parentStats.uid : uid,
|
|
@@ -110,7 +111,7 @@ export async function readlink(path) {
|
|
|
110
111
|
}
|
|
111
112
|
export async function mkdir(path, options = {}) {
|
|
112
113
|
path = normalizePath(path);
|
|
113
|
-
const { euid: uid, egid: gid } = this
|
|
114
|
+
const { euid: uid, egid: gid } = contextOf(this).credentials;
|
|
114
115
|
const { mode = 0o777, recursive } = options;
|
|
115
116
|
const { fs, path: resolved } = resolveMount(path, this, { syscall: 'mkdir' });
|
|
116
117
|
const __create = async (path, resolved, parent) => {
|
package/dist/vfs/file.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: LGPL-3.0-or-later
|
|
2
2
|
import { UV, withErrno } from 'kerium';
|
|
3
3
|
import * as c from '../constants.js';
|
|
4
|
-
import {
|
|
4
|
+
import { contextOf } from '../internal/contexts.js';
|
|
5
5
|
import { _chown, InodeFlags, isBlockDevice, isCharacterDevice } from '../internal/inode.js';
|
|
6
6
|
import '../polyfills.js';
|
|
7
7
|
/**
|
|
@@ -371,7 +371,7 @@ export class Handle {
|
|
|
371
371
|
* @internal @hidden
|
|
372
372
|
*/
|
|
373
373
|
export function toFD(file) {
|
|
374
|
-
const map = file.context
|
|
374
|
+
const map = contextOf(file.context).descriptors;
|
|
375
375
|
const fd = Math.max(map.size ? Math.max(...map.keys()) + 1 : 0, 4);
|
|
376
376
|
map.set(fd, file);
|
|
377
377
|
return fd;
|
|
@@ -380,12 +380,12 @@ export function toFD(file) {
|
|
|
380
380
|
* @internal @hidden
|
|
381
381
|
*/
|
|
382
382
|
export function fromFD($, fd) {
|
|
383
|
-
const map =
|
|
383
|
+
const map = contextOf($).descriptors;
|
|
384
384
|
const value = map.get(fd);
|
|
385
385
|
if (!value)
|
|
386
386
|
throw withErrno('EBADF');
|
|
387
387
|
return value;
|
|
388
388
|
}
|
|
389
389
|
export function deleteFD($, fd) {
|
|
390
|
-
return (
|
|
390
|
+
return contextOf($).descriptors.delete(fd);
|
|
391
391
|
}
|
package/dist/vfs/shared.js
CHANGED
|
@@ -4,7 +4,7 @@ import { Errno, Exception, UV, withErrno } from 'kerium';
|
|
|
4
4
|
import { alert, debug, err, info, notice, warn } from 'kerium/log';
|
|
5
5
|
import { InMemory } from '../backends/memory.js';
|
|
6
6
|
import { size_max } from '../constants.js';
|
|
7
|
-
import {
|
|
7
|
+
import { contextOf } from '../internal/contexts.js';
|
|
8
8
|
import { credentialsAllowRoot } from '../internal/credentials.js';
|
|
9
9
|
import { withExceptionContext } from '../internal/error.js';
|
|
10
10
|
import { join, resolve } from '../path.js';
|
|
@@ -53,9 +53,10 @@ export function umount(mountPoint) {
|
|
|
53
53
|
* @internal @hidden
|
|
54
54
|
*/
|
|
55
55
|
export function resolveMount(path, ctx, extra) {
|
|
56
|
-
const root = ctx
|
|
56
|
+
const { root } = contextOf(ctx);
|
|
57
57
|
const _exceptionContext = { path, ...extra };
|
|
58
|
-
path = normalizePath(join(root, path));
|
|
58
|
+
path = normalizePath(join(root, path), true);
|
|
59
|
+
path = resolve.call(ctx, path);
|
|
59
60
|
const sortedMounts = [...mounts].sort((a, b) => (a[0].length > b[0].length ? -1 : 1)); // descending order of the string length
|
|
60
61
|
for (const [mountPoint, fs] of sortedMounts) {
|
|
61
62
|
// We know path is normalized, so it would be a substring of the mount point.
|
|
@@ -94,7 +95,7 @@ export function _statfs(fs, bigint) {
|
|
|
94
95
|
* @category Backends and Configuration
|
|
95
96
|
*/
|
|
96
97
|
export function chroot(path) {
|
|
97
|
-
const $ = this
|
|
98
|
+
const $ = contextOf(this);
|
|
98
99
|
if (!credentialsAllowRoot($.credentials))
|
|
99
100
|
throw withErrno('EPERM', 'Can not chroot() as non-root user');
|
|
100
101
|
$.root ??= '/';
|
package/dist/vfs/sync.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { setUVMessage, UV } from 'kerium';
|
|
2
2
|
import { decodeUTF8 } from 'utilium';
|
|
3
3
|
import * as constants from '../constants.js';
|
|
4
|
-
import {
|
|
4
|
+
import { contextOf } from '../internal/contexts.js';
|
|
5
5
|
import { hasAccess, isDirectory, isSymbolicLink } from '../internal/inode.js';
|
|
6
6
|
import { basename, dirname, join, parse, resolve as resolvePath } from '../path.js';
|
|
7
7
|
import { normalizeMode, normalizePath } from '../utils.js';
|
|
@@ -18,6 +18,7 @@ import { emitChange } from './watchers.js';
|
|
|
18
18
|
* @internal @hidden
|
|
19
19
|
*/
|
|
20
20
|
export function resolve($, path, preserveSymlinks, extra) {
|
|
21
|
+
path = resolvePath.call($, path);
|
|
21
22
|
/* Try to resolve it directly. If this works,
|
|
22
23
|
that means we don't need to perform any resolution for parent directories. */
|
|
23
24
|
try {
|
|
@@ -87,7 +88,7 @@ export function open(path, opt) {
|
|
|
87
88
|
if (checkAccess && !hasAccess(this, parentStats, constants.W_OK)) {
|
|
88
89
|
throw UV('EACCES', 'open', path);
|
|
89
90
|
}
|
|
90
|
-
const { euid: uid, egid: gid } = this
|
|
91
|
+
const { euid: uid, egid: gid } = contextOf(this).credentials;
|
|
91
92
|
const inode = fs.createFileSync(resolved, {
|
|
92
93
|
mode,
|
|
93
94
|
uid: parentStats.mode & constants.S_ISUID ? parentStats.uid : uid,
|
|
@@ -124,7 +125,7 @@ export function readlink(path) {
|
|
|
124
125
|
export function mkdir(path, options = {}) {
|
|
125
126
|
path = normalizePath(path);
|
|
126
127
|
const { fs, path: resolved } = resolve(this, path);
|
|
127
|
-
const { euid: uid, egid: gid } = this
|
|
128
|
+
const { euid: uid, egid: gid } = contextOf(this).credentials;
|
|
128
129
|
const { mode = 0o777, recursive } = options;
|
|
129
130
|
const __create = (path, resolved, parent) => {
|
|
130
131
|
if (checkAccess && !hasAccess(this, parent, constants.W_OK))
|
package/dist/vfs/watchers.d.ts
CHANGED
|
@@ -75,5 +75,5 @@ export declare function removeWatcher(path: string, watcher: FSWatcher): void;
|
|
|
75
75
|
/**
|
|
76
76
|
* @internal @hidden
|
|
77
77
|
*/
|
|
78
|
-
export declare function emitChange(
|
|
78
|
+
export declare function emitChange(context: V_Context, eventType: fs.WatchEventType, filename: string): void;
|
|
79
79
|
export {};
|
package/dist/vfs/watchers.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { EventEmitter } from 'eventemitter3';
|
|
2
2
|
import { UV } from 'kerium';
|
|
3
|
-
import {
|
|
4
|
-
import { normalizePath } from '../utils.js';
|
|
3
|
+
import { contextOf } from '../internal/contexts.js';
|
|
5
4
|
import { isStatsEqual } from '../node/stats.js';
|
|
6
5
|
import { statSync } from '../node/sync.js';
|
|
6
|
+
import { basename, dirname, join, relative } from '../path.js';
|
|
7
|
+
import { normalizePath } from '../utils.js';
|
|
7
8
|
/**
|
|
8
9
|
* Base class for file system watchers.
|
|
9
10
|
* Provides event handling capabilities for watching file system changes.
|
|
@@ -61,9 +62,10 @@ export class FSWatcher extends Watcher {
|
|
|
61
62
|
options;
|
|
62
63
|
realpath;
|
|
63
64
|
constructor(context, path, options) {
|
|
64
|
-
|
|
65
|
+
const $ = contextOf(context);
|
|
66
|
+
super($, path);
|
|
65
67
|
this.options = options;
|
|
66
|
-
this.realpath =
|
|
68
|
+
this.realpath = join($.root, path);
|
|
67
69
|
addWatcher(this.realpath, this);
|
|
68
70
|
}
|
|
69
71
|
close() {
|
|
@@ -145,7 +147,8 @@ export function removeWatcher(path, watcher) {
|
|
|
145
147
|
/**
|
|
146
148
|
* @internal @hidden
|
|
147
149
|
*/
|
|
148
|
-
export function emitChange(
|
|
150
|
+
export function emitChange(context, eventType, filename) {
|
|
151
|
+
const $ = contextOf(context);
|
|
149
152
|
if ($)
|
|
150
153
|
filename = join($.root ?? '/', filename);
|
|
151
154
|
filename = normalizePath(filename);
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// SPDX-License-Identifier: LGPL-3.0-or-later
|
|
2
2
|
import assert from 'node:assert';
|
|
3
|
+
import { randomBytes } from 'node:crypto';
|
|
3
4
|
import { suite, test } from 'node:test';
|
|
4
5
|
import { Worker } from 'worker_threads';
|
|
5
|
-
import { fs, mount, resolveMountConfig, SingleBuffer } from '@zenfs/core';
|
|
6
|
+
import { fs, mount, resolveMountConfig, SingleBuffer, vfs } from '@zenfs/core';
|
|
6
7
|
import { setupLogs } from '../logs.js';
|
|
7
8
|
|
|
8
9
|
setupLogs();
|
|
@@ -51,4 +52,50 @@ await suite('SingleBuffer', () => {
|
|
|
51
52
|
|
|
52
53
|
assert(fs.existsSync('/shared/worker-file.ts'));
|
|
53
54
|
});
|
|
55
|
+
|
|
56
|
+
test('reliability across varied file sizes', async () => {
|
|
57
|
+
const mountPoint = '/sbfs-reliability';
|
|
58
|
+
const verifyMountPoint = '/sbfs-verify';
|
|
59
|
+
const buffer = new ArrayBuffer(0x400000);
|
|
60
|
+
const writable = await resolveMountConfig({ backend: SingleBuffer, buffer, label: 'reliability' });
|
|
61
|
+
mount(mountPoint, writable);
|
|
62
|
+
|
|
63
|
+
const filePath = `${mountPoint}/payload.bin`;
|
|
64
|
+
const growthSizes = [0, 1, 17, 512, 8192, 65535, 262144, 524288];
|
|
65
|
+
const shrinkSizes = [262144, 4096, 128, 0];
|
|
66
|
+
|
|
67
|
+
const verifySnapshot = async (expected: Buffer, size: number) => {
|
|
68
|
+
mount(verifyMountPoint, writable);
|
|
69
|
+
try {
|
|
70
|
+
const reopened = fs.readFileSync(`${verifyMountPoint}/payload.bin`);
|
|
71
|
+
assert.strictEqual(reopened.byteLength, size, `snapshot size mismatch for ${size} bytes`);
|
|
72
|
+
assert.deepStrictEqual(reopened, expected, `snapshot content mismatch for ${size} bytes`);
|
|
73
|
+
} finally {
|
|
74
|
+
vfs.umount(verifyMountPoint);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
for (const size of growthSizes) {
|
|
80
|
+
const payload = size ? randomBytes(size) : Buffer.alloc(0);
|
|
81
|
+
fs.writeFileSync(filePath, payload);
|
|
82
|
+
const direct = fs.readFileSync(filePath);
|
|
83
|
+
assert.strictEqual(direct.byteLength, size, `direct size mismatch for ${size} bytes`);
|
|
84
|
+
assert.deepStrictEqual(direct, payload, `direct content mismatch for ${size} bytes`);
|
|
85
|
+
await verifySnapshot(direct, size);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
for (const size of shrinkSizes) {
|
|
89
|
+
const payload = size ? randomBytes(size) : Buffer.alloc(0);
|
|
90
|
+
fs.writeFileSync(filePath, payload);
|
|
91
|
+
const direct = fs.readFileSync(filePath);
|
|
92
|
+
assert.strictEqual(direct.byteLength, size, `direct size mismatch after shrink to ${size} bytes`);
|
|
93
|
+
assert.deepStrictEqual(direct, payload, `direct content mismatch after shrink to ${size} bytes`);
|
|
94
|
+
await verifySnapshot(direct, size);
|
|
95
|
+
}
|
|
96
|
+
} finally {
|
|
97
|
+
if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
|
|
98
|
+
vfs.umount(mountPoint);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
54
101
|
});
|
|
@@ -60,8 +60,7 @@ suite('Context', () => {
|
|
|
60
60
|
await promise;
|
|
61
61
|
});
|
|
62
62
|
|
|
63
|
-
test('Path resolution of / with context root and mount point being the same', async () => {
|
|
64
|
-
// @zenfs/core#226
|
|
63
|
+
test('Path resolution of / with context root and mount point being the same #226', async () => {
|
|
65
64
|
await configure({
|
|
66
65
|
mounts: { '/bananas': InMemory },
|
|
67
66
|
});
|
|
@@ -73,8 +72,7 @@ suite('Context', () => {
|
|
|
73
72
|
assert.deepEqual(bananas.fs.readdirSync('/'), ['yellow']);
|
|
74
73
|
});
|
|
75
74
|
|
|
76
|
-
test('Different working directory',
|
|
77
|
-
// @zenfs/core#263
|
|
75
|
+
test('Different working directory #263', () => {
|
|
78
76
|
ctx.mkdirSync('/test');
|
|
79
77
|
context.pwd = '/test';
|
|
80
78
|
|
package/tests/fs/links.test.ts
CHANGED
|
@@ -19,8 +19,7 @@ suite('Links', () => {
|
|
|
19
19
|
assert(stats.isSymbolicLink());
|
|
20
20
|
});
|
|
21
21
|
|
|
22
|
-
test('lstat file inside symlinked directory', async () => {
|
|
23
|
-
// @zenfs/core#241
|
|
22
|
+
test('lstat file inside symlinked directory #241', async () => {
|
|
24
23
|
await fs.promises.mkdir('/a');
|
|
25
24
|
await fs.promises.writeFile('/a/hello.txt', 'hello world');
|
|
26
25
|
await fs.promises.symlink('/a', '/b');
|
package/tests/fs/read.test.ts
CHANGED
|
@@ -70,8 +70,7 @@ suite('read', () => {
|
|
|
70
70
|
assert.equal(bytesRead, expected.length);
|
|
71
71
|
});
|
|
72
72
|
|
|
73
|
-
test('read using callback API', async () => {
|
|
74
|
-
// @zenfs/core#239
|
|
73
|
+
test('read using callback API #239', async () => {
|
|
75
74
|
const path = '/text.txt';
|
|
76
75
|
|
|
77
76
|
fs.writeFileSync(path, 'hello world');
|