@zenfs/core 1.3.3 → 1.3.5

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/file.js CHANGED
@@ -2,7 +2,7 @@ import { config } from './emulation/config.js';
2
2
  import { O_APPEND, O_CREAT, O_EXCL, O_RDONLY, O_RDWR, O_SYNC, O_TRUNC, O_WRONLY, S_IFMT, size_max } from './emulation/constants.js';
3
3
  import { Errno, ErrnoError } from './error.js';
4
4
  import './polyfills.js';
5
- import { Stats } from './stats.js';
5
+ import { _chown, Stats } from './stats.js';
6
6
  const validFlags = ['r', 'r+', 'rs', 'rs+', 'w', 'wx', 'w+', 'wx+', 'a', 'ax', 'a+', 'ax+'];
7
7
  export function parseFlag(flag) {
8
8
  if (typeof flag === 'number') {
@@ -411,8 +411,8 @@ export class PreloadFile extends File {
411
411
  throw ErrnoError.With('EBADF', this.path, 'File.chmod');
412
412
  }
413
413
  this.dirty = true;
414
- this.stats.chmod(mode);
415
- if (config.syncImmediately)
414
+ this.stats.mode = (this.stats.mode & (mode > S_IFMT ? ~S_IFMT : S_IFMT)) | mode;
415
+ if (config.syncImmediately || mode > S_IFMT)
416
416
  await this.sync();
417
417
  }
418
418
  chmodSync(mode) {
@@ -420,8 +420,8 @@ export class PreloadFile extends File {
420
420
  throw ErrnoError.With('EBADF', this.path, 'File.chmod');
421
421
  }
422
422
  this.dirty = true;
423
- this.stats.chmod(mode);
424
- if (config.syncImmediately)
423
+ this.stats.mode = (this.stats.mode & (mode > S_IFMT ? ~S_IFMT : S_IFMT)) | mode;
424
+ if (config.syncImmediately || mode > S_IFMT)
425
425
  this.syncSync();
426
426
  }
427
427
  async chown(uid, gid) {
@@ -429,7 +429,7 @@ export class PreloadFile extends File {
429
429
  throw ErrnoError.With('EBADF', this.path, 'File.chown');
430
430
  }
431
431
  this.dirty = true;
432
- this.stats.chown(uid, gid);
432
+ _chown(this.stats, uid, gid);
433
433
  if (config.syncImmediately)
434
434
  await this.sync();
435
435
  }
@@ -438,7 +438,7 @@ export class PreloadFile extends File {
438
438
  throw ErrnoError.With('EBADF', this.path, 'File.chown');
439
439
  }
440
440
  this.dirty = true;
441
- this.stats.chown(uid, gid);
441
+ _chown(this.stats, uid, gid);
442
442
  if (config.syncImmediately)
443
443
  this.syncSync();
444
444
  }
@@ -462,22 +462,6 @@ export class PreloadFile extends File {
462
462
  if (config.syncImmediately)
463
463
  this.syncSync();
464
464
  }
465
- async _setType(type) {
466
- if (this.closed) {
467
- throw ErrnoError.With('EBADF', this.path, 'File._setType');
468
- }
469
- this.dirty = true;
470
- this.stats.mode = (this.stats.mode & ~S_IFMT) | type;
471
- await this.sync();
472
- }
473
- _setTypeSync(type) {
474
- if (this.closed) {
475
- throw ErrnoError.With('EBADF', this.path, 'File._setType');
476
- }
477
- this.dirty = true;
478
- this.stats.mode = (this.stats.mode & ~S_IFMT) | type;
479
- this.syncSync();
480
- }
481
465
  }
482
466
  /**
483
467
  * For the file systems which do not sync to anything.
package/dist/stats.d.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import type * as Node from 'node:fs';
2
- import { type Credentials } from './credentials.js';
3
2
  import { S_IFDIR, S_IFLNK, S_IFREG } from './emulation/constants.js';
4
3
  /**
5
4
  * Indicates the type of a file. Applied to 'mode'.
@@ -43,6 +42,10 @@ export interface StatsLike<T extends number | bigint = number | bigint> {
43
42
  * Inode number
44
43
  */
45
44
  ino: T;
