@zenfs/core 1.9.4 → 1.9.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.
@@ -10,7 +10,6 @@ export declare class InMemoryStore extends Map<number, Uint8Array> implements Sy
10
10
  readonly name = "tmpfs";
11
11
  constructor(label?: string | undefined);
12
12
  sync(): Promise<void>;
13
- clearSync(): void;
14
13
  transaction(): SyncMapTransaction;
15
14
  }
16
15
  declare const _InMemory: {
@@ -12,9 +12,6 @@ export class InMemoryStore extends Map {
12
12
  this.name = 'tmpfs';
13
13
  }
14
14
  async sync() { }
15
- clearSync() {
16
- this.clear();
17
- }
18
15
  transaction() {
19
16
  return new SyncMapTransaction(this);
20
17
  }
@@ -145,7 +145,7 @@ export async function resolveRemoteMount(port, config, _depth = 0) {
145
145
  const stopAndReplay = RPC.catchMessages(port);
146
146
  const fs = await resolveMountConfig(config, _depth);
147
147
  attachFS(port, fs);
148
- stopAndReplay(fs);
148
+ await stopAndReplay(fs);
149
149
  info('Resolved remote mount: ' + fs.toString());
150
150
  return fs;
151
151
  }
@@ -10,7 +10,7 @@ type _MessageEvent<T = any> = T | {
10
10
  /** @internal */
11
11
  export interface Port {
12
12
  postMessage(value: unknown, transfer?: TransferListItem[]): void;
13
- on?(event: 'message', listener: (value: unknown) => void): this;
13
+ on?(event: 'message' | 'online', listener: (value: unknown) => void): this;
14
14
  off?(event: 'message', listener: (value: unknown) => void): this;
15
15
  addEventListener?(type: 'message', listener: (ev: _MessageEvent) => void): void;
16
16
  removeEventListener?(type: 'message', listener: (ev: _MessageEvent) => void): void;
@@ -70,4 +70,8 @@ export declare function request<const TRequest extends Request, TValue>(request:
70
70
  export declare function handleResponse<const TResponse extends Response>(response: TResponse): void;
71
71
  export declare function attach<T extends Message>(port: Port, handler: (message: T) => unknown): void;
72
72
  export declare function detach<T extends Message>(port: Port, handler: (message: T) => unknown): void;
73
- export declare function catchMessages<T extends Backend>(port: Port): (fs: FilesystemOf<T>) => void;
73
+ export declare function catchMessages<T extends Backend>(port: Port): (fs: FilesystemOf<T>) => Promise<void>;
74
+ /**
75
+ * @internal
76
+ */
77
+ export declare function waitOnline(port: Port): Promise<void>;
@@ -64,7 +64,7 @@ export function attach(port, handler) {
64
64
  throw err(new ErrnoError(Errno.EINVAL, 'Cannot attach to non-existent port'));
65
65
  info('Attached handler to port: ' + handler.name);
66
66
  port['on' in port ? 'on' : 'addEventListener']('message', (message) => {
67
- handler('data' in message ? message.data : message);
67
+ handler(typeof message == 'object' && message !== null && 'data' in message ? message.data : message);
68
68
  });
69
69
  }
70
70
  export function detach(port, handler) {
@@ -72,18 +72,29 @@ export function detach(port, handler) {
72
72
  throw err(new ErrnoError(Errno.EINVAL, 'Cannot detach from non-existent port'));
73
73
  info('Detached handler from port: ' + handler.name);
74
74
  port['off' in port ? 'off' : 'removeEventListener']('message', (message) => {
75
- handler('data' in message ? message.data : message);
75
+ handler(typeof message == 'object' && message !== null && 'data' in message ? message.data : message);
76
76
  });
77
77
  }
78
78
  export function catchMessages(port) {
79
79
  const events = [];
80
80
  const handler = events.push.bind(events);
81
81
  attach(port, handler);
82
- return function (fs) {
82
+ return async function (fs) {
83
83
  detach(port, handler);
84
84
  for (const event of events) {
85
85
  const request = 'data' in event ? event.data : event;
86
- void handleRequest(port, fs, request);
86
+ await handleRequest(port, fs, request);
87
87
  }
88
88
  };
89
89
  }
90
+ /**
91
+ * @internal
92
+ */
93
+ export async function waitOnline(port) {
94
+ if (!('on' in port))
95
+ return; // Only need to wait in Node.js
96
+ const online = Promise.withResolvers();
97
+ setTimeout(online.reject, 500);
98
+ port.on('online', online.resolve);
99
+ await online.promise;
100
+ }
package/dist/utils.d.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  import type * as fs from 'node:fs';
2
2
  import type { ClassLike, OptionalTuple } from 'utilium';
3
3
  import { ErrnoError } from './internal/error.js';
4
- import type { AbsolutePath } from './vfs/path.js';
5
4
  declare global {
6
5
  function atob(data: string): string;
7
6
  function btoa(data: string): string;
@@ -54,7 +53,7 @@ export declare function normalizeTime(time: string | number | Date): number;
54
53
  * Normalizes a path
55
54
  * @internal
56
55
  */
57
- export declare function normalizePath(p: fs.PathLike): AbsolutePath;
56
+ export declare function normalizePath(p: fs.PathLike, noResolve?: boolean): string;
58
57
  /**
59
58
  * Normalizes options
60
59
  * @param options options to normalize
package/dist/utils.js CHANGED
@@ -101,15 +101,23 @@ export function normalizeTime(time) {
101
101
  * Normalizes a path
102
102
  * @internal
103
103
  */
104
- export function normalizePath(p) {
104
+ export function normalizePath(p, noResolve = false) {
105
+ if (p instanceof URL) {
106
+ if (p.protocol != 'file:')
107
+ throw new ErrnoError(Errno.EINVAL, 'URLs must use the file: protocol');
108
+ p = p.pathname;
109
+ }
105
110
  p = p.toString();
111
+ if (p.startsWith('file://'))
112
+ p = p.slice('file://'.length);
106
113
  if (p.includes('\x00')) {
107
114
  throw new ErrnoError(Errno.EINVAL, 'Path can not contain null character');
108
115
  }
109
116
  if (p.length == 0) {
110
117
  throw new ErrnoError(Errno.EINVAL, 'Path can not be empty');
111
118
  }
112
- return resolve(p.replaceAll(/[/\\]+/g, '/'));
119
+ p = p.replaceAll(/[/\\]+/g, '/');
120
+ return noResolve ? p : resolve(p);
113
121
  }
114
122
  /**
115
123
  * Normalizes options
@@ -89,9 +89,8 @@ export class FileHandle {
89
89
  */
90
90
  async chmod(mode) {
91
91
  const numMode = normalizeMode(mode, -1);
92
- if (numMode < 0) {
92
+ if (numMode < 0)
93
93
  throw new ErrnoError(Errno.EINVAL, 'Invalid mode.');
94
- }
95
94
  await this.file.chmod(numMode);
96
95
  this._emitChange();
97
96
  }
@@ -487,18 +486,18 @@ async function _open($, path, opt) {
487
486
  var _a;
488
487
  path = normalizePath(path);
489
488
  const mode = normalizeMode(opt.mode, 0o644), flag = parseFlag(opt.flag);
490
- const { fullPath: realpath, fs, path: resolved, stats } = await _resolve($, path.toString(), opt.preserveSymlinks);
489
+ const { fullPath, fs, path: resolved, stats } = await _resolve($, path.toString(), opt.preserveSymlinks);
491
490
  if (!stats) {
492
491
  if ((!isWriteable(flag) && !isAppendable(flag)) || flag == 'r+') {
493
- throw ErrnoError.With('ENOENT', realpath, '_open');
492
+ throw ErrnoError.With('ENOENT', fullPath, '_open');
494
493
  }
495
494
  // Create the file
496
495
  const parentStats = await fs.stat(dirname(resolved));
497
496
  if (config.checkAccess && !parentStats.hasAccess(constants.W_OK, $)) {
498
- throw ErrnoError.With('EACCES', dirname(realpath), '_open');
497
+ throw ErrnoError.With('EACCES', dirname(fullPath), '_open');
499
498
  }
500
499
  if (!parentStats.isDirectory()) {
501
- throw ErrnoError.With('ENOTDIR', dirname(realpath), '_open');
500
+ throw ErrnoError.With('ENOTDIR', dirname(fullPath), '_open');
502
501
  }
503
502
  const { euid: uid, egid: gid } = (_a = $ === null || $ === void 0 ? void 0 : $.credentials) !== null && _a !== void 0 ? _a : credentials;
504
503
  const file = await fs.createFile(resolved, flag, mode, { uid, gid });
@@ -506,10 +505,10 @@ async function _open($, path, opt) {
506
505
  return new FileHandle(file, $);
507
506
  }
508
507
  if (config.checkAccess && !stats.hasAccess(flagToMode(flag), $)) {
509
- throw ErrnoError.With('EACCES', realpath, '_open');
508
+ throw ErrnoError.With('EACCES', fullPath, '_open');
510
509
  }
511
510
  if (isExclusive(flag)) {
512
- throw ErrnoError.With('EEXIST', realpath, '_open');
511
+ throw ErrnoError.With('EEXIST', fullPath, '_open');
513
512
  }
514
513
  const handle = new FileHandle(await fs.openFile(resolved, flag), $);
515
514
  /*
@@ -768,11 +767,11 @@ export async function symlink(target, path, type = 'file') {
768
767
  if (!['file', 'dir', 'junction'].includes(type)) {
769
768
  throw new ErrnoError(Errno.EINVAL, 'Invalid symlink type: ' + type);
770
769
  }
771
- if (await exists.call(this, path)) {
772
- throw ErrnoError.With('EEXIST', path.toString(), 'symlink');
773
- }
770
+ path = normalizePath(path);
771
+ if (await exists.call(this, path))
772
+ throw ErrnoError.With('EEXIST', path, 'symlink');
774
773
  const handle = __addDisposableResource(env_5, await _open(this, path, { flag: 'w+', mode: 0o644, preserveSymlinks: true }), true);
775
- await handle.writeFile(target.toString());
774
+ await handle.writeFile(normalizePath(target, true));
776
775
  await handle.file.chmod(constants.S_IFLNK);
777
776
  }
778
777
  catch (e_5) {
@@ -965,8 +964,7 @@ async function _resolve($, path, preserveSymlinks) {
965
964
  return { ...resolved, fullPath: maybePath, stats };
966
965
  }
967
966
  const target = resolve(realDir, (await readlink.call($, maybePath)).toString());
968
- const real = await realpath.call($, target);
969
- return { ...resolved, fullPath: real, stats };
967
+ return await _resolve($, target);
970
968
  }
971
969
  catch (e) {
972
970
  if (e.code == 'ENOENT') {
package/dist/vfs/sync.js CHANGED
@@ -235,11 +235,12 @@ openSync;
235
235
  export function lopenSync(path, flag, mode) {
236
236
  return file2fd(_openSync.call(this, path, { flag, mode, preserveSymlinks: true }));
237
237
  }
238
- function _readFileSync(fname, flag, preserveSymlinks) {
238
+ function _readFileSync(path, flag, preserveSymlinks) {
239
239
  const env_2 = { stack: [], error: void 0, hasError: false };
240
240
  try {
241
+ path = normalizePath(path);
241
242
  // Get file.
242
- const file = __addDisposableResource(env_2, _openSync.call(this, fname, { flag, mode: 0o644, preserveSymlinks }), false);
243
+ const file = __addDisposableResource(env_2, _openSync.call(this, path, { flag, mode: 0o644, preserveSymlinks }), false);
243
244
  const stat = file.statSync();
244
245
  // Allocate buffer.
245
246
  const data = new Uint8Array(stat.size);
@@ -260,7 +261,7 @@ export function readFileSync(path, _options = {}) {
260
261
  if (!isReadable(flag)) {
261
262
  throw new ErrnoError(Errno.EINVAL, 'Flag passed to readFile must allow for reading.');
262
263
  }
263
- const data = Buffer.from(_readFileSync.call(this, typeof path == 'number' ? fd2file(path).path : path.toString(), options.flag, false));
264
+ const data = Buffer.from(_readFileSync.call(this, typeof path == 'number' ? fd2file(path).path : path, options.flag, false));
264
265
  return options.encoding ? data.toString(options.encoding) : data;
265
266
  }
266
267
  readFileSync;
@@ -564,13 +565,13 @@ export function symlinkSync(target, path, type = 'file') {
564
565
  if (existsSync.call(this, path)) {
565
566
  throw ErrnoError.With('EEXIST', path.toString(), 'symlink');
566
567
  }
567
- writeFileSync.call(this, path, target.toString());
568
+ writeFileSync.call(this, path, normalizePath(target, true));
568
569
  const file = _openSync.call(this, path, { flag: 'r+', mode: 0o644, preserveSymlinks: true });
569
570
  file.chmodSync(constants.S_IFLNK);
570
571
  }
571
572
  symlinkSync;
572
573
  export function readlinkSync(path, options) {
573
- const value = Buffer.from(_readFileSync.call(this, path.toString(), 'r', true));
574
+ const value = Buffer.from(_readFileSync.call(this, path, 'r', true));
574
575
  const encoding = typeof options == 'object' ? options === null || options === void 0 ? void 0 : options.encoding : options;
575
576
  if (encoding == 'buffer') {
576
577
  return value;
@@ -657,8 +658,7 @@ function _resolveSync($, path, preserveSymlinks) {
657
658
  return { ...resolved, fullPath: maybePath, stats };
658
659
  }
659
660
  const target = resolve(realDir, readlinkSync.call($, maybePath).toString());
660
- const real = realpathSync.call($, target);
661
- return { ...resolved, fullPath: real, stats };
661
+ return _resolveSync($, target);
662
662
  }
663
663
  catch (e) {
664
664
  if (e.code == 'ENOENT') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenfs/core",
3
- "version": "1.9.4",
3
+ "version": "1.9.5",
4
4
  "description": "A filesystem, anywhere",
5
5
  "funding": {
6
6
  "type": "individual",
package/scripts/test.js CHANGED
@@ -51,7 +51,7 @@ Output:
51
51
  -h, --help Outputs this help message
52
52
  -w, --verbose Output verbose messages
53
53
  -q, --quiet Don't output normal messages
54
- -l, --logs <level> Change the default log level for test output. Level can be a number or string
54
+ -l, --logs <level> Change the default log level for test output. Level can be a number or string
55
55
  -N, --file-names Use full file paths for tests from setup files instead of the base name
56
56
  -C, --ci Continuous integration (CI) mode. This interacts with the Github
57
57
  Checks API for better test status. Requires @octokit/action
@@ -166,9 +166,12 @@ if (options.common) {
166
166
  !options.quiet && console.log('Running common tests...');
167
167
  const { pass, fail } = await status('Common tests');
168
168
  try {
169
- execSync(`tsx ${options.inspect ? 'inspect' : ''} --test --experimental-test-coverage 'tests/*.test.ts' 'tests/**/!(fs)/*.test.ts'`, {
170
- stdio: ['ignore', options.verbose ? 'inherit' : 'ignore', 'inherit'],
171
- });
169
+ execSync(
170
+ `tsx ${options.inspect ? 'inspect' : ''} ${options.force ? '--test-force-exit' : ''} --test --experimental-test-coverage 'tests/*.test.ts' 'tests/**/!(fs)/*.test.ts'`,
171
+ {
172
+ stdio: ['ignore', options.verbose ? 'inherit' : 'ignore', 'inherit'],
173
+ }
174
+ );
172
175
  await pass();
173
176
  } catch {
174
177
  await fail();
@@ -2,6 +2,7 @@ import assert from 'node:assert/strict';
2
2
  import { suite, test } from 'node:test';
3
3
  import { MessageChannel, Worker } from 'node:worker_threads';
4
4
  import { Port, attachFS } from '../../dist/backends/port/fs.js';
5
+ import { waitOnline } from '../../dist/backends/port/rpc.js';
5
6
  import type { InMemoryStore, StoreFS } from '../../dist/index.js';
6
7
  import { ErrnoError, InMemory, configure, configureSingle, fs, resolveMountConfig } from '../../dist/index.js';
7
8
 
@@ -49,12 +50,13 @@ timeoutChannel.port1.unref();
49
50
  // Test configuration
50
51
 
51
52
  const configPort = new Worker(import.meta.dirname + '/config.worker.js');
53
+ await waitOnline(configPort);
52
54
 
53
55
  await suite('Remote FS with resolveRemoteMount', () => {
54
56
  const content = 'FS is in a port';
55
57
 
56
58
  test('Configuration', async () => {
57
- await configureSingle({ backend: Port, port: configPort, timeout: 500 });
59
+ await configureSingle({ backend: Port, port: configPort, timeout: 100 });
58
60
  });
59
61
 
60
62
  test('Write', async () => {
@@ -62,11 +64,11 @@ await suite('Remote FS with resolveRemoteMount', () => {
62
64
  });
63
65
 
64
66
  test('Read', async () => {
65
- assert((await fs.promises.readFile('/test', 'utf8')) === content);
67
+ assert.equal(await fs.promises.readFile('/test', 'utf8'), content);
66
68
  });
67
69
  });
68
70
 
69
- await configPort?.terminate();
71
+ await configPort.terminate();
70
72
  configPort.unref();
71
73
 
72
74
  // Test using a message channel
@@ -79,7 +81,7 @@ await suite('FS with MessageChannel', () => {
79
81
  test('configuration', async () => {
80
82
  tmpfs = await resolveMountConfig({ backend: InMemory, name: 'tmp' });
81
83
  attachFS(channel.port2, tmpfs);
82
- await configureSingle({ backend: Port, port: channel.port1, disableAsyncCache: true, timeout: 250 });
84
+ await configureSingle({ backend: Port, port: channel.port1, disableAsyncCache: true, timeout: 100 });
83
85
  });
84
86
 
85
87
  test('write', async () => {
@@ -88,12 +90,12 @@ await suite('FS with MessageChannel', () => {
88
90
 
89
91
  test('remote content', () => {
90
92
  fs.mount('/tmp', tmpfs);
91
- assert(fs.readFileSync('/tmp/test', 'utf8') == content);
93
+ assert.equal(fs.readFileSync('/tmp/test', 'utf8'), content);
92
94
  fs.umount('/tmp');
93
95
  });
94
96
 
95
97
  test('read', async () => {
96
- assert((await fs.promises.readFile('/test', 'utf8')) === content);
98
+ assert.equal(await fs.promises.readFile('/test', 'utf8'), content);
97
99
  });
98
100
 
99
101
  test('readFileSync should throw', () => {
@@ -101,6 +103,8 @@ await suite('FS with MessageChannel', () => {
101
103
  });
102
104
  });
103
105
 
106
+ channel.port1.close();
107
+ channel.port2.close();
104
108
  channel.port1.unref();
105
109
  channel.port2.unref();
106
110
 
@@ -112,7 +116,7 @@ await suite('Remote FS', () => {
112
116
  const content = 'FS is in a port';
113
117
 
114
118
  test('Configuration', async () => {
115
- await configureSingle({ backend: Port, port: remotePort, timeout: 500 });
119
+ await configureSingle({ backend: Port, port: remotePort, timeout: 100 });
116
120
  });
117
121
 
118
122
  test('Write', async () => {
@@ -122,8 +126,6 @@ await suite('Remote FS', () => {
122
126
  test('Read', async () => {
123
127
  assert.equal(await fs.promises.readFile('/test', 'utf8'), content);
124
128
  });
125
-
126
- test('Cleanup', async () => {});
127
129
  });
128
130
 
129
131
  await remotePort.terminate();
@@ -1,6 +1,7 @@
1
1
  import assert from 'node:assert/strict';
2
2
  import { suite, test } from 'node:test';
3
3
  import { basename, dirname, extname, join, normalize, resolve } from '../../dist/vfs/path.js';
4
+ import * as fs from '../../dist/vfs/index.js';
4
5
 
5
6
  suite('Path emulation', () => {
6
7
  test('resolve', () => {
@@ -31,4 +32,15 @@ suite('Path emulation', () => {
31
32
  assert.equal(extname('/path/to/file.txt'), '.txt');
32
33
  assert.equal(extname('/path/to/file'), '');
33
34
  });
35
+
36
+ test('file:// URL (string)', () => {
37
+ fs.writeFileSync('/example.txt', 'Yay');
38
+ assert.equal(fs.readFileSync('file:///example.txt', 'utf-8'), 'Yay');
39
+ });
40
+
41
+ test('file:// URL (URL)', () => {
42
+ fs.writeFileSync('/example.txt', 'Yay');
43
+ const url = new URL('file:///example.txt');
44
+ assert.equal(fs.readFileSync(url, 'utf-8'), 'Yay');
45
+ });
34
46
  });
@@ -27,6 +27,31 @@ suite('Links', () => {
27
27
  assert.equal(await fs.promises.readFile(target, 'utf-8'), await fs.promises.readFile(symlink, 'utf-8'));
28
28
  });
29
29
 
30
+ test('nested symlinks', async () => {
31
+ // Create the real directory structure
32
+ const realDir = '/real-dir';
33
+ const realFile = '/real-dir/realfile.txt';
34
+ const fileContent = 'hello world';
35
+ await fs.promises.mkdir(realDir);
36
+ await fs.promises.writeFile(realFile, fileContent);
37
+ // Create first symlink (symlink-dir -> real-dir)
38
+ const symlinkDir = '/symlink-dir';
39
+ await fs.promises.symlink(realDir, symlinkDir);
40
+ const symfile = 'symfile.txt';
41
+ const symlinkFile = join(realDir, symfile);
42
+ // Create second symlink (symlink-dir -> real-dir)
43
+ await fs.promises.symlink(realFile, symlinkFile);
44
+ // Now access file through nested symlinks
45
+ const nestedPath = join(symlinkDir, symfile);
46
+ // Verify realpath resolution
47
+ const resolvedPath = await fs.promises.realpath(nestedPath);
48
+ assert.equal(resolvedPath, realFile);
49
+ // Verify content can be read through nested symlinks
50
+ const content = await fs.promises.readFile(nestedPath, 'utf8');
51
+ assert.notEqual(content, '/real-dir/realfile.txt');
52
+ assert.equal(content, fileContent);
53
+ });
54
+
30
55
  test('unlink', async () => {
31
56
  await fs.promises.unlink(symlink);
32
57
  assert(!(await fs.promises.exists(symlink)));