@j0hanz/filesystem-mcp 2.7.6 → 2.8.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 (108) hide show
  1. package/dist/cli.d.ts +1 -9
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js.map +1 -1
  4. package/dist/core/errors.d.ts +4 -3
  5. package/dist/core/errors.d.ts.map +1 -1
  6. package/dist/core/errors.js +5 -16
  7. package/dist/core/errors.js.map +1 -1
  8. package/dist/core/fs.d.ts +11 -11
  9. package/dist/core/fs.d.ts.map +1 -1
  10. package/dist/core/fs.js +108 -43
  11. package/dist/core/fs.js.map +1 -1
  12. package/dist/core/glob.d.ts +8 -0
  13. package/dist/core/glob.d.ts.map +1 -1
  14. package/dist/core/glob.js +192 -29
  15. package/dist/core/glob.js.map +1 -1
  16. package/dist/core/input-required.d.ts +12 -0
  17. package/dist/core/input-required.d.ts.map +1 -1
  18. package/dist/core/input-required.js +7 -2
  19. package/dist/core/input-required.js.map +1 -1
  20. package/dist/core/observability.d.ts +2 -0
  21. package/dist/core/observability.d.ts.map +1 -1
  22. package/dist/core/observability.js +7 -0
  23. package/dist/core/observability.js.map +1 -1
  24. package/dist/core/path-completer.d.ts.map +1 -1
  25. package/dist/core/path-completer.js +11 -30
  26. package/dist/core/path-completer.js.map +1 -1
  27. package/dist/core/path.d.ts +0 -2
  28. package/dist/core/path.d.ts.map +1 -1
  29. package/dist/core/path.js +12 -29
  30. package/dist/core/path.js.map +1 -1
  31. package/dist/core/read.d.ts +1 -1
  32. package/dist/core/read.d.ts.map +1 -1
  33. package/dist/core/read.js +13 -14
  34. package/dist/core/read.js.map +1 -1
  35. package/dist/core/schema.d.ts +6 -1
  36. package/dist/core/schema.d.ts.map +1 -1
  37. package/dist/core/schema.js +17 -3
  38. package/dist/core/schema.js.map +1 -1
  39. package/dist/core/search.d.ts.map +1 -1
  40. package/dist/core/search.js +104 -51
  41. package/dist/core/search.js.map +1 -1
  42. package/dist/core/store.d.ts +36 -1
  43. package/dist/core/store.d.ts.map +1 -1
  44. package/dist/core/store.js +71 -6
  45. package/dist/core/store.js.map +1 -1
  46. package/dist/core/util.d.ts +34 -5
  47. package/dist/core/util.d.ts.map +1 -1
  48. package/dist/core/util.js +59 -5
  49. package/dist/core/util.js.map +1 -1
  50. package/dist/index.js +5 -16
  51. package/dist/index.js.map +1 -1
  52. package/dist/resources.d.ts +5 -1
  53. package/dist/resources.d.ts.map +1 -1
  54. package/dist/resources.js +45 -22
  55. package/dist/resources.js.map +1 -1
  56. package/dist/server.d.ts +9 -6
  57. package/dist/server.d.ts.map +1 -1
  58. package/dist/server.js +7 -8
  59. package/dist/server.js.map +1 -1
  60. package/dist/tools/create.d.ts.map +1 -1
  61. package/dist/tools/create.js +8 -19
  62. package/dist/tools/create.js.map +1 -1
  63. package/dist/tools/define.d.ts +8 -7
  64. package/dist/tools/define.d.ts.map +1 -1
  65. package/dist/tools/define.js +48 -16
  66. package/dist/tools/define.js.map +1 -1
  67. package/dist/tools/delete.d.ts.map +1 -1
  68. package/dist/tools/delete.js +39 -21
  69. package/dist/tools/delete.js.map +1 -1
  70. package/dist/tools/edit.d.ts.map +1 -1
  71. package/dist/tools/edit.js +24 -39
  72. package/dist/tools/edit.js.map +1 -1
  73. package/dist/tools/find-files.d.ts.map +1 -1
  74. package/dist/tools/find-files.js +49 -60
  75. package/dist/tools/find-files.js.map +1 -1
  76. package/dist/tools/index.d.ts +2 -11
  77. package/dist/tools/index.d.ts.map +1 -1
  78. package/dist/tools/index.js +1 -9
  79. package/dist/tools/index.js.map +1 -1
  80. package/dist/tools/list.d.ts.map +1 -1
  81. package/dist/tools/list.js +59 -69
  82. package/dist/tools/list.js.map +1 -1
  83. package/dist/tools/move.d.ts.map +1 -1
  84. package/dist/tools/move.js +5 -1
  85. package/dist/tools/move.js.map +1 -1
  86. package/dist/tools/patch.d.ts.map +1 -1
  87. package/dist/tools/patch.js +3 -6
  88. package/dist/tools/patch.js.map +1 -1
  89. package/dist/tools/read.d.ts.map +1 -1
  90. package/dist/tools/read.js +31 -40
  91. package/dist/tools/read.js.map +1 -1
  92. package/dist/tools/replace-text.d.ts.map +1 -1
  93. package/dist/tools/replace-text.js +23 -32
  94. package/dist/tools/replace-text.js.map +1 -1
  95. package/dist/tools/search-text.d.ts.map +1 -1
  96. package/dist/tools/search-text.js +48 -64
  97. package/dist/tools/search-text.js.map +1 -1
  98. package/dist/transport/http.js +1 -1
  99. package/dist/transport/http.js.map +1 -1
  100. package/dist/transport/shared.d.ts +7 -5
  101. package/dist/transport/shared.d.ts.map +1 -1
  102. package/dist/transport/shared.js +8 -2
  103. package/dist/transport/shared.js.map +1 -1
  104. package/dist/transport/stdio.d.ts +4 -1
  105. package/dist/transport/stdio.d.ts.map +1 -1
  106. package/dist/transport/stdio.js +45 -12
  107. package/dist/transport/stdio.js.map +1 -1
  108. package/package.json +3 -2
