@zenfs/core 2.7.0 → 2.7.2

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 CHANGED
@@ -43,6 +43,9 @@ Your financial support would go a long way toward improving ZenFS and its commun
43
43
 
44
44
  ## Usage
45
45
 
46
+ > [!IMPORTANT]
47
+ > **[Check out the ZenFS docs!](https://zenfs.dev/start/usage/)**
48
+
46
49
  ```js
47
50
  import { fs } from '@zenfs/core'; // You can also use the default export
48
51
 
@@ -52,15 +55,9 @@ const contents = fs.readFileSync('/test.txt', 'utf-8');
52
55
  console.log(contents);
53
56
  ```
54
57
 
55
- #### Using different and/or multiple backends
56
-
57
- A single `InMemory` backend is created by default, mounted on `/`.
58
-
59
- You can configure ZenFS to use a different backend and mount multiple backends. It is strongly recommended to do so using the `configure` function.
60
-
61
- You can use multiple backends by passing an object to `configure` which maps paths to file systems.
62
-
63
- The following example mounts a zip file to `/zip`, in-memory storage to `/tmp`, and IndexedDB to `/home`. Note that `/` has the default in-memory backend.
58
+ A single `InMemory` backend is created by default, mounted on `/`. To use different backends, and to
59
+ mount more than one, configure ZenFS with `configure`. This mounts a zip file to `/mnt/zip`,
60
+ in-memory storage to `/tmp`, and IndexedDB to `/home`:
64
61
 
65
62
  ```js
66
63
  import { configure, InMemory } from '@zenfs/core';
@@ -78,110 +75,16 @@ await configure({
78
75
  });
79
76
  ```
80
77
 
81
- Note that while you aren't required to use absolute paths for the keys of `mounts`, it is a good practice to do so.
82
-
83
- > [!TIP]
84
- > When configuring a mount point, you can pass in
85
- >
86
- > 1. A `Backend` object, if the backend has no required options
87
- > 2. An object that has the options accepted by the backend and a `backend` property which is a `Backend` object
88
- > 3. A `FileSystem` instance
89
-
90
- Here is an example that mounts the `WebStorage` backend from `@zenfs/dom` on `/`:
91
-
92
- ```js
93
- import { configureSingle, fs } from '@zenfs/core';
94
- import { WebStorage } from '@zenfs/dom';
95
-
96
- await configureSingle({ backend: WebStorage });
97
-
98
- if (!fs.existsSync('/test.txt')) {
99
- fs.writeFileSync('/test.txt', 'This will persist across reloads!');
100
- }
101
-
102
- const contents = fs.readFileSync('/test.txt', 'utf-8');
103
- console.log(contents);
104
- ```
105
-
106
- #### FS Promises
107
-
108
- The FS promises API is exposed as `promises`.
109
-
110
- ```js
111
- import { configureSingle } from '@zenfs/core';
112
- import { exists, writeFile } from '@zenfs/core/promises';
113
- import { IndexedDB } from '@zenfs/dom';
114
-
115
- await configureSingle({ backend: IndexedDB });
116
-
117
- const exists = await exists('/myfile.txt');
118
- if (!exists) {
119
- await writeFile('/myfile.txt', 'Lots of persistent data');
120
- }
121
- ```
122
-
123
- > [!NOTE]
124
- > You can import the promises API using:
125
- >
126
- > 1. Exports from `@zenfs/core/promises`
127
- > 2. The `promises` export from `@zenfs/core`
128
- > 3. `fs.promises` on the exported `fs` from `@zenfs/core`.
78
+ The `fs/promises` API is available from `@zenfs/core/promises`, as the `promises` export, or as
79
+ `fs.promises`.
129
80
 
130
- #### Mounting and unmounting, creating backends
131
-
132
- If you would like to create backends without configure (e.g. to do something dynamic at runtime), you may do so by importing the backend and calling `resolveMountConfig` with it.
133
-
134
- You can then mount and unmount the backend instance by using `mount` and `umount`.
135
-
136
- ```js
137
- import { configure, resolveMountConfig, InMemory } from '@zenfs/core';
138
- import { IndexedDB } from '@zenfs/dom';
139
- import { Zip } from '@zenfs/archives';
140
-
141
- await configure({
142
- mounts: {
143
- '/tmp': InMemory,
144
- '/home': IndexedDB,
145
- },
146
- });
147
-
148
- fs.mkdirSync('/mnt/zip', { recursive: true });
149
-
150
- const res = await fetch('mydata.zip');
151
- const zipfs = await resolveMountConfig({ backend: Zip, data: await res.arrayBuffer() });
152
- fs.mount('/mnt/zip', zipfs);
153
-
154
- // do stuff with the mounted zip
155
-
156
- fs.umount('/mnt/zip'); // finished using the zip
157
- ```
158
-
159
- > [!CAUTION]
160
- > Instances of backends follow the _internal_ API. You should never use a backend's methods unless you are extending a backend.
161
-
162
- ### Devices and device files
163
-
164
- ZenFS includes support for device files. These are designed to follow Linux's device file behavior, for consistency and ease of use. Check out the [Devices and Device Drivers](https://zenfs.dev/core/documents/Devices_and_Device_Drivers) documentation for more information.
165
-
166
- ## `node:*` emulation
167
-
168
- ZenFS also includes emulation of some other `node:` modules for various reasons, importable from `@zenfs/core/<name>`:
169
-
170
- - `node:path`
171
- - `node:readline`
172
-
173
- For example:
174
-
175
- ```ts
176
- import * as path from '@zenfs/core/path';
177
- ```
81
+ For the full usage guide, see **[the documentation](https://zenfs.dev/start/usage/)**.
82
+ This includes mounting at runtime, contexts and permissions, devices, and the `node:*`module emulation.
178
83
 
179
84
  ## Bundling
180
85
 
181
86
  ZenFS exports a drop-in for Node's `fs` module, so you can use it for your bundler of preference using the default export.
182
-
183
- > [!IMPORTANT]
184
- > See [COPYING.md](./COPYING.md)
87
+ See [COPYING.md](./COPYING.md) for more info.
185
88
 
186
89
  ## Sponsors
187
90
 
package/dist/config.d.ts CHANGED
@@ -56,6 +56,8 @@ export interface Configuration<T extends ConfigMounts> extends SharedConfig {
56
56
  /**
57
57
  * Whether to automatically add normal Linux devices
58
58
  * @default false
59
+ * @deprecated Use `@zenfs/linux`!
60
+ * @todo [breaking] remove
59
61
  */
60
62
  addDevices: boolean;
61
63
  /**
@@ -97,6 +99,8 @@ export declare function configureSingle<T extends Backend>(configuration: MountC
97
99
  export declare function configureSingleSync<T extends Backend>(configuration: MountConfiguration<T>): void;
98
100
  /**
99
101
  * @category Backends and Configuration
102
+ * @deprecated Use `@zenfs/linux`!
103
+ * @todo [breaking] remove
100
104
  */
101
105
  export declare function addDevice(driver: DeviceDriver, options?: object): Device;
102
106
  /**
package/dist/config.js CHANGED
@@ -189,6 +189,8 @@ function mountWithMkdirSync(path, fs) {
189
189
  }
190
190
  /**
191
191
  * @category Backends and Configuration
192
+ * @deprecated Use `@zenfs/linux`!
193
+ * @todo [breaking] remove
192
194
  */
193
195
  export function addDevice(driver, options) {
194
196
  const devfs = defaultContext.mounts.get('/dev');
package/dist/context.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: LGPL-3.0-or-later
2
2
  import { UV } from 'kerium';
3
3
  import { bindFunctions } from 'utilium';
4
- import { contextOf, createChildContext } from './internal/contexts.js';
4
+ import { contextOf, createChildContext, defaultContext } from './internal/contexts.js';
5
5
  import * as fs from './node/index.js';
6
6
  import * as path from './path.js';
7
7
  import * as xattr from './vfs/xattr.js';
@@ -17,9 +17,9 @@ export const boundContexts = new Map();
17
17
  * @category Contexts
18
18
  */
19
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 });
20
+ const $ = contextOf(this), parent = $.parent || defaultContext;
21
+ if (!fs.statSync.call(parent, parent.root).isDirectory())
22
+ throw UV('ENOTDIR', { syscall: 'chroot', path: parent.root });
23
23
  const ctx = createChildContext($, init);
24
24
  const bound = Object.assign(ctx, {
25
25
  fs: {
@@ -46,7 +46,7 @@ let _nextId = 2;
46
46
  */
47
47
  export function createChildContext(parent, init = {}) {
48
48
  assertContext(parent);
49
- const { root = parent.root, pwd = parent.pwd, credentials = structuredClone(parent.credentials), mounts = Object.fromEntries(parent.mounts), } = init;
49
+ const { root = parent.root, pwd = parent.pwd, credentials = structuredClone(parent.credentials), mounts } = init;
50
50
  const ctx = {
51
51
  [kIsContext]: true,
52
52
  id: _nextId++,
@@ -56,7 +56,7 @@ export function createChildContext(parent, init = {}) {
56
56
  descriptors: new Map(),
57
57
  parent: parent,
58
58
  children: [],
59
- mounts: new Map(Object.entries(mounts)),
59
+ mounts: mounts ? new Map(Object.entries(mounts)) : parent.mounts,
60
60
  };
61
61
  Object.defineProperties(ctx, {
62
62
  id: { configurable: false, writable: false },
@@ -4,10 +4,11 @@ import type { CreationOptions } from './filesystem.js';
4
4
  import { Inode, type InodeLike } from './inode.js';
5
5
  /**
6
6
  * A device
7
- * @todo Maybe add some other device information, like a UUID?
8
7
  * @category Internals
9
8
  * @privateRemarks
10
9
  * UUIDs were considered, however they don't make sense without an easy mechanism for persistence
10
+ * @deprecated Use `@zenfs/linux`!
11
+ * @todo [breaking] remove
11
12
  */
12
13
  export interface Device<TData = any> {
13
14
  /**
@@ -34,6 +35,8 @@ export interface Device<TData = any> {
34
35
  }
35
36
  /**
36
37
  * @category Internals
38
+ * @deprecated Use `@zenfs/linux`!
39
+ * @todo [breaking] remove
37
40
  */
38
41
  export interface DeviceInit<TData = any> {
39
42
  data?: TData;
@@ -45,6 +48,8 @@ export interface DeviceInit<TData = any> {
45
48
  /**
46
49
  * A device driver
47
50
  * @category Internals
51
+ * @deprecated Use `@zenfs/linux`!
52
+ * @todo [breaking] remove
48
53
  */
49
54
  export interface DeviceDriver<TData = any> {
50
55
  /**
@@ -88,6 +93,8 @@ export interface DeviceDriver<TData = any> {
88
93
  /**
89
94
  * A temporary file system that manages and interfaces with devices
90
95
  * @category Internals
96
+ * @deprecated Use `@zenfs/linux`!
97
+ * @todo [breaking] remove
91
98
  */
92
99
  export declare class DeviceFS extends StoreFS<InMemoryStore> {
93
100
  protected readonly devices: Map<string, Device<any>>;
@@ -132,6 +139,8 @@ export declare class DeviceFS extends StoreFS<InMemoryStore> {
132
139
  * - Writes discard data, advancing the file position.
133
140
  * @category Internals
134
141
  * @internal
142
+ * @deprecated Use `@zenfs/linux`!
143
+ * @todo [breaking] remove
135
144
  */
136
145
  export declare const nullDevice: DeviceDriver;
137
146
  /**
@@ -144,6 +153,8 @@ export declare const nullDevice: DeviceDriver;
144
153
  * - Provides basic file metadata, treating it as a character device.
145
154
  * @category Internals
146
155
  * @internal
156
+ * @deprecated Use `@zenfs/linux`!
157
+ * @todo [breaking] remove
147
158
  */
148
159
  export declare const zeroDevice: DeviceDriver;
149
160
  /**
@@ -152,6 +163,8 @@ export declare const zeroDevice: DeviceDriver;
152
163
  * - Writes always fail with ENOSPC (no space left on device).
153
164
  * @category Internals
154
165
  * @internal
166
+ * @deprecated Use `@zenfs/linux`!
167
+ * @todo [breaking] remove
155
168
  */
156
169
  export declare const fullDevice: DeviceDriver;
157
170
  /**
@@ -160,12 +173,16 @@ export declare const fullDevice: DeviceDriver;
160
173
  * - Writes discard data, advancing the file position.
161
174
  * @category Internals
162
175
  * @internal
176
+ * @deprecated Use `@zenfs/linux`!
177
+ * @todo [breaking] remove
163
178
  */
164
179
  export declare const randomDevice: DeviceDriver;
165
180
  /**
166
181
  * Shortcuts for importing.
167
182
  * @category Internals
168
183
  * @internal
184
+ * @deprecated Use `@zenfs/linux`!
185
+ * @todo [breaking] remove
169
186
  */
170
187
  export declare const devices: {
171
188
  null: DeviceDriver<any>;
@@ -13,6 +13,8 @@ import { Inode } from './inode.js';
13
13
  /**
14
14
  * A temporary file system that manages and interfaces with devices
15
15
  * @category Internals
16
+ * @deprecated Use `@zenfs/linux`!
17
+ * @todo [breaking] remove
16
18
  */
17
19
  export class DeviceFS extends StoreFS {
18
20
  devices = new Map();
@@ -231,6 +233,8 @@ const emptyBuffer = new Uint8Array();
231
233
  * - Writes discard data, advancing the file position.
232
234
  * @category Internals
233
235
  * @internal
236
+ * @deprecated Use `@zenfs/linux`!
237
+ * @todo [breaking] remove
234
238
  */
235
239
  export const nullDevice = {
236
240
  name: 'null',
@@ -255,6 +259,8 @@ export const nullDevice = {
255
259
  * - Provides basic file metadata, treating it as a character device.
256
260
  * @category Internals
257
261
  * @internal
262
+ * @deprecated Use `@zenfs/linux`!
263
+ * @todo [breaking] remove
258
264
  */
259
265
  export const zeroDevice = {
260
266
  name: 'zero',
@@ -275,6 +281,8 @@ export const zeroDevice = {
275
281
  * - Writes always fail with ENOSPC (no space left on device).
276
282
  * @category Internals
277
283
  * @internal
284
+ * @deprecated Use `@zenfs/linux`!
285
+ * @todo [breaking] remove
278
286
  */
279
287
  export const fullDevice = {
280
288
  name: 'full',
@@ -295,6 +303,8 @@ export const fullDevice = {
295
303
  * - Writes discard data, advancing the file position.
296
304
  * @category Internals
297
305
  * @internal
306
+ * @deprecated Use `@zenfs/linux`!
307
+ * @todo [breaking] remove
298
308
  */
299
309
  export const randomDevice = {
300
310
  name: 'random',
@@ -315,6 +325,8 @@ export const randomDevice = {
315
325
  * Simulates the `/dev/console` device.
316
326
  * @category Internals
317
327
  * @experimental @internal
328
+ * @deprecated Use `@zenfs/linux`!
329
+ * @todo [breaking] remove
318
330
  */
319
331
  const consoleDevice = {
320
332
  name: 'console',
@@ -334,6 +346,8 @@ const consoleDevice = {
334
346
  * Shortcuts for importing.
335
347
  * @category Internals
336
348
  * @internal
349
+ * @deprecated Use `@zenfs/linux`!
350
+ * @todo [breaking] remove
337
351
  */
338
352
  export const devices = {
339
353
  null: nullDevice,
@@ -642,8 +642,10 @@ export function isFIFO(metadata) {
642
642
  */
643
643
  export function hasAccess($, inode, access) {
644
644
  const { credentials } = contextOf($);
645
- if (isSymbolicLink(inode) || credentials.euid === 0 || credentials.egid === 0)
645
+ if (isSymbolicLink(inode))
646
646
  return true;
647
+ if (credentials.euid === 0 || credentials.egid === 0)
648
+ return !(access & c.X_OK) || isDirectory(inode) || !!(inode.mode & (c.S_IXUSR | c.S_IXGRP | c.S_IXOTH));
647
649
  let perm = 0;
648
650
  if (credentials.uid === inode.uid) {
649
651
  if (inode.mode & c.S_IRUSR)
@@ -328,7 +328,7 @@ export function access(path, cbMode, cb = nop) {
328
328
  access;
329
329
  const statWatchers = new Map();
330
330
  export function watchFile(path, options, listener) {
331
- const normalizedPath = normalizePath(path);
331
+ const normalizedPath = normalizePath.call(this, path);
332
332
  const opts = typeof options != 'function' ? options : {};
333
333
  if (typeof options == 'function') {
334
334
  listener = options;
@@ -365,7 +365,7 @@ watchFile;
365
365
  * @param listener Optional listener to remove.
366
366
  */
367
367
  export function unwatchFile(path, listener = nop) {
368
- const normalizedPath = normalizePath(path);
368
+ const normalizedPath = normalizePath.call(this, path);
369
369
  const entry = statWatchers.get(normalizedPath);
370
370
  if (entry) {
371
371
  if (listener && listener !== nop) {
@@ -384,7 +384,7 @@ export function unwatchFile(path, listener = nop) {
384
384
  }
385
385
  unwatchFile;
386
386
  export function watch(path, options, listener) {
387
- const watcher = new FSWatcher(this, normalizePath(path), typeof options == 'object' ? options : {});
387
+ const watcher = new FSWatcher(this, normalizePath.call(this, path), typeof options == 'object' ? options : {});
388
388
  listener = typeof options == 'function' ? options : listener;
389
389
  watcher.on('change', listener || nop);
390
390
  return watcher;
@@ -529,7 +529,7 @@ rename;
529
529
  * Test whether or not `path` exists by checking with the file system.
530
530
  */
531
531
  export async function exists(path) {
532
- path = normalizePath(path);
532
+ path = normalizePath.call(this, path);
533
533
  try {
534
534
  const { fs, path: resolved } = await _async.resolve(this, path);
535
535
  return await fs.exists(resolved);
@@ -579,7 +579,7 @@ truncate;
579
579
  export async function unlink(path) {
580
580
  const env_2 = { stack: [], error: void 0, hasError: false };
581
581
  try {
582
- path = normalizePath(path);
582
+ path = normalizePath.call(this, path);
583
583
  const { fs, path: resolved } = resolveMount(path, this);
584
584
  const $ex = { syscall: 'unlink', path };
585
585
  const stats = await fs.stat(resolved).catch(rethrow($ex));
@@ -691,7 +691,7 @@ appendFile;
691
691
  export async function rmdir(path) {
692
692
  const env_6 = { stack: [], error: void 0, hasError: false };
693
693
  try {
694
- path = normalizePath(path);
694
+ path = normalizePath.call(this, path);
695
695
  const { fs, path: resolved } = await _async.resolve(this, path);
696
696
  const $ex = { syscall: 'rmdir', path };
697
697
  const stats = await fs.stat(resolved).catch(rethrow($ex));
@@ -722,7 +722,7 @@ export async function mkdir(path, options) {
722
722
  }
723
723
  mkdir;
724
724
  export async function readdir(path, options) {
725
- path = normalizePath(path);
725
+ path = normalizePath.call(this, path);
726
726
  const opt = typeof options === 'object' && options != null ? options : { encoding: options, withFileTypes: false, recursive: false };
727
727
  const rawEntries = await _async.readdir.call(this, path, opt);
728
728
  const values = [];
@@ -755,11 +755,11 @@ export async function symlink(dest, path, type = 'file') {
755
755
  try {
756
756
  if (!['file', 'dir', 'junction'].includes(type))
757
757
  throw new TypeError('Invalid symlink type: ' + type);
758
- path = normalizePath(path);
758
+ path = normalizePath.call(this, path);
759
759
  if (await exists.call(this, path))
760
760
  throw UV('EEXIST', 'symlink', path);
761
761
  const handle = __addDisposableResource(env_7, await _async.open(this, path, { flag: 'w+', mode: 0o644, preserveSymlinks: true }), true);
762
- const encoded = encodeUTF8(normalizePath(dest, true));
762
+ const encoded = encodeUTF8(normalizePath.call(this, dest, true));
763
763
  await handle.write(encoded, 0, encoded.length, 0);
764
764
  await handle.chmod(constants.S_IFLNK);
765
765
  }
@@ -775,7 +775,7 @@ export async function symlink(dest, path, type = 'file') {
775
775
  }
776
776
  symlink;
777
777
  export async function readlink(path, options) {
778
- path = normalizePath(path);
778
+ path = normalizePath.call(this, path);
779
779
  const buf = Buffer.from(await _async.readlink.call(this, path), 'utf-8');
780
780
  const encoding = typeof options == 'object' ? options?.encoding : options;
781
781
  // always defaults to utf-8 to avoid wrangler (cloudflare) worker "unknown encoding" exception
@@ -824,7 +824,7 @@ lchown;
824
824
  export async function chmod(path, mode) {
825
825
  const env_10 = { stack: [], error: void 0, hasError: false };
826
826
  try {
827
- const handle = __addDisposableResource(env_10, await open.call(this, path, 'r+').catch(rethrow({ syscall: 'chmod', path: normalizePath(path) })), true);
827
+ const handle = __addDisposableResource(env_10, await open.call(this, path, 'r+').catch(rethrow({ syscall: 'chmod', path: normalizePath.call(this, path) })), true);
828
828
  await handle.chmod(mode);
829
829
  }
830
830
  catch (e_10) {
@@ -911,7 +911,7 @@ export async function lutimes(path, atime, mtime) {
911
911
  lutimes;
912
912
  export async function realpath(path, options) {
913
913
  const encoding = typeof options == 'string' ? options : (options?.encoding ?? 'utf8');
914
- path = normalizePath(path);
914
+ path = normalizePath.call(this, path);
915
915
  const { fullPath } = await _async.resolve(this, path);
916
916
  if (encoding == 'utf8' || encoding == 'utf-8')
917
917
  return fullPath;
@@ -976,7 +976,7 @@ access;
976
976
  * @param path The path to the file or directory to remove.
977
977
  */
978
978
  export async function rm(path, options) {
979
- path = normalizePath(path);
979
+ path = normalizePath.call(this, path);
980
980
  const stats = await lstat.call(this, path).catch((error) => {
981
981
  if (error.code == 'ENOENT' && options?.force)
982
982
  return undefined;
@@ -1034,8 +1034,8 @@ export async function mkdtempDisposable(prefix, options) {
1034
1034
  * * `fs.constants.COPYFILE_EXCL`: If the destination file already exists, the operation fails.
1035
1035
  */
1036
1036
  export async function copyFile(src, dest, mode) {
1037
- src = normalizePath(src);
1038
- dest = normalizePath(dest);
1037
+ src = normalizePath.call(this, src);
1038
+ dest = normalizePath.call(this, dest);
1039
1039
  if (mode && mode & constants.COPYFILE_EXCL && (await exists.call(this, dest)))
1040
1040
  throw UV('EEXIST', 'copyFile', dest);
1041
1041
  await writeFile.call(this, dest, await readFile.call(this, src));
@@ -1050,7 +1050,7 @@ copyFile;
1050
1050
  * @todo Use options
1051
1051
  */
1052
1052
  export function opendir(path, options) {
1053
- path = normalizePath(path);
1053
+ path = normalizePath.call(this, path);
1054
1054
  return Promise.resolve(new Dir(path, this));
1055
1055
  }
1056
1056
  opendir;
@@ -1067,8 +1067,8 @@ opendir;
1067
1067
  * * `recursive`: If `true`, copies directories recursively.
1068
1068
  */
1069
1069
  export async function cp(source, destination, opts) {
1070
- source = normalizePath(source);
1071
- destination = normalizePath(destination);
1070
+ source = normalizePath.call(this, source);
1071
+ destination = normalizePath.call(this, destination);
1072
1072
  const srcStats = await lstat.call(this, source); // Use lstat to follow symlinks if not dereferencing
1073
1073
  if (opts?.errorOnExist && (await exists.call(this, destination)))
1074
1074
  throw UV('EEXIST', 'cp', destination);
@@ -1116,7 +1116,7 @@ export async function cp(source, destination, opts) {
1116
1116
  }
1117
1117
  cp;
1118
1118
  export async function statfs(path, opts) {
1119
- path = normalizePath(path);
1119
+ path = normalizePath.call(this, path);
1120
1120
  const { fs } = resolveMount(path, this);
1121
1121
  return Promise.resolve(_statfs(fs, opts?.bigint));
1122
1122
  }
package/dist/node/sync.js CHANGED
@@ -76,7 +76,7 @@ renameSync;
76
76
  * Test whether or not `path` exists by checking with the file system.
77
77
  */
78
78
  export function existsSync(path) {
79
- path = normalizePath(path);
79
+ path = normalizePath.call(this, path);
80
80
  try {
81
81
  const { fs, path: resolvedPath } = _sync.resolve(this, path);
82
82
  return fs.existsSync(resolvedPath);
@@ -133,7 +133,7 @@ export function truncateSync(path, len = 0) {
133
133
  }
134
134
  truncateSync;
135
135
  export function unlinkSync(path) {
136
- path = normalizePath(path);
136
+ path = normalizePath.call(this, path);
137
137
  const { fs, path: resolved } = resolveMount(path, this);
138
138
  try {
139
139
  const env_2 = { stack: [], error: void 0, hasError: false };
@@ -250,7 +250,7 @@ export function appendFileSync(filename, data, _options = {}) {
250
250
  const encodedData = typeof data == 'string' ? Buffer.from(data, options.encoding) : new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
251
251
  const file = typeof filename == 'number'
252
252
  ? fromFD(this, filename)
253
- : _sync.open(this, normalizePath(filename), {
253
+ : _sync.open(this, normalizePath.call(this, filename), {
254
254
  flag,
255
255
  mode: options.mode,
256
256
  });
@@ -354,7 +354,7 @@ futimesSync;
354
354
  export function rmdirSync(path) {
355
355
  const env_5 = { stack: [], error: void 0, hasError: false };
356
356
  try {
357
- path = normalizePath(path);
357
+ path = normalizePath.call(this, path);
358
358
  const { fs, path: resolved } = _sync.resolve(this, path);
359
359
  const stats = wrap(fs, 'statSync', { path, syscall: 'rmdir' })(resolved);
360
360
  if (!isDirectory(stats))
@@ -383,7 +383,7 @@ export function mkdirSync(path, options) {
383
383
  mkdirSync;
384
384
  export function readdirSync(path, options) {
385
385
  options = typeof options === 'object' ? options : { encoding: options };
386
- path = normalizePath(path);
386
+ path = normalizePath.call(this, path);
387
387
  const entries = [];
388
388
  const rawEntries = _sync.readdir.call(this, path, options ?? undefined);
389
389
  for (const raw of rawEntries) {
@@ -415,9 +415,9 @@ export function symlinkSync(target, path, type = 'file') {
415
415
  try {
416
416
  if (!['file', 'dir', 'junction'].includes(type))
417
417
  throw new TypeError('Invalid symlink type: ' + type);
418
- path = normalizePath(path);
418
+ path = normalizePath.call(this, path);
419
419
  const file = __addDisposableResource(env_6, _sync.open(this, path, { flag: 'wx', mode: 0o644 }), false);
420
- file.writeSync(encodeUTF8(normalizePath(target, true)));
420
+ file.writeSync(encodeUTF8(normalizePath.call(this, target, true)));
421
421
  file.chmodSync(constants.S_IFLNK);
422
422
  }
423
423
  catch (e_6) {
@@ -466,7 +466,7 @@ export function chmodSync(path, mode) {
466
466
  fd = openSync.call(this, path, 'r+');
467
467
  }
468
468
  catch (e) {
469
- throw setUVMessage(Object.assign(e, { syscall: 'chmod', path: normalizePath(path) }));
469
+ throw setUVMessage(Object.assign(e, { syscall: 'chmod', path: normalizePath.call(this, path) }));
470
470
  }
471
471
  fchmodSync.call(this, fd, mode);
472
472
  closeSync.call(this, fd);
@@ -498,7 +498,7 @@ export function lutimesSync(path, atime, mtime) {
498
498
  lutimesSync;
499
499
  export function realpathSync(path, options) {
500
500
  const encoding = typeof options == 'string' ? options : (options?.encoding ?? 'utf8');
501
- path = normalizePath(path, true);
501
+ path = normalizePath.call(this, path, true);
502
502
  const { fullPath } = _sync.resolve(this, path);
503
503
  if (encoding == 'utf8' || encoding == 'utf-8')
504
504
  return fullPath;
@@ -521,7 +521,7 @@ accessSync;
521
521
  * @param path The path to the file or directory to remove.
522
522
  */
523
523
  export function rmSync(path, options) {
524
- path = normalizePath(path);
524
+ path = normalizePath.call(this, path);
525
525
  let stats;
526
526
  try {
527
527
  stats = lstatSync.bind(this)(path);
@@ -580,8 +580,8 @@ export function mkdtempDisposableSync(prefix, options) {
580
580
  * - `fs.constants.COPYFILE_EXCL`: If the destination file already exists, the operation fails.
581
581
  */
582
582
  export function copyFileSync(source, destination, flags) {
583
- source = normalizePath(source);
584
- destination = normalizePath(destination);
583
+ source = normalizePath.call(this, source);
584
+ destination = normalizePath.call(this, destination);
585
585
  if (flags && flags & constants.COPYFILE_EXCL && existsSync.call(this, destination))
586
586
  throw UV('EEXIST', 'copyFile', destination);
587
587
  writeFileSync.call(this, destination, readFileSync.call(this, source));
@@ -628,7 +628,7 @@ writevSync;
628
628
  * @todo Handle options
629
629
  */
630
630
  export function opendirSync(path, options) {
631
- path = normalizePath(path);
631
+ path = normalizePath.call(this, path);
632
632
  return new Dir(path, this);
633
633
  }
634
634
  opendirSync;
@@ -645,8 +645,8 @@ opendirSync;
645
645
  * - `recursive`: If `true`, copies directories recursively.
646
646
  */
647
647
  export function cpSync(source, destination, opts) {
648
- source = normalizePath(source);
649
- destination = normalizePath(destination);
648
+ source = normalizePath.call(this, source);
649
+ destination = normalizePath.call(this, destination);
650
650
  const srcStats = lstatSync.call(this, source); // Use lstat to follow symlinks if not dereferencing
651
651
  if (opts?.errorOnExist && existsSync.call(this, destination))
652
652
  throw UV('EEXIST', 'cp', destination);
@@ -688,7 +688,7 @@ export function cpSync(source, destination, opts) {
688
688
  }
689
689
  cpSync;
690
690
  export function statfsSync(path, options) {
691
- path = normalizePath(path);
691
+ path = normalizePath.call(this, path);
692
692
  const { fs } = resolveMount(path, this);
693
693
  return _statfs(fs, options?.bigint);
694
694
  }
package/dist/vfs/async.js CHANGED
@@ -114,7 +114,7 @@ export async function resolve($, path, preserveSymlinks, extra) {
114
114
  * @internal
115
115
  */
116
116
  export async function open($, path, opt) {
117
- path = normalizePath(path);
117
+ path = normalizePath.call($, path);
118
118
  const mode = normalizeMode(opt.mode, 0o644), flag = flags.parse(opt.flag);
119
119
  const $ex = { syscall: 'open', path };
120
120
  // eslint-disable-next-line prefer-const
@@ -169,7 +169,7 @@ export async function open($, path, opt) {
169
169
  return handle;
170
170
  }
171
171
  export async function readlink(path) {
172
- path = normalizePath(path);
172
+ path = normalizePath.call(this, path);
173
173
  const $ex = { syscall: 'readlink', path };
174
174
  const { fs, stats, path: resolved } = await resolve(this, path, true, $ex);
175
175
  if (!stats)
@@ -184,7 +184,7 @@ export async function readlink(path) {
184
184
  return decodeUTF8(data);
185
185
  }
186
186
  export async function mkdir(path, options = {}) {
187
- const original = normalizePath(path);
187
+ const original = normalizePath.call(this, path);
188
188
  const { euid: uid, egid: gid } = contextOf(this).credentials;
189
189
  const { mode = 0o777, recursive } = options;
190
190
  const $ex = { syscall: 'mkdir', path: original };
@@ -238,7 +238,7 @@ export async function mkdir(path, options = {}) {
238
238
  return firstCreated;
239
239
  }
240
240
  export async function readdir(path, options = {}) {
241
- path = normalizePath(path);
241
+ path = normalizePath.call(this, path);
242
242
  // Node reports `readdir` failures as `scandir`
243
243
  const $ex = { syscall: 'scandir', path };
244
244
  const { fs, path: resolved, stats } = await resolve(this, path, false, $ex);
@@ -290,8 +290,8 @@ export async function readdir(path, options = {}) {
290
290
  export async function rename(oldPath, newPath) {
291
291
  const env_4 = { stack: [], error: void 0, hasError: false };
292
292
  try {
293
- oldPath = normalizePath(oldPath);
294
- newPath = normalizePath(newPath);
293
+ oldPath = normalizePath.call(this, oldPath);
294
+ newPath = normalizePath.call(this, newPath);
295
295
  const $ex = { syscall: 'rename', path: oldPath, dest: newPath };
296
296
  const src = await resolve(this, oldPath, true, $ex);
297
297
  const dst = resolveMount(newPath, this, $ex);
@@ -344,8 +344,8 @@ export async function rename(oldPath, newPath) {
344
344
  export async function link(target, link) {
345
345
  const env_5 = { stack: [], error: void 0, hasError: false };
346
346
  try {
347
- target = normalizePath(target);
348
- link = normalizePath(link);
347
+ target = normalizePath.call(this, target);
348
+ link = normalizePath.call(this, link);
349
349
  const $ex = { syscall: 'link', path: target, dest: link };
350
350
  const { fs, path: resolved } = resolveMount(target, this, $ex);
351
351
  const dst = resolveMount(link, this, $ex);
@@ -45,6 +45,10 @@ export declare class Handle {
45
45
  * Whether the file is open or closed
46
46
  */
47
47
  protected closed: boolean;
48
+ /** How many descriptors refer to this open file */
49
+ protected refs: number;
50
+ /** Take another reference to the open file */
51
+ ref(): this;
48
52
  get isClosed(): boolean;
49
53
  /**
50
54
  * Creates a file handle for the vnode.
@@ -141,11 +145,17 @@ export declare class Handle {
141
145
  streamWrite(options: StreamOptions): WritableStream;
142
146
  }
143
147
  /**
148
+ * Give an open file a descriptor
144
149
  * @internal @hidden
145
150
  */
146
- export declare function toFD(file: Handle): number;
151
+ export declare function toFD(file: Handle, $?: V_Context): number;
147
152
  /**
148
153
  * @internal @hidden
149
154
  */
150
155
  export declare function fromFD($: V_Context, fd: number): Handle;
151
156
  export declare function deleteFD($: V_Context, fd: number): boolean;
157
+ /**
158
+ * Give an open file another descriptor
159
+ * @internal @hidden
160
+ */
161
+ export declare function dupFD($: V_Context, fd: number): number;
package/dist/vfs/file.js CHANGED
@@ -152,6 +152,15 @@ let Handle = (() => {
152
152
  * Whether the file is open or closed
153
153
  */
154
154
  closed = false;
155
+ /** How many descriptors refer to this open file */
156
+ refs = 1;
157
+ /** Take another reference to the open file */
158
+ ref() {
159
+ if (this.closed)
160
+ throw UV('EBADF', 'dup', this.path);
161
+ this.refs++;
162
+ return this;
163
+ }
155
164
  get isClosed() {
156
165
  return this.closed;
157
166
  }
@@ -199,6 +208,8 @@ let Handle = (() => {
199
208
  if (this.closed)
200
209
  throw UV('EBADF', 'close', this.path);
201
210
  this.syncSync();
211
+ if (--this.refs > 0)
212
+ return;
202
213
  this.disposeSync(true);
203
214
  }
204
215
  /**
@@ -406,6 +417,8 @@ let Handle = (() => {
406
417
  if (this.closed)
407
418
  throw UV('EBADF', 'close', this.path);
408
419
  await this.sync();
420
+ if (--this.refs > 0)
421
+ return;
409
422
  this.dispose(true);
410
423
  }
411
424
  /**
@@ -671,12 +684,14 @@ let Handle = (() => {
671
684
  export { Handle };
672
685
  // descriptors
673
686
  /**
687
+ * Give an open file a descriptor
674
688
  * @internal @hidden
675
689
  */
676
- export function toFD(file) {
677
- const map = contextOf(file.context).descriptors;
678
- // @todo use `Iterator` helper #296
679
- const fd = Array.from(map.keys()).reduce((next, k) => Math.max(next, k + 1), 4);
690
+ export function toFD(file, $ = file.context) {
691
+ const map = contextOf($).descriptors;
692
+ let fd = 0;
693
+ while (map.has(fd))
694
+ fd++;
680
695
  map.set(fd, file);
681
696
  return fd;
682
697
  }
@@ -694,3 +709,10 @@ export function fromFD($, fd) {
694
709
  export function deleteFD($, fd) {
695
710
  return contextOf($).descriptors.delete(fd);
696
711
  }
712
+ /**
713
+ * Give an open file another descriptor
714
+ * @internal @hidden
715
+ */
716
+ export function dupFD($, fd) {
717
+ return toFD(fromFD($, fd).ref(), $);
718
+ }
package/dist/vfs/ioctl.js CHANGED
@@ -26,7 +26,7 @@ export async function ioctl(path, command, ...args) {
26
26
  vnode.refs++;
27
27
  }
28
28
  else {
29
- path = normalizePath(path);
29
+ path = normalizePath.call(this, path);
30
30
  const mnt = await resolveAsync(this, path, false, { syscall: 'ioctl', path });
31
31
  if (!mnt.stats)
32
32
  throw UV('ENOENT', { syscall: 'ioctl', path });
@@ -64,7 +64,7 @@ export function ioctlSync(path, command, ...args) {
64
64
  vnode.refs++;
65
65
  }
66
66
  else {
67
- path = normalizePath(path);
67
+ path = normalizePath.call(this, path);
68
68
  const mnt = resolveSync(this, path, false, { syscall: 'ioctl', path });
69
69
  if (!mnt.stats)
70
70
  throw UV('ENOENT', { syscall: 'ioctl', path });
@@ -128,7 +128,7 @@ export function chroot(path) {
128
128
  throw UV('EBUSY', 'chroot', handle.path);
129
129
  handle.path = handle.path.slice($.root.length);
130
130
  }
131
- if (newRoot.length > $.root.length)
131
+ if (newRoot.length < $.root.length)
132
132
  throw withErrno('EPERM', 'Can not chroot() outside of current root');
133
133
  $.root = newRoot;
134
134
  }
package/dist/vfs/sync.js CHANGED
@@ -114,7 +114,7 @@ export function resolve($, path, preserveSymlinks, extra) {
114
114
  * @internal
115
115
  */
116
116
  export function open($, path, opt) {
117
- path = normalizePath(path);
117
+ path = normalizePath.call($, path);
118
118
  const mode = normalizeMode(opt.mode, 0o644), flag = flags.parse(opt.flag);
119
119
  path = opt.preserveSymlinks ? path : resolve($, path).fullPath;
120
120
  const { fs, path: resolved } = resolveMount(path, $);
@@ -175,7 +175,7 @@ export function open($, path, opt) {
175
175
  return file;
176
176
  }
177
177
  export function readlink(path) {
178
- path = normalizePath(path);
178
+ path = normalizePath.call(this, path);
179
179
  const { fs, stats, path: resolved } = resolve(this, path, true, { syscall: 'readlink' });
180
180
  if (!stats)
181
181
  throw UV('ENOENT', 'readlink', path);
@@ -189,7 +189,7 @@ export function readlink(path) {
189
189
  return decodeUTF8(data);
190
190
  }
191
191
  export function mkdir(path, options = {}) {
192
- const original = normalizePath(path);
192
+ const original = normalizePath.call(this, path);
193
193
  const $ex = { syscall: 'mkdir', path: original };
194
194
  const { fullPath: realParent } = resolve(this, dirname(original), false, $ex);
195
195
  const { fs, path: target } = resolveMount(join(realParent, basename(original)), this, $ex);
@@ -246,7 +246,7 @@ export function mkdir(path, options = {}) {
246
246
  return firstCreated;
247
247
  }
248
248
  export function readdir(path, options = {}) {
249
- path = normalizePath(path);
249
+ path = normalizePath.call(this, path);
250
250
  const { fs, path: resolved } = resolve(this, path);
251
251
  // Node reports `readdir` failures as `scandir`
252
252
  const stats = cacheOf(fs).get(resolved)?.inode ?? wrap(fs, 'statSync', { path, syscall: 'scandir' })(resolved);
@@ -300,8 +300,8 @@ export function readdir(path, options = {}) {
300
300
  export function rename(oldPath, newPath) {
301
301
  const env_4 = { stack: [], error: void 0, hasError: false };
302
302
  try {
303
- oldPath = normalizePath(oldPath);
304
- newPath = normalizePath(newPath);
303
+ oldPath = normalizePath.call(this, oldPath);
304
+ newPath = normalizePath.call(this, newPath);
305
305
  const $ex = { syscall: 'rename', path: oldPath, dest: newPath };
306
306
  const src = resolve(this, oldPath, true, $ex);
307
307
  const dst = resolveMount(newPath, this, $ex);
@@ -357,8 +357,8 @@ export function rename(oldPath, newPath) {
357
357
  export function link(target, link) {
358
358
  const env_5 = { stack: [], error: void 0, hasError: false };
359
359
  try {
360
- target = normalizePath(target);
361
- link = normalizePath(link);
360
+ target = normalizePath.call(this, target);
361
+ link = normalizePath.call(this, link);
362
362
  const $ex = { syscall: 'link', path: target, dest: link };
363
363
  const { fs, path: resolved } = resolveMount(target, this, $ex);
364
364
  const dst = resolveMount(link, this, $ex);
@@ -151,7 +151,7 @@ export function emitChange(context, eventType, filename) {
151
151
  const $ = contextOf(context);
152
152
  if ($)
153
153
  filename = join($.root ?? '/', filename);
154
- filename = normalizePath(filename);
154
+ filename = normalizePath.call(context, filename);
155
155
  // Notify watchers, including ones on parent directories if they are watching recursively.
156
156
  for (let path = filename;; path = dirname(path)) {
157
157
  const watchersForPath = watchers.get(path);
package/dist/vfs/xattr.js CHANGED
@@ -17,7 +17,7 @@ function checkName($, name, path, syscall) {
17
17
  throw UV('ENOTSUP', syscall, path);
18
18
  }
19
19
  export async function get(path, name, opt = {}) {
20
- path = normalizePath(path);
20
+ path = normalizePath.call(this, path);
21
21
  const { fs, path: resolved } = resolveMount(path, this);
22
22
  checkName(this, name, path, 'xattr.get');
23
23
  const inode = await fs.stat(resolved).catch(rethrow('xattr.get', path));
@@ -31,7 +31,7 @@ export async function get(path, name, opt = {}) {
31
31
  return opt.encoding == 'buffer' || !opt.encoding ? buffer : buffer.toString(opt.encoding);
32
32
  }
33
33
  export function getSync(path, name, opt = {}) {
34
- path = normalizePath(path);
34
+ path = normalizePath.call(this, path);
35
35
  checkName(this, name, path, 'xattr.get');
36
36
  const { fs, path: resolved } = resolveMount(path, this);
37
37
  let inode;
@@ -59,7 +59,7 @@ export function getSync(path, name, opt = {}) {
59
59
  * @param opt Options for the operation
60
60
  */
61
61
  export async function set(path, name, value, opt = {}) {
62
- path = normalizePath(path);
62
+ path = normalizePath.call(this, path);
63
63
  const { fs, path: resolved } = resolveMount(path, this);
64
64
  checkName(this, name, path, 'xattr.set');
65
65
  const inode = await fs.stat(resolved).catch(rethrow('xattr.set', path));
@@ -83,7 +83,7 @@ export async function set(path, name, value, opt = {}) {
83
83
  * @param opt Options for the operation
84
84
  */
85
85
  export function setSync(path, name, value, opt = {}) {
86
- path = normalizePath(path);
86
+ path = normalizePath.call(this, path);
87
87
  const { fs, path: resolved } = resolveMount(path, this);
88
88
  checkName(this, name, path, 'xattr.set');
89
89
  let inode;
@@ -116,7 +116,7 @@ export function setSync(path, name, value, opt = {}) {
116
116
  * @param name Name of the attribute to remove
117
117
  */
118
118
  export async function remove(path, name) {
119
- path = normalizePath(path);
119
+ path = normalizePath.call(this, path);
120
120
  const { fs, path: resolved } = resolveMount(path, this);
121
121
  checkName(this, name, path, 'xattr.remove');
122
122
  const inode = await fs.stat(resolved).catch(rethrow('xattr.remove', path));
@@ -136,7 +136,7 @@ export async function remove(path, name) {
136
136
  * @param name Name of the attribute to remove
137
137
  */
138
138
  export function removeSync(path, name) {
139
- path = normalizePath(path);
139
+ path = normalizePath.call(this, path);
140
140
  const { fs, path: resolved } = resolveMount(path, this);
141
141
  checkName(this, name, path, 'xattr.remove');
142
142
  let inode;
@@ -167,7 +167,7 @@ export function removeSync(path, name) {
167
167
  * @returns Array of attribute names
168
168
  */
169
169
  export async function list(path) {
170
- path = normalizePath(path);
170
+ path = normalizePath.call(this, path);
171
171
  const { fs, path: resolved } = resolveMount(path, this);
172
172
  const inode = await fs.stat(resolved).catch(rethrow('xattr.list', path));
173
173
  if (!inode.attributes)
@@ -181,7 +181,7 @@ export async function list(path) {
181
181
  * @returns Array of attribute names
182
182
  */
183
183
  export function listSync(path) {
184
- path = normalizePath(path);
184
+ path = normalizePath.call(this, path);
185
185
  const { fs, path: resolved } = resolveMount(path, this);
186
186
  let inode;
187
187
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenfs/core",
3
- "version": "2.7.0",
3
+ "version": "2.7.2",
4
4
  "description": "A filesystem, anywhere",
5
5
  "funding": {
6
6
  "type": "individual",
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: LGPL-3.0-or-later
2
- import { bindContext, configure, fs, InMemory } from '@zenfs/core';
2
+ import { bindContext, configure, fs, InMemory, type BoundContext } from '@zenfs/core';
3
3
  import assert from 'node:assert/strict';
4
4
  import { suite, test } from 'node:test';
5
5
  import { canary } from 'utilium';
@@ -124,4 +124,14 @@ suite('Context', () => {
124
124
  assert.equal(ctx.readFileSync('/copy.txt', 'utf-8'), 'not in real root!');
125
125
  assert(fs.readdirSync('/ctx').includes('copy.txt'));
126
126
  });
127
+
128
+ test('bindContext should resolve root relative to the parent context', () => {
129
+ fs.mkdirSync('/secure');
130
+ const ctx = bindContext({ root: '/secure', credentials: { uid: 333, gid: 333 } });
131
+
132
+ const plain = bindContext({ credentials: { uid: 1, gid: 1 } });
133
+ console.log('child of / context: ok, root =', plain.root);
134
+
135
+ assert.doesNotThrow(() => ctx.bind({ credentials: { uid: 500, gid: 500 } }));
136
+ });
127
137
  });
@@ -26,7 +26,7 @@ suite('Utf8Stream', config('write'), () => {
26
26
  assert.equal(stream.maxLength, 32);
27
27
  assert.equal(stream.periodicFlush, 0);
28
28
  assert.equal(stream.mkdir, false);
29
- assert.ok(stream.fd > 0);
29
+ assert.ok(stream.fd >= 0);
30
30
 
31
31
  stream.destroy();
32
32
  });
@@ -54,7 +54,7 @@ suite('Permissions', config('permissions'), () => {
54
54
  assert.equal(error.code, 'EACCES');
55
55
  });
56
56
  if (!stats) return;
57
- assert(hasAccess(defaultContext, stats, X_OK));
57
+ if (stats.isDirectory()) assert(hasAccess(defaultContext, stats, X_OK));
58
58
 
59
59
  function checkError(access: number) {
60
60
  return function (error: Exception) {