@zenfs/core 1.1.6 → 1.2.0

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.
Files changed (58) hide show
  1. package/dist/backends/file_index.js +0 -3
  2. package/dist/backends/overlay.js +0 -8
  3. package/dist/backends/store/fs.js +4 -17
  4. package/dist/config.d.ts +14 -0
  5. package/dist/config.js +4 -0
  6. package/dist/devices.js +0 -12
  7. package/dist/emulation/cache.d.ts +21 -0
  8. package/dist/emulation/cache.js +36 -0
  9. package/dist/emulation/promises.d.ts +9 -14
  10. package/dist/emulation/promises.js +71 -48
  11. package/dist/emulation/shared.d.ts +22 -0
  12. package/dist/emulation/shared.js +6 -0
  13. package/dist/emulation/sync.d.ts +11 -20
  14. package/dist/emulation/sync.js +44 -23
  15. package/package.json +4 -2
  16. package/scripts/test.js +14 -1
  17. package/src/backends/backend.ts +160 -0
  18. package/src/backends/fetch.ts +180 -0
  19. package/src/backends/file_index.ts +206 -0
  20. package/src/backends/memory.ts +50 -0
  21. package/src/backends/overlay.ts +560 -0
  22. package/src/backends/port/fs.ts +335 -0
  23. package/src/backends/port/readme.md +54 -0
  24. package/src/backends/port/rpc.ts +167 -0
  25. package/src/backends/readme.md +3 -0
  26. package/src/backends/store/fs.ts +700 -0
  27. package/src/backends/store/readme.md +9 -0
  28. package/src/backends/store/simple.ts +146 -0
  29. package/src/backends/store/store.ts +173 -0
  30. package/src/config.ts +173 -0
  31. package/src/credentials.ts +31 -0
  32. package/src/devices.ts +459 -0
  33. package/src/emulation/async.ts +834 -0
  34. package/src/emulation/cache.ts +44 -0
  35. package/src/emulation/constants.ts +182 -0
  36. package/src/emulation/dir.ts +138 -0
  37. package/src/emulation/index.ts +8 -0
  38. package/src/emulation/path.ts +440 -0
  39. package/src/emulation/promises.ts +1133 -0
  40. package/src/emulation/shared.ts +160 -0
  41. package/src/emulation/streams.ts +34 -0
  42. package/src/emulation/sync.ts +867 -0
  43. package/src/emulation/watchers.ts +193 -0
  44. package/src/error.ts +307 -0
  45. package/src/file.ts +661 -0
  46. package/src/filesystem.ts +174 -0
  47. package/src/index.ts +25 -0
  48. package/src/inode.ts +132 -0
  49. package/src/mixins/async.ts +208 -0
  50. package/src/mixins/index.ts +5 -0
  51. package/src/mixins/mutexed.ts +257 -0
  52. package/src/mixins/readonly.ts +96 -0
  53. package/src/mixins/shared.ts +25 -0
  54. package/src/mixins/sync.ts +58 -0
  55. package/src/polyfills.ts +21 -0
  56. package/src/stats.ts +363 -0
  57. package/src/utils.ts +288 -0
  58. package/tests/fs/readdir.test.ts +3 -3
