{"version":3,"file":"checkProblemDirIsolation.cjs","names":["path","child_process","fs","os","TEST_CASE_RESULT_PREFIX","testCaseResultSchema","DecisionCode"],"sources":["../../src/helpers/checkProblemDirIsolation.ts"],"sourcesContent":["import child_process from 'node:child_process';\nimport fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\n\nimport { DecisionCode } from '../types/decisionCode.js';\nimport { TEST_CASE_RESULT_PREFIX, testCaseResultSchema } from '../types/testCaseResult.js';\n\nimport { printDebugBanner } from './printDebugBanner.js';\nimport type { ResolvedCwd } from './resolveCwds.js';\n\nconst ISOLATION_CHECK_TIMEOUT_MS = 30_000;\n\nexport interface ProblemDirIsolationCheckResult {\n  passed: boolean;\n}\n\n/**\n * Check that a judge can run after only the problem directory is copied elsewhere.\n */\nexport async function checkProblemDirIsolation(\n  problemDir: string,\n  resolvedCwd: ResolvedCwd,\n  params: unknown\n): Promise<ProblemDirIsolationCheckResult> {\n  let tempRoot: string | undefined;\n  try {\n    const copyResult = await copyProblemDirToTemporaryRoot(problemDir);\n    tempRoot = copyResult.tempRoot;\n    const { copiedProblemDir } = copyResult;\n    const absoluteProblemDir = path.resolve(problemDir);\n\n    const relativeCwd = path.relative(absoluteProblemDir, path.resolve(resolvedCwd.cwd));\n    const copiedCwd = path.join(copiedProblemDir, relativeCwd);\n    const scriptPath = getInvokedScriptPath(absoluteProblemDir);\n    if (scriptPath.startsWith('..') || path.isAbsolute(scriptPath)) {\n      printDebugBanner([\n        '[DEBUG MODE] isolated problem directory check skipped',\n        '',\n        'The invoked judge script is located outside the problem directory.',\n        `Script path: ${scriptPath}`,\n      ]);\n      return { passed: true };\n    }\n    const execArgv = process.execArgv.filter(isIsolationExecArg);\n    const paramsJson = JSON.stringify(isJudgeParamsObject(params) ? params : {});\n    const spawnResult = child_process.spawnSync(process.execPath, [...execArgv, scriptPath, copiedCwd, paramsJson], {\n      cwd: copiedProblemDir,\n      encoding: 'utf8',\n      env: process.env,\n      timeout: ISOLATION_CHECK_TIMEOUT_MS,\n    });\n    const stdout = spawnResult.stdout ?? '';\n    const stderr = spawnResult.stderr ?? '';\n\n    if (spawnResult.status === 0 && isAcceptedJudgeOutput(stdout)) {\n      printDebugBanner([\n        '[DEBUG MODE] isolated problem directory check passed',\n        '',\n        `Copied problem dir : ${copiedProblemDir}`,\n        `Checked cwd        : ${relativeCwd}`,\n      ]);\n      return { passed: true };\n    }\n\n    printDebugBanner([\n      '[DEBUG MODE] isolated problem directory check failed',\n      '',\n      'The judge did not complete successfully after copying only the problem directory to a temporary location.',\n      'Make sure judge.ts imports only files included in the problem directory.',\n      '',\n      `Copied problem dir : ${copiedProblemDir}`,\n      `Checked cwd        : ${relativeCwd}`,\n      `Exit status        : ${spawnResult.status ?? spawnResult.signal ?? 'unknown'}`,\n      `Spawn error        : ${spawnResult.error?.message ?? '<none>'}`,\n      '',\n      'stdout:',\n      stdout.trimEnd() || '<empty>',\n      '',\n      'stderr:',\n      stderr.trimEnd() || '<empty>',\n    ]);\n    return { passed: false };\n  } catch (error) {\n    printDebugBanner([\n      '[DEBUG MODE] isolated problem directory check failed due to an unexpected error',\n      '',\n      error instanceof Error ? error.message : String(error),\n    ]);\n    return { passed: false };\n  } finally {\n    // Cleanup failures must not mask the primary isolation check result.\n    if (tempRoot) await forciblyRemoveDirectory(tempRoot);\n  }\n}\n\n/**\n * Copy a problem directory into a temporary root, symlinking every ancestor `node_modules` so the\n * copied harness still resolves its imports. Callers must remove the returned `tempRoot`.\n */\nexport async function copyProblemDirToTemporaryRoot(\n  problemDir: string\n): Promise<{ tempRoot: string; copiedProblemDir: string }> {\n  const tempRoot = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'problem-utils-isolation_'));\n  try {\n    const absoluteProblemDir = path.resolve(problemDir);\n    const copiedProblemDir = path.join(tempRoot, toTempRelativePath(absoluteProblemDir));\n    await fs.promises.mkdir(path.dirname(copiedProblemDir), { recursive: true });\n    await fs.promises.cp(absoluteProblemDir, copiedProblemDir, {\n      recursive: true,\n      filter: rejectAbsoluteSymlinks,\n      // Keep relative symlinks relative: the default rewrites them to absolute paths into the\n      // source tree, so judging the copy could write through them into the checked repository.\n      verbatimSymlinks: true,\n    });\n    await symlinkAllAncestorNodeModules(tempRoot, absoluteProblemDir);\n    return { tempRoot, copiedProblemDir };\n  } catch (error) {\n    await forciblyRemoveDirectory(tempRoot);\n    throw error;\n  }\n}\n\n/**\n * Remove a temporary directory even when judged code left permission-locked entries in it (e.g. a\n * mode-000 directory makes a plain `fs.rm` fail with EACCES). Returns whether removal succeeded.\n */\nexport async function forciblyRemoveDirectory(dir: string): Promise<boolean> {\n  try {\n    await fs.promises.rm(dir, { recursive: true, force: true });\n    return true;\n  } catch {\n    unlockPermissions(dir);\n    try {\n      await fs.promises.rm(dir, { recursive: true, force: true });\n      return true;\n    } catch {\n      return false;\n    }\n  }\n}\n\n/** Synchronous variant of {@link forciblyRemoveDirectory} for signal handlers. */\nexport function forciblyRemoveDirectorySync(dir: string): boolean {\n  try {\n    fs.rmSync(dir, { recursive: true, force: true });\n    return true;\n  } catch {\n    unlockPermissions(dir);\n    try {\n      fs.rmSync(dir, { recursive: true, force: true });\n      return true;\n    } catch {\n      return false;\n    }\n  }\n}\n\n// chmod does not exist on Windows, where POSIX-mode locks cannot occur anyway. spawnSync reports\n// spawn failures via its return value instead of throwing, so no try/catch is needed.\nfunction unlockPermissions(dir: string): void {\n  if (process.platform !== 'win32') child_process.spawnSync('chmod', ['-R', 'u+rwX', dir]);\n}\n\n// An absolute symlink is copied verbatim, so judging the copy could write through it into the\n// original tree; reject it instead of silently breaking the isolation guarantee.\nasync function rejectAbsoluteSymlinks(src: string): Promise<boolean> {\n  if (!isCopiedProblemPath(src)) return false;\n  const stats = await fs.promises.lstat(src);\n  if (stats.isSymbolicLink() && path.isAbsolute(await fs.promises.readlink(src))) {\n    throw new Error(`${src} is an absolute symlink, which would escape the temporary copy; use a relative symlink`);\n  }\n  return true;\n}\n\nfunction isCopiedProblemPath(src: string): boolean {\n  const name = path.basename(src);\n  return name !== 'node_modules' && name !== '.git';\n}\n\nfunction isIsolationExecArg(arg: string): boolean {\n  return !arg.startsWith('--inspect') && !arg.startsWith('--watch') && !arg.startsWith('--hot');\n}\n\nfunction isJudgeParamsObject(params: unknown): params is object {\n  return params !== undefined && params !== null && typeof params === 'object' && !Array.isArray(params);\n}\n\nasync function symlinkAllAncestorNodeModules(tempRoot: string, problemDir: string): Promise<void> {\n  let currentDir = path.resolve(problemDir);\n  while (true) {\n    const nodeModulesPath = path.join(currentDir, 'node_modules');\n    if (fs.existsSync(nodeModulesPath)) {\n      const targetSymlinkPath = path.join(tempRoot, toTempRelativePath(currentDir), 'node_modules');\n      try {\n        await fs.promises.symlink(\n          nodeModulesPath,\n          targetSymlinkPath,\n          process.platform === 'win32' ? 'junction' : 'dir'\n        );\n      } catch {\n        // Package resolution is best-effort; the isolation check still reports a clear spawn failure if imports break.\n      }\n    }\n\n    const parentDir = path.dirname(currentDir);\n    if (parentDir === currentDir) break;\n    currentDir = parentDir;\n  }\n}\n\nfunction toTempRelativePath(absolutePath: string): string {\n  const { root } = path.parse(absolutePath);\n  return path.relative(root, absolutePath);\n}\n\nfunction getInvokedScriptPath(problemDir: string): string {\n  const scriptPath = process.argv[1];\n  if (!scriptPath) return `.${path.sep}judge.ts`;\n  const relativeScriptPath = path.relative(problemDir, path.resolve(scriptPath));\n  if (path.isAbsolute(relativeScriptPath)) return relativeScriptPath;\n  return relativeScriptPath.startsWith('.') ? relativeScriptPath : `.${path.sep}${relativeScriptPath}`;\n}\n\nfunction isAcceptedJudgeOutput(stdout: string): boolean {\n  const resultLines = stdout.split(/\\r?\\n/).filter((line) => line.startsWith(TEST_CASE_RESULT_PREFIX));\n  if (resultLines.length === 0) return false;\n\n  return resultLines.every((line) => {\n    try {\n      const parsedResult = testCaseResultSchema.safeParse(JSON.parse(line.slice(TEST_CASE_RESULT_PREFIX.length)));\n      return parsedResult.success && parsedResult.data.decisionCode === DecisionCode.ACCEPTED;\n    } catch {\n      return false;\n    }\n  });\n}\n"],"mappings":"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"}