45
+ /**
46
+ * Number of hard links
47
+ */
48
+ nlink: T;
46
49
  }
47
50
  /**
48
51
  * Provides information about a particular entry in the file system.
@@ -136,28 +139,29 @@ export declare abstract class StatsCommon<T extends number | bigint> implements
136
139
  * @internal
137
140
  */
138
141
  hasAccess(mode: number): boolean;
139
- /**
140
- * Convert the current stats object into a credentials object
141
- * @internal
142
- */
143
- cred(uid?: number, gid?: number): Credentials;
144
142
  /**
145
143
  * Change the mode of the file.
146
144
  * We use this helper function to prevent messing up the type of the file.
147
145
  * @internal
146
+ * @deprecated This will be removed in the next minor release since it is internal
148
147
  */
149
148
  chmod(mode: number): void;
150
149
  /**
151
150
  * Change the owner user/group of the file.
152
151
  * This function makes sure it is a valid UID/GID (that is, a 32 unsigned int)
153
152
  * @internal
153
+ * @deprecated This will be removed in the next minor release since it is internal
154
154
  */
155
- chown(uid: number | bigint, gid: number | bigint): void;
155
+ chown(uid: number, gid: number): void;
156
156
  get atimeNs(): bigint;
157
157
  get mtimeNs(): bigint;
158
158
  get ctimeNs(): bigint;
159
159
  get birthtimeNs(): bigint;
160
160
  }
161
+ /**
162
+ * @hidden @internal
163
+ */
164
+ export declare function _chown(stats: Partial<StatsLike<number>>, uid: number, gid: number): void;
161
165
  /**
162
166
  * Implementation of Node's `Stats`.
163
167
  *
package/dist/stats.js CHANGED
@@ -124,7 +124,7 @@ export class StatsCommon {
124
124
  perm |= X_OK;
125
125
  }
126
126
  // Group permissions
127
- if (credentials.gid === this.gid) {
127
+ if (credentials.gid === this.gid || credentials.groups.includes(Number(this.gid))) {
128
128
  if (this.mode & S_IRGRP)
129
129
  perm |= R_OK;
130
130
  if (this.mode & S_IWGRP)
@@ -142,24 +142,11 @@ export class StatsCommon {
142
142
  // Perform the access check
143
143
  return (perm & mode) === mode;
144
144
  }
145
- /**
146
- * Convert the current stats object into a credentials object
147
- * @internal
148
- */
149
- cred(uid = Number(this.uid), gid = Number(this.gid)) {
150
- return {
151
- uid,
152
- gid,
153
- suid: Number(this.uid),
154
- sgid: Number(this.gid),
155
- euid: uid,
156
- egid: gid,
157
- };
158
- }
159
145
  /**
160
146
  * Change the mode of the file.
161
147
  * We use this helper function to prevent messing up the type of the file.
162
148
  * @internal
149
+ * @deprecated This will be removed in the next minor release since it is internal
163
150
  */
