@zenfs/core 1.2.9 → 1.3.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.
- package/dist/backends/fetch.js +1 -1
- package/dist/backends/memory.d.ts +1 -2
- package/dist/backends/overlay.js +2 -3
- package/dist/backends/port/fs.d.ts +2 -15
- package/dist/backends/store/fs.d.ts +17 -28
- package/dist/backends/store/fs.js +169 -191
- package/dist/backends/store/simple.d.ts +20 -21
- package/dist/backends/store/simple.js +24 -24
- package/dist/backends/store/store.d.ts +22 -23
- package/dist/backends/store/store.js +6 -6
- package/dist/config.d.ts +8 -0
- package/dist/config.js +2 -1
- package/dist/devices.d.ts +2 -3
- package/dist/emulation/cache.d.ts +40 -31
- package/dist/emulation/cache.js +62 -53
- package/dist/emulation/index.d.ts +1 -1
- package/dist/emulation/index.js +1 -1
- package/dist/emulation/promises.js +18 -17
- package/dist/emulation/shared.d.ts +6 -0
- package/dist/emulation/shared.js +13 -1
- package/dist/emulation/sync.d.ts +1 -1
- package/dist/emulation/sync.js +16 -15
- package/dist/file.d.ts +3 -15
- package/dist/file.js +7 -19
- package/dist/inode.d.ts +4 -13
- package/dist/inode.js +22 -29
- package/dist/mixins/async.d.ts +15 -10
- package/dist/mixins/async.js +3 -1
- package/dist/stats.js +30 -5
- package/dist/utils.d.ts +5 -7
- package/dist/utils.js +11 -20
- package/package.json +1 -1
- package/src/backends/fetch.ts +1 -1
- package/src/backends/memory.ts +1 -2
- package/src/backends/overlay.ts +2 -2
- package/src/backends/store/fs.ts +187 -220
- package/src/backends/store/simple.ts +36 -37
- package/src/backends/store/store.ts +25 -26
- package/src/config.ts +11 -1
- package/src/devices.ts +2 -3
- package/src/emulation/cache.ts +68 -60
- package/src/emulation/index.ts +1 -1
- package/src/emulation/promises.ts +20 -19
- package/src/emulation/shared.ts +13 -1
- package/src/emulation/sync.ts +16 -15
- package/src/file.ts +9 -21
- package/src/inode.ts +10 -31
- package/src/mixins/async.ts +27 -24
- package/src/stats.ts +47 -5
- package/src/utils.ts +11 -23
- package/tests/fs/dir.test.ts +21 -31
- package/tests/fs/directory.test.ts +6 -4
- package/tests/fs/links.test.ts +9 -2
- package/tests/fs/permissions.test.ts +2 -2
- package/tests/fs/stat.test.ts +42 -0
- package/tests/fs/times.test.ts +28 -28
- package/tests/setup/cow+fetch.ts +4 -2
package/src/emulation/sync.ts
CHANGED
|
@@ -106,7 +106,7 @@ export function unlinkSync(path: fs.PathLike): void {
|
|
|
106
106
|
path = normalizePath(path);
|
|
107
107
|
const { fs, path: resolved } = resolveMount(path);
|
|
108
108
|
try {
|
|
109
|
-
if (config.checkAccess && !(cache.
|
|
109
|
+
if (config.checkAccess && !(cache.stats.getSync(path) || fs.statSync(resolved)).hasAccess(constants.W_OK)) {
|
|
110
110
|
throw ErrnoError.With('EACCES', resolved, 'unlink');
|
|
111
111
|
}
|
|
112
112
|
fs.unlinkSync(resolved);
|
|
@@ -243,10 +243,10 @@ writeFileSync satisfies typeof fs.writeFileSync;
|
|
|
243
243
|
* Asynchronously append data to a file, creating the file if it not yet exists.
|
|
244
244
|
* @option encoding Defaults to `'utf8'`.
|
|
245
245
|
* @option mode Defaults to `0644`.
|
|
246
|
-
* @option flag Defaults to `'a'`.
|
|
246
|
+
* @option flag Defaults to `'a+'`.
|
|
247
247
|
*/
|
|
248
248
|
export function appendFileSync(filename: fs.PathOrFileDescriptor, data: FileContents, _options: fs.WriteFileOptions = {}): void {
|
|
249
|
-
const options = normalizeOptions(_options, 'utf8', 'a', 0o644);
|
|
249
|
+
const options = normalizeOptions(_options, 'utf8', 'a+', 0o644);
|
|
250
250
|
const flag = parseFlag(options.flag);
|
|
251
251
|
if (!isAppendable(flag)) {
|
|
252
252
|
throw new ErrnoError(Errno.EINVAL, 'Flag passed to appendFile must allow for appending.');
|
|
@@ -386,7 +386,7 @@ export function rmdirSync(path: fs.PathLike): void {
|
|
|
386
386
|
path = normalizePath(path);
|
|
387
387
|
const { fs, path: resolved } = resolveMount(realpathSync(path));
|
|
388
388
|
try {
|
|
389
|
-
const stats = cache.
|
|
389
|
+
const stats = cache.stats.getSync(path) || fs.statSync(resolved);
|
|
390
390
|
if (!stats.isDirectory()) {
|
|
391
391
|
throw ErrnoError.With('ENOTDIR', resolved, 'rmdir');
|
|
392
392
|
}
|
|
@@ -459,8 +459,8 @@ export function readdirSync(
|
|
|
459
459
|
const { fs, path: resolved } = resolveMount(realpathSync(path));
|
|
460
460
|
let entries: string[];
|
|
461
461
|
try {
|
|
462
|
-
const stats = cache.
|
|
463
|
-
cache.
|
|
462
|
+
const stats = cache.stats.getSync(path) || fs.statSync(resolved);
|
|
463
|
+
cache.stats.setSync(path, stats);
|
|
464
464
|
if (config.checkAccess && !stats.hasAccess(constants.R_OK)) {
|
|
465
465
|
throw ErrnoError.With('EACCES', resolved, 'readdir');
|
|
466
466
|
}
|
|
@@ -475,8 +475,8 @@ export function readdirSync(
|
|
|
475
475
|
// Iterate over entries and handle recursive case if needed
|
|
476
476
|
const values: (string | Dirent | Buffer)[] = [];
|
|
477
477
|
for (const entry of entries) {
|
|
478
|
-
const entryStat = cache.
|
|
479
|
-
cache.
|
|
478
|
+
const entryStat = cache.stats.getSync(join(path, entry)) || fs.statSync(join(resolved, entry));
|
|
479
|
+
cache.stats.setSync(join(path, entry), entryStat);
|
|
480
480
|
|
|
481
481
|
if (options?.withFileTypes) {
|
|
482
482
|
values.push(new Dirent(entry, entryStat));
|
|
@@ -500,7 +500,7 @@ export function readdirSync(
|
|
|
500
500
|
}
|
|
501
501
|
|
|
502
502
|
if (!options?._isIndirect) {
|
|
503
|
-
cache.
|
|
503
|
+
cache.stats.clearSync();
|
|
504
504
|
}
|
|
505
505
|
return values as string[] | Dirent[] | Buffer[];
|
|
506
506
|
}
|
|
@@ -622,7 +622,7 @@ export function realpathSync(path: fs.PathLike, options?: fs.EncodingOption): st
|
|
|
622
622
|
export function realpathSync(path: fs.PathLike, options?: fs.EncodingOption | fs.BufferEncodingOption): string | Buffer {
|
|
623
623
|
path = normalizePath(path);
|
|
624
624
|
const { base, dir } = parse(path);
|
|
625
|
-
const lpath = join(dir == '/' ? '/' : realpathSync(dir), base);
|
|
625
|
+
const lpath = join(dir == '/' ? '/' : cache.paths.getSync(dir) || realpathSync(dir), base);
|
|
626
626
|
const { fs, path: resolvedPath, mountPoint } = resolveMount(lpath);
|
|
627
627
|
|
|
628
628
|
try {
|
|
@@ -631,7 +631,8 @@ export function realpathSync(path: fs.PathLike, options?: fs.EncodingOption | fs
|
|
|
631
631
|
return lpath;
|
|
632
632
|
}
|
|
633
633
|
|
|
634
|
-
|
|
634
|
+
const target = mountPoint + readlinkSync(lpath, options).toString();
|
|
635
|
+
return cache.paths.getSync(target) || realpathSync(target);
|
|
635
636
|
} catch (e) {
|
|
636
637
|
if ((e as ErrnoError).code == 'ENOENT') {
|
|
637
638
|
return path;
|
|
@@ -658,7 +659,7 @@ export function rmSync(path: fs.PathLike, options?: fs.RmOptions & InternalOptio
|
|
|
658
659
|
|
|
659
660
|
let stats: Stats | undefined;
|
|
660
661
|
try {
|
|
661
|
-
stats = cache.
|
|
662
|
+
stats = cache.stats.getSync(path) || statSync(path);
|
|
662
663
|
} catch (error) {
|
|
663
664
|
if ((error as ErrnoError).code != 'ENOENT' || !options?.force) throw error;
|
|
664
665
|
}
|
|
@@ -667,7 +668,7 @@ export function rmSync(path: fs.PathLike, options?: fs.RmOptions & InternalOptio
|
|
|
667
668
|
return;
|
|
668
669
|
}
|
|
669
670
|
|
|
670
|
-
cache.
|
|
671
|
+
cache.stats.setSync(path, stats);
|
|
671
672
|
|
|
672
673
|
switch (stats.mode & constants.S_IFMT) {
|
|
673
674
|
case constants.S_IFDIR:
|
|
@@ -688,12 +689,12 @@ export function rmSync(path: fs.PathLike, options?: fs.RmOptions & InternalOptio
|
|
|
688
689
|
case constants.S_IFIFO:
|
|
689
690
|
case constants.S_IFSOCK:
|
|
690
691
|
default:
|
|
691
|
-
cache.
|
|
692
|
+
cache.stats.clearSync();
|
|
692
693
|
throw new ErrnoError(Errno.EPERM, 'File type not supported', path, 'rm');
|
|
693
694
|
}
|
|
694
695
|
|
|
695
696
|
if (!options?._isIndirect) {
|
|
696
|
-
cache.
|
|
697
|
+
cache.stats.clearSync();
|
|
697
698
|
}
|
|
698
699
|
}
|
|
699
700
|
rmSync satisfies typeof fs.rmSync;
|
package/src/file.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { FileReadResult } from 'node:fs/promises';
|
|
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
2
|
import { config } from './emulation/config.js';
|
|
3
|
+
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';
|
|
4
4
|
import { Errno, ErrnoError } from './error.js';
|
|
5
5
|
import type { FileSystem } from './filesystem.js';
|
|
6
6
|
import './polyfills.js';
|
|
@@ -145,7 +145,7 @@ export function isExclusive(flag: string): boolean {
|
|
|
145
145
|
return flag.indexOf('x') !== -1;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
export abstract class File {
|
|
148
|
+
export abstract class File<FS extends FileSystem = FileSystem> {
|
|
149
149
|
public constructor(
|
|
150
150
|
/**
|
|
151
151
|
* @internal
|
|
@@ -166,12 +166,12 @@ export abstract class File {
|
|
|
166
166
|
public abstract close(): Promise<void>;
|
|
167
167
|
public abstract closeSync(): void;
|
|
168
168
|
|
|
169
|
-
public [Symbol.asyncDispose](): Promise<void> {
|
|
170
|
-
|
|
169
|
+
public async [Symbol.asyncDispose](): Promise<void> {
|
|
170
|
+
await this.close();
|
|
171
171
|
}
|
|
172
172
|
|
|
173
173
|
public [Symbol.dispose](): void {
|
|
174
|
-
|
|
174
|
+
this.closeSync();
|
|
175
175
|
}
|
|
176
176
|
|
|
177
177
|
public abstract truncate(len: number): Promise<void>;
|
|
@@ -269,7 +269,7 @@ export abstract class File {
|
|
|
269
269
|
* An implementation of `File` that operates completely in-memory.
|
|
270
270
|
* `PreloadFile`s are backed by a `Uint8Array`.
|
|
271
271
|
*/
|
|
272
|
-
export class PreloadFile<FS extends FileSystem> extends File {
|
|
272
|
+
export class PreloadFile<FS extends FileSystem> extends File<FS> {
|
|
273
273
|
/**
|
|
274
274
|
* Current position
|
|
275
275
|
*/
|
|
@@ -290,11 +290,7 @@ export class PreloadFile<FS extends FileSystem> extends File {
|
|
|
290
290
|
* Note that, if contents is specified, it will be mutated by the file.
|
|
291
291
|
*/
|
|
292
292
|
public constructor(
|
|
293
|
-
|
|
294
|
-
* The file system that created the file.
|
|
295
|
-
* @internal
|
|
296
|
-
*/
|
|
297
|
-
public fs: FS,
|
|
293
|
+
fs: FS,
|
|
298
294
|
path: string,
|
|
299
295
|
public readonly flag: string,
|
|
300
296
|
public readonly stats: Stats,
|
|
@@ -431,12 +427,12 @@ export class PreloadFile<FS extends FileSystem> extends File {
|
|
|
431
427
|
if (length > this._buffer.length) {
|
|
432
428
|
const data = new Uint8Array(length - this._buffer.length);
|
|
433
429
|
// Write will set stats.size and handle syncing.
|
|
434
|
-
this.
|
|
430
|
+
this._write(data, 0, data.length, this._buffer.length);
|
|
435
431
|
return;
|
|
436
432
|
}
|
|
437
433
|
this.stats.size = length;
|
|
438
434
|
// Truncate.
|
|
439
|
-
this._buffer = this._buffer.slice(0, length);
|
|
435
|
+
this._buffer = length ? this._buffer.slice(0, length) : new Uint8Array();
|
|
440
436
|
}
|
|
441
437
|
|
|
442
438
|
public async truncate(length: number): Promise<void> {
|
|
@@ -645,14 +641,6 @@ export class PreloadFile<FS extends FileSystem> extends File {
|
|
|
645
641
|
this.stats.mode = (this.stats.mode & ~S_IFMT) | type;
|
|
646
642
|
this.syncSync();
|
|
647
643
|
}
|
|
648
|
-
|
|
649
|
-
public async [Symbol.asyncDispose]() {
|
|
650
|
-
await this.close();
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
public [Symbol.dispose]() {
|
|
654
|
-
this.closeSync();
|
|
655
|
-
}
|
|
656
644
|
}
|
|
657
645
|
|
|
658
646
|
/**
|
package/src/inode.ts
CHANGED
|
@@ -1,11 +1,6 @@
|
|
|
1
|
+
import { deserialize, sizeof, struct, types as t } from 'utilium';
|
|
1
2
|
import { Stats, type StatsLike } from './stats.js';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Alias for an ino.
|
|
6
|
-
* This will be helpful if in the future inode numbers/IDs are changed to strings or numbers.
|
|
7
|
-
*/
|
|
8
|
-
export type Ino = bigint;
|
|
3
|
+
import { randomBigInt } from './utils.js';
|
|
9
4
|
|
|
10
5
|
/**
|
|
11
6
|
* Room inode
|
|
@@ -13,31 +8,13 @@ export type Ino = bigint;
|
|
|
13
8
|
*/
|
|
14
9
|
export const rootIno = 0n;
|
|
15
10
|
|
|
16
|
-
/**
|
|
17
|
-
* Generates a random 32 bit integer, then converts to a hex string
|
|
18
|
-
*/
|
|
19
|
-
function _random() {
|
|
20
|
-
return Math.round(Math.random() * 2 ** 32).toString(16);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Generate a random ino
|
|
25
|
-
* @internal
|
|
26
|
-
*/
|
|
27
|
-
export function randomIno(): Ino {
|
|
28
|
-
return BigInt('0x' + _random() + _random());
|
|
29
|
-
}
|
|
30
|
-
|
|
31
11
|
/**
|
|
32
12
|
* Generic inode definition that can easily be serialized.
|
|
13
|
+
* @internal
|
|
33
14
|
*/
|
|
34
15
|
@struct()
|
|
35
16
|
export class Inode implements StatsLike {
|
|
36
|
-
public
|
|
37
|
-
return serialize(this);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
public constructor(buffer?: ArrayBufferLike) {
|
|
17
|
+
public constructor(buffer?: ArrayBufferLike | ArrayBufferView) {
|
|
41
18
|
if (buffer) {
|
|
42
19
|
if (buffer.byteLength < sizeof(Inode)) {
|
|
43
20
|
throw new RangeError(`Can not create an inode from a buffer less than ${sizeof(Inode)} bytes`);
|
|
@@ -48,7 +25,8 @@ export class Inode implements StatsLike {
|
|
|
48
25
|
}
|
|
49
26
|
|
|
50
27
|
// set defaults on a fresh inode
|
|
51
|
-
this.ino =
|
|
28
|
+
this.ino = randomBigInt();
|
|
29
|
+
this.data = randomBigInt();
|
|
52
30
|
this.nlink = 1;
|
|
53
31
|
this.size = 4096;
|
|
54
32
|
const now = Date.now();
|
|
@@ -58,7 +36,7 @@ export class Inode implements StatsLike {
|
|
|
58
36
|
this.birthtimeMs = now;
|
|
59
37
|
}
|
|
60
38
|
|
|
61
|
-
@t.uint64 public
|
|
39
|
+
@t.uint64 public data!: bigint;
|
|
62
40
|
@t.uint32 public size!: number;
|
|
63
41
|
@t.uint16 public mode!: number;
|
|
64
42
|
@t.uint32 public nlink!: number;
|
|
@@ -68,6 +46,7 @@ export class Inode implements StatsLike {
|
|
|
68
46
|
@t.float64 public birthtimeMs!: number;
|
|
69
47
|
@t.float64 public mtimeMs!: number;
|
|
70
48
|
@t.float64 public ctimeMs!: number;
|
|
49
|
+
@t.uint64 public ino!: bigint;
|
|
71
50
|
|
|
72
51
|
/**
|
|
73
52
|
* Handy function that converts the Inode to a Node Stats object.
|
|
@@ -108,8 +87,8 @@ export class Inode implements StatsLike {
|
|
|
108
87
|
hasChanged = true;
|
|
109
88
|
}
|
|
110
89
|
|
|
111
|
-
if (this.
|
|
112
|
-
this.
|
|
90
|
+
if (this.gid !== stats.gid) {
|
|
91
|
+
this.gid = stats.gid;
|
|
113
92
|
hasChanged = true;
|
|
114
93
|
}
|
|
115
94
|
|
package/src/mixins/async.ts
CHANGED
|
@@ -12,6 +12,28 @@ export type AsyncOperation = {
|
|
|
12
12
|
[K in keyof AsyncFSMethods]: [K, ...Parameters<FileSystem[K]>];
|
|
13
13
|
}[keyof AsyncFSMethods];
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* @internal
|
|
17
|
+
*/
|
|
18
|
+
export interface Async {
|
|
19
|
+
/**
|
|
20
|
+
* @internal @protected
|
|
21
|
+
*/
|
|
22
|
+
_sync?: FileSystem;
|
|
23
|
+
queueDone(): Promise<void>;
|
|
24
|
+
ready(): Promise<void>;
|
|
25
|
+
renameSync(oldPath: string, newPath: string): void;
|
|
26
|
+
statSync(path: string): Stats;
|
|
27
|
+
createFileSync(path: string, flag: string, mode: number): File;
|
|
28
|
+
openFileSync(path: string, flag: string): File;
|
|
29
|
+
unlinkSync(path: string): void;
|
|
30
|
+
rmdirSync(path: string): void;
|
|
31
|
+
mkdirSync(path: string, mode: number): void;
|
|
32
|
+
readdirSync(path: string): string[];
|
|
33
|
+
linkSync(srcpath: string, dstpath: string): void;
|
|
34
|
+
syncSync(path: string, data: Uint8Array, stats: Readonly<Stats>): void;
|
|
35
|
+
}
|
|
36
|
+
|
|
15
37
|
/**
|
|
16
38
|
* Async() implements synchronous methods on an asynchronous file system
|
|
17
39
|
*
|
|
@@ -22,29 +44,7 @@ export type AsyncOperation = {
|
|
|
22
44
|
* During loading, the contents of the async file system are preloaded into the synchronous store.
|
|
23
45
|
*
|
|
24
46
|
*/
|
|
25
|
-
export function Async<T extends typeof FileSystem>(
|
|
26
|
-
FS: T
|
|
27
|
-
): Mixin<
|
|
28
|
-
T,
|
|
29
|
-
{
|
|
30
|
-
/**
|
|
31
|
-
* @internal @protected
|
|
32
|
-
*/
|
|
33
|
-
_sync?: FileSystem;
|
|
34
|
-
queueDone(): Promise<void>;
|
|
35
|
-
ready(): Promise<void>;
|
|
36
|
-
renameSync(oldPath: string, newPath: string): void;
|
|
37
|
-
statSync(path: string): Stats;
|
|
38
|
-
createFileSync(path: string, flag: string, mode: number): File;
|
|
39
|
-
openFileSync(path: string, flag: string): File;
|
|
40
|
-
unlinkSync(path: string): void;
|
|
41
|
-
rmdirSync(path: string): void;
|
|
42
|
-
mkdirSync(path: string, mode: number): void;
|
|
43
|
-
readdirSync(path: string): string[];
|
|
44
|
-
linkSync(srcpath: string, dstpath: string): void;
|
|
45
|
-
syncSync(path: string, data: Uint8Array, stats: Readonly<Stats>): void;
|
|
46
|
-
}
|
|
47
|
-
> {
|
|
47
|
+
export function Async<const T extends typeof FileSystem>(FS: T): Mixin<T, Async> {
|
|
48
48
|
abstract class AsyncFS extends FS {
|
|
49
49
|
/**
|
|
50
50
|
* Queue of pending asynchronous operations.
|
|
@@ -67,6 +67,7 @@ export function Async<T extends typeof FileSystem>(
|
|
|
67
67
|
|
|
68
68
|
public async ready(): Promise<void> {
|
|
69
69
|
await super.ready();
|
|
70
|
+
await this.queueDone();
|
|
70
71
|
if (this._isInitialized || this._disableSync) {
|
|
71
72
|
return;
|
|
72
73
|
}
|
|
@@ -128,7 +129,7 @@ export function Async<T extends typeof FileSystem>(
|
|
|
128
129
|
|
|
129
130
|
public openFileSync(path: string, flag: string): PreloadFile<this> {
|
|
130
131
|
this.checkSync(path, 'openFile');
|
|
131
|
-
const file = this._sync.openFileSync(path, flag);
|
|
132
|
+
const file = this._sync.openFileSync(path, flag + '+');
|
|
132
133
|
const stats = file.statSync();
|
|
133
134
|
const buffer = new Uint8Array(stats.size);
|
|
134
135
|
file.readSync(buffer);
|
|
@@ -225,3 +226,5 @@ export function Async<T extends typeof FileSystem>(
|
|
|
225
226
|
|
|
226
227
|
return AsyncFS;
|
|
227
228
|
}
|
|
229
|
+
|
|
230
|
+
export function asyncPatch<T extends typeof FileSystem>(fs: Mixin<T, Async>) {}
|
package/src/stats.ts
CHANGED
|
@@ -1,6 +1,28 @@
|
|
|
1
1
|
import type * as Node from 'node:fs';
|
|
2
2
|
import { credentials, type Credentials } from './credentials.js';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
R_OK,
|
|
5
|
+
S_IFBLK,
|
|
6
|
+
S_IFCHR,
|
|
7
|
+
S_IFDIR,
|
|
8
|
+
S_IFIFO,
|
|
9
|
+
S_IFLNK,
|
|
10
|
+
S_IFMT,
|
|
11
|
+
S_IFREG,
|
|
12
|
+
S_IFSOCK,
|
|
13
|
+
S_IRGRP,
|
|
14
|
+
S_IROTH,
|
|
15
|
+
S_IRUSR,
|
|
16
|
+
S_IWGRP,
|
|
17
|
+
S_IWOTH,
|
|
18
|
+
S_IWUSR,
|
|
19
|
+
S_IXGRP,
|
|
20
|
+
S_IXOTH,
|
|
21
|
+
S_IXUSR,
|
|
22
|
+
size_max,
|
|
23
|
+
W_OK,
|
|
24
|
+
X_OK,
|
|
25
|
+
} from './emulation/constants.js';
|
|
4
26
|
|
|
5
27
|
/**
|
|
6
28
|
* Indicates the type of a file. Applied to 'mode'.
|
|
@@ -222,13 +244,33 @@ export abstract class StatsCommon<T extends number | bigint> implements Node.Sta
|
|
|
222
244
|
*/
|
|
223
245
|
public hasAccess(mode: number): boolean {
|
|
224
246
|
if (credentials.euid === 0 || credentials.egid === 0) {
|
|
225
|
-
//Running as root
|
|
247
|
+
// Running as root
|
|
226
248
|
return true;
|
|
227
249
|
}
|
|
228
250
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
251
|
+
let perm = 0;
|
|
252
|
+
|
|
253
|
+
// Owner permissions
|
|
254
|
+
if (credentials.uid === this.uid) {
|
|
255
|
+
if (this.mode & S_IRUSR) perm |= R_OK;
|
|
256
|
+
if (this.mode & S_IWUSR) perm |= W_OK;
|
|
257
|
+
if (this.mode & S_IXUSR) perm |= X_OK;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Group permissions
|
|
261
|
+
if (credentials.gid === this.gid) {
|
|
262
|
+
if (this.mode & S_IRGRP) perm |= R_OK;
|
|
263
|
+
if (this.mode & S_IWGRP) perm |= W_OK;
|
|
264
|
+
if (this.mode & S_IXGRP) perm |= X_OK;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Others permissions
|
|
268
|
+
if (this.mode & S_IROTH) perm |= R_OK;
|
|
269
|
+
if (this.mode & S_IWOTH) perm |= W_OK;
|
|
270
|
+
if (this.mode & S_IXOTH) perm |= X_OK;
|
|
271
|
+
|
|
272
|
+
// Perform the access check
|
|
273
|
+
return (perm & mode) === mode;
|
|
232
274
|
}
|
|
233
275
|
|
|
234
276
|
/**
|
package/src/utils.ts
CHANGED
|
@@ -182,22 +182,6 @@ export function encodeDirListing(data: Record<string, bigint>): Uint8Array {
|
|
|
182
182
|
|
|
183
183
|
export type Callback<Args extends unknown[] = []> = (e?: ErrnoError, ...args: OptionalTuple<Args>) => unknown;
|
|
184
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
185
|
/**
|
|
202
186
|
* Normalizes a mode
|
|
203
187
|
* @internal
|
|
@@ -230,15 +214,11 @@ export function normalizeTime(time: string | number | Date): Date {
|
|
|
230
214
|
return time;
|
|
231
215
|
}
|
|
232
216
|
|
|
233
|
-
|
|
234
|
-
return new Date(time * 1000);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
if (typeof time == 'string') {
|
|
217
|
+
try {
|
|
238
218
|
return new Date(time);
|
|
219
|
+
} catch {
|
|
220
|
+
throw new ErrnoError(Errno.EINVAL, 'Invalid time.');
|
|
239
221
|
}
|
|
240
|
-
|
|
241
|
-
throw new ErrnoError(Errno.EINVAL, 'Invalid time.');
|
|
242
222
|
}
|
|
243
223
|
|
|
244
224
|
/**
|
|
@@ -286,3 +266,11 @@ export function normalizeOptions(
|
|
|
286
266
|
}
|
|
287
267
|
|
|
288
268
|
export type Concrete<T extends ClassLike> = Pick<T, keyof T> & (new (...args: any[]) => InstanceType<T>);
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Generate a random ino
|
|
272
|
+
* @internal
|
|
273
|
+
*/
|
|
274
|
+
export function randomBigInt(): bigint {
|
|
275
|
+
return crypto.getRandomValues(new BigUint64Array(1))[0];
|
|
276
|
+
}
|
package/tests/fs/dir.test.ts
CHANGED
|
@@ -3,18 +3,20 @@ import { suite, test } from 'node:test';
|
|
|
3
3
|
import { fs } from '../common.js';
|
|
4
4
|
|
|
5
5
|
const testFile = 'test-file.txt';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
fs.writeFileSync(testFile, 'Sample content');
|
|
7
|
+
fs.mkdirSync('test-directory');
|
|
8
|
+
fs.symlinkSync(testFile, 'test-symlink');
|
|
9
9
|
const testDirPath = 'test-dir';
|
|
10
10
|
const testFiles = ['file1.txt', 'file2.txt'];
|
|
11
|
-
|
|
11
|
+
fs.mkdirSync(testDirPath);
|
|
12
12
|
for (const file of testFiles) {
|
|
13
|
-
|
|
13
|
+
fs.writeFileSync(`${testDirPath}/${file}`, 'Sample content');
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
await fs._synced();
|
|
17
|
+
|
|
16
18
|
suite('Dirent', () => {
|
|
17
|
-
test('
|
|
19
|
+
test('name and parentPath getters', async () => {
|
|
18
20
|
const stats = await fs.promises.lstat(testFile);
|
|
19
21
|
const dirent = new fs.Dirent(testFile, stats);
|
|
20
22
|
|
|
@@ -22,7 +24,7 @@ suite('Dirent', () => {
|
|
|
22
24
|
assert.equal(dirent.parentPath, testFile);
|
|
23
25
|
});
|
|
24
26
|
|
|
25
|
-
test('
|
|
27
|
+
test('isFile', async () => {
|
|
26
28
|
const fileStats = await fs.promises.lstat(testFile);
|
|
27
29
|
const fileDirent = new fs.Dirent(testFile, fileStats);
|
|
28
30
|
|
|
@@ -30,7 +32,7 @@ suite('Dirent', () => {
|
|
|
30
32
|
assert(!fileDirent.isDirectory());
|
|
31
33
|
});
|
|
32
34
|
|
|
33
|
-
test('
|
|
35
|
+
test('isDirectory', async () => {
|
|
34
36
|
const dirStats = await fs.promises.lstat('test-directory');
|
|
35
37
|
const dirDirent = new fs.Dirent('test-directory', dirStats);
|
|
36
38
|
|
|
@@ -38,14 +40,14 @@ suite('Dirent', () => {
|
|
|
38
40
|
assert(dirDirent.isDirectory());
|
|
39
41
|
});
|
|
40
42
|
|
|
41
|
-
test('
|
|
43
|
+
test('isSymbolicLink', async () => {
|
|
42
44
|
const symlinkStats = await fs.promises.lstat('test-symlink');
|
|
43
45
|
const symlinkDirent = new fs.Dirent('test-symlink', symlinkStats);
|
|
44
46
|
|
|
45
47
|
assert(symlinkDirent.isSymbolicLink());
|
|
46
48
|
});
|
|
47
49
|
|
|
48
|
-
test('
|
|
50
|
+
test('other methods return false', async () => {
|
|
49
51
|
const fileStats = await fs.promises.lstat(testFile);
|
|
50
52
|
const fileDirent = new fs.Dirent(testFile, fileStats);
|
|
51
53
|
|
|
@@ -56,7 +58,7 @@ suite('Dirent', () => {
|
|
|
56
58
|
});
|
|
57
59
|
|
|
58
60
|
suite('Dir', () => {
|
|
59
|
-
test('
|
|
61
|
+
test('read()', async () => {
|
|
60
62
|
const dir = new fs.Dir(testDirPath);
|
|
61
63
|
|
|
62
64
|
const dirent1 = await dir.read();
|
|
@@ -73,19 +75,7 @@ suite('Dir', () => {
|
|
|
73
75
|
await dir.close();
|
|
74
76
|
});
|
|
75
77
|
|
|
76
|
-
test('
|
|
77
|
-
const dir = new fs.Dir(testDirPath);
|
|
78
|
-
dir.read((err, dirent) => {
|
|
79
|
-
assert.strictEqual(err, undefined);
|
|
80
|
-
assert.notEqual(dirent, undefined);
|
|
81
|
-
assert(dirent instanceof fs.Dirent);
|
|
82
|
-
assert(testFiles.includes(dirent?.name));
|
|
83
|
-
dir.closeSync();
|
|
84
|
-
done();
|
|
85
|
-
});
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
test('Dir readSync() method', () => {
|
|
78
|
+
test('readSync()', () => {
|
|
89
79
|
const dir = new fs.Dir(testDirPath);
|
|
90
80
|
|
|
91
81
|
const dirent1 = dir.readSync();
|
|
@@ -102,19 +92,19 @@ suite('Dir', () => {
|
|
|
102
92
|
dir.closeSync();
|
|
103
93
|
});
|
|
104
94
|
|
|
105
|
-
test('
|
|
95
|
+
test('close()', async () => {
|
|
106
96
|
const dir = new fs.Dir(testDirPath);
|
|
107
97
|
await dir.close();
|
|
108
98
|
rejects(dir.read(), 'Can not use closed Dir');
|
|
109
99
|
});
|
|
110
100
|
|
|
111
|
-
test('
|
|
101
|
+
test('closeSync()', () => {
|
|
112
102
|
const dir = new fs.Dir(testDirPath);
|
|
113
103
|
dir.closeSync();
|
|
114
104
|
assert.throws(() => dir.readSync(), 'Can not use closed Dir');
|
|
115
105
|
});
|
|
116
106
|
|
|
117
|
-
test('
|
|
107
|
+
test('asynchronous iteration', async () => {
|
|
118
108
|
const dir = new fs.Dir(testDirPath);
|
|
119
109
|
const dirents: fs.Dirent[] = [];
|
|
120
110
|
|
|
@@ -128,26 +118,26 @@ suite('Dir', () => {
|
|
|
128
118
|
assert(testFiles.includes(dirents[1].name));
|
|
129
119
|
});
|
|
130
120
|
|
|
131
|
-
test('
|
|
121
|
+
test('read after directory is closed', async () => {
|
|
132
122
|
const dir = new fs.Dir(testDirPath);
|
|
133
123
|
await dir.close();
|
|
134
124
|
await assert.rejects(dir.read(), 'Can not use closed Dir');
|
|
135
125
|
});
|
|
136
126
|
|
|
137
|
-
test('
|
|
127
|
+
test('readSync after directory is closed', () => {
|
|
138
128
|
const dir = new fs.Dir(testDirPath);
|
|
139
129
|
dir.closeSync();
|
|
140
130
|
assert.throws(() => dir.readSync(), 'Can not use closed Dir');
|
|
141
131
|
});
|
|
142
132
|
|
|
143
|
-
test('
|
|
133
|
+
test('close multiple times', async () => {
|
|
144
134
|
const dir = new fs.Dir(testDirPath);
|
|
145
135
|
await dir.close();
|
|
146
136
|
await dir.close(); // Should not throw an error
|
|
147
137
|
assert(dir['closed']);
|
|
148
138
|
});
|
|
149
139
|
|
|
150
|
-
test('
|
|
140
|
+
test('closeSync multiple times', () => {
|
|
151
141
|
const dir = new fs.Dir(testDirPath);
|
|
152
142
|
dir.closeSync();
|
|
153
143
|
dir.closeSync(); // Should not throw an error
|
|
@@ -7,17 +7,19 @@ const testDir = 'test-dir';
|
|
|
7
7
|
const testFiles = ['file1.txt', 'file2.txt', 'file3.txt'];
|
|
8
8
|
const testDirectories = ['subdir1', 'subdir2'];
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
fs.mkdirSync(testDir);
|
|
11
11
|
for (const file of testFiles) {
|
|
12
|
-
|
|
12
|
+
fs.writeFileSync(`${testDir}/${file}`, 'Sample content');
|
|
13
13
|
}
|
|
14
14
|
for (const dir of testDirectories) {
|
|
15
|
-
|
|
15
|
+
fs.mkdirSync(`${testDir}/${dir}`);
|
|
16
16
|
for (const file of ['file4.txt', 'file5.txt']) {
|
|
17
|
-
|
|
17
|
+
fs.writeFileSync(`${testDir}/${dir}/${file}`, 'Sample content');
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
await fs._synced();
|
|
22
|
+
|
|
21
23
|
suite('Directories', () => {
|
|
22
24
|
test('mkdir', async () => {
|
|
23
25
|
await fs.promises.mkdir('/one', 0o755);
|
package/tests/fs/links.test.ts
CHANGED
|
@@ -2,6 +2,7 @@ import assert from 'node:assert';
|
|
|
2
2
|
import { suite, test } from 'node:test';
|
|
3
3
|
import { join } from '../../dist/emulation/path.js';
|
|
4
4
|
import { fs } from '../common.js';
|
|
5
|
+
import type { ErrnoError } from '../../dist/error.js';
|
|
5
6
|
|
|
6
7
|
suite('Links', () => {
|
|
7
8
|
const target = '/a1.js',
|
|
@@ -28,8 +29,14 @@ suite('Links', () => {
|
|
|
28
29
|
assert(await fs.promises.exists(target));
|
|
29
30
|
});
|
|
30
31
|
|
|
31
|
-
test('link', async
|
|
32
|
-
await fs.promises.link(target, hardlink)
|
|
32
|
+
test('link', async t => {
|
|
33
|
+
const _ = await fs.promises.link(target, hardlink).catch((e: ErrnoError) => {
|
|
34
|
+
if (e.code == 'ENOSYS') return e;
|
|
35
|
+
throw e;
|
|
36
|
+
});
|
|
37
|
+
if (_) {
|
|
38
|
+
return t.skip('Backend does not support hard links');
|
|
39
|
+
}
|
|
33
40
|
const targetContent = await fs.promises.readFile(target, 'utf8');
|
|
34
41
|
const linkContent = await fs.promises.readFile(hardlink, 'utf8');
|
|
35
42
|
assert.strictEqual(targetContent, linkContent);
|
|
@@ -43,8 +43,8 @@ suite('Permissions', () => {
|
|
|
43
43
|
const stats = await fs.promises.lstat(link);
|
|
44
44
|
assert.equal(stats.mode & 0o777, asyncMode);
|
|
45
45
|
|
|
46
|
-
fs.
|
|
47
|
-
assert.equal(fs.
|
|
46
|
+
await fs.promises.lchmod(link, syncMode);
|
|
47
|
+
assert.equal((await fs.promises.lstat(link)).mode & 0o777, syncMode);
|
|
48
48
|
});
|
|
49
49
|
|
|
50
50
|
async function test_item(path: string): Promise<void> {
|