diff --git a/src/integrations/cursor-effort-table.ts b/src/integrations/cursor-effort-table.ts index bca6a53c305..b69de7dfef6 100644 --- a/src/integrations/cursor-effort-table.ts +++ b/src/integrations/cursor-effort-table.ts @@ -8,7 +8,7 @@ * instead of a hand-copied mirror. Read-only, size-bounded, cached by (path, mtime, size); * any parse failure yields null so the caller falls back to the static mirror. */ -import { readFileSync, statSync } from "node:fs"; +import { closeSync, constants, fstatSync, openSync, readSync } from "node:fs"; import { join } from "node:path"; import type { CursorInstall } from "./cursor-detect"; @@ -111,32 +111,69 @@ function splitStrings(list: string): string[] { export interface CursorEffortTableDeps { platform: string; - stat(path: string): { mtimeMs: number; size: number } | null; - readText(path: string): string | null; + readBundle(path: string, cached?: { mtimeMs: number; size: number }): { mtimeMs: number; size: number; text: string | null } | null; } export function realCursorEffortTableDeps(): CursorEffortTableDeps { return { platform: process.platform, - stat: path => { try { const s = statSync(path); return { mtimeMs: s.mtimeMs, size: s.size }; } catch { return null; } }, - readText: path => { try { return readFileSync(path, "utf8"); } catch { return null; } }, + readBundle: readCursorBundle, }; } -let cache: { key: string; table: CursorEffortTable | null } | null = null; +function readCursorBundle(path: string, cached?: { mtimeMs: number; size: number }): { mtimeMs: number; size: number; text: string | null } | null { + let fd: number | null = null; + try { + // O_NOFOLLOW binds the validation and read to the same regular file. O_NONBLOCK + // keeps opening a substituted special file from stalling before fstat rejects it. + fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK); + const stat = fstatSync(fd); + if (!stat.isFile() || stat.size > BUNDLE_MAX_BYTES) return null; + if (cached?.mtimeMs === stat.mtimeMs && cached.size === stat.size) { + return { mtimeMs: stat.mtimeMs, size: stat.size, text: null }; + } + + const chunks: Buffer[] = []; + let size = 0; + while (size <= BUNDLE_MAX_BYTES) { + const chunk = Buffer.allocUnsafe(Math.min(64 * 1024, BUNDLE_MAX_BYTES + 1 - size)); + const bytesRead = readSync(fd, chunk, 0, chunk.length, null); + if (bytesRead === 0) break; + chunks.push(chunk.subarray(0, bytesRead)); + size += bytesRead; + } + if (size > BUNDLE_MAX_BYTES) return null; + return { mtimeMs: stat.mtimeMs, size, text: Buffer.concat(chunks, size).toString("utf8") }; + } catch { + return null; + } finally { + if (fd !== null) closeSync(fd); + } +} + +let cache: { key: string; path: string; mtimeMs: number; size: number; table: CursorEffortTable | null } | null = null; /** Table from the Private Inference install, else null (caller falls back to the static mirror). */ export function loadCursorEffortTable(install: CursorInstall | undefined, deps: CursorEffortTableDeps = realCursorEffortTableDeps()): CursorEffortTable | null { if (!install) return null; const bundlePath = cursorAgentBundlePath(install, deps.platform); - const st = deps.stat(bundlePath); - if (!st || st.size > BUNDLE_MAX_BYTES) return null; - const key = `${bundlePath}|${st.mtimeMs}|${st.size}`; - if (cache?.key === key) return cache.table; - const text = deps.readText(bundlePath); - const parsed = text ? parseCursorEffortTable(text) : null; + const cachedMetadata = cache?.path === bundlePath + ? { mtimeMs: cache.mtimeMs, size: cache.size } + : undefined; + const bundle = deps.readBundle(bundlePath, cachedMetadata); + if (!bundle) return null; + const key = `${bundlePath}|${bundle.mtimeMs}|${bundle.size}`; + if (cache?.key === key) { + // The cache key covers bundle identity only; install.version comes from + // product.json and can change or resolve without touching the bundle. + const cached = cache.table; + return cached && cached.version !== install.version + ? { ...cached, version: install.version } + : cached; + } + const parsed = bundle.text ? parseCursorEffortTable(bundle.text) : null; const table = parsed ? { ...parsed, version: install.version, bundlePath } : null; - cache = { key, table }; + cache = { key, path: bundlePath, mtimeMs: bundle.mtimeMs, size: bundle.size, table }; return table; } diff --git a/tests/providers/cursor/cursor-effort-table.test.ts b/tests/providers/cursor/cursor-effort-table.test.ts index 973941d5744..828c3bec5cf 100644 --- a/tests/providers/cursor/cursor-effort-table.test.ts +++ b/tests/providers/cursor/cursor-effort-table.test.ts @@ -1,7 +1,10 @@ import { beforeEach, describe, expect, test } from "bun:test"; -import { readFileSync } from "node:fs"; +import { execFileSync } from "node:child_process"; +import { mkdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; import { join } from "node:path"; import { + cursorAgentBundlePath, loadCursorEffortTable, parseCursorEffortTable, resetCursorEffortTableCacheForTests, @@ -65,15 +68,13 @@ describe("Cursor installed-bundle effort table", () => { test("activates the static fallback for missing installs, missing literals, and malformed regexes", () => { const missingStat: CursorEffortTableDeps = { platform: "darwin", - stat: () => null, - readText: () => { throw new Error("readText must not run without a stat"); }, + readBundle: () => null, }; expect(loadCursorEffortTable(INSTALL, missingStat)).toBeNull(); const loadSource = (source: string, mtimeMs: number) => loadCursorEffortTable(INSTALL, { platform: "darwin", - stat: () => ({ mtimeMs, size: source.length }), - readText: () => source, + readBundle: () => ({ mtimeMs, size: source.length, text: source }), }); expect(loadSource("function unrelated(){}", 1)).toBeNull(); expect(loadSource(FIXTURE.replace("/^claude-opus-5$/u", "/[/u"), 2)).toBeNull(); @@ -102,10 +103,12 @@ describe("Cursor installed-bundle effort table", () => { let reads = 0; const deps: CursorEffortTableDeps = { platform: "darwin", - stat: () => ({ mtimeMs, size: FIXTURE.length }), - readText: () => { + readBundle: (_path, cached) => { + if (cached?.mtimeMs === mtimeMs && cached.size === FIXTURE.length) { + return { ...cached, text: null }; + } reads += 1; - return FIXTURE; + return { mtimeMs, size: FIXTURE.length, text: FIXTURE }; }, }; expect(loadCursorEffortTable(INSTALL, deps)?.families).toHaveLength(16); @@ -115,4 +118,33 @@ describe("Cursor installed-bundle effort table", () => { expect(loadCursorEffortTable(INSTALL, deps)?.families).toHaveLength(16); expect(reads).toBe(2); }); + + test("refreshes the reported version on a bundle cache hit", () => { + const deps: CursorEffortTableDeps = { + platform: "darwin", + readBundle: () => ({ mtimeMs: 1, size: FIXTURE.length, text: FIXTURE }), + }; + expect(loadCursorEffortTable(INSTALL, deps)?.version).toBe("3.18.25"); + const upgraded = { ...INSTALL, version: "3.19.0" }; + expect(loadCursorEffortTable(upgraded, deps)?.version).toBe("3.19.0"); + }); + + test("rejects symlinks and special files without blocking", () => { + if (process.platform === "win32") return; + const root = `${tmpdir()}/ocx-cursor-bundle-${process.pid}-${Date.now()}`; + const install = { ...INSTALL, path: root }; + const bundlePath = cursorAgentBundlePath(install, process.platform); + const target = `${root}/target.js`; + mkdirSync(bundlePath.slice(0, bundlePath.lastIndexOf("/")), { recursive: true }); + writeFileSync(target, FIXTURE); + try { + symlinkSync(target, bundlePath); + expect(loadCursorEffortTable(install)).toBeNull(); + rmSync(bundlePath); + execFileSync("mkfifo", [bundlePath]); + expect(loadCursorEffortTable(install)).toBeNull(); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); });