@gjsify/fs 0.30.0 → 0.32.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.
@@ -0,0 +1,188 @@
1
+ import Gio from '@girs/gio-2.0';
2
+ import type { OpenSpec } from './posix-flags.js';
3
+ /** Build a Node `ErrnoException` from a code we determined ourselves. */
4
+ export declare function fsError(code: string, syscall: string, path?: string, dest?: string): NodeJS.ErrnoException;
5
+ /**
6
+ * Can this host name an open descriptor as a path?
7
+ *
8
+ * Measured once, and only ONCE it is needed — a module-eval probe would run in
9
+ * every consumer that merely imports `node:fs`. `respond-with-file.ts` carries
10
+ * the scar of assuming procfs exists: the path resolved to nothing on macOS,
11
+ * `statSync` threw, and a swallowed catch meant the check silently never ran.
12
+ */
13
+ export declare function hasProcFd(): boolean;
14
+ /**
15
+ * A path that names the DESCRIPTOR rather than the name it was opened from,
16
+ * or `null` where the host cannot express one.
17
+ *
18
+ * This is what makes `fstat`/`fchmod`/`ftruncate` survive a rename or an
19
+ * unlink: `/proc/self/fd/<fd>` resolves to the inode the fd holds, even after
20
+ * the directory entry is gone.
21
+ */
22
+ export declare function fdPath(fd: number): string | null;
23
+ /**
24
+ * `open(2)`.
25
+ *
26
+ * The mode reaches the kernel, so it is applied ATOMICALLY at creation, masked
27
+ * by the live umask, and it survives a crash — none of which a post-create or
28
+ * close-time `chmod` can offer. `O_EXCL` is likewise the kernel's, so it is
29
+ * atomic and it refuses a symlink whatever the link points at.
30
+ */
31
+ export declare function openFd(path: string, spec: OpenSpec, mode: number): number;
32
+ export declare function closeFd(fd: number): void;
33
+ /**
34
+ * Name the reason `open(2)` refused.
35
+ *
36
+ * GJS cannot read `errno`, so the code is reconstructed from what is observable
37
+ * AFTER the kernel has already refused. That makes this a classification, not a
38
+ * check-then-act: it never decides whether to create anything, so it carries no
39
+ * TOCTOU. The `lstat`-equivalent lookup (`NOFOLLOW_SYMLINKS`) matters — a
40
+ * dangling symlink must read as "the name is taken", which is exactly why
41
+ * `file_test(EXISTS)` was the wrong instrument for the check this replaces.
42
+ */
43
+ export declare function classifyOpenFailure(path: string, spec: OpenSpec): NodeJS.ErrnoException;
44
+ /**
45
+ * Does this `GFileInfo` actually ANSWER, or is it an empty shell?
46
+ *
47
+ * `g_file_query_info()` does NOT fail when the kernel refuses to stat a name.
48
+ * A path under a directory the process cannot search comes back as a non-NULL
49
+ * `GFileInfo` carrying NO attributes at all — measured directly:
50
+ * `list_attributes(null)` is `[]` and `has_attribute('standard::type')` is
51
+ * `false`, where an ordinary lookup returns `['standard::type']`.
52
+ *
53
+ * Two things follow, and this package got both wrong. Reading the type off
54
+ * that shell logs `GFileInfo created without standard::type` plus
55
+ * `g_file_info_get_file_type: should not be reached` — two `GLib-GIO-CRITICAL`
56
+ * lines that `G_DEBUG=fatal-criticals` (standard in GNOME CI, and the exact
57
+ * condition {@link isSeekableFd} below exists to satisfy) turns into a
58
+ * SIGABRT before the caller's `catch` can run. And taking the shell as proof
59
+ * the name EXISTS answered EEXIST — "the name is taken" — for a plain
60
+ * permission denial, which sends an `openSync(lock,'wx')` retry loop round
61
+ * forever and makes the ubiquitous
62
+ * `try { mkdirSync(d) } catch (e) { if (e.code !== 'EEXIST') throw }`
63
+ * swallow a hard EACCES and carry on as if the directory were there.
64
+ *
65
+ * So every read of an attribute in this module is gated on this, and a
66
+ * `false` means "no answer" — never "no".
67
+ *
68
+ * Exported because the stat family needs the identical gate and used to lack
69
+ * it: `statSync` under an unsearchable directory built a `Stats` from the empty
70
+ * shell, which is BOTH of the failures above at once — four CRITICALs (hence a
71
+ * SIGABRT under `G_DEBUG=fatal-criticals`) and then a fabricated `{mode: 0,
72
+ * size: 0, ino: 0}` returned as fact where Node raises EACCES. See
73
+ * `statsFrom()` in `stats.ts`, which is the one place that decision now lives.
74
+ */
75
+ export declare function answered(info: Gio.FileInfo | null): boolean;
76
+ /** The same reconstruction for `mkdir(2)`, which also returns a bare `-1`. */
77
+ export declare function classifyMkdirFailure(path: string): NodeJS.ErrnoException;
78
+ /**
79
+ * Can this descriptor be positioned at all?
80
+ *
81
+ * `seekFd` used to be issued before EVERY read and write, unconditionally, so a
82
+ * FIFO / pipe / socket / tty produced one
83
+ * `g_io_channel_seek_position: assertion 'channel->is_seekable' failed`
84
+ * per I/O call — a `GLib-CRITICAL`, which `G_DEBUG=fatal-criticals` (standard
85
+ * in GNOME CI) turns into an abort. The bytes happened to survive because a
86
+ * pipe ignores the offset, so the failed seek was a silent no-op and `_pos`
87
+ * accumulated a number that meant nothing.
88
+ *
89
+ * The answer is therefore ASKED FOR rather than discovered by tripping an
90
+ * assertion. `GIOChannel` decides it with `lseek(fd, 0, SEEK_CUR)`, which GJS
91
+ * cannot reach, and its `is_seekable` field is not introspectable — so the file
92
+ * TYPE is the available instrument.
93
+ *
94
+ * The rule is deliberately narrow: only `S_IFIFO` and `S_IFSOCK` are refused.
95
+ * The tempting "seekable means `S_IFREG` or `S_IFBLK`" is WRONG and would trade
96
+ * this defect for a worse one — a character device usually seeks fine
97
+ * (`noop_llseek`), and Node performs positional I/O on `/dev/null`,
98
+ * `/dev/zero` and `/dev/urandom` without complaint (measured against
99
+ * v24.15.0). Refusing those would have turned a stray log line into an ESPIPE
100
+ * that Node never raises.
101
+ *
102
+ * The type test alone is NOT the whole answer, and a TTY is where it runs out:
103
+ * `/dev/pts/N` is `S_IFCHR` exactly like `/dev/zero`, but it has `no_llseek`.
104
+ * So after the type test the DESCRIPTOR ITSELF is asked, with the same
105
+ * `lseek(fd, 0, SEEK_CUR)` `GIOChannel` uses internally — a pure query that
106
+ * moves nothing. Measured under gjs 1.88.1: it returns `G_IO_STATUS_NORMAL` on
107
+ * a regular file, on `/dev/zero` and on `/dev/null`, and THROWS
108
+ * `GLib.IOChannelError` ("Illegal seek") on a pty slave — cleanly, with no
109
+ * CRITICAL, because a `GIOChannel` over a tty is `is_seekable` and the failure
110
+ * comes from the kernel rather than from an assertion. Order matters: the type
111
+ * test runs FIRST because a FIFO / socket channel is NOT `is_seekable`, and
112
+ * asking one to seek is the `g_io_channel_seek_position: assertion failed`
113
+ * CRITICAL this function exists to avoid.
114
+ *
115
+ * Without the second test, writing to a terminal by path was impossible: the
116
+ * unconditional `seekFd` threw a RAW `Gio.IOErrorEnum` — `code === 8` as a
117
+ * NUMBER, `instanceof Error === false` — straight out of `writeFileSync`,
118
+ * `writeSync` and `createWriteStream`, so `catch (e) { if (e.code === 'ESPIPE') }`
119
+ * could not see it and the classic "write to the terminal even when stdout is
120
+ * redirected" idiom simply failed.
121
+ *
122
+ * The process's own stdin/stdout/stderr — the overwhelmingly common tty case —
123
+ * never reach this code: `isStdFd()` intercepts them first.
124
+ *
125
+ * Cached per fd: the type behind an open file description cannot change under
126
+ * it, and the cache is dropped by {@link releaseFd} so a recycled fd number
127
+ * never inherits the previous holder's answer.
128
+ */
129
+ export declare function isSeekableFd(fd: number, fallbackPath: string): boolean;
130
+ /** Absolute `lseek(2)`. Only valid on a descriptor {@link isSeekableFd} accepts. */
131
+ export declare function seekFd(fd: number, position: number): void;
132
+ /**
133
+ * The descriptor's true offset, or `null` where the host cannot report it.
134
+ *
135
+ * Only needed where the KERNEL moved the offset behind us — an `O_APPEND`
136
+ * write jumps to EOF without telling anyone, and computing the new position
137
+ * from the pre-write value is how the previous attempt let the cursor drift
138
+ * away from the file after the first append. `seek_position()` cannot answer
139
+ * this: it returns a `GIOStatus`, not an offset.
140
+ */
141
+ export declare function tellFd(fd: number): number | null;
142
+ /** `read(2)` at the descriptor's current offset. The kernel advances it. */
143
+ export declare function readFd(fd: number, length: number): Uint8Array;
144
+ /** `write(2)` at the descriptor's current offset. The kernel advances it. */
145
+ export declare function writeFd(fd: number, data: Uint8Array): number;
146
+ export declare function fsyncFd(fd: number): void;
147
+ /**
148
+ * Release the cached per-fd views.
149
+ *
150
+ * The streams must go before the descriptor does: a GioUnix stream outliving
151
+ * its fd would answer a later call against whatever the OS handed that number
152
+ * to next.
153
+ */
154
+ export declare function releaseFd(fd: number): void;
155
+ /** The size of the file behind `fd`, via the descriptor where the host allows. */
156
+ export declare function sizeOfFd(fd: number, fallbackPath: string): number;
157
+ /**
158
+ * `ftruncate(2)`, as closely as GJS can express it.
159
+ *
160
+ * THE PART THAT IS REACHABLE, AND WHERE IT LIVES
161
+ *
162
+ * `ftruncate(2)` checks ONE thing: that the descriptor is open for writing. It
163
+ * never looks at the file's mode. That check is pure bookkeeping we already
164
+ * hold, so it is enforced where the access mode lives — `FileHandle`, next to
165
+ * the identical gates in `_readCore`/`_writeCore` — and NOT here. Without it a
166
+ * handle opened `'r'` could destroy the file, because the re-open below asks
167
+ * the kernel for its own fresh permission and gets it.
168
+ *
169
+ * THE PART THAT IS NOT, MEASURED
170
+ *
171
+ * There is no `g_ftruncate`, and no route from an fd to a `GSeekable`:
172
+ * `GioUnix.OutputStream` does not implement it, and `GBufferedOutputStream` /
173
+ * `GDataOutputStream` wrapped around one both report `can_truncate() == false`
174
+ * (measured under gjs 1.88.1). Truncation therefore needs a second `open(2)`,
175
+ * by the descriptor's procfs name so it lands on the right inode after a rename
176
+ * or an unlink — and a second open is checked against the FILE's mode.
177
+ *
178
+ * So the re-open takes the least privilege that can truncate: `append_to()` is
179
+ * `O_WRONLY`, `open_readwrite()` is `O_RDWR`. That is not a micro-optimisation
180
+ * — it is the difference between working and not on a write-only file: mode
181
+ * 0o200 truncates through the first and is refused by the second (measured).
182
+ * What remains unreachable is a file whose mode denies its own owner write
183
+ * (0o444, 0o400, 0o000): Node truncates those through the descriptor, we cannot.
184
+ * The failure is at least REPORTED now — it used to escape as a bare
185
+ * `Gio.IOErrorEnum` with a numeric `code` and `instanceof Error === false`, so
186
+ * `catch (e) { if (e.code === 'EACCES') }` could not see it.
187
+ */
188
+ export declare function truncateFd(fd: number, length: number, fallbackPath: string): void;
@@ -14,24 +14,28 @@ export declare class FileHandle implements IFileHandle {
14
14
  flags?: OpenFlags | number;
15
15
  mode?: Mode;
16
16
  };
