@j0hanz/filesystem-mcp 2.8.0 → 2.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/README.md +32 -26
  2. package/dist/cli-help.d.ts.map +1 -1
  3. package/dist/cli-help.js +10 -1
  4. package/dist/cli-help.js.map +1 -1
  5. package/dist/cli.d.ts.map +1 -1
  6. package/dist/cli.js +10 -3
  7. package/dist/cli.js.map +1 -1
  8. package/dist/core/concurrency.d.ts +4 -4
  9. package/dist/core/concurrency.d.ts.map +1 -1
  10. package/dist/core/concurrency.js +1 -1
  11. package/dist/core/concurrency.js.map +1 -1
  12. package/dist/core/config.d.ts +2 -0
  13. package/dist/core/config.d.ts.map +1 -1
  14. package/dist/core/config.js.map +1 -1
  15. package/dist/core/cursor.d.ts +8 -0
  16. package/dist/core/cursor.d.ts.map +1 -1
  17. package/dist/core/cursor.js +14 -0
  18. package/dist/core/cursor.js.map +1 -1
  19. package/dist/core/file-uri.d.ts +12 -3
  20. package/dist/core/file-uri.d.ts.map +1 -1
  21. package/dist/core/file-uri.js +35 -7
  22. package/dist/core/file-uri.js.map +1 -1
  23. package/dist/core/fmt.d.ts +2 -0
  24. package/dist/core/fmt.d.ts.map +1 -1
  25. package/dist/core/fmt.js +16 -0
  26. package/dist/core/fmt.js.map +1 -1
  27. package/dist/core/fs.d.ts +16 -1
  28. package/dist/core/fs.d.ts.map +1 -1
  29. package/dist/core/fs.js +191 -72
  30. package/dist/core/fs.js.map +1 -1
  31. package/dist/core/glob.d.ts +8 -0
  32. package/dist/core/glob.d.ts.map +1 -1
  33. package/dist/core/glob.js +84 -11
  34. package/dist/core/glob.js.map +1 -1
  35. package/dist/core/input-required.d.ts +7 -0
  36. package/dist/core/input-required.d.ts.map +1 -1
  37. package/dist/core/input-required.js +13 -0
  38. package/dist/core/input-required.js.map +1 -1
  39. package/dist/core/path-completer.d.ts.map +1 -1
  40. package/dist/core/path-completer.js +5 -3
  41. package/dist/core/path-completer.js.map +1 -1
  42. package/dist/core/path-utils.d.ts +51 -12
  43. package/dist/core/path-utils.d.ts.map +1 -1
  44. package/dist/core/path-utils.js +69 -29
  45. package/dist/core/path-utils.js.map +1 -1
  46. package/dist/core/path.d.ts +108 -21
  47. package/dist/core/path.d.ts.map +1 -1
  48. package/dist/core/path.js +436 -83
  49. package/dist/core/path.js.map +1 -1
  50. package/dist/core/read.d.ts +13 -3
  51. package/dist/core/read.d.ts.map +1 -1
  52. package/dist/core/read.js +46 -40
  53. package/dist/core/read.js.map +1 -1
  54. package/dist/core/schema.d.ts +9 -6
  55. package/dist/core/schema.d.ts.map +1 -1
  56. package/dist/core/schema.js +22 -19
  57. package/dist/core/schema.js.map +1 -1
  58. package/dist/core/search.d.ts +6 -0
  59. package/dist/core/search.d.ts.map +1 -1
  60. package/dist/core/search.js +159 -33
  61. package/dist/core/search.js.map +1 -1
  62. package/dist/core/sensitive.d.ts.map +1 -1
  63. package/dist/core/sensitive.js +71 -29
  64. package/dist/core/sensitive.js.map +1 -1
  65. package/dist/core/store.d.ts +1 -0
  66. package/dist/core/store.d.ts.map +1 -1
  67. package/dist/core/store.js +3 -0
  68. package/dist/core/store.js.map +1 -1
  69. package/dist/core/util.d.ts +12 -1
  70. package/dist/core/util.d.ts.map +1 -1
  71. package/dist/core/util.js +24 -1
  72. package/dist/core/util.js.map +1 -1
  73. package/dist/prompts.d.ts +1 -1
  74. package/dist/prompts.d.ts.map +1 -1
  75. package/dist/prompts.js +1 -1
  76. package/dist/prompts.js.map +1 -1
  77. package/dist/resources.d.ts +1 -2
  78. package/dist/resources.d.ts.map +1 -1
  79. package/dist/resources.js +20 -13
  80. package/dist/resources.js.map +1 -1
  81. package/dist/server.js +2 -2
  82. package/dist/server.js.map +1 -1
  83. package/dist/tools/create.d.ts.map +1 -1
  84. package/dist/tools/create.js +122 -45
  85. package/dist/tools/create.js.map +1 -1
  86. package/dist/tools/define.d.ts +8 -4
  87. package/dist/tools/define.d.ts.map +1 -1
  88. package/dist/tools/define.js +36 -9
  89. package/dist/tools/define.js.map +1 -1
  90. package/dist/tools/delete.d.ts.map +1 -1
  91. package/dist/tools/delete.js +57 -17
  92. package/dist/tools/delete.js.map +1 -1
  93. package/dist/tools/diff.js +1 -1
  94. package/dist/tools/diff.js.map +1 -1
  95. package/dist/tools/edit.d.ts.map +1 -1
  96. package/dist/tools/edit.js +14 -12
  97. package/dist/tools/edit.js.map +1 -1
  98. package/dist/tools/find-files.d.ts.map +1 -1
  99. package/dist/tools/find-files.js +20 -20
  100. package/dist/tools/find-files.js.map +1 -1
  101. package/dist/tools/index.d.ts +1 -1
  102. package/dist/tools/index.d.ts.map +1 -1
  103. package/dist/tools/index.js +1 -1
  104. package/dist/tools/index.js.map +1 -1
  105. package/dist/tools/list.d.ts.map +1 -1
  106. package/dist/tools/list.js +142 -73
  107. package/dist/tools/list.js.map +1 -1
  108. package/dist/tools/move.d.ts.map +1 -1
  109. package/dist/tools/move.js +81 -36
  110. package/dist/tools/move.js.map +1 -1
  111. package/dist/tools/patch.js +13 -6
  112. package/dist/tools/patch.js.map +1 -1
  113. package/dist/tools/read.d.ts.map +1 -1
  114. package/dist/tools/read.js +114 -45
  115. package/dist/tools/read.js.map +1 -1
  116. package/dist/tools/replace-text.d.ts.map +1 -1
  117. package/dist/tools/replace-text.js +49 -35
  118. package/dist/tools/replace-text.js.map +1 -1
  119. package/dist/tools/search-text.d.ts.map +1 -1
  120. package/dist/tools/search-text.js +20 -49
  121. package/dist/tools/search-text.js.map +1 -1
  122. package/dist/tools/stat.d.ts.map +1 -1
  123. package/dist/tools/stat.js +26 -8
  124. package/dist/tools/stat.js.map +1 -1
  125. package/dist/transport/http-policy.d.ts +3 -2
  126. package/dist/transport/http-policy.d.ts.map +1 -1
  127. package/dist/transport/http-policy.js +3 -2
  128. package/dist/transport/http-policy.js.map +1 -1
  129. package/dist/transport/http.d.ts.map +1 -1
  130. package/dist/transport/http.js +2 -2
  131. package/dist/transport/http.js.map +1 -1
  132. package/dist/transport/stdio.js +1 -1
  133. package/dist/transport/stdio.js.map +1 -1
  134. package/package.json +5 -5
package/dist/core/fs.js CHANGED
@@ -51,38 +51,23 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
51
51
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
52
52
  });
53
53
  import { randomUUID } from 'node:crypto';
54
+ import { constants, existsSync } from 'node:fs';
54
55
  import { chmod as fsChmod, cp as fsCp, lstat as fsLstat, mkdir as fsMkdir, open as fsOpen, opendir as fsOpendir, readFile as fsReadFile, readlink as fsReadlink, rename as fsRename, rm as fsRm, rmdir as fsRmdir, stat as fsStat, unlink as fsUnlink, writeFile as fsWriteFile, } from 'node:fs/promises';
55
- import { dirname, isAbsolute, resolve } from 'node:path';
56
+ import { isAbsolute, join } from 'node:path';
56
57
  import { runExclusive, withAbort } from "./concurrency.js";
57
58
  import { ErrorCode, formatUnknownErrorMessage, FsError, isFsError, isNodeError, isNotFoundErrno, } from "./errors.js";
58
59
  import { detectMimeFromContent, MIME_SAMPLE_SIZE } from "./mime.js";
59
60
  import { Logger } from "./observability.js";
60
- import { normalizePath, respellCaseOnlyTarget } from "./path-utils.js";
61
- import { APPEND_CREATE_NOFOLLOW, APPEND_NOFOLLOW, assertFileStats, createTooLargeError, READ_NOFOLLOW, readFileWithStats, } from "./read.js";
61
+ import { isSamePath, normalizePath, respellSpellingOnlyTarget } from "./path-utils.js";
62
+ import { APPEND_CREATE_NOFOLLOW, APPEND_NOFOLLOW, assertFileStats, createTooLargeError, O_NOFOLLOW, READ_NOFOLLOW, readFileWithStats, } from "./read.js";
62
63
  import { getMaxTextFileSize } from "./util.js";
