@zenfs/core 1.3.3 → 1.3.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.
@@ -4,7 +4,7 @@ import { type MountConfiguration } from '../../config.js';
4
4
  import { File } from '../../file.js';
5
5
  import { FileSystem, type FileSystemMetadata } from '../../filesystem.js';
6
6
  import { Async } from '../../mixins/async.js';
7
- import { Stats, type FileType } from '../../stats.js';
7
+ import { Stats } from '../../stats.js';
8
8
  import type { Backend, FilesystemOf } from '../backend.js';
9
9
  import * as RPC from './rpc.js';
10
10
  type FileMethods = Omit<ExtractProperties<File, (...args: any[]) => Promise<any>>, typeof Symbol.asyncDispose>;
@@ -37,7 +37,6 @@ export declare class PortFile extends File {
37
37
  chmodSync(): void;
38
38
  utimes(atime: Date, mtime: Date): Promise<void>;
39
39
  utimesSync(): void;
40
- _setType(type: FileType): Promise<void>;
41
40
  _setTypeSync(): void;
42
41
  close(): Promise<void>;
43
42
  closeSync(): void;
@@ -67,9 +67,6 @@ export class PortFile extends File {
67
67
  utimesSync() {
68
68
  this._throwNoSync('utimes');
69
69
  }
70
- _setType(type) {
71
- return this.rpc('_setType', type);
72
- }
73
70
  _setTypeSync() {
74
71
  this._throwNoSync('_setType');
75
72
  }
package/dist/config.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { checkOptions, isBackend, isBackendConfig } from './backends/backend.js';
2
- import { credentials } from './credentials.js';
2
+ import { useCredentials } from './credentials.js';
3
3
  import { DeviceFS } from './devices.js';
4
4
  import * as cache from './emulation/cache.js';
5
5
  import { config } from './emulation/config.js';
@@ -87,7 +87,7 @@ async function mount(path, mount) {
87
87
  export async function configure(configuration) {
88
88
  const uid = 'uid' in configuration ? configuration.uid || 0 : 0;
89
89
  const gid = 'gid' in configuration ? configuration.gid || 0 : 0;
90
- Object.assign(credentials, { uid, gid, suid: uid, sgid: gid, euid: uid, egid: gid });
90
+ useCredentials({ uid, gid });
91
91
  cache.stats.isEnabled = configuration.cacheStats ?? false;
92
92
  cache.paths.isEnabled = configuration.cachePaths ?? false;
93
93
  config.checkAccess = !configuration.disableAccessChecks;
@@ -10,9 +10,21 @@ 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
  export declare const credentials: Credentials;
19
+ export interface CredentialInit {
20
+ uid: number;
21
+ gid: number;
22
+ suid?: number;
23
+ sgid?: number;
24
+ euid?: number;
25
+ egid?: number;
26
+ }
15
27
  /**
16
- * @deprecated
28
+ * Uses credentials from the provided uid and gid.
17
29
  */
18
- export declare const rootCredentials: Credentials;
30
+ export declare function useCredentials(source: CredentialInit): void;
@@ -5,15 +5,17 @@ export const credentials = {
5
5
  sgid: 0,
6
6
  euid: 0,
7
7
  egid: 0,
8
+ groups: [],
8
9
  };
9
10
  /**
10
- * @deprecated
11
+ * Uses credentials from the provided uid and gid.
11
12
  */
12
- export const rootCredentials = {
13
- uid: 0,
14
- gid: 0,
15
- suid: 0,
16
- sgid: 0,
17
- euid: 0,
18
- egid: 0,
19
- };
13
+ export function useCredentials(source) {
14
+ Object.assign(credentials, {
15
+ suid: source.uid,
16
+ sgid: source.gid,
17
+ euid: source.uid,
18
+ egid: source.gid,
19
+ ...source,
20
+ });
21
+ }
@@ -737,7 +737,7 @@ export async function symlink(target, path, type = 'file') {
737
737
  }
738
738
  const handle = __addDisposableResource(env_5, await _open(path, 'w+', 0o644, false), true);
739
739
  await handle.writeFile(target.toString());
740
- await handle.file._setType(constants.S_IFLNK);
740
+ await handle.file.chmod(constants.S_IFLNK);
741
741
  }
742
742
  catch (e_5) {
743
743
  env_5.error = e_5;
@@ -535,7 +535,7 @@ export function symlinkSync(target, path, type = 'file') {
535
535
  }
536
536
  writeFileSync(path, target.toString());
537
537
  const file = _openSync(path, 'r+', 0o644, false);
538
- file._setTypeSync(constants.S_IFLNK);
538
+ file.chmodSync(constants.S_IFLNK);
539
539
  }
540
540
  symlinkSync;
541
541
  export function readlinkSync(path, options) {
@@ -2,7 +2,7 @@ import { EventEmitter } from 'eventemitter3';
2
2
  import { ErrnoError } from '../error.js';
3
3
  import { isStatsEqual } from '../stats.js';
4
4
  import { normalizePath } from '../utils.js';
5
- import { dirname, basename } from './path.js';
5
+ import { basename, dirname } from './path.js';
6
6
  import { statSync } from './sync.js';
7
7
  /**
8
8
  * Base class for file system watchers.
@@ -130,22 +130,22 @@ export function removeWatcher(path, watcher) {
130
130
  }
131
131
  }
132
132
  export function emitChange(eventType, filename) {
133
- let normalizedFilename = normalizePath(filename);
133
+ filename = normalizePath(filename);
134
134
  // Notify watchers on the specific file
135
- if (watchers.has(normalizedFilename)) {
136
- for (const watcher of watchers.get(normalizedFilename)) {
135
+ if (watchers.has(filename)) {
136
+ for (const watcher of watchers.get(filename)) {
137
137
  watcher.emit('change', eventType, basename(filename));
138
138
  }
139
139
  }
140
140
  // Notify watchers on parent directories if they are watching recursively
141
- let parent = dirname(normalizedFilename);
142
- while (parent !== normalizedFilename && parent !== '/') {
141
+ let parent = filename, normalizedFilename;
142
+ while (parent !== normalizedFilename) {
143
+ normalizedFilename = parent;
144
+ parent = dirname(parent);
143
145
  if (watchers.has(parent)) {
144
146
  for (const watcher of watchers.get(parent)) {
145
- watcher.emit('change', eventType, basename(filename));
147
+ watcher.emit('change', eventType, filename.slice(parent.length + (parent == '/' ? 0 : 1)));
146
148
  }
147
149
  }
148
- normalizedFilename = parent;
149
- parent = dirname(parent);
150
150
  }
151
151
  }
package/dist/file.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { FileReadResult } from 'node:fs/promises';
2
2
  import type { FileSystem } from './filesystem.js';
3
3
  import './polyfills.js';
4
- import { Stats, type FileType } from './stats.js';
4
+ import { Stats } from './stats.js';
5
5
  /**
6
6
  Typescript does not include a type declaration for resizable array buffers.
7
7
  It has been standardized into ECMAScript though
@@ -123,16 +123,6 @@ export declare abstract class File<FS extends FileSystem = FileSystem> {
123
123
  * Change the file timestamps of the file.
124
124
  */
125
125
  abstract utimesSync(atime: Date, mtime: Date): void;
126
- /**
127
- * Set the file type
128
- * @internal
129
- */
130
- abstract _setType(type: FileType): Promise<void>;
131
- /**
132
- * Set the file type
133
- * @internal
134
- */
135
- abstract _setTypeSync(type: FileType): void;
136
126
  }
137
127
  /**
138
128
  * An implementation of `File` that operates completely in-memory.
@@ -243,8 +233,6 @@ export declare class PreloadFile<FS extends FileSystem> extends File<FS> {
243
233
  chownSync(uid: number, gid: number): void;
244
234
  utimes(atime: Date, mtime: Date): Promise<void>;
245
235
  utimesSync(atime: Date, mtime: Date): void;
246
- _setType(type: FileType): Promise<void>;
247
- _setTypeSync(type: FileType): void;
248
236
  }
249
237
  /**
250
238
  * For the file systems which do not sync to anything.
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.4",
4
4
  "description": "A filesystem, anywhere",
5
5
  "funding": {
6
6
  "type": "individual",
@@ -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,6 +1,6 @@
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';
@@ -188,7 +188,7 @@ export async function configure<T extends ConfigMounts>(configuration: Partial<C
188
188
  const uid = 'uid' in configuration ? configuration.uid || 0 : 0;
189
189
  const gid = 'gid' in configuration ? configuration.gid || 0 : 0;
190
190
 
191
- Object.assign(credentials, { uid, gid, suid: uid, sgid: gid, euid: uid, egid: gid });
191
+ useCredentials({ uid, gid });
192
192
 
193
193
  cache.stats.isEnabled = configuration.cacheStats ?? false;
194
194
  cache.paths.isEnabled = configuration.cachePaths ?? 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
+ }
@@ -822,7 +822,7 @@ export async function symlink(target: fs.PathLike, path: fs.PathLike, type: fs.s
822
822
 
823
823
  await using handle = await _open(path, 'w+', 0o644, false);
824
824
  await handle.writeFile(target.toString());
825
- await handle.file._setType(constants.S_IFLNK);
825
+ await handle.file.chmod(constants.S_IFLNK);
826
826
  }
827
827
  symlink satisfies typeof promises.symlink;
828
828
 
@@ -550,7 +550,7 @@ export function symlinkSync(target: fs.PathLike, path: fs.PathLike, type: fs.sym
550
550
 
551
551
  writeFileSync(path, target.toString());
552
552
  const file = _openSync(path, 'r+', 0o644, false);
553
- file._setTypeSync(constants.S_IFLNK);
553
+ file.chmodSync(constants.S_IFLNK);
554
554
  }
555
555
  symlinkSync satisfies typeof fs.symlinkSync;
556
556
 
@@ -4,7 +4,7 @@ import type * as fs from 'node:fs';
4
4
  import { ErrnoError } from '../error.js';
5
5
  import { isStatsEqual, type Stats } from '../stats.js';
6
6
  import { normalizePath } from '../utils.js';
7
- import { dirname, basename } from './path.js';
7
+ import { basename, dirname } from './path.js';
8
8
  import { statSync } from './sync.js';
9
9
 
10
10
  /**
@@ -171,23 +171,24 @@ export function removeWatcher(path: string, watcher: FSWatcher) {
171
171
  }
172
172
 
173
173
  export function emitChange(eventType: fs.WatchEventType, filename: string) {
174
- let normalizedFilename: string = normalizePath(filename);
174
+ filename = normalizePath(filename);
175
175
  // Notify watchers on the specific file
176
- if (watchers.has(normalizedFilename)) {
177
- for (const watcher of watchers.get(normalizedFilename)!) {
176
+ if (watchers.has(filename)) {
177
+ for (const watcher of watchers.get(filename)!) {
178
178
  watcher.emit('change', eventType, basename(filename));
179
179
  }
180
180
  }
181
181
 
182
182
  // Notify watchers on parent directories if they are watching recursively
183
- let parent = dirname(normalizedFilename);
184
- while (parent !== normalizedFilename && parent !== '/') {
183
+ let parent = filename,
184
+ normalizedFilename;
185
+ while (parent !== normalizedFilename) {
186
+ normalizedFilename = parent;
187
+ parent = dirname(parent);
185
188
  if (watchers.has(parent)) {
186
189
  for (const watcher of watchers.get(parent)!) {
187
- watcher.emit('change', eventType, basename(filename));
190
+ watcher.emit('change', eventType, filename.slice(parent.length + (parent == '/' ? 0 : 1)));
188
191
  }
189
192
  }
190
- normalizedFilename = parent;
191
- parent = dirname(parent);
192
193
  }
193
194
  }
package/src/file.ts CHANGED
@@ -4,7 +4,7 @@ import { O_APPEND, O_CREAT, O_EXCL, O_RDONLY, O_RDWR, O_SYNC, O_TRUNC, O_WRONLY,
4
4
  import { Errno, ErrnoError } from './error.js';
5
5
  import type { FileSystem } from './filesystem.js';
6
6
  import './polyfills.js';
7
- import { Stats, type FileType } from './stats.js';
7
+ import { _chown, Stats } from './stats.js';
8
8
 
9
9
  /**
10
10
  Typescript does not include a type declaration for resizable array buffers.
@@ -251,18 +251,6 @@ export abstract class File<FS extends FileSystem = FileSystem> {
251
251
  * Change the file timestamps of the file.
252
252
  */
253
253
  public abstract utimesSync(atime: Date, mtime: Date): void;
254
-
255
- /**
256
- * Set the file type
257
- * @internal
258
- */
259
- public abstract _setType(type: FileType): Promise<void>;
260
-
261
- /**
262
- * Set the file type
263
- * @internal
264
- */
265
- public abstract _setTypeSync(type: FileType): void;
266
254
  }
267
255
 
268
256
  /**
@@ -573,8 +561,8 @@ export class PreloadFile<FS extends FileSystem> extends File<FS> {
573
561
  throw ErrnoError.With('EBADF', this.path, 'File.chmod');
574
562
  }
575
563
  this.dirty = true;
576
- this.stats.chmod(mode);
577
- if (config.syncImmediately) await this.sync();
564
+ this.stats.mode = (this.stats.mode & (mode > S_IFMT ? ~S_IFMT : S_IFMT)) | mode;
565
+ if (config.syncImmediately || mode > S_IFMT) await this.sync();
578
566
  }
579
567
 
580
568
  public chmodSync(mode: number): void {
@@ -582,8 +570,8 @@ export class PreloadFile<FS extends FileSystem> extends File<FS> {
582
570
  throw ErrnoError.With('EBADF', this.path, 'File.chmod');
583
571
  }
584
572
  this.dirty = true;
585
- this.stats.chmod(mode);
586
- if (config.syncImmediately) this.syncSync();
573
+ this.stats.mode = (this.stats.mode & (mode > S_IFMT ? ~S_IFMT : S_IFMT)) | mode;
574
+ if (config.syncImmediately || mode > S_IFMT) this.syncSync();
587
575
  }
588
576
 
589
577
  public async chown(uid: number, gid: number): Promise<void> {
@@ -591,7 +579,7 @@ export class PreloadFile<FS extends FileSystem> extends File<FS> {
591
579
  throw ErrnoError.With('EBADF', this.path, 'File.chown');
592
580
  }
593
581
  this.dirty = true;
594
- this.stats.chown(uid, gid);
582
+ _chown(this.stats, uid, gid);
595
583
  if (config.syncImmediately) await this.sync();
596
584
  }
597
585
 
@@ -600,7 +588,7 @@ export class PreloadFile<FS extends FileSystem> extends File<FS> {
600
588
  throw ErrnoError.With('EBADF', this.path, 'File.chown');
601
589
  }
602
590
  this.dirty = true;
603
- this.stats.chown(uid, gid);
591
+ _chown(this.stats, uid, gid);
604
592
  if (config.syncImmediately) this.syncSync();
605
593
  }
606
594
 
@@ -623,24 +611,6 @@ export class PreloadFile<FS extends FileSystem> extends File<FS> {
623
611
  this.stats.mtime = mtime;
624
612
  if (config.syncImmediately) this.syncSync();
625
613
  }
626
-
627
- public async _setType(type: FileType): Promise<void> {
628
- if (this.closed) {
629
- throw ErrnoError.With('EBADF', this.path, 'File._setType');
630
- }
631
- this.dirty = true;
632
- this.stats.mode = (this.stats.mode & ~S_IFMT) | type;
633
- await this.sync();
634
- }
635
-
636
- public _setTypeSync(type: FileType): void {
637
- if (this.closed) {
638
- throw ErrnoError.With('EBADF', this.path, 'File._setType');
639
- }
640
- this.dirty = true;
641
- this.stats.mode = (this.stats.mode & ~S_IFMT) | type;
642
- this.syncSync();
643
- }
644
614
  }
645
615
 
646
616
  /**
package/src/stats.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type * as Node from 'node:fs';
2
- import { credentials, type Credentials } from './credentials.js';
2
+ import { credentials } from './credentials.js';
3
3
  import {
4
4
  R_OK,
5
5
  S_IFBLK,
@@ -67,6 +67,10 @@ export interface StatsLike<T extends number | bigint = number | bigint> {
67
67
  * Inode number
68
68
  */
69
69
  ino: T;
70
+ /**
71
+ * Number of hard links
72
+ */
73
+ nlink: T;
70
74
  }
71
75
 
72
76
  /**
@@ -258,7 +262,7 @@ export abstract class StatsCommon<T extends number | bigint> implements Node.Sta
258
262
  }
259
263
 
260
264
  // Group permissions
261
- if (credentials.gid === this.gid) {
265
+ if (credentials.gid === this.gid || credentials.groups.includes(Number(this.gid))) {
262
266
  if (this.mode & S_IRGRP) perm |= R_OK;
263
267
  if (this.mode & S_IWGRP) perm |= W_OK;
264
268
  if (this.mode & S_IXGRP) perm |= X_OK;
@@ -273,25 +277,11 @@ export abstract class StatsCommon<T extends number | bigint> implements Node.Sta
273
277
  return (perm & mode) === mode;
274
278
  }
275
279
 
276
- /**
277
- * Convert the current stats object into a credentials object
278
- * @internal
279
- */
280
- public cred(uid: number = Number(this.uid), gid: number = Number(this.gid)): Credentials {
281
- return {
282
- uid,
283
- gid,
284
- suid: Number(this.uid),
285
- sgid: Number(this.gid),
286
- euid: uid,
287
- egid: gid,
288
- };
289
- }
290
-
291
280
  /**
292
281
  * Change the mode of the file.
293
282
  * We use this helper function to prevent messing up the type of the file.
294
283
  * @internal
284
+ * @deprecated This will be removed in the next minor release since it is internal
295
285
  */
296
286
  public chmod(mode: number): void {
297
287
  this.mode = this._convert((this.mode & S_IFMT) | mode);
@@ -301,8 +291,9 @@ export abstract class StatsCommon<T extends number | bigint> implements Node.Sta
301
291
  * Change the owner user/group of the file.
302
292
  * This function makes sure it is a valid UID/GID (that is, a 32 unsigned int)
303
293
  * @internal
294
+ * @deprecated This will be removed in the next minor release since it is internal
304
295
  */
305
- public chown(uid: number | bigint, gid: number | bigint): void {
296
+ public chown(uid: number, gid: number): void {
306
297
  uid = Number(uid);
307
298
  gid = Number(gid);
308
299
  if (!isNaN(uid) && 0 <= uid && uid < 2 ** 32) {
@@ -330,6 +321,18 @@ export abstract class StatsCommon<T extends number | bigint> implements Node.Sta
330
321
  }
331
322
  }
332
323
 
324
+ /**
325
+ * @hidden @internal
326
+ */
327
+ export function _chown(stats: Partial<StatsLike<number>>, uid: number, gid: number) {
328
+ if (!isNaN(uid) && 0 <= uid && uid < 2 ** 32) {
329
+ stats.uid = uid;
330
+ }
331
+ if (!isNaN(gid) && 0 <= gid && gid < 2 ** 32) {
332
+ stats.gid = gid;
333
+ }
334
+ }
335
+
333
336
  /**
334
337
  * Implementation of Node's `Stats`.
335
338
  *
@@ -115,6 +115,27 @@ suite('Watch Features', () => {
115
115
  resolve();
116
116
  })();
117
117
 
118
+ await fs.promises.unlink(tempFile);
119
+ await promise;
120
+ });
121
+ test('fs.promises.watch should detect file creations recursively', async () => {
122
+ const rootDir = '/';
123
+ const subDir = `${testDir}sub-dir`;
124
+ const tempFile = `${subDir}/tempFile.txt`;
125
+ await fs.promises.mkdir(subDir);
126
+ const watcher = fs.promises.watch(rootDir);
127
+
128
+ await fs.promises.writeFile(tempFile, 'Temporary content');
129
+ const { promise, resolve } = Promise.withResolvers<void>();
130
+ (async () => {
131
+ for await (const event of watcher) {
132
+ assert.equal(event.eventType, 'rename');
133
+ assert.equal(event.filename, tempFile.substring(rootDir.length));
134
+ break;
135
+ }
136
+ resolve();
137
+ })();
138
+
118
139
  await fs.promises.unlink(tempFile);
119
140
  await promise;
120
141
  });