17
- /** Not part of the default implementation, used internal by gjsify */
18
- private _file;
17
+ private readonly _gFile;
18
+ private readonly _pathStr;
19
+ /** What the caller asked open(2) for — the ORIGINAL flags, nothing flattened. */
20
+ private readonly _spec;
19
21
  /**
20
- * Lazily-opened Gio streams for positional read() / write() so each call does
21
- * not re-load the entire file via Gio.File.load_contents(). The IOStream is
22
- * used when the handle was opened with write capability (r+, w, w+, a, a+) —
23
- * it shares seek state between input and output so writes are visible to
24
- * subsequent reads without a flush. For read-only handles we only open a
25
- * FileInputStream; trying to open_readwrite on a read-only file can fall
26
- * back to create_readwrite(REPLACE_DESTINATION) which would truncate it.
22
+ * The one byte position this handle has.
23
+ *
24
+ * A descriptor has exactly ONE offset, shared by reads and writes, and this
25
+ * is it: `_readCore`/`_writeCore` seek to it absolutely before every
26
+ * operation, so the kernel's own offset is a consequence of this value and
27
+ * can never drift away from it. It replaces four carriers that used to
28
+ * disagree — a read-only `_readPos`, the IOChannel's buffered offset, a
29
+ * cached Gio stream's cursor, and a fresh per-call stream that always
30
+ * started at 0.
31
+ *
32
+ * The single exception is an `O_APPEND` write: there the kernel repositions
33
+ * to EOF without telling us, so the new value is READ BACK rather than
34
+ * computed from the pre-write one. Computing it is how the previous attempt
35
+ * let the cursor drift away from the file after the first append.
27
36
  */