64
+ // Directory open for countEntries. O_NOFOLLOW refuses a symlink named as the
65
+ // directory; used only under COUNT_THROUGH_FD, like the re-check it feeds.
66
+ const DIR_NOFOLLOW = constants.O_RDONLY | constants.O_DIRECTORY | O_NOFOLLOW;
67
+ // The descriptor re-check and the read through /proc/self/fd need Linux with procfs mounted,
68
+ // as the read path's re-check does; elsewhere the count reads the directory by path.
69
+ const COUNT_THROUGH_FD = process.platform === 'linux' && existsSync('/proc/self/fd');
63
70
  // ─── Domain primitives ────────────────────────────────────────────────────────
64
- /**
65
- * validatePathForWrite + symlink resolution: a write aimed at a link must land
66
- * on its revalidated target, not create a new file beside the link. ENOENT is
67
- * the normal new-file case, so it falls through with the original path.
68
- */
69
- async function resolveForWrite(pathGuard, filePath) {
70
- let validPath = await pathGuard.validatePathForWrite(filePath);
71
- try {
72
- const stats = await fsLstat(validPath);
73
- if (stats.isSymbolicLink()) {
74
- const target = await fsReadlink(validPath);
75
- const resolvedTarget = isAbsolute(target) ? target : resolve(dirname(validPath), target);
76
- validPath = await pathGuard.validatePathForWrite(resolvedTarget);
77
- }
78
- }
79
- catch (error) {
80
- if (!isNotFoundErrno(error)) {
81
- throw error;
82
- }
83
- }
84
- return validPath;
85
- }
86
71
  const CHAR_LF = 10;
87
72
  const LINE_COUNT_CHUNK_BYTES = 64 * 1024;
88
73
  export class GuardedFileSystem {
@@ -95,6 +80,7 @@ export class GuardedFileSystem {
95
80
  * for a symlink after validation still passes. Re-read the open descriptor's
96
81
  * real path (Linux `/proc/self/fd`; skipped elsewhere) and require it to stay
97
82
  * inside the roots. Run before any byte is read or written through `handle`.
83
+ * Resolves to the normalized real path checked, or undefined when /proc is unreadable.
98
84
  */
99
85
  async #assertFdWithinRoots(handle, requestedPath) {
100
86
  let fdPath;
@@ -102,7 +88,7 @@ export class GuardedFileSystem {
102
88
  fdPath = await fsReadlink(`/proc/self/fd/${String(handle.fd)}`);
103
89
  }
104
90
  catch {
105
- return;
91
+ return undefined;
106
92
  }
107
93
  // A replaced target reads back as "<path> (deleted)".
108
94
  const inRoots = isAbsolute(fdPath) &&
@@ -112,6 +98,49 @@ export class GuardedFileSystem {
112
98
  if (!inRoots) {
113
99
  throw new FsError(ErrorCode.ACCESS_DENIED, 'Path changed under validation and now resolves outside the allowed roots (TOCTOU).', requestedPath);
114
100
  }
101
+ return normalizePath(fdPath);
102
+ }
103
+ /** The one read open: O_NOFOLLOW, then the descriptor re-check above. */
104
+ async #openVerified(validPath, requestedPath, signal) {
105
+ const opening = fsOpen(validPath, READ_NOFOLLOW);
106
+ let handle;
107
+ try {
108
+ handle = await withAbort(opening, signal);
109
+ }
110
+ catch (error) {
111
+ // An open the signal abandoned still lands; close it then.
112
+ void opening
113
+ .then((late) => late.close(), () => undefined)
114
+ .catch((closeErr) => {
115
+ Logger.warn(`Failed to close file handle for ${validPath} after abort: ${formatUnknownErrorMessage(closeErr)}`);
116
+ });
117
+ throw error;
118
+ }
119
+ try {
120
+ await this.#assertFdWithinRoots(handle, requestedPath);
121
+ return handle;
122
+ }
123
+ catch (error) {
124
+ await handle.close();
125
+ throw error;
126
+ }
127
+ }
128
+ /**
129
+ * Waits for `pending` until `signal` fires. A result that lands after the abort goes to
130
+ * `release` without anyone waiting on it, so a stalled open or read never holds the caller.
131
+ */
132
+ async #abandonOnAbort(pending, signal, release, label) {
133
+ try {
134
+ return await withAbort(pending, signal);
135
+ }
136
+ catch (error) {
137
+ void pending
138
+ .then(release, () => undefined)
139
+ .catch((releaseErr) => {
140
+ Logger.warn(`Failed to release ${label} after abort: ${formatUnknownErrorMessage(releaseErr)}`);
141
+ });
142
+ throw error;
143
+ }
115
144
  }
116
145
  /**
117
146
  * Serialize a read-modify-write on the resolved write target, so two writers
@@ -120,7 +149,7 @@ export class GuardedFileSystem {
120
149
  * do not take the lock, so `fn` may call them.
121
150
  */
122
151
  async lockForWrite(filePath, fn) {
123
- const key = await resolveForWrite(this.pathGuard, filePath);
152
+ const key = await this.pathGuard.validatePathForWrite(filePath);
124
153
  return runExclusive(key, () => fn(key));
125
154
  }
