@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.
- package/lib/esm/callback.js +1 -1
- package/lib/esm/dirent.js +1 -1
- package/lib/esm/errors.js +1 -1
- package/lib/esm/fd-io.js +1 -0
- package/lib/esm/fd-ops.js +1 -1
- package/lib/esm/file-handle.js +1 -1
- package/lib/esm/index.js +1 -1
- package/lib/esm/posix-flags.js +1 -0
- package/lib/esm/promises.js +1 -1
- package/lib/esm/read-stream.js +1 -1
- package/lib/esm/stats.js +1 -1
- package/lib/esm/sync.js +1 -1
- package/lib/esm/utils.js +1 -1
- package/lib/esm/utimes.js +1 -1
- package/lib/esm/write-stream.js +1 -1
- package/lib/types/capabilities.spec.d.ts +92 -0
- package/lib/types/dirent.d.ts +22 -2
- package/lib/types/errors.d.ts +11 -0
- package/lib/types/fd-io.d.ts +188 -0
- package/lib/types/file-handle.d.ts +90 -28
- package/lib/types/fs-semantics.spec.d.ts +2 -0
- package/lib/types/index.d.ts +6 -26
- package/lib/types/posix-flags.d.ts +67 -0
- package/lib/types/promises.d.ts +6 -2
- package/lib/types/read-stream.d.ts +63 -2
- package/lib/types/stats.d.ts +22 -0
- package/lib/types/sync.d.ts +26 -7
- package/lib/types/utils.d.ts +0 -1
- package/lib/types/utimes.d.ts +6 -2
- package/lib/types/write-stream.d.ts +45 -4
- package/package.json +17 -8
- package/lib/esm/file-handle-open.js +0 -1
- package/lib/types/file-handle-open.d.ts +0 -25
|
@@ -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
|
-
|
|
18
|
-
private
|
|
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
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* it
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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
|
|
29
|
-
private
|
|
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
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
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
|
|
49
|
-
/**
|
|
50
|
-
|
|
51
|
-
private
|
|
52
|
-
/**
|
|
53
|
-
|
|
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
|
-
/**
|
|
60
|
-
|
|
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.
|
package/lib/types/index.d.ts
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
143
|
-
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;
|
package/lib/types/promises.d.ts
CHANGED
|
@@ -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
|
|
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?:
|
|
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?:
|
|
84
|
+
export declare function createReadStream(path: string | URL, options?: ReadStreamOptions): ReadStream;
|
package/lib/types/stats.d.ts
CHANGED
|
@@ -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;
|