@zenfs/core 2.4.3 → 2.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/context.d.ts CHANGED
@@ -11,4 +11,4 @@ export declare const boundContexts: Map<number, BoundContext>;
11
11
  * Note that the default credentials of a bound context are copied from the global credentials.
12
12
  * @category Contexts
13
13
  */
14
- export declare function bindContext(this: void | null | FSContext, { root, pwd, credentials }?: ContextInit): BoundContext;
14
+ export declare function bindContext(this: V_Context, init?: ContextInit): BoundContext;
package/dist/context.js CHANGED
@@ -1,12 +1,10 @@
1
1
  // SPDX-License-Identifier: LGPL-3.0-or-later
2
+ import { UV } from 'kerium';
2
3
  import { bindFunctions } from 'utilium';
3
- import { defaultContext } from './internal/contexts.js';
4
- import { createCredentials } from './internal/credentials.js';
5
- import * as path from './path.js';
4
+ import { contextOf, createChildContext } from './internal/contexts.js';
6
5
  import * as fs from './node/index.js';
6
+ import * as path from './path.js';
7
7
  import * as xattr from './vfs/xattr.js';
8
- // 0 is reserved for the global/default context
9
- let _nextId = 1;
10
8
  /**
11
9
  * A map of all contexts.
12
10
  * @internal
@@ -18,19 +16,12 @@ export const boundContexts = new Map();
18
16
  * Note that the default credentials of a bound context are copied from the global credentials.
19
17
  * @category Contexts
20
18
  */
