@zenfs/core 2.7.6 → 2.8.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/README.md +5 -0
- package/dist/backends/index.d.ts +3 -1
- package/dist/backends/index.js +3 -1
- package/dist/backends/single_buffer/backend.d.ts +42 -0
- package/dist/backends/single_buffer/backend.js +204 -0
- package/dist/backends/{single_buffer.d.ts → single_buffer/v1.d.ts} +24 -34
- package/dist/backends/{single_buffer.js → single_buffer/v1.js} +10 -26
- package/dist/backends/single_buffer/v2.d.ts +257 -0
- package/dist/backends/single_buffer/v2.js +1283 -0
- package/dist/backends/store/fs.d.ts +1 -1
- package/dist/backends/store/fs.js +35 -18
- package/dist/backends/store/map.d.ts +6 -6
- package/dist/backends/store/map.js +6 -6
- package/dist/backends/store/store.d.ts +18 -14
- package/dist/backends/store/store.js +12 -14
- package/dist/config.d.ts +7 -0
- package/dist/config.js +9 -3
- package/dist/internal/contexts.d.ts +5 -0
- package/dist/internal/contexts.js +7 -1
- package/dist/internal/error.d.ts +5 -0
- package/dist/internal/error.js +10 -0
- package/dist/internal/inode.d.ts +25 -0
- package/dist/internal/inode.js +75 -10
- package/dist/mixins/async.js +47 -30
- package/dist/node/promises.js +23 -124
- package/dist/node/stats.d.ts +1 -1
- package/dist/node/stats.js +4 -1
- package/dist/node/sync.js +20 -51
- package/dist/polyfills.js +16 -0
- package/dist/vfs/async.d.ts +4 -1
- package/dist/vfs/async.js +149 -69
- package/dist/vfs/config.d.ts +10 -1
- package/dist/vfs/config.js +5 -2
- package/dist/vfs/file.js +10 -4
- package/dist/vfs/ioctl.js +2 -2
- package/dist/vfs/shared.d.ts +2 -0
- package/dist/vfs/shared.js +3 -4
- package/dist/vfs/sync.d.ts +4 -1
- package/dist/vfs/sync.js +142 -64
- package/dist/vfs/vcache.d.ts +14 -14
- package/dist/vfs/vcache.js +65 -57
- package/dist/vfs/watchers.d.ts +1 -0
- package/dist/vfs/watchers.js +2 -4
- package/dist/vfs/xattr.js +18 -17
- package/package.json +3 -4
- package/scripts/make-index.js +1 -1
- package/tests/backend/single-buffer-rotate.worker.js +1 -1
- package/tests/backend/single-buffer-v2.test.ts +342 -0
- package/tests/backend/single-buffer-v2.worker.js +51 -0
- package/tests/backend/single-buffer.test.ts +6 -6
- package/tests/backend/single-buffer.worker.js +1 -0
- package/tests/common/async.test.ts +46 -0
- package/tests/common/config.test.ts +1 -1
- package/tests/common/context.test.ts +30 -0
- package/tests/common/file_index.test.ts +1 -1
- package/tests/common/mounts.test.ts +12 -0
- package/tests/common/vcache.test.ts +13 -0
- package/tests/fs/concurrency.test.ts +3 -3
- package/tests/fs/links.test.ts +76 -0
- package/tests/fs/permissions.test.ts +253 -0
- package/tests/fs/stat.test.ts +1 -1
- package/tests/fs/streams.test.ts +1 -1
- package/tests/fs/watch.test.ts +14 -0
- package/tests/fs/xattr.test.ts +10 -0
- package/tests/perf/config.json +31 -1
- package/tests/perf/single-buffer-random.ts +38 -0
- package/tests/perf/single-buffer-read.ts +28 -0
- package/tests/perf/single-buffer-write.ts +41 -0
- package/tests/setup/single-buffer-v2.ts +10 -0
- package/tests/setup/single-buffer.ts +1 -0
|
@@ -123,7 +123,7 @@ export declare class StoreFS<T extends Store = Store> extends FileSystem {
|
|
|
123
123
|
protected findInodeSync(tx: WrappedTransaction, path: string): Inode;
|
|
124
124
|
private _lastID?;
|
|
125
125
|
/** Allocates a new ID and adds the ID/path */
|
|
126
|
-
protected allocNew(path: string): number;
|
|
126
|
+
protected allocNew(path: string): [number, number];
|
|
127
127
|
/**
|
|
128
128
|
* Commits a new file (well, a FILE or a DIRECTORY) to the file system with `mode`.
|
|
129
129
|
* Note: This will commit the transaction.
|
|
@@ -93,9 +93,7 @@ export class StoreFS extends FileSystem {
|
|
|
93
93
|
* Add a inode/path pair
|
|
94
94
|
*/
|
|
95
95
|
_add(ino, path) {
|
|
96
|
-
|
|
97
|
-
this._paths.set(ino, new Set());
|
|
98
|
-
this._paths.get(ino).add(path);
|
|
96
|
+
this._paths.getOrInsertComputed(ino, () => new Set()).add(path);
|
|
99
97
|
this._ids.set(path, ino);
|
|
100
98
|
}
|
|
101
99
|
/**
|
|
@@ -618,13 +616,20 @@ export class StoreFS extends FileSystem {
|
|
|
618
616
|
const tx = __addDisposableResource(env_17, this.transaction(), true);
|
|
619
617
|
const inode = await this.findInode(tx, path);
|
|
620
618
|
const end = offset + data.byteLength;
|
|
621
|
-
let buffer = data;
|
|
622
619
|
if (!tx.flag('partial')) {
|
|
623
|
-
buffer = extendBuffer((await tx.get(inode.data)) ?? new Uint8Array(), end);
|
|
620
|
+
const buffer = extendBuffer((await tx.get(inode.data)) ?? new Uint8Array(), end);
|
|
624
621
|
buffer.set(data, offset);
|
|
625
|
-
|
|
622
|
+
await tx.set(inode.data, buffer);
|
|
623
|
+
}
|
|
624
|
+
else if (offset <= inode.size) {
|
|
625
|
+
await tx.set(inode.data, data, offset);
|
|
626
|
+
}
|
|
627
|
+
else {
|
|
628
|
+
// Writes past the end of the file also zero the bytes before them
|
|
629
|
+
const extended = new Uint8Array(end - inode.size);
|
|
630
|
+
extended.set(data, offset - inode.size);
|
|
631
|
+
await tx.set(inode.data, extended, inode.size);
|
|
626
632
|
}
|
|
627
|
-
await tx.set(inode.data, buffer, offset);
|
|
628
633
|
// Update the inode in the same transaction, so data and metadata can not get out of sync (#286)
|
|
629
634
|
inode.update({ size: Math.max(inode.size, end), mtimeMs: Date.now() });
|
|
630
635
|
await tx.set(inode.ino, inode);
|
|
@@ -647,13 +652,20 @@ export class StoreFS extends FileSystem {
|
|
|
647
652
|
const tx = __addDisposableResource(env_18, this.transaction(), false);
|
|
648
653
|
const inode = this.findInodeSync(tx, path);
|
|
649
654
|
const end = offset + data.byteLength;
|
|
650
|
-
let buffer = data;
|
|
651
655
|
if (!tx.flag('partial')) {
|
|
652
|
-
buffer = extendBuffer(tx.getSync(inode.data) ?? new Uint8Array(), end);
|
|
656
|
+
const buffer = extendBuffer(tx.getSync(inode.data) ?? new Uint8Array(), end);
|
|
653
657
|
buffer.set(data, offset);
|
|
654
|
-
|
|
658
|
+
tx.setSync(inode.data, buffer);
|
|
659
|
+
}
|
|
660
|
+
else if (offset <= inode.size) {
|
|
661
|
+
tx.setSync(inode.data, data, offset);
|
|
662
|
+
}
|
|
663
|
+
else {
|
|
664
|
+
// Writes past the end of the file also zero the bytes before them
|
|
665
|
+
const extended = new Uint8Array(end - inode.size);
|
|
666
|
+
extended.set(data, offset - inode.size);
|
|
667
|
+
tx.setSync(inode.data, extended, inode.size);
|
|
655
668
|
}
|
|
656
|
-
tx.setSync(inode.data, buffer, offset);
|
|
657
669
|
// Update the inode in the same transaction, so data and metadata can not get out of sync (#286)
|
|
658
670
|
inode.update({ size: Math.max(inode.size, end), mtimeMs: Date.now() });
|
|
659
671
|
tx.setSync(inode.ino, inode);
|
|
@@ -917,6 +929,11 @@ export class StoreFS extends FileSystem {
|
|
|
917
929
|
_lastID;
|
|
918
930
|
/** Allocates a new ID and adds the ID/path */
|
|
919
931
|
allocNew(path) {
|
|
932
|
+
if (this.store.allocId) {
|
|
933
|
+
const ids = this.store.allocId();
|
|
934
|
+
this._add(ids[0], path);
|
|
935
|
+
return ids;
|
|
936
|
+
}
|
|
920
937
|
// @todo use `Iterator` helper #296
|
|
921
938
|
this._lastID ||= Array.from(this._paths.keys()).reduce((max, ino) => Math.max(max, ino), 0);
|
|
922
939
|
this._lastID += 2;
|
|
@@ -924,7 +941,7 @@ export class StoreFS extends FileSystem {
|
|
|
924
941
|
if (id > size_max)
|
|
925
942
|
throw err(withErrno('ENOSPC', 'No IDs available'));
|
|
926
943
|
this._add(id, path);
|
|
927
|
-
return id;
|
|
944
|
+
return [id, id + 1];
|
|
928
945
|
}
|
|
929
946
|
/**
|
|
930
947
|
* Commits a new file (well, a FILE or a DIRECTORY) to the file system with `mode`.
|
|
@@ -949,11 +966,11 @@ export class StoreFS extends FileSystem {
|
|
|
949
966
|
const listing = decodeDirListing((await tx.get(parent.data)) ?? _throw(withErrno('ENOENT')));
|
|
950
967
|
if (listing[fname])
|
|
951
968
|
throw withErrno('EEXIST');
|
|
952
|
-
const
|
|
969
|
+
const [ino, dataId] = this.allocNew(path);
|
|
953
970
|
const inode = new Inode({
|
|
954
971
|
...options,
|
|
955
|
-
ino
|
|
956
|
-
data:
|
|
972
|
+
ino,
|
|
973
|
+
data: dataId,
|
|
957
974
|
size: data.byteLength,
|
|
958
975
|
nlink: 1,
|
|
959
976
|
});
|
|
@@ -998,11 +1015,11 @@ export class StoreFS extends FileSystem {
|
|
|
998
1015
|
const listing = decodeDirListing(tx.getSync(parent.data) ?? _throw(withErrno('ENOENT')));
|
|
999
1016
|
if (listing[fname])
|
|
1000
1017
|
throw withErrno('EEXIST');
|
|
1001
|
-
const
|
|
1018
|
+
const [ino, dataId] = this.allocNew(path);
|
|
1002
1019
|
const inode = new Inode({
|
|
1003
1020
|
...options,
|
|
1004
|
-
ino
|
|
1005
|
-
data:
|
|
1021
|
+
ino,
|
|
1022
|
+
data: dataId,
|
|
1006
1023
|
size: data.byteLength,
|
|
1007
1024
|
nlink: 1,
|
|
1008
1025
|
});
|
|
@@ -6,9 +6,9 @@ import { AsyncTransaction, SyncTransaction } from './store.js';
|
|
|
6
6
|
*/
|
|
7
7
|
export interface SyncMapStore extends Store {
|
|
8
8
|
keys(): Iterable<number>;
|
|
9
|
-
get(id: number): Uint8Array | undefined;
|
|
10
|
-
getAsync?(id: number): Promise<Uint8Array | undefined>;
|
|
11
|
-
set(id: number, data: Uint8Array): void;
|
|
9
|
+
get(id: number, offset?: number, end?: number): Uint8Array | undefined;
|
|
10
|
+
getAsync?(id: number, offset?: number, end?: number): Promise<Uint8Array | undefined>;
|
|
11
|
+
set(id: number, data: Uint8Array, offset?: number): void;
|
|
12
12
|
delete(id: number): void;
|
|
13
13
|
}
|
|
14
14
|
/**
|
|
@@ -19,9 +19,9 @@ export interface SyncMapStore extends Store {
|
|
|
19
19
|
export declare class SyncMapTransaction extends SyncTransaction<SyncMapStore> {
|
|
20
20
|
readonly store: SyncMapStore;
|
|
21
21
|
keys(): Promise<Iterable<number>>;
|
|
22
|
-
get(id: number): Promise<Uint8Array | undefined>;
|
|
23
|
-
getSync(id: number): Uint8Array | undefined;
|
|
24
|
-
setSync(id: number, data: Uint8Array): void;
|
|
22
|
+
get(id: number, offset?: number, end?: number): Promise<Uint8Array | undefined>;
|
|
23
|
+
getSync(id: number, offset?: number, end?: number): Uint8Array | undefined;
|
|
24
|
+
setSync(id: number, data: Uint8Array, offset?: number): void;
|
|
25
25
|
removeSync(id: number): void;
|
|
26
26
|
}
|
|
27
27
|
/**
|
|
@@ -9,14 +9,14 @@ export class SyncMapTransaction extends SyncTransaction {
|
|
|
9
9
|
async keys() {
|
|
10
10
|
return this.store.keys();
|
|
11
11
|
}
|
|
12
|
-
async get(id) {
|
|
13
|
-
return await (this.store.getAsync?.(id) ?? this.store.get(id));
|
|
12
|
+
async get(id, offset, end) {
|
|
13
|
+
return await (this.store.getAsync?.(id, offset, end) ?? this.store.get(id, offset, end));
|
|
14
14
|
}
|
|
15
|
-
getSync(id) {
|
|
16
|
-
return this.store.get(id);
|
|
15
|
+
getSync(id, offset, end) {
|
|
16
|
+
return this.store.get(id, offset, end);
|
|
17
17
|
}
|
|
18
|
-
setSync(id, data) {
|
|
19
|
-
this.store.set(id, data);
|
|
18
|
+
setSync(id, data, offset) {
|
|
19
|
+
this.store.set(id, data, offset);
|
|
20
20
|
}
|
|
21
21
|
removeSync(id) {
|
|
22
22
|
this.store.delete(id);
|
|
@@ -7,7 +7,7 @@ import type { StoreFS } from './fs.js';
|
|
|
7
7
|
* @category Stores and Transactions
|
|
8
8
|
*/
|
|
9
9
|
export type StoreFlag =
|
|
10
|
-
/** The store supports
|
|
10
|
+
/** The store supports reading ranges and writing at offsets */
|
|
11
11
|
'partial';
|
|
12
12
|
/**
|
|
13
13
|
* Represents a key-value store.
|
|
@@ -48,6 +48,10 @@ export interface Store {
|
|
|
48
48
|
* Usage information for the store
|
|
49
49
|
*/
|
|
50
50
|
usage?(): UsageInfo;
|
|
51
|
+
/**
|
|
52
|
+
* Allocate IDs for a new inode + data
|
|
53
|
+
*/
|
|
54
|
+
allocId?(): [ino: number, data: number];
|
|
51
55
|
/**
|
|
52
56
|
* @internal @hidden
|
|
53
57
|
*/
|
|
@@ -77,17 +81,17 @@ export declare abstract class Transaction<T extends Store = Store> {
|
|
|
77
81
|
*/
|
|
78
82
|
abstract getSync(id: number, offset: number, end?: number): Uint8Array | undefined;
|
|
79
83
|
/**
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
84
|
+
* Stores data under an id.
|
|
85
|
+
* Without `offset`, replaces any existing data.
|
|
86
|
+
* With `offset`, writes the data at `offset` and keeps the rest, which partial stores must support.
|
|
83
87
|
*/
|
|
84
|
-
abstract set(id: number, data: Uint8Array, offset
|
|
88
|
+
abstract set(id: number, data: Uint8Array, offset?: number): Promise<void>;
|
|
85
89
|
/**
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
90
|
+
* Stores data under an id.
|
|
91
|
+
* Without `offset`, replaces any existing data.
|
|
92
|
+
* With `offset`, writes the data at `offset` and keeps the rest, which partial stores must support.
|
|
89
93
|
*/
|
|
90
|
-
abstract setSync(id: number, data: Uint8Array, offset
|
|
94
|
+
abstract setSync(id: number, data: Uint8Array, offset?: number): void;
|
|
91
95
|
/**
|
|
92
96
|
* Deletes the data at `ino`.
|
|
93
97
|
* @param id The key to delete from the store.
|
|
@@ -105,7 +109,7 @@ export declare abstract class Transaction<T extends Store = Store> {
|
|
|
105
109
|
*/
|
|
106
110
|
export declare abstract class SyncTransaction<T extends Store = Store> extends Transaction<T> {
|
|
107
111
|
get(id: number, offset: number, end?: number): Promise<Uint8Array | undefined>;
|
|
108
|
-
set(id: number, data: Uint8Array, offset
|
|
112
|
+
set(id: number, data: Uint8Array, offset?: number): Promise<void>;
|
|
109
113
|
remove(id: number): Promise<void>;
|
|
110
114
|
}
|
|
111
115
|
/**
|
|
@@ -136,7 +140,7 @@ export declare abstract class AsyncTransaction<T extends AsyncStore = AsyncStore
|
|
|
136
140
|
size: number;
|
|
137
141
|
}): Resource<number> | undefined;
|
|
138
142
|
getSync(id: number, offset: number, end?: number): Uint8Array | undefined;
|
|
139
|
-
setSync(id: number, data: Uint8Array, offset
|
|
143
|
+
setSync(id: number, data: Uint8Array, offset?: number): void;
|
|
140
144
|
removeSync(id: number): void;
|
|
141
145
|
}
|
|
142
146
|
/**
|
|
@@ -160,7 +164,7 @@ export declare class WrappedTransaction<T extends Store = Store> {
|
|
|
160
164
|
*/
|
|
161
165
|
protected originalData: Map<number, {
|
|
162
166
|
data?: Uint8Array;
|
|
163
|
-
offset
|
|
167
|
+
offset?: number;
|
|
164
168
|
}[]>;
|
|
165
169
|
/**
|
|
166
170
|
* List of keys modified in this transaction, if any.
|
|
@@ -189,9 +193,9 @@ export declare class WrappedTransaction<T extends Store = Store> {
|
|
|
189
193
|
/**
|
|
190
194
|
* Marks an id as modified, and stashes its value if it has not been stashed already.
|
|
191
195
|
*/
|
|
192
|
-
protected markModified(id: number, offset
|
|
196
|
+
protected markModified(id: number, offset?: number, length?: number): Promise<void>;
|
|
193
197
|
/**
|
|
194
198
|
* Marks an id as modified, and stashes its value if it has not been stashed already.
|
|
195
199
|
*/
|
|
196
|
-
protected markModifiedSync(id: number, offset
|
|
200
|
+
protected markModifiedSync(id: number, offset?: number, length?: number): void;
|
|
197
201
|
}
|
|
@@ -126,22 +126,22 @@ export class WrappedTransaction {
|
|
|
126
126
|
this.stash(id);
|
|
127
127
|
return data;
|
|
128
128
|
}
|
|
129
|
-
async set(id, view, offset
|
|
129
|
+
async set(id, view, offset) {
|
|
130
130
|
await this.markModified(id, offset, view.byteLength);
|
|
131
131
|
const buffer = view instanceof Uint8Array ? view : new Uint8Array(view.buffer, view.byteOffset, view.byteLength);
|
|
132
132
|
await this.raw.set(id, buffer, offset);
|
|
133
133
|
}
|
|
134
|
-
setSync(id, view, offset
|
|
134
|
+
setSync(id, view, offset) {
|
|
135
135
|
this.markModifiedSync(id, offset, view.byteLength);
|
|
136
136
|
const buffer = view instanceof Uint8Array ? view : new Uint8Array(view.buffer, view.byteOffset, view.byteLength);
|
|
137
137
|
this.raw.setSync(id, buffer, offset);
|
|
138
138
|
}
|
|
139
139
|
async remove(id) {
|
|
140
|
-
await this.markModified(id
|
|
140
|
+
await this.markModified(id);
|
|
141
141
|
await this.raw.remove(id);
|
|
142
142
|
}
|
|
143
143
|
removeSync(id) {
|
|
144
|
-
this.markModifiedSync(id
|
|
144
|
+
this.markModifiedSync(id);
|
|
145
145
|
this.raw.removeSync(id);
|
|
146
146
|
}
|
|
147
147
|
commit() {
|
|
@@ -201,19 +201,17 @@ export class WrappedTransaction {
|
|
|
201
201
|
* prevent needless `get` requests if the program modifies the data later
|
|
202
202
|
* on during the transaction.
|
|
203
203
|
*/
|
|
204
|
-
stash(id, data, offset
|
|
205
|
-
|
|
206
|
-
this.originalData.set(id, []);
|
|
207
|
-
this.originalData.get(id).push({ data, offset });
|
|
204
|
+
stash(id, data, offset) {
|
|
205
|
+
this.originalData.getOrInsertComputed(id, () => []).push({ data, offset });
|
|
208
206
|
}
|
|
209
207
|
/**
|
|
210
208
|
* Marks an id as modified, and stashes its value if it has not been stashed already.
|
|
211
209
|
*/
|
|
212
210
|
async markModified(id, offset, length) {
|
|
213
211
|
this.modifiedKeys.add(id);
|
|
214
|
-
const end = length ? offset + length : undefined;
|
|
212
|
+
const end = offset !== undefined && length ? offset + length : undefined;
|
|
215
213
|
try {
|
|
216
|
-
this.stash(id, await this.raw.get(id, offset, end), offset);
|
|
214
|
+
this.stash(id, await this.raw.get(id, offset ?? 0, end), offset);
|
|
217
215
|
}
|
|
218
216
|
catch (e) {
|
|
219
217
|
if (!(this.raw instanceof AsyncTransaction))
|
|
@@ -227,7 +225,7 @@ export class WrappedTransaction {
|
|
|
227
225
|
const resource = tx._cached(id);
|
|
228
226
|
if (!resource)
|
|
229
227
|
throw e;
|
|
230
|
-
for (const range of resource.cached(offset, end ?? offset)) {
|
|
228
|
+
for (const range of resource.cached(offset ?? 0, end ?? offset ?? 0)) {
|
|
231
229
|
this.stash(id, await this.raw.get(id, range.start, range.end), range.start);
|
|
232
230
|
}
|
|
233
231
|
}
|
|
@@ -237,9 +235,9 @@ export class WrappedTransaction {
|
|
|
237
235
|
*/
|
|
238
236
|
markModifiedSync(id, offset, length) {
|
|
239
237
|
this.modifiedKeys.add(id);
|
|
240
|
-
const end = length ? offset + length : undefined;
|
|
238
|
+
const end = offset !== undefined && length ? offset + length : undefined;
|
|
241
239
|
try {
|
|
242
|
-
this.stash(id, this.raw.getSync(id, offset, end), offset);
|
|
240
|
+
this.stash(id, this.raw.getSync(id, offset ?? 0, end), offset);
|
|
243
241
|
}
|
|
244
242
|
catch (e) {
|
|
245
243
|
if (!(this.raw instanceof AsyncTransaction))
|
|
@@ -253,7 +251,7 @@ export class WrappedTransaction {
|
|
|
253
251
|
const resource = tx._cached(id);
|
|
254
252
|
if (!resource)
|
|
255
253
|
throw e;
|
|
256
|
-
for (const range of resource.cached(offset, end ?? offset)) {
|
|
254
|
+
for (const range of resource.cached(offset ?? 0, end ?? offset ?? 0)) {
|
|
257
255
|
this.stash(id, this.raw.getSync(id, range.start, range.end), range.start);
|
|
258
256
|
}
|
|
259
257
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -72,6 +72,12 @@ export interface Configuration<T extends ConfigMounts> extends SharedConfig {
|
|
|
72
72
|
* @default false
|
|
73
73
|
*/
|
|
74
74
|
disableAccessChecks: boolean;
|
|
75
|
+
/**
|
|
76
|
+
* If true, path resolution always walks each directory in a path, checking search permission on each.
|
|
77
|
+
* This is more correct but can be much slower.
|
|
78
|
+
* @default false
|
|
79
|
+
*/
|
|
80
|
+
resolveFullWalk: boolean;
|
|
75
81
|
/**
|
|
76
82
|
* If true, files will only sync to the file system when closed.
|
|
77
83
|
* This overrides `disableUpdateOnRead`
|
|
@@ -79,6 +85,7 @@ export interface Configuration<T extends ConfigMounts> extends SharedConfig {
|
|
|
79
85
|
* This can increase performance.
|
|
80
86
|
* @experimental
|
|
81
87
|
* @default false
|
|
88
|
+
* @deprecated Replaced by the `Sync` inode flag in v2.0.0
|
|
82
89
|
*/
|
|
83
90
|
onlySyncOnClose: boolean;
|
|
84
91
|
/**
|
package/dist/config.js
CHANGED
|
@@ -6,7 +6,7 @@ import { DeviceFS } from './internal/devices.js';
|
|
|
6
6
|
import { FileSystem } from './internal/filesystem.js';
|
|
7
7
|
import { exists, mkdir, stat } from './node/promises.js';
|
|
8
8
|
import { existsSync, mkdirSync, statSync } from './node/sync.js';
|
|
9
|
-
import {
|
|
9
|
+
import { _setVFSConfig } from './vfs/config.js';
|
|
10
10
|
import { mount, umount } from './vfs/shared.js';
|
|
11
11
|
/**
|
|
12
12
|
* Update the configuration of a file system.
|
|
@@ -209,7 +209,10 @@ export async function configure(configuration) {
|
|
|
209
209
|
uid: configuration.uid || 0,
|
|
210
210
|
gid: configuration.gid || 0,
|
|
211
211
|
}));
|
|
212
|
-
|
|
212
|
+
_setVFSConfig({
|
|
213
|
+
checkAccess: !configuration.disableAccessChecks,
|
|
214
|
+
resolveFullWalk: !!configuration.resolveFullWalk,
|
|
215
|
+
});
|
|
213
216
|
if (configuration.log)
|
|
214
217
|
log.configure(configuration.log);
|
|
215
218
|
if (configuration.mounts) {
|
|
@@ -257,7 +260,10 @@ export function configureSync(configuration) {
|
|
|
257
260
|
uid: configuration.uid || 0,
|
|
258
261
|
gid: configuration.gid || 0,
|
|
259
262
|
}));
|
|
260
|
-
|
|
263
|
+
_setVFSConfig({
|
|
264
|
+
checkAccess: !configuration.disableAccessChecks,
|
|
265
|
+
resolveFullWalk: !!configuration.resolveFullWalk,
|
|
266
|
+
});
|
|
261
267
|
if (configuration.log)
|
|
262
268
|
log.configure(configuration.log);
|
|
263
269
|
if (configuration.mounts) {
|
|
@@ -28,6 +28,8 @@ export interface FSContext {
|
|
|
28
28
|
pwd: string;
|
|
29
29
|
/** The credentials of the context, used for access checks */
|
|
30
30
|
readonly credentials: Credentials;
|
|
31
|
+
/** Permission bits cleared from the mode of newly created files and directories */
|
|
32
|
+
umask: number;
|
|
31
33
|
/** A map of open file descriptors to their handles */
|
|
32
34
|
readonly descriptors: Map<number, Handle>;
|
|
33
35
|
/** The parent context, if any. */
|
|
@@ -66,6 +68,7 @@ export interface ContextInit {
|
|
|
66
68
|
root?: string;
|
|
67
69
|
pwd?: string;
|
|
68
70
|
credentials?: CredentialsInit;
|
|
71
|
+
umask?: number;
|
|
69
72
|
mounts?: Record<string, FileSystem>;
|
|
70
73
|
}
|
|
71
74
|
/**
|
|
@@ -75,6 +78,8 @@ export interface ContextInit {
|
|
|
75
78
|
*/
|
|
76
79
|
export declare const defaultContext: FSContext;
|
|
77
80
|
export declare function contextOf($: unknown): FSContext;
|
|
81
|
+
/** Clears the permission bits of `mode` that are set in the context's umask. */
|
|
82
|
+
export declare function applyUmask($: unknown, mode: number): number;
|
|
78
83
|
/**
|
|
79
84
|
* Create a blank FS Context
|
|
80
85
|
* @internal
|
|
@@ -26,6 +26,7 @@ export const defaultContext = {
|
|
|
26
26
|
root: '/',
|
|
27
27
|
pwd: '/',
|
|
28
28
|
credentials: createCredentials({ uid: 0, gid: 0 }),
|
|
29
|
+
umask: 0,
|
|
29
30
|
descriptors: new Map(),
|
|
30
31
|
parent: null,
|
|
31
32
|
children: [],
|
|
@@ -34,6 +35,10 @@ export const defaultContext = {
|
|
|
34
35
|
export function contextOf($) {
|
|
35
36
|
return isContext($) ? $ : defaultContext;
|
|
36
37
|
}
|
|
38
|
+
/** Clears the permission bits of `mode` that are set in the context's umask. */
|
|
39
|
+
export function applyUmask($, mode) {
|
|
40
|
+
return mode & ~(contextOf($).umask & 0o777);
|
|
41
|
+
}
|
|
37
42
|
// 1 is reserved for the global/default context
|
|
38
43
|
let _nextId = 2;
|
|
39
44
|
/**
|
|
@@ -46,13 +51,14 @@ let _nextId = 2;
|
|
|
46
51
|
*/
|
|
47
52
|
export function createChildContext(parent, init = {}) {
|
|
48
53
|
assertContext(parent);
|
|
49
|
-
const { root = parent.root, pwd = parent.pwd, credentials = structuredClone(parent.credentials), mounts } = init;
|
|
54
|
+
const { root = parent.root, pwd = parent.pwd, credentials = structuredClone(parent.credentials), umask = parent.umask, mounts } = init;
|
|
50
55
|
const ctx = {
|
|
51
56
|
[kIsContext]: true,
|
|
52
57
|
id: _nextId++,
|
|
53
58
|
root,
|
|
54
59
|
pwd,
|
|
55
60
|
credentials: createCredentials(credentials),
|
|
61
|
+
umask,
|
|
56
62
|
descriptors: new Map(),
|
|
57
63
|
parent: parent,
|
|
58
64
|
children: [],
|
package/dist/internal/error.d.ts
CHANGED
|
@@ -25,3 +25,8 @@ export declare function wrap<const FS, const Prop extends keyof FS & string>(fs:
|
|
|
25
25
|
* Wraps an `fs` so that thrown errors aren't empty
|
|
26
26
|
*/
|
|
27
27
|
export declare function withExceptionContext<const FS extends FileSystem>(fs: FS, context: ExceptionExtra): FS;
|
|
28
|
+
/**
|
|
29
|
+
* @internal
|
|
30
|
+
* Returns the file system wrapped by {@link withExceptionContext}, or `fs` if it is not wrapped
|
|
31
|
+
*/
|
|
32
|
+
export declare function withoutExceptionContext<const FS extends FileSystem>(fs: FS): FS;
|
package/dist/internal/error.js
CHANGED
|
@@ -26,6 +26,7 @@ export function wrap(fs, prop, path, dest) {
|
|
|
26
26
|
}
|
|
27
27
|
};
|
|
28
28
|
}
|
|
29
|
+
const kTarget = Symbol('withExceptionContext:target');
|
|
29
30
|
/**
|
|
30
31
|
* @internal
|
|
31
32
|
* Wraps an `fs` so that thrown errors aren't empty
|
|
@@ -33,6 +34,8 @@ export function wrap(fs, prop, path, dest) {
|
|
|
33
34
|
export function withExceptionContext(fs, context) {
|
|
34
35
|
return new Proxy(fs, {
|
|
35
36
|
get(target, prop) {
|
|
37
|
+
if (prop === kTarget)
|
|
38
|
+
return target;
|
|
36
39
|
const value = Reflect.get(target, prop);
|
|
37
40
|
if (typeof value != 'function')
|
|
38
41
|
return value;
|
|
@@ -58,3 +61,10 @@ export function withExceptionContext(fs, context) {
|
|
|
58
61
|
},
|
|
59
62
|
});
|
|
60
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* @internal
|
|
66
|
+
* Returns the file system wrapped by {@link withExceptionContext}, or `fs` if it is not wrapped
|
|
67
|
+
*/
|
|
68
|
+
export function withoutExceptionContext(fs) {
|
|
69
|
+
return fs[kTarget] ?? fs;
|
|
70
|
+
}
|
package/dist/internal/inode.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ExceptionExtra } from 'kerium';
|
|
1
2
|
import { BufferView } from 'utilium/buffer';
|
|
2
3
|
import { Stats } from '../node/stats.js';
|
|
3
4
|
import { type V_Context } from './contexts.js';
|
|
@@ -234,10 +235,34 @@ export declare function isFIFO(metadata: {
|
|
|
234
235
|
* @internal
|
|
235
236
|
*/
|
|
236
237
|
export declare function hasAccess($: V_Context, inode: Pick<InodeLike, 'mode' | 'uid' | 'gid'>, access: number): boolean;
|
|
238
|
+
/**
|
|
239
|
+
* Throws if a given user may not search the directory `dir`
|
|
240
|
+
* @internal
|
|
241
|
+
*/
|
|
242
|
+
export declare function assertSearchable($: V_Context, dir: Pick<InodeLike, 'mode' | 'uid' | 'gid'>, extra?: ExceptionExtra): void;
|
|
243
|
+
/**
|
|
244
|
+
* Throws if a given user may not remove `entry` from the directory `parent`
|
|
245
|
+
* @internal
|
|
246
|
+
*/
|
|
247
|
+
export declare function assertRemovable($: V_Context, parent: Pick<InodeLike, 'mode' | 'uid' | 'gid'>, entry: Pick<InodeLike, 'uid'>, extra?: ExceptionExtra): void;
|
|
248
|
+
/**
|
|
249
|
+
* Throws if a given user is neither root nor the owner of `inode`
|
|
250
|
+
* @internal
|
|
251
|
+
*/
|
|
252
|
+
export declare function assertOwner($: V_Context, inode: Pick<InodeLike, 'uid'>, extra?: ExceptionExtra): void;
|
|
253
|
+
/**
|
|
254
|
+
* Throws if a given user may not change the owner of `inode` to `uid` and `gid`, where `-1` leaves an ID unchanged
|
|
255
|
+
* @internal
|
|
256
|
+
*/
|
|
257
|
+
export declare function assertCanChown($: V_Context, inode: Pick<InodeLike, 'uid' | 'gid'>, uid: number, gid: number, extra?: ExceptionExtra): void;
|
|
237
258
|
/**
|
|
238
259
|
* @hidden @internal
|
|
239
260
|
*
|
|
240
261
|
* @todo https://github.com/nodejs/node/pull/58836 support -1 for `*chown`
|
|
241
262
|
*/
|
|
242
263
|
export declare function _chown(stats: Partial<InodeLike>, uid: number, gid: number): boolean;
|
|
264
|
+
/**
|
|
265
|
+
* @hidden @internal
|
|
266
|
+
*/
|
|
267
|
+
export declare function _chmod($: V_Context, inode: Pick<InodeLike, 'mode' | 'gid'>, mode: number): void;
|
|
243
268
|
export {};
|
package/dist/internal/inode.js
CHANGED
|
@@ -33,7 +33,7 @@ var __runInitializers = (this && this.__runInitializers) || function (thisArg, i
|
|
|
33
33
|
return useValue ? value : void 0;
|
|
34
34
|
};
|
|
35
35
|
// SPDX-License-Identifier: LGPL-3.0-or-later
|
|
36
|
-
import { withErrno } from 'kerium';
|
|
36
|
+
import { UV, withErrno } from 'kerium';
|
|
37
37
|
import { crit, warn } from 'kerium/log';
|
|
38
38
|
import { sizeof } from 'memium';
|
|
39
39
|
import { $from, field, struct, types as t } from 'memium/decorators';
|
|
@@ -41,6 +41,7 @@ import { decodeUTF8, encodeUTF8, pick } from 'utilium';
|
|
|
41
41
|
import { BufferView } from 'utilium/buffer';
|
|
42
42
|
import * as c from '../constants.js';
|
|
43
43
|
import { Stats } from '../node/stats.js';
|
|
44
|
+
import { checkAccess } from '../vfs/config.js';
|
|
44
45
|
import { contextOf } from './contexts.js';
|
|
45
46
|
/**
|
|
46
47
|
* Root inode
|
|
@@ -654,10 +655,10 @@ export function hasAccess($, inode, access) {
|
|
|
654
655
|
const { credentials } = contextOf($);
|
|
655
656
|
if (isSymbolicLink(inode))
|
|
656
657
|
return true;
|
|
657
|
-
if (credentials.euid === 0
|
|
658
|
+
if (credentials.euid === 0)
|
|
658
659
|
return !(access & c.X_OK) || isDirectory(inode) || !!(inode.mode & (c.S_IXUSR | c.S_IXGRP | c.S_IXOTH));
|
|
659
660
|
let perm = 0;
|
|
660
|
-
if (credentials.
|
|
661
|
+
if (credentials.euid === inode.uid) {
|
|
661
662
|
if (inode.mode & c.S_IRUSR)
|
|
662
663
|
perm |= c.R_OK;
|
|
663
664
|
if (inode.mode & c.S_IWUSR)
|
|
@@ -665,7 +666,7 @@ export function hasAccess($, inode, access) {
|
|
|
665
666
|
if (inode.mode & c.S_IXUSR)
|
|
666
667
|
perm |= c.X_OK;
|
|
667
668
|
}
|
|
668
|
-
if (credentials.
|
|
669
|
+
else if (credentials.egid === inode.gid || credentials.groups.includes(Number(inode.gid))) {
|
|
669
670
|
if (inode.mode & c.S_IRGRP)
|
|
670
671
|
perm |= c.R_OK;
|
|
671
672
|
if (inode.mode & c.S_IWGRP)
|
|
@@ -673,14 +674,64 @@ export function hasAccess($, inode, access) {
|
|
|
673
674
|
if (inode.mode & c.S_IXGRP)
|
|
674
675
|
perm |= c.X_OK;
|
|
675
676
|
}
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
677
|
+
else {
|
|
678
|
+
if (inode.mode & c.S_IROTH)
|
|
679
|
+
perm |= c.R_OK;
|
|
680
|
+
if (inode.mode & c.S_IWOTH)
|
|
681
|
+
perm |= c.W_OK;
|
|
682
|
+
if (inode.mode & c.S_IXOTH)
|
|
683
|
+
perm |= c.X_OK;
|
|
684
|
+
}
|
|
682
685
|
return (perm & access) === access;
|
|
683
686
|
}
|
|
687
|
+
/**
|
|
688
|
+
* Throws if a given user may not search the directory `dir`
|
|
689
|
+
* @internal
|
|
690
|
+
*/
|
|
691
|
+
export function assertSearchable($, dir, extra) {
|
|
692
|
+
if (checkAccess && !hasAccess($, dir, c.X_OK))
|
|
693
|
+
throw UV('EACCES', extra);
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Throws if a given user may not remove `entry` from the directory `parent`
|
|
697
|
+
* @internal
|
|
698
|
+
*/
|
|
699
|
+
export function assertRemovable($, parent, entry, extra) {
|
|
700
|
+
if (!checkAccess)
|
|
701
|
+
return;
|
|
702
|
+
if (!hasAccess($, parent, c.W_OK | c.X_OK))
|
|
703
|
+
throw UV('EACCES', extra);
|
|
704
|
+
const { euid } = contextOf($).credentials;
|
|
705
|
+
if (parent.mode & c.S_ISVTX && euid !== 0 && euid !== entry.uid && euid !== parent.uid)
|
|
706
|
+
throw UV('EPERM', extra);
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Throws if a given user is neither root nor the owner of `inode`
|
|
710
|
+
* @internal
|
|
711
|
+
*/
|
|
712
|
+
export function assertOwner($, inode, extra) {
|
|
713
|
+
if (!checkAccess)
|
|
714
|
+
return;
|
|
715
|
+
const { euid } = contextOf($).credentials;
|
|
716
|
+
if (euid !== 0 && euid !== inode.uid)
|
|
717
|
+
throw UV('EPERM', extra);
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* Throws if a given user may not change the owner of `inode` to `uid` and `gid`, where `-1` leaves an ID unchanged
|
|
721
|
+
* @internal
|
|
722
|
+
*/
|
|
723
|
+
export function assertCanChown($, inode, uid, gid, extra) {
|
|
724
|
+
if (!checkAccess)
|
|
725
|
+
return;
|
|
726
|
+
const { euid, egid, groups } = contextOf($).credentials;
|
|
727
|
+
if (euid === 0)
|
|
728
|
+
return;
|
|
729
|
+
const isOwner = euid === inode.uid;
|
|
730
|
+
if (uid !== -1 && !(isOwner && uid === inode.uid))
|
|
731
|
+
throw UV('EPERM', extra);
|
|
732
|
+
if (gid !== -1 && !(isOwner && (gid === inode.gid || gid === egid || groups.includes(gid))))
|
|
733
|
+
throw UV('EPERM', extra);
|
|
734
|
+
}
|
|
684
735
|
/**
|
|
685
736
|
* @hidden @internal
|
|
686
737
|
*
|
|
@@ -696,5 +747,19 @@ export function _chown(stats, uid, gid) {
|
|
|
696
747
|
stats.gid = gid;
|
|
697
748
|
else
|
|
698
749
|
valid = false;
|
|
750
|
+
if (stats.mode !== undefined && !isDirectory({ mode: stats.mode })) {
|
|
751
|
+
stats.mode &= ~c.S_ISUID;
|
|
752
|
+
if (stats.mode & c.S_IXGRP)
|
|
753
|
+
stats.mode &= ~c.S_ISGID;
|
|
754
|
+
}
|
|
699
755
|
return valid;
|
|
700
756
|
}
|
|
757
|
+
/**
|
|
758
|
+
* @hidden @internal
|
|
759
|
+
*/
|
|
760
|
+
export function _chmod($, inode, mode) {
|
|
761
|
+
const { euid, egid, groups } = contextOf($).credentials;
|
|
762
|
+
if (checkAccess && euid !== 0 && egid !== inode.gid && !groups.includes(inode.gid))
|
|
763
|
+
mode &= ~c.S_ISGID;
|
|
764
|
+
inode.mode = (inode.mode & (mode > c.S_IFMT ? ~c.S_IFMT : c.S_IFMT)) | mode;
|
|
765
|
+
}
|