@@ -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 } 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\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 */\nexport async function countFileLines(filePath: string): Promise<number> {\n const handle = await fsOpen(filePath, READ_NOFOLLOW);\n try {\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 } finally {\n await handle.close();\n }\n}\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, size: number): Promise<Buffer> {\n const { validPath } = await this.stat(filePath);\n const handle = await fsOpen(validPath, READ_NOFOLLOW);\n try {\n const buffer = Buffer.alloc(size);\n const { bytesRead } = await handle.read(buffer, 0, size, 0);\n return buffer.subarray(0, bytesRead);\n } finally {\n await handle.close();\n }\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 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,4 +1,12 @@
1
1
  import type { DirentLike } from './path-utils.ts';
2
+ /**
3
+ * More brace alternatives than this is not a file filter. fs.glob's matcher
4
+ * dedupes its alternatives pairwise, synchronously: `{1..100000}` pins the
5
+ * event loop for most of a minute, where 1024 costs tens of milliseconds.
6
+ */
7
+ export declare const MAX_BRACE_ALTERNATIVES = 1024;
8
+ /** Bound synchronous glob compilation when checking candidate traversal segments. */
9
+ export declare const MAX_DOTDOT_CHECK_CHARS = 4096;
2
10
  export declare function isSafeGlobSyntax(pattern: string): boolean;
3
11
  export interface GlobEntry {
4
12
  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;AAOlD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAkBzD;AA8MD,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;AA6ND,wBAAuB,WAAW,CAAC,OAAO,EAAE,kBAAkB,GAAG,cAAc,CAAC,SAAS,CAAC,CAwBzF"}
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"}
package/dist/core/glob.js CHANGED
@@ -1,29 +1,155 @@
1
1
  import { glob as fsGlob, readFile as fsReadFile, lstat } from 'node:fs/promises';
2
- import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
2
+ import { basename, dirname, join, matchesGlob, relative, resolve, win32 } from 'node:path';
3
3
  import ignore from 'ignore';
4
4
  import { processInParallel } from "./concurrency.js";
5
5
  import { formatUnknownErrorMessage } from "./errors.js";
6
6
  import { Logger } from "./observability.js";
7
- import { isPathInsideDirectory, isWindowsDriveRelativePath, toPosixPath } from "./path-utils.js";
7
+ import { IS_WINDOWS, isPathInsideDirectory, toPosixPath } from "./path-utils.js";
8
8
  import { MIB } from "./util.js";
9
- /** `..` bounded on both sides by a separator, a brace delimiter, or the pattern edge. */
10
- const TRAVERSAL_DOTDOT_RE = /(?:^|[/\\{,])\.\.(?=[/\\},]|$)/u;
9
+ /**
10
+ * More brace alternatives than this is not a file filter. fs.glob's matcher
11
+ * dedupes its alternatives pairwise, synchronously: `{1..100000}` pins the
12
+ * event loop for most of a minute, where 1024 costs tens of milliseconds.
13
+ */
14
+ export const MAX_BRACE_ALTERNATIVES = 1024;
15
+ /** Bound synchronous glob compilation when checking candidate traversal segments. */
16
+ export const MAX_DOTDOT_CHECK_CHARS = 4096;
17
+ // The range bodies fs.glob's brace expansion recognises, as it writes them.
18
+ const NUMERIC_RANGE_RE = /^-?\d+\.\.-?\d+(?:\.\.-?\d+)?$/u;
19
+ const ALPHA_RANGE_RE = /^[a-zA-Z]\.\.[a-zA-Z](?:\.\.-?\d+)?$/u;
20
+ /** The values a range body (`x..y`, `x..y..step`) expands to, or `undefined` past the cap. */
21
+ function rangeValues(body) {
22
+ const alpha = ALPHA_RANGE_RE.test(body);
23
+ const [from = '', to = '', step = '1'] = body.split('..');
24
+ const bound = (end) => (alpha ? end.charCodeAt(0) : Number.parseInt(end, 10));
25
+ const start = bound(from);
26
+ const stop = bound(to);
27
+ const stride = Math.max(Math.abs(Number.parseInt(step, 10)), 1);
28
+ // Past 2^53 a step no longer moves the number (2^53 + 1 === 2^53), and past
29
+ // Number.MAX_VALUE it reads as Infinity: either way the loop below would
30
+ // never end. Refuse them like any other long range.
31
+ if (!Number.isSafeInteger(start) || !Number.isSafeInteger(stop))
32
+ return undefined;
33
+ if (Math.floor(Math.abs(stop - start) / stride) >= MAX_BRACE_ALTERNATIVES)
34
+ return undefined;
35
+ const values = [];
36
+ const direction = stop < start ? -1 : 1;
37
+ for (let i = start; (stop - i) * direction >= 0; i += stride * direction) {
38
+ // The engine drops a `\` a letter range passes through (`{Z..a}`), and
39
+ // digits can never spell a root or a `..`, so their padding is moot.
40
+ const char = String.fromCharCode(i);
41
+ values.push(alpha ? (char === '\\' ? '' : char) : String(i));
42
+ }
43
+ return values;
44
+ }
45
+ /**
46
+ * The leftmost `{…}` that fs.glob would expand: a comma list or a range.
47
+ * The expansion is textual, as fs.glob's is: a brace inside `[…]` still
48
+ * counts (unlike SensitiveMatcher's own glob dialect in sensitive.ts).
49
+ */
50
+ function firstBraceSet(pattern) {
51
+ const open = [];
52
+ let first;
53
+ for (let i = 0; i < pattern.length; i++) {
54
+ const char = pattern[i];
55
+ if (char === '{')
56
+ open.push({ at: i, commas: [] });
57
+ else if (char === ',')
58
+ open.at(-1)?.commas.push(i);
59
+ else if (char === '}') {
60
+ const set = open.pop();
61
+ if (!set || (first && first.at < set.at))
62
+ continue;
63
+ const body = pattern.slice(set.at + 1, i);
64
+ if (set.commas.length > 0 || NUMERIC_RANGE_RE.test(body) || ALPHA_RANGE_RE.test(body)) {
65
+ first = { ...set, close: i };
66
+ }
67
+ }
68
+ }
69
+ if (!first)
70
+ return undefined;
71
+ let alternatives;
72
+ if (first.commas.length > 0) {
73
+ alternatives = [];
74
+ let start = first.at + 1;
75
+ for (const end of [...first.commas, first.close]) {
76
+ alternatives.push(pattern.slice(start, end));
77
+ start = end + 1;
78
+ }
79
+ }
80
+ else {
81
+ alternatives = rangeValues(pattern.slice(first.at + 1, first.close));
82
+ }
83
+ return { pre: pattern.slice(0, first.at), alternatives, post: pattern.slice(first.close + 1) };
84
+ }
85
+ /** Every brace alternative of `pattern`, or `undefined` past the cap. */
86
+ function expandBraceSets(pattern, out = []) {
87
+ const set = firstBraceSet(pattern);
88
+ if (!set) {
89
+ out.push(pattern);
90
+ return out.length > MAX_BRACE_ALTERNATIVES ? undefined : out;
91
+ }
92
+ if (!set.alternatives)
93
+ return undefined;
94
+ for (const alternative of set.alternatives) {
95
+ if (!expandBraceSets(`${set.pre}${alternative}${set.post}`, out))
96
+ return undefined;
97
+ }
98
+ return out;
99
+ }
100
+ /** The characters minimatch can spell a literal `..` with: dots and one-character classes. */
101
+ const DOTDOT_SPELLING_RE = /^[.[\]-]+$/u;
102
+ /**
103
+ * Whether the glob engine reads this brace-free segment as `..`. readdir never
104
+ * yields `..`, so only a literal reaches it, and minimatch spells a literal dot
105
+ * as `.`, `[.]` or `[.-.]`. Any other character (`*`, `?`, an extglob, a
106
+ * letter) either makes the segment a pattern the walk never follows to `..`,
107
+ * or puts that character in the name. Only the rest is worth asking the engine
108
+ * about, so a long or exotic segment costs no compile; one the engine cannot
109
+ * compile is refused.
110
+ */
111
+ const isTraversalSegment = (segment) => {
112
+ try {
113
+ return matchesGlob('..', segment);
114
+ }
115
+ catch {
116
+ return true;
117
+ }
118
+ };
119
+ /**
120
+ * An alternative that starts at a root: `/x`, `//host/share`, `C:/x`, `C:x`,
121
+ * on any host. On Windows fs.glob also reads a literal first segment ending in
122
+ * `:` as a drive (`[C]:/x`); elsewhere that is an ordinary name (`logs:`).
123
+ */
124
+ const isRooted = (alternative) => win32.parse(alternative).root !== '' ||
125
+ (IS_WINDOWS && (alternative.split('/')[0] ?? '').endsWith(':'));
11
126
  export function isSafeGlobSyntax(pattern) {
12
127
  if (!pattern || pattern.trim().length === 0) {
13
128
  return false;
14
129
  }
15
- if (isAbsolute(pattern)) {
16
- return false;
17
- }
18
- if (isWindowsDriveRelativePath(pattern)) {
130
+ // The walk sees the pattern with `\` as `/` (normalizePattern), so check
131
+ // that form: `\etc\x` is `/etc/x` there, an absolute walk on any host.
132
+ // Brace sets can build a root or a `..` from parts (`{/etc,x}`, `..{,x}`,
133
+ // `.{Z..a}.`), so check every alternative, not the pattern as written.
134
+ // globEntries still drops any match outside its cwd: this check is what
135
+ // turns such a pattern into an error rather than a quietly narrower walk.
136
+ const alternatives = expandBraceSets(toPosixPath(pattern));
137
+ if (!alternatives)
19
138
  return false;
139
+ const segments = new Set();
140
+ for (const alternative of alternatives) {
141
+ if (isRooted(alternative))
142
+ return false;
143
+ for (const segment of alternative.split('/'))
144
+ segments.add(segment);
20
145
  }
21
- // Only a whole `..` — a path segment, or one alternative of a `{a,..}`
22
- // brace set — can traverse. `..` inside a segment (`[...slug]`, `v1..2.md`,
23
- // `[..]`) is a literal name or a character class, and PathGuard re-checks
24
- // containment on every resolved entry regardless.
25
- if (TRAVERSAL_DOTDOT_RE.test(pattern)) {
26
- return false;
146
+ let dotdotCheckChars = 0;
147
+ for (const segment of segments) {
148
+ if (!segment.includes('.') || !DOTDOT_SPELLING_RE.test(segment))
149
+ continue;
150
+ dotdotCheckChars += segment.length;
151
+ if (dotdotCheckChars > MAX_DOTDOT_CHECK_CHARS || isTraversalSegment(segment))
152
+ return false;
27
153
  }
28
154
  return true;
29
155
  }
@@ -101,11 +227,17 @@ class GitignoreManager {
101
227
  this.ancestors.push({ prefix, matcher });
102
228
  }
103
229
  /** `null` when no `.gitignore` was found, so callers skip the filter entirely. */
104
- static async load(root, signal, maxDepth, ignoreCeiling) {
230
+ static async load(root, includeHidden, signal, maxDepth, ignoreCeiling) {
105
231
  const manager = new GitignoreManager();
106
232
  try {
107
233
  const gitignorePaths = [];
108
- const gitignoreEntries = fsGlob('**/.gitignore', {
234
+ // Search dot-directories (.venv, .terraform, ...) for rules only when the
235
+ // walk includes hidden entries; for any other walk, enumerating every
236
+ // hidden tree would cost more than the walk itself.
237
+ const gitignorePatterns = includeHidden
238
+ ? [...buildHiddenPatterns('**/.gitignore')]
239
+ : ['**/.gitignore'];
240
+ const gitignoreEntries = fsGlob(gitignorePatterns, {
109
241
  cwd: root,
110
242
  // Skip what the walk itself skips, and stop one level past its depth
111
243
  // bound: a .gitignore at depth d only affects entries at depth >= d.
@@ -207,7 +339,6 @@ class GitignoreManager {
207
339
  const GLOB_MAGIC_RE = /[*?[\]{}!]/u;
208
340
  const GLOB_BATCH_CONCURRENCY = 64;
209
341
  const SEP = '/';
210
- const DOT_CHAR_CODE = 46;
211
342
  function normalizePattern(pattern, baseNameMatch) {
212
343
  const normalized = toPosixPath(pattern);
213
344
  if (!baseNameMatch)
@@ -230,6 +361,26 @@ function splitPatternPrefix(normalizedPattern) {
230
361
  remainder: segments.slice(splitIndex).join(SEP),
231
362
  };
232
363
  }
364
+ /**
365
+ * The segment matching the dot-prefixed names `segment` would match if fs.glob
366
+ * did not skip them, or `undefined` when there are none. Only a leading `*` or
367
+ * `?` stands for that dot: a literal never does, and a character class or a
368
+ * brace set is left as written rather than parsed.
369
+ */
370
+ function dotVariant(segment) {
371
+ // `*(…)` and `?(…)` are extglobs, not wildcards.
372
+ if (segment[1] === '(')
373
+ return undefined;
374
+ if (segment.startsWith('*'))
375
+ return `.${segment}`;
376
+ if (!segment.startsWith('?'))
377
+ return undefined;
378
+ // `?` is exactly one character, so the dot replaces it, unless what follows
379
+ // could leave just `.` or `..` (`?`, `?.`, `?{.,x}`, `?[.]`): the directory
380
+ // and its parent, not a dot-prefixed entry.
381
+ const rest = segment.slice(1);
382
+ return rest === '' || /^[.[{]/u.test(rest) ? undefined : `.${rest}`;
383
+ }
233
384
  function addFirstDotSegment(patterns, prefix, remainder) {
234
385
  if (remainder.length === 0)
235
386
  return;
@@ -237,9 +388,10 @@ function addFirstDotSegment(patterns, prefix, remainder) {
237
388
  const idx = segments.findIndex((seg) => seg !== '**' && seg.length > 0);
238
389
  if (idx !== -1) {
239
390
  const original = segments[idx];
240
- if (original && original.charCodeAt(0) !== DOT_CHAR_CODE) {
391
+ const dotted = original ? dotVariant(original) : undefined;
392
+ if (dotted) {
241
393
  const newSegments = [...segments];
242
- newSegments[idx] = `.${original}`;
394
+ newSegments[idx] = dotted;
243
395
  patterns.add(`${prefix}${newSegments.join(SEP)}`);
244
396
  }
245
397
  }
@@ -261,11 +413,12 @@ function expandHiddenGlobstars(patterns, prefix, remainder) {
261
413
  // starts with '.', and its `dot` option is not honored. So `**/*` alone misses
262
414
  // hidden files. Two depth-agnostic patterns cover everything the per-depth
263
415
  // unroll did: contents of dot-directories anywhere, and dotfiles/dotdirs
264
- // anywhere.
416
+ // anywhere (when the first segment after the globstar can match a dot).
265
417
  patterns.add(`${prefix}**/.*/**/${afterGlobstar}`);
266
- const addDotFile = afterGlobstar.length > 0 && afterGlobstar.charCodeAt(0) !== DOT_CHAR_CODE;
267
- if (addDotFile)
268
- patterns.add(`${prefix}**/.${afterGlobstar}`);
418
+ const [firstSegment = '', ...rest] = afterGlobstar.split(SEP);
419
+ const dotted = dotVariant(firstSegment);
420
+ if (dotted)
421
+ patterns.add(`${prefix}**/${[dotted, ...rest].join(SEP)}`);
269
422
  }
270
423
  function buildHiddenPatterns(normalizedPattern) {
271
424
  const patterns = new Set([normalizedPattern]);
@@ -292,6 +445,11 @@ function normalizeGlobOptions(options) {
292
445
  }
293
446
  function* processDirentMatch(match, cwd, maxDepth, seen, onlyFiles) {
294
447
  const absolutePath = resolve(cwd, match.parentPath, match.name);
448
+ // The walk's scope is its cwd, whatever the pattern spells: a `..` or a root
449
+ // the validator did not foresee must not reach a caller, where it would be
450
+ // read, rewritten, or counted and named as inaccessible.
451
+ if (!isPathInsideDirectory(cwd, absolutePath))
452
+ return;
295
453
  if (maxDepth !== undefined) {
296
454
  const rel = relative(cwd, absolutePath);
297
455
  if (rel.split(/[/\\]/u).length - 1 > maxDepth)
@@ -305,13 +463,12 @@ function* processDirentMatch(match, cwd, maxDepth, seen, onlyFiles) {
305
463
  yield { path: absolutePath, dirent: match };
306
464
  }
307
465
  function createWalkFilter(cwd, skipIgnored, gitignoreMatcher, maxDepth, signal) {
308
- if (!skipIgnored && maxDepth === undefined && signal === undefined)
309
- return undefined;
310
466
  // fs.glob can hand the predicate a dirent whose parentPath is "." — the walk
311
467
  // root, not the process cwd — when it re-visits a directory under `**` whose
312
468
  // name also exists in the process cwd. Resolving against `cwd` names the
313
469
  // entry the dirent describes; `join` would resolve it against the process.
314
- const relativeOf = (match) => toPosixPath(relative(cwd, resolve(cwd, match.parentPath, match.name)));
470
+ const absoluteOf = (match) => resolve(cwd, match.parentPath, match.name);
471
+ const relativeOf = (match) => toPosixPath(relative(cwd, absoluteOf(match)));
315
472
  // Every default exclude is a bare name that applies at any depth, so a Set
316
473
  // lookup per segment replaces compiling dozens of globs per entry.
317
474
  const excluded = (posixRel, isDir) => skipIgnored &&
@@ -323,6 +480,12 @@ function createWalkFilter(cwd, skipIgnored, gitignoreMatcher, maxDepth, signal)
323
480
  // pruning everything is the only way to stop a walk that yields nothing.
324
481
  if (signal?.aborted)
325
482
  return true;
483
+ // fs.glob asks before it queues a directory, so a walk that would
484
+ // climb to `../` or start at `/` stops at the edge instead of reading
485
+ // there. Not every climb asks (`**/..` does not): processDirentMatch is
486
+ // the gate, this only saves the walk.
487
+ if (!isPathInsideDirectory(cwd, absoluteOf(match)))
488
+ return true;
326
489
  const posixRel = relativeOf(match);
327
490
  const isDir = match.isDirectory();
328
491
  if (excluded(posixRel, isDir))
@@ -343,7 +506,7 @@ async function* processGlobPattern(pattern, plan, seen, onlyFiles, filter, signa
343
506
  try {
344
507
  iterable = fsGlob(pattern, {
345
508
  cwd,
346
- ...(filter ? { exclude: filter.prune } : {}),
509
+ exclude: filter.prune,
347
510
  withFileTypes: true,
348
511
  });
349
512
  }
@@ -359,7 +522,7 @@ async function* processGlobPattern(pattern, plan, seen, onlyFiles, filter, signa
359
522
  // A function `exclude` only prunes descent in fs.glob — it still yields
360
523
  // the rejected dirent itself, and any rejected entry below the top level.
361
524
  // Re-apply the ignore rules (not the depth bound) to drop those.
362
- if (filter?.isExcluded(match))
525
+ if (filter.isExcluded(match))
363
526
  continue;
364
527
  yield* processDirentMatch(match, cwd, maxDepth, seen, onlyFiles);
365
528
  }
@@ -375,7 +538,7 @@ async function* processGlobPattern(pattern, plan, seen, onlyFiles, filter, signa
375
538
  }
376
539
  export async function* globEntries(options) {
377
540
  const gitignoreMatcher = options.skipIgnored
378
- ? await GitignoreManager.load(options.cwd, options.signal, options.maxDepth, options.ignoreCeiling)
541
+ ? await GitignoreManager.load(options.cwd, options.includeHidden ?? false, options.signal, options.maxDepth, options.ignoreCeiling)
379
542
  : null;
380
543
  const plan = normalizeGlobOptions(options);
381
544
  const seen = new Set();