From a3c729cebde5dad8c13c3ddca1cfa83ccd1ac1aa Mon Sep 17 00:00:00 2001 From: undefined Date: Sat, 9 Oct 2021 22:03:34 +0800 Subject: [PATCH] vjudge: add poj support --- packages/utils/lib/utils.ts | 13 +- packages/utils/package.json | 2 +- packages/vjudge/package.json | 2 +- packages/vjudge/src/providers/index.ts | 3 +- packages/vjudge/src/providers/poj.ts | 254 +++++++++++++++++++++++++ packages/vjudge/src/providers/uoj.ts | 17 +- packages/vjudge/src/verdict.ts | 19 ++ 7 files changed, 289 insertions(+), 21 deletions(-) create mode 100644 packages/vjudge/src/providers/poj.ts create mode 100644 packages/vjudge/src/verdict.ts diff --git a/packages/utils/lib/utils.ts b/packages/utils/lib/utils.ts index 9c23146c9..1fc1d6a7a 100644 --- a/packages/utils/lib/utils.ts +++ b/packages/utils/lib/utils.ts @@ -171,14 +171,14 @@ export function parseTimeMS(str: string | number) { if (typeof str === 'number') return str; const match = TIME_RE.exec(str); if (!match) throw new Error(`${str} error parsing time`); - return Math.floor(parseFloat(match[1]) * TIME_UNITS[match[2]]); + return Math.floor(parseFloat(match[1]) * TIME_UNITS[match[2].toLowerCase()]); } export function parseMemoryMB(str: string | number) { if (typeof str === 'number') return str; const match = MEMORY_RE.exec(str); if (!match) throw new Error(`${str} error parsing memory`); - return Math.ceil(parseFloat(match[1]) * MEMORY_UNITS[match[2]]); + return Math.ceil(parseFloat(match[1]) * MEMORY_UNITS[match[2].toLowerCase()]); } export function isClass(obj: any, strict = false) { @@ -432,3 +432,12 @@ export function sortFiles(files: { _id: string }[] | string[]) { }); return isString ? result.map((x) => x.name) : result; } + +export const htmlEncode = (str) => str.replace(/[&<>'"]/g, + (tag) => ({ + '&': '&', + '<': '<', + '>': '>', + "'": ''', + '"': '"', + }[tag])); diff --git a/packages/utils/package.json b/packages/utils/package.json index 73d9b534a..4a4442090 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -1,6 +1,6 @@ { "name": "@hydrooj/utils", - "version": "1.1.4", + "version": "1.1.5", "description": "hydrooj utils", "main": "package.json", "repository": "https://github.com/hydro-dev/Hydro.git", diff --git a/packages/vjudge/package.json b/packages/vjudge/package.json index 862b1bd3a..8f290946a 100644 --- a/packages/vjudge/package.json +++ b/packages/vjudge/package.json @@ -1,6 +1,6 @@ { "name": "@hydrooj/vjudge", - "version": "1.3.11", + "version": "1.4.0", "description": "Submit problems to remote oj", "main": "package.json", "repository": "https://github.com/hydro-dev/Hydro.git", diff --git a/packages/vjudge/src/providers/index.ts b/packages/vjudge/src/providers/index.ts index abaa5434c..0f5509f1a 100644 --- a/packages/vjudge/src/providers/index.ts +++ b/packages/vjudge/src/providers/index.ts @@ -1,8 +1,9 @@ import codeforces from './codeforces'; import luogu from './luogu'; +import poj from './poj'; import spoj from './spoj'; import uoj from './uoj'; export = { - codeforces, luogu, spoj, uoj, + codeforces, luogu, poj, spoj, uoj, }; diff --git a/packages/vjudge/src/providers/poj.ts b/packages/vjudge/src/providers/poj.ts new file mode 100644 index 000000000..7f293efbc --- /dev/null +++ b/packages/vjudge/src/providers/poj.ts @@ -0,0 +1,254 @@ +/* eslint-disable no-await-in-loop */ +import { PassThrough } from 'stream'; +import { JSDOM } from 'jsdom'; +import * as superagent from 'superagent'; +import proxy from 'superagent-proxy'; +import { STATUS } from '@hydrooj/utils/lib/status'; +import { + htmlEncode, parseMemoryMB, parseTimeMS, sleep, +} from '@hydrooj/utils/lib/utils'; +import { Logger } from 'hydrooj/src/logger'; +import * as setting from 'hydrooj/src/model/setting'; +import { IBasicProvider, RemoteAccount } from '../interface'; +import { VERDICT } from '../verdict'; + +proxy(superagent as any); +const logger = new Logger('remote/poj'); + +/* langs +poj: + display: POJ + execute: /bin/echo Invalid + domain: + - poj +poj.0: + display: G++ + monaco: cpp + highlight: cpp astyle-c + comment: // +poj.1: + display: GCC + monaco: c + highlight: c astyle-c + comment: // +poj.2: + display: Java + monaco: java + highlight: java astyle-java + comment: // +poj.3: + display: Pascal + monaco: pascal + highlight: pascal + comment: // +poj.4: + display: C++ + monaco: cpp + highlight: cpp astyle-c + comment: // +poj.5: + display: C + monaco: c + highlight: c astyle-c + comment: // +poj.6: + display: Fortran + monaco: plain + highlight: plain +*/ + +const langs = { + default: 'en', + 'zh-CN': 'zh_CN', + es: 'es', + ja: 'ja', +}; + +export default class POJProvider implements IBasicProvider { + constructor(public account: RemoteAccount, private save: (data: any) => Promise) { + if (account.cookie) this.cookie = account.cookie; + } + + cookie: string[] = []; + + get(url: string) { + logger.debug('get', url); + if (!url.includes('//')) url = `${this.account.endpoint || 'http://poj.org'}${url}`; + const req = superagent.get(url).set('Cookie', this.cookie); + if (this.account.proxy) return req.proxy(this.account.proxy); + return req; + } + + post(url: string) { + logger.debug('post', url, this.cookie); + if (!url.includes('//')) url = `${this.account.endpoint || 'http://poj.org'}${url}`; + const req = superagent.post(url).set('Cookie', this.cookie).type('form'); + if (this.account.proxy) return req.proxy(this.account.proxy); + return req; + } + + async getCsrfToken(url: string) { + const { header } = await this.get(url); + if (header['set-cookie']) { + await this.save({ cookie: header['set-cookie'] }); + this.cookie = header['set-cookie']; + } + return ''; + } + + get loggedIn() { + return this.get('/submit?problem_id=1000').then(({ text: html }) => !html.includes('
')); + } + + async ensureLogin() { + if (await this.loggedIn) return true; + logger.info('retry login'); + await this.getCsrfToken('/'); + const res = await this.post('/login') + .set('referer', 'http://poj.org/') + .send({ + user_id1: this.account.handle, + password1: this.account.password, + B1: 'login', + url: '/', + }); + return this.loggedIn; + } + + async getProblem(id: string) { + logger.info(id); + const res = await this.get(`/problem?id=${id.split('P')[1]}`); + const { window: { document } } = new JSDOM(res.text); + const files = {}; + const main = document.querySelector('[background="images/table_back.jpg"]>tbody>tr>td'); + const languages = [...main.children[0].children[0].children] + .map((i) => i.getAttribute('value')); + const info = main.getElementsByClassName('plm')[0] + .children[0].children[0].children[0]; + const time = info.children[0].innerHTML.split(' ')[1].toLowerCase().trim(); + const memory = info.children[2].innerHTML.split(' ')[1].toLowerCase().trim(); + const contents = {}; + const images = {}; + for (const lang of languages) { + await sleep(1000); + const { text } = await this.get(`/problem?id=${id.split('P')[1]}&lang=${lang}&change=true`); + const { window: { document: page } } = new JSDOM(text); + const content = page.querySelector('[background="images/table_back.jpg"]>tbody>tr>td'); + content.children[0].remove(); + content.children[0].remove(); + content.children[0].remove(); + content.querySelectorAll('img[src]').forEach((ele) => { + const src = ele.getAttribute('src'); + if (!src.startsWith('http')) return; + if (images[src]) { + ele.setAttribute('src', `file://${images[src]}.png`); + return; + } + const file = new PassThrough(); + this.get(src).pipe(file); + const fid = String.random(8); + images[src] = fid; + files[`${fid}.png`] = file; + ele.setAttribute('src', `file://${fid}.png`); + }); + let lastId = 0; + let markNext = ''; + let html = ''; + for (const node of content.children) { + if (node.className.includes('pst')) { + if (!node.innerHTML.startsWith('Sample ')) { + html += `

${htmlEncode(node.innerHTML)}

`; + } else if (node.innerHTML.startsWith('Sample Input')) { + lastId++; + markNext = 'input'; + } else { + markNext = 'output'; + } + } else if (node.className.includes('sio')) { + html += `
${htmlEncode(node.innerHTML)}
`; + } else if (node.className.includes('ptx')) { + for (const item of node.childNodes) { + if (item.nodeType === 3) { + const p = page.createElement('p'); + p.innerHTML = htmlEncode(item.textContent); + item.replaceWith(p); + } + if (item.nodeName.includes('BR')) item.remove(); + } + html += node.innerHTML; + } else html += node.innerHTML; + } + contents[langs[lang]] = html; + } + return { + title: main.getElementsByClassName('ptt')[0].innerHTML, + data: { + 'config.yaml': Buffer.from(`time: ${time}\nmemory: ${memory}\ntype: remote_judge\nsubType: poj\ntarget: ${id}`), + }, + files, + tag: [], + content: JSON.stringify(contents), + }; + } + + async listProblem(page: number, resync = false) { + if (resync && page > 1) return []; + const { text } = await this.get(`/problemlist?volume=${page}`); + const $dom = new JSDOM(text); + return Array.from($dom.window.document.querySelectorAll('.a>tbody>tr[align="center"]')) + .map((i) => `P${+i.children[0].innerHTML ? i.children[0].innerHTML : i.children[1].innerHTML}`); + } + + async submitProblem(id: string, lang: string, code: string, info) { + const language = lang.includes('poj.') ? lang.split('poj.')[1] : '0'; + const comment = setting.langs[lang].comment; + if (comment) { + const msg = `Hydro submission #${info.rid}@${new Date().getTime()}`; + if (typeof comment === 'string') code = `${comment} ${msg}\n${code}`; + else if (comment instanceof Array) code = `${comment[0]} ${msg} ${comment[1]}\n${code}`; + } + code = Buffer.from(code).toString('base64'); + const { text } = await this.post('/submit').send({ + problem_id: id.split('P')[1], + language, + source: code, + submit: 'Submit', + encoded: 1, + }); + if (text.includes('Error Occurred')) { + throw new Error(text.split('
  • ')[1].split('
  • ')[0]); + } + const { text: status } = await this.get(`/status?problem_id=${id.split('P')[1]}&user_id=${this.account.handle}&result=&language=${language}`); + const $dom = new JSDOM(status); + return $dom.window.document.querySelector('.a>tbody>tr[align="center"]>td').innerHTML; + } + + // eslint-disable-next-line consistent-return + async waitForSubmission(id: string, next, end) { + let count = 0; + // eslint-disable-next-line no-constant-condition + while (count < 60) { + count++; + await sleep(3000); + const { text } = await this.get(`/status?top=${+id + 1}`); + const { window: { document } } = new JSDOM(text); + const submission = document.querySelector('.a>tbody>tr[align="center"]'); + const status = VERDICT[submission.children[3].children[0].innerHTML.toUpperCase()] + || STATUS.STATUS_SYSTEM_ERROR; + if (status === STATUS.STATUS_JUDGING) continue; + if (status === STATUS.STATUS_COMPILE_ERROR) { + const { text: info } = await this.get(`/showcompileinfo?solution_id=${id}`); + const ceInfo = new JSDOM(info); + await next({ compilerText: ceInfo.window.document.querySelector('pre>font').innerHTML }); + } + const memory = parseMemoryMB(submission.children[4].innerHTML.trim() || 0) * 1024; + const time = parseTimeMS(submission.children[5].innerHTML.trim() || 0); + return await end({ + status, + score: status === STATUS.STATUS_ACCEPTED ? 100 : 0, + time, + memory, + }); + } + } +} diff --git a/packages/vjudge/src/providers/uoj.ts b/packages/vjudge/src/providers/uoj.ts index bbe7ad69d..fb1dc99f0 100644 --- a/packages/vjudge/src/providers/uoj.ts +++ b/packages/vjudge/src/providers/uoj.ts @@ -8,6 +8,7 @@ import { parseMemoryMB, parseTimeMS, sleep } from '@hydrooj/utils/lib/utils'; import { Logger } from 'hydrooj/src/logger'; import * as setting from 'hydrooj/src/model/setting'; import { IBasicProvider, RemoteAccount } from '../interface'; +import { VERDICT } from '../verdict'; proxy(superagent as any); const logger = new Logger('remote/uoj'); @@ -24,22 +25,6 @@ const MAPPING = { 十: 10, }; -const VERDICT = { - RUNTIME_ERROR: STATUS.STATUS_RUNTIME_ERROR, - WRONG_ANSWER: STATUS.STATUS_WRONG_ANSWER, - OK: STATUS.STATUS_ACCEPTED, - TIME_LIMIT_EXCEEDED: STATUS.STATUS_TIME_LIMIT_EXCEEDED, - MEMORY_LIMIT_EXCEEDED: STATUS.STATUS_MEMORY_LIMIT_EXCEEDED, - IDLENESS_LIMIT_EXCEEDED: STATUS.STATUS_TIME_LIMIT_EXCEEDED, - ACCEPTED: STATUS.STATUS_ACCEPTED, - 'WRONG ANSWER': STATUS.STATUS_WRONG_ANSWER, - 'RUNTIME ERROR': STATUS.STATUS_RUNTIME_ERROR, - 'TIME LIMIT EXCEEDED': STATUS.STATUS_TIME_LIMIT_EXCEEDED, - 'MEMORY LIMIT EXCEEDED': STATUS.STATUS_MEMORY_LIMIT_EXCEEDED, - 'IDLENESS LIMIT EXCEEDED': STATUS.STATUS_TIME_LIMIT_EXCEEDED, - 'EXTRA TEST PASSED': STATUS.STATUS_ACCEPTED, -}; - export default class UOJProvider implements IBasicProvider { constructor(public account: RemoteAccount, private save: (data: any) => Promise) { if (account.cookie) this.cookie = account.cookie; diff --git a/packages/vjudge/src/verdict.ts b/packages/vjudge/src/verdict.ts new file mode 100644 index 000000000..507945479 --- /dev/null +++ b/packages/vjudge/src/verdict.ts @@ -0,0 +1,19 @@ +import { STATUS } from '@hydrooj/utils/lib/status'; + +export const VERDICT = { + RUNTIME_ERROR: STATUS.STATUS_RUNTIME_ERROR, + WRONG_ANSWER: STATUS.STATUS_WRONG_ANSWER, + OK: STATUS.STATUS_ACCEPTED, + TIME_LIMIT_EXCEEDED: STATUS.STATUS_TIME_LIMIT_EXCEEDED, + MEMORY_LIMIT_EXCEEDED: STATUS.STATUS_MEMORY_LIMIT_EXCEEDED, + IDLENESS_LIMIT_EXCEEDED: STATUS.STATUS_TIME_LIMIT_EXCEEDED, + ACCEPTED: STATUS.STATUS_ACCEPTED, + 'WRONG ANSWER': STATUS.STATUS_WRONG_ANSWER, + 'RUNTIME ERROR': STATUS.STATUS_RUNTIME_ERROR, + 'TIME LIMIT EXCEEDED': STATUS.STATUS_TIME_LIMIT_EXCEEDED, + 'MEMORY LIMIT EXCEEDED': STATUS.STATUS_MEMORY_LIMIT_EXCEEDED, + 'IDLENESS LIMIT EXCEEDED': STATUS.STATUS_TIME_LIMIT_EXCEEDED, + 'EXTRA TEST PASSED': STATUS.STATUS_ACCEPTED, + 'COMPILE ERROR': STATUS.STATUS_COMPILE_ERROR, + 'RUNNING & JUDGING': STATUS.STATUS_JUDGING, +};