@gjsify/fs 0.35.0 → 0.37.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.
@@ -1 +1 @@
1
- import"./_virtual/_rolldown/runtime.js";import{encodeUint8Array as e,getEncodingFromOptions as t}from"./encoding.js";import{normalizePath as n}from"./utils.js";import{ReadStream as r}from"./read-stream.js";import{normalizeMode as i,parseOpenFlags as a}from"./posix-flags.js";import{WriteStream as o}from"./write-stream.js";import{closeFd as s,fdPath as c,fsError as l,fsyncFd as u,isSeekableFd as d,openFd as f,readFd as p,releaseFd as m,seekFd as h,sizeOfFd as g,tellFd as _,truncateFd as v,writeFd as y}from"./fd-io.js";import{STAT_ATTRIBUTES as b,statsFrom as x}from"./stats.js";import{chmodSync as S,chownSync as C}from"./sync.js";import w from"@girs/glib-2.0";import T from"@girs/gio-2.0";import{Buffer as E}from"node:buffer";import{createInterface as D}from"node:readline";var O=class FileHandle{options;_gFile;_pathStr;_spec;_pos=0;_closed=!1;static instances={};constructor(e){this.options=e,this.options.flags??=`r`;let t=n(e.path);this._spec=a(this.options.flags),this.options.mode??=438;let r=i(this.options.mode,438);return this.fd=f(t,this._spec,r),this._pathStr=t,this._gFile=T.File.new_for_path(t),FileHandle.instances[this.fd]=this,FileHandle.getInstance(this.fd)}_readCore(e,t,n,r){if(this._assertOpen(`read`),!this._spec.readable)throw l(`EBADF`,`read`,this._pathStr);if(n>e.byteLength-t){let r=RangeError(`The value of "length" is out of range. It must be <= ${e.byteLength-t}. Received ${n}`);throw r.code=`ERR_OUT_OF_RANGE`,r}let i=r!=null&&r>=0,a=d(this.fd,this._pathStr);if(i&&!a)throw l(`ESPIPE`,`read`,this._pathStr);let o=i?r:this._pos;a&&h(this.fd,o);let s=p(this.fd,n);return s.length>0&&new Uint8Array(e.buffer,e.byteOffset+t,s.length).set(s),!i&&a&&(this._pos=o+s.length),s.length}_writeCore(e,t){if(this._assertOpen(`write`),!this._spec.writable)throw l(`EBADF`,`write`,this._pathStr);if(this._spec.append){let t=y(this.fd,e);return this._pos=_(this.fd)??g(this.fd,this._pathStr),t}let n=t!=null&&t>=0,r=d(this.fd,this._pathStr);if(n&&!r)throw l(`ESPIPE`,`write`,this._pathStr);let i=n?t:this._pos;r&&h(this.fd,i);let a=y(this.fd,e);return!n&&r&&(this._pos=i+a),a}_assertOpen(e){if(this._closed)throw l(`EBADF`,e,this._pathStr)}_truncateCore(e){if(this._assertOpen(`ftruncate`),!this._spec.writable)throw l(`EINVAL`,`ftruncate`,this._pathStr);v(this.fd,Math.max(0,e),this._pathStr)}_readToEnd(){let e=[],t=0;for(;;){let n=new Uint8Array(64*1024),r=this._readCore(n,0,n.length,null);if(r===0)break;e.push(n.subarray(0,r)),t+=r}let n=new Uint8Array(t),r=0;for(let t of e)n.set(t,r),r+=t.length;return n}_fdTarget(){return c(this.fd)??this._pathStr}fd;static getInstance(e,t=`read`){let n=e instanceof FileHandle?e.fd:e,r=FileHandle.instances[n];if(!r)throw l(`EBADF`,t);return r}async appendFile(e,t){await this.writeFile(e,t)}async chown(e,t){this._assertOpen(`fchown`),C(n(this.options.path),e,t)}async chmod(e){this._assertOpen(`fchmod`),S(this._fdTarget(),e)}createReadStream(e){return new r(this.options.path,{...e,fd:this.fd})}createWriteStream(e){return new o(this.options.path,{...e,fd:this.fd,fdFromHandle:!0})}async datasync(){this._assertOpen(`fdatasync`),u(this.fd)}async sync(){this._assertOpen(`fsync`),u(this.fd)}async read(...e){let t,n,r,i;if(typeof e[0]==`object`&&!(e[0]instanceof Uint8Array)&&!(e[0]instanceof E)){let a=e[0];t=a.buffer,n=a.offset,r=a.length,i=a.position}else t=e[0],n=e[1],r=e[2],i=e[3];let a=t,o=n??0,s=r??(a?a.byteLength-o:65536);return{bytesRead:this._readCore(t,o,s,i??null),buffer:t}}readableWebStream(){let e=globalThis.ReadableStream;if(typeof e!=`function`)throw Error(`readableWebStream() requires a global ReadableStream. Import "node:stream/web" or "@gjsify/streams" before calling this method.`);return new e}async readFile(n){return e(t(n,`buffer`),this._readToEnd())}readLines(e){return D({input:this.createReadStream(e),crlfDelay:1/0})}async stat(e){this._assertOpen(`fstat`);let t=T.File.new_for_path(this._fdTarget());return x(await new Promise((e,n)=>{t.query_info_async(b,T.FileQueryInfoFlags.NONE,w.PRIORITY_DEFAULT,null,(r,i)=>{try{e(t.query_info_finish(i))}catch(e){n(e)}})}),this._pathStr,`fstat`,e?.bigint)}async truncate(e=0){this._truncateCore(e)}async utimes(e,t){this._assertOpen(`futimes`);let{utimesSync:r}=await import(`./utimes.js`);r(n(this.options.path),e,t)}async writeFile(e,n){let r=t(n),i;i=typeof e==`string`?E.from(e,r||`utf8`):e,this._writeCore(i,null)}async write(e,...n){let r=null,i=null,a=null,o=null;typeof e==`string`?(r=n[0],i=n[1]):(a=n[0],o=n[1],r=n[2]),i=t(i,typeof e==`string`?`utf8`:null);let s;s=typeof e==`string`?new TextEncoder().encode(e):e;let c=a??0,l=o??s.byteLength-c,u=s.subarray(c,c+l);return{bytesWritten:this._writeCore(u,r??null),buffer:e}}async writev(e,t){let n=0;for(let r of e){let e=E.from(r.buffer,r.byteOffset,r.byteLength),i=await this.write(e,0,e.byteLength,t==null?null:t+n);n+=i.bytesWritten}return{bytesWritten:n,buffers:e}}async readv(e,t){let n=0;for(let r of e){let e=await this.read({buffer:E.from(r.buffer,r.byteOffset,r.byteLength),position:t==null?null:t+n});if(n+=e.bytesRead,e.bytesRead<r.byteLength)break}return{bytesRead:n,buffers:e}}_flushSync(){u(this.fd)}_closeSync(){this._teardown()}_readSync(e,t,n,r){return this._readCore(e,t,n,r)}_writeSync(e,t){return this._writeCore(e,t)}_readToEndSync(){return this._readToEnd()}_truncateSync(e){this._truncateCore(e)}_fdStatTarget(){return this._fdTarget()}_teardown(){if(this._closed)return;this._closed=!0,m(this.fd),s(this.fd);let e=FileHandle;delete e.instances[this.fd]}async close(){this._teardown()}async[Symbol.asyncDispose](){await this.close()}pull(...e){throw Error('FileHandle.pull() is not implemented in @gjsify/fs yet — requires `node:stream/iter` (Node 25.9+) which @gjsify/stream has not ported. Track gjsify Open TODO "Node 25 stream/iter integration".')}writer(e){throw Error('FileHandle.writer() is not implemented in @gjsify/fs yet — requires `node:stream/iter` (Node 25.9+) which @gjsify/stream has not ported. Track gjsify Open TODO "Node 25 stream/iter integration".')}};export{O as FileHandle};
1
+ import"./_virtual/_rolldown/runtime.js";import{encodeUint8Array as e,getEncodingFromOptions as t}from"./encoding.js";import{normalizePath as n}from"./utils.js";import{ReadStream as r}from"./read-stream.js";import{normalizeMode as i,parseOpenFlags as a}from"./posix-flags.js";import{WriteStream as o}from"./write-stream.js";import{closeFd as s,fdPath as c,fsError as l,fsyncFd as u,isSeekableFd as d,openFd as f,readFd as p,releaseFd as m,seekFd as h,sizeOfFd as g,tellFd as _,truncateFd as v,writeFd as y}from"./fd-io.js";import{STAT_ATTRIBUTES as b,statsFrom as x}from"./stats.js";import{chmodSync as S,chownSync as C}from"./sync.js";import w from"@girs/glib-2.0";import T from"@girs/gio-2.0";import{Buffer as E}from"node:buffer";import{createInterface as D}from"node:readline";var O=class FileHandle{options;_gFile;_pathStr;_spec;_pos=0;_closed=!1;static instances={};constructor(e){this.options=e,this.options.flags??=`r`;let t=n(e.path);this._spec=a(this.options.flags),this.options.mode??=438;let r=i(this.options.mode,438);return this.fd=f(t,this._spec,r),this._pathStr=t,this._gFile=T.File.new_for_path(t),FileHandle.instances[this.fd]=this,FileHandle.getInstance(this.fd)}_readCore(e,t,n,r){if(this._assertOpen(`read`),!this._spec.readable)throw l(`EBADF`,`read`,this._pathStr);if(n>e.byteLength-t){let r=RangeError(`The value of "length" is out of range. It must be <= ${e.byteLength-t}. Received ${n}`);throw r.code=`ERR_OUT_OF_RANGE`,r}let i=r!=null&&r>=0,a=d(this.fd,this._pathStr);if(i&&!a)throw l(`ESPIPE`,`read`,this._pathStr);let o=i?r:this._pos;a&&h(this.fd,o);let s=p(this.fd,n);return s.length>0&&new Uint8Array(e.buffer,e.byteOffset+t,s.length).set(s),!i&&a&&(this._pos=o+s.length),s.length}_writeCore(e,t){if(this._assertOpen(`write`),!this._spec.writable)throw l(`EBADF`,`write`,this._pathStr);if(this._spec.append){let t=y(this.fd,e);return this._pos=_(this.fd)??g(this.fd,this._pathStr),t}let n=t!=null&&t>=0,r=d(this.fd,this._pathStr);if(n&&!r)throw l(`ESPIPE`,`write`,this._pathStr);let i=n?t:this._pos;r&&h(this.fd,i);let a=y(this.fd,e);return!n&&r&&(this._pos=i+a),a}_assertOpen(e){if(this._closed)throw l(`EBADF`,e,this._pathStr)}_truncateCore(e){if(this._assertOpen(`ftruncate`),!this._spec.writable)throw l(`EINVAL`,`ftruncate`,this._pathStr);v(this.fd,Math.max(0,e),this._pathStr)}_readToEnd(){let e=[],t=0;for(;;){let n=new Uint8Array(64*1024),r=this._readCore(n,0,n.length,null);if(r===0)break;e.push(n.subarray(0,r)),t+=r}let n=new Uint8Array(t),r=0;for(let t of e)n.set(t,r),r+=t.length;return n}_fdTarget(){return c(this.fd)??this._pathStr}fd;static getInstance(e,t=`read`){let n=e instanceof FileHandle?e.fd:e,r=FileHandle.instances[n];if(!r)throw l(`EBADF`,t);return r}async appendFile(e,t){await this.writeFile(e,t)}async chown(e,t){this._assertOpen(`fchown`),C(n(this.options.path),e,t)}async chmod(e){this._assertOpen(`fchmod`),S(this._fdTarget(),e)}createReadStream(e){return new r(this.options.path,{...e,fd:this.fd})}createWriteStream(e){return new o(this.options.path,{...e,fd:this.fd,fdFromHandle:!0})}async datasync(){this._assertOpen(`fdatasync`),u(this.fd)}async sync(){this._assertOpen(`fsync`),u(this.fd)}async read(...e){let t,n,r,i;if(typeof e[0]==`object`&&!(e[0]instanceof Uint8Array)&&!(e[0]instanceof E)){let a=e[0];t=a.buffer,n=a.offset,r=a.length,i=a.position}else t=e[0],n=e[1],r=e[2],i=e[3];let a=t,o=n??0,s=r??(a?a.byteLength-o:65536);return{bytesRead:this._readCore(t,o,s,i??null),buffer:t}}readableWebStream(){let e=globalThis.ReadableStream;if(typeof e!=`function`)throw Error(`readableWebStream() requires a global ReadableStream. Import "node:stream/web" or "@gjsify/web-streams/register" before calling this method.`);return new e}async readFile(n){return e(t(n,`buffer`),this._readToEnd())}readLines(e){return D({input:this.createReadStream(e),crlfDelay:1/0})}async stat(e){this._assertOpen(`fstat`);let t=T.File.new_for_path(this._fdTarget());return x(await new Promise((e,n)=>{t.query_info_async(b,T.FileQueryInfoFlags.NONE,w.PRIORITY_DEFAULT,null,(r,i)=>{try{e(t.query_info_finish(i))}catch(e){n(e)}})}),this._pathStr,`fstat`,e?.bigint)}async truncate(e=0){this._truncateCore(e)}async utimes(e,t){this._assertOpen(`futimes`);let{utimesSync:r}=await import(`./utimes.js`);r(n(this.options.path),e,t)}async writeFile(e,n){let r=t(n),i;i=typeof e==`string`?E.from(e,r||`utf8`):e,this._writeCore(i,null)}async write(e,...n){let r=null,i=null,a=null,o=null;typeof e==`string`?(r=n[0],i=n[1]):(a=n[0],o=n[1],r=n[2]),i=t(i,typeof e==`string`?`utf8`:null);let s;s=typeof e==`string`?new TextEncoder().encode(e):e;let c=a??0,l=o??s.byteLength-c,u=s.subarray(c,c+l);return{bytesWritten:this._writeCore(u,r??null),buffer:e}}async writev(e,t){let n=0;for(let r of e){let e=E.from(r.buffer,r.byteOffset,r.byteLength),i=await this.write(e,0,e.byteLength,t==null?null:t+n);n+=i.bytesWritten}return{bytesWritten:n,buffers:e}}async readv(e,t){let n=0;for(let r of e){let e=await this.read({buffer:E.from(r.buffer,r.byteOffset,r.byteLength),position:t==null?null:t+n});if(n+=e.bytesRead,e.bytesRead<r.byteLength)break}return{bytesRead:n,buffers:e}}_flushSync(){u(this.fd)}_closeSync(){this._teardown()}_readSync(e,t,n,r){return this._readCore(e,t,n,r)}_writeSync(e,t){return this._writeCore(e,t)}_readToEndSync(){return this._readToEnd()}_truncateSync(e){this._truncateCore(e)}_fdStatTarget(){return this._fdTarget()}_teardown(){if(this._closed)return;this._closed=!0,m(this.fd),s(this.fd);let e=FileHandle;delete e.instances[this.fd]}async close(){this._teardown()}async[Symbol.asyncDispose](){await this.close()}pull(...e){throw Error('FileHandle.pull() is not implemented in @gjsify/fs yet — requires `node:stream/iter` (Node 25.9+) which @gjsify/stream has not ported. Track gjsify Open TODO "Node 25 stream/iter integration".')}writer(e){throw Error('FileHandle.writer() is not implemented in @gjsify/fs yet — requires `node:stream/iter` (Node 25.9+) which @gjsify/stream has not ported. Track gjsify Open TODO "Node 25 stream/iter integration".')}};export{O as FileHandle};
@@ -5,72 +5,52 @@ export declare function fsError(code: string, syscall: string, path?: string, de
5
5
  /**
6
6
  * Can this host name an open descriptor as a path?
7
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.
8
+ * Lazy, not at module eval: a probe there runs in every consumer that merely
9
+ * imports `node:fs`. Do not assume procfs exists — in `respond-with-file.ts` the
10
+ * path resolved to nothing on macOS, `statSync` threw, and a swallowed catch meant
11
+ * the check silently never ran.
12
12
  */
13
13
  export declare function hasProcFd(): boolean;
14
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.
15
+ * A path naming the DESCRIPTOR rather than the name it was opened from, or `null`
16
+ * where the host cannot express one. `/proc/self/fd/<fd>` resolves to the inode
17
+ * the fd holds even after the directory entry is gone, which is what lets
18
+ * `fstat`/`fchmod`/`ftruncate` survive a rename or unlink.
21
19
  */
22
20
  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
- */
21
+ /** `open(2)`. */
31
22
  export declare function openFd(path: string, spec: OpenSpec, mode: number): number;
32
23
  export declare function closeFd(fd: number): void;
33
24
  /**
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.
25
+ * Name the reason `open(2)` refused, reconstructed from what is observable AFTER
26
+ * the kernel refused. A classification, not a check-then-act: it decides nothing
27
+ * about creating anything, so it carries no TOCTOU. The lookup must be
28
+ * `NOFOLLOW_SYMLINKS` so a dangling symlink reads as "the name is taken".
42
29
  */
43
30
  export declare function classifyOpenFailure(path: string, spec: OpenSpec): NodeJS.ErrnoException;
44
31
  /**
45
32
  * Does this `GFileInfo` actually ANSWER, or is it an empty shell?
46
33
  *
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.
34
+ * `g_file_query_info()` does NOT fail when the kernel refuses to stat a name. A
35
+ * path under a directory the process cannot search returns a non-NULL `GFileInfo`
36
+ * with NO attributes at all — measured: `list_attributes(null)` is `[]` and
37
+ * `has_attribute('standard::type')` is `false`, where an ordinary lookup gives
38
+ * `['standard::type']`. Every attribute read in this module is therefore gated on
39
+ * this, and `false` means "no answer", NEVER "no".
40
+ *
41
+ * Both ways of skipping the gate are live hazards:
42
+ * - reading the type off the shell logs `GFileInfo created without
43
+ * standard::type` and `g_file_info_get_file_type: should not be reached`, two
44
+ * `GLib-GIO-CRITICAL`s that `G_DEBUG=fatal-criticals` (standard in GNOME CI)
45
+ * turns into a SIGABRT before the caller's `catch` can run;
46
+ * - taking the shell as proof the name EXISTS answers EEXIST for a plain
47
+ * permission denial, so an `openSync(lock,'wx')` retry loop spins forever and
48
+ * the ubiquitous `catch (e) { if (e.code !== 'EEXIST') throw }` around
49
+ * `mkdirSync` swallows a hard EACCES.
50
+ *
51
+ * Exported because the stat family needs the identical gate; without it `statSync`
52
+ * hits both hazards at once and returns a fabricated `{mode: 0, size: 0, ino: 0}`
53
+ * as fact where Node raises EACCES. `statsFrom()` in `stats.ts` owns that decision.
74
54
  */
75
55
  export declare function answered(info: Gio.FileInfo | null): boolean;
76
56
  /** The same reconstruction for `mkdir(2)`, which also returns a bare `-1`. */
@@ -78,53 +58,37 @@ export declare function classifyMkdirFailure(path: string): NodeJS.ErrnoExceptio
78
58
  /**
79
59
  * Can this descriptor be positioned at all?
80
60
  *
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.
61
+ * Seeking unconditionally before every read and write emits one
62
+ * `g_io_channel_seek_position: assertion 'channel->is_seekable' failed` per I/O
63
+ * call on a FIFO / pipe / socket / tty — a `GLib-CRITICAL`, which
64
+ * `G_DEBUG=fatal-criticals` (standard in GNOME CI) turns into an abort. The bytes
65
+ * survive, because a pipe ignores the offset, so `_pos` silently accumulates a
66
+ * number that means nothing.
67
+ *
68
+ * The answer must therefore be ASKED FOR, not discovered by tripping the
69
+ * assertion, and it takes TWO tests in this order:
70
+ *
71
+ * 1. The file TYPE, refusing ONLY `S_IFIFO` and `S_IFSOCK` — the two whose channel
72
+ * is not `is_seekable`, so asking them at all is the CRITICAL above. The
73
+ * tempting "seekable means `S_IFREG` or `S_IFBLK`" is WRONG: a character device
74
+ * usually seeks fine (`noop_llseek`) and Node does positional I/O on
75
+ * `/dev/null`, `/dev/zero` and `/dev/urandom` (measured, v24.15.0), so
76
+ * refusing those raises an ESPIPE Node never raises.
77
+ * 2. The DESCRIPTOR itself, via the same `lseek(fd, 0, SEEK_CUR)` `GIOChannel` uses
78
+ * internally — a pure query that moves nothing. Needed because a TTY is
79
+ * `S_IFCHR` exactly like `/dev/zero` but has `no_llseek`. Measured under gjs
80
+ * 1.88.1: `G_IO_STATUS_NORMAL` on a regular file, `/dev/zero` and `/dev/null`;
81
+ * a clean `GLib.IOChannelError` ("Illegal seek") on a pty slave, with no
82
+ * CRITICAL, because that channel IS `is_seekable` and the kernel refuses.
83
+ *
84
+ * Skipping test 2 makes writing to a terminal by path impossible: the seek throws
85
+ * a RAW `Gio.IOErrorEnum` — numeric `code`, `instanceof Error === false` — out of
86
+ * `writeFileSync`/`writeSync`/`createWriteStream`, invisible to
87
+ * `catch (e) { if (e.code === 'ESPIPE') }`.
88
+ *
89
+ * The process's own stdin/stdout/stderr never arrive here; `isStdFd()` takes them
90
+ * first. Cached per fd — the type behind an open file description cannot change —
91
+ * and dropped by {@link releaseFd} so a recycled fd number inherits nothing.
128
92
  */
129
93
  export declare function isSeekableFd(fd: number, fallbackPath: string): boolean;
130
94
  /** Absolute `lseek(2)`. Only valid on a descriptor {@link isSeekableFd} accepts. */
@@ -132,11 +96,9 @@ export declare function seekFd(fd: number, position: number): void;
132
96
  /**
133
97
  * The descriptor's true offset, or `null` where the host cannot report it.
134
98
  *
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.
99
+ * Only needed where the KERNEL moved the offset behind us: an `O_APPEND` write jumps
100
+ * to EOF silently, and computing the new position from the pre-write value drifts.
101
+ * `seek_position()` cannot answer this — it returns a `GIOStatus`, not an offset.
140
102
  */
141
103
  export declare function tellFd(fd: number): number | null;
142
104
  /** `read(2)` at the descriptor's current offset. The kernel advances it. */
@@ -145,11 +107,9 @@ export declare function readFd(fd: number, length: number): Uint8Array;
145
107
  export declare function writeFd(fd: number, data: Uint8Array): number;
146
108
  export declare function fsyncFd(fd: number): void;
147
109
  /**
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.
110
+ * Release the cached per-fd views. They must go before the descriptor does: a
111
+ * GioUnix stream outliving its fd answers later calls against whatever the OS
112
+ * handed that number to next.
153
113
  */
154
114
  export declare function releaseFd(fd: number): void;
155
115
  /** The size of the file behind `fd`, via the descriptor where the host allows. */
@@ -157,32 +117,25 @@ export declare function sizeOfFd(fd: number, fallbackPath: string): number;
157
117
  /**
158
118
  * `ftruncate(2)`, as closely as GJS can express it.
159
119
  *
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.
120
+ * The access-mode check `ftruncate(2)` performs — descriptor open for writing, mode
121
+ * never consulted — is bookkeeping we already hold, so it lives in `FileHandle`
122
+ * beside the identical gates in `_readCore`/`_writeCore`, NOT here. Without it a
123
+ * handle opened `'r'` destroys the file, because the re-open below asks the kernel
124
+ * for fresh permission and gets it.
125
+ *
126
+ * A re-open is unavoidable: there is no `g_ftruncate` and no route from an fd to a
127
+ * `GSeekable` — `GioUnix.OutputStream` does not implement it, and
128
+ * `GBufferedOutputStream` / `GDataOutputStream` around one both report
129
+ * `can_truncate() == false` (measured, gjs 1.88.1). It goes by the descriptor's
130
+ * procfs name so it lands on the right inode after a rename or unlink, and being a
131
+ * second open it IS checked against the file's mode.
132
+ *
133
+ * Hence the least privilege that can truncate: `append_to()` is `O_WRONLY`,
134
+ * `open_readwrite()` is `O_RDWR`. Not an optimisation — on a write-only file, mode
135
+ * 0o200 truncates through the first and is refused by the second (measured). Still
136
+ * unreachable: a mode denying its own owner write (0o444, 0o400, 0o000), which Node
137
+ * truncates through the descriptor. That failure must at least be REPORTED as an
138
+ * ErrnoException — raw it escapes as a `Gio.IOErrorEnum` with a numeric `code` and
139
+ * `instanceof Error === false`, invisible to `catch (e) { if (e.code === 'EACCES') }`.
187
140
  */
188
141
  export declare function truncateFd(fd: number, length: number, fallbackPath: string): void;
@@ -19,20 +19,14 @@ export declare class FileHandle implements IFileHandle {
19
19
  /** What the caller asked open(2) for — the ORIGINAL flags, nothing flattened. */
20
20
  private readonly _spec;
21
21
  /**
22
- * The one byte position this handle has.
22
+ * The one byte position this handle has — a descriptor has exactly ONE offset,
23
+ * shared by reads and writes. `_readCore`/`_writeCore` seek to it absolutely
24
+ * before every operation, so the kernel's offset is a consequence of this
25
+ * value and cannot drift from it.
23
26
  *
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
+ * Exception: an `O_APPEND` write, where the kernel repositions to EOF without
28
+ * telling us. There the new value must be READ BACK; computing it from the
29
+ * pre-write value drifts after the very first append.
36
30
  */
37
31
  private _pos;
38
32
  private _closed;
@@ -46,14 +40,11 @@ export declare class FileHandle implements IFileHandle {
46
40
  /**
47
41
  * Read at `position`, or at the cursor when it is `null`/negative.
48
42
  *
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.
43
+ * SYNCHRONOUS ON PURPOSE: the cursor's read-modify-write — read `_pos`, seek,
44
+ * transfer, store `_pos` — contains no `await`, so on a single-threaded runtime
45
+ * it is indivisible by construction. An async lock cannot substitute for that;
46
+ * one held around the I/O while the offset was computed outside it let two
47
+ * concurrent writers capture the same offset and lose a write.
57
48
  */
58
49
  private _readCore;
59
50
  /** Write at `position`, or at the cursor when it is `null`/negative. See {@link _readCore}. */
@@ -62,250 +53,93 @@ export declare class FileHandle implements IFileHandle {
62
53
  /**
63
54
  * `ftruncate(2)` — ONE body for the sync and async halves.
64
55
  *
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.
56
+ * The access-mode gate belongs HERE, not in `truncateFd`: it is a property of
57
+ * the descriptor, and `truncateFd` re-opens the file (see there), so the kernel
58
+ * grants it fresh permission of its own. Without this line
59
+ * `ftruncateSync(openSync(f,'r'), 2)` destroys bytes through a READ-ONLY
60
+ * descriptor and returns normally. Node reports EINVAL, and so does this.
76
61
  */
77
62
  private _truncateCore;
78
63
  /** Read from the cursor to EOF, advancing it — Node's whole-file read on a handle. */
79
64
  private _readToEnd;
80
65
  /** The path that names this DESCRIPTOR, falling back to the name it was opened from. */
81
66
  private _fdTarget;
82
- /**
83
- * The numeric file descriptor managed by the {FileHandle} object.
84
- * @since v10.0.0
85
- */
67
+ /** The numeric file descriptor managed by this object. */
86
68
  readonly fd: number;
87
69
  /**
88
70
  * The handle behind a descriptor number. Not part of Node's API.
89
71
  *
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.
72
+ * Must fail as `EBADF`, not a bare Error: the common way to get here is an fd
73
+ * that WAS valid, since `_teardown()` deletes the instance, so every call on a
74
+ * closed descriptor lands here instead of on `_assertOpen`. Without the code,
75
+ * `catch (e) { if (e.code === 'EBADF') }` — the idiom for tolerating a racing
76
+ * close — falls through. `syscall` comes from the caller because Node names it
77
+ * in the message and only the caller knows it.
101
78
  *
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.
79
+ * A `FileHandle` is accepted as well as a number because `openSync()` returns
80
+ * the handle where Node returns a number (a divergence this does not change);
81
+ * without unwrapping, `fs.write(fs.openSync(p,'w'), …)` stringifies the object
82
+ * into a key and misses.
108
83
  */
109
84
  static getInstance(fd: number | FileHandle, syscall?: string): FileHandle;
110
85
  /**
111
- * Alias of `filehandle.writeFile()`.
112
- *
113
- * When operating on file handles, the mode cannot be changed from what it was set
114
- * to with `fsPromises.open()`. Therefore, this is equivalent to `filehandle.writeFile()`.
115
- * @since v10.0.0
116
- * @return Fulfills with `undefined` upon success.
86
+ * Alias of `writeFile()`: a handle's mode cannot be changed after
87
+ * `fsPromises.open()`, so it appends only because the descriptor carries
88
+ * O_APPEND. It must therefore be a plain CURSOR write — writing at some
89
+ * separately-tracked offset overwrites from byte 0 on a fresh handle.
117
90
  */
118
91
  appendFile(data: string | Uint8Array, options?: (ObjectEncodingOptions & FlagAndOpenMode) | BufferEncoding | null): Promise<void>;
119
- /**
120
- * Changes the ownership of the file. A wrapper for [`chown(2)`](http://man7.org/linux/man-pages/man2/chown.2.html).
121
- * @since v10.0.0
122
- * @param uid The file's new owner's user id.
123
- * @param gid The file's new group's group id.
124
- * @return Fulfills with `undefined` upon success.
125
- */
92
+ /** [`chown(2)`](http://man7.org/linux/man-pages/man2/chown.2.html). */
126
93
  chown(uid: number, gid: number): Promise<void>;
127
- /**
128
- * Modifies the permissions on the file. See [`chmod(2)`](http://man7.org/linux/man-pages/man2/chmod.2.html).
129
- * @since v10.0.0
130
- * @param mode the file mode bit mask.
131
- * @return Fulfills with `undefined` upon success.
132
- */
94
+ /** [`chmod(2)`](http://man7.org/linux/man-pages/man2/chmod.2.html). */
133
95
  chmod(mode: Mode): Promise<void>;
134
96
  /**
135
- * Unlike the 16 kb default `highWaterMark` for a `stream.Readable`, the stream
136
- * returned by this method has a default `highWaterMark` of 64 kb.
137
- *
138
- * `options` can include `start` and `end` values to read a range of bytes from
139
- * the file instead of the entire file. Both `start` and `end` are inclusive and
140
- * start counting at 0, allowed values are in the
141
- * \[0, [`Number.MAX_SAFE_INTEGER`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER)\] range. If `start` is
142
- * omitted or `undefined`, `filehandle.createReadStream()` reads sequentially from
143
- * the current file position. The `encoding` can be any one of those accepted by `Buffer`.
144
- *
145
- * If the `FileHandle` points to a character device that only supports blocking
146
- * reads (such as keyboard or sound card), read operations do not finish until data
147
- * is available. This can prevent the process from exiting and the stream from
148
- * closing naturally.
149
- *
150
- * By default, the stream will emit a `'close'` event after it has been
151
- * destroyed. Set the `emitClose` option to `false` to change this behavior.
152
- *
153
- * ```js
154
- * import { open } from 'node:fs/promises';
155
- *
156
- * const fd = await open('/dev/input/event0');
157
- * // Create a stream from some character device.
158
- * const stream = fd.createReadStream();
159
- * setTimeout(() => {
160
- * stream.close(); // This may not close the stream.
161
- * // Artificially marking end-of-stream, as if the underlying resource had
162
- * // indicated end-of-file by itself, allows the stream to close.
163
- * // This does not cancel pending read operations, and if there is such an
164
- * // operation, the process may still not be able to exit successfully
165
- * // until it finishes.
166
- * stream.push(null);
167
- * stream.read(0);
168
- * }, 100);
169
- * ```
170
- *
171
- * If `autoClose` is false, then the file descriptor won't be closed, even if
172
- * there's an error. It is the application's responsibility to close it and make
173
- * sure there's no file descriptor leak. If `autoClose` is set to true (default
174
- * behavior), on `'error'` or `'end'` the file descriptor will be closed
175
- * automatically.
176
- *
177
- * An example to read the last 10 bytes of a file which is 100 bytes long:
178
- *
179
- * ```js
180
- * import { open } from 'node:fs/promises';
181
- *
182
- * const fd = await open('sample.txt');
183
- * fd.createReadStream({ start: 90, end: 99 });
184
- * ```
185
- * @since v16.11.0
97
+ * `highWaterMark` defaults to 64 kb here rather than a Readable's 16 kb. With
98
+ * no `start`, reading is sequential from the current file position.
186
99
  */
187
100
  createReadStream(options?: CreateReadStreamOptions): ReadStream;
188
101
  /**
189
- * `options` may also include a `start` option to allow writing data at some
190
- * position past the beginning of the file, allowed values are in the
191
- * \[0, [`Number.MAX_SAFE_INTEGER`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER)\] range. Modifying a file rather than
192
- * replacing it may require the `flags` `open` option to be set to `r+` rather than
193
- * the default `r`. The `encoding` can be any one of those accepted by `Buffer`.
194
- *
195
- * If `autoClose` is set to true (default behavior) on `'error'` or `'finish'`the file descriptor will be closed automatically. If `autoClose` is false,
196
- * then the file descriptor won't be closed, even if there's an error.
197
- * It is the application's responsibility to close it and make sure there's no
198
- * file descriptor leak.
199
- *
200
- * By default, the stream will emit a `'close'` event after it has been
201
- * destroyed. Set the `emitClose` option to `false` to change this behavior.
202
- * @since v16.11.0
102
+ * `options.start` writes past the beginning of the file; modifying rather than
103
+ * replacing may need `flags: 'r+'` instead of the default `'r'`.
203
104
  */
204
105
  createWriteStream(options?: CreateWriteStreamOptions): WriteStream;
205
- /**
206
- * Forces all currently queued I/O operations associated with the file to the
207
- * operating system's synchronized I/O completion state. Refer to the POSIX [`fdatasync(2)`](http://man7.org/linux/man-pages/man2/fdatasync.2.html) documentation for details.
208
- *
209
- * Unlike `filehandle.sync` this method does not flush modified metadata.
210
- * @since v10.0.0
211
- * @return Fulfills with `undefined` upon success.
212
- */
106
+ /** [`fdatasync(2)`](http://man7.org/linux/man-pages/man2/fdatasync.2.html) —
107
+ * unlike `sync()` this does not flush modified metadata. */
213
108
  datasync(): Promise<void>;
214
- /**
215
- * Request that all data for the open file descriptor is flushed to the storage
216
- * device. The specific implementation is operating system and device specific.
217
- * Refer to the POSIX [`fsync(2)`](http://man7.org/linux/man-pages/man2/fsync.2.html) documentation for more detail.
218
- * @since v10.0.0
219
- * @return Fufills with `undefined` upon success.
220
- */
109
+ /** [`fsync(2)`](http://man7.org/linux/man-pages/man2/fsync.2.html). */
221
110
  sync(): Promise<void>;
222
111
  /**
223
- * Reads data from the file and stores that in the given buffer.
112
+ * Read into `buffer`. Absent concurrent modification, EOF is `bytesRead === 0`.
224
113
  *
225
- * If the file is not modified concurrently, the end-of-file is reached when the
226
- * number of bytes read is zero.
227
- * @since v10.0.0
228
- * @param buffer A buffer that will be filled with the file data read.
229
- * @param offset The location in the buffer at which to start filling.
230
- * @param length The number of bytes to read.
231
- * @param position The location where to begin reading data from the file. If `null`, data will be read from the current file position, and the position will be updated. If `position` is an
232
- * integer, the current file position will remain unchanged.
233
- * @return Fulfills upon success with an object with two properties:
114
+ * A `null` `position` reads from the current file position AND advances it; an
115
+ * integer leaves the position unchanged.
234
116
  */
235
117
  read<T extends NodeJS.ArrayBufferView>(buffer: T, offset?: number | null, length?: number | null, position?: ReadPosition | null): Promise<FileReadResult<T>>;
236
118
  read<T extends NodeJS.ArrayBufferView = Buffer>(options?: FileReadOptions<T>): Promise<FileReadResult<T>>;
237
119
  /**
238
- * Returns a `ReadableStream` that may be used to read the files data.
239
- *
240
- * An error will be thrown if this method is called more than once or is called after the `FileHandle` is closed
241
- * or closing.
242
- *
243
- * ```js
244
- * import { open } from 'node:fs/promises';
245
- *
246
- * const file = await open('./some/file/to/read');
247
- *
248
- * for await (const chunk of file.readableWebStream())
249
- * console.log(chunk);
250
- *
251
- * await file.close();
252
- * ```
120
+ * INCOMPLETE. Node returns a `ReadableStream` over the file's bytes and throws
121
+ * on a second call or a closed handle; this returns a SOURCELESS stream that
122
+ * yields nothing, and enforces neither rule. Reading the file needs a source
123
+ * wired to `_readCore` — until then prefer `createReadStream()`.
253
124
  *
254
- * While the `ReadableStream` will read the file to completion, it will not close the `FileHandle` automatically. User code must still call the `fileHandle.close()` method.
255
- *
256
- * @since v17.0.0
257
- * @experimental
125
+ * Node also never closes the handle for you: user code still calls `close()`.
258
126
  */
259
127
  readableWebStream(): ReadableStream;
260
128
  /**
261
- * Asynchronously reads the entire contents of a file.
262
- *
263
- * If `options` is a string, then it specifies the `encoding`.
264
- *
265
- * The `FileHandle` has to support reading.
266
- *
267
- * If one or more `filehandle.read()` calls are made on a file handle and then a`filehandle.readFile()` call is made, the data will be read from the current
268
- * position till the end of the file. It doesn't always read from the beginning
269
- * of the file.
270
- * @since v10.0.0
271
- * @return Fulfills upon a successful read with the contents of the file. If no encoding is specified (using `options.encoding`), the data is returned as a {Buffer} object. Otherwise, the
272
- * data will be a string.
129
+ * Read from the CURRENT position to EOF — not from the beginning, so earlier
130
+ * `read()` calls on this handle move the starting point. The handle must have
131
+ * been opened for reading, and is not closed for you.
273
132
  */
274
133
  readFile(options?: {
275
134
  encoding?: null | undefined;
276
135
  flag?: OpenMode | undefined;
277
136
  } | null): Promise<Buffer<ArrayBuffer>>;
278
- /**
279
- * Asynchronously reads the entire contents of a file. The underlying file will _not_ be closed automatically.
280
- * The `FileHandle` must have been opened for reading.
281
- * @param options An object that may contain an optional flag.
282
- * If a flag is not provided, it defaults to `'r'`.
283
- */
284
137
  readFile(options: {
285
138
  encoding: BufferEncoding;
286
139
  flag?: OpenMode | undefined;
287
140
  } | BufferEncoding): Promise<string>;
288
- /**
289
- * Convenience method to create a `readline` interface and stream over the file. For example:
290
- *
291
- * ```js
292
- * import { open } from 'node:fs/promises';
293
- *
294
- * const file = await open('./some/file/to/read');
295
- *
296
- * for await (const line of file.readLines()) {
297
- * console.log(line);
298
- * }
299
- * ```
300
- *
301
- * @since v18.11.0
302
- * @param options See `filehandle.createReadStream()` for the options.
303
- */
141
+ /** A `readline` interface over the file; `options` are `createReadStream()`'s. */
304
142
  readLines(options?: CreateReadStreamOptions): ReadlineInterface;
305
- /**
306
- * @since v10.0.0
307
- * @return Fulfills with an {fs.Stats} for the file.
308
- */
309
143
  stat(opts?: StatOptions & {
310
144
  bigint?: false | undefined;
311
145
  }): Promise<Stats>;
@@ -313,75 +147,25 @@ export declare class FileHandle implements IFileHandle {
313
147
  bigint: true;
314
148
  }): Promise<BigIntStats>;
315
149
  /**
316
- * Truncates the file.
317
- *
318
- * If the file was larger than `len` bytes, only the first `len` bytes will be
319
- * retained in the file.
320
- *
321
- * The following example retains only the first four bytes of the file:
322
- *
323
- * ```js
324
- * import { open } from 'node:fs/promises';
325
- *
326
- * let filehandle = null;
327
- * try {
328
- * filehandle = await open('temp.txt', 'r+');
329
- * await filehandle.truncate(4);
330
- * } finally {
331
- * await filehandle?.close();
332
- * }
333
- * ```
334
- *
335
- * If the file previously was shorter than `len` bytes, it is extended, and the
336
- * extended part is filled with null bytes (`'\0'`):
337
- *
338
- * If `len` is negative then `0` will be used.
339
- * @since v10.0.0
340
- * @param [len=0]
341
- * @return Fulfills with `undefined` upon success.
150
+ * Keep only the first `len` bytes, or extend with NUL bytes when the file was
151
+ * shorter. A negative `len` is treated as 0.
342
152
  */
343
153
  truncate(len?: number): Promise<void>;
344
- /**
345
- * Change the file system timestamps of the object referenced by the `FileHandle` then resolves the promise with no arguments upon success.
346
- * @since v10.0.0
347
- */
154
+ /** Change the file system timestamps of the referenced object. */
348
155
  utimes(atime: string | number | Date, mtime: string | number | Date): Promise<void>;
349
156
  /**
350
- * Asynchronously writes data to a file, replacing the file if it already exists.`data` can be a string, a buffer, an
351
- * [AsyncIterable](https://tc39.github.io/ecma262/#sec-asynciterable-interface) or
352
- * [Iterable](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#The_iterable_protocol) object.
353
- * The promise is resolved with no arguments upon success.
354
- *
355
- * If `options` is a string, then it specifies the `encoding`.
356
- *
357
- * The `FileHandle` has to support writing.
358
- *
359
- * It is unsafe to use `filehandle.writeFile()` multiple times on the same file
360
- * without waiting for the promise to be resolved (or rejected).
361
- *
362
- * If one or more `filehandle.write()` calls are made on a file handle and then a`filehandle.writeFile()` call is made, the data will be written from the
363
- * current position till the end of the file. It doesn't always write from the
364
- * beginning of the file.
365
- * @since v10.0.0
157
+ * Write from the CURRENT position to the end — not from the beginning, so
158
+ * earlier `write()` calls on this handle move the starting point. Unsafe to
159
+ * call again before the promise settles.
366
160
  */
367
161
  writeFile(data: string | Uint8Array, options?: (ObjectEncodingOptions & FlagAndOpenMode & Abortable) | BufferEncoding | null): Promise<void>;
368
162
  /**
369
- * Write `buffer` to the file.
370
- *
371
- * The promise is resolved with an object containing two properties:
372
- *
373
- * It is unsafe to use `filehandle.write()` multiple times on the same file
374
- * without waiting for the promise to be resolved (or rejected). For this
375
- * scenario, use `filehandle.createWriteStream()`.
163
+ * Write `buffer` at `position`, or at the current position when `position` is
164
+ * not a number (POSIX pwrite(2)). Unsafe to call again before the promise
165
+ * settles — use `createWriteStream()` for that.
376
166
  *
377
- * On Linux, positional writes do not work when the file is opened in append mode.
378
- * The kernel ignores the position argument and always appends the data to
379
- * the end of the file.
380
- * @since v10.0.0
381
- * @param [offset=0] The start position from within `buffer` where the data to write begins.
382
- * @param [length=buffer.byteLength - offset] The number of bytes from `buffer` to write.
383
- * @param position The offset from the beginning of the file where the data from `buffer` should be written. If `position` is not a `number`, the data will be written at the current position.
384
- * See the POSIX pwrite(2) documentation for more detail.
167
+ * On Linux a positional write is IGNORED on an append-mode file: the kernel
168
+ * always appends.
385
169
  */
386
170
  write<TBuffer extends NodeJS.ArrayBufferView>(buffer: TBuffer, offset?: number | null, length?: number | null, position?: number | null): Promise<{
387
171
  bytesWritten: number;
@@ -400,27 +184,12 @@ export declare class FileHandle implements IFileHandle {
400
184
  buffer: string;
401
185
  }>;
402
186
  /**
403
- * Write an array of [ArrayBufferView](https://developer.mozilla.org/en-US/docs/Web/API/ArrayBufferView) s to the file.
404
- *
405
- * The promise is resolved with an object containing a two properties:
406
- *
407
- * It is unsafe to call `writev()` multiple times on the same file without waiting
408
- * for the promise to be resolved (or rejected).
409
- *
410
- * On Linux, positional writes don't work when the file is opened in append mode.
411
- * The kernel ignores the position argument and always appends the data to
412
- * the end of the file.
413
- * @since v12.9.0
414
- * @param position The offset from the beginning of the file where the data from `buffers` should be written. If `position` is not a `number`, the data will be written at the current
415
- * position.
187
+ * Write several views in sequence. `position` is where the FIRST one lands, the
188
+ * current position when it is not a number; unsafe to call again before the
189
+ * promise settles. As with `write()`, Linux ignores it in append mode.
416
190
  */
417
191
  writev<TBuffers extends readonly NodeJS.ArrayBufferView[]>(buffers: TBuffers, position?: number): Promise<WriteVResult<TBuffers>>;
418
- /**
419
- * Read from a file and write to an array of [ArrayBufferView](https://developer.mozilla.org/en-US/docs/Web/API/ArrayBufferView) s
420
- * @since v13.13.0, v12.17.0
421
- * @param position The offset from the beginning of the file where the data should be read from. If `position` is not a `number`, the data will be read from the current position.
422
- * @return Fulfills upon success an object containing two properties:
423
- */
192
+ /** Fill several views in sequence, from `position` or the current position. */
424
193
  readv<TBuffers extends readonly NodeJS.ArrayBufferView[]>(buffers: TBuffers, position?: number): Promise<ReadVResult<TBuffers>>;
425
194
  /** @internal */ _flushSync(): void;
426
195
  /** @internal */ _closeSync(): void;
@@ -17,19 +17,17 @@ export declare function readdirSync(path: PathLike, options?: {
17
17
  /**
18
18
  * POSIX `realpath(3)`: resolve EVERY component, not merely a trailing symlink.
19
19
  *
20
- * This used to `query_info` the path once and return it UNCHANGED whenever the
21
- * LEAF was not a symlink, so a symlinked ANCESTOR survived into the result. On a
22
- * typical Linux box that is invisible — `/tmp` is a real directory, so the
23
- * identity answer happens to be the right one. On macOS `/var` is a symlink to
24
- * `private/var`, so `realpathSync(os.tmpdir())` returned `/var/folders/…` while a
25
- * child process asked for its own cwd reported `/private/var/folders/…`. That
26
- * surfaced as six `@gjsify/child_process` cwd failures on main's macOS leg — the
27
- * only leg that runs it, macOS being off for pull requests. The platform was
28
- * incidental: reproduced on Linux with nothing but a symlinked parent directory.
20
+ * Checking only the LEAF lets a symlinked ANCESTOR survive into the result, which is
21
+ * invisible on a typical Linux box — `/tmp` is a real directory, so the identity
22
+ * answer is accidentally right. On macOS `/var` is a symlink to `private/var`, so
23
+ * `realpathSync(os.tmpdir())` answered `/var/folders/…` while the child process
24
+ * asked for its own cwd reported `/private/var/folders/…`: six
25
+ * `@gjsify/child_process` cwd failures on main's macOS leg, the only leg that runs
26
+ * it. The platform was incidental — a symlinked parent directory reproduces it on
27
+ * Linux.
29
28
  *
30
- * The symlink budget is shared across the whole walk because POSIX counts total
31
- * resolutions rather than per-component ones, so a cycle reachable through any
32
- * component still terminates.
29
+ * The symlink budget is shared across the whole walk because POSIX counts TOTAL
30
+ * resolutions, not per-component ones, so a cycle through any component terminates.
33
31
  */
34
32
  export declare function realpathSync(path: PathLike): string;
35
33
  export declare function symlinkSync(target: PathLike, path: PathLike, _type?: 'file' | 'dir' | 'junction'): void;
@@ -38,39 +36,21 @@ export declare function readFileSync(path: PathLike | number | FileHandle, optio
38
36
  flag?: string;
39
37
  } | string | null): string | Buffer<ArrayBuffer>;
40
38
  /**
41
- * Synchronously creates a directory. Returns `undefined`, or if `recursive` is`true`, the first directory path created.
42
- * This is the synchronous version of {@link mkdir}.
43
- *
44
- * See the POSIX [`mkdir(2)`](http://man7.org/linux/man-pages/man2/mkdir.2.html) documentation for more details.
45
- * @since v0.1.21
39
+ * [`mkdir(2)`](http://man7.org/linux/man-pages/man2/mkdir.2.html). Returns the FIRST
40
+ * directory created under `recursive: true`, otherwise `undefined`. A string mode is
41
+ * parsed as octal; the default is `0o777`.
46
42
  */
47
43
  export declare function mkdirSync(path: PathLike, options: MakeDirectoryOptions & {
48
44
  recursive: true;
49
45
  }): string | undefined;
50
- /**
51
- * Synchronous mkdir(2) - create a directory.
52
- * @param path A path to a file. If a URL is provided, it must use the `file:` protocol.
53
- * @param options Either the file mode, or an object optionally specifying the file mode and whether parent folders
54
- * should be created. If a string is passed, it is parsed as an octal integer. If not specified, defaults to `0o777`.
55
- */
56
46
  export declare function mkdirSync(path: PathLike, options?: Mode | (MakeDirectoryOptions & {
57
47
  recursive?: false | undefined;
58
48
  }) | null): void;
59
- /**
60
- * Synchronous mkdir(2) - create a directory.
61
- * @param path A path to a file. If a URL is provided, it must use the `file:` protocol.
62
- * @param options Either the file mode, or an object optionally specifying the file mode and whether parent folders
63
- * should be created. If a string is passed, it is parsed as an octal integer. If not specified, defaults to `0o777`.
64
- */
65
49
  export declare function mkdirSync(path: PathLike, options?: Mode | MakeDirectoryOptions | null): string | undefined | void;
66
50
  /**
67
- * Synchronous [`rmdir(2)`](http://man7.org/linux/man-pages/man2/rmdir.2.html). Returns `undefined`.
68
- *
69
- * Using `fs.rmdirSync()` on a file (not a directory) results in an `ENOENT` error
70
- * on Windows and an `ENOTDIR` error on POSIX.
71
- *
72
- * To get a behavior similar to the `rm -rf` Unix command, use {@link rmSync} with options `{ recursive: true, force: true }`.
73
- * @since v0.1.21
51
+ * [`rmdir(2)`](http://man7.org/linux/man-pages/man2/rmdir.2.html) — empty directories
52
+ * only; `rm -rf` behaviour is {@link rmSync} with `{ recursive: true, force: true }`.
53
+ * On a FILE this is ENOTDIR on POSIX but ENOENT on Windows.
74
54
  */
75
55
  export declare function rmdirSync(path: PathLike, _options?: RmDirOptions): void;
76
56
  export declare function unlinkSync(path: PathLike): void;
@@ -99,47 +79,25 @@ export declare function watch(filename: PathLike, options: {
99
79
  } | undefined, listener: ((eventType: string, filename: string | null) => void) | undefined): FSWatcher;
100
80
  export declare function openSync(path: PathLike, flags?: OpenFlags | number, mode?: Mode): FileHandle;
101
81
  /**
102
- * Returns the created directory path.
103
- *
104
- * For detailed information, see the documentation of the asynchronous version of
105
- * this API: {@link mkdtemp}.
106
- *
107
- * The optional `options` argument can be a string specifying an encoding, or an
108
- * object with an `encoding` property specifying the character encoding to use.
109
- * @since v5.10.0
82
+ * Create a unique temporary directory: six random characters appended to `prefix`.
83
+ * Returns its path, encoded per `options` (default `'utf8'`).
110
84
  */
111
85
  export declare function mkdtempSync(prefix: string, options?: EncodingOption): string;
112
- /**
113
- * Synchronously creates a unique temporary directory.
114
- * Generates six random characters to be appended behind a required prefix to create a unique temporary directory.
115
- * @param options The encoding (or an object specifying the encoding), used as the encoding of the result. If not provided, `'utf8'` is used.
116
- */
117
86
  export declare function mkdtempSync(prefix: string, options: BufferEncodingOption): Buffer;
118
- /**
119
- * Synchronously creates a unique temporary directory.
120
- * Generates six random characters to be appended behind a required prefix to create a unique temporary directory.
121
- * @param options The encoding (or an object specifying the encoding), used as the encoding of the result. If not provided, `'utf8'` is used.
122
- */
123
87
  export declare function mkdtempSync(prefix: string, options?: EncodingOption): string | Buffer;
124
88
  /**
125
89
  * `mkdtemp(3)`: claim a unique name and create it 0700, atomically.
126
90
  *
127
- * TWO defects lived in the four lines this replaces. It asked `mkdirSync` for
128
- * `mode: 0o777` — inert while `mode` was ignored, and a WORLD-READABLE
129
- * directory the moment `mode` started working, from the one API whose whole
130
- * purpose is a private scratch space. And it picked the name with
131
- * `while (existsSync(path))`, a test-then-create whose `existsSync` was
132
- * `Gio.File.query_exists()` — true for a dangling symlink, and racy against
133
- * any other process regardless. `mkdir(2)` decides both questions in one
134
- * syscall: it fails EEXIST if the name is taken, so the loop retries instead
135
- * of guessing.
91
+ * The mode is not negotiable — this is the one API whose whole purpose is a PRIVATE
92
+ * scratch space, and 0o777 hands it to everyone. Nor is the atomicity: a
93
+ * `while (existsSync(path))` test-then-create is racy against any other process, and
94
+ * `existsSync` is `Gio.File.query_exists()`, true even for a dangling symlink.
95
+ * `mkdir(2)` answers both in one syscall, failing EEXIST on a taken name, so the loop
96
+ * retries rather than guesses.
136
97
  *
137
- * Both entry points call THIS, so `mkdtempSync` and `promises.mkdtemp` cannot
138
- * disagree on the mode again — they already did, by 0o077.
98
+ * Both entry points call THIS so `mkdtempSync` and `promises.mkdtemp` cannot disagree
99
+ * on the mode — they once did, by 0o077.
139
100
  */
140
101
  export declare function mkdtempAt(prefix: string): string;
141
- /**
142
- * Synchronously removes files and directories (modeled on the standard POSIX `rm`utility). Returns `undefined`.
143
- * @since v14.14.0
144
- */
102
+ /** Remove files and directories, modelled on POSIX `rm`. */
145
103
  export declare function rmSync(path: PathLike, options?: RmOptions): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/fs",
3
- "version": "0.35.0",
3
+ "version": "0.37.0",
4
4
  "description": "Node.js fs module for Gjs",
5
5
  "module": "lib/esm/index.js",
6
6
  "types": "lib/types/index.d.ts",
@@ -66,8 +66,8 @@
66
66
  "fs"
67
67
  ],
68
68
  "devDependencies": {
69
- "@gjsify/cli": "^0.35.0",
70
- "@gjsify/unit": "^0.35.0",
69
+ "@gjsify/cli": "^0.37.0",
70
+ "@gjsify/unit": "^0.37.0",
71
71
  "@types/node": "^25.9.2",
72
72
  "typescript": "^6.0.3"
73
73
  },
@@ -75,11 +75,11 @@
75
75
  "@girs/gio-2.0": "^4.1.0",
76
76
  "@girs/giounix-2.0": "^4.1.0",
77
77
  "@girs/glib-2.0": "^4.1.0",
78
- "@gjsify/buffer": "^0.35.0",
79
- "@gjsify/events": "^0.35.0",
80
- "@gjsify/stream": "^0.35.0",
81
- "@gjsify/url": "^0.35.0",
82
- "@gjsify/utils": "^0.35.0"
78
+ "@gjsify/buffer": "^0.37.0",
79
+ "@gjsify/events": "^0.37.0",
80
+ "@gjsify/stream": "^0.37.0",
81
+ "@gjsify/url": "^0.37.0",
82
+ "@gjsify/utils": "^0.37.0"
83
83
  },
84
84
  "gjsify": {
85
85
  "runtimes": {