@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.
Files changed (70) hide show
  1. package/README.md +5 -0
  2. package/dist/backends/index.d.ts +3 -1
  3. package/dist/backends/index.js +3 -1
  4. package/dist/backends/single_buffer/backend.d.ts +42 -0
  5. package/dist/backends/single_buffer/backend.js +204 -0
  6. package/dist/backends/{single_buffer.d.ts → single_buffer/v1.d.ts} +24 -34
  7. package/dist/backends/{single_buffer.js → single_buffer/v1.js} +10 -26
  8. package/dist/backends/single_buffer/v2.d.ts +257 -0
  9. package/dist/backends/single_buffer/v2.js +1283 -0
  10. package/dist/backends/store/fs.d.ts +1 -1
  11. package/dist/backends/store/fs.js +35 -18
  12. package/dist/backends/store/map.d.ts +6 -6
  13. package/dist/backends/store/map.js +6 -6
  14. package/dist/backends/store/store.d.ts +18 -14
  15. package/dist/backends/store/store.js +12 -14
  16. package/dist/config.d.ts +7 -0
  17. package/dist/config.js +9 -3
  18. package/dist/internal/contexts.d.ts +5 -0
  19. package/dist/internal/contexts.js +7 -1
  20. package/dist/internal/error.d.ts +5 -0
  21. package/dist/internal/error.js +10 -0
  22. package/dist/internal/inode.d.ts +25 -0
  23. package/dist/internal/inode.js +75 -10
  24. package/dist/mixins/async.js +47 -30
  25. package/dist/node/promises.js +23 -124
  26. package/dist/node/stats.d.ts +1 -1
  27. package/dist/node/stats.js +4 -1
  28. package/dist/node/sync.js +20 -51
  29. package/dist/polyfills.js +16 -0
  30. package/dist/vfs/async.d.ts +4 -1
  31. package/dist/vfs/async.js +149 -69
  32. package/dist/vfs/config.d.ts +10 -1
  33. package/dist/vfs/config.js +5 -2
  34. package/dist/vfs/file.js +10 -4
  35. package/dist/vfs/ioctl.js +2 -2
  36. package/dist/vfs/shared.d.ts +2 -0
  37. package/dist/vfs/shared.js +3 -4
  38. package/dist/vfs/sync.d.ts +4 -1
  39. package/dist/vfs/sync.js +142 -64
  40. package/dist/vfs/vcache.d.ts +14 -14
  41. package/dist/vfs/vcache.js +65 -57
  42. package/dist/vfs/watchers.d.ts +1 -0
  43. package/dist/vfs/watchers.js +2 -4
  44. package/dist/vfs/xattr.js +18 -17
  45. package/package.json +3 -4
  46. package/scripts/make-index.js +1 -1
  47. package/tests/backend/single-buffer-rotate.worker.js +1 -1
  48. package/tests/backend/single-buffer-v2.test.ts +342 -0
  49. package/tests/backend/single-buffer-v2.worker.js +51 -0
  50. package/tests/backend/single-buffer.test.ts +6 -6
  51. package/tests/backend/single-buffer.worker.js +1 -0
  52. package/tests/common/async.test.ts +46 -0
  53. package/tests/common/config.test.ts +1 -1
  54. package/tests/common/context.test.ts +30 -0
  55. package/tests/common/file_index.test.ts +1 -1
  56. package/tests/common/mounts.test.ts +12 -0
  57. package/tests/common/vcache.test.ts +13 -0
  58. package/tests/fs/concurrency.test.ts +3 -3
  59. package/tests/fs/links.test.ts +76 -0
  60. package/tests/fs/permissions.test.ts +253 -0
  61. package/tests/fs/stat.test.ts +1 -1
  62. package/tests/fs/streams.test.ts +1 -1
  63. package/tests/fs/watch.test.ts +14 -0
  64. package/tests/fs/xattr.test.ts +10 -0
  65. package/tests/perf/config.json +31 -1
  66. package/tests/perf/single-buffer-random.ts +38 -0
  67. package/tests/perf/single-buffer-read.ts +28 -0
  68. package/tests/perf/single-buffer-write.ts +41 -0
  69. package/tests/setup/single-buffer-v2.ts +10 -0
  70. 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
- if (!this._paths.has(ino))
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
- offset = 0;
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
- offset = 0;
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 id = this.allocNew(path);
969
+ const [ino, dataId] = this.allocNew(path);
953
970
  const inode = new Inode({
954
971
  ...options,
955
- ino: id,
956
- data: id + 1,
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 id = this.allocNew(path);
1018
+ const [ino, dataId] = this.allocNew(path);
1002
1019
  const inode = new Inode({
1003
1020
  ...options,
1004
- ino: id,
1005
- data: id + 1,
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 partial reads and writes */
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
- * Adds the data to the store under an id. Overwrites any existing data.
81
- * @param id The key to add the data under.
82
- * @param data The data to add to the store.
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: number): Promise<void>;
88
+ abstract set(id: number, data: Uint8Array, offset?: number): Promise<void>;
85
89
  /**
86
- * Adds the data to the store under and id.
87
- * @param id The key to add the data under.
88
- * @param data The data to add to the store.
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: number): void;
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: number): Promise<void>;
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: number): void;
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: number;
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: number, length?: number): Promise<void>;
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: number, length?: number): void;
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 = 0) {
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 = 0) {
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, 0, undefined);
140
+ await this.markModified(id);
141
141
  await this.raw.remove(id);
142
142
  }
143
143
  removeSync(id) {
144
- this.markModifiedSync(id, 0, undefined);
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 = 0) {
205
- if (!this.originalData.has(id))
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 { _setAccessChecks } from './vfs/config.js';
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
- _setAccessChecks(!configuration.disableAccessChecks);
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
- _setAccessChecks(!configuration.disableAccessChecks);
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: [],
@@ -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;
@@ -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
+ }
@@ -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 {};
@@ -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 || credentials.egid === 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.uid === inode.uid) {
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.gid === inode.gid || credentials.groups.includes(Number(inode.gid))) {
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
- if (inode.mode & c.S_IROTH)
677
- perm |= c.R_OK;
678
- if (inode.mode & c.S_IWOTH)
679
- perm |= c.W_OK;
680
- if (inode.mode & c.S_IXOTH)
681
- perm |= c.X_OK;
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
+ }