From 4e0c9bb00a20c49b9a491257819f9b8d3e19bf72 Mon Sep 17 00:00:00 2001 From: undefined Date: Sun, 4 Sep 2022 14:29:44 +0800 Subject: [PATCH] core&judge&ui: hack submission --- packages/hydrojudge/src/cases.ts | 12 ++- packages/hydrojudge/src/flow.ts | 8 +- packages/hydrojudge/src/hosts/builtin.ts | 1 + packages/hydrojudge/src/hosts/hydro.ts | 1 + packages/hydrojudge/src/interface.ts | 11 +++ packages/hydrojudge/src/judge/hack.ts | 17 ++-- packages/hydrojudge/src/task.ts | 63 +++++--------- packages/hydrooj/src/handler/manage.ts | 2 +- packages/hydrooj/src/handler/problem.ts | 76 +++++++++++++++-- packages/hydrooj/src/handler/record.ts | 83 ++++++++++--------- packages/hydrooj/src/interface.ts | 12 ++- packages/hydrooj/src/lib/testdataConfig.ts | 1 + packages/hydrooj/src/model/record.ts | 29 ++++--- packages/hydrooj/src/upgrade.ts | 11 +++ .../ui-default/templates/problem_hack.html | 33 ++++++++ .../ui-default/templates/record_detail.html | 21 +++-- packages/vjudge/src/model.ts | 3 - 17 files changed, 256 insertions(+), 128 deletions(-) create mode 100644 packages/ui-default/templates/problem_hack.html diff --git a/packages/hydrojudge/src/cases.ts b/packages/hydrojudge/src/cases.ts index d28917de0..420636784 100644 --- a/packages/hydrojudge/src/cases.ts +++ b/packages/hydrojudge/src/cases.ts @@ -6,8 +6,10 @@ import { max, sum } from 'lodash'; import readYamlCases, { convertIniConfig } from '@hydrooj/utils/lib/cases'; import { normalizeSubtasks, readSubtasksFromFiles } from '@hydrooj/utils/lib/common'; import { changeErrorType } from '@hydrooj/utils/lib/utils'; +import { ProblemConfigFile } from 'hydrooj'; import { getConfig } from './config'; import { FormatError, SystemError } from './error'; +import { NextFunction, ParsedConfig } from './interface'; import { ensureFile, parseMemoryMB } from './utils'; function isValidConfig(config) { @@ -62,7 +64,13 @@ export async function processTestdata(folder: string) { })); } -export default async function readCases(folder: string, cfg: Record = {}, args) { +interface Args { + next: NextFunction; + key: string; + isSelfSubmission: boolean; +} + +export default async function readCases(folder: string, cfg: ProblemConfigFile = {}, args: Args): Promise { const iniConfig = path.resolve(folder, 'config.ini'); const yamlConfig = path.resolve(folder, 'config.yaml'); const ymlConfig = path.resolve(folder, 'config.yml'); @@ -93,7 +101,7 @@ export default async function readCases(folder: string, cfg: Record try { result.subtasks = readSubtasksFromFiles(await collectFiles(folder), cfg); result.count = Math.sum(result.subtasks.map((i) => i.cases.length)); - if (cfg.isSelfSubmission) args.next?.({ message: { message: 'Found {0} testcases.', params: [result.count] } }); + if (args.isSelfSubmission) args.next?.({ message: { message: 'Found {0} testcases.', params: [result.count] } }); } catch (e) { throw new SystemError('Cannot parse testdata.', [e.message, ...(e.params || [])]); } diff --git a/packages/hydrojudge/src/flow.ts b/packages/hydrojudge/src/flow.ts index 6cbc2ab46..b2083769e 100644 --- a/packages/hydrojudge/src/flow.ts +++ b/packages/hydrojudge/src/flow.ts @@ -1,7 +1,7 @@ import Queue from 'p-queue'; import { STATUS } from '@hydrooj/utils/lib/status'; import { getConfig } from './config'; -import { CompileError, FormatError } from './error'; +import { FormatError } from './error'; import { Context, ContextSubTask } from './judge/interface'; import { NormalizedCase, NormalizedSubtask } from './utils'; @@ -60,12 +60,6 @@ function judgeSubtask(subtask: NormalizedSubtask, sid: string, judgeCase: Task[' export const runFlow = async (ctx: Context, task: Task) => { if (!ctx.config.subtasks.length) throw new FormatError('Problem data not found.'); ctx.next({ status: STATUS.STATUS_COMPILING }); - if (ctx.config.template && 'content' in ctx.code) { - if (ctx.config.template[ctx.lang]) { - const tpl = ctx.config.template[ctx.lang]; - ctx.code.content = tpl[0] + ctx.code.content + tpl[1]; - } else throw new CompileError('Language not supported by provided templates'); - } await task.compile(); ctx.next({ status: STATUS.STATUS_JUDGING, progress: 0 }); const tasks = []; diff --git a/packages/hydrojudge/src/hosts/builtin.ts b/packages/hydrojudge/src/hosts/builtin.ts index 4c8025f8e..f9b864113 100644 --- a/packages/hydrojudge/src/hosts/builtin.ts +++ b/packages/hydrojudge/src/hosts/builtin.ts @@ -20,6 +20,7 @@ const logger = new Logger('judge'); const session = { config: { detail: system.get('hydrojudge.detail') }, async fetchFile(name: string) { + name = name.split('#')[0]; const target = path.join('/tmp/hydro/judge', name.replace(/\//g, '_')); await storage.get(`submission/${name}`, target); return target; diff --git a/packages/hydrojudge/src/hosts/hydro.ts b/packages/hydrojudge/src/hosts/hydro.ts index cabc01ac0..655d4fcb0 100644 --- a/packages/hydrojudge/src/hosts/hydro.ts +++ b/packages/hydrojudge/src/hosts/hydro.ts @@ -110,6 +110,7 @@ export default class Hydro { } async fetchFile(name: string) { + name = name.split('#')[0]; const res = await this.post('judge/code', { id: name }); const target = path.join('/tmp/hydro/judge', name.replace(/\//g, '_')); const w = fs.createWriteStream(target); diff --git a/packages/hydrojudge/src/interface.ts b/packages/hydrojudge/src/interface.ts index 31ee95dee..11cc4eb2a 100644 --- a/packages/hydrojudge/src/interface.ts +++ b/packages/hydrojudge/src/interface.ts @@ -1,3 +1,5 @@ +import { NormalizedSubtask } from '@hydrooj/utils/lib/utils'; +import { JudgeResultBody, ProblemConfigFile } from 'hydrooj'; import { CopyInFile } from './sandbox'; export interface Execute { @@ -6,3 +8,12 @@ export interface Execute { copyIn: Record; time?: number; } + +export type NextFunction = (body: Partial) => Promise | void; + +export interface ParsedConfig extends Omit { + count: number; + time: number; + memory: number; + subtasks: NormalizedSubtask[]; +} diff --git a/packages/hydrojudge/src/judge/hack.ts b/packages/hydrojudge/src/judge/hack.ts index c5c2648b0..d09ee69c3 100644 --- a/packages/hydrojudge/src/judge/hack.ts +++ b/packages/hydrojudge/src/judge/hack.ts @@ -23,23 +23,24 @@ export async function judge(ctx: Context) { 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.session.fetchFile(ctx.files.hack), ]); ctx.next({ status: STATUS.STATUS_JUDGING, progress: 0 }); const validateResult = await run( validator.execute, { stdin: { src: input }, - copyIn: judgeExtraFiles, + copyIn: { ...validator.copyIn, ...judgeExtraFiles }, time: parseTimeMS(ctx.config.time || '1s') * execute.time, memory: parseMemoryMB(ctx.config.memory || '256m'), }, ); + console.log(validateResult); if (validateResult.status !== STATUS.STATUS_ACCEPTED) { - const message = `${validateResult.files.stdout || ''}\n${validateResult.files.stderr || ''}`.trim(); + const message = `${validateResult.stdout || ''}\n${validateResult.stderr || ''}`.trim(); return ctx.end({ status: STATUS.STATUS_FORMAT_ERROR, message }); } - const copyIn = { ...ctx.execute.copyIn }; + const copyIn = { ...execute.copyIn }; const { filename } = ctx.config; if (filename) copyIn[`${filename}.in`] = { src: input }; const res = await run( @@ -47,7 +48,7 @@ export async function judge(ctx: Context) { { stdin: filename ? null : { src: input }, copyIn, - copyOutCached: [filename ? `${filename}.out?` : 'stdout'], + copyOutCached: [filename ? `${filename}.out?` : 'stdout', 'stderr'], time: parseTimeMS(ctx.config.time || '1s') * execute.time, memory: parseMemoryMB(ctx.config.memory || '256m'), }, @@ -55,7 +56,7 @@ export async function judge(ctx: Context) { const { code, time, memory } = res; let { status } = res; let stdout: CmdFile = { fileId: res.fileIds[filename ? `${filename}.out` : 'stdout'] }; - const stderr = { fileId: res.fileIds['stderr'] }; + console.log(res); if (!stdout.fileId) stdout = { content: '' }; let message: any = ''; if (status === STATUS.STATUS_ACCEPTED) { @@ -68,9 +69,9 @@ export async function judge(ctx: Context) { execute: checker.execute, copyIn: checker.copyIn || {}, input: { src: input }, - output: null, + output: { content: '' }, user_stdout: stdout, - user_stderr: stderr, + user_stderr: { fileId: res.fileIds['stderr'] }, score: 100, detail: ctx.config.detail ?? true, env: { ...ctx.env, HYDRO_TESTCASE: '0' }, diff --git a/packages/hydrojudge/src/task.ts b/packages/hydrojudge/src/task.ts index 166d38022..c584537d9 100644 --- a/packages/hydrojudge/src/task.ts +++ b/packages/hydrojudge/src/task.ts @@ -4,18 +4,18 @@ import { noop } from 'lodash'; import { LangConfig } from '@hydrooj/utils/lib/lang'; import { STATUS } from '@hydrooj/utils/lib/status'; import type { - FileInfo, JudgeRequest, JudgeResultBody, ProblemConfigFile, + FileInfo, JudgeMeta, JudgeRequest, JudgeResultBody, } from 'hydrooj'; import { Logger } from 'hydrooj/src/logger'; import readCases from './cases'; import { getConfig } from './config'; -import { CompileError, FormatError, SystemError } from './error'; +import { CompileError, FormatError } from './error'; +import { NextFunction, ParsedConfig } from './interface'; import judge from './judge'; import { CopyInFile } from './sandbox'; import * as tmpfs from './tmpfs'; import { compilerText, Lock, md5 } from './utils'; -type NextFunction = (data: Partial) => void; interface Session { getLang: (name: string) => LangConfig; getNext: (task: JudgeTask) => NextFunction; @@ -38,7 +38,9 @@ export class JudgeTask { clean: (() => Promise)[]; data: FileInfo[]; folder: string; - config: ProblemConfigFile & { problemOwner: number }; + config: ParsedConfig; + meta: JudgeMeta; + files?:Record; next: (data: Partial) => void; end: (data: Partial) => void; env: Record; @@ -58,7 +60,8 @@ export class JudgeTask { this.code = { content: this.request.code }; this.data = this.request.data; this.source = this.request.source; - this.config = this.request.config; + this.meta = this.request.meta; + this.files = this.request.files; this.input = this.request.input; let tid = this.request.contest?.toString() || ''; if (tid === '000000000000000000000000') tid = ''; @@ -77,9 +80,7 @@ export class JudgeTask { fs.ensureDirSync(this.tmpdir); tmpfs.mount(this.tmpdir, getConfig('tmpfs_size')); logger.info('Submission: %s/%s/%s', host, this.source, this.rid); - this.stat.cache_start = new Date(); - if (tid === '000000000000000000000001') await this.doHack(); - else await this.doSubmission(); + await this.doSubmission(); } catch (e) { if (e instanceof CompileError) { this.next({ compilerText: compilerText(e.stdout, e.stderr) }); @@ -109,11 +110,10 @@ export class JudgeTask { } async doSubmission() { + this.stat.cache_start = new Date(); this.folder = await this.session.cacheOpen(this.source, this.data, this.next); - if ((this.code as any).content.startsWith('@@hydro_submission_file@@')) { - const id = (this.code as any).content.split('@@hydro_submission_file@@')[1]?.split('#')?.[0]; - if (!id) throw new SystemError('Submission File Not Found'); - const target = await this.session.fetchFile(id); + if (this.files?.code) { + const target = await this.session.fetchFile(this.files?.code); this.code = { src: target }; this.clean.push(() => fs.remove(target)); } @@ -122,39 +122,20 @@ export class JudgeTask { this.folder, { detail: this.session.config.detail, - isSelfSubmission: this.config.problemOwner === this.request.uid, - ...this.config, + ...this.request.config, + }, + { + next: this.next, + isSelfSubmission: this.meta.problemOwner === this.request.uid, + key: md5(`${this.source}/${getConfig('secret')}`), }, - { next: this.next, key: md5(`${this.source}/${getConfig('secret')}`) }, ); this.stat.judge = new Date(); - const type = typeof this.input === 'string' ? 'run' : this.config.type || 'default'; + const type = this.request.contest?.toString() === '000000000000000000000000' ? 'run' + : this.files?.hack + ? 'hack' + : this.config.type || 'default'; if (!judge[type]) throw new FormatError('Unrecognized problemType: {0}', [type]); await judge[type].judge(this); } - - async doHack() { - this.folder = await this.session.cacheOpen(this.source, this.data, this.next); - if ((this.code as any).content.startsWith('@@hydro_submission_file@@')) { - const id = (this.code as any).content.split('@@hydro_submission_file@@')[1]?.split('#')?.[0]; - if (!id) throw new SystemError('Submission File Not Found'); - const target = await this.session.fetchFile(id); - this.code = { src: target }; - this.clean.push(() => fs.remove(target)); - } - this.stat.read_cases = new Date(); - this.config = await readCases( - this.folder, - { - detail: this.session.config.detail, - isSelfSubmission: this.config.problemOwner === this.request.uid, - ...this.config, - }, - { next: this.next, key: md5(`${this.source}/${getConfig('secret')}`) }, - ); - this.stat.judge = new Date(); - const type = typeof this.input === 'string' ? 'run' : this.config.type || 'default'; - if (type !== 'default') throw new FormatError('Cannot hack: only type=default allowed'); - await judge.hack.judge(this); - } } diff --git a/packages/hydrooj/src/handler/manage.ts b/packages/hydrooj/src/handler/manage.ts index b41957fe9..148445cc5 100644 --- a/packages/hydrooj/src/handler/manage.ts +++ b/packages/hydrooj/src/handler/manage.ts @@ -110,7 +110,7 @@ class SystemScriptHandler extends SystemHandler { logger.warn('You are using the legacy script validation API, which will be dropped in the future.'); validate(global.Hydro.script[id].validate, args); } - const rid = await record.add(domainId, -1, this.user._id, '-', id, false, raw); + const rid = await record.add(domainId, -1, this.user._id, '-', id, false, { input: raw, type: 'pretest' }); const report = (data) => judge.next({ domainId, rid, ...data }); report({ message: `Running script: ${id} `, status: STATUS.STATUS_JUDGING }); const start = Date.now(); diff --git a/packages/hydrooj/src/handler/problem.ts b/packages/hydrooj/src/handler/problem.ts index 6c4210d47..9791b773d 100644 --- a/packages/hydrooj/src/handler/problem.ts +++ b/packages/hydrooj/src/handler/problem.ts @@ -11,11 +11,11 @@ import { BadRequestError, ContestNotAttendedError, ContestNotEndedError, ContestNotFoundError, ContestNotLiveError, ForbiddenError, NoProblemError, NotFoundError, - PermissionError, ProblemNotFoundError, SolutionNotFoundError, + PermissionError, ProblemNotFoundError, RecordNotFoundError, SolutionNotFoundError, ValidationError, } from '../error'; import { - ProblemDoc, ProblemSearchOptions, ProblemStatusDoc, User, + ProblemDoc, ProblemSearchOptions, ProblemStatusDoc, RecordDoc, User, } from '../interface'; import paginate from '../lib/paginate'; import { isPid, parsePid as convertPid } from '../lib/validator'; @@ -498,9 +498,12 @@ export class ProblemSubmitHandler extends ProblemDetailHandler { throw new BadRequestError('unable to run pretest'); } } + await this.limitRate('add_record', 60, system.get('limit.submission_user'), true); + await this.limitRate('add_record', 60, system.get('limit.submission')); + const files: Record = {}; if (!code) { const file = this.request.files?.file; - if (!file) throw new ValidationError('code'); + if (!file || file.size === 0) throw new ValidationError('code'); const config = this.response.body.pdoc.config; if (typeof config !== 'object' || config === null) throw new BadRequestError('Incorrect problem config'); const sizeLimit = config.type === 'submit_answer' ? 128 * 1024 * 1024 : 65535; @@ -508,15 +511,16 @@ export class ProblemSubmitHandler extends ProblemDetailHandler { if (file.size > 65535 || await isBinaryFile(file.filepath, file.size)) { const id = nanoid(); await storage.put(`submission/${this.user._id}/${id}`, file.filepath, this.user._id); - code = `@@hydro_submission_file@@${this.user._id}/${id}#${file.originalFilename}`; + files.code = `${this.user._id}/${id}#${file.originalFilename}`; } else { // TODO auto detect & convert encoding code = await readFile(file.filepath, 'utf-8'); } - } else if (code.startsWith('@@hydro_submission_file@@')) throw new ValidationError('code'); - await this.limitRate('add_record', 60, system.get('limit.submission_user'), true); - await this.limitRate('add_record', 60, system.get('limit.submission')); - const rid = await record.add(domainId, this.pdoc.docId, this.user._id, lang, code, true, pretest ? input : tid, tid && !pretest); + } + const rid = await record.add( + domainId, this.pdoc.docId, this.user._id, lang, code, true, + pretest ? { input, type: 'pretest' } : { contest: tid, files, type: tid ? 'contest' : 'judge' }, + ); const rdoc = await record.get(domainId, rid); if (!pretest) { await Promise.all([ @@ -537,6 +541,61 @@ export class ProblemSubmitHandler extends ProblemDetailHandler { } } +export class ProblemHackHandler extends ProblemDetailHandler { + rdoc: RecordDoc; + + @param('rid', Types.ObjectID) + @param('tid', Types.ObjectID, true) + async prepare(domainId: string, rid: ObjectID, tid?: ObjectID) { + this.rdoc = await record.get(domainId, rid); + if (!this.rdoc || this.rdoc.pid !== this.pdoc.docId + || this.rdoc.contest?.toString() !== tid?.toString()) throw new RecordNotFoundError(domainId, rid); + if (tid) { + if (this.tdoc.rule !== 'codeforces') throw new ForbiddenError('This contest is not hackable.'); + if (!contest.isOngoing(this.tdoc, this.tsdoc)) throw new ContestNotLiveError(this.tdoc.docId); + } + if (this.rdoc.uid === this.user._id) throw new BadRequestError('You cannot hack your own submission'); + if (this.psdoc.status !== STATUS.STATUS_ACCEPTED) throw new ForbiddenError('You must accept this problem before hacking.'); + if (this.rdoc.status !== STATUS.STATUS_ACCEPTED) throw new ForbiddenError('You cannot hack a unsuccessful submission.'); + } + + async get() { + this.response.template = 'problem_hack.html'; + this.response.body = { + pdoc: this.pdoc, + udoc: this.udoc, + title: this.pdoc.title, + page_name: this.tdoc ? 'contest_detail_problem_hack' : 'problem_hack', + }; + } + + @param('input', Types.String, true) + @param('tid', Types.ObjectID, true) + async post(domainId: string, input = '', tid?: ObjectID) { + await this.limitRate('add_record', 60, system.get('limit.submission_user'), true); + await this.limitRate('add_record', 60, system.get('limit.submission')); + const id = `${this.user._id}/${nanoid()}`; + console.log(input); + if (this.request.files?.file?.size > 0) { + const file = this.request.files.file; + if (!file || file.size > 128 * 1024 * 1024) throw new ValidationError('input'); + await storage.put(`submission/${id}`, file.filepath, this.user._id); + } else if (input) { + await storage.put(`submission/${id}`, Buffer.from(input), this.user._id); + } + const rid = await record.add( + domainId, this.pdoc.docId, this.user._id, + this.rdoc.lang, this.rdoc.code, true, + { contest: tid, type: 'hack', files: { hack: `${id}#input.txt` } }, + ); + const rdoc = await record.get(domainId, rid); + // TODO contest: update status; + bus.broadcast('record/change', rdoc); + this.response.body = { rid }; + this.response.redirect = this.url('record_detail', { rid }); + } +} + export class ProblemManageHandler extends ProblemDetailHandler { async prepare() { if (!this.user.own(this.pdoc, PERM.PERM_EDIT_PROBLEM_SELF)) this.checkPerm(PERM.PERM_EDIT_PROBLEM); @@ -935,6 +994,7 @@ export async function apply() { Route('problem_random', '/problem/random', ProblemRandomHandler, PERM.PERM_VIEW_PROBLEM); Route('problem_detail', '/p/:pid', ProblemDetailHandler, PERM.PERM_VIEW_PROBLEM); Route('problem_submit', '/p/:pid/submit', ProblemSubmitHandler, PERM.PERM_SUBMIT_PROBLEM); + Route('problem_hack', '/p/:pid/hack/:rid', ProblemHackHandler, PERM.PERM_SUBMIT_PROBLEM); Route('problem_edit', '/p/:pid/edit', ProblemEditHandler); Route('problem_config', '/p/:pid/config', ProblemConfigHandler); Route('problem_files', '/p/:pid/files', ProblemFilesHandler, PERM.PERM_VIEW_PROBLEM); diff --git a/packages/hydrooj/src/handler/record.ts b/packages/hydrooj/src/handler/record.ts index 6f2d7eafc..6f1cd07e5 100644 --- a/packages/hydrooj/src/handler/record.ts +++ b/packages/hydrooj/src/handler/record.ts @@ -107,13 +107,34 @@ class RecordListHandler extends Handler { } class RecordDetailHandler extends Handler { + rdoc: RecordDoc; + @param('rid', Types.ObjectID) + async prepare(domainId: string, rid: ObjectID) { + this.rdoc = await record.get(domainId, rid); + if (!this.rdoc) throw new RecordNotFoundError(rid); + } + + async download() { + for (const file of ['code', 'hack']) { + if (!this.rdoc.files?.[file]) continue; + const [id, filename] = this.rdoc.files?.[file]?.split('#') || []; + // eslint-disable-next-line no-await-in-loop + this.response.redirect = await storage.signDownloadLink(`submission/${id}`, filename || file, true, 'user'); + return; + } + const lang = langs[this.rdoc.lang]?.pretest || this.rdoc.lang; + this.response.body = this.rdoc.code; + this.response.type = 'text/plain'; + this.response.disposition = `attachment; filename="${langs[lang].code_file || `foo.${this.rdoc.lang}`}"`; + } + @param('rid', Types.ObjectID) @param('download', Types.Boolean) + // eslint-disable-next-line consistent-return async get(domainId: string, rid: ObjectID, download = false) { - const rdoc = await record.get(domainId, rid); - if (!rdoc) throw new RecordNotFoundError(rid); + const rdoc = this.rdoc; let tdoc; - if (rdoc.contest?.toHexString() === '000000000000000000000000') { + if (rdoc.contest?.toString() === '000000000000000000000000') { if (rdoc.uid !== this.user._id) throw new PermissionError(PERM.PERM_READ_RECORD_CODE); } else if (rdoc.contest) { tdoc = await contest.get(domainId, rdoc.contest); @@ -136,21 +157,9 @@ class RecordDetailHandler extends Handler { canViewCode ||= this.user.hasPerm(PERM.PERM_READ_RECORD_CODE_ACCEPT) && self?.status === STATUS.STATUS_ACCEPTED; if (!canViewCode) { rdoc.code = ''; + rdoc.files = {}; rdoc.compilerTexts = []; - } - - if (download && rdoc.code.startsWith('@@hydro_submission_file@@')) { - const [id, filename] = rdoc.code.split('@@hydro_submission_file@@')[1].split('#'); - this.response.redirect = await storage.signDownloadLink(`submission/${id}`, filename || 'code', true, 'judge'); - return; - } - if (download) { - const lang = langs[rdoc.lang]?.pretest || rdoc.lang; - this.response.body = rdoc.code; - this.response.type = 'text/plain'; - this.response.disposition = `attachment; filename="${langs[lang].code_file || `foo.${rdoc.lang}`}"`; - return; - } + } else if (download) return await this.download(); if (pdoc && !(rdoc.contest && this.user._id === rdoc.uid)) { if (!problem.canViewBy(pdoc, this.user)) throw new PermissionError(PERM.PERM_VIEW_PROBLEM_HIDDEN); } @@ -165,34 +174,28 @@ class RecordDetailHandler extends Handler { async postRejudge(domainId: string, rid: ObjectID) { this.checkPerm(PERM.PERM_REJUDGE); const priority = await record.submissionPriority(this.user._id, -20); - const rdoc = await record.get(domainId, rid); - if (rdoc) { - const isContest = rdoc.contest && rdoc.contest.toHexString() !== '000000000000000000000000'; - await record.reset(domainId, rid, true); - await record.judge(domainId, rid, priority, isContest ? { detail: false } : {}); - } + const isContest = this.rdoc.contest && this.rdoc.contest.toString() !== '000000000000000000000000'; + await record.reset(domainId, rid, true); + await record.judge(domainId, rid, priority, isContest ? { detail: false } : {}); this.back(); } @param('rid', Types.ObjectID) async postCancel(domainId: string, rid: ObjectID) { - const rdoc = await record.get(domainId, rid); - if (rdoc) { - const $set = { - status: STATUS.STATUS_CANCELED, - score: 0, - time: 0, - memory: 0, - testCases: [{ - id: 0, subtaskId: 0, status: 9, score: 0, time: 0, memory: 0, message: 'score canceled', - }], - }; - const [latest] = await Promise.all([ - record.update(domainId, rid, $set), - TaskModel.deleteMany({ rid: rdoc._id }), - ]); - if (latest) await postJudge(latest); - } + const $set = { + status: STATUS.STATUS_CANCELED, + score: 0, + time: 0, + memory: 0, + testCases: [{ + id: 0, subtaskId: 0, status: 9, score: 0, time: 0, memory: 0, message: 'score canceled', + }], + }; + const [latest] = await Promise.all([ + record.update(domainId, rid, $set), + TaskModel.deleteMany({ rid: this.rdoc._id }), + ]); + if (latest) await postJudge(latest); this.back(); } } diff --git a/packages/hydrooj/src/interface.ts b/packages/hydrooj/src/interface.ts index 6caffea17..e2d821fdf 100644 --- a/packages/hydrooj/src/interface.ts +++ b/packages/hydrooj/src/interface.ts @@ -181,6 +181,7 @@ export interface ProblemConfig { type: string; subType?: string; target?: string; + hackable?:boolean; } export interface PlainContentNode { @@ -293,17 +294,24 @@ export interface RecordDoc { judgeAt: Date; status: number; progress?: number; - /** pretest */ + /** pretest & hack */ input?: string; /** 0 if pretest&script */ contest?: ObjectID; + + files?: Record +} + +export interface JudgeMeta { + problemOwner: number; } export interface JudgeRequest extends Omit { priority: number; type: 'judge'; rid: ObjectID; - config: ProblemConfigFile & { problemOwner: number }; + config: ProblemConfigFile; + meta: JudgeMeta; data: FileInfo[]; source: string; } diff --git a/packages/hydrooj/src/lib/testdataConfig.ts b/packages/hydrooj/src/lib/testdataConfig.ts index c6a1fde45..1c46cc038 100644 --- a/packages/hydrooj/src/lib/testdataConfig.ts +++ b/packages/hydrooj/src/lib/testdataConfig.ts @@ -14,6 +14,7 @@ export async function parseConfig(config: string | ProblemConfigFile = {}) { timeMin: Number.MAX_SAFE_INTEGER, timeMax: 0, type: cfg.type || 'default', + hackable: cfg.validator && cfg.checker && !['default', 'strict'].includes(cfg.checker_type), }; if (cfg.subType) result.subType = cfg.subType; if (cfg.target) result.target = cfg.target; diff --git a/packages/hydrooj/src/model/record.ts b/packages/hydrooj/src/model/record.ts index 79c2a0da9..0be067aaa 100644 --- a/packages/hydrooj/src/model/record.ts +++ b/packages/hydrooj/src/model/record.ts @@ -8,7 +8,7 @@ import { } from 'mongodb'; import { ProblemNotFoundError } from '../error'; import { - FileInfo, ProblemConfigFile, RecordDoc, + FileInfo, JudgeMeta, JudgeRequest, ProblemConfigFile, RecordDoc, } from '../interface'; import * as bus from '../service/bus'; import db from '../service/db'; @@ -67,6 +67,7 @@ class RecordModel { if (!rdoc) return null; let data: FileInfo[] = []; let source = `${domainId}/${rdoc.pid}`; + const meta: JudgeMeta = { problemOwner: 1 }; if (rdoc.pid) { let pdoc = await problem.get(rdoc.domainId, rdoc.pid); if (!pdoc) throw new ProblemNotFoundError(rdoc.domainId, rdoc.pid); @@ -74,7 +75,7 @@ class RecordModel { pdoc = await problem.get(pdoc.reference.domainId, pdoc.reference.pid); if (!pdoc) throw new ProblemNotFoundError(rdoc.domainId, rdoc.pid); } - (config as any).problemOwner = pdoc.owner; + meta.problemOwner = pdoc.owner; source = `${pdoc.domainId}/${pdoc.docId}`; data = pdoc.data; if (typeof pdoc.config === 'string') throw new Error(pdoc.config); @@ -103,12 +104,19 @@ class RecordModel { config, data, source, - }); + meta, + } as unknown as JudgeRequest); } static async add( domainId: string, pid: number, uid: number, - lang: string, code: string, addTask: boolean, tidOrInput?: ObjectID | string, isContest = false, + lang: string, code: string, addTask: boolean, + args: { + contest?: ObjectID, + input?: string, + files?: Record, + type: 'judge' | 'contest' | 'pretest' | 'hack', + } = { type: 'judge' }, ) { const data: RecordDoc = { status: STATUS.STATUS_WAITING, @@ -128,15 +136,16 @@ class RecordModel { judgeAt: null, rejudged: false, }; - if (typeof tidOrInput === 'string') { - // is Run - data.input = tidOrInput; + if (args.contest) data.contest = args.contest; + if (args.files) data.files = args.files; + if (args.type === 'pretest') { + data.input = args.input || ''; data.contest = new ObjectID('000000000000000000000000'); - } else data.contest = tidOrInput; + } const res = await RecordModel.coll.insertOne(data); if (addTask) { - const priority = await RecordModel.submissionPriority(uid, typeof tidOrInput === 'string' ? -20 : (isContest ? 50 : 0)); - await RecordModel.judge(domainId, res.insertedId, priority, isContest ? { detail: false } : {}); + const priority = await RecordModel.submissionPriority(uid, args.type === 'pretest' ? -20 : (args.type === 'contest' ? 50 : 0)); + await RecordModel.judge(domainId, res.insertedId, priority, args.type === 'contest' ? { detail: false } : {}); } return res.insertedId; } diff --git a/packages/hydrooj/src/upgrade.ts b/packages/hydrooj/src/upgrade.ts index 47370f211..8f19538b5 100644 --- a/packages/hydrooj/src/upgrade.ts +++ b/packages/hydrooj/src/upgrade.ts @@ -830,6 +830,17 @@ const scripts: UpgradeScript[] = [ } return true; }, + async function _67_78() { + const rdocs = RecordModel.coll.find({ code: /^@@hydro_submission_file@@/ }); + let rdoc; + while (rdoc = await rdocs.next()) { // eslint-disable-line + await RecordModel.update(rdoc.domainId, rdoc._id, { + files: { code: rdoc.code.split('@@hydro_submission_file@@')[1] }, + code: '', + }); + } + return true; + }, ]; export default scripts; diff --git a/packages/ui-default/templates/problem_hack.html b/packages/ui-default/templates/problem_hack.html new file mode 100644 index 000000000..597007f1d --- /dev/null +++ b/packages/ui-default/templates/problem_hack.html @@ -0,0 +1,33 @@ +{% extends "layout/basic.html" %} +{% block content %} +{{ set(UiContext, 'pdoc', pdoc) }} +
+
+
+
+

{{ _('Hack a submission') }}

+
+
+
+ {{ form.form_textarea({label:'Input', columns:12, name:'input', extra_class:'monospace', autofocus:true}) }} +
+
+

{{ _('Or upload a file:') }}

+
+
+
+
+ + +
+
+
+
+
+
+ {% set owner_udoc = udoc %} + {% include "partials/problem_sidebar.html" %} +
+
+ +{% endblock %} diff --git a/packages/ui-default/templates/record_detail.html b/packages/ui-default/templates/record_detail.html index 62c7786f2..0038a5e0e 100644 --- a/packages/ui-default/templates/record_detail.html +++ b/packages/ui-default/templates/record_detail.html @@ -5,7 +5,7 @@
{% include 'record_detail_status.html' %} - {% if rdoc['code'] %} + {% if rdoc['code'] or rdoc.files.code %}

{{ _('Code') }}

@@ -13,11 +13,9 @@ {{ _('Download') }}
- {% if not rdoc.code.startsWith('@@hydro_submission_file@@') %} -
-
{{ rdoc['code'] }}
-
- {% endif %} +
+
{{ rdoc['code'] }}
+
{% endif %}
@@ -49,6 +47,17 @@ + {% endif %} + {% if handler.user.hasPerm(perm.PERM_SUBMIT_PROBLEM) and pdoc.config.hackable and rdoc.status === STATUS.STATUS_ACCEPTED %} + {% endif %}
diff --git a/packages/vjudge/src/model.ts b/packages/vjudge/src/model.ts index d37d35519..ade04afaf 100644 --- a/packages/vjudge/src/model.ts +++ b/packages/vjudge/src/model.ts @@ -1,6 +1,5 @@ /* eslint-disable no-await-in-loop */ import { sleep } from '@hydrooj/utils/lib/utils'; -import { writeFileSync } from 'fs-extra'; import * as Judge from 'hydrooj/src/handler/judge'; import { Logger } from 'hydrooj/src/logger'; import { STATUS } from 'hydrooj/src/model/builtin'; @@ -56,8 +55,6 @@ class Service { async sync(domainId: string, resync = false, list: string) { let page = 1; - const res = await this.api.getProblem('P1721D', {}); - writeFileSync('a.md', res.content); let pids = await this.api.listProblem(page, resync, list); while (pids.length) { logger.info(`${domainId}: Syncing page ${page}`);