126
155
  async stat(filePath, options) {
@@ -151,14 +180,19 @@ export class GuardedFileSystem {
151
180
  // link would rename its target and leave the link dangling.
152
181
  const validOld = await this.pathGuard.validatePathForDelete(oldPath);
153
182
  let validNew = await this.pathGuard.validatePathForWrite(newPath);
154
- // Renaming a path onto itself is only meaningful as a case-only rename.
155
- if (validOld === validNew)
156
- validNew = respellCaseOnlyTarget(newPath, validNew);
183
+ // A new path naming the old entry itself, in another case or Unicode
184
+ // spelling, renames that entry. Probed without following a final link:
185
+ // validatePathForWrite resolved a link to its target, and renaming onto
186
+ // that would replace the target with the link.
187
+ const sameEntry = await this.pathGuard.validatePathForDelete(newPath).then((entry) => isSamePath(entry, validOld), () => false);
188
+ if (sameEntry || validOld === validNew) {
189
+ validNew = respellSpellingOnlyTarget(newPath, sameEntry ? validOld : validNew);
190
+ }
157
191
  await fsRename(validOld, validNew);
158
192
  }
159
193
  async writeFile(filePath, content, options = {}) {
160
194
  const { signal } = options;
161
- const validPath = await resolveForWrite(this.pathGuard, filePath);
195
+ const validPath = await this.pathGuard.validatePathForWrite(filePath);
162
196
  const tempSuffix = randomUUID().replace(/-/g, '').slice(0, 12);
163
197
  const tempPath = `${validPath}.${tempSuffix}.tmp`;
164
198
  // The rename below swaps in the temp file's inode, so the target would
@@ -217,7 +251,7 @@ export class GuardedFileSystem {
217
251
  */
218
252
  async appendFile(filePath, content, options = {}) {
219
253
  const { signal } = options;
220
- const validPath = await resolveForWrite(this.pathGuard, filePath);
254
+ const validPath = await this.pathGuard.validatePathForWrite(filePath);
221
255
  // Non-regular targets must be rejected before the open: 'a' on a FIFO
222
256
  // (POSIX) blocks until a reader appears, and fsOpen takes no signal, so
223
257
  // the call would hang unabortable and pin its concurrency slot. The
@@ -372,7 +406,7 @@ export class GuardedFileSystem {
372
406
  spec.signal?.throwIfAborted();
373
407
  const validPath = await this.pathGuard.validateExistingPath(filePath);
374
408
  const stats = await withAbort(fsStat(validPath), spec.signal);
375
- return readFileWithStats(filePath, validPath, stats, spec, (handle) => this.#assertFdWithinRoots(handle, filePath));
409
+ return readFileWithStats(filePath, validPath, stats, spec, () => this.#openVerified(validPath, filePath, spec.signal));
376
410
  }
377
411
  async readEditableText(filePath, options) {
378
412
  const { stats, validPath } = await this.stat(filePath, options);
@@ -385,55 +419,57 @@ export class GuardedFileSystem {
385
419
  kind: 'full',
386
420
  strictUtf8: true,
387
421
  ...(options?.signal ? { signal: options.signal } : {}),
388
- }, (handle) => this.#assertFdWithinRoots(handle, filePath));
422
+ }, () => this.#openVerified(validPath, filePath, options?.signal));
389
423
  return { validPath, content, stats };
390
424
  }
391
425
  async readRaw(filePath, options) {
392
- options?.signal?.throwIfAborted();
393
- const validPath = await this.pathGuard.validateExistingPath(filePath);
394
- options?.signal?.throwIfAborted();
395
- const stats = await withAbort(fsStat(validPath), options?.signal);
396
- assertFileStats(filePath, stats);
397
- // Enforce size limit before reading to avoid loading large files into memory.
398
- // Binary detection is best-effort, but this is a hard limit.
399
- const maxTextFileSize = getMaxTextFileSize();
400
- if (stats.size > maxTextFileSize) {
401
- throw createTooLargeError(stats.size, maxTextFileSize, filePath);
402
- }
403
- let content;
404
- const handle = await fsOpen(validPath, READ_NOFOLLOW);
426
+ const env_3 = { stack: [], error: void 0, hasError: false };
405
427
  try {
406
- await this.#assertFdWithinRoots(handle, filePath);
407
- content = await fsReadFile(handle, options);
408
- }
409
- catch (error) {
410
- // Node wraps even a TimeoutError signal reason in AbortError.
411
- if (error instanceof Error && error.name === 'AbortError') {
412
- options?.signal?.throwIfAborted();
428
+ options?.signal?.throwIfAborted();
429
+ const validPath = await this.pathGuard.validateExistingPath(filePath);
430
+ options?.signal?.throwIfAborted();
431
+ const stats = await withAbort(fsStat(validPath), options?.signal);
432
+ assertFileStats(filePath, stats);
433
+ // Enforce size limit before reading to avoid loading large files into memory.
434
+ // Binary detection is best-effort, but this is a hard limit.
435
+ const maxTextFileSize = getMaxTextFileSize();
436
+ if (stats.size > maxTextFileSize) {
437
+ throw createTooLargeError(stats.size, maxTextFileSize, filePath);
413
438
  }
414
- throw error;
439
+ let content;
440
+ const handle = __addDisposableResource(env_3, await this.#openVerified(validPath, filePath), true);
441
+ try {
442
+ content = await fsReadFile(handle, options);
443
+ }
444
+ catch (error) {
445
+ // Node wraps even a TimeoutError signal reason in AbortError.
446
+ if (error instanceof Error && error.name === 'AbortError') {
447
+ options?.signal?.throwIfAborted();
448
+ }
449
+ throw error;
450
+ }
451
+ const mimeInfo = detectMimeFromContent(validPath, content);
452
+ return {
453
+ content,
454
+ mimeType: mimeInfo.mimeType,
455
+ kind: mimeInfo.kind,
456
+ isBinary: mimeInfo.kind !== 'text',
457
+ validPath,
458
+ modified: stats.mtime.toISOString(),
459
+ };
460
+ }
461
+ catch (e_3) {
462
+ env_3.error = e_3;
463
+ env_3.hasError = true;
415
464
  }
416
465
  finally {
417
- await handle.close();
466
+ const result_3 = __disposeResources(env_3);
467
+ if (result_3)
468
+ await result_3;
418
469
  }
419
- const mimeInfo = detectMimeFromContent(validPath, content);
420
- return {
421
- content,
422
- mimeType: mimeInfo.mimeType,
423
- isBinary: mimeInfo.kind !== 'text',
424
- };
425
470
  }
426
471
  async open(filePath) {
427
- const validPath = await this.pathGuard.validateExistingPath(filePath);
428
- const handle = await fsOpen(validPath, READ_NOFOLLOW);
429
- try {
430
- await this.#assertFdWithinRoots(handle, filePath);
431
- }
432
- catch (error) {
433
- await handle.close();
434
- throw error;
435
- }
436
- return handle;
472
+ return this.#openVerified(await this.pathGuard.validateExistingPath(filePath), filePath);
437
473
  }
438
474
  // Single resolution + stat: validateExistingPathDetailed resolves the real
439
475
  // path (following symlinks, re-checking sensitivity) once, then we stat the
@@ -456,6 +492,89 @@ export class GuardedFileSystem {
456
492
  });
457
493
  }
458
494
  }
495
+ /**
496
+ * Direct children of a directory, leaving out entries the guard treats as
497
+ * sensitive so the count never reveals what listings hide. Hidden and
498
+ * ignored entries count: this answers "is it empty?", not "what would list show?".
499
+ * A symlinked child counts only when list would show it. The path validation, open, opendir,
500
+ * each read and each link check race the signal, so a stalled mount ends the call at its
501
+ * deadline.
502
+ */
503
+ async countEntries(dirPath, options) {
504
+ const signal = options?.signal;
505
+ // Refuse an already-cancelled call before any validation or I/O.
506
+ signal?.throwIfAborted();
507
+ // A late validation result is a plain object, so a stalled realpath needs no release.
508
+ const { resolvedPath } = await withAbort(this.pathGuard.validateExistingPathDetailed(dirPath), signal);
509
+ // With procfs (COUNT_THROUGH_FD), open the directory itself with O_NOFOLLOW and check where
510
+ // the open descriptor really is, so a directory or ancestor swapped for a symlink after
511
+ // validation is refused. Then read entries through the descriptor, which a later swap cannot
512
+ // redirect.
513
+ const handle = COUNT_THROUGH_FD
514
+ ? await this.#abandonOnAbort(fsOpen(resolvedPath, DIR_NOFOLLOW), signal, (late) => late.close(), `directory descriptor for ${resolvedPath}`)
515
+ : undefined;
516
+ let dir;
517
+ let pendingRead;
518
+ try {
519
+ const openedPath = handle ? await this.#assertFdWithinRoots(handle, dirPath) : undefined;
520
+ // An in-root swap passes the roots check, but the children would be filtered under the
521
+ // validated directory's spelling, not the one actually opened.
522
+ if (openedPath !== undefined && !isSamePath(openedPath, resolvedPath)) {
523
+ throw new FsError(ErrorCode.ACCESS_DENIED, 'Path changed under validation and now resolves to another directory (TOCTOU).', dirPath);
524
+ }
525
+ const readPath = handle ? `/proc/self/fd/${String(handle.fd)}` : resolvedPath;
526
+ dir = await this.#abandonOnAbort(fsOpendir(readPath), signal, (late) => late.close(), `directory stream for ${resolvedPath}`);
527
+ let count = 0;
528
+ for (;;) {
529
+ // Each read races the signal, so a read that stalls ends the call at its deadline.
530
+ const read = dir.read();
531
+ pendingRead = read;
532
+ const entry = await withAbort(read, signal);
533
+ pendingRead = undefined;
534
+ if (entry === null)
535
+ break;
536
+ if (this.pathGuard.isSensitive(join(dirPath, entry.name)))
537
+ continue;
538
+ // list walks the real directory, so it checks every child, links
539
+ // included, under its real spelling: a deny rule naming the real
540
+ // directory hides the child under an alias too.
541
+ if (this.pathGuard.isSensitive(join(resolvedPath, entry.name)))
542
+ continue;
543
+ // A link counts only when list would show it: same rule, same error policy.
544
+ if (entry.isSymbolicLink() &&
545
+ !(await withAbort(this.pathGuard.isEntryAccessible(join(resolvedPath, entry.name)), signal))) {
546
+ continue;
547
+ }
548
+ count += 1;
549
+ }
550
+ // A deadline that fired during the last read still ends the call, even with no entries left.
551
+ signal?.throwIfAborted();
552
+ return count;
553
+ }
554
+ finally {
555
+ if (dir) {
556
+ const stream = dir;
557
+ if (pendingRead) {
558
+ // close() queues behind a read still in flight, so awaiting it would hold the TIMEOUT
559
+ // response on a stalled mount. Close once that read settles, without waiting for it.
560
+ void pendingRead
561
+ .then(() => stream.close(), () => stream.close())
562
+ .catch((closeErr) => {
563
+ Logger.warn(`Failed to close dir handle for ${resolvedPath} after abort: ${formatUnknownErrorMessage(closeErr)}`);
564
+ });
565
+ }
566
+ else {
567
+ await stream.close().catch((closeErr) => {
568
+ Logger.warn(`Failed to close dir handle for ${resolvedPath}: ${formatUnknownErrorMessage(closeErr)}`);
569
+ });
570
+ }
571
+ }
572
+ // A failed close must not replace the error the count threw, so it is logged, not thrown.
573
+ await handle?.close().catch((closeErr) => {
574
+ Logger.warn(`Failed to close dir descriptor for ${resolvedPath}: ${formatUnknownErrorMessage(closeErr)}`);
575
+ });
576
+ }
577
+ }
459
578
  }
460
579
  /**
461
580
  * Returns whether `path` exists, treating "not found" as absent and logging any
@@ -1 +1 @@
1
- {"version":3,"file":"fs.js","sourceRoot":"","sources":["../../src/core/fs.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomUUID } from 'node:crypto';\nimport type { Stats } from 'node:fs';\nimport type { FileHandle } from 'node:fs/promises';\nimport {\n chmod as fsChmod,\n cp as fsCp,\n lstat as fsLstat,\n mkdir as fsMkdir,\n open as fsOpen,\n opendir as fsOpendir,\n readFile as fsReadFile,\n readlink as fsReadlink,\n rename as fsRename,\n rm as fsRm,\n rmdir as fsRmdir,\n stat as fsStat,\n unlink as fsUnlink,\n writeFile as fsWriteFile,\n} from 'node:fs/promises';\nimport { dirname, isAbsolute, resolve } from 'node:path';\n\nimport { runExclusive, withAbort } from './concurrency.ts';\nimport {\n ErrorCode,\n formatUnknownErrorMessage,\n FsError,\n isFsError,\n isNodeError,\n isNotFoundErrno,\n} from './errors.ts';\nimport { detectMimeFromContent, MIME_SAMPLE_SIZE } from './mime.ts';\nimport { Logger } from './observability.ts';\nimport type { EntryType as FileType } from './path-utils.ts';\nimport { normalizePath, respellCaseOnlyTarget } from './path-utils.ts';\nimport type { PathGuard } from './path.ts';\nimport type { ReadFileResult, ReadSpec } from './read.ts';\nimport {\n APPEND_CREATE_NOFOLLOW,\n APPEND_NOFOLLOW,\n assertFileStats,\n createTooLargeError,\n READ_NOFOLLOW,\n readFileWithStats,\n} from './read.ts';\nimport { getMaxTextFileSize } from './util.ts';\n\nexport type { FileType };\nexport type { Stats };\n\n// ─── Domain primitives ────────────────────────────────────────────────────────\n\n/**\n * validatePathForWrite + symlink resolution: a write aimed at a link must land\n * on its revalidated target, not create a new file beside the link. ENOENT is\n * the normal new-file case, so it falls through with the original path.\n */\nasync function resolveForWrite(pathGuard: PathGuard, filePath: string): Promise<string> {\n let validPath = await pathGuard.validatePathForWrite(filePath);\n\n try {\n const stats = await fsLstat(validPath);\n if (stats.isSymbolicLink()) {\n const target = await fsReadlink(validPath);\n const resolvedTarget = isAbsolute(target) ? target : resolve(dirname(validPath), target);\n validPath = await pathGuard.validatePathForWrite(resolvedTarget);\n }\n } catch (error) {\n if (!isNotFoundErrno(error)) {\n throw error;\n }\n }\n\n return validPath;\n}\n\nconst CHAR_LF = 10;\n\nconst LINE_COUNT_CHUNK_BYTES = 64 * 1024;\n\nexport class GuardedFileSystem {\n readonly pathGuard: PathGuard;\n\n constructor(pathGuard: PathGuard) {\n this.pathGuard = pathGuard;\n }\n\n /**\n * O_NOFOLLOW only guards the final component; an ANCESTOR directory swapped\n * for a symlink after validation still passes. Re-read the open descriptor's\n * real path (Linux `/proc/self/fd`; skipped elsewhere) and require it to stay\n * inside the roots. Run before any byte is read or written through `handle`.\n */\n async #assertFdWithinRoots(handle: FileHandle, requestedPath: string): Promise<void> {\n let fdPath: string;\n try {\n fdPath = await fsReadlink(`/proc/self/fd/${String(handle.fd)}`);\n } catch {\n return;\n }\n // A replaced target reads back as \"<path> (deleted)\".\n const inRoots =\n isAbsolute(fdPath) &&\n !fdPath.endsWith(' (deleted)') &&\n this.pathGuard.allowedRootContaining(normalizePath(fdPath)) !== undefined &&\n !this.pathGuard.isSensitive(normalizePath(fdPath));\n if (!inRoots) {\n throw new FsError(\n ErrorCode.ACCESS_DENIED,\n 'Path changed under validation and now resolves outside the allowed roots (TOCTOU).',\n requestedPath,\n );\n }\n }\n\n /**\n * Serialize a read-modify-write on the resolved write target, so two writers\n * of one file (any spelling, including via a symlinked parent) cannot both\n * read the same base and have the last rename win. `writeFile`/`appendFile`\n * do not take the lock, so `fn` may call them.\n */\n async lockForWrite<T>(filePath: string, fn: (resolvedTarget: string) => Promise<T>): Promise<T> {\n const key = await resolveForWrite(this.pathGuard, filePath);\n return runExclusive(key, () => fn(key));\n }\n\n async stat(\n filePath: string,\n options?: { signal?: AbortSignal | undefined },\n ): Promise<{ stats: Stats; validPath: string }> {\n const validPath = await this.pathGuard.validateExistingPath(filePath);\n const stats = await withAbort(fsStat(validPath), options?.signal);\n return { stats, validPath };\n }\n\n async lstat(\n filePath: string,\n options?: { signal?: AbortSignal },\n ): Promise<{ stats: Stats; validPath: string }> {\n const validPath = await this.pathGuard.validatePathForDelete(filePath);\n const stats = await withAbort(fsLstat(validPath), options?.signal);\n return { stats, validPath };\n }\n\n async readlink(\n filePath: string,\n options?: Parameters<typeof fsReadlink>[1],\n ): Promise<{ linkString: string; validPath: string }> {\n // validateExistingPath resolves through the symlink, so readlink would always\n // be handed the final target — a regular file — and fail EINVAL. This guard\n // keeps the link itself while still enforcing containment and the denylist.\n const validPath = await this.pathGuard.validatePathForDelete(filePath);\n const raw = await fsReadlink(validPath, options);\n const linkString = Buffer.isBuffer(raw) ? raw.toString('utf-8') : raw;\n return { linkString, validPath };\n }\n\n async mkdir(filePath: string, options?: Parameters<typeof fsMkdir>[1]): Promise<void> {\n const validPath = await this.pathGuard.validatePathForWrite(filePath);\n await fsMkdir(validPath, options);\n }\n\n async rename(oldPath: string, newPath: string): Promise<void> {\n // Not validateExistingPath: that resolves through a symlink, so renaming a\n // link would rename its target and leave the link dangling.\n const validOld = await this.pathGuard.validatePathForDelete(oldPath);\n let validNew = await this.pathGuard.validatePathForWrite(newPath);\n // Renaming a path onto itself is only meaningful as a case-only rename.\n if (validOld === validNew) validNew = respellCaseOnlyTarget(newPath, validNew);\n await fsRename(validOld, validNew);\n }\n\n async writeFile(\n filePath: string,\n content: string,\n options: { signal?: AbortSignal | undefined } = {},\n ): Promise<{ validPath: string }> {\n const { signal } = options;\n const validPath = await resolveForWrite(this.pathGuard, filePath);\n\n const tempSuffix = randomUUID().replace(/-/g, '').slice(0, 12);\n const tempPath = `${validPath}.${tempSuffix}.tmp`;\n\n // The rename below swaps in the temp file's inode, so the target would\n // inherit fsWriteFile's default 0o666 & ~umask — silently widening a 0600\n // file to 0644 on every write. Carry the existing mode across instead.\n let existingMode: number | undefined;\n try {\n existingMode = (await fsStat(validPath)).mode & 0o777;\n } catch (error) {\n // ENOENT is the normal new-file case: the default mode is correct there.\n // Anything else (EACCES, EIO) means the mode about to be overwritten could\n // not be read, and the write will silently widen the file — say so rather\n // than swallowing it.\n if (!isNotFoundErrno(error)) {\n Logger.warn(\n `atomicWriteFile: cannot read the existing mode of ${validPath}; the write will use the default mode: ${formatUnknownErrorMessage(error)}`,\n );\n }\n }\n\n try {\n signal?.throwIfAborted();\n // Create the temp file already narrowed to the target's mode: a 0600\n // file's new bytes must never sit at 0644 while the write is in flight.\n // `wx` fails on a name collision instead of overwriting; the chmod below\n // still restores bits the umask masked off at creation.\n await fsWriteFile(tempPath, content, {\n signal,\n flag: 'wx',\n ...(existingMode !== undefined ? { mode: existingMode } : {}),\n });\n if (existingMode !== undefined) {\n await fsChmod(tempPath, existingMode);\n }\n // Last cancellation point. The rename IS the commit: once it starts the\n // target may already be replaced, so it is never raced against the\n // signal — a withAbort race would report a finished write as failed, and\n // a client retry would apply it twice (same reasoning as appendFile).\n signal?.throwIfAborted();\n await fsRename(tempPath, validPath);\n } catch (error) {\n try {\n await fsUnlink(tempPath);\n } catch (cleanupError) {\n Logger.warn(\n `Failed to clean up temp file ${tempPath} after write error (${formatUnknownErrorMessage(error)}): ${formatUnknownErrorMessage(cleanupError)}`,\n );\n }\n throw error;\n }\n return { validPath };\n }\n\n /**\n * Append to the existing file (created if missing). Deliberately NOT the\n * writeFile temp+rename dance: the rename would swap in a fresh inode,\n * and the whole point of appending is to extend the file that's already\n * there — inode and mode survive by construction.\n */\n async appendFile(\n filePath: string,\n content: string,\n options: { signal?: AbortSignal | undefined } = {},\n ): Promise<{ validPath: string }> {\n const { signal } = options;\n const validPath = await resolveForWrite(this.pathGuard, filePath);\n // Non-regular targets must be rejected before the open: 'a' on a FIFO\n // (POSIX) blocks until a reader appears, and fsOpen takes no signal, so\n // the call would hang unabortable and pin its concurrency slot. The\n // overwrite path can't hang — it writes a temp file and renames over.\n // resolveForWrite has already resolved any symlink to its target, so the\n // lstat here doubles as the new-vs-existing probe for the cleanup below.\n let existed = true;\n try {\n assertFileStats(validPath, await fsLstat(validPath));\n } catch (error) {\n if (!isNotFoundErrno(error)) throw error;\n existed = false;\n }\n // fs.promises.appendFile takes no signal, so a withAbort race would\n // report failure while the append still lands — a client retry then\n // appends twice. A handle opened with 'a' gets a genuinely abortable\n // FileHandle.writeFile instead, same as the writeFile path. 'a' on a\n // missing target CREATES it, so: check the signal before the open, and\n // if the call then fails after creating the file, remove what it made —\n // an aborted or errored call must not leave a phantom file behind.\n // The pre-open lstat only proves the file was missing a moment ago:\n // 'ax' makes creation exclusive, so a racer that created it in between\n // fails EEXIST, reopens plainly, and never claims cleanup rights over\n // the winner's file.\n signal?.throwIfAborted();\n let created = false;\n let handle: FileHandle;\n if (existed) {\n handle = await fsOpen(validPath, APPEND_NOFOLLOW);\n } else {\n try {\n handle = await fsOpen(validPath, APPEND_CREATE_NOFOLLOW);\n created = true;\n } catch (error) {\n if (!isNodeError(error) || error.code !== 'EEXIST') throw error;\n handle = await fsOpen(validPath, APPEND_NOFOLLOW);\n }\n }\n try {\n signal?.throwIfAborted();\n // A file we just O_EXCL-created is a fresh regular file; only an existing one\n // can have been reached through a swapped ancestor.\n if (!created) await this.#assertFdWithinRoots(handle, filePath);\n // Ceiling: an append is not transactional, so an abort landing\n // mid-write can leave a prefix of the content on disk. Unwiring the\n // signal would trade that for a silently-completed write; a client\n // that retries is double-appending either way, so the abortable\n // write stays.\n await handle.writeFile(content, { signal });\n } catch (error) {\n if (created) {\n await fsUnlink(validPath).catch((unlinkError: unknown) => {\n Logger.warn(\n `Failed to remove newly created ${validPath} after a failed append: ${formatUnknownErrorMessage(unlinkError)}`,\n );\n });\n }\n throw error;\n } finally {\n await handle.close();\n }\n return { validPath };\n }\n\n /**\n * Leading bytes of a file, for MIME sniffing without loading the whole\n * file — an append's result must describe the resulting file (which may be\n * a multi-GB log), not the chunk that was appended.\n */\n async readLeadingSample(filePath: string): Promise<Buffer> {\n await using handle = await this.open(filePath);\n const buffer = Buffer.alloc(MIME_SAMPLE_SIZE);\n const { bytesRead } = await handle.read(buffer, 0, MIME_SAMPLE_SIZE, 0);\n return buffer.subarray(0, bytesRead);\n }\n\n /**\n * Line count for a file on disk, counting newline bytes in fixed 64 KiB\n * chunks so an arbitrarily large file (one append's result metadata) costs\n * bounded memory. NOT FileHandle.readLines: it decodes each physical line\n * whole, so a multi-GB single-line log OOMs the server after the append has\n * already landed — a reported failure invites a retry that appends twice.\n * Matches read.ts's countLines semantics: a trailing newline does not open\n * a new line, an unterminated final line counts, an empty file is 0.\n */\n async countFileLines(filePath: string): Promise<number> {\n await using handle = await this.open(filePath);\n let newlines = 0;\n let lastByte = 0;\n let totalBytes = 0;\n const chunk = Buffer.alloc(LINE_COUNT_CHUNK_BYTES);\n for (;;) {\n const { bytesRead } = await handle.read(chunk, 0, chunk.length, null);\n if (bytesRead === 0) break;\n totalBytes += bytesRead;\n for (let i = 0; i < bytesRead; i++) {\n const byte = chunk[i] ?? 0;\n if (byte === CHAR_LF) newlines++;\n lastByte = byte;\n }\n }\n // totalBytes is the empty-file sentinel: a NUL (0x00) final byte is real\n // content and must count as an unterminated final line, not \"empty\".\n return totalBytes === 0 ? 0 : lastByte === CHAR_LF ? newlines : newlines + 1;\n }\n\n async rm(filePath: string, options?: Parameters<typeof fsRm>[1]): Promise<void> {\n const validPath = await this.pathGuard.validatePathForDelete(filePath);\n await fsRm(validPath, options);\n }\n\n async rmdir(filePath: string, options?: Parameters<typeof fsRmdir>[1]): Promise<void> {\n const validPath = await this.pathGuard.validatePathForDelete(filePath);\n await fsRmdir(validPath, options);\n }\n\n async cp(\n source: string,\n destination: string,\n options?: Parameters<typeof fsCp>[2],\n ): Promise<void> {\n // As in rename: keep the link itself so callers passing verbatimSymlinks\n // actually copy the link rather than a dereferenced target.\n const validSource = await this.pathGuard.validatePathForDelete(source);\n const validDest = await this.pathGuard.validatePathForWrite(destination);\n await fsCp(validSource, validDest, options);\n }\n\n async readFile(filePath: string, spec: ReadSpec): Promise<ReadFileResult> {\n spec.signal?.throwIfAborted();\n const validPath = await this.pathGuard.validateExistingPath(filePath);\n const stats = await withAbort(fsStat(validPath), spec.signal);\n return readFileWithStats(filePath, validPath, stats, spec, (handle) =>\n this.#assertFdWithinRoots(handle, filePath),\n );\n }\n\n async readEditableText(\n filePath: string,\n options?: { signal?: AbortSignal; tool?: string },\n ): Promise<{ validPath: string; content: string; stats: Stats }> {\n const { stats, validPath } = await this.stat(filePath, options);\n const maxSize = getMaxTextFileSize();\n if (stats.size > maxSize) {\n // `tool` labels the TOO_LARGE message so the client sees which tool refused.\n throw new FsError(\n ErrorCode.TOO_LARGE,\n `File too large for ${options?.tool ?? 'edit'} (${stats.size} bytes > ${maxSize} bytes)`,\n filePath,\n { size: stats.size, maxFileSize: maxSize },\n );\n }\n const { content } = await readFileWithStats(\n filePath,\n validPath,\n stats,\n {\n kind: 'full',\n strictUtf8: true,\n ...(options?.signal ? { signal: options.signal } : {}),\n },\n (handle) => this.#assertFdWithinRoots(handle, filePath),\n );\n return { validPath, content, stats };\n }\n\n async readRaw(\n filePath: string,\n options?: { signal?: AbortSignal },\n ): Promise<{ content: Buffer; mimeType: string; isBinary: boolean }> {\n options?.signal?.throwIfAborted();\n const validPath = await this.pathGuard.validateExistingPath(filePath);\n options?.signal?.throwIfAborted();\n const stats = await withAbort(fsStat(validPath), options?.signal);\n assertFileStats(filePath, stats);\n // Enforce size limit before reading to avoid loading large files into memory.\n // Binary detection is best-effort, but this is a hard limit.\n const maxTextFileSize = getMaxTextFileSize();\n if (stats.size > maxTextFileSize) {\n throw createTooLargeError(stats.size, maxTextFileSize, filePath);\n }\n let content: Buffer;\n const handle = await fsOpen(validPath, READ_NOFOLLOW);\n try {\n await this.#assertFdWithinRoots(handle, filePath);\n content = await fsReadFile(handle, options);\n } catch (error) {\n // Node wraps even a TimeoutError signal reason in AbortError.\n if (error instanceof Error && error.name === 'AbortError') {\n options?.signal?.throwIfAborted();\n }\n throw error;\n } finally {\n await handle.close();\n }\n const mimeInfo = detectMimeFromContent(validPath, content);\n return {\n content,\n mimeType: mimeInfo.mimeType,\n isBinary: mimeInfo.kind !== 'text',\n };\n }\n\n async open(filePath: string): Promise<FileHandle> {\n const validPath = await this.pathGuard.validateExistingPath(filePath);\n const handle = await fsOpen(validPath, READ_NOFOLLOW);\n try {\n await this.#assertFdWithinRoots(handle, filePath);\n } catch (error) {\n await handle.close();\n throw error;\n }\n return handle;\n }\n\n // Single resolution + stat: validateExistingPathDetailed resolves the real\n // path (following symlinks, re-checking sensitivity) once, then we stat the\n // resolved target. Replaces the tool-side validateExistingPathDetailed +\n // fs.stat pair that re-validated through the free stat() guard.\n async statDetailed(\n filePath: string,\n options?: { signal?: AbortSignal },\n ): Promise<{ requestedPath: string; isSymlink: boolean; stats: Stats }> {\n const { requestedPath, resolvedPath, isSymlink } =\n await this.pathGuard.validateExistingPathDetailed(filePath);\n const stats = await withAbort(fsStat(resolvedPath), options?.signal);\n return { requestedPath, isSymlink, stats };\n }\n\n async hasChildrenUnchecked(dirPath: string): Promise<boolean> {\n const dir = await fsOpendir(dirPath);\n try {\n const entry = await dir.read();\n return entry !== null;\n } finally {\n await dir.close().catch((closeErr: unknown) => {\n Logger.warn(\n `Failed to close dir handle for ${dirPath}: ${formatUnknownErrorMessage(closeErr)}`,\n );\n });\n }\n }\n}\n\n/**\n * Returns whether `path` exists, treating \"not found\" as absent and logging any\n * other stat failure. Used for TOCTOU re-checks between plan and execute phases.\n */\nexport async function destExists(\n fs: Pick<GuardedFileSystem, 'stat'>,\n path: string,\n label: string,\n): Promise<boolean> {\n try {\n await fs.stat(path);\n return true;\n } catch (err) {\n const missing = isNotFoundErrno(err) || (isFsError(err) && err.code === ErrorCode.NOT_FOUND);\n if (!missing) {\n Logger.warn(`${label}: dest stat failed unexpectedly for \"${path}\": ${String(err)}`);\n }\n return false;\n }\n}\n"]}
1
+ {"version":3,"file":"fs.js","sourceRoot":"","sources":["../../src/core/fs.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomUUID } from 'node:crypto';\nimport { constants, existsSync } from 'node:fs';\nimport type { Dir, Dirent, Stats } from 'node:fs';\nimport type { FileHandle } from 'node:fs/promises';\nimport {\n chmod as fsChmod,\n cp as fsCp,\n lstat as fsLstat,\n mkdir as fsMkdir,\n open as fsOpen,\n opendir as fsOpendir,\n readFile as fsReadFile,\n readlink as fsReadlink,\n rename as fsRename,\n rm as fsRm,\n rmdir as fsRmdir,\n stat as fsStat,\n unlink as fsUnlink,\n writeFile as fsWriteFile,\n} from 'node:fs/promises';\nimport { isAbsolute, join } from 'node:path';\n\nimport { runExclusive, withAbort } from './concurrency.ts';\nimport {\n ErrorCode,\n formatUnknownErrorMessage,\n FsError,\n isFsError,\n isNodeError,\n isNotFoundErrno,\n} from './errors.ts';\nimport { detectMimeFromContent, type FileKind, MIME_SAMPLE_SIZE } from './mime.ts';\nimport { Logger } from './observability.ts';\nimport type { EntryType as FileType } from './path-utils.ts';\nimport { isSamePath, normalizePath, respellSpellingOnlyTarget } from './path-utils.ts';\nimport type { PathGuard } from './path.ts';\nimport type { ReadFileResult, ReadSpec } from './read.ts';\nimport {\n APPEND_CREATE_NOFOLLOW,\n APPEND_NOFOLLOW,\n assertFileStats,\n createTooLargeError,\n O_NOFOLLOW,\n READ_NOFOLLOW,\n readFileWithStats,\n} from './read.ts';\nimport { getMaxTextFileSize } from './util.ts';\n\nexport type { FileType };\nexport type { Stats };\n\n// Directory open for countEntries. O_NOFOLLOW refuses a symlink named as the\n// directory; used only under COUNT_THROUGH_FD, like the re-check it feeds.\nconst DIR_NOFOLLOW = constants.O_RDONLY | constants.O_DIRECTORY | O_NOFOLLOW;\n// The descriptor re-check and the read through /proc/self/fd need Linux with procfs mounted,\n// as the read path's re-check does; elsewhere the count reads the directory by path.\nconst COUNT_THROUGH_FD = process.platform === 'linux' && existsSync('/proc/self/fd');\n\n// ─── Domain primitives ────────────────────────────────────────────────────────\n\nconst CHAR_LF = 10;\n\nconst LINE_COUNT_CHUNK_BYTES = 64 * 1024;\n\nexport class GuardedFileSystem {\n readonly pathGuard: PathGuard;\n\n constructor(pathGuard: PathGuard) {\n this.pathGuard = pathGuard;\n }\n\n /**\n * O_NOFOLLOW only guards the final component; an ANCESTOR directory swapped\n * for a symlink after validation still passes. Re-read the open descriptor's\n * real path (Linux `/proc/self/fd`; skipped elsewhere) and require it to stay\n * inside the roots. Run before any byte is read or written through `handle`.\n * Resolves to the normalized real path checked, or undefined when /proc is unreadable.\n */\n async #assertFdWithinRoots(\n handle: FileHandle,\n requestedPath: string,\n ): Promise<string | undefined> {\n let fdPath: string;\n try {\n fdPath = await fsReadlink(`/proc/self/fd/${String(handle.fd)}`);\n } catch {\n return undefined;\n }\n // A replaced target reads back as \"<path> (deleted)\".\n const inRoots =\n isAbsolute(fdPath) &&\n !fdPath.endsWith(' (deleted)') &&\n this.pathGuard.allowedRootContaining(normalizePath(fdPath)) !== undefined &&\n !this.pathGuard.isSensitive(normalizePath(fdPath));\n if (!inRoots) {\n throw new FsError(\n ErrorCode.ACCESS_DENIED,\n 'Path changed under validation and now resolves outside the allowed roots (TOCTOU).',\n requestedPath,\n );\n }\n return normalizePath(fdPath);\n }\n\n /** The one read open: O_NOFOLLOW, then the descriptor re-check above. */\n async #openVerified(\n validPath: string,\n requestedPath: string,\n signal?: AbortSignal,\n ): Promise<FileHandle> {\n const opening = fsOpen(validPath, READ_NOFOLLOW);\n let handle: FileHandle;\n try {\n handle = await withAbort(opening, signal);\n } catch (error) {\n // An open the signal abandoned still lands; close it then.\n void opening\n .then(\n (late) => late.close(),\n () => undefined,\n )\n .catch((closeErr: unknown) => {\n Logger.warn(\n `Failed to close file handle for ${validPath} after abort: ${formatUnknownErrorMessage(closeErr)}`,\n );\n });\n throw error;\n }\n try {\n await this.#assertFdWithinRoots(handle, requestedPath);\n return handle;\n } catch (error) {\n await handle.close();\n throw error;\n }\n }\n\n /**\n * Waits for `pending` until `signal` fires. A result that lands after the abort goes to\n * `release` without anyone waiting on it, so a stalled open or read never holds the caller.\n */\n async #abandonOnAbort<T>(\n pending: Promise<T>,\n signal: AbortSignal | undefined,\n release: (late: T) => Promise<unknown>,\n label: string,\n ): Promise<T> {\n try {\n return await withAbort(pending, signal);\n } catch (error) {\n void pending\n .then(release, () => undefined)\n .catch((releaseErr: unknown) => {\n Logger.warn(\n `Failed to release ${label} after abort: ${formatUnknownErrorMessage(releaseErr)}`,\n );\n });\n throw error;\n }\n }\n\n /**\n * Serialize a read-modify-write on the resolved write target, so two writers\n * of one file (any spelling, including via a symlinked parent) cannot both\n * read the same base and have the last rename win. `writeFile`/`appendFile`\n * do not take the lock, so `fn` may call them.\n */\n async lockForWrite<T>(filePath: string, fn: (resolvedTarget: string) => Promise<T>): Promise<T> {\n const key = await this.pathGuard.validatePathForWrite(filePath);\n return runExclusive(key, () => fn(key));\n }\n\n async stat(\n filePath: string,\n options?: { signal?: AbortSignal | undefined },\n ): Promise<{ stats: Stats; validPath: string }> {\n const validPath = await this.pathGuard.validateExistingPath(filePath);\n const stats = await withAbort(fsStat(validPath), options?.signal);\n return { stats, validPath };\n }\n\n async lstat(\n filePath: string,\n options?: { signal?: AbortSignal | undefined },\n ): Promise<{ stats: Stats; validPath: string }> {\n const validPath = await this.pathGuard.validatePathForDelete(filePath);\n const stats = await withAbort(fsLstat(validPath), options?.signal);\n return { stats, validPath };\n }\n\n async readlink(\n filePath: string,\n options?: Parameters<typeof fsReadlink>[1],\n ): Promise<{ linkString: string; validPath: string }> {\n // validateExistingPath resolves through the symlink, so readlink would always\n // be handed the final target — a regular file — and fail EINVAL. This guard\n // keeps the link itself while still enforcing containment and the denylist.\n const validPath = await this.pathGuard.validatePathForDelete(filePath);\n const raw = await fsReadlink(validPath, options);\n const linkString = Buffer.isBuffer(raw) ? raw.toString('utf-8') : raw;\n return { linkString, validPath };\n }\n\n async mkdir(filePath: string, options?: Parameters<typeof fsMkdir>[1]): Promise<void> {\n const validPath = await this.pathGuard.validatePathForWrite(filePath);\n await fsMkdir(validPath, options);\n }\n\n async rename(oldPath: string, newPath: string): Promise<void> {\n // Not validateExistingPath: that resolves through a symlink, so renaming a\n // link would rename its target and leave the link dangling.\n const validOld = await this.pathGuard.validatePathForDelete(oldPath);\n let validNew = await this.pathGuard.validatePathForWrite(newPath);\n // A new path naming the old entry itself, in another case or Unicode\n // spelling, renames that entry. Probed without following a final link:\n // validatePathForWrite resolved a link to its target, and renaming onto\n // that would replace the target with the link.\n const sameEntry = await this.pathGuard.validatePathForDelete(newPath).then(\n (entry) => isSamePath(entry, validOld),\n () => false,\n );\n if (sameEntry || validOld === validNew) {\n validNew = respellSpellingOnlyTarget(newPath, sameEntry ? validOld : validNew);\n }\n await fsRename(validOld, validNew);\n }\n\n async writeFile(\n filePath: string,\n content: string,\n options: { signal?: AbortSignal | undefined } = {},\n ): Promise<{ validPath: string }> {\n const { signal } = options;\n const validPath = await this.pathGuard.validatePathForWrite(filePath);\n\n const tempSuffix = randomUUID().replace(/-/g, '').slice(0, 12);\n const tempPath = `${validPath}.${tempSuffix}.tmp`;\n\n // The rename below swaps in the temp file's inode, so the target would\n // inherit fsWriteFile's default 0o666 & ~umask — silently widening a 0600\n // file to 0644 on every write. Carry the existing mode across instead.\n let existingMode: number | undefined;\n try {\n existingMode = (await fsStat(validPath)).mode & 0o777;\n } catch (error) {\n // ENOENT is the normal new-file case: the default mode is correct there.\n // Anything else (EACCES, EIO) means the mode about to be overwritten could\n // not be read, and the write will silently widen the file — say so rather\n // than swallowing it.\n if (!isNotFoundErrno(error)) {\n Logger.warn(\n `atomicWriteFile: cannot read the existing mode of ${validPath}; the write will use the default mode: ${formatUnknownErrorMessage(error)}`,\n );\n }\n }\n\n try {\n signal?.throwIfAborted();\n // Create the temp file already narrowed to the target's mode: a 0600\n // file's new bytes must never sit at 0644 while the write is in flight.\n // `wx` fails on a name collision instead of overwriting; the chmod below\n // still restores bits the umask masked off at creation.\n await fsWriteFile(tempPath, content, {\n signal,\n flag: 'wx',\n ...(existingMode !== undefined ? { mode: existingMode } : {}),\n });\n if (existingMode !== undefined) {\n await fsChmod(tempPath, existingMode);\n }\n // Last cancellation point. The rename IS the commit: once it starts the\n // target may already be replaced, so it is never raced against the\n // signal — a withAbort race would report a finished write as failed, and\n // a client retry would apply it twice (same reasoning as appendFile).\n signal?.throwIfAborted();\n await fsRename(tempPath, validPath);\n } catch (error) {\n try {\n await fsUnlink(tempPath);\n } catch (cleanupError) {\n Logger.warn(\n `Failed to clean up temp file ${tempPath} after write error (${formatUnknownErrorMessage(error)}): ${formatUnknownErrorMessage(cleanupError)}`,\n );\n }\n throw error;\n }\n return { validPath };\n }\n\n /**\n * Append to the existing file (created if missing). Deliberately NOT the\n * writeFile temp+rename dance: the rename would swap in a fresh inode,\n * and the whole point of appending is to extend the file that's already\n * there — inode and mode survive by construction.\n */\n async appendFile(\n filePath: string,\n content: string,\n options: { signal?: AbortSignal | undefined } = {},\n ): Promise<{ validPath: string }> {\n const { signal } = options;\n const validPath = await this.pathGuard.validatePathForWrite(filePath);\n // Non-regular targets must be rejected before the open: 'a' on a FIFO\n // (POSIX) blocks until a reader appears, and fsOpen takes no signal, so\n // the call would hang unabortable and pin its concurrency slot. The\n // overwrite path can't hang — it writes a temp file and renames over.\n // resolveForWrite has already resolved any symlink to its target, so the\n // lstat here doubles as the new-vs-existing probe for the cleanup below.\n let existed = true;\n try {\n assertFileStats(validPath, await fsLstat(validPath));\n } catch (error) {\n if (!isNotFoundErrno(error)) throw error;\n existed = false;\n }\n // fs.promises.appendFile takes no signal, so a withAbort race would\n // report failure while the append still lands — a client retry then\n // appends twice. A handle opened with 'a' gets a genuinely abortable\n // FileHandle.writeFile instead, same as the writeFile path. 'a' on a\n // missing target CREATES it, so: check the signal before the open, and\n // if the call then fails after creating the file, remove what it made —\n // an aborted or errored call must not leave a phantom file behind.\n // The pre-open lstat only proves the file was missing a moment ago:\n // 'ax' makes creation exclusive, so a racer that created it in between\n // fails EEXIST, reopens plainly, and never claims cleanup rights over\n // the winner's file.\n signal?.throwIfAborted();\n let created = false;\n let handle: FileHandle;\n if (existed) {\n handle = await fsOpen(validPath, APPEND_NOFOLLOW);\n } else {\n try {\n handle = await fsOpen(validPath, APPEND_CREATE_NOFOLLOW);\n created = true;\n } catch (error) {\n if (!isNodeError(error) || error.code !== 'EEXIST') throw error;\n handle = await fsOpen(validPath, APPEND_NOFOLLOW);\n }\n }\n try {\n signal?.throwIfAborted();\n // A file we just O_EXCL-created is a fresh regular file; only an existing one\n // can have been reached through a swapped ancestor.\n if (!created) await this.#assertFdWithinRoots(handle, filePath);\n // Ceiling: an append is not transactional, so an abort landing\n // mid-write can leave a prefix of the content on disk. Unwiring the\n // signal would trade that for a silently-completed write; a client\n // that retries is double-appending either way, so the abortable\n // write stays.\n await handle.writeFile(content, { signal });\n } catch (error) {\n if (created) {\n await fsUnlink(validPath).catch((unlinkError: unknown) => {\n Logger.warn(\n `Failed to remove newly created ${validPath} after a failed append: ${formatUnknownErrorMessage(unlinkError)}`,\n );\n });\n }\n throw error;\n } finally {\n await handle.close();\n }\n return { validPath };\n }\n\n /**\n * Leading bytes of a file, for MIME sniffing without loading the whole\n * file — an append's result must describe the resulting file (which may be\n * a multi-GB log), not the chunk that was appended.\n */\n async readLeadingSample(filePath: string): Promise<Buffer> {\n await using handle = await this.open(filePath);\n const buffer = Buffer.alloc(MIME_SAMPLE_SIZE);\n const { bytesRead } = await handle.read(buffer, 0, MIME_SAMPLE_SIZE, 0);\n return buffer.subarray(0, bytesRead);\n }\n\n /**\n * Line count for a file on disk, counting newline bytes in fixed 64 KiB\n * chunks so an arbitrarily large file (one append's result metadata) costs\n * bounded memory. NOT FileHandle.readLines: it decodes each physical line\n * whole, so a multi-GB single-line log OOMs the server after the append has\n * already landed — a reported failure invites a retry that appends twice.\n * Matches read.ts's countLines semantics: a trailing newline does not open\n * a new line, an unterminated final line counts, an empty file is 0.\n */\n async countFileLines(filePath: string): Promise<number> {\n await using handle = await this.open(filePath);\n let newlines = 0;\n let lastByte = 0;\n let totalBytes = 0;\n const chunk = Buffer.alloc(LINE_COUNT_CHUNK_BYTES);\n for (;;) {\n const { bytesRead } = await handle.read(chunk, 0, chunk.length, null);\n if (bytesRead === 0) break;\n totalBytes += bytesRead;\n for (let i = 0; i < bytesRead; i++) {\n const byte = chunk[i] ?? 0;\n if (byte === CHAR_LF) newlines++;\n lastByte = byte;\n }\n }\n // totalBytes is the empty-file sentinel: a NUL (0x00) final byte is real\n // content and must count as an unterminated final line, not \"empty\".\n return totalBytes === 0 ? 0 : lastByte === CHAR_LF ? newlines : newlines + 1;\n }\n\n async rm(filePath: string, options?: Parameters<typeof fsRm>[1]): Promise<void> {\n const validPath = await this.pathGuard.validatePathForDelete(filePath);\n await fsRm(validPath, options);\n }\n\n async rmdir(filePath: string, options?: Parameters<typeof fsRmdir>[1]): Promise<void> {\n const validPath = await this.pathGuard.validatePathForDelete(filePath);\n await fsRmdir(validPath, options);\n }\n\n async cp(\n source: string,\n destination: string,\n options?: Parameters<typeof fsCp>[2],\n ): Promise<void> {\n // As in rename: keep the link itself so callers passing verbatimSymlinks\n // actually copy the link rather than a dereferenced target.\n const validSource = await this.pathGuard.validatePathForDelete(source);\n const validDest = await this.pathGuard.validatePathForWrite(destination);\n await fsCp(validSource, validDest, options);\n }\n\n async readFile(filePath: string, spec: ReadSpec): Promise<ReadFileResult> {\n spec.signal?.throwIfAborted();\n const validPath = await this.pathGuard.validateExistingPath(filePath);\n const stats = await withAbort(fsStat(validPath), spec.signal);\n return readFileWithStats(filePath, validPath, stats, spec, () =>\n this.#openVerified(validPath, filePath, spec.signal),\n );\n }\n\n async readEditableText(\n filePath: string,\n options?: { signal?: AbortSignal; tool?: string },\n ): Promise<{ validPath: string; content: string; stats: Stats }> {\n const { stats, validPath } = await this.stat(filePath, options);\n const maxSize = getMaxTextFileSize();\n if (stats.size > maxSize) {\n // `tool` labels the TOO_LARGE message so the client sees which tool refused.\n throw new FsError(\n ErrorCode.TOO_LARGE,\n `File too large for ${options?.tool ?? 'edit'} (${stats.size} bytes > ${maxSize} bytes)`,\n filePath,\n { size: stats.size, maxFileSize: maxSize },\n );\n }\n const { content } = await readFileWithStats(\n filePath,\n validPath,\n stats,\n {\n kind: 'full',\n strictUtf8: true,\n ...(options?.signal ? { signal: options.signal } : {}),\n },\n () => this.#openVerified(validPath, filePath, options?.signal),\n );\n return { validPath, content, stats };\n }\n\n async readRaw(\n filePath: string,\n options?: { signal?: AbortSignal },\n ): Promise<{\n content: Buffer;\n mimeType: string;\n kind: FileKind;\n isBinary: boolean;\n validPath: string;\n modified: string;\n }> {\n options?.signal?.throwIfAborted();\n const validPath = await this.pathGuard.validateExistingPath(filePath);\n options?.signal?.throwIfAborted();\n const stats = await withAbort(fsStat(validPath), options?.signal);\n assertFileStats(filePath, stats);\n // Enforce size limit before reading to avoid loading large files into memory.\n // Binary detection is best-effort, but this is a hard limit.\n const maxTextFileSize = getMaxTextFileSize();\n if (stats.size > maxTextFileSize) {\n throw createTooLargeError(stats.size, maxTextFileSize, filePath);\n }\n let content: Buffer;\n await using handle = await this.#openVerified(validPath, filePath);\n try {\n content = await fsReadFile(handle, options);\n } catch (error) {\n // Node wraps even a TimeoutError signal reason in AbortError.\n if (error instanceof Error && error.name === 'AbortError') {\n options?.signal?.throwIfAborted();\n }\n throw error;\n }\n const mimeInfo = detectMimeFromContent(validPath, content);\n return {\n content,\n mimeType: mimeInfo.mimeType,\n kind: mimeInfo.kind,\n isBinary: mimeInfo.kind !== 'text',\n validPath,\n modified: stats.mtime.toISOString(),\n };\n }\n\n async open(filePath: string): Promise<FileHandle> {\n return this.#openVerified(await this.pathGuard.validateExistingPath(filePath), filePath);\n }\n\n // Single resolution + stat: validateExistingPathDetailed resolves the real\n // path (following symlinks, re-checking sensitivity) once, then we stat the\n // resolved target. Replaces the tool-side validateExistingPathDetailed +\n // fs.stat pair that re-validated through the free stat() guard.\n async statDetailed(\n filePath: string,\n options?: { signal?: AbortSignal },\n ): Promise<{ requestedPath: string; isSymlink: boolean; stats: Stats }> {\n const { requestedPath, resolvedPath, isSymlink } =\n await this.pathGuard.validateExistingPathDetailed(filePath);\n const stats = await withAbort(fsStat(resolvedPath), options?.signal);\n return { requestedPath, isSymlink, stats };\n }\n\n async hasChildrenUnchecked(dirPath: string): Promise<boolean> {\n const dir = await fsOpendir(dirPath);\n try {\n const entry = await dir.read();\n return entry !== null;\n } finally {\n await dir.close().catch((closeErr: unknown) => {\n Logger.warn(\n `Failed to close dir handle for ${dirPath}: ${formatUnknownErrorMessage(closeErr)}`,\n );\n });\n }\n }\n\n /**\n * Direct children of a directory, leaving out entries the guard treats as\n * sensitive so the count never reveals what listings hide. Hidden and\n * ignored entries count: this answers \"is it empty?\", not \"what would list show?\".\n * A symlinked child counts only when list would show it. The path validation, open, opendir,\n * each read and each link check race the signal, so a stalled mount ends the call at its\n * deadline.\n */\n async countEntries(dirPath: string, options?: { signal?: AbortSignal }): Promise<number> {\n const signal = options?.signal;\n // Refuse an already-cancelled call before any validation or I/O.\n signal?.throwIfAborted();\n // A late validation result is a plain object, so a stalled realpath needs no release.\n const { resolvedPath } = await withAbort(\n this.pathGuard.validateExistingPathDetailed(dirPath),\n signal,\n );\n // With procfs (COUNT_THROUGH_FD), open the directory itself with O_NOFOLLOW and check where\n // the open descriptor really is, so a directory or ancestor swapped for a symlink after\n // validation is refused. Then read entries through the descriptor, which a later swap cannot\n // redirect.\n const handle = COUNT_THROUGH_FD\n ? await this.#abandonOnAbort(\n fsOpen(resolvedPath, DIR_NOFOLLOW),\n signal,\n (late) => late.close(),\n `directory descriptor for ${resolvedPath}`,\n )\n : undefined;\n let dir: Dir | undefined;\n let pendingRead: Promise<Dirent | null> | undefined;\n try {\n const openedPath = handle ? await this.#assertFdWithinRoots(handle, dirPath) : undefined;\n // An in-root swap passes the roots check, but the children would be filtered under the\n // validated directory's spelling, not the one actually opened.\n if (openedPath !== undefined && !isSamePath(openedPath, resolvedPath)) {\n throw new FsError(\n ErrorCode.ACCESS_DENIED,\n 'Path changed under validation and now resolves to another directory (TOCTOU).',\n dirPath,\n );\n }\n const readPath = handle ? `/proc/self/fd/${String(handle.fd)}` : resolvedPath;\n dir = await this.#abandonOnAbort(\n fsOpendir(readPath),\n signal,\n (late) => late.close(),\n `directory stream for ${resolvedPath}`,\n );\n let count = 0;\n for (;;) {\n // Each read races the signal, so a read that stalls ends the call at its deadline.\n const read = dir.read();\n pendingRead = read;\n const entry = await withAbort(read, signal);\n pendingRead = undefined;\n if (entry === null) break;\n if (this.pathGuard.isSensitive(join(dirPath, entry.name))) continue;\n // list walks the real directory, so it checks every child, links\n // included, under its real spelling: a deny rule naming the real\n // directory hides the child under an alias too.\n if (this.pathGuard.isSensitive(join(resolvedPath, entry.name))) continue;\n // A link counts only when list would show it: same rule, same error policy.\n if (\n entry.isSymbolicLink() &&\n !(await withAbort(\n this.pathGuard.isEntryAccessible(join(resolvedPath, entry.name)),\n signal,\n ))\n ) {\n continue;\n }\n count += 1;\n }\n // A deadline that fired during the last read still ends the call, even with no entries left.\n signal?.throwIfAborted();\n return count;\n } finally {\n if (dir) {\n const stream = dir;\n if (pendingRead) {\n // close() queues behind a read still in flight, so awaiting it would hold the TIMEOUT\n // response on a stalled mount. Close once that read settles, without waiting for it.\n void pendingRead\n .then(\n () => stream.close(),\n () => stream.close(),\n )\n .catch((closeErr: unknown) => {\n Logger.warn(\n `Failed to close dir handle for ${resolvedPath} after abort: ${formatUnknownErrorMessage(closeErr)}`,\n );\n });\n } else {\n await stream.close().catch((closeErr: unknown) => {\n Logger.warn(\n `Failed to close dir handle for ${resolvedPath}: ${formatUnknownErrorMessage(closeErr)}`,\n );\n });\n }\n }\n // A failed close must not replace the error the count threw, so it is logged, not thrown.\n await handle?.close().catch((closeErr: unknown) => {\n Logger.warn(\n `Failed to close dir descriptor for ${resolvedPath}: ${formatUnknownErrorMessage(closeErr)}`,\n );\n });\n }\n }\n}\n\n/**\n * Returns whether `path` exists, treating \"not found\" as absent and logging any\n * other stat failure. Used for TOCTOU re-checks between plan and execute phases.\n */\nexport async function destExists(\n fs: Pick<GuardedFileSystem, 'stat'>,\n path: string,\n label: string,\n): Promise<boolean> {\n try {\n await fs.stat(path);\n return true;\n } catch (err) {\n const missing = isNotFoundErrno(err) || (isFsError(err) && err.code === ErrorCode.NOT_FOUND);\n if (!missing) {\n Logger.warn(`${label}: dest stat failed unexpectedly for \"${path}\": ${String(err)}`);\n }\n return false;\n }\n}\n"]}
@@ -7,6 +7,14 @@ import type { DirentLike } from './path-utils.ts';
7
7
  export declare const MAX_BRACE_ALTERNATIVES = 1024;
8
8
  /** Bound synchronous glob compilation when checking candidate traversal segments. */
9
9
  export declare const MAX_DOTDOT_CHECK_CHARS = 4096;
10
+ /**
11
+ * fs.glob tests each name against a regex compiled from the segment,
12
+ * synchronously. Every run of `*` and every extglob group (`?(`, `+(`, `@(`,
13
+ * `!(`, `*(`) adds a degree of backtracking: on a 255-character name three cost
14
+ * under 100 ms, four about 1 s, and five do not finish; a chain of `?(a|aa)`
15
+ * groups grows the same way.
16
+ */
17
+ export declare const MAX_SEGMENT_WILDCARDS = 3;
10
18
  export declare function isSafeGlobSyntax(pattern: string): boolean;
11
19
  export interface GlobEntry {
12
20
  path: string;
@@ -1 +1 @@
1
- {"version":3,"file":"glob.d.ts","sourceRoot":"","sources":["../../src/core/glob.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAIlD;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,OAAO,CAAC;AAE3C,qFAAqF;AACrF,eAAO,MAAM,sBAAsB,OAAO,CAAC;AAoH3C,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAwBzD;AAqND,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,UAAU,CAAC;CACpB;AAED,MAAM,WAAW,kBAAkB;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,kFAAkF;IAClF,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,4EAA4E;IAC5E,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAwPD,wBAAuB,WAAW,CAAC,OAAO,EAAE,kBAAkB,GAAG,cAAc,CAAC,SAAS,CAAC,CAyBzF"}
1
+ {"version":3,"file":"glob.d.ts","sourceRoot":"","sources":["../../src/core/glob.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAIlD;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,OAAO,CAAC;AAE3C,qFAAqF;AACrF,eAAO,MAAM,sBAAsB,OAAO,CAAC;AAE3C;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB,IAAI,CAAC;AAqKvC,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAyBzD;AA8ND,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,UAAU,CAAC;CACpB;AAED,MAAM,WAAW,kBAAkB;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,kFAAkF;IAClF,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,4EAA4E;IAC5E,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAwPD,wBAAuB,WAAW,CAAC,OAAO,EAAE,kBAAkB,GAAG,cAAc,CAAC,SAAS,CAAC,CAyBzF"}