package/src/stats.ts ADDED
@@ -0,0 +1,363 @@
1
+ import type * as Node from 'node:fs';
2
+ import { credentials, type Credentials } from './credentials.js';
3
+ import { S_IFBLK, S_IFCHR, S_IFDIR, S_IFIFO, S_IFLNK, S_IFMT, S_IFREG, S_IFSOCK, S_IRWXG, S_IRWXO, S_IRWXU, size_max } from './emulation/constants.js';
4
+
5
+ /**
6
+ * Indicates the type of a file. Applied to 'mode'.
7
+ */
8
+ export type FileType = typeof S_IFREG | typeof S_IFDIR | typeof S_IFLNK;
9
+
10
+ export interface StatsLike<T extends number | bigint = number | bigint> {
11
+ /**
12
+ * Size of the item in bytes.
13
+ * For directories/symlinks, this is normally the size of the struct that represents the item.
14
+ */
15
+ size: T;
16
+ /**
17
+ * Unix-style file mode (e.g. 0o644) that includes the item type
18
+ */
19
+ mode: T;
20
+ /**
21
+ * Time of last access, since epoch
22
+ */
23
+ atimeMs: T;
24
+ /**
25
+ * Time of last modification, since epoch
26
+ */
27
+ mtimeMs: T;
28
+ /**
29
+ * Time of last time file status was changed, since epoch
30
+ */
31
+ ctimeMs: T;
32
+ /**
33
+ * Time of file creation, since epoch
34
+ */
35
+ birthtimeMs: T;
36
+ /**
37
+ * The id of the user that owns the file
38
+ */
39
+ uid: T;
40
+ /**
41
+ * The id of the group that owns the file
42
+ */
43
+ gid: T;
44
+ /**
45
+ * Inode number
46
+ */
47
+ ino: T;
48
+ }
49
+
50
+ /**
51
+ * Provides information about a particular entry in the file system.
52
+ * Common code used by both Stats and BigIntStats.
53
+ */
54
+ export abstract class StatsCommon<T extends number | bigint> implements Node.StatsBase<T>, StatsLike {
55
+ protected abstract _isBigint: T extends bigint ? true : false;
56
+
57
+ protected _convert(arg: number | bigint | string | boolean): T {
58
+ return (this._isBigint ? BigInt(arg) : Number(arg)) as T;
59
+ }
60
+
61
+ public get blocks(): T {
62
+ return this._convert(Math.ceil(Number(this.size) / 512));
63
+ }
64
+
65
+ /**
66
+ * Unix-style file mode (e.g. 0o644) that includes the type of the item.
67
+ * Type of the item can be FILE, DIRECTORY, SYMLINK, or SOCKET
68
+ */
69
+ public mode: T;
70
+
71
+ /**
72
+ * ID of device containing file
73
+ */
74
+ public dev: T = this._convert(0);
75
+
76
+ /**
77
+ * Inode number
78
+ */
79
+ public ino: T = this._convert(0);
80
+
81
+ /**
82
+ * Device ID (if special file)
83
+ */
84
+ public rdev: T = this._convert(0);
85
+
86
+ /**
87
+ * Number of hard links
88
+ */
89
+ public nlink: T = this._convert(1);
90
+
91
+ /**
92
+ * Block size for file system I/O
93
+ */
94
+ public blksize: T = this._convert(4096);
95
+
96
+ /**
97
+ * User ID of owner
98
+ */
99
+ public uid: T = this._convert(0);
100
+
101
+ /**
102
+ * Group ID of owner
103
+ */
104
+ public gid: T = this._convert(0);
105
+
106
+ /**
107
+ * Some file systems stash data on stats objects.
108
+ */
109
+ public fileData?: Uint8Array;
110
+
111
+ /**
112
+ * Time of last access, since epoch
113
+ */
114
+ public atimeMs: T;
115
+
116
+ public get atime(): Date {
117
+ return new Date(Number(this.atimeMs));
118
+ }
119
+
120
+ public set atime(value: Date) {
121
+ this.atimeMs = this._convert(value.getTime());
122
+ }
123
+
124
+ /**
125
+ * Time of last modification, since epoch
126
+ */
127
+ public mtimeMs: T;
128
+
129
+ public get mtime(): Date {
130
+ return new Date(Number(this.mtimeMs));
131
+ }
132
+
133
+ public set mtime(value: Date) {
134
+ this.mtimeMs = this._convert(value.getTime());
135
+ }
136
+
137
+ /**
138
+ * Time of last time file status was changed, since epoch
139
+ */
140
+ public ctimeMs: T;
141
+
142
+ public get ctime(): Date {
143
+ return new Date(Number(this.ctimeMs));
144
+ }
145
+
146
+ public set ctime(value: Date) {
147
+ this.ctimeMs = this._convert(value.getTime());
148
+ }
149
+
150
+ /**
151
+ * Time of file creation, since epoch
152
+ */
153
+ public birthtimeMs: T;
154
+
155
+ public get birthtime(): Date {
156
+ return new Date(Number(this.birthtimeMs));
157
+ }
158
+
159
+ public set birthtime(value: Date) {
160
+ this.birthtimeMs = this._convert(value.getTime());
161
+ }
162
+
163
+ /**
164
+ * Size of the item in bytes.
165
+ * For directories/symlinks, this is normally the size of the struct that represents the item.
166
+ */
167
+ public size: T;
168
+
169
+ /**
170
+ * Creates a new stats instance from a stats-like object. Can be used to copy stats (note)
171
+ */
172
+ public constructor({ atimeMs, mtimeMs, ctimeMs, birthtimeMs, uid, gid, size, mode, ino }: Partial<StatsLike> = {}) {
173
+ const now = Date.now();
174
+ this.atimeMs = this._convert(atimeMs ?? now);
175
+ this.mtimeMs = this._convert(mtimeMs ?? now);
176
+ this.ctimeMs = this._convert(ctimeMs ?? now);
177
+ this.birthtimeMs = this._convert(birthtimeMs ?? now);
178
+ this.uid = this._convert(uid ?? 0);
179
+ this.gid = this._convert(gid ?? 0);
180
+ this.size = this._convert(size ?? 0);
181
+ this.ino = this._convert(ino ?? 0);
182
+ this.mode = this._convert(mode ?? 0o644 & S_IFREG);
183
+
184
+ if ((this.mode & S_IFMT) == 0) {
185
+ this.mode = (this.mode | this._convert(S_IFREG)) as T;
186
+ }
187
+ }
188
+
189
+ public isFile(): boolean {
190
+ return (this.mode & S_IFMT) === S_IFREG;
191
+ }
192
+
193
+ public isDirectory(): boolean {
194
+ return (this.mode & S_IFMT) === S_IFDIR;
195
+ }
196
+
197
+ public isSymbolicLink(): boolean {
198
+ return (this.mode & S_IFMT) === S_IFLNK;
199
+ }
200
+
201
+ public isSocket(): boolean {
202
+ return (this.mode & S_IFMT) === S_IFSOCK;
203
+ }
204
+
205
+ public isBlockDevice(): boolean {
206
+ return (this.mode & S_IFMT) === S_IFBLK;
207
+ }
208
+
209
+ public isCharacterDevice(): boolean {
210
+ return (this.mode & S_IFMT) === S_IFCHR;
211
+ }
212
+
213
+ public isFIFO(): boolean {
214
+ return (this.mode & S_IFMT) === S_IFIFO;
215
+ }
216
+
217
+ /**
218
+ * Checks if a given user/group has access to this item
219
+ * @param mode The requested access, combination of W_OK, R_OK, and X_OK
220
+ * @returns True if the request has access, false if the request does not
221
+ * @internal
222
+ */
223
+ public hasAccess(mode: number): boolean {
224
+ if (credentials.euid === 0 || credentials.egid === 0) {
225
+ //Running as root
226
+ return true;
227
+ }
228
+
229
+ // Mask for
230
+ const adjusted = (credentials.uid == this.uid ? S_IRWXU : 0) | (credentials.gid == this.gid ? S_IRWXG : 0) | S_IRWXO;
231
+ return (mode & this.mode & adjusted) == mode;
232
+ }
233
+
234
+ /**
235
+ * Convert the current stats object into a credentials object
236
+ * @internal
237
+ */
238
+ public cred(uid: number = Number(this.uid), gid: number = Number(this.gid)): Credentials {
239
+ return {
240
+ uid,
241
+ gid,
242
+ suid: Number(this.uid),
243
+ sgid: Number(this.gid),
244
+ euid: uid,
245
+ egid: gid,
246
+ };
247
+ }
248
+
249
+ /**
250
+ * Change the mode of the file.
251
+ * We use this helper function to prevent messing up the type of the file.
252
+ * @internal
253
+ */
254
+ public chmod(mode: number): void {
255
+ this.mode = this._convert((this.mode & S_IFMT) | mode);
256
+ }
257
+
258
+ /**
259
+ * Change the owner user/group of the file.
260
+ * This function makes sure it is a valid UID/GID (that is, a 32 unsigned int)
261
+ * @internal
262
+ */
263
+ public chown(uid: number | bigint, gid: number | bigint): void {
264
+ uid = Number(uid);
265
+ gid = Number(gid);
266
+ if (!isNaN(uid) && 0 <= uid && uid < 2 ** 32) {
267
+ this.uid = this._convert(uid);
268
+ }
269
+ if (!isNaN(gid) && 0 <= gid && gid < 2 ** 32) {
270
+ this.gid = this._convert(gid);
271
+ }
272
+ }
273
+
274
+ public get atimeNs(): bigint {
275
+ return BigInt(this.atimeMs) * 1000n;
276
+ }
277
+
278
+ public get mtimeNs(): bigint {
279
+ return BigInt(this.mtimeMs) * 1000n;
280
+ }
281
+
282
+ public get ctimeNs(): bigint {
283
+ return BigInt(this.ctimeMs) * 1000n;
284
+ }
285
+
286
+ public get birthtimeNs(): bigint {
287
+ return BigInt(this.birthtimeMs) * 1000n;
288
+ }
289
+ }
290
+
291
+ /**
292
+ * Implementation of Node's `Stats`.
293
+ *
294
+ * Attribute descriptions are from `man 2 stat'
295
+ * @see http://nodejs.org/api/fs.html#fs_class_fs_stats
296
+ * @see http://man7.org/linux/man-pages/man2/stat.2.html
297
+ */
298
+ export class Stats extends StatsCommon<number> implements Node.Stats, StatsLike {
299
+ protected _isBigint = false as const;
300
+ }
301
+ Stats satisfies typeof Node.Stats;
302
+
303
+ /**
304
+ * Stats with bigint
305
+ */
306
+ export class BigIntStats extends StatsCommon<bigint> implements Node.BigIntStats, StatsLike {
307
+ protected _isBigint = true as const;
308
+ }
309
+
310
+ /**
311
+ * Determines if the file stats have changed by comparing relevant properties.
312
+ *
313
+ * @param left The previous stats.
314
+ * @param right The current stats.
315
+ * @returns `true` if stats have changed; otherwise, `false`.
316
+ * @internal
317
+ */
318
+ export function isStatsEqual<T extends number | bigint>(left: StatsCommon<T>, right: StatsCommon<T>): boolean {
319
+ return left.size == right.size && +left.atime == +right.atime && +left.mtime == +right.mtime && +left.ctime == +right.ctime && left.mode == right.mode;
320
+ }
321
+
322
+ /** @internal */
323
+ export const ZenFsType = 0x7a656e6673; // 'z' 'e' 'n' 'f' 's'
324
+
325
+ /**
326
+ * @hidden
327
+ */
328
+ export class StatsFs implements Node.StatsFsBase<number> {
329
+ /** Type of file system. */
330
+ public type: number = 0x7a656e6673;
331
+ /** Optimal transfer block size. */
332
+ public bsize: number = 4096;
333
+ /** Total data blocks in file system. */
334
+ public blocks: number = 0;
335
+ /** Free blocks in file system. */
336
+ public bfree: number = 0;
337
+ /** Available blocks for unprivileged users */
338
+ public bavail: number = 0;
339
+ /** Total file nodes in file system. */
340
+ public files: number = size_max;
341
+ /** Free file nodes in file system. */
342
+ public ffree: number = size_max;
343
+ }
344
+
345
+ /**
346
+ * @hidden
347
+ */
348
+ export class BigIntStatsFs implements Node.StatsFsBase<bigint> {
349
+ /** Type of file system. */
350
+ public type: bigint = 0x7a656e6673n;
351
+ /** Optimal transfer block size. */
352
+ public bsize: bigint = 4096n;
353
+ /** Total data blocks in file system. */
354
+ public blocks: bigint = 0n;
355
+ /** Free blocks in file system. */
356
+ public bfree: bigint = 0n;
357
+ /** Available blocks for unprivileged users */
358
+ public bavail: bigint = 0n;
359
+ /** Total file nodes in file system. */
360
+ public files: bigint = BigInt(size_max);
361
+ /** Free file nodes in file system. */
362
+ public ffree: bigint = BigInt(size_max);
363
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,288 @@
1
+ /* eslint-disable @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-return */
2
+ import type * as fs from 'node:fs';
3
+ import type { ClassLike, OptionalTuple } from 'utilium';
4
+ import { dirname, resolve, type AbsolutePath } from './emulation/path.js';
5
+ import { Errno, ErrnoError } from './error.js';
6
+ import type { FileSystem } from './filesystem.js';
7
+
8
+ declare global {
9
+ function atob(data: string): string;
10
+ function btoa(data: string): string;
11
+ }
12
+
13
+ /**
14
+ * Synchronous recursive makedir.
15
+ * @hidden
16
+ */
17
+ export function mkdirpSync(path: string, mode: number, fs: FileSystem): void {
18
+ if (!fs.existsSync(path)) {
19
+ mkdirpSync(dirname(path), mode, fs);
20
+ fs.mkdirSync(path, mode);
21
+ }
22
+ }
23
+
24
+ function _min(d0: number, d1: number, d2: number, bx: number, ay: number): number {
25
+ return Math.min(d0 + 1, d1 + 1, d2 + 1, bx === ay ? d1 : d1 + 1);
26
+ }
27
+
28
+ /**
29
+ * Calculates levenshtein distance.
30
+ * @hidden
31
+ */
32
+ export function levenshtein(a: string, b: string): number {
33
+ if (a === b) {
34
+ return 0;
35
+ }
36
+
37
+ if (a.length > b.length) {
38
+ [a, b] = [b, a]; // Swap a and b
39
+ }
40
+
41
+ let la = a.length;
42
+ let lb = b.length;
43
+
44
+ // Trim common suffix
45
+ while (la > 0 && a.charCodeAt(la - 1) === b.charCodeAt(lb - 1)) {
46
+ la--;
47
+ lb--;
48
+ }
49
+
50
+ let offset = 0;
51
+
52
+ // Trim common prefix
53
+ while (offset < la && a.charCodeAt(offset) === b.charCodeAt(offset)) {
54
+ offset++;
55
+ }
56
+
57
+ la -= offset;
58
+ lb -= offset;
59
+
60
+ if (la === 0 || lb === 1) {
61
+ return lb;
62
+ }
63
+
64
+ const vector = new Array<number>(la << 1);
65
+
66
+ for (let y = 0; y < la; ) {
67
+ vector[la + y] = a.charCodeAt(offset + y);
68
+ vector[y] = ++y;
69
+ }
70
+
71
+ let x: number;
72
+ let d0: number;
73
+ let d1: number;
74
+ let d2: number;
75
+ let d3: number;
76
+ for (x = 0; x + 3 < lb; ) {
77
+ const bx0 = b.charCodeAt(offset + (d0 = x));
78
+ const bx1 = b.charCodeAt(offset + (d1 = x + 1));
79
+ const bx2 = b.charCodeAt(offset + (d2 = x + 2));
80
+ const bx3 = b.charCodeAt(offset + (d3 = x + 3));
81
+ let dd = (x += 4);
82
+ for (let y = 0; y < la; ) {
83
+ const ay = vector[la + y];
84
+ const dy = vector[y];
85
+ d0 = _min(dy, d0, d1, bx0, ay);
86
+ d1 = _min(d0, d1, d2, bx1, ay);
87
+ d2 = _min(d1, d2, d3, bx2, ay);
88
+ dd = _min(d2, d3, dd, bx3, ay);
89
+ vector[y++] = dd;
90
+ d3 = d2;
91
+ d2 = d1;
92
+ d1 = d0;
93
+ d0 = dy;
94
+ }
95
+ }
96
+
97
+ let dd: number = 0;
98
+ for (; x < lb; ) {
99
+ const bx0 = b.charCodeAt(offset + (d0 = x));
100
+ dd = ++x;
101
+ for (let y = 0; y < la; y++) {
102
+ const dy = vector[y];
103
+ vector[y] = dd = dy < d0 || dd < d0 ? (dy > dd ? dd + 1 : dy + 1) : bx0 === vector[la + y] ? d0 : d0 + 1;
104
+ d0 = dy;
105
+ }
106
+ }
107
+
108
+ return dd;
109
+ }
110
+
111
+ /**
112
+ * Encodes a string into a buffer
113
+ * @internal
114
+ */
115
+ export function encodeRaw(input: string): Uint8Array {
116
+ if (typeof input != 'string') {
117
+ throw new ErrnoError(Errno.EINVAL, 'Can not encode a non-string');
118
+ }
119
+ return new Uint8Array(Array.from(input).map(char => char.charCodeAt(0)));
120
+ }
121
+
122
+ /**
123
+ * Decodes a string from a buffer
124
+ * @internal
125
+ */
126
+ export function decodeRaw(input?: Uint8Array): string {
127
+ if (!(input instanceof Uint8Array)) {
128
+ throw new ErrnoError(Errno.EINVAL, 'Can not decode a non-Uint8Array');
129
+ }
130
+
131
+ return Array.from(input)
132
+ .map(char => String.fromCharCode(char))
133
+ .join('');
134
+ }
135
+
136
+ const encoder = new TextEncoder();
137
+
138
+ /**
139
+ * Encodes a string into a buffer
140
+ * @internal
141
+ */
142
+ export function encodeUTF8(input: string): Uint8Array {
143
+ if (typeof input != 'string') {
144
+ throw new ErrnoError(Errno.EINVAL, 'Can not encode a non-string');
145
+ }
146
+ return encoder.encode(input);
147
+ }
148
+
149
+ export { /** @deprecated @hidden */ encodeUTF8 as encode };
150
+
151
+ const decoder = new TextDecoder();
152
+
153
+ /**
154
+ * Decodes a string from a buffer
155
+ * @internal
156
+ */
157
+ export function decodeUTF8(input?: Uint8Array): string {
158
+ if (!(input instanceof Uint8Array)) {
159
+ throw new ErrnoError(Errno.EINVAL, 'Can not decode a non-Uint8Array');
160
+ }
161
+
162
+ return decoder.decode(input);
163
+ }
164
+
165
+ export { /** @deprecated @hidden */ decodeUTF8 as decode };
166
+
167
+ /**
168
+ * Decodes a directory listing
169
+ * @hidden
170
+ */
171
+ export function decodeDirListing(data: Uint8Array): Record<string, bigint> {
172
+ return JSON.parse(decodeUTF8(data), (k, v) => (k == '' ? v : BigInt(v as string)));
173
+ }
174
+
175
+ /**
176
+ * Encodes a directory listing
177
+ * @hidden
178
+ */
179
+ export function encodeDirListing(data: Record<string, bigint>): Uint8Array {
180
+ return encodeUTF8(JSON.stringify(data, (k, v) => (k == '' ? v : v.toString())));
181
+ }
182
+
183
+ export type Callback<Args extends unknown[] = []> = (e?: ErrnoError, ...args: OptionalTuple<Args>) => unknown;
184
+
185
+ /**
186
+ * converts Date or number to a integer UNIX timestamp
187
+ * Grabbed from NodeJS sources (lib/fs.js)
188
+ *
189
+ * @internal
190
+ */
191
+ export function _toUnixTimestamp(time: Date | number): number {
192
+ if (typeof time === 'number') {
193
+ return Math.floor(time);
194
+ }
195
+ if (time instanceof Date) {
196
+ return Math.floor(time.getTime() / 1000);
197
+ }
198
+ throw new Error('Cannot parse time');
199
+ }
200
+
201
+ /**
202
+ * Normalizes a mode
203
+ * @internal
204
+ */
205
+ export function normalizeMode(mode: unknown, def?: number): number {
206
+ if (typeof mode == 'number') {
207
+ return mode;
208
+ }
209
+
210
+ if (typeof mode == 'string') {
211
+ const parsed = parseInt(mode, 8);
212
+ if (!isNaN(parsed)) {
213
+ return parsed;
214
+ }
215
+ }
216
+
217
+ if (typeof def == 'number') {
218
+ return def;
219
+ }
220
+
221
+ throw new ErrnoError(Errno.EINVAL, 'Invalid mode: ' + mode?.toString());
222
+ }
223
+
224
+ /**
225
+ * Normalizes a time
226
+ * @internal
227
+ */
228
+ export function normalizeTime(time: string | number | Date): Date {
229
+ if (time instanceof Date) {
230
+ return time;
231
+ }
232
+
233
+ if (typeof time == 'number') {
234
+ return new Date(time * 1000);
235
+ }
236
+
237
+ if (typeof time == 'string') {
238
+ return new Date(time);
239
+ }
240
+
241
+ throw new ErrnoError(Errno.EINVAL, 'Invalid time.');
242
+ }
243
+
244
+ /**
245
+ * Normalizes a path
246
+ * @internal
247
+ */
248
+ export function normalizePath(p: fs.PathLike): AbsolutePath {
249
+ p = p.toString();
250
+ if (p.includes('\x00')) {
251
+ throw new ErrnoError(Errno.EINVAL, 'Path can not contain null character');
252
+ }
253
+ if (p.length == 0) {
254
+ throw new ErrnoError(Errno.EINVAL, 'Path can not be empty');
255
+ }
256
+ return resolve(p.replaceAll(/[/\\]+/g, '/'));
257
+ }
258
+
259
+ /**
260
+ * Normalizes options
261
+ * @param options options to normalize
262
+ * @param encoding default encoding
263
+ * @param flag default flag
264
+ * @param mode default mode
265
+ * @internal
266
+ */
267
+ export function normalizeOptions(
268
+ options: fs.WriteFileOptions | (fs.EncodingOption & { flag?: fs.OpenMode }) | undefined,
269
+ encoding: BufferEncoding | null = 'utf8',
270
+ flag: string,
271
+ mode: number = 0
272
+ ): { encoding?: BufferEncoding | null; flag: string; mode: number } {
273
+ if (typeof options != 'object' || options === null) {
274
+ return {
275
+ encoding: typeof options == 'string' ? options : encoding ?? null,
276
+ flag,
277
+ mode,
278
+ };
279
+ }
280
+
281
+ return {
282
+ encoding: typeof options?.encoding == 'string' ? options.encoding : encoding ?? null,
283
+ flag: typeof options?.flag == 'string' ? options.flag : flag,
284
+ mode: normalizeMode('mode' in options ? options?.mode : null, mode),
285
+ };
286
+ }
287
+
288
+ export type Concrete<T extends ClassLike> = Pick<T, keyof T> & (new (...args: any[]) => InstanceType<T>);
@@ -67,9 +67,9 @@ suite('readdir and readdirSync', () => {
67
67
 
68
68
  test('readdir returns Dirent recursively', async () => {
69
69
  const entries = await fs.promises.readdir(testDir, { recursive: true, withFileTypes: true });
70
- assert.equal(entries[0].path, 'file1.txt');
71
- assert.equal(entries[4].path, 'subdir1/file4.txt');
72
- assert.equal(entries[entries.length - 1].path, 'subdir2/file5.txt');
70
+ assert(entries.find(entry => entry.path === 'file1.txt'));
71
+ assert(entries.find(entry => entry.path === 'subdir1/file4.txt'));
72
+ assert(entries.find(entry => entry.path === 'subdir2/file5.txt'));
73
73
  });
74
74
 
75
75
  // New test for readdirSync with recursive: true