28
- private _ioStream;
29
- private _readStream;
30
- private readonly _gFile;
31
- private readonly _ioMode;
32
- /** Current byte offset for `position: null` sync reads — Node advances the fd position. */
33
- private _readPos;
34
- private _ioLock;
37
+ private _pos;
38
+ private _closed;
35
39
  /** Not part of the default implementation, used internal by gjsify */
36
40
  private static instances;
37
41
  constructor(options: {
@@ -40,24 +44,69 @@ export declare class FileHandle implements IFileHandle {
40
44
  mode?: Mode;
41
45
  });
42
46
  /**
43
- * Lazy-open the read-capable stream and return both the input stream and
44
- * its seekable view. Both FileInputStream (read-only handle) and
45
- * FileIOStream (read/write handle) implement Gio.Seekable, but we return
46
- * both to avoid callers needing to know which concrete type they got.
47
+ * Read at `position`, or at the cursor when it is `null`/negative.
48
+ *
49
+ * SYNCHRONOUS ON PURPOSE. The cursor's read-modify-write — read `_pos`,
50
+ * seek, transfer, store `_pos` — has no `await` inside it, so it is
51
+ * indivisible by construction on a single-threaded runtime. The previous
52
+ * attempt held an async lock around the I/O but computed the offset outside
53
+ * it, so two concurrent writers captured the same offset and one write was
54
+ * lost. That class cannot exist here: there is no suspension point for
55
+ * anything to interleave with, which is why the async lock this replaces is
56
+ * gone rather than tightened.
47
57
  */
48
- private _getReadStream;
49
- /** Lazy-open the write-capable stream (IOStream) for this handle. Only valid
50
- * when the handle was opened with a write-capable mode. */
51
- private _getWriteStream;
52
- /** Serialize an async operation on the shared FileIOStream. */
53
- private _serialize;
58
+ private _readCore;
59
+ /** Write at `position`, or at the cursor when it is `null`/negative. See {@link _readCore}. */
60
+ private _writeCore;
61
+ private _assertOpen;
62
+ /**
63
+ * `ftruncate(2)` — ONE body for the sync and async halves.
64
+ *
65
+ * The access-mode gate belongs HERE and not in `truncateFd`, because it is
66
+ * the descriptor's property and `truncateFd` has to re-open the file to do
67
+ * its work (see there) — a re-open that asks the kernel for its own fresh
68
+ * permission and is granted it. Without this line a handle opened `'r'`
69
+ * truncates the file: `writeFileSync(f,'ABCDEFGH'); ftruncateSync(openSync(f,'r'), 2)`
70
+ * destroyed six bytes through a READ-ONLY descriptor and returned normally.
71
+ * Node reports EINVAL, and so does this.
72
+ *
73
+ * `_readCore`/`_writeCore` have carried the identical gate since this
74
+ * redesign began; truncate was the one byte-mover left outside it, which is
75
+ * exactly the shape of divergence the single-core rule exists to prevent.
76
+ */
77
+ private _truncateCore;
78
+ /** Read from the cursor to EOF, advancing it — Node's whole-file read on a handle. */
79
+ private _readToEnd;
80
+ /** The path that names this DESCRIPTOR, falling back to the name it was opened from. */
81
+ private _fdTarget;
54
82
  /**
55
83
  * The numeric file descriptor managed by the {FileHandle} object.
56
84
  * @since v10.0.0
57
85
  */
58
86
  readonly fd: number;
59
- /** Not part of the default implementation, used internal by gjsify */
60
- static getInstance(fd: number): FileHandle;
87
+ /**
88
+ * The handle behind a descriptor number. Not part of Node's API.
89
+ *
90
+ * TWO things used to go wrong here, both of them at the boundary where a
91
+ * caller holds a NUMBER and this package holds an object.
92
+ *
93
+ * It threw `Error('No instance found for fd!')` — no `code`, not an
94
+ * `ErrnoException`. But the overwhelmingly common way to reach it is an fd
95
+ * that WAS valid: `_teardown()` deletes the instance, so every call on a
96
+ * closed descriptor landed here rather than on `_assertOpen`, which made
97
+ * that guard dead for every fd-number call site. `catch (e) { if (e.code
98
+ * === 'EBADF') }` — the idiom for tolerating a racing close — fell through
99
+ * to the generic branch instead. The syscall is threaded in from the caller
100
+ * because Node names it in the message, and we know it there and only there.
101
+ *
102
+ * It also indexed `instances` with whatever it was given. `openSync()`
103
+ * returns a FileHandle rather than Node's number (a pre-existing divergence
104
+ * this does not attempt to change), so `fs.write(fs.openSync(p,'w'), …)`
105
+ * stringified the object into a key, missed, and threw out of the callback
106
+ * machinery. Unwrapping is one line and makes every fd call site accept
107
+ * both spellings.
108
+ */
109
+ static getInstance(fd: number | FileHandle, syscall?: string): FileHandle;
61
110
  /**
62
111
  * Alias of `filehandle.writeFile()`.
63
112
  *
@@ -377,6 +426,19 @@ export declare class FileHandle implements IFileHandle {
377
426
  /** @internal */ _closeSync(): void;
378
427
  /** @internal */ _readSync(buffer: NodeJS.ArrayBufferView, offset: number, length: number, position: number | null): number;
379
428
  /** @internal */ _writeSync(data: Uint8Array, position: number | null): number;
429
+ /** @internal Read from the cursor to EOF, advancing it — `readFileSync(fd)`. */ _readToEndSync(): Uint8Array;
430
+ /** @internal Truncate through the descriptor. */ _truncateSync(len: number): void;
431
+ /** @internal The path that names this descriptor — `/proc/self/fd/N` where the host has it. */
432
+ _fdStatTarget(): string;
433
+ /**
434
+ * Close the descriptor and deregister the handle.
435
+ *
436
+ * ONE implementation for both halves. `close()` used to leave the fd in
437
+ * `instances` while `_closeSync()` removed it, so every `fsPromises` close
438
+ * leaked its entry — and a later `openSync` reusing that fd number found a
439
+ * stale handle waiting for it.
440
+ */
441
+ private _teardown;
380
442
  /**
381
443
  * Closes the file handle after waiting for any pending operation on the handle to
382
444
  * complete.
@@ -0,0 +1,2 @@
1
+ declare const _default: () => Promise<void>;
2
+ export default _default;
@@ -13,7 +13,8 @@ import { Stats, BigIntStats } from './stats.js';
13
13
  import { Dirent } from './dirent.js';
14
14
  import { utimesSync, utimes, lutimesSync, lutimes, lchownSync, lchown, lchmodSync, lchmod } from './utimes.js';
15
15
  import { fstatSync, fstat, ftruncateSync, ftruncate, fdatasyncSync, fdatasync, fsyncSync, fsync, fchmodSync, fchmod, fchownSync, fchown, futimesSync, futimes, closeSync, readSync, writeSync, readvSync, readv, writevSync, writev, exists, openAsBlob } from './fd-ops.js';
16
- export declare const constants: {
16
+ import { type PosixOpenFlags } from './posix-flags.js';
17
+ declare const fixedConstants: {
17
18
  F_OK: number;
18
19
  R_OK: number;
19
20
  W_OK: number;
@@ -21,17 +22,6 @@ export declare const constants: {
21
22
  COPYFILE_EXCL: number;
22
23
  COPYFILE_FICLONE: number;
23
24
  COPYFILE_FICLONE_FORCE: number;
24
- O_RDONLY: number;
25
- O_WRONLY: number;
26
- O_RDWR: number;
27
- O_CREAT: number;
28
- O_EXCL: number;
29
- O_TRUNC: number;
30
- O_APPEND: number;
31
- O_SYNC: number;
32
- O_NONBLOCK: number;
33
- O_DIRECTORY: number;
34
- O_NOFOLLOW: number;
35
25
  S_IFMT: number;
36
26
  S_IFREG: number;
37
27
  S_IFDIR: number;
@@ -53,6 +43,7 @@ export declare const constants: {
53
43
  S_IWOTH: number;
54
44
  S_IXOTH: number;
55
45
  };
46
+ export declare const constants: typeof fixedConstants & PosixOpenFlags;
56
47
  export { FSWatcher, StatWatcher, Stats, BigIntStats, Dirent, Dir, existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, rmdirSync, unlinkSync, mkdtempSync, rmSync, statSync, openSync, realpathSync, symlinkSync, lstatSync, renameSync, copyFileSync, accessSync, appendFileSync, readlinkSync, linkSync, truncateSync, chmodSync, chownSync, cpSync, opendirSync, globSync, glob, watch, watchFile, unwatchFile, statfsSync, statfs, utimesSync, utimes, lutimesSync, lutimes, lchownSync, lchown, lchmodSync, lchmod, fstatSync, fstat, ftruncateSync, ftruncate, fdatasyncSync, fdatasync, fsyncSync, fsync, fchmodSync, fchmod, fchownSync, fchown, futimesSync, futimes, closeSync, readSync, writeSync, readvSync, readv, writevSync, writev, exists, openAsBlob, createReadStream, ReadStream, createWriteStream, WriteStream, promises, open, close, read, write, rm, realpath, readdir, symlink, lstat, stat, rename, copyFile, cp, access, appendFile, readlink, truncate, chmod, chown, mkdir, rmdir, readFile, writeFile, unlink, link, opendir, };
57
48
  declare const _default: {
58
49
  FSWatcher: typeof FSWatcher;
@@ -69,17 +60,6 @@ declare const _default: {
69
60
  COPYFILE_EXCL: number;
70
61
  COPYFILE_FICLONE: number;
71
62
  COPYFILE_FICLONE_FORCE: number;
72
- O_RDONLY: number;
73
- O_WRONLY: number;
74
- O_RDWR: number;
75
- O_CREAT: number;
76
- O_EXCL: number;
77
- O_TRUNC: number;
78
- O_APPEND: number;
79
- O_SYNC: number;
80
- O_NONBLOCK: number;
81
- O_DIRECTORY: number;
82
- O_NOFOLLOW: number;
83
63
  S_IFMT: number;
84
64
  S_IFREG: number;
85
65
  S_IFDIR: number;
@@ -100,7 +80,7 @@ declare const _default: {
100
80
  S_IROTH: number;
101
81
  S_IWOTH: number;
102
82
  S_IXOTH: number;
103
- };
83
+ } & PosixOpenFlags;
104
84
  existsSync: typeof existsSync;
105
85
  readdirSync: typeof readdirSync;
106
86
  readFileSync: typeof readFileSync;
@@ -139,8 +119,8 @@ declare const _default: {
139
119
  lutimes: typeof lutimes;
140
120
  lchownSync: typeof lchownSync;
141
121
  lchown: typeof lchown;
142
- lchmodSync: typeof lchmodSync;
143
- lchmod: typeof lchmod;
122
+ lchmodSync: (path: import("node:fs").PathLike, mode: number) => void;
123
+ lchmod: (path: import("node:fs").PathLike, mode: number, callback: (err: NodeJS.ErrnoException | null) => void) => void;
144
124
  fstatSync: typeof fstatSync;
145
125
  fstat: typeof fstat;
146
126
  ftruncateSync: typeof ftruncateSync;
@@ -0,0 +1,67 @@
1
+ import type { Mode } from 'node:fs';
2
+ import type { OpenFlags } from './types/index.js';
3
+ /** The `O_*` bits `open(2)` understands on one host. */
4
+ export interface PosixOpenFlags {
5
+ O_RDONLY: number;
6
+ O_WRONLY: number;
7
+ O_RDWR: number;
8
+ O_CREAT: number;
9
+ O_EXCL: number;
10
+ O_NOCTTY: number;
11
+ O_TRUNC: number;
12
+ O_APPEND: number;
13
+ O_NONBLOCK: number;
14
+ O_DSYNC: number;
15
+ O_SYNC: number;
16
+ O_DIRECTORY: number;
17
+ O_NOFOLLOW: number;
18
+ }
19
+ /**
20
+ * The host's `O_*` values.
21
+ *
22
+ * A function and not a module-level constant for the reason `host-os.ts`
23
+ * documents: `process.platform` is a lazy property installed by
24
+ * `@gjsify/process` at register time, so a value frozen during module
25
+ * evaluation can cache the byte-1 stub's answer for the life of the process.
26
+ */
27
+ export declare function openFlagValues(): PosixOpenFlags;
28
+ /** What the caller asked `open(2)` for, with nothing flattened away. */
29
+ export interface OpenSpec {
30
+ /** The exact bit set to hand to `open(2)`. */
31
+ posix: number;
32
+ readable: boolean;
33
+ writable: boolean;
34
+ /** `O_APPEND` — the kernel then ignores every write position. */
35
+ append: boolean;
36
+ /** `O_CREAT` */
37
+ creat: boolean;
38
+ /** `O_CREAT | O_EXCL` — exclusivity needs BOTH, see below. */
39
+ excl: boolean;
40
+ /** `O_TRUNC` */
41
+ trunc: boolean;
42
+ /** `O_NOFOLLOW` — the open refuses a symlink instead of following it. */
43
+ nofollow: boolean;
44
+ /** `O_DIRECTORY` — the open refuses anything that is not a directory. */
45
+ directory: boolean;
46
+ }
47
+ /** Translate Node open flags into the exact `open(2)` argument. */
48
+ export declare function parseOpenFlags(flags: OpenFlags | number | string | undefined | null): OpenSpec;
49
+ /**
50
+ * Node's `mode` argument as a number.
51
+ *
52
+ * Node documents `mode` as `string | integer` and parses a string as OCTAL —
53
+ * a plain numeric cast turns `'600'` into decimal 600 = `0o1130` (sticky plus
54
+ * `--x-wx---`), so the caller cannot read back the file it just created. One
55
+ * parser, shared by open / mkdir / mkdtemp / writeFile / appendFile / chmod,
56
+ * because the two that disagreed were the two that drifted.
57
+ *
58
+ * `fallback` is used only when `mode` is absent, and it is OPTIONAL because not
59
+ * every caller has one: Node's `open`/`mkdir`/`writeFile` document a default,
60
+ * and `chmod` does NOT — its `mode` is required, and Node rejects a missing one
61
+ * with `ERR_INVALID_ARG_TYPE`. Handing `chmod` a 0o666 default here made
62
+ * `fs.chmodSync(p, cfg.mode)` with an absent `cfg.mode` silently WIDEN a 0600
63
+ * secret to world-writable, from the shared parser this redesign introduced to
64
+ * stop the two spellings drifting. `0` is a VALID mode and must survive: the
65
+ * old `options.mode ||= 0o666` silently replaced it.
66
+ */
67
+ export declare function normalizeMode(mode: Mode | undefined | null, fallback?: number): number;
@@ -81,7 +81,11 @@ declare function mkdtemp(prefix: string, options: BufferEncodingOption): Promise
81
81
  * @param options The encoding (or an object specifying the encoding), used as the encoding of the result. If not provided, `'utf8'` is used.
82
82
  */
83
83
  declare function mkdtemp(prefix: string, options?: ObjectEncodingOptions | BufferEncoding | null): Promise<string | Buffer>;
84
- declare function writeFile(path: PathLike, data: string | Uint8Array | unknown): Promise<void>;
84
+ declare function writeFile(path: PathLike | FileHandle, data: string | Uint8Array | unknown, options?: {
85
+ encoding?: string | null;
86
+ mode?: Mode;
87
+ flag?: OpenFlags | string;
88
+ } | string | null): Promise<void>;
85
89
  /**
86
90
  * Removes the directory identified by `path`.
87
91
  *
@@ -125,7 +129,7 @@ declare function rm(path: PathLike, options?: RmOptions): Promise<void>;
125
129
  declare function rename(oldPath: PathLike, newPath: PathLike): Promise<void>;
126
130
  declare function copyFile(src: PathLike, dest: PathLike, mode?: number): Promise<void>;
127
131
  declare function access(path: PathLike, mode?: number): Promise<void>;
128
- declare function appendFile(path: PathLike, data: string | Uint8Array, options?: {
132
+ declare function appendFile(path: PathLike | FileHandle, data: string | Uint8Array, options?: {
129
133
  encoding?: string;
130
134
  mode?: number;
131
135
  flag?: string;
@@ -2,22 +2,83 @@ import { Buffer } from 'node:buffer';
2
2
  import { Readable } from 'node:stream';
3
3
  import type { CreateReadStreamOptions } from 'node:fs/promises';
4
4
  import type { PathLike, ReadStream as IReadStream } from 'node:fs';
5
+ /**
6
+ * What `fs.createReadStream()` accepts, as opposed to
7
+ * `filehandle.createReadStream()`.
8
+ *
9
+ * `CreateReadStreamOptions` is the fs/promises shape and has no `fd` — which is
10
+ * exactly why one was never read here, and why this stream had no way to reach
11
+ * a descriptor at all.
12
+ */
13
+ export type ReadStreamOptions = CreateReadStreamOptions & {
14
+ fd?: number | null;
15
+ };
5
16
  export declare class ReadStream extends Readable implements IReadStream {
6
17
  bytesRead: number;
7
18
  path: string | Buffer;
8
19
  pending: boolean;
9
20
  fd: number | null;
21
+ /**
22
+ * Read through the DESCRIPTOR rather than by opening the path again.
23
+ *
24
+ * `filehandle.createReadStream()` returned a stream that opened the path
25
+ * afresh at offset 0: a second read cursor that neither consulted nor
26
+ * advanced the handle's. After `await fh.read(buf,0,4,null)` the stream
27
+ * replayed the whole file from the beginning where Node resumes at byte 4,
28
+ * and it bound a different inode if the path had been replaced since the
29
+ * open. The write half was moved onto the descriptor earlier in this
30
+ * redesign, so until now the "one cursor" invariant held for writes only.
31
+ */
32
+ private _ownFd;
33
+ private _autoCloseFd;
34
+ /** Whether reads carry an explicit position (`start` given) or ride the shared cursor. */
35
+ private _positional;
10
36
  private _gioFile;
11
37
  private _inputStream;
12
38
  private _cancellable;
13
39
  private _start;
14
40
  private _end;
15
41
  private _pos;
42
+ /**
43
+ * Whether a `read_bytes_async` is in flight.
44
+ *
45
+ * A `GInputStream` allows exactly ONE outstanding async operation; a second
46
+ * one fails with `G_IO_ERROR_PENDING` ("Datenstrom hat noch einen
47
+ * ausstehenden Vorgang"). Nothing here used to prevent the second call, and
48
+ * the only thing that kept it rare was that the write side of a pipe took a
49
+ * main-loop turn to acknowledge each chunk. The moment writes stopped
50
+ * needing one, `pipe()` of a 128 KB file started failing about two runs in
51
+ * three — a race that was always there, timed out of reach.
52
+ *
53
+ * So the invariant is enforced rather than relied on: a `_read()` that
54
+ * arrives mid-flight is remembered and re-issued on completion instead of
55
+ * being dropped (which would stall the stream) or issued (which throws).
56
+ */
57
+ private _reading;
58
+ private _readAgain;
59
+ private _lastSize;
16
60
  close(callback?: (err?: NodeJS.ErrnoException | null) => void): void;
17
- constructor(path: PathLike, opts?: CreateReadStreamOptions);
61
+ constructor(path: PathLike, opts?: ReadStreamOptions);
18
62
  _construct(callback: (err?: Error | null) => void): void;
19
63
  _read(size: number): void;
20
64
  private _doRead;
65
+ /**
66
+ * One chunk through the descriptor, on the handle's own cursor.
67
+ *
68
+ * `position: null` when the caller gave no `start`, so the read advances
69
+ * the shared offset exactly as `fh.read()` would and the stream picks up
70
+ * where the last operation left off. With a `start` the reads are
71
+ * positional (`pread`), which by the same core leaves the cursor alone —
72
+ * Node's rule, verified against it.
73
+ *
74
+ * The `end` bound is applied HERE rather than in `_doRead`, because the
75
+ * descriptor path is scheduled and `_doRead` no longer knows the cursor's
76
+ * value at the moment the read actually happens.
77
+ */
78
+ private _readThroughFd;
79
+ /** Every OS resource this stream holds, released once and idempotently. */
80
+ private _release;
81
+ private _releaseFd;
21
82
  _destroy(error: Error | null, callback: (error?: Error | null) => void): void;
22
83
  }
23
- export declare function createReadStream(path: string | URL, options?: CreateReadStreamOptions): ReadStream;
84
+ export declare function createReadStream(path: string | URL, options?: ReadStreamOptions): ReadStream;
@@ -65,3 +65,25 @@ export declare class BigIntStats extends Dirent implements NodeBigIntStats {
65
65
  constructor(info: Gio.FileInfo, path: PathLike, filename?: string);
66
66
  constructor(path: PathLike, filename?: string);
67
67
  }
68
+ /**
69
+ * A `Stats` for `info`, or the error the kernel actually gave.
70
+ *
71
+ * `g_file_query_info()` does NOT fail when the kernel refuses to stat a name —
72
+ * a path under a directory this process cannot search comes back as a non-NULL
73
+ * `GFileInfo` carrying no attributes at all (see {@link answered}, where the
74
+ * measurement lives). Every stat entry point built a `Stats` straight from
75
+ * whatever it got, and that is two failures in one call: reading the type and
76
+ * the size off the empty shell logs four `GLib-GIO-CRITICAL` lines — a SIGABRT
77
+ * under `G_DEBUG=fatal-criticals` — and what it then returns is a FABRICATED
78
+ * `{mode: 0, size: 0, ino: 0, nlink: 0}` presented as fact, where Node raises
79
+ * EACCES. A caller checking `statSync(p).mode & 0o200` before writing is told
80
+ * the file is unwritable rather than that it may not look; one checking
81
+ * `.size` is told the file is empty.
82
+ *
83
+ * All five GJS-side call sites (`statSync`, `lstatSync`, the `stat`/`lstat`
84
+ * callbacks, `fsPromises.stat` and `filehandle.stat()`) go through here, so the
85
+ * gate cannot be present in four of them and missing in the fifth — which is
86
+ * how it got here: the open path grew {@link answered} in this same redesign
87
+ * and the stat path, which asks the identical question, did not.
88
+ */
89
+ export declare function statsFrom(info: Gio.FileInfo, pathStr: string, syscall: string, bigint?: boolean): Stats | BigIntStats;