@exercode/problem-utils 4.3.6 → 5.0.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 (128) hide show
  1. package/dist/cli/checkAllProblems.cjs +1 -1
  2. package/dist/cli/checkAllProblems.cjs.map +1 -1
  3. package/dist/cli/checkAllProblems.d.ts +2 -1
  4. package/dist/cli/checkAllProblems.js +1 -1
  5. package/dist/cli/checkAllProblems.js.map +1 -1
  6. package/dist/cli/exercodeProblem.cjs.map +1 -1
  7. package/dist/cli/exercodeProblem.js.map +1 -1
  8. package/dist/cli/runSingleHarness.cjs.map +1 -1
  9. package/dist/cli/runSingleHarness.js.map +1 -1
  10. package/dist/cli/validateLearningMaterial.cjs.map +1 -1
  11. package/dist/cli/validateLearningMaterial.js.map +1 -1
  12. package/dist/helpers/checkProblemDirIsolation.cjs.map +1 -1
  13. package/dist/helpers/checkProblemDirIsolation.js.map +1 -1
  14. package/dist/helpers/cleanWorkingDirectory.cjs.map +1 -1
  15. package/dist/helpers/cleanWorkingDirectory.js.map +1 -1
  16. package/dist/helpers/compareExpectedOutputFiles.cjs.map +1 -1
  17. package/dist/helpers/compareExpectedOutputFiles.js.map +1 -1
  18. package/dist/helpers/compareStdoutAsSpaceSeparatedTokens.cjs.map +1 -1
  19. package/dist/helpers/compareStdoutAsSpaceSeparatedTokens.js.map +1 -1
  20. package/dist/helpers/copyTestCaseFileInput.cjs.map +1 -1
  21. package/dist/helpers/copyTestCaseFileInput.js.map +1 -1
  22. package/dist/helpers/defaultStdioHarness.cjs.map +1 -1
  23. package/dist/helpers/defaultStdioHarness.js.map +1 -1
  24. package/dist/helpers/findEntryPointFile.cjs.map +1 -1
  25. package/dist/helpers/findEntryPointFile.js.map +1 -1
  26. package/dist/helpers/findLanguageDefinitionByPath.cjs.map +1 -1
  27. package/dist/helpers/findLanguageDefinitionByPath.js.map +1 -1
  28. package/dist/helpers/findModelAnswerDirs.cjs.map +1 -1
  29. package/dist/helpers/findModelAnswerDirs.js.map +1 -1
  30. package/dist/helpers/guiRecording.cjs.map +1 -1
  31. package/dist/helpers/guiRecording.js.map +1 -1
  32. package/dist/helpers/javaJudge.cjs.map +1 -1
  33. package/dist/helpers/javaJudge.js.map +1 -1
  34. package/dist/helpers/judgeByStaticAnalysis.cjs.map +1 -1
  35. package/dist/helpers/judgeByStaticAnalysis.js.map +1 -1
  36. package/dist/helpers/parseArgs.cjs.map +1 -1
  37. package/dist/helpers/parseArgs.js.map +1 -1
  38. package/dist/helpers/parseCsv.cjs.map +1 -1
  39. package/dist/helpers/parseCsv.js.map +1 -1
  40. package/dist/helpers/printDebugBanner.cjs.map +1 -1
  41. package/dist/helpers/printDebugBanner.js.map +1 -1
  42. package/dist/helpers/printTestCaseResult.cjs.map +1 -1
  43. package/dist/helpers/printTestCaseResult.js.map +1 -1
  44. package/dist/helpers/readOutputFiles.cjs.map +1 -1
  45. package/dist/helpers/readOutputFiles.js.map +1 -1
  46. package/dist/helpers/readProblemMarkdownFrontMatter.cjs.map +1 -1
  47. package/dist/helpers/readProblemMarkdownFrontMatter.js.map +1 -1
  48. package/dist/helpers/readTestCases.cjs.map +1 -1
  49. package/dist/helpers/readTestCases.js.map +1 -1
  50. package/dist/helpers/removeCommentsInSourceCode.cjs.map +1 -1
  51. package/dist/helpers/removeCommentsInSourceCode.js.map +1 -1
  52. package/dist/helpers/resolveCwds.cjs.map +1 -1
  53. package/dist/helpers/resolveCwds.js.map +1 -1
  54. package/dist/helpers/runCommandInTemporaryPackageManagerProject.cjs.map +1 -1
  55. package/dist/helpers/runCommandInTemporaryPackageManagerProject.js.map +1 -1
  56. package/dist/helpers/runHarnessProcess.cjs.map +1 -1
  57. package/dist/helpers/runHarnessProcess.js.map +1 -1
  58. package/dist/helpers/runIsolationCheckInDebugMode.cjs.map +1 -1
  59. package/dist/helpers/runIsolationCheckInDebugMode.js.map +1 -1
  60. package/dist/helpers/runPythonInTemporaryUvProject.cjs.map +1 -1
  61. package/dist/helpers/runPythonInTemporaryUvProject.js.map +1 -1
  62. package/dist/helpers/sourceCodeGrammars.cjs.map +1 -1
  63. package/dist/helpers/sourceCodeGrammars.js.map +1 -1
  64. package/dist/helpers/spawnWithLimits.cjs.map +1 -1
  65. package/dist/helpers/spawnWithLimits.js.map +1 -1
  66. package/dist/helpers/spawnWithTimeout.cjs.map +1 -1
  67. package/dist/helpers/spawnWithTimeout.js.map +1 -1
  68. package/dist/helpers/startHttpServer.cjs.map +1 -1
  69. package/dist/helpers/startHttpServer.js.map +1 -1
  70. package/dist/helpers/stdioJudgeRules.cjs.map +1 -1
  71. package/dist/helpers/stdioJudgeRules.js.map +1 -1
  72. package/dist/helpers/temporaryProblemDirCopy.cjs.map +1 -1
  73. package/dist/helpers/temporaryProblemDirCopy.js.map +1 -1
  74. package/dist/index.cjs +1 -1
  75. package/dist/index.js +1 -1
  76. package/dist/learningMaterial/frontmatter.cjs.map +1 -1
  77. package/dist/learningMaterial/frontmatter.js.map +1 -1
  78. package/dist/learningMaterial/fsHelpers.cjs.map +1 -1
  79. package/dist/learningMaterial/fsHelpers.js.map +1 -1
  80. package/dist/learningMaterial/schemas.cjs +1 -1
  81. package/dist/learningMaterial/schemas.cjs.map +1 -1
  82. package/dist/learningMaterial/schemas.d.ts +43 -3
  83. package/dist/learningMaterial/schemas.js +1 -1
  84. package/dist/learningMaterial/schemas.js.map +1 -1
  85. package/dist/learningMaterial/validateContest.cjs +1 -1
  86. package/dist/learningMaterial/validateContest.cjs.map +1 -1
  87. package/dist/learningMaterial/validateContest.d.ts +0 -2
  88. package/dist/learningMaterial/validateContest.js +1 -1
  89. package/dist/learningMaterial/validateContest.js.map +1 -1
  90. package/dist/learningMaterial/validateCourse.cjs +1 -1
  91. package/dist/learningMaterial/validateCourse.cjs.map +1 -1
  92. package/dist/learningMaterial/validateCourse.js +1 -1
  93. package/dist/learningMaterial/validateCourse.js.map +1 -1
  94. package/dist/learningMaterial/validateMaterial.cjs +3 -3
  95. package/dist/learningMaterial/validateMaterial.cjs.map +1 -1
  96. package/dist/learningMaterial/validateMaterial.d.ts +0 -7
  97. package/dist/learningMaterial/validateMaterial.js +3 -3
  98. package/dist/learningMaterial/validateMaterial.js.map +1 -1
  99. package/dist/learningMaterial/validateProblem.cjs +1 -1
  100. package/dist/learningMaterial/validateProblem.cjs.map +1 -1
  101. package/dist/learningMaterial/validateProblem.js +1 -1
  102. package/dist/learningMaterial/validateProblem.js.map +1 -1
  103. package/dist/learningMaterial/validationResult.cjs.map +1 -1
  104. package/dist/learningMaterial/validationResult.js.map +1 -1
  105. package/dist/presets/command.cjs.map +1 -1
  106. package/dist/presets/command.js.map +1 -1
  107. package/dist/presets/evaluation.cjs.map +1 -1
  108. package/dist/presets/evaluation.js.map +1 -1
  109. package/dist/presets/guiCommand.cjs.map +1 -1
  110. package/dist/presets/guiCommand.js.map +1 -1
  111. package/dist/presets/springBoot.cjs.map +1 -1
  112. package/dist/presets/springBoot.js.map +1 -1
  113. package/dist/presets/stdio.cjs.map +1 -1
  114. package/dist/presets/stdio.js.map +1 -1
  115. package/dist/presets/tomcat.cjs.map +1 -1
  116. package/dist/presets/tomcat.js.map +1 -1
  117. package/dist/types/decisionCode.cjs.map +1 -1
  118. package/dist/types/decisionCode.js.map +1 -1
  119. package/dist/types/language.cjs.map +1 -1
  120. package/dist/types/language.js.map +1 -1
  121. package/dist/types/problem.cjs +1 -1
  122. package/dist/types/problem.cjs.map +1 -1
  123. package/dist/types/problem.d.ts +1 -0
  124. package/dist/types/problem.js +1 -1
  125. package/dist/types/problem.js.map +1 -1
  126. package/dist/types/testCaseResult.cjs.map +1 -1
  127. package/dist/types/testCaseResult.js.map +1 -1
  128. package/package.json +2 -2