21
- export function bindContext({ root = this?.root || '/', pwd = this?.pwd || '/', credentials = structuredClone(defaultContext.credentials) } = {}) {
22
- const parent = this ?? defaultContext;
23
- const ctx = {
24
- id: _nextId++,
25
- root,
26
- pwd,
27
- credentials: createCredentials(credentials),
28
- descriptors: new Map(),
29
- parent,
30
- children: [],
31
- };
32
- const bound = {
33
- ...ctx,
19
+ export function bindContext(init = {}) {
20
+ const $ = contextOf(this);
21
+ if (!fs.statSync.call(this, $.root).isDirectory())
22
+ throw UV('ENOTDIR', { syscall: 'chroot', path: $.root });
23
+ const ctx = createChildContext($, init);
24
+ const bound = Object.assign(ctx, {
34
25
  fs: {
35
26
  ...bindFunctions(fs, ctx),
36
27
  promises: bindFunctions(fs.promises, ctx),
@@ -42,7 +33,7 @@ export function bindContext({ root = this?.root || '/', pwd = this?.pwd || '/',
42
33
  ctx.children.push(child);
43
34
  return child;
44
35
  },
45
- };
36
+ });
46
37
  boundContexts.set(ctx.id, bound);
47
38
  return bound;
48
39
  }
@@ -4,11 +4,18 @@ import type * as path from '../path.js';
4
4
  import type { Handle } from '../vfs/file.js';
5
5
  import type * as xattr from '../vfs/xattr.js';
6
6
  import type { Credentials, CredentialsInit } from './credentials.js';
7
+ /**
8
+ * Symbol used for context branding
9
+ * @internal @hidden
10
+ */
11
+ declare const kIsContext: unique symbol;
7
12
  /**
8
13
  * A context used for FS operations
9
14
  * @category Contexts
10
15
  */
11
16
  export interface FSContext {
17
+ /** The unique ID of the context */
18
+ readonly [kIsContext]: boolean;
12
19
  /** The unique ID of the context */
13
20
  readonly id: number;
14
21
  /**
@@ -22,16 +29,16 @@ export interface FSContext {
22
29
  /** The credentials of the context, used for access checks */
23
30
  readonly credentials: Credentials;
24
31
  /** A map of open file descriptors to their handles */
25
- descriptors: Map<number, Handle>;
32
+ readonly descriptors: Map<number, Handle>;
26
33
  /** The parent context, if any. */
27
- parent: V_Context;
34
+ readonly parent: FSContext | null;
28
35
  /** The child contexts */
29
- children: FSContext[];
36
+ readonly children: FSContext[];
30
37
  }
31
38
  /**
32
39
  * maybe an FS context
33
40
  */
34
- export type V_Context = void | null | (Partial<FSContext> & object);
41
+ export type V_Context = unknown;
35
42
  /**
36
43
  * Allows you to restrict operations to a specific root path and set of credentials.
37
44
  * @category Contexts
@@ -62,3 +69,16 @@ export interface ContextInit {
62
69
  * @category Contexts
63
70
  */
64
71
  export declare const defaultContext: FSContext;
72
+ export declare function contextOf($: unknown): FSContext;
73
+ /**
74
+ * Create a blank FS Context
75
+ * @internal
76
+ * @category Contexts
77
+ * @todo Make sure parent root can't be escaped
78
+ *
79
+ * This exists so that `kIsContext` is not exported and to make sure the context is "secure".
80
+ */
81
+ export declare function createChildContext(parent: FSContext, init?: ContextInit): FSContext & {
82
+ parent: FSContext;
83
+ };
84
+ export {};
@@ -1,10 +1,16 @@
1
1
  import { createCredentials } from './credentials.js';
2
+ /**
3
+ * Symbol used for context branding
4
+ * @internal @hidden
5
+ */
6
+ const kIsContext = Symbol('ZenFSContext');
2
7
  /**
3
8
  * The default/global context.
4
9
  * @internal @hidden
5
10
  * @category Contexts
6
11
  */
7
12
  export const defaultContext = {
13
+ [kIsContext]: true,
8
14
  id: 0,
9
15
  root: '/',
10
16
  pwd: '/',
@@ -13,3 +19,37 @@ export const defaultContext = {
13
19
  parent: null,
14
20
  children: [],
15
21
  };
22
+ export function contextOf($) {
23
+ return typeof $ === 'object' && $ !== null && kIsContext in $ ? $ : defaultContext;
24
+ }
25
+ // 0 is reserved for the global/default context
26
+ let _nextId = 1;
27
+ /**
28
+ * Create a blank FS Context
29
+ * @internal
30
+ * @category Contexts
31
+ * @todo Make sure parent root can't be escaped
32
+ *
33
+ * This exists so that `kIsContext` is not exported and to make sure the context is "secure".
34
+ */
35
+ export function createChildContext(parent, init = {}) {
36
+ const { root = parent.root, pwd = parent.pwd, credentials = structuredClone(parent.credentials) } = init;
37
+ const ctx = {
38
+ [kIsContext]: true,
39
+ id: _nextId++,
40
+ root,
41
+ pwd,
42
+ credentials: createCredentials(credentials),
43
+ descriptors: new Map(),
44
+ parent: parent,
45
+ children: [],
46
+ };
47
+ Object.defineProperties(ctx, {
48
+ id: { configurable: false, writable: false },
49
+ credentials: { configurable: false, writable: false },
50
+ descriptors: { configurable: false, writable: false },
51
+ parent: { configurable: false, writable: false },
52
+ children: { configurable: false, writable: false },
53
+ });
54
+ return ctx;
55
+ }
@@ -39,9 +39,9 @@ import { sizeof } from 'memium';
39
39
  import { $from, field, struct, types as t } from 'memium/decorators';
40
40
  import { decodeUTF8, encodeUTF8, pick } from 'utilium';
41
41
  import { BufferView } from 'utilium/buffer.js';
42
- import { Stats } from '../node/stats.js';
43
42
  import * as c from '../constants.js';
44
- import { defaultContext } from './contexts.js';
43
+ import { Stats } from '../node/stats.js';
44
+ import { contextOf } from './contexts.js';
45
45
  /**
46
46
  * Root inode
47
47
  * @hidden
@@ -620,7 +620,7 @@ export function isFIFO(metadata) {
620
620
  * @internal
621
621
  */
622
622
  export function hasAccess($, inode, access) {
623
- const credentials = $?.credentials || defaultContext.credentials;
623
+ const { credentials } = contextOf($);
624
624
  if (isSymbolicLink(inode) || credentials.euid === 0 || credentials.egid === 0)
625
625
  return true;
626
626
  let perm = 0;
package/dist/node/sync.js CHANGED
@@ -467,7 +467,7 @@ export function lutimesSync(path, atime, mtime) {
467
467
  lutimesSync;
468
468
  export function realpathSync(path, options) {
469
469
  const encoding = typeof options == 'string' ? options : (options?.encoding ?? 'utf8');
470
- path = normalizePath(path);
470
+ path = normalizePath(path, true);
471
471
  const { fullPath } = _sync.resolve(this, path);
472
472
  if (encoding == 'utf8' || encoding == 'utf-8')
473
473
  return fullPath;
package/dist/path.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: LGPL-3.0-or-later
2
- import { defaultContext } from './internal/contexts.js';
2
+ import { contextOf } from './internal/contexts.js';
3
3
  import { globToRegex } from './utils.js';
4
4
  export const sep = '/';
5
5
  function validateObject(str, name) {
@@ -81,7 +81,7 @@ export function formatExt(ext) {
81
81
  }
82
82
  export function resolve(...parts) {
83
83
  let resolved = '';
84
- for (const part of [...parts.reverse(), this?.pwd ?? defaultContext.pwd]) {
84
+ for (const part of [...parts.reverse(), contextOf(this).pwd]) {
85
85
  if (!part?.length)
86
86
  continue;
87
87
  resolved = `${part}/${resolved}`;
@@ -334,7 +334,7 @@ export function format(pathObject) {
334
334
  return dir === pathObject.root ? `${dir}${base}` : `${dir}/${base}`;
335
335
  }
336
336
  export function parse(path) {
337
- const isAbsolute = path.startsWith('/');
337
+ const isAbsolute = path[0] === '/';
338
338
  const ret = { root: isAbsolute ? '/' : '', dir: '', base: '', ext: '', name: '' };
339
339
  if (path.length === 0)
340
340
  return ret;
package/dist/utils.d.ts CHANGED
@@ -2,6 +2,7 @@ import { type Exception } from 'kerium';
2
2
  import type * as fs from 'node:fs';
3
3
  import type { Worker as NodeWorker } from 'node:worker_threads';
4
4
  import { type OptionalTuple } from 'utilium';
5
+ import type { V_Context } from './internal/contexts.js';
5
6
  declare global {
6
7
  function atob(data: string): string;
7
8
  function btoa(data: string): string;
@@ -31,8 +32,9 @@ export declare function normalizeTime(time: fs.TimeLike): number;
31
32
  /**
32
33
  * Normalizes a path
33
34
  * @internal
35
+ * @todo clean this up and make it so `path.resolve` is only called when an explicit context is passed (i.e. `normalizePath(..., $)` to use `path.resolve`)
34
36
  */
35
- export declare function normalizePath(p: fs.PathLike, noResolve?: boolean): string;
37
+ export declare function normalizePath(this: V_Context, p: fs.PathLike, noResolve?: boolean): string;
36
38
  /**
37
39
  * Normalizes options
38
40
  * @param options options to normalize
package/dist/utils.js CHANGED
@@ -51,6 +51,7 @@ export function normalizeTime(time) {
51
51
  /**
52
52
  * Normalizes a path
53
53
  * @internal
54
+ * @todo clean this up and make it so `path.resolve` is only called when an explicit context is passed (i.e. `normalizePath(..., $)` to use `path.resolve`)
54
55
  */
55
56
  export function normalizePath(p, noResolve = false) {
56
57
  if (p instanceof URL) {
@@ -67,7 +68,7 @@ export function normalizePath(p, noResolve = false) {
67
68
  throw withErrno('EINVAL', 'Path can not be empty');
68
69
  p = p.replaceAll(/[/\\]+/g, '/');
69
70
  // Note: PWD is not resolved here, it is resolved later.
70
- return noResolve ? p : resolve(p);
71
+ return noResolve ? p : resolve.call(this, p);
71
72
  }
72
73
  /**
73
74
  * Normalizes options
package/dist/vfs/acl.js CHANGED
@@ -44,9 +44,9 @@ import { err } from 'kerium/log';
44
44
  import { packed, sizeof } from 'memium';
45
45
  import { $from, struct, types as t } from 'memium/decorators';
46
46
  import { BufferView } from 'utilium/buffer.js';
47
- import { defaultContext } from '../internal/contexts.js';
48
- import { Attributes } from '../internal/inode.js';
49
47
  import { R_OK, S_IRWXG, S_IRWXO, S_IRWXU, W_OK, X_OK } from '../constants.js';
48
+ import { contextOf } from '../internal/contexts.js';
49
+ import { Attributes } from '../internal/inode.js';
50
50
  import * as xattr from './xattr.js';
51
51
  const version = 2;
52
52
  export var Type;
@@ -206,7 +206,7 @@ export function check($, inode, access) {
206
206
  if (!shouldCheck)
207
207
  return true;
208
208
  inode.attributes ??= new Attributes();
209
- const { euid, egid } = $?.credentials ?? defaultContext.credentials;
209
+ const { euid, egid } = contextOf($).credentials;
210
210
  const data = inode.attributes.get('system.posix_acl_access');
211
211
  if (!data)
212
212
  return true;
package/dist/vfs/async.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { setUVMessage, UV } from 'kerium';
2
2
  import { decodeUTF8 } from 'utilium';
3
3
  import * as constants from '../constants.js';
4
- import { defaultContext } from '../internal/contexts.js';
4
+ import { contextOf } from '../internal/contexts.js';
5
5
  import { hasAccess, isDirectory, isSymbolicLink } from '../internal/inode.js';
6
6
  import { basename, dirname, join, parse, resolve as resolvePath } from '../path.js';
7
7
  import { normalizeMode, normalizePath } from '../utils.js';
@@ -17,6 +17,7 @@ import { emitChange } from './watchers.js';
17
17
  * @internal @hidden
18
18
  */
19
19
  export async function resolve($, path, preserveSymlinks, extra) {
20
+ path = resolvePath.call($, path);
20
21
  if (preserveSymlinks) {
21
22
  const resolved = resolveMount(path, $, extra);
22
23
  const stats = await resolved.fs.stat(resolved.path).catch(() => undefined);
@@ -74,7 +75,7 @@ export async function open($, path, opt) {
74
75
  throw UV('ENOTDIR', 'open', dirname(path));
75
76
  if (!opt.allowDirectory && mode & constants.S_IFDIR)
76
77
  throw UV('EISDIR', 'open', path);
77
- const { euid: uid, egid: gid } = $?.credentials ?? defaultContext.credentials;
78
+ const { euid: uid, egid: gid } = contextOf($).credentials;
78
79
  const inode = await fs.createFile(resolved, {
79
80
  mode,
80
81
  uid: parentStats.mode & constants.S_ISUID ? parentStats.uid : uid,
@@ -110,7 +111,7 @@ export async function readlink(path) {
110
111
  }
111
112
  export async function mkdir(path, options = {}) {
112
113
  path = normalizePath(path);
113
- const { euid: uid, egid: gid } = this?.credentials ?? defaultContext.credentials;
114
+ const { euid: uid, egid: gid } = contextOf(this).credentials;
114
115
  const { mode = 0o777, recursive } = options;
115
116
  const { fs, path: resolved } = resolveMount(path, this, { syscall: 'mkdir' });
116
117
  const __create = async (path, resolved, parent) => {
package/dist/vfs/file.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: LGPL-3.0-or-later
2
2
  import { UV, withErrno } from 'kerium';
3
3
  import * as c from '../constants.js';
4
- import { defaultContext } from '../internal/contexts.js';
4
+ import { contextOf } from '../internal/contexts.js';
5
5
  import { _chown, InodeFlags, isBlockDevice, isCharacterDevice } from '../internal/inode.js';
6
6
  import '../polyfills.js';
7
7
  /**
@@ -371,7 +371,7 @@ export class Handle {
371
371
  * @internal @hidden
372
372
  */
373
373
  export function toFD(file) {
374
- const map = file.context?.descriptors ?? defaultContext.descriptors;
374
+ const map = contextOf(file.context).descriptors;
375
375
  const fd = Math.max(map.size ? Math.max(...map.keys()) + 1 : 0, 4);
376
376
  map.set(fd, file);
377
377
  return fd;
@@ -380,12 +380,12 @@ export function toFD(file) {
380
380
  * @internal @hidden
381
381
  */
382
382
  export function fromFD($, fd) {
383
- const map = $?.descriptors ?? defaultContext.descriptors;
383
+ const map = contextOf($).descriptors;
384
384
  const value = map.get(fd);
385
385
  if (!value)
386
386
  throw withErrno('EBADF');
387
387
  return value;
388
388
  }
389
389
  export function deleteFD($, fd) {
390
- return ($?.descriptors ?? defaultContext.descriptors).delete(fd);
390
+ return contextOf($).descriptors.delete(fd);
391
391
  }
@@ -4,7 +4,7 @@ import { Errno, Exception, UV, withErrno } from 'kerium';
4
4
  import { alert, debug, err, info, notice, warn } from 'kerium/log';
5
5
  import { InMemory } from '../backends/memory.js';
6
6
  import { size_max } from '../constants.js';
7
- import { defaultContext } from '../internal/contexts.js';
7
+ import { contextOf } from '../internal/contexts.js';
8
8
  import { credentialsAllowRoot } from '../internal/credentials.js';
9
9
  import { withExceptionContext } from '../internal/error.js';
10
10
  import { join, resolve } from '../path.js';
@@ -53,9 +53,10 @@ export function umount(mountPoint) {
53
53
  * @internal @hidden
54
54
  */
55
55
  export function resolveMount(path, ctx, extra) {
56
- const root = ctx?.root || defaultContext.root;
56
+ const { root } = contextOf(ctx);
57
57
  const _exceptionContext = { path, ...extra };
58
- path = normalizePath(join(root, path));
58
+ path = normalizePath(join(root, path), true);
59
+ path = resolve.call(ctx, path);
59
60
  const sortedMounts = [...mounts].sort((a, b) => (a[0].length > b[0].length ? -1 : 1)); // descending order of the string length
60
61
  for (const [mountPoint, fs] of sortedMounts) {
61
62
  // We know path is normalized, so it would be a substring of the mount point.
@@ -94,7 +95,7 @@ export function _statfs(fs, bigint) {
94
95
  * @category Backends and Configuration
95
96
  */
96
97
  export function chroot(path) {
97
- const $ = this ?? defaultContext;
98
+ const $ = contextOf(this);
98
99
  if (!credentialsAllowRoot($.credentials))
99
100
  throw withErrno('EPERM', 'Can not chroot() as non-root user');
100
101
  $.root ??= '/';
package/dist/vfs/sync.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { setUVMessage, UV } from 'kerium';
2
2
  import { decodeUTF8 } from 'utilium';
3
3
  import * as constants from '../constants.js';
4
- import { defaultContext } from '../internal/contexts.js';
4
+ import { contextOf } from '../internal/contexts.js';
5
5
  import { hasAccess, isDirectory, isSymbolicLink } from '../internal/inode.js';
6
6
  import { basename, dirname, join, parse, resolve as resolvePath } from '../path.js';
7
7
  import { normalizeMode, normalizePath } from '../utils.js';
@@ -18,6 +18,7 @@ import { emitChange } from './watchers.js';
18
18
  * @internal @hidden
19
19
  */
20
20
  export function resolve($, path, preserveSymlinks, extra) {
21
+ path = resolvePath.call($, path);
21
22
  /* Try to resolve it directly. If this works,
22
23
  that means we don't need to perform any resolution for parent directories. */
23
24
  try {
@@ -87,7 +88,7 @@ export function open(path, opt) {
87
88
  if (checkAccess && !hasAccess(this, parentStats, constants.W_OK)) {
88
89
  throw UV('EACCES', 'open', path);
89
90
  }
90
- const { euid: uid, egid: gid } = this?.credentials ?? defaultContext.credentials;
91
+ const { euid: uid, egid: gid } = contextOf(this).credentials;
91
92
  const inode = fs.createFileSync(resolved, {
92
93
  mode,
93
94
  uid: parentStats.mode & constants.S_ISUID ? parentStats.uid : uid,
@@ -124,7 +125,7 @@ export function readlink(path) {
124
125
  export function mkdir(path, options = {}) {
125
126
  path = normalizePath(path);
126
127
  const { fs, path: resolved } = resolve(this, path);
127
- const { euid: uid, egid: gid } = this?.credentials ?? defaultContext.credentials;
128
+ const { euid: uid, egid: gid } = contextOf(this).credentials;
128
129
  const { mode = 0o777, recursive } = options;
129
130
  const __create = (path, resolved, parent) => {
130
131
  if (checkAccess && !hasAccess(this, parent, constants.W_OK))
@@ -75,5 +75,5 @@ export declare function removeWatcher(path: string, watcher: FSWatcher): void;
75
75
  /**
76
76
  * @internal @hidden
77
77
  */
78
- export declare function emitChange($: V_Context, eventType: fs.WatchEventType, filename: string): void;
78
+ export declare function emitChange(context: V_Context, eventType: fs.WatchEventType, filename: string): void;
79
79
  export {};
@@ -1,9 +1,10 @@
1
1
  import { EventEmitter } from 'eventemitter3';
2
2
  import { UV } from 'kerium';
3
- import { basename, dirname, join, relative } from '../path.js';
4
- import { normalizePath } from '../utils.js';
3
+ import { contextOf } from '../internal/contexts.js';
5
4
  import { isStatsEqual } from '../node/stats.js';
6
5
  import { statSync } from '../node/sync.js';
6
+ import { basename, dirname, join, relative } from '../path.js';
7
+ import { normalizePath } from '../utils.js';
7
8
  /**
8
9
  * Base class for file system watchers.
9
10
  * Provides event handling capabilities for watching file system changes.
@@ -61,9 +62,10 @@ export class FSWatcher extends Watcher {
61
62
  options;
62
63
  realpath;
63
64
  constructor(context, path, options) {
64
- super(context, path);
65
+ const $ = contextOf(context);
66
+ super($, path);
65
67
  this.options = options;
66
- this.realpath = context?.root ? join(context.root, path) : path;
68
+ this.realpath = join($.root, path);
67
69
  addWatcher(this.realpath, this);
68
70
  }
69
71
  close() {
@@ -145,7 +147,8 @@ export function removeWatcher(path, watcher) {
145
147
  /**
146
148
  * @internal @hidden
147
149
  */
148
- export function emitChange($, eventType, filename) {
150
+ export function emitChange(context, eventType, filename) {
151
+ const $ = contextOf(context);
149
152
  if ($)
150
153
  filename = join($.root ?? '/', filename);
151
154
  filename = normalizePath(filename);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenfs/core",
3
- "version": "2.4.3",
3
+ "version": "2.4.4",
4
4
  "description": "A filesystem, anywhere",
5
5
  "funding": {
6
6
  "type": "individual",
@@ -60,8 +60,7 @@ suite('Context', () => {
60
60
  await promise;
61
61
  });
62
62
 
63
- test('Path resolution of / with context root and mount point being the same', async () => {
64
- // @zenfs/core#226
63
+ test('Path resolution of / with context root and mount point being the same #226', async () => {
65
64
  await configure({
66
65
  mounts: { '/bananas': InMemory },
67
66
  });
@@ -73,8 +72,7 @@ suite('Context', () => {
73
72
  assert.deepEqual(bananas.fs.readdirSync('/'), ['yellow']);
74
73
  });
75
74
 
76
- test('Different working directory', { todo: true }, () => {
77
- // @zenfs/core#263
75
+ test('Different working directory #263', () => {
78
76
  ctx.mkdirSync('/test');
79
77
  context.pwd = '/test';
80
78
 
@@ -19,8 +19,7 @@ suite('Links', () => {
19
19
  assert(stats.isSymbolicLink());
20
20
  });
21
21
 
22
- test('lstat file inside symlinked directory', async () => {
23
- // @zenfs/core#241
22
+ test('lstat file inside symlinked directory #241', async () => {
24
23
  await fs.promises.mkdir('/a');
25
24
  await fs.promises.writeFile('/a/hello.txt', 'hello world');
26
25
  await fs.promises.symlink('/a', '/b');
@@ -70,8 +70,7 @@ suite('read', () => {
70
70
  assert.equal(bytesRead, expected.length);
71
71
  });
72
72
 
73
- test('read using callback API', async () => {
74
- // @zenfs/core#239
73
+ test('read using callback API #239', async () => {
75
74
  const path = '/text.txt';
76
75
 
77
76
  fs.writeFileSync(path, 'hello world');