From 15ea8232a808ce688341391ce9b855c5eab02209 Mon Sep 17 00:00:00 2001 From: undefined Date: Fri, 2 Sep 2022 14:18:48 +0800 Subject: [PATCH] judge: hack --- .eslintrc.yaml | 2 +- packages/hydrojudge/src/compile.ts | 30 +++++--- packages/hydrojudge/src/judge/hack.ts | 95 ++++++++++++++++++++++++++ packages/hydrojudge/src/judge/index.ts | 3 +- packages/utils/lib/status.ts | 84 ++++++++++++----------- 5 files changed, 165 insertions(+), 49 deletions(-) create mode 100644 packages/hydrojudge/src/judge/hack.ts diff --git a/.eslintrc.yaml b/.eslintrc.yaml index 94305f58..8de1bd39 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -73,7 +73,7 @@ rules: - selector: enum format: [PascalCase] - selector: [class, interface, typeAlias, enumMember] - format: [PascalCase, camelCase] + format: [PascalCase, camelCase, UPPER_CASE] - selector: typeParameter format: [camelCase, PascalCase, UPPER_CASE] diff --git a/packages/hydrojudge/src/compile.ts b/packages/hydrojudge/src/compile.ts index 669cceb0..3254e1e4 100644 --- a/packages/hydrojudge/src/compile.ts +++ b/packages/hydrojudge/src/compile.ts @@ -42,15 +42,12 @@ export default async function compile( }; } -const testlibSrc = findFileSync('@hydrooj/hydrojudge/vendor/testlib/testlib.h'); +const testlibFile = { + src: findFileSync('@hydrooj/hydrojudge/vendor/testlib/testlib.h') +}; -export async function compileChecker(getLang: Function, checkerType: string, checker: string, copyIn: any): Promise { - if (['default', 'strict'].includes(checkerType)) { - return { execute: '', copyIn: {}, clean: () => Promise.resolve(null) }; - } - if (!checkers[checkerType]) throw new FormatError('Unknown checker type {0}.', [checkerType]); - if (checkerType === 'testlib') copyIn['testlib.h'] = { src: testlibSrc }; - const s = checker.replace('@', '.').split('.'); +const guessLanguage = (filename: string, getLang: Function) => { + const s = filename.replace('@', '.').split('.'); let lang; let langId = s.pop(); while (s.length) { @@ -58,7 +55,24 @@ export async function compileChecker(getLang: Function, checkerType: string, che if (lang) break; langId = `${s.pop()}.${langId}`; } + return lang; +}; + +export async function compileChecker(getLang: Function, checkerType: string, checker: string, copyIn: any): Promise { + if (['default', 'strict'].includes(checkerType)) { + return { execute: '', copyIn: {}, clean: () => Promise.resolve(null) }; + } + if (!checkers[checkerType]) throw new FormatError('Unknown checker type {0}.', [checkerType]); + if (checkerType === 'testlib') copyIn['testlib.h'] = testlibFile; + const lang = guessLanguage(checker, getLang); if (!lang) throw new FormatError('Unknown checker language.'); // TODO cache compiled checker return await compile(lang, { src: checker }, copyIn); } + +export async function compileValidator(getLang: Function, validator: string, copyIn: any): Promise { + const lang = guessLanguage(validator, getLang); + if (!lang) throw new FormatError('Unknown validator language.'); + // TODO cache compiled checker + return await compile(lang, { src: validator }, { ...copyIn, 'testlib.h': testlibFile }); +} diff --git a/packages/hydrojudge/src/judge/hack.ts b/packages/hydrojudge/src/judge/hack.ts new file mode 100644 index 00000000..c5c2648b --- /dev/null +++ b/packages/hydrojudge/src/judge/hack.ts @@ -0,0 +1,95 @@ +/* eslint-disable no-sequences */ +import { basename } from 'path'; +import { STATUS } from '@hydrooj/utils/lib/status'; +import checkers from '../checkers'; +import compile, { compileChecker, compileValidator } from '../compile'; +import { Execute } from '../interface'; +import { CmdFile, del, run } from '../sandbox'; +import signals from '../signals'; +import { parseMemoryMB, parseTimeMS } from '../utils'; +import { Context } from './interface'; + +export async function judge(ctx: Context) { + const { config, session } = ctx; + ctx.next({ status: STATUS.STATUS_COMPILING, progress: 0 }); + const userExtraFiles = Object.fromEntries( + (config.user_extra_files || []).map((i) => [basename(i), { src: i }]), + ); + const judgeExtraFiles = Object.fromEntries( + (config.judge_extra_files || []).map((i) => [basename(i), { src: i }]), + ); + const markCleanup = (i: Execute) => (ctx.clean.push(i.clean), i); + const [execute, checker, validator, input] = await Promise.all([ + compile(session.getLang(ctx.lang), ctx.code, userExtraFiles, ctx.next).then(markCleanup), + compileChecker(session.getLang, config.checker_type, config.checker, judgeExtraFiles).then(markCleanup), + compileValidator(session.getLang, config.validator, judgeExtraFiles).then(markCleanup), + ctx.session.fetchFile(ctx.input), + ]); + ctx.next({ status: STATUS.STATUS_JUDGING, progress: 0 }); + const validateResult = await run( + validator.execute, + { + stdin: { src: input }, + copyIn: judgeExtraFiles, + time: parseTimeMS(ctx.config.time || '1s') * execute.time, + memory: parseMemoryMB(ctx.config.memory || '256m'), + }, + ); + if (validateResult.status !== STATUS.STATUS_ACCEPTED) { + const message = `${validateResult.files.stdout || ''}\n${validateResult.files.stderr || ''}`.trim(); + return ctx.end({ status: STATUS.STATUS_FORMAT_ERROR, message }); + } + const copyIn = { ...ctx.execute.copyIn }; + const { filename } = ctx.config; + if (filename) copyIn[`${filename}.in`] = { src: input }; + const res = await run( + execute.execute, + { + stdin: filename ? null : { src: input }, + copyIn, + copyOutCached: [filename ? `${filename}.out?` : 'stdout'], + time: parseTimeMS(ctx.config.time || '1s') * execute.time, + memory: parseMemoryMB(ctx.config.memory || '256m'), + }, + ); + const { code, time, memory } = res; + let { status } = res; + let stdout: CmdFile = { fileId: res.fileIds[filename ? `${filename}.out` : 'stdout'] }; + const stderr = { fileId: res.fileIds['stderr'] }; + if (!stdout.fileId) stdout = { content: '' }; + let message: any = ''; + if (status === STATUS.STATUS_ACCEPTED) { + if (time > ctx.config.time * execute.time) { + status = STATUS.STATUS_TIME_LIMIT_EXCEEDED; + } else if (memory > ctx.config.memory * 1024) { + status = STATUS.STATUS_MEMORY_LIMIT_EXCEEDED; + } else { + ({ status, message } = await checkers[ctx.config.checker_type]({ + execute: checker.execute, + copyIn: checker.copyIn || {}, + input: { src: input }, + output: null, + user_stdout: stdout, + user_stderr: stderr, + score: 100, + detail: ctx.config.detail ?? true, + env: { ...ctx.env, HYDRO_TESTCASE: '0' }, + })); + } + } else if (status === STATUS.STATUS_RUNTIME_ERROR && code) { + if (code < 32) message = signals[code]; + else message = { message: 'Your program returned {0}.', params: [code] }; + } + await Promise.all( + Object.values(res.fileIds).map((id) => del(id)), + ).catch(() => { /* Ignore file doesn't exist */ }); + + if (message) ctx.next({ message }); + + return ctx.end({ + status: status === STATUS.STATUS_ACCEPTED ? STATUS.STATUS_HACK_UNSUCCESSFUL : STATUS.STATUS_HACK_SUCCESSFUL, + score: 0, + time, + memory, + }); +} diff --git a/packages/hydrojudge/src/judge/index.ts b/packages/hydrojudge/src/judge/index.ts index ee431ccc..b188af12 100644 --- a/packages/hydrojudge/src/judge/index.ts +++ b/packages/hydrojudge/src/judge/index.ts @@ -1,4 +1,5 @@ import * as def from './default'; +import * as hack from './hack'; import * as interactive from './interactive'; import { Context } from './interface'; import * as objective from './objective'; @@ -6,5 +7,5 @@ import * as run from './run'; import * as submit_answer from './submit_answer'; export = { - default: def, interactive, run, submit_answer, objective, + default: def, interactive, run, submit_answer, objective, hack, } as Record }>; diff --git a/packages/utils/lib/status.ts b/packages/utils/lib/status.ts index 5769705a..eac1031c 100644 --- a/packages/utils/lib/status.ts +++ b/packages/utils/lib/status.ts @@ -1,24 +1,26 @@ -export const STATUS = { - STATUS_WAITING: 0, - STATUS_ACCEPTED: 1, - STATUS_WRONG_ANSWER: 2, - STATUS_TIME_LIMIT_EXCEEDED: 3, - STATUS_MEMORY_LIMIT_EXCEEDED: 4, - STATUS_OUTPUT_LIMIT_EXCEEDED: 5, - STATUS_RUNTIME_ERROR: 6, - STATUS_COMPILE_ERROR: 7, - STATUS_SYSTEM_ERROR: 8, - STATUS_CANCELED: 9, - STATUS_ETC: 10, - STATUS_HACKED: 11, - STATUS_JUDGING: 20, - STATUS_COMPILING: 21, - STATUS_FETCHED: 22, - STATUS_IGNORED: 30, - STATUS_FORMAT_ERROR: 31, -}; +export enum STATUS { + STATUS_WAITING = 0, + STATUS_ACCEPTED = 1, + STATUS_WRONG_ANSWER = 2, + STATUS_TIME_LIMIT_EXCEEDED = 3, + STATUS_MEMORY_LIMIT_EXCEEDED = 4, + STATUS_OUTPUT_LIMIT_EXCEEDED = 5, + STATUS_RUNTIME_ERROR = 6, + STATUS_COMPILE_ERROR = 7, + STATUS_SYSTEM_ERROR = 8, + STATUS_CANCELED = 9, + STATUS_ETC = 10, + STATUS_HACKED = 11, + STATUS_JUDGING = 20, + STATUS_COMPILING = 21, + STATUS_FETCHED = 22, + STATUS_IGNORED = 30, + STATUS_FORMAT_ERROR = 31, + STATUS_HACK_SUCCESSFUL = 32, + STATUS_HACK_UNSUCCESSFUL = 33, +} -export const STATUS_TEXTS = { +export const STATUS_TEXTS: Record = { [STATUS.STATUS_WAITING]: 'Waiting', [STATUS.STATUS_ACCEPTED]: 'Accepted', [STATUS.STATUS_WRONG_ANSWER]: 'Wrong Answer', @@ -36,9 +38,11 @@ export const STATUS_TEXTS = { [STATUS.STATUS_FETCHED]: 'Fetched', [STATUS.STATUS_IGNORED]: 'Ignored', [STATUS.STATUS_FORMAT_ERROR]: 'Format Error', + [STATUS.STATUS_HACK_SUCCESSFUL]: 'Hack Successful', + [STATUS.STATUS_HACK_UNSUCCESSFUL]: 'Hack Unsuccessful', }; -export const STATUS_SHORT_TEXTS = { +export const STATUS_SHORT_TEXTS: Partial> = { [STATUS.STATUS_ACCEPTED]: 'AC', [STATUS.STATUS_WRONG_ANSWER]: 'WA', [STATUS.STATUS_TIME_LIMIT_EXCEEDED]: 'TLE', @@ -53,24 +57,26 @@ export const STATUS_SHORT_TEXTS = { [STATUS.STATUS_FORMAT_ERROR]: 'FE', }; -export const STATUS_CODES = { - 0: 'pending', - 1: 'pass', - 2: 'fail', - 3: 'fail', - 4: 'fail', - 5: 'fail', - 6: 'fail', - 7: 'fail', - 8: 'fail', - 9: 'ignored', - 10: 'fail', - 11: 'fail', - 20: 'progress', - 21: 'progress', - 22: 'progress', - 30: 'ignored', - 31: 'ignored', +export const STATUS_CODES: Record = { + [STATUS.STATUS_WAITING]: 'pending', + [STATUS.STATUS_ACCEPTED]: 'pass', + [STATUS.STATUS_WRONG_ANSWER]: 'fail', + [STATUS.STATUS_TIME_LIMIT_EXCEEDED]: 'fail', + [STATUS.STATUS_MEMORY_LIMIT_EXCEEDED]: 'fail', + [STATUS.STATUS_OUTPUT_LIMIT_EXCEEDED]: 'fail', + [STATUS.STATUS_RUNTIME_ERROR]: 'fail', + [STATUS.STATUS_COMPILE_ERROR]: 'fail', + [STATUS.STATUS_SYSTEM_ERROR]: 'fail', + [STATUS.STATUS_CANCELED]: 'ignored', + [STATUS.STATUS_ETC]: 'fail', + [STATUS.STATUS_HACKED]: 'fail', + [STATUS.STATUS_JUDGING]: 'progress', + [STATUS.STATUS_COMPILING]: 'progress', + [STATUS.STATUS_FETCHED]: 'progress', + [STATUS.STATUS_IGNORED]: 'ignored', + [STATUS.STATUS_FORMAT_ERROR]: 'ignored', + [STATUS.STATUS_HACK_SUCCESSFUL]: 'pass', + [STATUS.STATUS_HACK_UNSUCCESSFUL]: 'fail', }; export function getScoreColor(score: number | string): string {