@@ -1 +1 @@
1
- {"version":3,"file":"spawnWithLimits.js","names":["path","fs","os","childProcess","killGracePeriodMilliseconds","timeCommand","resolveTimeCommand","isTimeCommandAvailable","liveSubprocesses","subprocess","killSubprocessGroup","spawnWithLimits","timeOutput","openUnlinkedFile","detached","boundedCommand","context","command","spawnedCommand","timeLimitMilliseconds","startTimeMilliseconds","stdoutChunks","stderrChunks","outputBytes","timedOut","outputLimitExceeded","wallTimeSeconds","spawnErrorMessage","appendOutputChunk","chunk","remainingBytes","appendedChunk","chunks","timeout","killTimeout","settled","pendingError","closeTimeout","settle","reject","resolve","code","exitSignal","failAfterClose","error","timeResult","parseTimeOutput","readTail","endedByTimeout","status","timeLimitSeconds","signal","hasErrorCode","directory","prefix","file","length","size","maxBytes","buffer","match","content","parseSeconds","text","result"],"sources":["../../src/helpers/spawnWithLimits.ts"],"sourcesContent":["import childProcess from 'node:child_process';\nimport fs from 'node:fs/promises';\nimport os from 'node:os';\nimport path from 'node:path';\n\nimport { hasErrorCode } from '@willbooster/shared-lib';\n\nexport interface SpawnWithLimitsResult {\n stdout: string;\n stderr: string;\n status: number | undefined;\n signal: NodeJS.Signals | undefined;\n /** Wall time measured by GNU time, or measured here until the command exited when it produced none. */\n timeSeconds: number;\n /**\n * User plus system CPU time of the command and the descendants it waited for, measured by GNU\n * time, or 0 when it produced no measurement. A run `timeout` ended is measured too, so a caller\n * can tell a program that used up its time limit from one that waited for the CPU.\n */\n cpuTimeSeconds: number;\n /**\n * Peak resident set size measured by GNU time, or 0 when it produced no measurement. GNU time\n * measures its child `timeout` and the descendants it waited for, so the value is at least\n * `timeout`'s own footprint (about 1 MiB), which only shows for the smallest native programs.\n */\n memoryBytes: number;\n /** The note GNU time adds when the command exits abnormally, e.g. `Command terminated by signal 11`. */\n timeCommandMessage: string | undefined;\n timedOut: boolean;\n outputLimitExceeded: boolean;\n}\n\nconst killGracePeriodMilliseconds = 1000;\nconst MIN_TIMEOUT_SECONDS = 0.001;\n// GNU `timeout` exits with 124 after ending a run at its limit, or with 128 + SIGKILL when the run\n// ignored its SIGTERM and it had to be killed after the grace period.\nconst TIMEOUT_EXIT_STATUS = 124;\nconst KILLED_AFTER_TIMEOUT_EXIT_STATUS = 137;\n// GNU time writes its record to a file this process opened and unlinked at once, handed down as fd 3\n// of the command and reopened by GNU time through `/dev/fd`: the command runs as the same OS user,\n// so a record reachable by path could be swapped for a fake or for a FIFO that blocks the read.\nconst TIME_OUTPUT_FD = 3;\n// The command inherits that descriptor too, so only the tail that can hold GNU time's record is read.\nconst TIME_OUTPUT_TAIL_BYTES = 4096;\nconst timeCommand = resolveTimeCommand();\n\n/** Whether GNU time is available, i.e. whether `timeSeconds`, `cpuTimeSeconds`, and `memoryBytes` are measured at all. */\nexport const isTimeCommandAvailable = timeCommand !== undefined;\n\n// The commands run in their own sessions, so they outlive this process unless it ends them itself\n// on the way out (e.g. a preset's SIGINT handler calling `process.exit`).\nconst liveSubprocesses = new Set<childProcess.ChildProcess>();\nprocess.once('exit', () => {\n for (const subprocess of liveSubprocesses) killSubprocessGroup(subprocess, 'SIGKILL');\n});\n\n/**\n * Runs a command in its own process group with `stdin` piped in, killing the whole group once it\n * exceeds the time or output limit, and reports the wall time, CPU time, and peak memory measured\n * by GNU time.\n */\nexport async function spawnWithLimits(\n command: readonly [string, ...string[]],\n context: {\n cwd: string;\n env: NodeJS.ProcessEnv;\n outputLimitBytes: number;\n stdin: string;\n timeLimitSeconds: number;\n }\n): Promise<SpawnWithLimitsResult> {\n const timeOutput = timeCommand === undefined ? undefined : await openUnlinkedFile('exercode-time-');\n try {\n const detached = process.platform !== 'win32';\n // The command runs in its own session so the group kill below cannot hit this process, but that\n // also puts it out of reach of whoever kills this process. GNU `timeout` keeps the run bounded\n // in that case, and it is what normally ends a run at its limit: GNU time wraps it, so a run\n // that hit the limit still gets its CPU time recorded (a group kill from here would take GNU\n // time down with the command), and `--foreground` keeps the command in this group so the kill\n // below still reaches every descendant. The timers below only back `timeout` up. Should this\n // process die first, `timeout` ends the command alone (`--foreground` signals no descendant),\n // an accepted gap: the judge server sweeps every process of the request's sandbox user anyway.\n const boundedCommand: readonly [string, ...string[]] = detached\n ? [\n 'timeout',\n '--foreground',\n '-k',\n String(killGracePeriodMilliseconds / 1000),\n // A zero duration would disable `timeout`; the front matter allows a zero time limit.\n String(Math.max(context.timeLimitSeconds, MIN_TIMEOUT_SECONDS)),\n ...command,\n ]\n : command;\n const spawnedCommand: readonly [string, ...string[]] =\n timeCommand === undefined\n ? boundedCommand\n : [...timeCommand, `--output=/dev/fd/${TIME_OUTPUT_FD}`, ...boundedCommand];\n // `timeout` sends SIGKILL a grace period after its SIGTERM; the timers here fire once even that\n // should have ended the run.\n const timeLimitMilliseconds = detached\n ? context.timeLimitSeconds * 1000 + 2 * killGracePeriodMilliseconds\n : context.timeLimitSeconds * 1000;\n // Monotonic: a wall-clock step must not turn a run `timeout` ended into one that finished in time.\n const startTimeMilliseconds = performance.now();\n // The standard streams are always pipes; the type cannot express that with the extra entry.\n const subprocess = childProcess.spawn(spawnedCommand[0], spawnedCommand.slice(1), {\n cwd: context.cwd,\n detached,\n env: context.env,\n stdio: timeOutput === undefined ? ['pipe', 'pipe', 'pipe'] : ['pipe', 'pipe', 'pipe', timeOutput.fd],\n }) as childProcess.ChildProcessWithoutNullStreams;\n liveSubprocesses.add(subprocess);\n\n const stdoutChunks: Buffer[] = [];\n const stderrChunks: Buffer[] = [];\n let outputBytes = 0;\n let timedOut = false;\n let outputLimitExceeded = false;\n let wallTimeSeconds = 0;\n let spawnErrorMessage: string | undefined;\n\n const appendOutputChunk = (chunks: Buffer[], chunk: Buffer): void => {\n if (outputBytes >= context.outputLimitBytes) {\n if (chunk.byteLength > 0) {\n outputLimitExceeded = true;\n killSubprocessGroup(subprocess, 'SIGKILL');\n }\n return;\n }\n\n const remainingBytes = context.outputLimitBytes - outputBytes;\n const appendedChunk = chunk.byteLength > remainingBytes ? chunk.subarray(0, remainingBytes) : chunk;\n chunks.push(appendedChunk);\n outputBytes += appendedChunk.byteLength;\n\n if (chunk.byteLength > remainingBytes) {\n outputLimitExceeded = true;\n killSubprocessGroup(subprocess, 'SIGKILL');\n }\n };\n\n subprocess.stdout.on('data', (chunk: Buffer) => appendOutputChunk(stdoutChunks, chunk));\n subprocess.stderr.on('data', (chunk: Buffer) => appendOutputChunk(stderrChunks, chunk));\n\n const timeout = setTimeout(() => {\n timedOut = true;\n killSubprocessGroup(subprocess, 'SIGTERM');\n }, timeLimitMilliseconds);\n const killTimeout = setTimeout(() => {\n if (timedOut) killSubprocessGroup(subprocess, 'SIGKILL');\n }, timeLimitMilliseconds + killGracePeriodMilliseconds);\n killTimeout.unref();\n\n const { status, signal } = await new Promise<{ status: number | undefined; signal: NodeJS.Signals | undefined }>(\n (resolve, reject) => {\n let settled = false;\n let pendingError: Error | undefined;\n let closeTimeout: NodeJS.Timeout | undefined;\n const settle = (code: number | null | undefined, exitSignal: NodeJS.Signals | null | undefined): void => {\n if (settled) return;\n settled = true;\n clearTimeout(closeTimeout);\n if (pendingError) {\n reject(pendingError);\n return;\n }\n resolve({ status: code ?? undefined, signal: exitSignal ?? undefined });\n };\n const failAfterClose = (error: Error): void => {\n if (settled) return;\n if (subprocess.pid === undefined) {\n // The spawn itself failed, i.e. the head of the chain (GNU time, or `timeout` without\n // it, or the command itself on Windows) or the cwd is missing. A missing judged command\n // is reported as status 127 by the wrapper instead. Report this like a run that produced\n // only this message.\n spawnErrorMessage = error.message;\n settle(undefined, undefined);\n return;\n }\n pendingError = error;\n killSubprocessGroup(subprocess, 'SIGKILL');\n };\n subprocess.on('error', failAfterClose);\n subprocess.stdin.on('error', (error: NodeJS.ErrnoException) => {\n if (error.code !== 'EPIPE') failAfterClose(error);\n });\n subprocess.on('exit', (code, exitSignal) => {\n // The command is gone, so the limit timers must not fire during the grace period below. A\n // descendant that inherited the pipes (e.g. `sleep 1000 &`) would keep them open and hold\n // back 'close' until it ends: kill what is left of the group. A descendant that moved to\n // its own session survives that, so stop waiting for the pipes after the grace period and\n // keep the output read so far.\n clearTimeout(timeout);\n clearTimeout(killTimeout);\n wallTimeSeconds = (performance.now() - startTimeMilliseconds) / 1000;\n killSubprocessGroup(subprocess, 'SIGKILL');\n closeTimeout = setTimeout(() => {\n subprocess.stdout.destroy();\n subprocess.stderr.destroy();\n settle(code, exitSignal);\n }, killGracePeriodMilliseconds);\n });\n subprocess.on('close', settle);\n subprocess.stdin.end(context.stdin);\n }\n ).finally(() => {\n clearTimeout(timeout);\n clearTimeout(killTimeout);\n liveSubprocesses.delete(subprocess);\n });\n\n const timeResult =\n timeOutput === undefined ? undefined : parseTimeOutput(await readTail(timeOutput, TIME_OUTPUT_TAIL_BYTES));\n\n return {\n stdout: Buffer.concat(stdoutChunks).toString(),\n stderr: spawnErrorMessage ?? Buffer.concat(stderrChunks).toString(),\n status,\n signal,\n // GNU time rounds a fast run to 0.00; the wall time until 'exit' stands in for it (never the\n // grace period spent on the pipes afterwards).\n timeSeconds: timeResult?.timeSeconds || wallTimeSeconds,\n cpuTimeSeconds: timeResult?.cpuTimeSeconds ?? 0,\n memoryBytes: timeResult?.memoryBytes ?? 0,\n timeCommandMessage: timeResult?.message,\n timedOut: timedOut || endedByTimeout(status, wallTimeSeconds, context.timeLimitSeconds),\n outputLimitExceeded,\n };\n } finally {\n await timeOutput?.close();\n }\n}\n\n/**\n * Whether `timeout` ended the run: 124 says it did so at the limit, and 137 that the run ignored its\n * SIGTERM and had to be killed after the grace period. A program can exit with either status on its\n * own, so each is trusted only once the wall time reached the moment `timeout` would have produced it.\n */\nfunction endedByTimeout(status: number | undefined, wallTimeSeconds: number, timeLimitSeconds: number): boolean {\n if (status === TIMEOUT_EXIT_STATUS) return wallTimeSeconds >= timeLimitSeconds;\n if (status === KILLED_AFTER_TIMEOUT_EXIT_STATUS) {\n return wallTimeSeconds >= timeLimitSeconds + killGracePeriodMilliseconds / 1000;\n }\n return false;\n}\n\nfunction killSubprocessGroup(subprocess: childProcess.ChildProcess, signal: NodeJS.Signals): void {\n if (subprocess.pid === undefined) return;\n\n try {\n if (process.platform === 'win32') {\n subprocess.kill(signal);\n return;\n }\n process.kill(-subprocess.pid, signal);\n } catch (error) {\n if (!hasErrorCode(error, 'ESRCH') && !hasErrorCode(error, 'EPERM')) throw error;\n }\n}\n\n// The file is unlinked before it is returned, so nothing can reach it by path anymore.\nasync function openUnlinkedFile(prefix: string): Promise<fs.FileHandle> {\n const directory = await fs.mkdtemp(path.join(os.tmpdir(), prefix));\n try {\n return await fs.open(path.join(directory, 'record'), 'wx+', 0o600);\n } finally {\n await fs.rm(directory, { recursive: true, force: true });\n }\n}\n\nasync function readTail(file: fs.FileHandle, maxBytes: number): Promise<string> {\n const { size } = await file.stat();\n const length = Math.min(size, maxBytes);\n const { buffer } = await file.read(Buffer.alloc(length), 0, length, size - length);\n return buffer.toString();\n}\n\n// Empty when the command was killed before GNU time wrote its record.\nfunction parseTimeOutput(\n content: string\n): { timeSeconds: number; cpuTimeSeconds: number; memoryBytes: number; message: string | undefined } | undefined {\n const match = /(?:^|\\n)(\\d+(?:[.,]\\d+)?) (\\d+(?:[.,]\\d+)?) (\\d+(?:[.,]\\d+)?) (\\d+)\\s*$/.exec(content);\n if (!match) return undefined;\n\n return {\n timeSeconds: parseSeconds(match[1]!),\n cpuTimeSeconds: parseSeconds(match[2]!) + parseSeconds(match[3]!),\n memoryBytes: Number(match[4]) * 1024,\n message: content.slice(0, match.index).trim() || undefined,\n };\n}\n\n// GNU time formats its seconds with the locale's decimal separator.\nfunction parseSeconds(text: string): number {\n return Number(text.replace(',', '.'));\n}\n\nfunction resolveTimeCommand(): readonly [string, ...string[]] | undefined {\n const command = os.platform() === 'darwin' ? 'gtime' : '/usr/bin/time';\n const result = childProcess.spawnSync(command, ['--version'], { stdio: 'ignore' });\n if (result.error || result.status !== 0) return undefined;\n\n // Wall seconds, user CPU seconds, system CPU seconds, peak resident set size in KiB.\n return [command, '--format', '%e %U %S %M'];\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"spawnWithLimits.js","names":["path","fs","os","childProcess","killGracePeriodMilliseconds","timeCommand","resolveTimeCommand","isTimeCommandAvailable","liveSubprocesses","subprocess","killSubprocessGroup","spawnWithLimits","timeOutput","openUnlinkedFile","detached","boundedCommand","context","command","spawnedCommand","timeLimitMilliseconds","startTimeMilliseconds","stdoutChunks","stderrChunks","outputBytes","timedOut","outputLimitExceeded","wallTimeSeconds","spawnErrorMessage","appendOutputChunk","chunk","remainingBytes","appendedChunk","chunks","timeout","killTimeout","settled","pendingError","closeTimeout","settle","reject","resolve","code","exitSignal","failAfterClose","error","timeResult","parseTimeOutput","readTail","endedByTimeout","status","timeLimitSeconds","signal","hasErrorCode","directory","prefix","file","length","size","maxBytes","buffer","match","content","parseSeconds","text","result"],"sources":["../../src/helpers/spawnWithLimits.ts"],"sourcesContent":["import childProcess from 'node:child_process';\nimport fs from 'node:fs/promises';\nimport os from 'node:os';\nimport path from 'node:path';\n\nimport { hasErrorCode } from '@willbooster/shared-lib';\n\nexport interface SpawnWithLimitsResult {\n stdout: string;\n stderr: string;\n status: number | undefined;\n signal: NodeJS.Signals | undefined;\n /** Wall time measured by GNU time, or measured here until the command exited when it produced none. */\n timeSeconds: number;\n /**\n * User plus system CPU time of the command and the descendants it waited for, measured by GNU\n * time, or 0 when it produced no measurement. A run `timeout` ended is measured too, so a caller\n * can tell a program that used up its time limit from one that waited for the CPU.\n */\n cpuTimeSeconds: number;\n /**\n * Peak resident set size measured by GNU time, or 0 when it produced no measurement. GNU time\n * measures its child `timeout` and the descendants it waited for, so the value is at least\n * `timeout`'s own footprint (about 1 MiB), which only shows for the smallest native programs.\n */\n memoryBytes: number;\n /** The note GNU time adds when the command exits abnormally, e.g. `Command terminated by signal 11`. */\n timeCommandMessage: string | undefined;\n timedOut: boolean;\n outputLimitExceeded: boolean;\n}\n\nconst killGracePeriodMilliseconds = 1000;\nconst MIN_TIMEOUT_SECONDS = 0.001;\n// GNU `timeout` exits with 124 after ending a run at its limit, or with 128 + SIGKILL when the run\n// ignored its SIGTERM and it had to be killed after the grace period.\nconst TIMEOUT_EXIT_STATUS = 124;\nconst KILLED_AFTER_TIMEOUT_EXIT_STATUS = 137;\n// GNU time writes its record to a file this process opened and unlinked at once, handed down as fd 3\n// of the command and reopened by GNU time through `/dev/fd`: the command runs as the same OS user,\n// so a record reachable by path could be swapped for a fake or for a FIFO that blocks the read.\nconst TIME_OUTPUT_FD = 3;\n// The command inherits that descriptor too, so only the tail that can hold GNU time's record is read.\nconst TIME_OUTPUT_TAIL_BYTES = 4096;\nconst timeCommand = resolveTimeCommand();\n\n/** Whether GNU time is available, i.e. whether `timeSeconds`, `cpuTimeSeconds`, and `memoryBytes` are measured at all. */\nexport const isTimeCommandAvailable = timeCommand !== undefined;\n\n// The commands run in their own sessions, so they outlive this process unless it ends them itself\n// on the way out (e.g. a preset's SIGINT handler calling `process.exit`).\nconst liveSubprocesses = new Set<childProcess.ChildProcess>();\nprocess.once('exit', () => {\n for (const subprocess of liveSubprocesses) killSubprocessGroup(subprocess, 'SIGKILL');\n});\n\n/**\n * Runs a command in its own process group with `stdin` piped in, killing the whole group once it\n * exceeds the time or output limit, and reports the wall time, CPU time, and peak memory measured\n * by GNU time.\n */\nexport async function spawnWithLimits(\n command: readonly [string, ...string[]],\n context: {\n cwd: string;\n env: NodeJS.ProcessEnv;\n outputLimitBytes: number;\n stdin: string;\n timeLimitSeconds: number;\n }\n): Promise<SpawnWithLimitsResult> {\n const timeOutput = timeCommand === undefined ? undefined : await openUnlinkedFile('exercode-time-');\n try {\n const detached = process.platform !== 'win32';\n // The command runs in its own session so the group kill below cannot hit this process, but that\n // also puts it out of reach of whoever kills this process. GNU `timeout` keeps the run bounded\n // in that case, and it is what normally ends a run at its limit: GNU time wraps it, so a run\n // that hit the limit still gets its CPU time recorded (a group kill from here would take GNU\n // time down with the command), and `--foreground` keeps the command in this group so the kill\n // below still reaches every descendant. The timers below only back `timeout` up. Should this\n // process die first, `timeout` ends the command alone (`--foreground` signals no descendant),\n // an accepted gap: the judge server sweeps every process of the request's sandbox user anyway.\n const boundedCommand: readonly [string, ...string[]] = detached\n ? [\n 'timeout',\n '--foreground',\n '-k',\n String(killGracePeriodMilliseconds / 1000),\n // A zero duration would disable `timeout`; the front matter allows a zero time limit.\n String(Math.max(context.timeLimitSeconds, MIN_TIMEOUT_SECONDS)),\n ...command,\n ]\n : command;\n const spawnedCommand: readonly [string, ...string[]] =\n timeCommand === undefined\n ? boundedCommand\n : [...timeCommand, `--output=/dev/fd/${TIME_OUTPUT_FD}`, ...boundedCommand];\n // `timeout` sends SIGKILL a grace period after its SIGTERM; the timers here fire once even that\n // should have ended the run.\n const timeLimitMilliseconds = detached\n ? context.timeLimitSeconds * 1000 + 2 * killGracePeriodMilliseconds\n : context.timeLimitSeconds * 1000;\n // Monotonic: a wall-clock step must not turn a run `timeout` ended into one that finished in time.\n const startTimeMilliseconds = performance.now();\n // The standard streams are always pipes; the type cannot express that with the extra entry.\n const subprocess = childProcess.spawn(spawnedCommand[0], spawnedCommand.slice(1), {\n cwd: context.cwd,\n detached,\n env: context.env,\n stdio: timeOutput === undefined ? ['pipe', 'pipe', 'pipe'] : ['pipe', 'pipe', 'pipe', timeOutput.fd],\n }) as childProcess.ChildProcessWithoutNullStreams;\n liveSubprocesses.add(subprocess);\n\n const stdoutChunks: Buffer[] = [];\n const stderrChunks: Buffer[] = [];\n let outputBytes = 0;\n let timedOut = false;\n let outputLimitExceeded = false;\n let wallTimeSeconds = 0;\n let spawnErrorMessage: string | undefined;\n\n const appendOutputChunk = (chunks: Buffer[], chunk: Buffer): void => {\n if (outputBytes >= context.outputLimitBytes) {\n if (chunk.byteLength > 0) {\n outputLimitExceeded = true;\n killSubprocessGroup(subprocess, 'SIGKILL');\n }\n return;\n }\n\n const remainingBytes = context.outputLimitBytes - outputBytes;\n const appendedChunk = chunk.byteLength > remainingBytes ? chunk.subarray(0, remainingBytes) : chunk;\n chunks.push(appendedChunk);\n outputBytes += appendedChunk.byteLength;\n\n if (chunk.byteLength > remainingBytes) {\n outputLimitExceeded = true;\n killSubprocessGroup(subprocess, 'SIGKILL');\n }\n };\n\n subprocess.stdout.on('data', (chunk: Buffer) => appendOutputChunk(stdoutChunks, chunk));\n subprocess.stderr.on('data', (chunk: Buffer) => appendOutputChunk(stderrChunks, chunk));\n\n const timeout = setTimeout(() => {\n timedOut = true;\n killSubprocessGroup(subprocess, 'SIGTERM');\n }, timeLimitMilliseconds);\n const killTimeout = setTimeout(() => {\n if (timedOut) killSubprocessGroup(subprocess, 'SIGKILL');\n }, timeLimitMilliseconds + killGracePeriodMilliseconds);\n killTimeout.unref();\n\n const { status, signal } = await new Promise<{ status: number | undefined; signal: NodeJS.Signals | undefined }>(\n (resolve, reject) => {\n let settled = false;\n let pendingError: Error | undefined;\n let closeTimeout: NodeJS.Timeout | undefined;\n const settle = (code: number | null | undefined, exitSignal: NodeJS.Signals | null | undefined): void => {\n if (settled) return;\n settled = true;\n clearTimeout(closeTimeout);\n if (pendingError) {\n reject(pendingError);\n return;\n }\n resolve({ status: code ?? undefined, signal: exitSignal ?? undefined });\n };\n const failAfterClose = (error: Error): void => {\n if (settled) return;\n if (subprocess.pid === undefined) {\n // The spawn itself failed, i.e. the head of the chain (GNU time, or `timeout` without\n // it, or the command itself on Windows) or the cwd is missing. A missing judged command\n // is reported as status 127 by the wrapper instead. Report this like a run that produced\n // only this message.\n spawnErrorMessage = error.message;\n settle(undefined, undefined);\n return;\n }\n pendingError = error;\n killSubprocessGroup(subprocess, 'SIGKILL');\n };\n subprocess.on('error', failAfterClose);\n subprocess.stdin.on('error', (error: NodeJS.ErrnoException) => {\n if (error.code !== 'EPIPE') failAfterClose(error);\n });\n subprocess.on('exit', (code, exitSignal) => {\n // The command is gone, so the limit timers must not fire during the grace period below. A\n // descendant that inherited the pipes (e.g. `sleep 1000 &`) would keep them open and hold\n // back 'close' until it ends: kill what is left of the group. A descendant that moved to\n // its own session survives that, so stop waiting for the pipes after the grace period and\n // keep the output read so far.\n clearTimeout(timeout);\n clearTimeout(killTimeout);\n wallTimeSeconds = (performance.now() - startTimeMilliseconds) / 1000;\n killSubprocessGroup(subprocess, 'SIGKILL');\n closeTimeout = setTimeout(() => {\n subprocess.stdout.destroy();\n subprocess.stderr.destroy();\n settle(code, exitSignal);\n }, killGracePeriodMilliseconds);\n });\n subprocess.on('close', settle);\n subprocess.stdin.end(context.stdin);\n }\n ).finally(() => {\n clearTimeout(timeout);\n clearTimeout(killTimeout);\n liveSubprocesses.delete(subprocess);\n });\n\n const timeResult =\n timeOutput === undefined ? undefined : parseTimeOutput(await readTail(timeOutput, TIME_OUTPUT_TAIL_BYTES));\n\n return {\n stdout: Buffer.concat(stdoutChunks).toString(),\n stderr: spawnErrorMessage ?? Buffer.concat(stderrChunks).toString(),\n status,\n signal,\n // GNU time rounds a fast run to 0.00; the wall time until 'exit' stands in for it (never the\n // grace period spent on the pipes afterwards).\n timeSeconds: timeResult?.timeSeconds || wallTimeSeconds,\n cpuTimeSeconds: timeResult?.cpuTimeSeconds ?? 0,\n memoryBytes: timeResult?.memoryBytes ?? 0,\n timeCommandMessage: timeResult?.message,\n timedOut: timedOut || endedByTimeout(status, wallTimeSeconds, context.timeLimitSeconds),\n outputLimitExceeded,\n };\n } finally {\n await timeOutput?.close();\n }\n}\n\n/**\n * Whether `timeout` ended the run: 124 says it did so at the limit, and 137 that the run ignored its\n * SIGTERM and had to be killed after the grace period. A program can exit with either status on its\n * own, so each is trusted only once the wall time reached the moment `timeout` would have produced it.\n */\nfunction endedByTimeout(status: number | undefined, wallTimeSeconds: number, timeLimitSeconds: number): boolean {\n if (status === TIMEOUT_EXIT_STATUS) return wallTimeSeconds >= timeLimitSeconds;\n if (status === KILLED_AFTER_TIMEOUT_EXIT_STATUS) {\n return wallTimeSeconds >= timeLimitSeconds + killGracePeriodMilliseconds / 1000;\n }\n return false;\n}\n\nfunction killSubprocessGroup(subprocess: childProcess.ChildProcess, signal: NodeJS.Signals): void {\n if (subprocess.pid === undefined) return;\n\n try {\n if (process.platform === 'win32') {\n subprocess.kill(signal);\n return;\n }\n process.kill(-subprocess.pid, signal);\n } catch (error) {\n if (!hasErrorCode(error, 'ESRCH') && !hasErrorCode(error, 'EPERM')) throw error;\n }\n}\n\n// The file is unlinked before it is returned, so nothing can reach it by path anymore.\nasync function openUnlinkedFile(prefix: string): Promise<fs.FileHandle> {\n const directory = await fs.mkdtemp(path.join(os.tmpdir(), prefix));\n try {\n return await fs.open(path.join(directory, 'record'), 'wx+', 0o600);\n } finally {\n await fs.rm(directory, { recursive: true, force: true });\n }\n}\n\nasync function readTail(file: fs.FileHandle, maxBytes: number): Promise<string> {\n const { size } = await file.stat();\n const length = Math.min(size, maxBytes);\n const { buffer } = await file.read(Buffer.alloc(length), 0, length, size - length);\n return buffer.toString();\n}\n\n// Empty when the command was killed before GNU time wrote its record.\nfunction parseTimeOutput(\n content: string\n): { timeSeconds: number; cpuTimeSeconds: number; memoryBytes: number; message: string | undefined } | undefined {\n const match = /(?:^|\\n)(\\d+(?:[.,]\\d+)?) (\\d+(?:[.,]\\d+)?) (\\d+(?:[.,]\\d+)?) (\\d+)\\s*$/.exec(content);\n if (!match) return undefined;\n\n return {\n timeSeconds: parseSeconds(match[1]!),\n cpuTimeSeconds: parseSeconds(match[2]!) + parseSeconds(match[3]!),\n memoryBytes: Number(match[4]) * 1024,\n message: content.slice(0, match.index).trim() || undefined,\n };\n}\n\n// GNU time formats its seconds with the locale's decimal separator.\nfunction parseSeconds(text: string): number {\n return Number(text.replace(',', '.'));\n}\n\nfunction resolveTimeCommand(): readonly [string, ...string[]] | undefined {\n const command = os.platform() === 'darwin' ? 'gtime' : '/usr/bin/time';\n const result = childProcess.spawnSync(command, ['--version'], { stdio: 'ignore' });\n if (result.error || result.status !== 0) return undefined;\n\n // Wall seconds, user CPU seconds, system CPU seconds, peak resident set size in KiB.\n return [command, '--format', '%e %U %S %M'];\n}\n"],"mappings":"A,yKAgCA,MAAMI,EAA8B,IAY9BC,EAAcC,EAAmB,EAG1BC,EAAyBF,IAAgB,IAAA,GAIhDG,kBAAmB,IAAI,IAC7B,QAAQ,KAAK,WAAc,CACzB,IAAK,IAAMC,KAAcD,EAAkBE,EAAoBD,EAAY,SAAS,CACtF,CAAC,EAOD,eAAsBE,EACpB,EACA,EAOgC,CAChC,IAAMC,EAAaP,IAAgB,IAAA,GAAY,IAAA,GAAY,MAAMQ,EAAiB,gBAAgB,EAClG,GAAI,CACF,IAAMC,EAAW,QAAQ,WAAa,QAShCC,EAAiDD,EACnD,CACE,UACA,eACA,KACA,IAEA,OAAO,KAAK,IAAIE,EAAQ,iBAAkB,IAAmB,CAAC,EAC9D,GAAGC,CACL,EACAA,EACEC,EACJb,IAAgB,IAAA,GACZU,EACA,CAAC,GAAGV,EAAa,qBAAsC,GAAGU,CAAc,EAGxEI,EAAwBL,EAC1BE,EAAQ,iBAAmB,IAAO,EAAIZ,EACtCY,EAAQ,iBAAmB,IAEzBI,EAAwB,YAAY,IAAI,EAExCX,EAAaN,EAAa,MAAMe,EAAe,GAAIA,EAAe,MAAM,CAAC,EAAG,CAChF,IAAKF,EAAQ,IACb,WACA,IAAKA,EAAQ,IACb,MAAOJ,IAAe,IAAA,GAAY,CAAC,OAAQ,OAAQ,MAAM,EAAI,CAAC,OAAQ,OAAQ,OAAQA,EAAW,EAAE,CACrG,CAAC,EACDJ,EAAiB,IAAIC,CAAU,EAE/B,IAAMY,EAAyB,CAAC,EAC1BC,EAAyB,CAAC,EAC5BC,EAAc,EACdC,EAAW,GACXC,EAAsB,GACtBC,EAAkB,EAClBC,EAEEC,GAAqB,EAAkB,IAAwB,CACnE,GAAIL,GAAeP,EAAQ,iBAAkB,CACvCa,EAAM,WAAa,IACrB,EAAsB,GACtBnB,EAAoBD,EAAY,SAAS,GAE3C,MACF,CAEA,IAAMqB,EAAiBd,EAAQ,iBAAmBO,EAC5CQ,EAAgBF,EAAM,WAAaC,EAAiBD,EAAM,SAAS,EAAGC,CAAc,EAAID,EAC9FG,EAAO,KAAKD,CAAa,EACzB,GAAeA,EAAc,WAEzBF,EAAM,WAAaC,IACrB,EAAsB,GACtBpB,EAAoBD,EAAY,SAAS,EAE7C,EAEAA,EAAW,OAAO,GAAG,OAAS,GAAkBmB,EAAkBP,EAAcQ,CAAK,CAAC,EACtFpB,EAAW,OAAO,GAAG,OAAS,GAAkBmB,EAAkBN,EAAcO,CAAK,CAAC,EAEtF,IAAMI,EAAU,eAAiB,CAC/B,EAAW,GACXvB,EAAoBD,EAAY,SAAS,CAC3C,EAAGU,CAAqB,EAClBe,EAAc,eAAiB,CAC/BV,GAAUd,EAAoBD,EAAY,SAAS,CACzD,EAAGU,EAAwBf,CAA2B,EACtD8B,EAAY,MAAM,EAElB,GAAM,CAAE,SAAQ,UAAW,MAAM,IAAI,SAClC,EAAS,IAAW,CACnB,IAAIC,EAAU,GACVC,EACAC,EACEC,GAAU,EAAiC,IAAwD,CACnGH,IACJ,EAAU,GACV,aAAaE,CAAY,EACrBD,EACFG,EAAOH,CAAY,EAGrBI,EAAQ,CAAE,OAAQC,GAAQ,IAAA,GAAW,OAAQC,GAAc,IAAA,EAAU,CAAC,EACxE,EACMC,EAAkB,GAAuB,CACzCR,IACA1B,EAAW,MAAQ,IAAA,IAKrB,EAAoBmC,EAAM,QAC1BN,EAAO,IAAA,GAAW,IAAA,EAAS,IAG7B,EAAeM,EACflC,EAAoBD,EAAY,SAAS,GAC3C,EACAA,EAAW,GAAG,QAASkC,CAAc,EACrClC,EAAW,MAAM,GAAG,QAAU,GAAiC,CACzDmC,EAAM,OAAS,SAASD,EAAeC,CAAK,CAClD,CAAC,EACDnC,EAAW,GAAG,QAAS,EAAM,IAAe,CAM1C,aAAawB,CAAO,EACpB,aAAaC,CAAW,EACxB,GAAmB,YAAY,IAAI,EAAId,GAAyB,IAChEV,EAAoBD,EAAY,SAAS,EACzC,EAAe,eAAiB,CAC9BA,EAAW,OAAO,QAAQ,EAC1BA,EAAW,OAAO,QAAQ,EAC1B6B,EAAOG,EAAMC,CAAU,CACzB,EAAGtC,CAA2B,CAChC,CAAC,EACDK,EAAW,GAAG,QAAS6B,CAAM,EAC7B7B,EAAW,MAAM,IAAIO,EAAQ,KAAK,CACpC,CACF,CAAC,CAAC,YAAc,CACd,aAAaiB,CAAO,EACpB,aAAaC,CAAW,EACxB1B,EAAiB,OAAOC,CAAU,CACpC,CAAC,EAEKoC,EACJjC,IAAe,IAAA,GAAY,IAAA,GAAYkC,EAAgB,MAAMC,EAASnC,EAAY,IAAsB,CAAC,EAE3G,MAAO,CACL,OAAQ,OAAO,OAAOS,CAAY,CAAC,CAAC,SAAS,EAC7C,OAAQM,GAAqB,OAAO,OAAOL,CAAY,CAAC,CAAC,SAAS,EAClE,SACA,SAGA,YAAauB,GAAY,aAAenB,EACxC,eAAgBmB,GAAY,gBAAkB,EAC9C,YAAaA,GAAY,aAAe,EACxC,mBAAoBA,GAAY,QAChC,SAAUrB,GAAYwB,EAAeC,EAAQvB,EAAiBV,EAAQ,gBAAgB,EACtF,qBACF,CACF,QAAU,CACR,MAAMJ,GAAY,MAAM,CAC1B,CACF,CAOA,SAASoC,EAAe,EAA4B,EAAyB,EAAmC,CAK9G,OAJIC,IAAW,IAA4BvB,GAAmBwB,EAC1DD,IAAW,KACNvB,GAAmBwB,EAAmB9C,EAA8B,GAG/E,CAEA,SAASM,EAAoB,EAAuC,EAA8B,CAChG,GAAID,EAAW,MAAQ,IAAA,GAEvB,GAAI,CACF,GAAI,QAAQ,WAAa,QAAS,CAChCA,EAAW,KAAK0C,CAAM,EACtB,MACF,CACA,QAAQ,KAAK,CAAC1C,EAAW,IAAK0C,CAAM,CACtC,OAASP,EAAO,CACd,GAAI,CAACQ,EAAaR,EAAO,OAAO,GAAK,CAACQ,EAAaR,EAAO,OAAO,EAAG,MAAMA,CAC5E,CACF,CAGA,eAAe/B,EAAiB,EAAwC,CACtE,IAAMwC,EAAY,MAAMpD,EAAG,QAAQD,EAAK,KAAKE,EAAG,OAAO,EAAGoD,CAAM,CAAC,EACjE,GAAI,CACF,OAAO,MAAMrD,EAAG,KAAKD,EAAK,KAAKqD,EAAW,QAAQ,EAAG,MAAO,GAAK,CACnE,QAAU,CACR,MAAMpD,EAAG,GAAGoD,EAAW,CAAE,UAAW,GAAM,MAAO,EAAK,CAAC,CACzD,CACF,CAEA,eAAeN,EAAS,EAAqB,EAAmC,CAC9E,GAAM,CAAE,QAAS,MAAMQ,EAAK,KAAK,EAC3BC,EAAS,KAAK,IAAIC,EAAMC,CAAQ,EAChC,CAAE,UAAW,MAAMH,EAAK,KAAK,OAAO,MAAMC,CAAM,EAAG,EAAGA,EAAQC,EAAOD,CAAM,EACjF,OAAOG,EAAO,SAAS,CACzB,CAGA,SAASb,EACP,EAC+G,CAC/G,IAAMc,EAAQ,0EAA0E,KAAKC,CAAO,EACpG,GAAKD,EAEL,MAAO,CACL,YAAaE,EAAaF,EAAM,EAAG,EACnC,eAAgBE,EAAaF,EAAM,EAAG,EAAIE,EAAaF,EAAM,EAAG,EAChE,YAAa,OAAOA,EAAM,EAAE,EAAI,KAChC,QAASC,EAAQ,MAAM,EAAGD,EAAM,KAAK,CAAC,CAAC,KAAK,GAAK,IAAA,EACnD,CACF,CAGA,SAASE,EAAa,EAAsB,CAC1C,OAAO,OAAOC,EAAK,QAAQ,IAAK,GAAG,CAAC,CACtC,CAEA,SAASzD,GAAiE,CACxE,IAAMW,EAAUf,EAAG,SAAS,IAAM,SAAW,QAAU,gBACjD8D,EAAS7D,EAAa,UAAUc,EAAS,CAAC,WAAW,EAAG,CAAE,MAAO,QAAS,CAAC,EACjF,GAAI,IAAO,OAAS+C,EAAO,SAAW,GAGtC,MAAO,CAAC/C,EAAS,WAAY,aAAa,CAC5C,C"}
@@ -1 +1 @@
1
- {"version":3,"file":"spawnWithTimeout.cjs","names":["isTimeCommandAvailable","spawnWithLimits","require_spawnWithLimits","spawnWithTimeout","result","command","args","context","timeoutSeconds","stderr"],"sources":["../../src/helpers/spawnWithTimeout.ts"],"sourcesContent":["import { isTimeCommandAvailable, spawnWithLimits } from './spawnWithLimits.js';\n\nconst OUTPUT_LIMIT_BYTES = 8 * 1024 * 1024;\n\n/**\n * Runs a submission-derived command under a time limit and an output cap. A run that hits a limit is\n * reported like a normal exit (status 0), with `timeSeconds` just above the limit or the output\n * truncated at the cap, so callers judge it by the limit it hit. A missing command is reported by\n * GNU time as status 127; a spawn that fails outright (e.g. a missing cwd) is reported with no\n * status and the error as stderr; a failure on the judge's side throws.\n */\nexport async function spawnWithTimeout(\n command: string,\n args: readonly string[],\n context: { cwd: string; env: NodeJS.ProcessEnv; stdin?: string },\n timeoutSeconds: number\n): Promise<{\n stdout: string;\n stderr: string;\n status: number | undefined;\n timeSeconds: number;\n /** See `SpawnWithLimitsResult.cpuTimeSeconds`. */\n cpuTimeSeconds: number;\n memoryBytes: number;\n outputLimitExceeded: boolean;\n}> {\n // Without GNU time's measurements, the memory limit would silently never apply and the reported\n // time would be this process's wall clock rather than the program's.\n if (!isTimeCommandAvailable) throw new Error('GNU time (gtime on macOS, /usr/bin/time on Linux) is required.');\n\n const result = await spawnWithLimits([command, ...args], {\n cwd: context.cwd,\n env: context.env,\n outputLimitBytes: OUTPUT_LIMIT_BYTES,\n stdin: context.stdin ?? '',\n timeLimitSeconds: timeoutSeconds,\n });\n\n // Keep GNU time's note about an abnormal exit (e.g. a segmentation fault) visible to the learner;\n // the note about `timeout`'s own exit status after ending the run says nothing to them.\n const stderr =\n result.timeCommandMessage && !result.timedOut\n ? `${result.stderr}${result.stderr && !result.stderr.endsWith('\\n') ? '\\n' : ''}${result.timeCommandMessage}\\n`\n : result.stderr;\n\n return {\n stdout: result.stdout,\n stderr,\n status: result.timedOut || result.outputLimitExceeded ? 0 : result.status,\n timeSeconds: result.timedOut ? timeoutSeconds + 1e-3 : result.timeSeconds,\n cpuTimeSeconds: result.cpuTimeSeconds,\n memoryBytes: result.memoryBytes,\n outputLimitExceeded: result.outputLimitExceeded,\n };\n}\n"],"mappings":"sDAWA,eAAsBG,EACpB,EACA,EACA,EACA,EAUC,CAGD,GAAI,CAACH,EAAAA,uBAAwB,MAAU,MAAM,gEAAgE,EAE7G,IAAMI,EAAS,MAAMH,EAAAA,gBAAgB,CAACI,EAAS,GAAGC,CAAI,EAAG,CACvD,IAAKC,EAAQ,IACb,IAAKA,EAAQ,IACb,iBAAkB,QAClB,MAAOA,EAAQ,OAAS,GACxB,iBAAkBC,CACpB,CAAC,EAIKC,EACJL,EAAO,oBAAsB,CAACA,EAAO,SACjC,GAAGA,EAAO,SAASA,EAAO,QAAU,CAACA,EAAO,OAAO,SAAS;CAAI,EAAI;EAAO,KAAKA,EAAO,mBAAmB,IAC1GA,EAAO,OAEb,MAAO,CACL,OAAQA,EAAO,OACf,SACA,OAAQA,EAAO,UAAYA,EAAO,oBAAsB,EAAIA,EAAO,OACnE,YAAaA,EAAO,SAAWI,EAAiB,KAAOJ,EAAO,YAC9D,eAAgBA,EAAO,eACvB,YAAaA,EAAO,YACpB,oBAAqBA,EAAO,mBAC9B,CACF"}
1
+ {"version":3,"file":"spawnWithTimeout.cjs","names":["isTimeCommandAvailable","spawnWithLimits","require_spawnWithLimits","spawnWithTimeout","result","command","args","context","timeoutSeconds","stderr"],"sources":["../../src/helpers/spawnWithTimeout.ts"],"sourcesContent":["import { isTimeCommandAvailable, spawnWithLimits } from './spawnWithLimits.js';\n\nconst OUTPUT_LIMIT_BYTES = 8 * 1024 * 1024;\n\n/**\n * Runs a submission-derived command under a time limit and an output cap. A run that hits a limit is\n * reported like a normal exit (status 0), with `timeSeconds` just above the limit or the output\n * truncated at the cap, so callers judge it by the limit it hit. A missing command is reported by\n * GNU time as status 127; a spawn that fails outright (e.g. a missing cwd) is reported with no\n * status and the error as stderr; a failure on the judge's side throws.\n */\nexport async function spawnWithTimeout(\n command: string,\n args: readonly string[],\n context: { cwd: string; env: NodeJS.ProcessEnv; stdin?: string },\n timeoutSeconds: number\n): Promise<{\n stdout: string;\n stderr: string;\n status: number | undefined;\n timeSeconds: number;\n /** See `SpawnWithLimitsResult.cpuTimeSeconds`. */\n cpuTimeSeconds: number;\n memoryBytes: number;\n outputLimitExceeded: boolean;\n}> {\n // Without GNU time's measurements, the memory limit would silently never apply and the reported\n // time would be this process's wall clock rather than the program's.\n if (!isTimeCommandAvailable) throw new Error('GNU time (gtime on macOS, /usr/bin/time on Linux) is required.');\n\n const result = await spawnWithLimits([command, ...args], {\n cwd: context.cwd,\n env: context.env,\n outputLimitBytes: OUTPUT_LIMIT_BYTES,\n stdin: context.stdin ?? '',\n timeLimitSeconds: timeoutSeconds,\n });\n\n // Keep GNU time's note about an abnormal exit (e.g. a segmentation fault) visible to the learner;\n // the note about `timeout`'s own exit status after ending the run says nothing to them.\n const stderr =\n result.timeCommandMessage && !result.timedOut\n ? `${result.stderr}${result.stderr && !result.stderr.endsWith('\\n') ? '\\n' : ''}${result.timeCommandMessage}\\n`\n : result.stderr;\n\n return {\n stdout: result.stdout,\n stderr,\n status: result.timedOut || result.outputLimitExceeded ? 0 : result.status,\n timeSeconds: result.timedOut ? timeoutSeconds + 1e-3 : result.timeSeconds,\n cpuTimeSeconds: result.cpuTimeSeconds,\n memoryBytes: result.memoryBytes,\n outputLimitExceeded: result.outputLimitExceeded,\n };\n}\n"],"mappings":"A,sDAWA,eAAsBG,EACpB,EACA,EACA,EACA,EAUC,CAGD,GAAI,CAACH,EAAAA,uBAAwB,MAAU,MAAM,gEAAgE,EAE7G,IAAMI,EAAS,MAAMH,EAAAA,gBAAgB,CAACI,EAAS,GAAGC,CAAI,EAAG,CACvD,IAAKC,EAAQ,IACb,IAAKA,EAAQ,IACb,iBAAkB,QAClB,MAAOA,EAAQ,OAAS,GACxB,iBAAkBC,CACpB,CAAC,EAIKC,EACJL,EAAO,oBAAsB,CAACA,EAAO,SACjC,GAAGA,EAAO,SAASA,EAAO,QAAU,CAACA,EAAO,OAAO,SAAS;CAAI,EAAI;EAAO,KAAKA,EAAO,mBAAmB,IAC1GA,EAAO,OAEb,MAAO,CACL,OAAQA,EAAO,OACf,SACA,OAAQA,EAAO,UAAYA,EAAO,oBAAsB,EAAIA,EAAO,OACnE,YAAaA,EAAO,SAAWI,EAAiB,KAAOJ,EAAO,YAC9D,eAAgBA,EAAO,eACvB,YAAaA,EAAO,YACpB,oBAAqBA,EAAO,mBAC9B,CACF,C"}
@@ -1 +1 @@
1
- {"version":3,"file":"spawnWithTimeout.js","names":["spawnWithTimeout","isTimeCommandAvailable","result","spawnWithLimits","command","args","context","timeoutSeconds","stderr"],"sources":["../../src/helpers/spawnWithTimeout.ts"],"sourcesContent":["import { isTimeCommandAvailable, spawnWithLimits } from './spawnWithLimits.js';\n\nconst OUTPUT_LIMIT_BYTES = 8 * 1024 * 1024;\n\n/**\n * Runs a submission-derived command under a time limit and an output cap. A run that hits a limit is\n * reported like a normal exit (status 0), with `timeSeconds` just above the limit or the output\n * truncated at the cap, so callers judge it by the limit it hit. A missing command is reported by\n * GNU time as status 127; a spawn that fails outright (e.g. a missing cwd) is reported with no\n * status and the error as stderr; a failure on the judge's side throws.\n */\nexport async function spawnWithTimeout(\n command: string,\n args: readonly string[],\n context: { cwd: string; env: NodeJS.ProcessEnv; stdin?: string },\n timeoutSeconds: number\n): Promise<{\n stdout: string;\n stderr: string;\n status: number | undefined;\n timeSeconds: number;\n /** See `SpawnWithLimitsResult.cpuTimeSeconds`. */\n cpuTimeSeconds: number;\n memoryBytes: number;\n outputLimitExceeded: boolean;\n}> {\n // Without GNU time's measurements, the memory limit would silently never apply and the reported\n // time would be this process's wall clock rather than the program's.\n if (!isTimeCommandAvailable) throw new Error('GNU time (gtime on macOS, /usr/bin/time on Linux) is required.');\n\n const result = await spawnWithLimits([command, ...args], {\n cwd: context.cwd,\n env: context.env,\n outputLimitBytes: OUTPUT_LIMIT_BYTES,\n stdin: context.stdin ?? '',\n timeLimitSeconds: timeoutSeconds,\n });\n\n // Keep GNU time's note about an abnormal exit (e.g. a segmentation fault) visible to the learner;\n // the note about `timeout`'s own exit status after ending the run says nothing to them.\n const stderr =\n result.timeCommandMessage && !result.timedOut\n ? `${result.stderr}${result.stderr && !result.stderr.endsWith('\\n') ? '\\n' : ''}${result.timeCommandMessage}\\n`\n : result.stderr;\n\n return {\n stdout: result.stdout,\n stderr,\n status: result.timedOut || result.outputLimitExceeded ? 0 : result.status,\n timeSeconds: result.timedOut ? timeoutSeconds + 1e-3 : result.timeSeconds,\n cpuTimeSeconds: result.cpuTimeSeconds,\n memoryBytes: result.memoryBytes,\n outputLimitExceeded: result.outputLimitExceeded,\n };\n}\n"],"mappings":"mFAWA,eAAsBA,EACpB,EACA,EACA,EACA,EAUC,CAGD,GAAI,CAACC,EAAwB,MAAU,MAAM,gEAAgE,EAE7G,IAAMC,EAAS,MAAMC,EAAgB,CAACC,EAAS,GAAGC,CAAI,EAAG,CACvD,IAAKC,EAAQ,IACb,IAAKA,EAAQ,IACb,iBAAkB,QAClB,MAAOA,EAAQ,OAAS,GACxB,iBAAkBC,CACpB,CAAC,EAIKC,EACJN,EAAO,oBAAsB,CAACA,EAAO,SACjC,GAAGA,EAAO,SAASA,EAAO,QAAU,CAACA,EAAO,OAAO,SAAS;CAAI,EAAI;EAAO,KAAKA,EAAO,mBAAmB,IAC1GA,EAAO,OAEb,MAAO,CACL,OAAQA,EAAO,OACf,SACA,OAAQA,EAAO,UAAYA,EAAO,oBAAsB,EAAIA,EAAO,OACnE,YAAaA,EAAO,SAAWK,EAAiB,KAAOL,EAAO,YAC9D,eAAgBA,EAAO,eACvB,YAAaA,EAAO,YACpB,oBAAqBA,EAAO,mBAC9B,CACF"}
1
+ {"version":3,"file":"spawnWithTimeout.js","names":["spawnWithTimeout","isTimeCommandAvailable","result","spawnWithLimits","command","args","context","timeoutSeconds","stderr"],"sources":["../../src/helpers/spawnWithTimeout.ts"],"sourcesContent":["import { isTimeCommandAvailable, spawnWithLimits } from './spawnWithLimits.js';\n\nconst OUTPUT_LIMIT_BYTES = 8 * 1024 * 1024;\n\n/**\n * Runs a submission-derived command under a time limit and an output cap. A run that hits a limit is\n * reported like a normal exit (status 0), with `timeSeconds` just above the limit or the output\n * truncated at the cap, so callers judge it by the limit it hit. A missing command is reported by\n * GNU time as status 127; a spawn that fails outright (e.g. a missing cwd) is reported with no\n * status and the error as stderr; a failure on the judge's side throws.\n */\nexport async function spawnWithTimeout(\n command: string,\n args: readonly string[],\n context: { cwd: string; env: NodeJS.ProcessEnv; stdin?: string },\n timeoutSeconds: number\n): Promise<{\n stdout: string;\n stderr: string;\n status: number | undefined;\n timeSeconds: number;\n /** See `SpawnWithLimitsResult.cpuTimeSeconds`. */\n cpuTimeSeconds: number;\n memoryBytes: number;\n outputLimitExceeded: boolean;\n}> {\n // Without GNU time's measurements, the memory limit would silently never apply and the reported\n // time would be this process's wall clock rather than the program's.\n if (!isTimeCommandAvailable) throw new Error('GNU time (gtime on macOS, /usr/bin/time on Linux) is required.');\n\n const result = await spawnWithLimits([command, ...args], {\n cwd: context.cwd,\n env: context.env,\n outputLimitBytes: OUTPUT_LIMIT_BYTES,\n stdin: context.stdin ?? '',\n timeLimitSeconds: timeoutSeconds,\n });\n\n // Keep GNU time's note about an abnormal exit (e.g. a segmentation fault) visible to the learner;\n // the note about `timeout`'s own exit status after ending the run says nothing to them.\n const stderr =\n result.timeCommandMessage && !result.timedOut\n ? `${result.stderr}${result.stderr && !result.stderr.endsWith('\\n') ? '\\n' : ''}${result.timeCommandMessage}\\n`\n : result.stderr;\n\n return {\n stdout: result.stdout,\n stderr,\n status: result.timedOut || result.outputLimitExceeded ? 0 : result.status,\n timeSeconds: result.timedOut ? timeoutSeconds + 1e-3 : result.timeSeconds,\n cpuTimeSeconds: result.cpuTimeSeconds,\n memoryBytes: result.memoryBytes,\n outputLimitExceeded: result.outputLimitExceeded,\n };\n}\n"],"mappings":"A,mFAWA,eAAsBA,EACpB,EACA,EACA,EACA,EAUC,CAGD,GAAI,CAACC,EAAwB,MAAU,MAAM,gEAAgE,EAE7G,IAAMC,EAAS,MAAMC,EAAgB,CAACC,EAAS,GAAGC,CAAI,EAAG,CACvD,IAAKC,EAAQ,IACb,IAAKA,EAAQ,IACb,iBAAkB,QAClB,MAAOA,EAAQ,OAAS,GACxB,iBAAkBC,CACpB,CAAC,EAIKC,EACJN,EAAO,oBAAsB,CAACA,EAAO,SACjC,GAAGA,EAAO,SAASA,EAAO,QAAU,CAACA,EAAO,OAAO,SAAS;CAAI,EAAI;EAAO,KAAKA,EAAO,mBAAmB,IAC1GA,EAAO,OAEb,MAAO,CACL,OAAQA,EAAO,OACf,SACA,OAAQA,EAAO,UAAYA,EAAO,oBAAsB,EAAIA,EAAO,OACnE,YAAaA,EAAO,SAAWK,EAAiB,KAAOL,EAAO,YAC9D,eAAgBA,EAAO,eACvB,YAAaA,EAAO,YACpB,oBAAqBA,EAAO,mBAC9B,CACF,C"}
@@ -1 +1 @@
1
- {"version":3,"file":"startHttpServer.cjs","names":["path","http","fs","require_runtime","node_fs","node_path","node_http","startHttpServer","server","createAssetServer","dir","createServerHandle","startLocalHttpServer","reject","resolve","rootPath","pathname","encodedPathname","request","response","pathnameWithIndexHtml","filePath","relativePath","buffer","getContentType","address","CONTENT_TYPE_BY_SUFFIX","suffix","contentType"],"sources":["../../src/helpers/startHttpServer.ts"],"sourcesContent":["import fs from 'node:fs';\nimport http from 'node:http';\nimport path from 'node:path';\n\nexport interface HttpServer {\n [Symbol.asyncDispose](): Promise<void>;\n url: string;\n port: number | undefined;\n}\n\n/** Starts a web-page test server and returns its address synchronously. */\nexport function startHttpServer(dir: string): HttpServer {\n const server = createAssetServer(dir);\n server.listen();\n return createServerHandle(server);\n}\n\n/** Starts an asset server reachable only from the local machine. */\nexport async function startLocalHttpServer(dir: string): Promise<HttpServer> {\n const server = createAssetServer(dir);\n await new Promise<void>((resolve, reject) => {\n server.once('error', reject);\n server.listen(0, '127.0.0.1', () => {\n server.off('error', reject);\n resolve();\n });\n });\n return createServerHandle(server);\n}\n\nfunction createAssetServer(dir: string): http.Server {\n const rootPath = path.resolve(dir);\n return http.createServer((request, response) => {\n let pathname: string;\n try {\n const encodedPathname = new URL(request.url ?? '/', 'http://127.0.0.1').pathname;\n pathname = decodeURIComponent(encodedPathname.replaceAll(/%(?![\\da-f]{2})/giu, '%25'));\n } catch {\n response.writeHead(400);\n response.end();\n return;\n }\n\n if (pathname === '/') {\n pathname = '/index.html';\n }\n\n const pathnameWithIndexHtml = pathname.endsWith('/') ? path.join(pathname, 'index.html') : pathname;\n\n const filePath = path.resolve(rootPath, `.${pathnameWithIndexHtml}`);\n const relativePath = path.relative(rootPath, filePath);\n if (relativePath === '..' || relativePath.startsWith(`..${path.sep}`)) {\n response.writeHead(403);\n response.end();\n return;\n }\n\n if (fs.existsSync(filePath)) {\n try {\n const buffer = fs.readFileSync(filePath);\n response.writeHead(200, {\n 'Access-Control-Allow-Origin': '*',\n Pragma: 'no-cache',\n 'Cache-Control': 'no-cache',\n 'Content-Type': getContentType(pathnameWithIndexHtml),\n });\n response.end(buffer);\n } catch {\n response.statusCode = 500;\n response.end();\n }\n } else {\n response.statusCode = 404;\n response.end();\n }\n });\n}\n\nfunction createServerHandle(server: http.Server): HttpServer {\n const address = server.address();\n if (!address) throw new Error('server has been unexpectedly closed');\n\n return {\n [Symbol.asyncDispose]: async () => {\n server.closeAllConnections();\n await new Promise<void>((resolve) => {\n server.close(() => {\n resolve();\n });\n });\n },\n url: typeof address === 'string' ? address : `http://127.0.0.1:${address.port}`,\n port: typeof address === 'object' ? address.port : undefined,\n };\n}\n\nconst CONTENT_TYPE_BY_SUFFIX: Record<string, string> = {\n '.css': 'text/css',\n '.html': 'text/html',\n '.js': 'text/javascript',\n '.gif': 'image/gif',\n '.jpg': 'image/jpeg',\n '.png': 'image/png',\n '.svg': 'image/svg+xml',\n};\n\nfunction getContentType(pathname: string): string {\n for (const [suffix, contentType] of Object.entries(CONTENT_TYPE_BY_SUFFIX)) {\n if (pathname.endsWith(suffix)) return contentType;\n }\n return 'text/plain';\n}\n"],"mappings":"oMAWA,SAAgBO,EAAgB,EAAyB,CACvD,IAAMC,EAASC,EAAkBC,CAAG,EAEpC,OADAF,EAAO,OAAO,EACPG,EAAmBH,CAAM,CAClC,CAGA,eAAsBI,EAAqB,EAAkC,CAC3E,IAAMJ,EAASC,EAAkBC,CAAG,EAQpC,OAPA,MAAM,IAAI,SAAe,EAAS,IAAW,CAC3CF,EAAO,KAAK,QAASK,CAAM,EAC3BL,EAAO,OAAO,EAAG,gBAAmB,CAClCA,EAAO,IAAI,QAASK,CAAM,EAC1BC,EAAQ,CACV,CAAC,CACH,CAAC,EACMH,EAAmBH,CAAM,CAClC,CAEA,SAASC,EAAkB,EAA0B,CACnD,IAAMM,EAAWf,EAAAA,QAAK,QAAQU,CAAG,EACjC,OAAOT,EAAAA,QAAK,cAAc,EAAS,IAAa,CAC9C,IAAIe,EACJ,GAAI,CACF,IAAMC,EAAkB,IAAI,IAAIC,EAAQ,KAAO,IAAK,kBAAkB,CAAC,CAAC,SACxE,EAAW,mBAAmBD,EAAgB,WAAW,qBAAsB,KAAK,CAAC,CACvF,MAAQ,CACNE,EAAS,UAAU,GAAG,EACtBA,EAAS,IAAI,EACb,MACF,CAEIH,IAAa,MACf,EAAW,eAGb,IAAMI,EAAwBJ,EAAS,SAAS,GAAG,EAAIhB,EAAAA,QAAK,KAAKgB,EAAU,YAAY,EAAIA,EAErFK,EAAWrB,EAAAA,QAAK,QAAQe,EAAU,IAAIK,GAAuB,EAC7DE,EAAetB,EAAAA,QAAK,SAASe,EAAUM,CAAQ,EACrD,GAAIC,IAAiB,MAAQA,EAAa,WAAW,KAAKtB,EAAAA,QAAK,KAAK,EAClEmB,EAAS,UAAU,GAAG,EACtBA,EAAS,IAAI,OAIf,GAAIjB,EAAAA,QAAG,WAAWmB,CAAQ,EACxB,GAAI,CACF,IAAME,EAASrB,EAAAA,QAAG,aAAamB,CAAQ,EACvCF,EAAS,UAAU,IAAK,CACtB,8BAA+B,IAC/B,OAAQ,WACR,gBAAiB,WACjB,eAAgBK,EAAeJ,CAAqB,CACtD,CAAC,EACDD,EAAS,IAAII,CAAM,CACrB,MAAQ,CACN,EAAS,WAAa,IACtBJ,EAAS,IAAI,CACf,KAEA,GAAS,WAAa,IACtBA,EAAS,IAAI,CAEjB,CAAC,CACH,CAEA,SAASR,EAAmB,EAAiC,CAC3D,IAAMc,EAAUjB,EAAO,QAAQ,EAC/B,GAAI,CAACiB,EAAS,MAAU,MAAM,qCAAqC,EAEnE,MAAO,EACJ,OAAO,cAAe,SAAY,CACjCjB,EAAO,oBAAoB,EAC3B,MAAM,IAAI,QAAe,GAAY,CACnCA,EAAO,UAAY,CACjBM,EAAQ,CACV,CAAC,CACH,CAAC,CACH,EACA,IAAK,OAAOW,GAAY,SAAWA,EAAU,oBAAoBA,EAAQ,OACzE,KAAM,OAAOA,GAAY,SAAWA,EAAQ,KAAO,IAAA,EACrD,CACF,CAEA,MAAMC,EAAiD,CACrD,OAAQ,WACR,QAAS,YACT,MAAO,kBACP,OAAQ,YACR,OAAQ,aACR,OAAQ,YACR,OAAQ,eACV,EAEA,SAASF,EAAe,EAA0B,CAChD,IAAK,GAAM,CAACG,EAAQC,KAAgB,OAAO,QAAQF,CAAsB,EACvE,GAAIV,EAAS,SAASW,CAAM,EAAG,OAAOC,EAExC,MAAO,YACT"}
1
+ {"version":3,"file":"startHttpServer.cjs","names":["path","http","fs","require_runtime","node_fs","node_path","node_http","startHttpServer","server","createAssetServer","dir","createServerHandle","startLocalHttpServer","reject","resolve","rootPath","pathname","encodedPathname","request","response","pathnameWithIndexHtml","filePath","relativePath","buffer","getContentType","address","CONTENT_TYPE_BY_SUFFIX","suffix","contentType"],"sources":["../../src/helpers/startHttpServer.ts"],"sourcesContent":["import fs from 'node:fs';\nimport http from 'node:http';\nimport path from 'node:path';\n\nexport interface HttpServer {\n [Symbol.asyncDispose](): Promise<void>;\n url: string;\n port: number | undefined;\n}\n\n/** Starts a web-page test server and returns its address synchronously. */\nexport function startHttpServer(dir: string): HttpServer {\n const server = createAssetServer(dir);\n server.listen();\n return createServerHandle(server);\n}\n\n/** Starts an asset server reachable only from the local machine. */\nexport async function startLocalHttpServer(dir: string): Promise<HttpServer> {\n const server = createAssetServer(dir);\n await new Promise<void>((resolve, reject) => {\n server.once('error', reject);\n server.listen(0, '127.0.0.1', () => {\n server.off('error', reject);\n resolve();\n });\n });\n return createServerHandle(server);\n}\n\nfunction createAssetServer(dir: string): http.Server {\n const rootPath = path.resolve(dir);\n return http.createServer((request, response) => {\n let pathname: string;\n try {\n const encodedPathname = new URL(request.url ?? '/', 'http://127.0.0.1').pathname;\n pathname = decodeURIComponent(encodedPathname.replaceAll(/%(?![\\da-f]{2})/giu, '%25'));\n } catch {\n response.writeHead(400);\n response.end();\n return;\n }\n\n if (pathname === '/') {\n pathname = '/index.html';\n }\n\n const pathnameWithIndexHtml = pathname.endsWith('/') ? path.join(pathname, 'index.html') : pathname;\n\n const filePath = path.resolve(rootPath, `.${pathnameWithIndexHtml}`);\n const relativePath = path.relative(rootPath, filePath);\n if (relativePath === '..' || relativePath.startsWith(`..${path.sep}`)) {\n response.writeHead(403);\n response.end();\n return;\n }\n\n if (fs.existsSync(filePath)) {\n try {\n const buffer = fs.readFileSync(filePath);\n response.writeHead(200, {\n 'Access-Control-Allow-Origin': '*',\n Pragma: 'no-cache',\n 'Cache-Control': 'no-cache',\n 'Content-Type': getContentType(pathnameWithIndexHtml),\n });\n response.end(buffer);\n } catch {\n response.statusCode = 500;\n response.end();\n }\n } else {\n response.statusCode = 404;\n response.end();\n }\n });\n}\n\nfunction createServerHandle(server: http.Server): HttpServer {\n const address = server.address();\n if (!address) throw new Error('server has been unexpectedly closed');\n\n return {\n [Symbol.asyncDispose]: async () => {\n server.closeAllConnections();\n await new Promise<void>((resolve) => {\n server.close(() => {\n resolve();\n });\n });\n },\n url: typeof address === 'string' ? address : `http://127.0.0.1:${address.port}`,\n port: typeof address === 'object' ? address.port : undefined,\n };\n}\n\nconst CONTENT_TYPE_BY_SUFFIX: Record<string, string> = {\n '.css': 'text/css',\n '.html': 'text/html',\n '.js': 'text/javascript',\n '.gif': 'image/gif',\n '.jpg': 'image/jpeg',\n '.png': 'image/png',\n '.svg': 'image/svg+xml',\n};\n\nfunction getContentType(pathname: string): string {\n for (const [suffix, contentType] of Object.entries(CONTENT_TYPE_BY_SUFFIX)) {\n if (pathname.endsWith(suffix)) return contentType;\n }\n return 'text/plain';\n}\n"],"mappings":"A,oMAWA,SAAgBO,EAAgB,EAAyB,CACvD,IAAMC,EAASC,EAAkBC,CAAG,EAEpC,OADAF,EAAO,OAAO,EACPG,EAAmBH,CAAM,CAClC,CAGA,eAAsBI,EAAqB,EAAkC,CAC3E,IAAMJ,EAASC,EAAkBC,CAAG,EAQpC,OAPA,MAAM,IAAI,SAAe,EAAS,IAAW,CAC3CF,EAAO,KAAK,QAASK,CAAM,EAC3BL,EAAO,OAAO,EAAG,gBAAmB,CAClCA,EAAO,IAAI,QAASK,CAAM,EAC1BC,EAAQ,CACV,CAAC,CACH,CAAC,EACMH,EAAmBH,CAAM,CAClC,CAEA,SAASC,EAAkB,EAA0B,CACnD,IAAMM,EAAWf,EAAAA,QAAK,QAAQU,CAAG,EACjC,OAAOT,EAAAA,QAAK,cAAc,EAAS,IAAa,CAC9C,IAAIe,EACJ,GAAI,CACF,IAAMC,EAAkB,IAAI,IAAIC,EAAQ,KAAO,IAAK,kBAAkB,CAAC,CAAC,SACxE,EAAW,mBAAmBD,EAAgB,WAAW,qBAAsB,KAAK,CAAC,CACvF,MAAQ,CACNE,EAAS,UAAU,GAAG,EACtBA,EAAS,IAAI,EACb,MACF,CAEIH,IAAa,MACf,EAAW,eAGb,IAAMI,EAAwBJ,EAAS,SAAS,GAAG,EAAIhB,EAAAA,QAAK,KAAKgB,EAAU,YAAY,EAAIA,EAErFK,EAAWrB,EAAAA,QAAK,QAAQe,EAAU,IAAIK,GAAuB,EAC7DE,EAAetB,EAAAA,QAAK,SAASe,EAAUM,CAAQ,EACrD,GAAIC,IAAiB,MAAQA,EAAa,WAAW,KAAKtB,EAAAA,QAAK,KAAK,EAClEmB,EAAS,UAAU,GAAG,EACtBA,EAAS,IAAI,OAIf,GAAIjB,EAAAA,QAAG,WAAWmB,CAAQ,EACxB,GAAI,CACF,IAAME,EAASrB,EAAAA,QAAG,aAAamB,CAAQ,EACvCF,EAAS,UAAU,IAAK,CACtB,8BAA+B,IAC/B,OAAQ,WACR,gBAAiB,WACjB,eAAgBK,EAAeJ,CAAqB,CACtD,CAAC,EACDD,EAAS,IAAII,CAAM,CACrB,MAAQ,CACN,EAAS,WAAa,IACtBJ,EAAS,IAAI,CACf,KAEA,GAAS,WAAa,IACtBA,EAAS,IAAI,CAEjB,CAAC,CACH,CAEA,SAASR,EAAmB,EAAiC,CAC3D,IAAMc,EAAUjB,EAAO,QAAQ,EAC/B,GAAI,CAACiB,EAAS,MAAU,MAAM,qCAAqC,EAEnE,MAAO,EACJ,OAAO,cAAe,SAAY,CACjCjB,EAAO,oBAAoB,EAC3B,MAAM,IAAI,QAAe,GAAY,CACnCA,EAAO,UAAY,CACjBM,EAAQ,CACV,CAAC,CACH,CAAC,CACH,EACA,IAAK,OAAOW,GAAY,SAAWA,EAAU,oBAAoBA,EAAQ,OACzE,KAAM,OAAOA,GAAY,SAAWA,EAAQ,KAAO,IAAA,EACrD,CACF,CAEA,MAAMC,EAAiD,CACrD,OAAQ,WACR,QAAS,YACT,MAAO,kBACP,OAAQ,YACR,OAAQ,aACR,OAAQ,YACR,OAAQ,eACV,EAEA,SAASF,EAAe,EAA0B,CAChD,IAAK,GAAM,CAACG,EAAQC,KAAgB,OAAO,QAAQF,CAAsB,EACvE,GAAIV,EAAS,SAASW,CAAM,EAAG,OAAOC,EAExC,MAAO,YACT,C"}
@@ -1 +1 @@
1
- {"version":3,"file":"startHttpServer.js","names":["fs","path","http","startHttpServer","server","createAssetServer","dir","createServerHandle","startLocalHttpServer","reject","resolve","rootPath","pathname","encodedPathname","request","response","pathnameWithIndexHtml","filePath","relativePath","buffer","getContentType","address","CONTENT_TYPE_BY_SUFFIX","suffix","contentType"],"sources":["../../src/helpers/startHttpServer.ts"],"sourcesContent":["import fs from 'node:fs';\nimport http from 'node:http';\nimport path from 'node:path';\n\nexport interface HttpServer {\n [Symbol.asyncDispose](): Promise<void>;\n url: string;\n port: number | undefined;\n}\n\n/** Starts a web-page test server and returns its address synchronously. */\nexport function startHttpServer(dir: string): HttpServer {\n const server = createAssetServer(dir);\n server.listen();\n return createServerHandle(server);\n}\n\n/** Starts an asset server reachable only from the local machine. */\nexport async function startLocalHttpServer(dir: string): Promise<HttpServer> {\n const server = createAssetServer(dir);\n await new Promise<void>((resolve, reject) => {\n server.once('error', reject);\n server.listen(0, '127.0.0.1', () => {\n server.off('error', reject);\n resolve();\n });\n });\n return createServerHandle(server);\n}\n\nfunction createAssetServer(dir: string): http.Server {\n const rootPath = path.resolve(dir);\n return http.createServer((request, response) => {\n let pathname: string;\n try {\n const encodedPathname = new URL(request.url ?? '/', 'http://127.0.0.1').pathname;\n pathname = decodeURIComponent(encodedPathname.replaceAll(/%(?![\\da-f]{2})/giu, '%25'));\n } catch {\n response.writeHead(400);\n response.end();\n return;\n }\n\n if (pathname === '/') {\n pathname = '/index.html';\n }\n\n const pathnameWithIndexHtml = pathname.endsWith('/') ? path.join(pathname, 'index.html') : pathname;\n\n const filePath = path.resolve(rootPath, `.${pathnameWithIndexHtml}`);\n const relativePath = path.relative(rootPath, filePath);\n if (relativePath === '..' || relativePath.startsWith(`..${path.sep}`)) {\n response.writeHead(403);\n response.end();\n return;\n }\n\n if (fs.existsSync(filePath)) {\n try {\n const buffer = fs.readFileSync(filePath);\n response.writeHead(200, {\n 'Access-Control-Allow-Origin': '*',\n Pragma: 'no-cache',\n 'Cache-Control': 'no-cache',\n 'Content-Type': getContentType(pathnameWithIndexHtml),\n });\n response.end(buffer);\n } catch {\n response.statusCode = 500;\n response.end();\n }\n } else {\n response.statusCode = 404;\n response.end();\n }\n });\n}\n\nfunction createServerHandle(server: http.Server): HttpServer {\n const address = server.address();\n if (!address) throw new Error('server has been unexpectedly closed');\n\n return {\n [Symbol.asyncDispose]: async () => {\n server.closeAllConnections();\n await new Promise<void>((resolve) => {\n server.close(() => {\n resolve();\n });\n });\n },\n url: typeof address === 'string' ? address : `http://127.0.0.1:${address.port}`,\n port: typeof address === 'object' ? address.port : undefined,\n };\n}\n\nconst CONTENT_TYPE_BY_SUFFIX: Record<string, string> = {\n '.css': 'text/css',\n '.html': 'text/html',\n '.js': 'text/javascript',\n '.gif': 'image/gif',\n '.jpg': 'image/jpeg',\n '.png': 'image/png',\n '.svg': 'image/svg+xml',\n};\n\nfunction getContentType(pathname: string): string {\n for (const [suffix, contentType] of Object.entries(CONTENT_TYPE_BY_SUFFIX)) {\n if (pathname.endsWith(suffix)) return contentType;\n }\n return 'text/plain';\n}\n"],"mappings":"yEAWA,SAAgBG,EAAgB,EAAyB,CACvD,IAAMC,EAASC,EAAkBC,CAAG,EAEpC,OADAF,EAAO,OAAO,EACPG,EAAmBH,CAAM,CAClC,CAGA,eAAsBI,EAAqB,EAAkC,CAC3E,IAAMJ,EAASC,EAAkBC,CAAG,EAQpC,OAPA,MAAM,IAAI,SAAe,EAAS,IAAW,CAC3CF,EAAO,KAAK,QAASK,CAAM,EAC3BL,EAAO,OAAO,EAAG,gBAAmB,CAClCA,EAAO,IAAI,QAASK,CAAM,EAC1BC,EAAQ,CACV,CAAC,CACH,CAAC,EACMH,EAAmBH,CAAM,CAClC,CAEA,SAASC,EAAkB,EAA0B,CACnD,IAAMM,EAAWV,EAAK,QAAQK,CAAG,EACjC,OAAOJ,EAAK,cAAc,EAAS,IAAa,CAC9C,IAAIU,EACJ,GAAI,CACF,IAAMC,EAAkB,IAAI,IAAIC,EAAQ,KAAO,IAAK,kBAAkB,CAAC,CAAC,SACxE,EAAW,mBAAmBD,EAAgB,WAAW,qBAAsB,KAAK,CAAC,CACvF,MAAQ,CACNE,EAAS,UAAU,GAAG,EACtBA,EAAS,IAAI,EACb,MACF,CAEIH,IAAa,MACf,EAAW,eAGb,IAAMI,EAAwBJ,EAAS,SAAS,GAAG,EAAIX,EAAK,KAAKW,EAAU,YAAY,EAAIA,EAErFK,EAAWhB,EAAK,QAAQU,EAAU,IAAIK,GAAuB,EAC7DE,EAAejB,EAAK,SAASU,EAAUM,CAAQ,EACrD,GAAIC,IAAiB,MAAQA,EAAa,WAAW,KAAKjB,EAAK,KAAK,EAClEc,EAAS,UAAU,GAAG,EACtBA,EAAS,IAAI,OAIf,GAAIf,EAAG,WAAWiB,CAAQ,EACxB,GAAI,CACF,IAAME,EAASnB,EAAG,aAAaiB,CAAQ,EACvCF,EAAS,UAAU,IAAK,CACtB,8BAA+B,IAC/B,OAAQ,WACR,gBAAiB,WACjB,eAAgBK,EAAeJ,CAAqB,CACtD,CAAC,EACDD,EAAS,IAAII,CAAM,CACrB,MAAQ,CACN,EAAS,WAAa,IACtBJ,EAAS,IAAI,CACf,KAEA,GAAS,WAAa,IACtBA,EAAS,IAAI,CAEjB,CAAC,CACH,CAEA,SAASR,EAAmB,EAAiC,CAC3D,IAAMc,EAAUjB,EAAO,QAAQ,EAC/B,GAAI,CAACiB,EAAS,MAAU,MAAM,qCAAqC,EAEnE,MAAO,EACJ,OAAO,cAAe,SAAY,CACjCjB,EAAO,oBAAoB,EAC3B,MAAM,IAAI,QAAe,GAAY,CACnCA,EAAO,UAAY,CACjBM,EAAQ,CACV,CAAC,CACH,CAAC,CACH,EACA,IAAK,OAAOW,GAAY,SAAWA,EAAU,oBAAoBA,EAAQ,OACzE,KAAM,OAAOA,GAAY,SAAWA,EAAQ,KAAO,IAAA,EACrD,CACF,CAEA,MAAMC,EAAiD,CACrD,OAAQ,WACR,QAAS,YACT,MAAO,kBACP,OAAQ,YACR,OAAQ,aACR,OAAQ,YACR,OAAQ,eACV,EAEA,SAASF,EAAe,EAA0B,CAChD,IAAK,GAAM,CAACG,EAAQC,KAAgB,OAAO,QAAQF,CAAsB,EACvE,GAAIV,EAAS,SAASW,CAAM,EAAG,OAAOC,EAExC,MAAO,YACT"}
1
+ {"version":3,"file":"startHttpServer.js","names":["fs","path","http","startHttpServer","server","createAssetServer","dir","createServerHandle","startLocalHttpServer","reject","resolve","rootPath","pathname","encodedPathname","request","response","pathnameWithIndexHtml","filePath","relativePath","buffer","getContentType","address","CONTENT_TYPE_BY_SUFFIX","suffix","contentType"],"sources":["../../src/helpers/startHttpServer.ts"],"sourcesContent":["import fs from 'node:fs';\nimport http from 'node:http';\nimport path from 'node:path';\n\nexport interface HttpServer {\n [Symbol.asyncDispose](): Promise<void>;\n url: string;\n port: number | undefined;\n}\n\n/** Starts a web-page test server and returns its address synchronously. */\nexport function startHttpServer(dir: string): HttpServer {\n const server = createAssetServer(dir);\n server.listen();\n return createServerHandle(server);\n}\n\n/** Starts an asset server reachable only from the local machine. */\nexport async function startLocalHttpServer(dir: string): Promise<HttpServer> {\n const server = createAssetServer(dir);\n await new Promise<void>((resolve, reject) => {\n server.once('error', reject);\n server.listen(0, '127.0.0.1', () => {\n server.off('error', reject);\n resolve();\n });\n });\n return createServerHandle(server);\n}\n\nfunction createAssetServer(dir: string): http.Server {\n const rootPath = path.resolve(dir);\n return http.createServer((request, response) => {\n let pathname: string;\n try {\n const encodedPathname = new URL(request.url ?? '/', 'http://127.0.0.1').pathname;\n pathname = decodeURIComponent(encodedPathname.replaceAll(/%(?![\\da-f]{2})/giu, '%25'));\n } catch {\n response.writeHead(400);\n response.end();\n return;\n }\n\n if (pathname === '/') {\n pathname = '/index.html';\n }\n\n const pathnameWithIndexHtml = pathname.endsWith('/') ? path.join(pathname, 'index.html') : pathname;\n\n const filePath = path.resolve(rootPath, `.${pathnameWithIndexHtml}`);\n const relativePath = path.relative(rootPath, filePath);\n if (relativePath === '..' || relativePath.startsWith(`..${path.sep}`)) {\n response.writeHead(403);\n response.end();\n return;\n }\n\n if (fs.existsSync(filePath)) {\n try {\n const buffer = fs.readFileSync(filePath);\n response.writeHead(200, {\n 'Access-Control-Allow-Origin': '*',\n Pragma: 'no-cache',\n 'Cache-Control': 'no-cache',\n 'Content-Type': getContentType(pathnameWithIndexHtml),\n });\n response.end(buffer);\n } catch {\n response.statusCode = 500;\n response.end();\n }\n } else {\n response.statusCode = 404;\n response.end();\n }\n });\n}\n\nfunction createServerHandle(server: http.Server): HttpServer {\n const address = server.address();\n if (!address) throw new Error('server has been unexpectedly closed');\n\n return {\n [Symbol.asyncDispose]: async () => {\n server.closeAllConnections();\n await new Promise<void>((resolve) => {\n server.close(() => {\n resolve();\n });\n });\n },\n url: typeof address === 'string' ? address : `http://127.0.0.1:${address.port}`,\n port: typeof address === 'object' ? address.port : undefined,\n };\n}\n\nconst CONTENT_TYPE_BY_SUFFIX: Record<string, string> = {\n '.css': 'text/css',\n '.html': 'text/html',\n '.js': 'text/javascript',\n '.gif': 'image/gif',\n '.jpg': 'image/jpeg',\n '.png': 'image/png',\n '.svg': 'image/svg+xml',\n};\n\nfunction getContentType(pathname: string): string {\n for (const [suffix, contentType] of Object.entries(CONTENT_TYPE_BY_SUFFIX)) {\n if (pathname.endsWith(suffix)) return contentType;\n }\n return 'text/plain';\n}\n"],"mappings":"A,yEAWA,SAAgBG,EAAgB,EAAyB,CACvD,IAAMC,EAASC,EAAkBC,CAAG,EAEpC,OADAF,EAAO,OAAO,EACPG,EAAmBH,CAAM,CAClC,CAGA,eAAsBI,EAAqB,EAAkC,CAC3E,IAAMJ,EAASC,EAAkBC,CAAG,EAQpC,OAPA,MAAM,IAAI,SAAe,EAAS,IAAW,CAC3CF,EAAO,KAAK,QAASK,CAAM,EAC3BL,EAAO,OAAO,EAAG,gBAAmB,CAClCA,EAAO,IAAI,QAASK,CAAM,EAC1BC,EAAQ,CACV,CAAC,CACH,CAAC,EACMH,EAAmBH,CAAM,CAClC,CAEA,SAASC,EAAkB,EAA0B,CACnD,IAAMM,EAAWV,EAAK,QAAQK,CAAG,EACjC,OAAOJ,EAAK,cAAc,EAAS,IAAa,CAC9C,IAAIU,EACJ,GAAI,CACF,IAAMC,EAAkB,IAAI,IAAIC,EAAQ,KAAO,IAAK,kBAAkB,CAAC,CAAC,SACxE,EAAW,mBAAmBD,EAAgB,WAAW,qBAAsB,KAAK,CAAC,CACvF,MAAQ,CACNE,EAAS,UAAU,GAAG,EACtBA,EAAS,IAAI,EACb,MACF,CAEIH,IAAa,MACf,EAAW,eAGb,IAAMI,EAAwBJ,EAAS,SAAS,GAAG,EAAIX,EAAK,KAAKW,EAAU,YAAY,EAAIA,EAErFK,EAAWhB,EAAK,QAAQU,EAAU,IAAIK,GAAuB,EAC7DE,EAAejB,EAAK,SAASU,EAAUM,CAAQ,EACrD,GAAIC,IAAiB,MAAQA,EAAa,WAAW,KAAKjB,EAAK,KAAK,EAClEc,EAAS,UAAU,GAAG,EACtBA,EAAS,IAAI,OAIf,GAAIf,EAAG,WAAWiB,CAAQ,EACxB,GAAI,CACF,IAAME,EAASnB,EAAG,aAAaiB,CAAQ,EACvCF,EAAS,UAAU,IAAK,CACtB,8BAA+B,IAC/B,OAAQ,WACR,gBAAiB,WACjB,eAAgBK,EAAeJ,CAAqB,CACtD,CAAC,EACDD,EAAS,IAAII,CAAM,CACrB,MAAQ,CACN,EAAS,WAAa,IACtBJ,EAAS,IAAI,CACf,KAEA,GAAS,WAAa,IACtBA,EAAS,IAAI,CAEjB,CAAC,CACH,CAEA,SAASR,EAAmB,EAAiC,CAC3D,IAAMc,EAAUjB,EAAO,QAAQ,EAC/B,GAAI,CAACiB,EAAS,MAAU,MAAM,qCAAqC,EAEnE,MAAO,EACJ,OAAO,cAAe,SAAY,CACjCjB,EAAO,oBAAoB,EAC3B,MAAM,IAAI,QAAe,GAAY,CACnCA,EAAO,UAAY,CACjBM,EAAQ,CACV,CAAC,CACH,CAAC,CACH,EACA,IAAK,OAAOW,GAAY,SAAWA,EAAU,oBAAoBA,EAAQ,OACzE,KAAM,OAAOA,GAAY,SAAWA,EAAQ,KAAO,IAAA,EACrD,CACF,CAEA,MAAMC,EAAiD,CACrD,OAAQ,WACR,QAAS,YACT,MAAO,kBACP,OAAQ,YACR,OAAQ,aACR,OAAQ,YACR,OAAQ,eACV,EAEA,SAASF,EAAe,EAA0B,CAChD,IAAK,GAAM,CAACG,EAAQC,KAAgB,OAAO,QAAQF,CAAsB,EACvE,GAAIV,EAAS,SAASW,CAAM,EAAG,OAAOC,EAExC,MAAO,YACT,C"}
@@ -1 +1 @@
1
- {"version":3,"file":"stdioJudgeRules.cjs","names":["EXAMPLE_TEST_CASE_ID_PATTERN"],"sources":["../../src/helpers/stdioJudgeRules.ts"],"sourcesContent":["/** Longest stdout (and stderr) the stdio judge accepts from one run; a longer output is rejected. */\nexport const MAX_STDOUT_LENGTH = 50_000;\n\n/** The judge server shows a test case on the problem page when its id contains `example`. */\nexport const EXAMPLE_TEST_CASE_ID_PATTERN = /example/;\n"],"mappings":"aACA,MAGaA,EAA+B"}
1
+ {"version":3,"file":"stdioJudgeRules.cjs","names":["EXAMPLE_TEST_CASE_ID_PATTERN"],"sources":["../../src/helpers/stdioJudgeRules.ts"],"sourcesContent":["/** Longest stdout (and stderr) the stdio judge accepts from one run; a longer output is rejected. */\nexport const MAX_STDOUT_LENGTH = 50_000;\n\n/** The judge server shows a test case on the problem page when its id contains `example`. */\nexport const EXAMPLE_TEST_CASE_ID_PATTERN = /example/;\n"],"mappings":"A,aACA,MAGaA,EAA+B,U"}
@@ -1 +1 @@
1
- {"version":3,"file":"stdioJudgeRules.js","names":["MAX_STDOUT_LENGTH","EXAMPLE_TEST_CASE_ID_PATTERN"],"sources":["../../src/helpers/stdioJudgeRules.ts"],"sourcesContent":["/** Longest stdout (and stderr) the stdio judge accepts from one run; a longer output is rejected. */\nexport const MAX_STDOUT_LENGTH = 50_000;\n\n/** The judge server shows a test case on the problem page when its id contains `example`. */\nexport const EXAMPLE_TEST_CASE_ID_PATTERN = /example/;\n"],"mappings":"AACA,MAAaA,EAAoB,IAGpBC,EAA+B"}
1
+ {"version":3,"file":"stdioJudgeRules.js","names":["MAX_STDOUT_LENGTH","EXAMPLE_TEST_CASE_ID_PATTERN"],"sources":["../../src/helpers/stdioJudgeRules.ts"],"sourcesContent":["/** Longest stdout (and stderr) the stdio judge accepts from one run; a longer output is rejected. */\nexport const MAX_STDOUT_LENGTH = 50_000;\n\n/** The judge server shows a test case on the problem page when its id contains `example`. */\nexport const EXAMPLE_TEST_CASE_ID_PATTERN = /example/;\n"],"mappings":"AACA,MAAaA,EAAoB,IAGpBC,EAA+B,U"}
@@ -1 +1 @@
1
- {"version":3,"file":"temporaryProblemDirCopy.cjs","names":["fs","path","os","require_runtime","node_fs","node_path","node_os","node_child_process","copyProblemDirToTemporaryRoot","tempRoot","options","absoluteProblemDir","problemDir","copiedProblemDir","toTempRelativePath","rejectAbsoluteSymlinks","symlinkAllAncestorNodeModules","error","forciblyRemoveDirectory","dir","unlockPermissions","forciblyRemoveDirectorySync","isCopiedProblemPath","src","name","currentDir","nodeModulesPath","targetSymlinkPath","parentDir","absolutePath","root"],"sources":["../../src/helpers/temporaryProblemDirCopy.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\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 options: {\n /** Called as soon as the root exists, so a caller's signal cleanup can cover the copy phase too. */\n onTempRootCreated?: (tempRoot: string) => void;\n } = {}\n): Promise<{ tempRoot: string; copiedProblemDir: string }> {\n const tempRoot = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'problem-utils-isolation_'));\n options.onTempRootCreated?.(tempRoot);\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\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"],"mappings":"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"}
1
+ {"version":3,"file":"temporaryProblemDirCopy.cjs","names":["fs","path","os","require_runtime","node_fs","node_path","node_os","node_child_process","copyProblemDirToTemporaryRoot","tempRoot","options","absoluteProblemDir","problemDir","copiedProblemDir","toTempRelativePath","rejectAbsoluteSymlinks","symlinkAllAncestorNodeModules","error","forciblyRemoveDirectory","dir","unlockPermissions","forciblyRemoveDirectorySync","isCopiedProblemPath","src","name","currentDir","nodeModulesPath","targetSymlinkPath","parentDir","absolutePath","root"],"sources":["../../src/helpers/temporaryProblemDirCopy.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\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 options: {\n /** Called as soon as the root exists, so a caller's signal cleanup can cover the copy phase too. */\n onTempRootCreated?: (tempRoot: string) => void;\n } = {}\n): Promise<{ tempRoot: string; copiedProblemDir: string }> {\n const tempRoot = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'problem-utils-isolation_'));\n options.onTempRootCreated?.(tempRoot);\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\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"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"temporaryProblemDirCopy.js","names":["fs","path","os","childProcess","copyProblemDirToTemporaryRoot","tempRoot","options","absoluteProblemDir","problemDir","copiedProblemDir","toTempRelativePath","rejectAbsoluteSymlinks","symlinkAllAncestorNodeModules","error","forciblyRemoveDirectory","dir","unlockPermissions","forciblyRemoveDirectorySync","isCopiedProblemPath","src","name","currentDir","nodeModulesPath","targetSymlinkPath","parentDir","absolutePath","root"],"sources":["../../src/helpers/temporaryProblemDirCopy.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\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 options: {\n /** Called as soon as the root exists, so a caller's signal cleanup can cover the copy phase too. */\n onTempRootCreated?: (tempRoot: string) => void;\n } = {}\n): Promise<{ tempRoot: string; copiedProblemDir: string }> {\n const tempRoot = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'problem-utils-isolation_'));\n options.onTempRootCreated?.(tempRoot);\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\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"],"mappings":"yGASA,eAAsBI,EACpB,EACA,EAGI,CAAC,EACoD,CACzD,IAAMC,EAAW,MAAML,EAAG,SAAS,QAAQC,EAAK,KAAKC,EAAG,OAAO,EAAG,0BAA0B,CAAC,EAC7FI,EAAQ,oBAAoBD,CAAQ,EACpC,GAAI,CACF,IAAME,EAAqBN,EAAK,QAAQO,CAAU,EAC5CC,EAAmBR,EAAK,KAAKI,EAAUK,EAAmBH,CAAkB,CAAC,EAUnF,OATA,MAAMP,EAAG,SAAS,MAAMC,EAAK,QAAQQ,CAAgB,EAAG,CAAE,UAAW,EAAK,CAAC,EAC3E,MAAMT,EAAG,SAAS,GAAGO,EAAoBE,EAAkB,CACzD,UAAW,GACX,OAAQE,EAGR,iBAAkB,EACpB,CAAC,EACD,MAAMC,EAA8BP,EAAUE,CAAkB,EACzD,CAAE,WAAU,kBAAiB,CACtC,OAASM,EAAO,CAEd,MADA,MAAMC,EAAwBT,CAAQ,EAChCQ,CACR,CACF,CAMA,eAAsBC,EAAwB,EAA+B,CAC3E,GAAI,CAEF,OADA,MAAMd,EAAG,SAAS,GAAGe,EAAK,CAAE,UAAW,GAAM,MAAO,EAAK,CAAC,EACnD,EACT,MAAQ,CACNC,EAAkBD,CAAG,EACrB,GAAI,CAEF,OADA,MAAMf,EAAG,SAAS,GAAGe,EAAK,CAAE,UAAW,GAAM,MAAO,EAAK,CAAC,EACnD,EACT,MAAQ,CACN,MAAO,EACT,CACF,CACF,CAGA,SAAgBE,EAA4B,EAAsB,CAChE,GAAI,CAEF,OADAjB,EAAG,OAAOe,EAAK,CAAE,UAAW,GAAM,MAAO,EAAK,CAAC,EACxC,EACT,MAAQ,CACNC,EAAkBD,CAAG,EACrB,GAAI,CAEF,OADAf,EAAG,OAAOe,EAAK,CAAE,UAAW,GAAM,MAAO,EAAK,CAAC,EACxC,EACT,MAAQ,CACN,MAAO,EACT,CACF,CACF,CAIA,SAASC,EAAkB,EAAmB,CACxC,QAAQ,WAAa,SAASb,EAAc,UAAU,QAAS,CAAC,KAAM,QAASY,CAAG,CAAC,CACzF,CAIA,eAAeJ,EAAuB,EAA+B,CACnE,GAAI,CAACO,EAAoBC,CAAG,EAAG,MAAO,GAEtC,IAAI,MADgBnB,EAAG,SAAS,MAAMmB,CAAG,EAAA,CAC/B,eAAe,GAAKlB,EAAK,WAAW,MAAMD,EAAG,SAAS,SAASmB,CAAG,CAAC,EAC3E,MAAU,MAAM,GAAGA,EAAI,uFAAuF,EAEhH,MAAO,EACT,CAEA,SAASD,EAAoB,EAAsB,CACjD,IAAME,EAAOnB,EAAK,SAASkB,CAAG,EAC9B,OAAOC,IAAS,gBAAkBA,IAAS,MAC7C,CAEA,eAAeR,EAA8B,EAAkB,EAAmC,CAChG,IAAIS,EAAapB,EAAK,QAAQO,CAAU,EACxC,OAAa,CACX,IAAMc,EAAkBrB,EAAK,KAAKoB,EAAY,cAAc,EAC5D,GAAIrB,EAAG,WAAWsB,CAAe,EAAG,CAClC,IAAMC,EAAoBtB,EAAK,KAAKI,EAAUK,EAAmBW,CAAU,EAAG,cAAc,EAC5F,GAAI,CACF,MAAMrB,EAAG,SAAS,QAChBsB,EACAC,EACA,QAAQ,WAAa,QAAU,WAAa,KAC9C,CACF,MAAQ,CAER,CACF,CAEA,IAAMC,EAAYvB,EAAK,QAAQoB,CAAU,EACzC,GAAIG,IAAcH,EAAY,MAC9B,EAAaG,CACf,CACF,CAEA,SAASd,EAAmB,EAA8B,CACxD,GAAM,CAAE,QAAST,EAAK,MAAMwB,CAAY,EACxC,OAAOxB,EAAK,SAASyB,EAAMD,CAAY,CACzC"}
1
+ {"version":3,"file":"temporaryProblemDirCopy.js","names":["fs","path","os","childProcess","copyProblemDirToTemporaryRoot","tempRoot","options","absoluteProblemDir","problemDir","copiedProblemDir","toTempRelativePath","rejectAbsoluteSymlinks","symlinkAllAncestorNodeModules","error","forciblyRemoveDirectory","dir","unlockPermissions","forciblyRemoveDirectorySync","isCopiedProblemPath","src","name","currentDir","nodeModulesPath","targetSymlinkPath","parentDir","absolutePath","root"],"sources":["../../src/helpers/temporaryProblemDirCopy.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\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 options: {\n /** Called as soon as the root exists, so a caller's signal cleanup can cover the copy phase too. */\n onTempRootCreated?: (tempRoot: string) => void;\n } = {}\n): Promise<{ tempRoot: string; copiedProblemDir: string }> {\n const tempRoot = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'problem-utils-isolation_'));\n options.onTempRootCreated?.(tempRoot);\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\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"],"mappings":"A,yGASA,eAAsBI,EACpB,EACA,EAGI,CAAC,EACoD,CACzD,IAAMC,EAAW,MAAML,EAAG,SAAS,QAAQC,EAAK,KAAKC,EAAG,OAAO,EAAG,0BAA0B,CAAC,EAC7FI,EAAQ,oBAAoBD,CAAQ,EACpC,GAAI,CACF,IAAME,EAAqBN,EAAK,QAAQO,CAAU,EAC5CC,EAAmBR,EAAK,KAAKI,EAAUK,EAAmBH,CAAkB,CAAC,EAUnF,OATA,MAAMP,EAAG,SAAS,MAAMC,EAAK,QAAQQ,CAAgB,EAAG,CAAE,UAAW,EAAK,CAAC,EAC3E,MAAMT,EAAG,SAAS,GAAGO,EAAoBE,EAAkB,CACzD,UAAW,GACX,OAAQE,EAGR,iBAAkB,EACpB,CAAC,EACD,MAAMC,EAA8BP,EAAUE,CAAkB,EACzD,CAAE,WAAU,kBAAiB,CACtC,OAASM,EAAO,CAEd,MADA,MAAMC,EAAwBT,CAAQ,EAChCQ,CACR,CACF,CAMA,eAAsBC,EAAwB,EAA+B,CAC3E,GAAI,CAEF,OADA,MAAMd,EAAG,SAAS,GAAGe,EAAK,CAAE,UAAW,GAAM,MAAO,EAAK,CAAC,EACnD,EACT,MAAQ,CACNC,EAAkBD,CAAG,EACrB,GAAI,CAEF,OADA,MAAMf,EAAG,SAAS,GAAGe,EAAK,CAAE,UAAW,GAAM,MAAO,EAAK,CAAC,EACnD,EACT,MAAQ,CACN,MAAO,EACT,CACF,CACF,CAGA,SAAgBE,EAA4B,EAAsB,CAChE,GAAI,CAEF,OADAjB,EAAG,OAAOe,EAAK,CAAE,UAAW,GAAM,MAAO,EAAK,CAAC,EACxC,EACT,MAAQ,CACNC,EAAkBD,CAAG,EACrB,GAAI,CAEF,OADAf,EAAG,OAAOe,EAAK,CAAE,UAAW,GAAM,MAAO,EAAK,CAAC,EACxC,EACT,MAAQ,CACN,MAAO,EACT,CACF,CACF,CAIA,SAASC,EAAkB,EAAmB,CACxC,QAAQ,WAAa,SAASb,EAAc,UAAU,QAAS,CAAC,KAAM,QAASY,CAAG,CAAC,CACzF,CAIA,eAAeJ,EAAuB,EAA+B,CACnE,GAAI,CAACO,EAAoBC,CAAG,EAAG,MAAO,GAEtC,IAAI,MADgBnB,EAAG,SAAS,MAAMmB,CAAG,EAAA,CAC/B,eAAe,GAAKlB,EAAK,WAAW,MAAMD,EAAG,SAAS,SAASmB,CAAG,CAAC,EAC3E,MAAU,MAAM,GAAGA,EAAI,uFAAuF,EAEhH,MAAO,EACT,CAEA,SAASD,EAAoB,EAAsB,CACjD,IAAME,EAAOnB,EAAK,SAASkB,CAAG,EAC9B,OAAOC,IAAS,gBAAkBA,IAAS,MAC7C,CAEA,eAAeR,EAA8B,EAAkB,EAAmC,CAChG,IAAIS,EAAapB,EAAK,QAAQO,CAAU,EACxC,OAAa,CACX,IAAMc,EAAkBrB,EAAK,KAAKoB,EAAY,cAAc,EAC5D,GAAIrB,EAAG,WAAWsB,CAAe,EAAG,CAClC,IAAMC,EAAoBtB,EAAK,KAAKI,EAAUK,EAAmBW,CAAU,EAAG,cAAc,EAC5F,GAAI,CACF,MAAMrB,EAAG,SAAS,QAChBsB,EACAC,EACA,QAAQ,WAAa,QAAU,WAAa,KAC9C,CACF,MAAQ,CAER,CACF,CAEA,IAAMC,EAAYvB,EAAK,QAAQoB,CAAU,EACzC,GAAIG,IAAcH,EAAY,MAC9B,EAAaG,CACf,CACF,CAEA,SAASd,EAAmB,EAA8B,CACxD,GAAM,CAAE,QAAST,EAAK,MAAMwB,CAAY,EACxC,OAAOxB,EAAK,SAASyB,EAAMD,CAAY,CACzC,C"}
package/dist/index.cjs CHANGED
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./helpers/compareStdoutAsSpaceSeparatedTokens.cjs"),t=require("./types/testCaseResult.cjs"),n=require("./helpers/printTestCaseResult.cjs"),r=require("./helpers/compareExpectedOutputFiles.cjs"),i=require("./helpers/defaultStdioHarness.cjs"),a=require("./helpers/parseArgs.cjs"),o=require("./helpers/parseCsv.cjs"),s=require("./helpers/readTestCases.cjs"),c=require("./helpers/removeCommentsInSourceCode.cjs"),l=require("./helpers/runCommandInTemporaryPackageManagerProject.cjs"),u=require("./helpers/runPythonInTemporaryUvProject.cjs"),d=require("./helpers/sourceCodeGrammars.cjs"),f=require("./helpers/startHttpServer.cjs"),p=require("./types/problem.cjs"),m=require("./learningMaterial/frontmatter.cjs"),h=require("./learningMaterial/validationResult.cjs"),g=require("./learningMaterial/validateMaterial.cjs"),_=require("./learningMaterial/validateContest.cjs"),v=require("./learningMaterial/validateCourse.cjs"),y=require("./learningMaterial/validateProblem.cjs"),b=require("./types/decisionCode.cjs"),x=require("./helpers/guiRecording.cjs");exports.CsvParseError=o.CsvParseError,exports.DecisionCode=b.DecisionCode,exports.HARNESS_FILE_PRESETS=i.HARNESS_FILE_PRESETS,exports.MAX_COMPARED_FILE_BYTES=r.MAX_COMPARED_FILE_BYTES,exports.MAX_GUI_RECORDING_BYTES=x.MAX_GUI_RECORDING_BYTES,exports.MAX_REPORTED_FILE_BYTES=r.MAX_REPORTED_FILE_BYTES,exports.TEST_CASE_RESULT_PREFIX=t.TEST_CASE_RESULT_PREFIX,exports.codeRuleSchema=p.codeRuleSchema,exports.compareExpectedOutputFiles=r.compareExpectedOutputFiles,exports.compareStdoutAsSpaceSeparatedTokens=e.compareStdoutAsSpaceSeparatedTokens,exports.copyPackageManagerProjectFiles=l.copyPackageManagerProjectFiles,exports.encodeFileForTestCaseResult=n.encodeFileForTestCaseResult,exports.findDefaultStdioHarnessFiles=i.findDefaultStdioHarnessFiles,exports.formatValidationReport=h.formatValidationReport,exports.formatZodIssues=h.formatZodIssues,exports.isDefaultStdioHarnessSource=i.isDefaultStdioHarnessSource,exports.judgeAgainstExpectations=r.judgeAgainstExpectations,exports.languageIdToSourceCodeGrammar=d.languageIdToSourceCodeGrammar,exports.mergeComparisonOutputFiles=r.mergeComparisonOutputFiles,exports.mergeSubmissionPeriods=g.mergeSubmissionPeriods,exports.normalizeCodeRule=p.normalizeCodeRule,exports.parseArgs=a.parseArgs,exports.parseCsv=o.parseCsv,exports.parseCsvRecords=o.parseCsvRecords,exports.parseFrontmatter=m.parseFrontmatter,exports.printTestCaseResult=n.printTestCaseResult,exports.problemMarkdownFrontMatterSchema=p.problemMarkdownFrontMatterSchema,exports.readTestCases=s.readTestCases,exports.removeCommentsAndStringsInSourceCode=c.removeCommentsAndStringsInSourceCode,exports.removeCommentsInSourceCode=c.removeCommentsInSourceCode,exports.reportDuplicateIds=h.reportDuplicateIds,exports.reportExcessChatMarkers=g.reportExcessChatMarkers,exports.reportForeignProblemCourseIds=g.reportForeignProblemCourseIds,exports.runCommandInTemporaryPackageManagerProject=l.runCommandInTemporaryPackageManagerProject,exports.runPythonInTemporaryUvProject=u.runPythonInTemporaryUvProject,exports.startHttpServer=f.startHttpServer,exports.startLocalHttpServer=f.startLocalHttpServer,exports.testCaseResultSchema=t.testCaseResultSchema,exports.validateContestFile=_.validateContestFile,exports.validateCourseDirectory=v.validateCourseDirectory,exports.validateMaterialFile=g.validateMaterialFile,exports.validateProblemDirectory=y.validateProblemDirectory,exports.validateSubmissionPeriods=g.validateSubmissionPeriods;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./helpers/compareStdoutAsSpaceSeparatedTokens.cjs"),t=require("./types/testCaseResult.cjs"),n=require("./helpers/printTestCaseResult.cjs"),r=require("./helpers/compareExpectedOutputFiles.cjs"),i=require("./helpers/defaultStdioHarness.cjs"),a=require("./helpers/parseArgs.cjs"),o=require("./helpers/parseCsv.cjs"),s=require("./helpers/readTestCases.cjs"),c=require("./helpers/removeCommentsInSourceCode.cjs"),l=require("./helpers/runCommandInTemporaryPackageManagerProject.cjs"),u=require("./helpers/runPythonInTemporaryUvProject.cjs"),d=require("./helpers/sourceCodeGrammars.cjs"),f=require("./helpers/startHttpServer.cjs"),p=require("./types/problem.cjs"),m=require("./learningMaterial/frontmatter.cjs"),h=require("./learningMaterial/validationResult.cjs"),g=require("./learningMaterial/validateMaterial.cjs"),_=require("./learningMaterial/validateContest.cjs"),v=require("./learningMaterial/validateCourse.cjs"),y=require("./learningMaterial/validateProblem.cjs"),b=require("./types/decisionCode.cjs"),x=require("./helpers/guiRecording.cjs");exports.CsvParseError=o.CsvParseError,exports.DecisionCode=b.DecisionCode,exports.HARNESS_FILE_PRESETS=i.HARNESS_FILE_PRESETS,exports.MAX_COMPARED_FILE_BYTES=r.MAX_COMPARED_FILE_BYTES,exports.MAX_GUI_RECORDING_BYTES=x.MAX_GUI_RECORDING_BYTES,exports.MAX_REPORTED_FILE_BYTES=r.MAX_REPORTED_FILE_BYTES,exports.TEST_CASE_RESULT_PREFIX=t.TEST_CASE_RESULT_PREFIX,exports.codeRuleSchema=p.codeRuleSchema,exports.compareExpectedOutputFiles=r.compareExpectedOutputFiles,exports.compareStdoutAsSpaceSeparatedTokens=e.compareStdoutAsSpaceSeparatedTokens,exports.copyPackageManagerProjectFiles=l.copyPackageManagerProjectFiles,exports.encodeFileForTestCaseResult=n.encodeFileForTestCaseResult,exports.findDefaultStdioHarnessFiles=i.findDefaultStdioHarnessFiles,exports.formatValidationReport=h.formatValidationReport,exports.formatZodIssues=h.formatZodIssues,exports.isDefaultStdioHarnessSource=i.isDefaultStdioHarnessSource,exports.judgeAgainstExpectations=r.judgeAgainstExpectations,exports.languageIdToSourceCodeGrammar=d.languageIdToSourceCodeGrammar,exports.mergeComparisonOutputFiles=r.mergeComparisonOutputFiles,exports.mergeSubmissionPeriods=g.mergeSubmissionPeriods,exports.normalizeCodeRule=p.normalizeCodeRule,exports.parseArgs=a.parseArgs,exports.parseCsv=o.parseCsv,exports.parseCsvRecords=o.parseCsvRecords,exports.parseFrontmatter=m.parseFrontmatter,exports.printTestCaseResult=n.printTestCaseResult,exports.problemMarkdownFrontMatterSchema=p.problemMarkdownFrontMatterSchema,exports.readTestCases=s.readTestCases,exports.removeCommentsAndStringsInSourceCode=c.removeCommentsAndStringsInSourceCode,exports.removeCommentsInSourceCode=c.removeCommentsInSourceCode,exports.reportDuplicateIds=h.reportDuplicateIds,exports.reportExcessChatMarkers=g.reportExcessChatMarkers,exports.runCommandInTemporaryPackageManagerProject=l.runCommandInTemporaryPackageManagerProject,exports.runPythonInTemporaryUvProject=u.runPythonInTemporaryUvProject,exports.startHttpServer=f.startHttpServer,exports.startLocalHttpServer=f.startLocalHttpServer,exports.testCaseResultSchema=t.testCaseResultSchema,exports.validateContestFile=_.validateContestFile,exports.validateCourseDirectory=v.validateCourseDirectory,exports.validateMaterialFile=g.validateMaterialFile,exports.validateProblemDirectory=y.validateProblemDirectory,exports.validateSubmissionPeriods=g.validateSubmissionPeriods;
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- import{compareStdoutAsSpaceSeparatedTokens as e}from"./helpers/compareStdoutAsSpaceSeparatedTokens.js";import{TEST_CASE_RESULT_PREFIX as t,testCaseResultSchema as n}from"./types/testCaseResult.js";import{encodeFileForTestCaseResult as r,printTestCaseResult as i}from"./helpers/printTestCaseResult.js";import{MAX_COMPARED_FILE_BYTES as a,MAX_REPORTED_FILE_BYTES as o,compareExpectedOutputFiles as s,judgeAgainstExpectations as c,mergeComparisonOutputFiles as l}from"./helpers/compareExpectedOutputFiles.js";import{HARNESS_FILE_PRESETS as u,findDefaultStdioHarnessFiles as d,isDefaultStdioHarnessSource as f}from"./helpers/defaultStdioHarness.js";import{parseArgs as p}from"./helpers/parseArgs.js";import{CsvParseError as m,parseCsv as h,parseCsvRecords as g}from"./helpers/parseCsv.js";import{readTestCases as _}from"./helpers/readTestCases.js";import{removeCommentsAndStringsInSourceCode as v,removeCommentsInSourceCode as y}from"./helpers/removeCommentsInSourceCode.js";import{copyPackageManagerProjectFiles as b,runCommandInTemporaryPackageManagerProject as x}from"./helpers/runCommandInTemporaryPackageManagerProject.js";import{runPythonInTemporaryUvProject as S}from"./helpers/runPythonInTemporaryUvProject.js";import{languageIdToSourceCodeGrammar as C}from"./helpers/sourceCodeGrammars.js";import{startHttpServer as w,startLocalHttpServer as T}from"./helpers/startHttpServer.js";import{codeRuleSchema as E,normalizeCodeRule as D,problemMarkdownFrontMatterSchema as O}from"./types/problem.js";import{parseFrontmatter as k}from"./learningMaterial/frontmatter.js";import{formatValidationReport as A,formatZodIssues as j,reportDuplicateIds as M}from"./learningMaterial/validationResult.js";import{mergeSubmissionPeriods as N,reportExcessChatMarkers as P,reportForeignProblemCourseIds as F,validateMaterialFile as I,validateSubmissionPeriods as L}from"./learningMaterial/validateMaterial.js";import{validateContestFile as R}from"./learningMaterial/validateContest.js";import{validateCourseDirectory as z}from"./learningMaterial/validateCourse.js";import{validateProblemDirectory as B}from"./learningMaterial/validateProblem.js";import{DecisionCode as V}from"./types/decisionCode.js";import{MAX_GUI_RECORDING_BYTES as H}from"./helpers/guiRecording.js";export{m as CsvParseError,V as DecisionCode,u as HARNESS_FILE_PRESETS,a as MAX_COMPARED_FILE_BYTES,H as MAX_GUI_RECORDING_BYTES,o as MAX_REPORTED_FILE_BYTES,t as TEST_CASE_RESULT_PREFIX,E as codeRuleSchema,s as compareExpectedOutputFiles,e as compareStdoutAsSpaceSeparatedTokens,b as copyPackageManagerProjectFiles,r as encodeFileForTestCaseResult,d as findDefaultStdioHarnessFiles,A as formatValidationReport,j as formatZodIssues,f as isDefaultStdioHarnessSource,c as judgeAgainstExpectations,C as languageIdToSourceCodeGrammar,l as mergeComparisonOutputFiles,N as mergeSubmissionPeriods,D as normalizeCodeRule,p as parseArgs,h as parseCsv,g as parseCsvRecords,k as parseFrontmatter,i as printTestCaseResult,O as problemMarkdownFrontMatterSchema,_ as readTestCases,v as removeCommentsAndStringsInSourceCode,y as removeCommentsInSourceCode,M as reportDuplicateIds,P as reportExcessChatMarkers,F as reportForeignProblemCourseIds,x as runCommandInTemporaryPackageManagerProject,S as runPythonInTemporaryUvProject,w as startHttpServer,T as startLocalHttpServer,n as testCaseResultSchema,R as validateContestFile,z as validateCourseDirectory,I as validateMaterialFile,B as validateProblemDirectory,L as validateSubmissionPeriods};
1
+ import{compareStdoutAsSpaceSeparatedTokens as e}from"./helpers/compareStdoutAsSpaceSeparatedTokens.js";import{TEST_CASE_RESULT_PREFIX as t,testCaseResultSchema as n}from"./types/testCaseResult.js";import{encodeFileForTestCaseResult as r,printTestCaseResult as i}from"./helpers/printTestCaseResult.js";import{MAX_COMPARED_FILE_BYTES as a,MAX_REPORTED_FILE_BYTES as o,compareExpectedOutputFiles as s,judgeAgainstExpectations as c,mergeComparisonOutputFiles as l}from"./helpers/compareExpectedOutputFiles.js";import{HARNESS_FILE_PRESETS as u,findDefaultStdioHarnessFiles as d,isDefaultStdioHarnessSource as f}from"./helpers/defaultStdioHarness.js";import{parseArgs as p}from"./helpers/parseArgs.js";import{CsvParseError as m,parseCsv as h,parseCsvRecords as g}from"./helpers/parseCsv.js";import{readTestCases as _}from"./helpers/readTestCases.js";import{removeCommentsAndStringsInSourceCode as v,removeCommentsInSourceCode as y}from"./helpers/removeCommentsInSourceCode.js";import{copyPackageManagerProjectFiles as b,runCommandInTemporaryPackageManagerProject as x}from"./helpers/runCommandInTemporaryPackageManagerProject.js";import{runPythonInTemporaryUvProject as S}from"./helpers/runPythonInTemporaryUvProject.js";import{languageIdToSourceCodeGrammar as C}from"./helpers/sourceCodeGrammars.js";import{startHttpServer as w,startLocalHttpServer as T}from"./helpers/startHttpServer.js";import{codeRuleSchema as E,normalizeCodeRule as D,problemMarkdownFrontMatterSchema as O}from"./types/problem.js";import{parseFrontmatter as k}from"./learningMaterial/frontmatter.js";import{formatValidationReport as A,formatZodIssues as j,reportDuplicateIds as M}from"./learningMaterial/validationResult.js";import{mergeSubmissionPeriods as N,reportExcessChatMarkers as P,validateMaterialFile as F,validateSubmissionPeriods as I}from"./learningMaterial/validateMaterial.js";import{validateContestFile as L}from"./learningMaterial/validateContest.js";import{validateCourseDirectory as R}from"./learningMaterial/validateCourse.js";import{validateProblemDirectory as z}from"./learningMaterial/validateProblem.js";import{DecisionCode as B}from"./types/decisionCode.js";import{MAX_GUI_RECORDING_BYTES as V}from"./helpers/guiRecording.js";export{m as CsvParseError,B as DecisionCode,u as HARNESS_FILE_PRESETS,a as MAX_COMPARED_FILE_BYTES,V as MAX_GUI_RECORDING_BYTES,o as MAX_REPORTED_FILE_BYTES,t as TEST_CASE_RESULT_PREFIX,E as codeRuleSchema,s as compareExpectedOutputFiles,e as compareStdoutAsSpaceSeparatedTokens,b as copyPackageManagerProjectFiles,r as encodeFileForTestCaseResult,d as findDefaultStdioHarnessFiles,A as formatValidationReport,j as formatZodIssues,f as isDefaultStdioHarnessSource,c as judgeAgainstExpectations,C as languageIdToSourceCodeGrammar,l as mergeComparisonOutputFiles,N as mergeSubmissionPeriods,D as normalizeCodeRule,p as parseArgs,h as parseCsv,g as parseCsvRecords,k as parseFrontmatter,i as printTestCaseResult,O as problemMarkdownFrontMatterSchema,_ as readTestCases,v as removeCommentsAndStringsInSourceCode,y as removeCommentsInSourceCode,M as reportDuplicateIds,P as reportExcessChatMarkers,x as runCommandInTemporaryPackageManagerProject,S as runPythonInTemporaryUvProject,w as startHttpServer,T as startLocalHttpServer,n as testCaseResultSchema,L as validateContestFile,R as validateCourseDirectory,F as validateMaterialFile,z as validateProblemDirectory,I as validateSubmissionPeriods};
@@ -1 +1 @@
1
- {"version":3,"file":"frontmatter.cjs","names":["parseYaml","yaml","FRONTMATTER_REGEX","FRONTMATTER_OPENER_REGEX","parseFrontmatter","match","text"],"sources":["../../src/learningMaterial/frontmatter.ts"],"sourcesContent":["import { parse as parseYaml } from 'yaml';\n\n// The judge front-matter parser (a port of jxson/front-matter) requires the document to start with\n// `---` or `= yaml =` immediately followed by the line end (LF or CRLF, no trailing whitespace) and\n// end the block with the same delimiter or a `...` line; the closer must be alone on its line\n// (`(?!.)` below), or the judge treats the whole document as body with empty attributes.\nconst FRONTMATTER_REGEX = /^\\uFEFF?(= yaml =|---)\\r?\\n([\\s\\S]*?)^(?:\\1|\\.\\.\\.)[ \\t]*\\r?(?:\\n|(?!.))/m;\nconst FRONTMATTER_OPENER_REGEX = /^\\uFEFF?(?:= yaml =|---)/;\n\n/**\n * Splits a Markdown document into YAML frontmatter attributes and the remaining body.\n * Throws when the frontmatter block is present but contains invalid YAML.\n */\nexport function parseFrontmatter(text: string): { attributes: unknown; body: string } {\n const match = FRONTMATTER_REGEX.exec(text);\n if (!match || match.index !== 0) {\n // A document that starts with --- was clearly meant to carry frontmatter; failing here beats\n // letting the schema report \"name is missing\" for a file whose name: line is plainly visible.\n if (FRONTMATTER_OPENER_REGEX.test(text)) {\n throw new Error(\n 'frontmatter block not recognized; the opening --- (or = yaml =) must be followed immediately by the line end (no trailing whitespace) and the closing delimiter must be alone on its line'\n );\n }\n return { attributes: {}, body: text };\n }\n return { attributes: parseYaml(match[2] ?? '') ?? {}, body: text.slice(match[0].length) };\n}\n"],"mappings":"mCAMA,MAAME,EAAoB,4EACpBC,EAA2B,2BAMjC,SAAgBC,EAAiB,EAAqD,CACpF,IAAMC,EAAQH,EAAkB,KAAKI,CAAI,EACzC,GAAI,CAACD,GAASA,EAAM,QAAU,EAAG,CAG/B,GAAIF,EAAyB,KAAKG,CAAI,EACpC,MAAU,MACR,2LACF,EAEF,MAAO,CAAE,WAAY,CAAC,EAAG,KAAMA,CAAK,CACtC,CACA,MAAO,CAAE,YAAA,EAAYN,EAAAA,MAAAA,CAAUK,EAAM,IAAM,EAAE,GAAK,CAAC,EAAG,KAAMC,EAAK,MAAMD,EAAM,EAAE,CAAC,MAAM,CAAE,CAC1F"}
1
+ {"version":3,"file":"frontmatter.cjs","names":["parseYaml","yaml","FRONTMATTER_REGEX","FRONTMATTER_OPENER_REGEX","parseFrontmatter","match","text"],"sources":["../../src/learningMaterial/frontmatter.ts"],"sourcesContent":["import { parse as parseYaml } from 'yaml';\n\n// The judge front-matter parser (a port of jxson/front-matter) requires the document to start with\n// `---` or `= yaml =` immediately followed by the line end (LF or CRLF, no trailing whitespace) and\n// end the block with the same delimiter or a `...` line; the closer must be alone on its line\n// (`(?!.)` below), or the judge treats the whole document as body with empty attributes.\nconst FRONTMATTER_REGEX = /^\\uFEFF?(= yaml =|---)\\r?\\n([\\s\\S]*?)^(?:\\1|\\.\\.\\.)[ \\t]*\\r?(?:\\n|(?!.))/m;\nconst FRONTMATTER_OPENER_REGEX = /^\\uFEFF?(?:= yaml =|---)/;\n\n/**\n * Splits a Markdown document into YAML frontmatter attributes and the remaining body.\n * Throws when the frontmatter block is present but contains invalid YAML.\n */\nexport function parseFrontmatter(text: string): { attributes: unknown; body: string } {\n const match = FRONTMATTER_REGEX.exec(text);\n if (!match || match.index !== 0) {\n // A document that starts with --- was clearly meant to carry frontmatter; failing here beats\n // letting the schema report \"name is missing\" for a file whose name: line is plainly visible.\n if (FRONTMATTER_OPENER_REGEX.test(text)) {\n throw new Error(\n 'frontmatter block not recognized; the opening --- (or = yaml =) must be followed immediately by the line end (no trailing whitespace) and the closing delimiter must be alone on its line'\n );\n }\n return { attributes: {}, body: text };\n }\n return { attributes: parseYaml(match[2] ?? '') ?? {}, body: text.slice(match[0].length) };\n}\n"],"mappings":"A,mCAMA,MAAME,EAAoB,4EACpBC,EAA2B,2BAMjC,SAAgBC,EAAiB,EAAqD,CACpF,IAAMC,EAAQH,EAAkB,KAAKI,CAAI,EACzC,GAAI,CAACD,GAASA,EAAM,QAAU,EAAG,CAG/B,GAAIF,EAAyB,KAAKG,CAAI,EACpC,MAAU,MACR,2LACF,EAEF,MAAO,CAAE,WAAY,CAAC,EAAG,KAAMA,CAAK,CACtC,CACA,MAAO,CAAE,YAAA,EAAYN,EAAAA,MAAAA,CAAUK,EAAM,IAAM,EAAE,GAAK,CAAC,EAAG,KAAMC,EAAK,MAAMD,EAAM,EAAE,CAAC,MAAM,CAAE,CAC1F,C"}
@@ -1 +1 @@
1
- {"version":3,"file":"frontmatter.js","names":["parseYaml","FRONTMATTER_REGEX","FRONTMATTER_OPENER_REGEX","parseFrontmatter","match","text","parse"],"sources":["../../src/learningMaterial/frontmatter.ts"],"sourcesContent":["import { parse as parseYaml } from 'yaml';\n\n// The judge front-matter parser (a port of jxson/front-matter) requires the document to start with\n// `---` or `= yaml =` immediately followed by the line end (LF or CRLF, no trailing whitespace) and\n// end the block with the same delimiter or a `...` line; the closer must be alone on its line\n// (`(?!.)` below), or the judge treats the whole document as body with empty attributes.\nconst FRONTMATTER_REGEX = /^\\uFEFF?(= yaml =|---)\\r?\\n([\\s\\S]*?)^(?:\\1|\\.\\.\\.)[ \\t]*\\r?(?:\\n|(?!.))/m;\nconst FRONTMATTER_OPENER_REGEX = /^\\uFEFF?(?:= yaml =|---)/;\n\n/**\n * Splits a Markdown document into YAML frontmatter attributes and the remaining body.\n * Throws when the frontmatter block is present but contains invalid YAML.\n */\nexport function parseFrontmatter(text: string): { attributes: unknown; body: string } {\n const match = FRONTMATTER_REGEX.exec(text);\n if (!match || match.index !== 0) {\n // A document that starts with --- was clearly meant to carry frontmatter; failing here beats\n // letting the schema report \"name is missing\" for a file whose name: line is plainly visible.\n if (FRONTMATTER_OPENER_REGEX.test(text)) {\n throw new Error(\n 'frontmatter block not recognized; the opening --- (or = yaml =) must be followed immediately by the line end (no trailing whitespace) and the closing delimiter must be alone on its line'\n );\n }\n return { attributes: {}, body: text };\n }\n return { attributes: parseYaml(match[2] ?? '') ?? {}, body: text.slice(match[0].length) };\n}\n"],"mappings":"6BAMA,MAAMC,EAAoB,4EACpBC,EAA2B,2BAMjC,SAAgBC,EAAiB,EAAqD,CACpF,IAAMC,EAAQH,EAAkB,KAAKI,CAAI,EACzC,GAAI,CAACD,GAASA,EAAM,QAAU,EAAG,CAG/B,GAAIF,EAAyB,KAAKG,CAAI,EACpC,MAAU,MACR,2LACF,EAEF,MAAO,CAAE,WAAY,CAAC,EAAG,KAAMA,CAAK,CACtC,CACA,MAAO,CAAE,WAAYL,EAAUI,EAAM,IAAM,EAAE,GAAK,CAAC,EAAG,KAAMC,EAAK,MAAMD,EAAM,EAAE,CAAC,MAAM,CAAE,CAC1F"}
1
+ {"version":3,"file":"frontmatter.js","names":["parseYaml","FRONTMATTER_REGEX","FRONTMATTER_OPENER_REGEX","parseFrontmatter","match","text","parse"],"sources":["../../src/learningMaterial/frontmatter.ts"],"sourcesContent":["import { parse as parseYaml } from 'yaml';\n\n// The judge front-matter parser (a port of jxson/front-matter) requires the document to start with\n// `---` or `= yaml =` immediately followed by the line end (LF or CRLF, no trailing whitespace) and\n// end the block with the same delimiter or a `...` line; the closer must be alone on its line\n// (`(?!.)` below), or the judge treats the whole document as body with empty attributes.\nconst FRONTMATTER_REGEX = /^\\uFEFF?(= yaml =|---)\\r?\\n([\\s\\S]*?)^(?:\\1|\\.\\.\\.)[ \\t]*\\r?(?:\\n|(?!.))/m;\nconst FRONTMATTER_OPENER_REGEX = /^\\uFEFF?(?:= yaml =|---)/;\n\n/**\n * Splits a Markdown document into YAML frontmatter attributes and the remaining body.\n * Throws when the frontmatter block is present but contains invalid YAML.\n */\nexport function parseFrontmatter(text: string): { attributes: unknown; body: string } {\n const match = FRONTMATTER_REGEX.exec(text);\n if (!match || match.index !== 0) {\n // A document that starts with --- was clearly meant to carry frontmatter; failing here beats\n // letting the schema report \"name is missing\" for a file whose name: line is plainly visible.\n if (FRONTMATTER_OPENER_REGEX.test(text)) {\n throw new Error(\n 'frontmatter block not recognized; the opening --- (or = yaml =) must be followed immediately by the line end (no trailing whitespace) and the closing delimiter must be alone on its line'\n );\n }\n return { attributes: {}, body: text };\n }\n return { attributes: parseYaml(match[2] ?? '') ?? {}, body: text.slice(match[0].length) };\n}\n"],"mappings":"A,6BAMA,MAAMC,EAAoB,4EACpBC,EAA2B,2BAMjC,SAAgBC,EAAiB,EAAqD,CACpF,IAAMC,EAAQH,EAAkB,KAAKI,CAAI,EACzC,GAAI,CAACD,GAASA,EAAM,QAAU,EAAG,CAG/B,GAAIF,EAAyB,KAAKG,CAAI,EACpC,MAAU,MACR,2LACF,EAEF,MAAO,CAAE,WAAY,CAAC,EAAG,KAAMA,CAAK,CACtC,CACA,MAAO,CAAE,WAAYL,EAAUI,EAAM,IAAM,EAAE,GAAK,CAAC,EAAG,KAAMC,EAAK,MAAMD,EAAM,EAAE,CAAC,MAAM,CAAE,CAC1F,C"}
@@ -1 +1 @@
1
- {"version":3,"file":"fsHelpers.cjs","names":["stat","lstat","readdir","basename","relative","join","LEARNING_MATERIAL_ID_REGEX","extname","readFile","require_schemas","node_path","node_fs_promises","IGNORED_DIRECTORY_NAMES","NON_CODE_EXTENSIONS","isFile","path","isRegularFile","isRegularDirectory","isDirectory","collectProblemDefinitions","definitionPathsById","collectProblemDefinitionsInto","rootDirectoryPath","dirents","directoryPath","dirent","problemId","definitionPaths","isExcludedFromDiscovery","directoryName","parentDirectoryPath","reportProblemDefinitionIssues","errors","isUsableProblemsDirectory","problemsDirectoryPath","collectProblemIdsFromDirectory","reportDanglingProblemReferences","location","problemIds","availableProblemIds","readSourceFilesRecursively","files","collectSourceFiles","f1","f2","entryPath"],"sources":["../../src/learningMaterial/fsHelpers.ts"],"sourcesContent":["import { lstat, readdir, readFile, stat } from 'node:fs/promises';\nimport { basename, extname, join, relative } from 'node:path';\n\nimport { LEARNING_MATERIAL_ID_REGEX } from './schemas.js';\n\n// Files the judge importer skips when reading problem directories. The extension filter is ported\n// from the judge's readSourceCodeFilesInDirectory so compile artifacts (e.g. Main.class next to\n// Main.java) are excluded from pattern checks exactly like the judge excludes them at import time;\n// the '' entry also drops dot files such as `.DS_Store`, whose extension is ''.\nconst IGNORED_DIRECTORY_NAMES: ReadonlySet<string> = new Set(['__pycache__']);\nconst NON_CODE_EXTENSIONS: ReadonlySet<string> = new Set([\n '',\n '.class',\n '.exe',\n '.jar',\n '.out',\n '.pyc',\n '.png',\n '.gif',\n]);\n\nexport interface SourceFile {\n path: string;\n data: string;\n}\n\nexport async function isFile(path: string): Promise<boolean> {\n try {\n const stats = await stat(path);\n return stats.isFile();\n } catch {\n return false;\n }\n}\n\n/** Whether the path is a regular file (not a symlink), as the judge's problem discovery requires. */\nexport async function isRegularFile(path: string): Promise<boolean> {\n try {\n const stats = await lstat(path);\n return stats.isFile();\n } catch {\n return false;\n }\n}\n\n/** Whether the path is a directory that is not a symbolic link, which the judge's discovery does not traverse. */\nexport async function isRegularDirectory(path: string): Promise<boolean> {\n try {\n const stats = await lstat(path);\n return stats.isDirectory();\n } catch {\n return false;\n }\n}\n\nexport async function isDirectory(path: string): Promise<boolean> {\n try {\n const stats = await stat(path);\n return stats.isDirectory();\n } catch {\n return false;\n }\n}\n\n/**\n * Collects the problem definitions under a directory the way the judge discovers them: `problem.md`\n * names its directory and `<id>.problem.md` names itself, at any depth, without traversing symbolic\n * links or a nested course (whose problems belong to that nearer course). The result maps each\n * problem ID to the paths of its definitions (relative to the root); the judge rejects an ID\n * defined more than once.\n */\nexport async function collectProblemDefinitions(rootDirectoryPath: string): Promise<Map<string, string[]>> {\n const definitionPathsById = new Map<string, string[]>();\n await collectProblemDefinitionsInto(rootDirectoryPath, rootDirectoryPath, definitionPathsById);\n return definitionPathsById;\n}\n\nasync function collectProblemDefinitionsInto(\n rootDirectoryPath: string,\n directoryPath: string,\n definitionPathsById: Map<string, string[]>\n): Promise<void> {\n const dirents = await readdir(directoryPath, { withFileTypes: true });\n if (\n directoryPath !== rootDirectoryPath &&\n dirents.some((dirent) => dirent.isFile() && (dirent.name === 'course.yaml' || dirent.name === 'course.yml'))\n ) {\n return;\n }\n for (const dirent of dirents) {\n if (dirent.isFile()) {\n const problemId =\n dirent.name === 'problem.md'\n ? basename(directoryPath)\n : dirent.name.endsWith('.problem.md')\n ? // The judge lowercases a v1 file name before using it as the ID (a directory name is used as is).\n dirent.name.slice(0, -'.problem.md'.length).toLowerCase()\n : undefined;\n if (problemId === undefined) continue;\n const definitionPaths = definitionPathsById.get(problemId) ?? [];\n definitionPaths.push(relative(rootDirectoryPath, join(directoryPath, dirent.name)));\n definitionPathsById.set(problemId, definitionPaths);\n } else if (dirent.isDirectory() && !isExcludedFromDiscovery(directoryPath, dirent.name)) {\n await collectProblemDefinitionsInto(rootDirectoryPath, join(directoryPath, dirent.name), definitionPathsById);\n }\n }\n}\n\n// The judge's discovery glob skips dot directories and these excludes.\nfunction isExcludedFromDiscovery(parentDirectoryPath: string, directoryName: string): boolean {\n return (\n directoryName.startsWith('.') ||\n directoryName === 'node_modules' ||\n directoryName === 'target' ||\n (directoryName === 'temp' && basename(parentDirectoryPath) === 'judge-environments')\n );\n}\n\n/**\n * Reports discovered problem definitions the judge refuses: an ID that does not match the ID pattern\n * aborts discovery of the whole repository, and an ID defined more than once is a conflict.\n */\nexport function reportProblemDefinitionIssues(\n definitionPathsById: ReadonlyMap<string, string[]>,\n errors: string[]\n): void {\n for (const [problemId, definitionPaths] of definitionPathsById) {\n if (!LEARNING_MATERIAL_ID_REGEX.test(problemId)) {\n errors.push(\n `problem ID \"${problemId}\" (${definitionPaths.join(', ')}) must match ${LEARNING_MATERIAL_ID_REGEX}; the judge aborts discovery on it`\n );\n }\n if (definitionPaths.length > 1) {\n errors.push(\n `problem ID \"${problemId}\" is defined more than once (${definitionPaths.join(', ')}); the judge rejects the conflict`\n );\n }\n }\n}\n\n/** Reports a problems directory the judge cannot search (missing, or a symbolic link); returns whether it is usable. */\nexport async function isUsableProblemsDirectory(problemsDirectoryPath: string, errors: string[]): Promise<boolean> {\n if (await isRegularDirectory(problemsDirectoryPath)) return true;\n errors.push(\n (await isDirectory(problemsDirectoryPath))\n ? `problems directory is a symbolic link, which the judge does not traverse: ${problemsDirectoryPath}`\n : `problems directory not found: ${problemsDirectoryPath}`\n );\n return false;\n}\n\n/** Collects the problem IDs a directory defines, reporting the issues the judge refuses; undefined when it cannot be searched. */\nexport async function collectProblemIdsFromDirectory(\n problemsDirectoryPath: string,\n errors: string[]\n): Promise<Set<string> | undefined> {\n if (!(await isUsableProblemsDirectory(problemsDirectoryPath, errors))) return undefined;\n const definitionPathsById = await collectProblemDefinitions(problemsDirectoryPath);\n reportProblemDefinitionIssues(definitionPathsById, errors);\n return new Set(definitionPathsById.keys());\n}\n\nexport function reportDanglingProblemReferences(\n problemIds: Iterable<string>,\n availableProblemIds: ReadonlySet<string>,\n problemsDirectoryPath: string | undefined,\n errors: string[]\n): void {\n const location = problemsDirectoryPath === undefined ? '' : ` under ${problemsDirectoryPath}`;\n for (const problemId of new Set(problemIds)) {\n if (!availableProblemIds.has(problemId)) {\n errors.push(`problem \"${problemId}\" is referenced but does not exist${location}`);\n }\n }\n}\n\n/** Reads all text files under a directory, with paths relative to the given root. */\nexport async function readSourceFilesRecursively(rootDirectoryPath: string): Promise<SourceFile[]> {\n const files: SourceFile[] = [];\n await collectSourceFiles(rootDirectoryPath, rootDirectoryPath, files);\n return files.toSorted((f1, f2) => f1.path.localeCompare(f2.path));\n}\n\nasync function collectSourceFiles(\n rootDirectoryPath: string,\n directoryPath: string,\n files: SourceFile[]\n): Promise<void> {\n for (const dirent of await readdir(directoryPath, { withFileTypes: true })) {\n const entryPath = join(directoryPath, dirent.name);\n if (dirent.isDirectory()) {\n if (!IGNORED_DIRECTORY_NAMES.has(dirent.name)) {\n await collectSourceFiles(rootDirectoryPath, entryPath, files);\n }\n } else if (dirent.isFile() && !NON_CODE_EXTENSIONS.has(extname(dirent.name).toLowerCase())) {\n files.push({ path: relative(rootDirectoryPath, entryPath), data: await readFile(entryPath, 'utf8') });\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"fsHelpers.cjs","names":["stat","lstat","readdir","basename","relative","join","LEARNING_MATERIAL_ID_REGEX","extname","readFile","require_schemas","node_path","node_fs_promises","IGNORED_DIRECTORY_NAMES","NON_CODE_EXTENSIONS","isFile","path","isRegularFile","isRegularDirectory","isDirectory","collectProblemDefinitions","definitionPathsById","collectProblemDefinitionsInto","rootDirectoryPath","dirents","directoryPath","dirent","problemId","definitionPaths","isExcludedFromDiscovery","directoryName","parentDirectoryPath","reportProblemDefinitionIssues","errors","isUsableProblemsDirectory","problemsDirectoryPath","collectProblemIdsFromDirectory","reportDanglingProblemReferences","location","problemIds","availableProblemIds","readSourceFilesRecursively","files","collectSourceFiles","f1","f2","entryPath"],"sources":["../../src/learningMaterial/fsHelpers.ts"],"sourcesContent":["import { lstat, readdir, readFile, stat } from 'node:fs/promises';\nimport { basename, extname, join, relative } from 'node:path';\n\nimport { LEARNING_MATERIAL_ID_REGEX } from './schemas.js';\n\n// Files the judge importer skips when reading problem directories. The extension filter is ported\n// from the judge's readSourceCodeFilesInDirectory so compile artifacts (e.g. Main.class next to\n// Main.java) are excluded from pattern checks exactly like the judge excludes them at import time;\n// the '' entry also drops dot files such as `.DS_Store`, whose extension is ''.\nconst IGNORED_DIRECTORY_NAMES: ReadonlySet<string> = new Set(['__pycache__']);\nconst NON_CODE_EXTENSIONS: ReadonlySet<string> = new Set([\n '',\n '.class',\n '.exe',\n '.jar',\n '.out',\n '.pyc',\n '.png',\n '.gif',\n]);\n\nexport interface SourceFile {\n path: string;\n data: string;\n}\n\nexport async function isFile(path: string): Promise<boolean> {\n try {\n const stats = await stat(path);\n return stats.isFile();\n } catch {\n return false;\n }\n}\n\n/** Whether the path is a regular file (not a symlink), as the judge's problem discovery requires. */\nexport async function isRegularFile(path: string): Promise<boolean> {\n try {\n const stats = await lstat(path);\n return stats.isFile();\n } catch {\n return false;\n }\n}\n\n/** Whether the path is a directory that is not a symbolic link, which the judge's discovery does not traverse. */\nexport async function isRegularDirectory(path: string): Promise<boolean> {\n try {\n const stats = await lstat(path);\n return stats.isDirectory();\n } catch {\n return false;\n }\n}\n\nexport async function isDirectory(path: string): Promise<boolean> {\n try {\n const stats = await stat(path);\n return stats.isDirectory();\n } catch {\n return false;\n }\n}\n\n/**\n * Collects the problem definitions under a directory the way the judge discovers them: `problem.md`\n * names its directory and `<id>.problem.md` names itself, at any depth, without traversing symbolic\n * links or a nested course (whose problems belong to that nearer course). The result maps each\n * problem ID to the paths of its definitions (relative to the root); the judge rejects an ID\n * defined more than once.\n */\nexport async function collectProblemDefinitions(rootDirectoryPath: string): Promise<Map<string, string[]>> {\n const definitionPathsById = new Map<string, string[]>();\n await collectProblemDefinitionsInto(rootDirectoryPath, rootDirectoryPath, definitionPathsById);\n return definitionPathsById;\n}\n\nasync function collectProblemDefinitionsInto(\n rootDirectoryPath: string,\n directoryPath: string,\n definitionPathsById: Map<string, string[]>\n): Promise<void> {\n const dirents = await readdir(directoryPath, { withFileTypes: true });\n if (\n directoryPath !== rootDirectoryPath &&\n dirents.some((dirent) => dirent.isFile() && (dirent.name === 'course.yaml' || dirent.name === 'course.yml'))\n ) {\n return;\n }\n for (const dirent of dirents) {\n if (dirent.isFile()) {\n const problemId =\n dirent.name === 'problem.md'\n ? basename(directoryPath)\n : dirent.name.endsWith('.problem.md')\n ? // The judge lowercases a v1 file name before using it as the ID (a directory name is used as is).\n dirent.name.slice(0, -'.problem.md'.length).toLowerCase()\n : undefined;\n if (problemId === undefined) continue;\n const definitionPaths = definitionPathsById.get(problemId) ?? [];\n definitionPaths.push(relative(rootDirectoryPath, join(directoryPath, dirent.name)));\n definitionPathsById.set(problemId, definitionPaths);\n } else if (dirent.isDirectory() && !isExcludedFromDiscovery(directoryPath, dirent.name)) {\n await collectProblemDefinitionsInto(rootDirectoryPath, join(directoryPath, dirent.name), definitionPathsById);\n }\n }\n}\n\n// The judge's discovery glob skips dot directories and these excludes.\nfunction isExcludedFromDiscovery(parentDirectoryPath: string, directoryName: string): boolean {\n return (\n directoryName.startsWith('.') ||\n directoryName === 'node_modules' ||\n directoryName === 'target' ||\n (directoryName === 'temp' && basename(parentDirectoryPath) === 'judge-environments')\n );\n}\n\n/**\n * Reports discovered problem definitions the judge refuses: an ID that does not match the ID pattern\n * aborts discovery of the whole repository, and an ID defined more than once is a conflict.\n */\nexport function reportProblemDefinitionIssues(\n definitionPathsById: ReadonlyMap<string, string[]>,\n errors: string[]\n): void {\n for (const [problemId, definitionPaths] of definitionPathsById) {\n if (!LEARNING_MATERIAL_ID_REGEX.test(problemId)) {\n errors.push(\n `problem ID \"${problemId}\" (${definitionPaths.join(', ')}) must match ${LEARNING_MATERIAL_ID_REGEX}; the judge aborts discovery on it`\n );\n }\n if (definitionPaths.length > 1) {\n errors.push(\n `problem ID \"${problemId}\" is defined more than once (${definitionPaths.join(', ')}); the judge rejects the conflict`\n );\n }\n }\n}\n\n/** Reports a problems directory the judge cannot search (missing, or a symbolic link); returns whether it is usable. */\nexport async function isUsableProblemsDirectory(problemsDirectoryPath: string, errors: string[]): Promise<boolean> {\n if (await isRegularDirectory(problemsDirectoryPath)) return true;\n errors.push(\n (await isDirectory(problemsDirectoryPath))\n ? `problems directory is a symbolic link, which the judge does not traverse: ${problemsDirectoryPath}`\n : `problems directory not found: ${problemsDirectoryPath}`\n );\n return false;\n}\n\n/** Collects the problem IDs a directory defines, reporting the issues the judge refuses; undefined when it cannot be searched. */\nexport async function collectProblemIdsFromDirectory(\n problemsDirectoryPath: string,\n errors: string[]\n): Promise<Set<string> | undefined> {\n if (!(await isUsableProblemsDirectory(problemsDirectoryPath, errors))) return undefined;\n const definitionPathsById = await collectProblemDefinitions(problemsDirectoryPath);\n reportProblemDefinitionIssues(definitionPathsById, errors);\n return new Set(definitionPathsById.keys());\n}\n\nexport function reportDanglingProblemReferences(\n problemIds: Iterable<string>,\n availableProblemIds: ReadonlySet<string>,\n problemsDirectoryPath: string | undefined,\n errors: string[]\n): void {\n const location = problemsDirectoryPath === undefined ? '' : ` under ${problemsDirectoryPath}`;\n for (const problemId of new Set(problemIds)) {\n if (!availableProblemIds.has(problemId)) {\n errors.push(`problem \"${problemId}\" is referenced but does not exist${location}`);\n }\n }\n}\n\n/** Reads all text files under a directory, with paths relative to the given root. */\nexport async function readSourceFilesRecursively(rootDirectoryPath: string): Promise<SourceFile[]> {\n const files: SourceFile[] = [];\n await collectSourceFiles(rootDirectoryPath, rootDirectoryPath, files);\n return files.toSorted((f1, f2) => f1.path.localeCompare(f2.path));\n}\n\nasync function collectSourceFiles(\n rootDirectoryPath: string,\n directoryPath: string,\n files: SourceFile[]\n): Promise<void> {\n for (const dirent of await readdir(directoryPath, { withFileTypes: true })) {\n const entryPath = join(directoryPath, dirent.name);\n if (dirent.isDirectory()) {\n if (!IGNORED_DIRECTORY_NAMES.has(dirent.name)) {\n await collectSourceFiles(rootDirectoryPath, entryPath, files);\n }\n } else if (dirent.isFile() && !NON_CODE_EXTENSIONS.has(extname(dirent.name).toLowerCase())) {\n files.push({ path: relative(rootDirectoryPath, entryPath), data: await readFile(entryPath, 'utf8') });\n }\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"fsHelpers.js","names":["IGNORED_DIRECTORY_NAMES","NON_CODE_EXTENSIONS","isFile","stat","path","isRegularFile","lstat","isRegularDirectory","isDirectory","collectProblemDefinitions","definitionPathsById","collectProblemDefinitionsInto","rootDirectoryPath","dirents","readdir","directoryPath","dirent","problemId","basename","definitionPaths","relative","join","isExcludedFromDiscovery","directoryName","parentDirectoryPath","reportProblemDefinitionIssues","LEARNING_MATERIAL_ID_REGEX","errors","isUsableProblemsDirectory","problemsDirectoryPath","collectProblemIdsFromDirectory","reportDanglingProblemReferences","location","problemIds","availableProblemIds","readSourceFilesRecursively","files","collectSourceFiles","f1","f2","entryPath","extname","readFile"],"sources":["../../src/learningMaterial/fsHelpers.ts"],"sourcesContent":["import { lstat, readdir, readFile, stat } from 'node:fs/promises';\nimport { basename, extname, join, relative } from 'node:path';\n\nimport { LEARNING_MATERIAL_ID_REGEX } from './schemas.js';\n\n// Files the judge importer skips when reading problem directories. The extension filter is ported\n// from the judge's readSourceCodeFilesInDirectory so compile artifacts (e.g. Main.class next to\n// Main.java) are excluded from pattern checks exactly like the judge excludes them at import time;\n// the '' entry also drops dot files such as `.DS_Store`, whose extension is ''.\nconst IGNORED_DIRECTORY_NAMES: ReadonlySet<string> = new Set(['__pycache__']);\nconst NON_CODE_EXTENSIONS: ReadonlySet<string> = new Set([\n '',\n '.class',\n '.exe',\n '.jar',\n '.out',\n '.pyc',\n '.png',\n '.gif',\n]);\n\nexport interface SourceFile {\n path: string;\n data: string;\n}\n\nexport async function isFile(path: string): Promise<boolean> {\n try {\n const stats = await stat(path);\n return stats.isFile();\n } catch {\n return false;\n }\n}\n\n/** Whether the path is a regular file (not a symlink), as the judge's problem discovery requires. */\nexport async function isRegularFile(path: string): Promise<boolean> {\n try {\n const stats = await lstat(path);\n return stats.isFile();\n } catch {\n return false;\n }\n}\n\n/** Whether the path is a directory that is not a symbolic link, which the judge's discovery does not traverse. */\nexport async function isRegularDirectory(path: string): Promise<boolean> {\n try {\n const stats = await lstat(path);\n return stats.isDirectory();\n } catch {\n return false;\n }\n}\n\nexport async function isDirectory(path: string): Promise<boolean> {\n try {\n const stats = await stat(path);\n return stats.isDirectory();\n } catch {\n return false;\n }\n}\n\n/**\n * Collects the problem definitions under a directory the way the judge discovers them: `problem.md`\n * names its directory and `<id>.problem.md` names itself, at any depth, without traversing symbolic\n * links or a nested course (whose problems belong to that nearer course). The result maps each\n * problem ID to the paths of its definitions (relative to the root); the judge rejects an ID\n * defined more than once.\n */\nexport async function collectProblemDefinitions(rootDirectoryPath: string): Promise<Map<string, string[]>> {\n const definitionPathsById = new Map<string, string[]>();\n await collectProblemDefinitionsInto(rootDirectoryPath, rootDirectoryPath, definitionPathsById);\n return definitionPathsById;\n}\n\nasync function collectProblemDefinitionsInto(\n rootDirectoryPath: string,\n directoryPath: string,\n definitionPathsById: Map<string, string[]>\n): Promise<void> {\n const dirents = await readdir(directoryPath, { withFileTypes: true });\n if (\n directoryPath !== rootDirectoryPath &&\n dirents.some((dirent) => dirent.isFile() && (dirent.name === 'course.yaml' || dirent.name === 'course.yml'))\n ) {\n return;\n }\n for (const dirent of dirents) {\n if (dirent.isFile()) {\n const problemId =\n dirent.name === 'problem.md'\n ? basename(directoryPath)\n : dirent.name.endsWith('.problem.md')\n ? // The judge lowercases a v1 file name before using it as the ID (a directory name is used as is).\n dirent.name.slice(0, -'.problem.md'.length).toLowerCase()\n : undefined;\n if (problemId === undefined) continue;\n const definitionPaths = definitionPathsById.get(problemId) ?? [];\n definitionPaths.push(relative(rootDirectoryPath, join(directoryPath, dirent.name)));\n definitionPathsById.set(problemId, definitionPaths);\n } else if (dirent.isDirectory() && !isExcludedFromDiscovery(directoryPath, dirent.name)) {\n await collectProblemDefinitionsInto(rootDirectoryPath, join(directoryPath, dirent.name), definitionPathsById);\n }\n }\n}\n\n// The judge's discovery glob skips dot directories and these excludes.\nfunction isExcludedFromDiscovery(parentDirectoryPath: string, directoryName: string): boolean {\n return (\n directoryName.startsWith('.') ||\n directoryName === 'node_modules' ||\n directoryName === 'target' ||\n (directoryName === 'temp' && basename(parentDirectoryPath) === 'judge-environments')\n );\n}\n\n/**\n * Reports discovered problem definitions the judge refuses: an ID that does not match the ID pattern\n * aborts discovery of the whole repository, and an ID defined more than once is a conflict.\n */\nexport function reportProblemDefinitionIssues(\n definitionPathsById: ReadonlyMap<string, string[]>,\n errors: string[]\n): void {\n for (const [problemId, definitionPaths] of definitionPathsById) {\n if (!LEARNING_MATERIAL_ID_REGEX.test(problemId)) {\n errors.push(\n `problem ID \"${problemId}\" (${definitionPaths.join(', ')}) must match ${LEARNING_MATERIAL_ID_REGEX}; the judge aborts discovery on it`\n );\n }\n if (definitionPaths.length > 1) {\n errors.push(\n `problem ID \"${problemId}\" is defined more than once (${definitionPaths.join(', ')}); the judge rejects the conflict`\n );\n }\n }\n}\n\n/** Reports a problems directory the judge cannot search (missing, or a symbolic link); returns whether it is usable. */\nexport async function isUsableProblemsDirectory(problemsDirectoryPath: string, errors: string[]): Promise<boolean> {\n if (await isRegularDirectory(problemsDirectoryPath)) return true;\n errors.push(\n (await isDirectory(problemsDirectoryPath))\n ? `problems directory is a symbolic link, which the judge does not traverse: ${problemsDirectoryPath}`\n : `problems directory not found: ${problemsDirectoryPath}`\n );\n return false;\n}\n\n/** Collects the problem IDs a directory defines, reporting the issues the judge refuses; undefined when it cannot be searched. */\nexport async function collectProblemIdsFromDirectory(\n problemsDirectoryPath: string,\n errors: string[]\n): Promise<Set<string> | undefined> {\n if (!(await isUsableProblemsDirectory(problemsDirectoryPath, errors))) return undefined;\n const definitionPathsById = await collectProblemDefinitions(problemsDirectoryPath);\n reportProblemDefinitionIssues(definitionPathsById, errors);\n return new Set(definitionPathsById.keys());\n}\n\nexport function reportDanglingProblemReferences(\n problemIds: Iterable<string>,\n availableProblemIds: ReadonlySet<string>,\n problemsDirectoryPath: string | undefined,\n errors: string[]\n): void {\n const location = problemsDirectoryPath === undefined ? '' : ` under ${problemsDirectoryPath}`;\n for (const problemId of new Set(problemIds)) {\n if (!availableProblemIds.has(problemId)) {\n errors.push(`problem \"${problemId}\" is referenced but does not exist${location}`);\n }\n }\n}\n\n/** Reads all text files under a directory, with paths relative to the given root. */\nexport async function readSourceFilesRecursively(rootDirectoryPath: string): Promise<SourceFile[]> {\n const files: SourceFile[] = [];\n await collectSourceFiles(rootDirectoryPath, rootDirectoryPath, files);\n return files.toSorted((f1, f2) => f1.path.localeCompare(f2.path));\n}\n\nasync function collectSourceFiles(\n rootDirectoryPath: string,\n directoryPath: string,\n files: SourceFile[]\n): Promise<void> {\n for (const dirent of await readdir(directoryPath, { withFileTypes: true })) {\n const entryPath = join(directoryPath, dirent.name);\n if (dirent.isDirectory()) {\n if (!IGNORED_DIRECTORY_NAMES.has(dirent.name)) {\n await collectSourceFiles(rootDirectoryPath, entryPath, files);\n }\n } else if (dirent.isFile() && !NON_CODE_EXTENSIONS.has(extname(dirent.name).toLowerCase())) {\n files.push({ path: relative(rootDirectoryPath, entryPath), data: await readFile(entryPath, 'utf8') });\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"fsHelpers.js","names":["IGNORED_DIRECTORY_NAMES","NON_CODE_EXTENSIONS","isFile","stat","path","isRegularFile","lstat","isRegularDirectory","isDirectory","collectProblemDefinitions","definitionPathsById","collectProblemDefinitionsInto","rootDirectoryPath","dirents","readdir","directoryPath","dirent","problemId","basename","definitionPaths","relative","join","isExcludedFromDiscovery","directoryName","parentDirectoryPath","reportProblemDefinitionIssues","LEARNING_MATERIAL_ID_REGEX","errors","isUsableProblemsDirectory","problemsDirectoryPath","collectProblemIdsFromDirectory","reportDanglingProblemReferences","location","problemIds","availableProblemIds","readSourceFilesRecursively","files","collectSourceFiles","f1","f2","entryPath","extname","readFile"],"sources":["../../src/learningMaterial/fsHelpers.ts"],"sourcesContent":["import { lstat, readdir, readFile, stat } from 'node:fs/promises';\nimport { basename, extname, join, relative } from 'node:path';\n\nimport { LEARNING_MATERIAL_ID_REGEX } from './schemas.js';\n\n// Files the judge importer skips when reading problem directories. The extension filter is ported\n// from the judge's readSourceCodeFilesInDirectory so compile artifacts (e.g. Main.class next to\n// Main.java) are excluded from pattern checks exactly like the judge excludes them at import time;\n// the '' entry also drops dot files such as `.DS_Store`, whose extension is ''.\nconst IGNORED_DIRECTORY_NAMES: ReadonlySet<string> = new Set(['__pycache__']);\nconst NON_CODE_EXTENSIONS: ReadonlySet<string> = new Set([\n '',\n '.class',\n '.exe',\n '.jar',\n '.out',\n '.pyc',\n '.png',\n '.gif',\n]);\n\nexport interface SourceFile {\n path: string;\n data: string;\n}\n\nexport async function isFile(path: string): Promise<boolean> {\n try {\n const stats = await stat(path);\n return stats.isFile();\n } catch {\n return false;\n }\n}\n\n/** Whether the path is a regular file (not a symlink), as the judge's problem discovery requires. */\nexport async function isRegularFile(path: string): Promise<boolean> {\n try {\n const stats = await lstat(path);\n return stats.isFile();\n } catch {\n return false;\n }\n}\n\n/** Whether the path is a directory that is not a symbolic link, which the judge's discovery does not traverse. */\nexport async function isRegularDirectory(path: string): Promise<boolean> {\n try {\n const stats = await lstat(path);\n return stats.isDirectory();\n } catch {\n return false;\n }\n}\n\nexport async function isDirectory(path: string): Promise<boolean> {\n try {\n const stats = await stat(path);\n return stats.isDirectory();\n } catch {\n return false;\n }\n}\n\n/**\n * Collects the problem definitions under a directory the way the judge discovers them: `problem.md`\n * names its directory and `<id>.problem.md` names itself, at any depth, without traversing symbolic\n * links or a nested course (whose problems belong to that nearer course). The result maps each\n * problem ID to the paths of its definitions (relative to the root); the judge rejects an ID\n * defined more than once.\n */\nexport async function collectProblemDefinitions(rootDirectoryPath: string): Promise<Map<string, string[]>> {\n const definitionPathsById = new Map<string, string[]>();\n await collectProblemDefinitionsInto(rootDirectoryPath, rootDirectoryPath, definitionPathsById);\n return definitionPathsById;\n}\n\nasync function collectProblemDefinitionsInto(\n rootDirectoryPath: string,\n directoryPath: string,\n definitionPathsById: Map<string, string[]>\n): Promise<void> {\n const dirents = await readdir(directoryPath, { withFileTypes: true });\n if (\n directoryPath !== rootDirectoryPath &&\n dirents.some((dirent) => dirent.isFile() && (dirent.name === 'course.yaml' || dirent.name === 'course.yml'))\n ) {\n return;\n }\n for (const dirent of dirents) {\n if (dirent.isFile()) {\n const problemId =\n dirent.name === 'problem.md'\n ? basename(directoryPath)\n : dirent.name.endsWith('.problem.md')\n ? // The judge lowercases a v1 file name before using it as the ID (a directory name is used as is).\n dirent.name.slice(0, -'.problem.md'.length).toLowerCase()\n : undefined;\n if (problemId === undefined) continue;\n const definitionPaths = definitionPathsById.get(problemId) ?? [];\n definitionPaths.push(relative(rootDirectoryPath, join(directoryPath, dirent.name)));\n definitionPathsById.set(problemId, definitionPaths);\n } else if (dirent.isDirectory() && !isExcludedFromDiscovery(directoryPath, dirent.name)) {\n await collectProblemDefinitionsInto(rootDirectoryPath, join(directoryPath, dirent.name), definitionPathsById);\n }\n }\n}\n\n// The judge's discovery glob skips dot directories and these excludes.\nfunction isExcludedFromDiscovery(parentDirectoryPath: string, directoryName: string): boolean {\n return (\n directoryName.startsWith('.') ||\n directoryName === 'node_modules' ||\n directoryName === 'target' ||\n (directoryName === 'temp' && basename(parentDirectoryPath) === 'judge-environments')\n );\n}\n\n/**\n * Reports discovered problem definitions the judge refuses: an ID that does not match the ID pattern\n * aborts discovery of the whole repository, and an ID defined more than once is a conflict.\n */\nexport function reportProblemDefinitionIssues(\n definitionPathsById: ReadonlyMap<string, string[]>,\n errors: string[]\n): void {\n for (const [problemId, definitionPaths] of definitionPathsById) {\n if (!LEARNING_MATERIAL_ID_REGEX.test(problemId)) {\n errors.push(\n `problem ID \"${problemId}\" (${definitionPaths.join(', ')}) must match ${LEARNING_MATERIAL_ID_REGEX}; the judge aborts discovery on it`\n );\n }\n if (definitionPaths.length > 1) {\n errors.push(\n `problem ID \"${problemId}\" is defined more than once (${definitionPaths.join(', ')}); the judge rejects the conflict`\n );\n }\n }\n}\n\n/** Reports a problems directory the judge cannot search (missing, or a symbolic link); returns whether it is usable. */\nexport async function isUsableProblemsDirectory(problemsDirectoryPath: string, errors: string[]): Promise<boolean> {\n if (await isRegularDirectory(problemsDirectoryPath)) return true;\n errors.push(\n (await isDirectory(problemsDirectoryPath))\n ? `problems directory is a symbolic link, which the judge does not traverse: ${problemsDirectoryPath}`\n : `problems directory not found: ${problemsDirectoryPath}`\n );\n return false;\n}\n\n/** Collects the problem IDs a directory defines, reporting the issues the judge refuses; undefined when it cannot be searched. */\nexport async function collectProblemIdsFromDirectory(\n problemsDirectoryPath: string,\n errors: string[]\n): Promise<Set<string> | undefined> {\n if (!(await isUsableProblemsDirectory(problemsDirectoryPath, errors))) return undefined;\n const definitionPathsById = await collectProblemDefinitions(problemsDirectoryPath);\n reportProblemDefinitionIssues(definitionPathsById, errors);\n return new Set(definitionPathsById.keys());\n}\n\nexport function reportDanglingProblemReferences(\n problemIds: Iterable<string>,\n availableProblemIds: ReadonlySet<string>,\n problemsDirectoryPath: string | undefined,\n errors: string[]\n): void {\n const location = problemsDirectoryPath === undefined ? '' : ` under ${problemsDirectoryPath}`;\n for (const problemId of new Set(problemIds)) {\n if (!availableProblemIds.has(problemId)) {\n errors.push(`problem \"${problemId}\" is referenced but does not exist${location}`);\n }\n }\n}\n\n/** Reads all text files under a directory, with paths relative to the given root. */\nexport async function readSourceFilesRecursively(rootDirectoryPath: string): Promise<SourceFile[]> {\n const files: SourceFile[] = [];\n await collectSourceFiles(rootDirectoryPath, rootDirectoryPath, files);\n return files.toSorted((f1, f2) => f1.path.localeCompare(f2.path));\n}\n\nasync function collectSourceFiles(\n rootDirectoryPath: string,\n directoryPath: string,\n files: SourceFile[]\n): Promise<void> {\n for (const dirent of await readdir(directoryPath, { withFileTypes: true })) {\n const entryPath = join(directoryPath, dirent.name);\n if (dirent.isDirectory()) {\n if (!IGNORED_DIRECTORY_NAMES.has(dirent.name)) {\n await collectSourceFiles(rootDirectoryPath, entryPath, files);\n }\n } else if (dirent.isFile() && !NON_CODE_EXTENSIONS.has(extname(dirent.name).toLowerCase())) {\n files.push({ path: relative(rootDirectoryPath, entryPath), data: await readFile(entryPath, 'utf8') });\n }\n }\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- "use strict";const e=require("../types/language.cjs"),t=require("../types/problem.cjs");let n=require("zod");const r=/^[0-9_a-z-]+$/,i=n.z.string().regex(r),a=Object.keys(e.languageIdToDefinition),o=n.z.enum(a),s=n.z.string().regex(/^\w+$/),c=n.z.strictObject({name:n.z.string().min(1),type:n.z.enum([`prompt_study`]).optional(),timeLimitMs:n.z.number().int().min(0).optional(),memoryLimitByte:n.z.number().int().min(0).optional(),requiredRegExpsInCode:t.codeRuleSchema.array().default([]),forbiddenRegExpsInCode:t.codeRuleSchema.array().default([]),forbiddenTextsInCode:t.codeRuleSchema.array().default([]),canCreateFiles:n.z.boolean().optional(),isEditorDisabled:n.z.boolean().optional(),isAttachedFileRequired:n.z.boolean().optional(),isManualScoringRequired:n.z.boolean().optional(),isVotable:n.z.boolean().optional(),requiredEnvironmentVariables:n.z.array(s).default([]),requiredOutputFilePaths:n.z.string().min(1).array().default([]),requiredSubmissionFilePaths:n.z.string().min(1).array().default([])}),l={availableLanguageIds:o.array().optional(),areTestCasesHidden:n.z.boolean().optional(),isProblemGradingResultHidden:n.z.boolean().optional(),isAutoFormatDisabled:n.z.boolean().optional(),isCopyAndPasteDisabled:n.z.boolean().optional(),isDebugHintDisabled:n.z.boolean().optional(),isFixHintDisabled:n.z.boolean().optional(),isDiffHintDisabled:n.z.boolean().optional(),debugHintWaitingSeconds:n.z.number().min(0).optional(),fixHintWaitingSeconds:n.z.number().min(0).optional(),diffHintWaitingSeconds:n.z.number().min(0).optional(),submissionOpenedAt:n.z.iso.datetime({offset:!0}).optional(),submissionSoftClosedAt:n.z.iso.datetime({offset:!0}).optional(),submissionHardClosedAt:n.z.iso.datetime({offset:!0}).optional(),isAutoTranslationDisabled:n.z.boolean().optional(),isModelAnswerShownAfterDeadline:n.z.boolean().optional(),isVotable:n.z.boolean().optional(),isMaterialChatDisabled:n.z.boolean().optional()},u=n.z.strictObject({...l,name:n.z.string().min(1),description:n.z.string(),author:n.z.string().min(1).optional(),isMotivationFeatureEnabled:n.z.boolean().optional(),isPublic:n.z.boolean().optional(),lectures:n.z.array(n.z.object({id:i,name:n.z.string().min(1),description:n.z.string()})).optional()}),d=n.z.string().array().min(1).refine(e=>e.length===new Set(e).size,{error:`options must contain unique values`}),f=n.z.string().regex(/^\d+$/,{error:`must be a non-negative integer index (quoted numbers such as "0" are accepted)`}).transform(Number),p={id:i,isResubmittable:n.z.boolean().optional(),isSurvey:n.z.boolean().optional(),explanation:n.z.string().optional(),hint:n.z.string().optional()},m={...p,type:n.z.literal(`select`),options:d,answerIndex:n.z.union([n.z.number().int().nonnegative(),f,n.z.number().int().nonnegative().array(),f.array()]).optional()},h={...p,type:n.z.literal(`select_multiple`),options:d,answerIndices:n.z.union([n.z.number().int().nonnegative().array(),f.array()]).optional()},g={...p,type:n.z.literal(`text`),answerPattern:n.z.string().min(1).optional(),modelAnswer:n.z.string().optional()},_=n.z.discriminatedUnion(`type`,[n.z.strictObject({...m,question:n.z.string()}),n.z.strictObject({...h,question:n.z.string()}),n.z.strictObject({...g,question:n.z.string()})]),v=n.z.discriminatedUnion(`type`,[n.z.strictObject({...m,question:n.z.string().optional()}),n.z.strictObject({...h,question:n.z.string().optional()}),n.z.strictObject({...g,question:n.z.string().optional()})]),y=n.z.strictObject({...l,name:n.z.string().min(1),problems:n.z.object({id:i,courseId:i.optional()}).array().optional(),questions:n.z.array(v).optional(),turtleGraphicsQuestions:n.z.object({id:i}).array().optional(),isExamination:n.z.boolean().optional(),isMockExamination:n.z.boolean().optional(),isRealtimeSurvey:n.z.boolean().optional()}),b=n.z.strictObject({id:i,name:n.z.string().min(1),openedAt:n.z.iso.datetime({offset:!0}),closedAt:n.z.iso.datetime({offset:!0}),password:n.z.string().min(1).optional()}).refine(e=>new Date(e.openedAt)<new Date(e.closedAt),{error:`openedAt must be earlier than closedAt`}),x=n.z.strictObject({id:i,score:n.z.number().int().nonnegative(),courseId:i.optional(),areTestCasesHidden:l.areTestCasesHidden,isDebugHintDisabled:l.isDebugHintDisabled,isFixHintDisabled:l.isFixHintDisabled,isDiffHintDisabled:l.isDiffHintDisabled,debugHintWaitingSeconds:l.debugHintWaitingSeconds,fixHintWaitingSeconds:l.fixHintWaitingSeconds,diffHintWaitingSeconds:l.diffHintWaitingSeconds,isMaterialChatDisabled:l.isMaterialChatDisabled}),S=n.z.strictObject({...l,name:n.z.string().min(1),description:n.z.string().optional(),adminEmails:n.z.array(n.z.string().trim().email()).optional(),showsProblemsAfterClose:n.z.boolean().optional(),divisions:n.z.array(b).min(1),problems:n.z.array(x).min(1)});exports.CONTEST_MATERIAL_FILE_SUFFIX=`.contest.yaml`,exports.LEARNING_MATERIAL_ID_REGEX=r,exports.availableLanguageIds=a,exports.contestFileSchema=S,exports.courseFileSchema=u,exports.learningMaterialIdSchema=i,exports.materialFrontmatterSchema=y,exports.problemFrontmatterSchema=c,exports.questionInCodeBlockSchema=_;
1
+ "use strict";const e=require("../types/language.cjs"),t=require("../types/problem.cjs");let n=require("zod"),r=require("@willbooster/shared-lib");const i=/^[0-9_a-z-]+$/,a=n.z.string().regex(i),o=Object.keys(e.languageIdToDefinition),s=n.z.enum(o),c=n.z.string().regex(/^\w+$/),l=n.z.record(n.z.string(),n.z.unknown()),u=n.z.number().int().min(100).max(599),d=n.z.string().startsWith(`/`).refine(e=>!e.includes(`?`)&&!e.includes(`#`),`path must not carry a query or fragment; use query`).refine(e=>e.split(`/`).every(e=>!/[{}]/.test(e)||/^\{[^{}]+\}$/.test(e)),`a {name} placeholder must be one whole path segment`),f=l.refine(e=>k(e),`header names in properties and required must be lower case`),p=n.z.strictObject({playgrounds:n.z.array(n.z.enum([`bbs`,`backlog`,`hosting`])).min(1),expectedRequest:n.z.strictObject({method:n.z.enum([`GET`,`POST`,`PUT`,`PATCH`,`DELETE`]),path:d,status:n.z.union([u,n.z.array(u).min(1)]).optional(),query:l.optional(),headers:f.optional(),body:l.optional()})}),m=n.z.strictObject({name:n.z.string().min(1),type:n.z.enum([`prompt_study`]).optional(),timeLimitMs:n.z.number().int().min(0).optional(),memoryLimitByte:n.z.number().int().min(0).optional(),requiredRegExpsInCode:t.codeRuleSchema.array().default([]),forbiddenRegExpsInCode:t.codeRuleSchema.array().default([]),forbiddenTextsInCode:t.codeRuleSchema.array().default([]),canCreateFiles:n.z.boolean().optional(),isEditorDisabled:n.z.boolean().optional(),isAttachedFileRequired:n.z.boolean().optional(),isManualScoringRequired:n.z.boolean().optional(),isVotable:n.z.boolean().optional(),requiredEnvironmentVariables:n.z.array(c).default([]),requiredOutputFilePaths:n.z.string().min(1).array().default([]),requiredSubmissionFilePaths:n.z.string().min(1).array().default([]),agentChallenge:p.optional()}),h={availableLanguageIds:s.array().optional(),areTestCasesHidden:n.z.boolean().optional(),isProblemGradingResultHidden:n.z.boolean().optional(),isAutoFormatDisabled:n.z.boolean().optional(),isCopyAndPasteDisabled:n.z.boolean().optional(),isDebugHintDisabled:n.z.boolean().optional(),isFixHintDisabled:n.z.boolean().optional(),isDiffHintDisabled:n.z.boolean().optional(),debugHintWaitingSeconds:n.z.number().min(0).optional(),fixHintWaitingSeconds:n.z.number().min(0).optional(),diffHintWaitingSeconds:n.z.number().min(0).optional(),submissionOpenedAt:n.z.iso.datetime({offset:!0}).optional(),submissionSoftClosedAt:n.z.iso.datetime({offset:!0}).optional(),submissionHardClosedAt:n.z.iso.datetime({offset:!0}).optional(),isAutoTranslationDisabled:n.z.boolean().optional(),isModelAnswerShownAfterDeadline:n.z.boolean().optional(),isVotable:n.z.boolean().optional(),isMaterialChatDisabled:n.z.boolean().optional()},g=n.z.strictObject({...h,name:n.z.string().min(1),description:n.z.string(),author:n.z.string().min(1).optional(),isMotivationFeatureEnabled:n.z.boolean().optional(),isPublic:n.z.boolean().optional(),lectures:n.z.array(n.z.object({id:a,name:n.z.string().min(1),description:n.z.string()})).optional()}),_=n.z.string().array().min(1).refine(e=>e.length===new Set(e).size,{error:`options must contain unique values`}),v=n.z.string().regex(/^\d+$/,{error:`must be a non-negative integer index (quoted numbers such as "0" are accepted)`}).transform(Number),y={id:a,isResubmittable:n.z.boolean().optional(),isSurvey:n.z.boolean().optional(),explanation:n.z.string().optional(),hint:n.z.string().optional()},b={...y,type:n.z.literal(`select`),options:_,answerIndex:n.z.union([n.z.number().int().nonnegative(),v,n.z.number().int().nonnegative().array(),v.array()]).optional()},x={...y,type:n.z.literal(`select_multiple`),options:_,answerIndices:n.z.union([n.z.number().int().nonnegative().array(),v.array()]).optional()},S={...y,type:n.z.literal(`text`),answerPattern:n.z.string().min(1).optional(),modelAnswer:n.z.string().optional()},C=n.z.discriminatedUnion(`type`,[n.z.strictObject({...b,question:n.z.string()}),n.z.strictObject({...x,question:n.z.string()}),n.z.strictObject({...S,question:n.z.string()})]),w=n.z.discriminatedUnion(`type`,[n.z.strictObject({...b,question:n.z.string().optional()}),n.z.strictObject({...x,question:n.z.string().optional()}),n.z.strictObject({...S,question:n.z.string().optional()})]),T=n.z.strictObject({...h,name:n.z.string().min(1),problems:n.z.strictObject({id:a}).array().optional(),questions:n.z.array(w).optional(),turtleGraphicsQuestions:n.z.object({id:a}).array().optional(),isExamination:n.z.boolean().optional(),isMockExamination:n.z.boolean().optional(),isRealtimeSurvey:n.z.boolean().optional()}),E=n.z.strictObject({id:a,name:n.z.string().min(1),openedAt:n.z.iso.datetime({offset:!0}),closedAt:n.z.iso.datetime({offset:!0}),password:n.z.string().min(1).optional()}).refine(e=>new Date(e.openedAt)<new Date(e.closedAt),{error:`openedAt must be earlier than closedAt`}),D=n.z.strictObject({id:a,score:n.z.number().int().nonnegative(),areTestCasesHidden:h.areTestCasesHidden,isDebugHintDisabled:h.isDebugHintDisabled,isFixHintDisabled:h.isFixHintDisabled,isDiffHintDisabled:h.isDiffHintDisabled,debugHintWaitingSeconds:h.debugHintWaitingSeconds,fixHintWaitingSeconds:h.fixHintWaitingSeconds,diffHintWaitingSeconds:h.diffHintWaitingSeconds,isMaterialChatDisabled:h.isMaterialChatDisabled}),O=n.z.strictObject({...h,name:n.z.string().min(1),description:n.z.string().optional(),adminEmails:n.z.array(n.z.string().trim().email()).optional(),showsProblemsAfterClose:n.z.boolean().optional(),divisions:n.z.array(E).min(1),problems:n.z.array(D).min(1)});function k(e,t=/* @__PURE__ */ new WeakSet){if(typeof e!=`object`||!e||t.has(e))return!0;if(t.add(e),Array.isArray(e))return e.every(e=>k(e,t));if(!(0,r.isRecord)(e))return!0;let n=(0,r.isRecord)(e.properties)?Object.keys(e.properties):[],i=Array.isArray(e.required)?e.required:[];return[...n,...i].every(e=>typeof e!=`string`||e===e.toLowerCase())&&Object.values(e).every(e=>k(e,t))}exports.CONTEST_MATERIAL_FILE_SUFFIX=`.contest.yaml`,exports.LEARNING_MATERIAL_ID_REGEX=i,exports.agentChallengeSchema=p,exports.availableLanguageIds=o,exports.contestFileSchema=O,exports.courseFileSchema=g,exports.learningMaterialIdSchema=a,exports.materialFrontmatterSchema=T,exports.problemFrontmatterSchema=m,exports.questionInCodeBlockSchema=C;
2
2
  //# sourceMappingURL=schemas.cjs.map