164
151
  chmod(mode) {
165
152
  this.mode = this._convert((this.mode & S_IFMT) | mode);
@@ -168,6 +155,7 @@ export class StatsCommon {
168
155
  * Change the owner user/group of the file.
169
156
  * This function makes sure it is a valid UID/GID (that is, a 32 unsigned int)
170
157
  * @internal
158
+ * @deprecated This will be removed in the next minor release since it is internal
171
159
  */
172
160
  chown(uid, gid) {
173
161
  uid = Number(uid);
@@ -192,6 +180,17 @@ export class StatsCommon {
192
180
  return BigInt(this.birthtimeMs) * 1000n;
193
181
  }
194
182
  }
183
+ /**
184
+ * @hidden @internal
185
+ */
186
+ export function _chown(stats, uid, gid) {
187
+ if (!isNaN(uid) && 0 <= uid && uid < 2 ** 32) {
188
+ stats.uid = uid;
189
+ }
190
+ if (!isNaN(gid) && 0 <= gid && gid < 2 ** 32) {
191
+ stats.gid = gid;
192
+ }
193
+ }
195
194
  /**
196
195
  * Implementation of Node's `Stats`.
197
196
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenfs/core",
3
- "version": "1.3.3",
3
+ "version": "1.3.5",
4
4
  "description": "A filesystem, anywhere",
5
5
  "funding": {
6
6
  "type": "individual",
@@ -74,10 +74,12 @@
74
74
  "@types/readable-stream": "^4.0.10",
75
75
  "buffer": "^6.0.3",
76
76
  "eventemitter3": "^5.0.1",
77
- "minimatch": "^9.0.3",
78
77
  "readable-stream": "^4.5.2",
79
78
  "utilium": "^1.0.0"
80
79
  },
80
+ "optionalDependencies": {
81
+ "minimatch": "^9.0.3"
82
+ },
81
83
  "devDependencies": {
82
84
  "@eslint/js": "^9.8.0",
83
85
  "@types/eslint__js": "^8.42.3",
package/readme.md CHANGED
@@ -64,6 +64,8 @@ await configure({
64
64
  };
65
65
  ```
66
66
 
67
+ Note that while you aren't required to use absolute paths for the keys of `mounts`, it is a good practice to do so.
68
+
67
69
  > [!TIP]
68
70
  > When configuring a mount point, you can pass in
69
71
  >
@@ -1,7 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { readdirSync, statSync, writeFileSync } from 'node:fs';
3
- import { minimatch } from 'minimatch';
4
- import { join, relative, resolve } from 'node:path/posix';
3
+ import _path from 'node:path/posix';
5
4
  import { parseArgs } from 'node:util';
6
5
 
7
6
  const { values: options, positionals } = parseArgs({
@@ -37,6 +36,24 @@ if (options.quiet && options.verbose) {
37
36
  process.exit();
38
37
  }
39
38
 
39
+ let matchesGlob = _path.matchesGlob;
40
+
41
+ if (matchesGlob && options.verbose) {
42
+ console.debug('[debug] path.matchesGlob is available.');
43
+ }
44
+
45
+ if (!matchesGlob) {
46
+ console.warn('Warning: path.matchesGlob is not available, falling back to minimatch. (Node 20.17.0+ or 22.5.0+ needed)');
47
+
48
+ try {
49
+ const { minimatch } = await import('minimatch');
50
+ matchesGlob = minimatch;
51
+ } catch {
52
+ console.error('Fatal error: Failed to fall back to minimatch (is it installed?)');
53
+ process.exit(1);
54
+ }
55
+ }
56
+
40
57
  function fixSlash(path) {
41
58
  return path.replaceAll('\\', '/');
42
59
  }
@@ -71,7 +88,7 @@ const entries = new Map();
71
88
 
72
89
  function computeEntries(path) {
73
90
  try {
74
- if (options.ignore.some(pattern => minimatch(path, pattern))) {
91
+ if (options.ignore.some(pattern => matchesGlob(path, pattern))) {
75
92
  if (!options.quiet) console.log(`${color('yellow', 'skip')} ${path}`);
76
93
  return;
77
94
  }
@@ -79,7 +96,7 @@ function computeEntries(path) {
79
96
  const stats = statSync(path);
80
97
 
81
98
  if (stats.isFile()) {
82
- entries.set('/' + relative(resolvedRoot, path), stats);
99
+ entries.set('/' + _path.relative(resolvedRoot, path), stats);
83
100
  if (options.verbose) {
84
101
  console.log(`${color('green', 'file')} ${path}`);
85
102
  }
@@ -87,9 +104,9 @@ function computeEntries(path) {
87
104
  }
88
105
 
89
106
  for (const file of readdirSync(path)) {
90
- computeEntries(join(path, file));
107
+ computeEntries(_path.join(path, file));
91
108
  }
92
- entries.set('/' + relative(resolvedRoot, path), stats);
109
+ entries.set('/' + _path.relative(resolvedRoot, path), stats);
93
110
  if (options.verbose) {
94
111
  console.log(`${color('bright_green', ' dir')} ${path}`);
95
112
  }
@@ -102,7 +119,7 @@ function computeEntries(path) {
102
119
 
103
120
  computeEntries(resolvedRoot);
104
121
  if (!options.quiet) {
105
- console.log('Generated listing for ' + fixSlash(resolve(root)));
122
+ console.log('Generated listing for ' + fixSlash(_path.resolve(root)));
106
123
  }
107
124
 
108
125
  const index = {
@@ -6,7 +6,7 @@ import { Errno, ErrnoError } from '../../error.js';
6
6
  import { File } from '../../file.js';
7
7
  import { FileSystem, type FileSystemMetadata } from '../../filesystem.js';
8
8
  import { Async } from '../../mixins/async.js';
9
- import { Stats, type FileType } from '../../stats.js';
9
+ import { Stats } from '../../stats.js';
10
10
  import type { Backend, FilesystemOf } from '../backend.js';
11
11
  import { InMemory } from '../memory.js';
12
12
  import * as RPC from './rpc.js';
@@ -104,10 +104,6 @@ export class PortFile extends File {
104
104
  this._throwNoSync('utimes');
105
105
  }
106
106
 
107
- public _setType(type: FileType): Promise<void> {
108
- return this.rpc('_setType', type);
109
- }
110
-
111
107
  public _setTypeSync(): void {
112
108
  this._throwNoSync('_setType');
113
109
  }
package/src/config.ts CHANGED
@@ -1,11 +1,10 @@
1
1
  import type { Backend, BackendConfiguration, FilesystemOf, SharedConfig } from './backends/backend.js';
2
2
  import { checkOptions, isBackend, isBackendConfig } from './backends/backend.js';
3
- import { credentials } from './credentials.js';
3
+ import { useCredentials } from './credentials.js';
4
4
  import { DeviceFS } from './devices.js';
5
5
  import * as cache from './emulation/cache.js';
6
6
  import { config } from './emulation/config.js';
7
7
  import * as fs from './emulation/index.js';
8
- import type { AbsolutePath } from './emulation/path.js';
9
8
  import { Errno, ErrnoError } from './error.js';
10
9
  import { FileSystem } from './filesystem.js';
11
10
 
@@ -68,8 +67,11 @@ export async function resolveMountConfig<T extends Backend>(configuration: Mount
68
67
  return mount;
69
68
  }
70
69
 
70
+ /**
71
+ * An object mapping mount points to backends
72
+ */
71
73
  export interface ConfigMounts {
72
- [K: AbsolutePath]: Backend;
74
+ [K: string]: Backend;
73
75
  }
74
76
 
75
77
  /**
@@ -79,7 +81,7 @@ export interface Configuration<T extends ConfigMounts> extends SharedConfig {
79
81
  /**
80
82
  * An object mapping mount points to mount configuration
81
83
  */
82
- mounts: { [K in keyof T & AbsolutePath]: MountConfiguration<T[K]> };
84
+ mounts: { [K in keyof T]: MountConfiguration<T[K]> };
83
85
 
84
86
  /**
85
87
  * The uid to use
@@ -188,7 +190,7 @@ export async function configure<T extends ConfigMounts>(configuration: Partial<C
188
190
  const uid = 'uid' in configuration ? configuration.uid || 0 : 0;
189
191
  const gid = 'gid' in configuration ? configuration.gid || 0 : 0;
190
192
 
191
- Object.assign(credentials, { uid, gid, suid: uid, sgid: gid, euid: uid, egid: gid });
193
+ useCredentials({ uid, gid });
192
194
 
193
195
  cache.stats.isEnabled = configuration.cacheStats ?? false;
194
196
  cache.paths.isEnabled = configuration.cachePaths ?? false;
@@ -200,10 +202,8 @@ export async function configure<T extends ConfigMounts>(configuration: Partial<C
200
202
  const toMount: [string, FileSystem][] = [];
201
203
  let unmountRoot = false;
202
204
 
203
- for (const [point, mountConfig] of Object.entries(configuration.mounts)) {
204
- if (!point.startsWith('/')) {
205
- throw new ErrnoError(Errno.EINVAL, 'Mount points must have absolute paths');
206
- }
205
+ for (const [_point, mountConfig] of Object.entries(configuration.mounts)) {
206
+ const point = _point.startsWith('/') ? _point : '/' + _point;
207
207
 
208
208
  if (isBackendConfig(mountConfig)) {
209
209
  mountConfig.disableAsyncCache ??= configuration.disableAsyncCache || false;
@@ -10,6 +10,10 @@ export interface Credentials {
10
10
  sgid: number;
11
11
  euid: number;
12
12
  egid: number;
13
+ /**
14
+ * List of group IDs.
15
+ */
16
+ groups: number[];
13
17
  }
14
18
 
15
19
  export const credentials: Credentials = {
@@ -19,16 +23,27 @@ export const credentials: Credentials = {
19
23
  sgid: 0,
20
24
  euid: 0,
21
25
  egid: 0,
26
+ groups: [],
22
27
  };
23
28
 
29
+ export interface CredentialInit {
30
+ uid: number;
31
+ gid: number;
32
+ suid?: number;
33
+ sgid?: number;
34
+ euid?: number;
35
+ egid?: number;
36
+ }
37
+
24
38
  /**
25
- * @deprecated
39
+ * Uses credentials from the provided uid and gid.
26
40
  */
27
- export const rootCredentials: Credentials = {
28
- uid: 0,
29
- gid: 0,
30
- suid: 0,
31
- sgid: 0,
32
- euid: 0,
33
- egid: 0,
34
- };
41
+ export function useCredentials(source: CredentialInit): void {
42
+ Object.assign(credentials, {
43
+ suid: source.uid,
44
+ sgid: source.gid,
45
+ euid: source.uid,
46
+ egid: source.gid,
47
+ ...source,
48
+ });
49
+ }
@@ -13,10 +13,17 @@ export class Cache<T> {
13
13
 
14
14
  protected async = new Map<string, Promise<T>>();
15
15
 
16
+ /**
17
+ * Whether the data exists in the cache
18
+ */
19
+ has(path: string): boolean {
20
+ return this.isEnabled && this.sync.has(path);
21
+ }
22
+
16
23
  /**
17
24
  * Gets data from the cache, if is exists and the cache is enabled.
18
25
  */
19
- getSync(path: string): T | undefined {
26
+ get(path: string): T | undefined {
20
27
  if (!this.isEnabled) return;
21
28
 
22
29
  return this.sync.get(path);
@@ -25,7 +32,7 @@ export class Cache<T> {
25
32
  /**
26
33
  * Adds data if the cache is enabled
27
34
  */
28
- setSync(path: string, value: T): void {
35
+ set(path: string, value: T): void {
29
36
  if (!this.isEnabled) return;
30
37
 
31
38
  this.sync.set(path, value);
@@ -33,18 +40,16 @@ export class Cache<T> {
33
40
  }
34
41
 
35
42
  /**
36
- * Clears the cache if it is enabled
43
+ * Whether the data exists in the cache
37
44
  */
38
- clearSync(): void {
39
- if (!this.isEnabled) return;
40
-
41
- this.sync.clear();
45
+ hasAsync(path: string): boolean {
46
+ return this.isEnabled && this.async.has(path);
42
47
  }
43
48
 
44
49
  /**
45
50
  * Gets data from the cache, if it exists and the cache is enabled.
46
51
  */
47
- get(path: string): Promise<T> | undefined {
52
+ getAsync(path: string): Promise<T> | undefined {
48
53
  if (!this.isEnabled) return;
49
54
 
50
55
  return this.async.get(path);
@@ -53,7 +58,7 @@ export class Cache<T> {
53
58
  /**
54
59
  * Adds data if the cache is enabled
55
60
  */
56
- set(path: string, value: Promise<T>): void {
61
+ setAsync(path: string, value: Promise<T>): void {
57
62
  if (!this.isEnabled) return;
58
63
 
59
64
  this.async.set(path, value);
@@ -65,7 +70,7 @@ export class Cache<T> {
65
70
  */
66
71
  clear(): void {
67
72
  if (!this.isEnabled) return;
68
-
73
+ this.sync.clear();
69
74
  this.async.clear();
70
75
  }
71
76
  }
@@ -16,7 +16,7 @@ import * as cache from './cache.js';
16
16
  import { config } from './config.js';
17
17
  import * as constants from './constants.js';
18
18
  import { Dir, Dirent } from './dir.js';
19
- import { dirname, join, parse } from './path.js';
19
+ import { dirname, join, parse, resolve } from './path.js';
20
20
  import { _statfs, fd2file, fdMap, file2fd, fixError, resolveMount, type InternalOptions, type ReaddirOptions } from './shared.js';
21
21
  import { ReadStream, WriteStream } from './streams.js';
22
22
  import { FSWatcher, emitChange } from './watchers.js';
@@ -395,6 +395,7 @@ export async function rename(oldPath: fs.PathLike, newPath: fs.PathLike): Promis
395
395
  if (src.mountPoint == dst.mountPoint) {
396
396
  await src.fs.rename(src.path, dst.path);
397
397
  emitChange('rename', oldPath.toString());
398
+ emitChange('change', newPath.toString());
398
399
  return;
399
400
  }
400
401
  await writeFile(newPath, await readFile(oldPath));
@@ -471,7 +472,7 @@ export async function unlink(path: fs.PathLike): Promise<void> {
471
472
  path = normalizePath(path);
472
473
  const { fs, path: resolved } = resolveMount(path);
473
474
  try {
474
- if (config.checkAccess && !(await (cache.stats.get(path) || fs.stat(resolved))).hasAccess(constants.W_OK)) {
475
+ if (config.checkAccess && !(await (cache.stats.getAsync(path) || fs.stat(resolved))).hasAccess(constants.W_OK)) {
475
476
  throw ErrnoError.With('EACCES', resolved, 'unlink');
476
477
  }
477
478
  await fs.unlink(resolved);
@@ -623,7 +624,7 @@ export async function rmdir(path: fs.PathLike): Promise<void> {
623
624
  path = await realpath(path);
624
625
  const { fs, path: resolved } = resolveMount(path);
625
626
  try {
626
- const stats = await (cache.stats.get(path) || fs.stat(resolved));
627
+ const stats = await (cache.stats.getAsync(path) || fs.stat(resolved));
627
628
  if (!stats) {
628
629
  throw ErrnoError.With('ENOENT', path, 'rmdir');
629
630
  }
@@ -718,8 +719,8 @@ export async function readdir(
718
719
 
719
720
  const { fs, path: resolved } = resolveMount(path);
720
721
 
721
- const _stats = cache.stats.get(path) || fs.stat(resolved).catch(handleError);
722
- cache.stats.set(path, _stats);
722
+ const _stats = cache.stats.getAsync(path) || fs.stat(resolved).catch(handleError);
723
+ cache.stats.setAsync(path, _stats);
723
724
  const stats = await _stats;
724
725
 
725
726
  if (!stats) {
@@ -740,8 +741,8 @@ export async function readdir(
740
741
  const addEntry = async (entry: string) => {
741
742
  let entryStats: Stats | undefined;
742
743
  if (options?.recursive || options?.withFileTypes) {
743
- const _entryStats = cache.stats.get(join(path, entry)) || fs.stat(join(resolved, entry)).catch(handleError);
744
- cache.stats.set(join(path, entry), _entryStats);
744
+ const _entryStats = cache.stats.getAsync(join(path, entry)) || fs.stat(join(resolved, entry)).catch(handleError);
745
+ cache.stats.setAsync(join(path, entry), _entryStats);
745
746
  entryStats = await _entryStats;
746
747
  }
747
748
  if (options?.withFileTypes) {
@@ -822,7 +823,7 @@ export async function symlink(target: fs.PathLike, path: fs.PathLike, type: fs.s
822
823
 
823
824
  await using handle = await _open(path, 'w+', 0o644, false);
824
825
  await handle.writeFile(target.toString());
825
- await handle.file._setType(constants.S_IFLNK);
826
+ await handle.file.chmod(constants.S_IFLNK);
826
827
  }
827
828
  symlink satisfies typeof promises.symlink;
828
829
 
@@ -891,20 +892,25 @@ export async function realpath(path: fs.PathLike, options: fs.BufferEncodingOpti
891
892
  export async function realpath(path: fs.PathLike, options?: fs.EncodingOption | BufferEncoding): Promise<string>;
892
893
  export async function realpath(path: fs.PathLike, options?: fs.EncodingOption | BufferEncoding | fs.BufferEncodingOption): Promise<string | Buffer> {
893
894
  path = normalizePath(path);
895
+ if (cache.paths.hasAsync(path)) return cache.paths.getAsync(path)!;
894
896
  const { base, dir } = parse(path);
895
- const lpath = join(dir == '/' ? '/' : await (cache.paths.get(dir) || realpath(dir)), base);
896
- const { fs, path: resolvedPath, mountPoint } = resolveMount(lpath);
897
+ const realDir = dir == '/' ? '/' : await (cache.paths.getAsync(dir) || realpath(dir));
898
+ const lpath = join(realDir, base);
899
+ const { fs, path: resolvedPath } = resolveMount(lpath);
897
900
 
898
901
  try {
899
- const _stats = cache.stats.get(lpath) || fs.stat(resolvedPath);
900
- cache.stats.set(lpath, _stats);
902
+ const _stats = cache.stats.getAsync(lpath) || fs.stat(resolvedPath);
903
+ cache.stats.setAsync(lpath, _stats);
901
904
  if (!(await _stats).isSymbolicLink()) {
905
+ cache.paths.set(path, lpath);
902
906
  return lpath;
903
907
  }
904
908
 
905
- const target = mountPoint + (await readlink(lpath));
909
+ const target = resolve(realDir, await readlink(lpath));
906
910
 
907
- return await (cache.paths.get(target) || realpath(target));
911
+ const real = cache.paths.getAsync(target) || realpath(target);
912
+ cache.paths.setAsync(path, real);
913
+ return await real;
908
914
  } catch (e) {
909
915
  if ((e as ErrnoError).code == 'ENOENT') {
910
916
  return path;
@@ -968,7 +974,7 @@ access satisfies typeof promises.access;
968
974
  export async function rm(path: fs.PathLike, options?: fs.RmOptions & InternalOptions) {
969
975
  path = normalizePath(path);
970
976
 
971
- const stats = await (cache.stats.get(path) ||
977
+ const stats = await (cache.stats.getAsync(path) ||
972
978
  stat(path).catch((error: ErrnoError) => {
973
979
  if (error.code == 'ENOENT' && options?.force) return undefined;
974
980
  throw error;
@@ -978,7 +984,7 @@ export async function rm(path: fs.PathLike, options?: fs.RmOptions & InternalOpt
978
984
  return;
979
985
  }
980
986
 
981
- cache.stats.setSync(path, stats);
987
+ cache.stats.set(path, stats);
982
988
 
983
989
  switch (stats.mode & constants.S_IFMT) {
984
990
  case constants.S_IFDIR:
@@ -992,10 +998,10 @@ export async function rm(path: fs.PathLike, options?: fs.RmOptions & InternalOpt
992
998
  break;
993
999
  case constants.S_IFREG:
994
1000
  case constants.S_IFLNK:
995
- await unlink(path);
996
- break;
997
1001
  case constants.S_IFBLK:
998
1002
  case constants.S_IFCHR:
1003
+ await unlink(path);
1004
+ break;
999
1005
  case constants.S_IFIFO:
1000
1006
  case constants.S_IFSOCK:
1001
1007
  default:
@@ -8,6 +8,7 @@ import type { FileSystem } from '../filesystem.js';
8
8
  import { normalizePath } from '../utils.js';
9
9
  import { resolve, type AbsolutePath } from './path.js';
10
10
  import { size_max } from './constants.js';
11
+ import { paths as pathCache } from './cache.js';
11
12
 
12
13
  // descriptors
13
14
  export const fdMap: Map<number, File> = new Map();
@@ -47,6 +48,7 @@ export function mount(mountPoint: string, fs: FileSystem): void {
47
48
  throw new ErrnoError(Errno.EINVAL, 'Mount point ' + mountPoint + ' is already in use.');
48
49
  }
49
50
  mounts.set(mountPoint, fs);
51
+ pathCache.clear();
50
52
  }
51
53
 
52
54
  /**
@@ -61,6 +63,7 @@ export function umount(mountPoint: string): void {
61
63
  throw new ErrnoError(Errno.EINVAL, 'Mount point ' + mountPoint + ' is already unmounted.');
62
64
  }
63
65
  mounts.delete(mountPoint);
66
+ pathCache.clear();
64
67
  }
65
68
 
66
69
  /**