@wasm-oj/organizer 0.2.0 → 0.2.1
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.
- package/README.md +10 -1
- package/dist/index.d.ts +246 -2
- package/dist/index.js +2176 -444
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,9 +1,1919 @@
|
|
|
1
1
|
import { constants } from "node:fs";
|
|
2
|
-
import { lstat, mkdir, open,
|
|
2
|
+
import { lstat, mkdir, open, realpath, rename, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
|
+
import { BROWSER_COLLECTION_SCHEMA, BROWSER_PROBLEM_SCHEMA, BROWSER_PROBLEM_SCHEMA as BROWSER_PROBLEM_SCHEMA$1, assertJudgeDataMatchesPracticePublic, canonicalJsonBytes, deriveContestPublic, deriveJudgeData, derivePracticePublic, encodeJudgePackage, parseProblemCollectionIndex, parseStandaloneProblemBundle, parseStandaloneProblemBundle as parseStandaloneProblemBundle$1, problemCollectionRevision, validateJudgePackage, verifyProblemBundleBytes, verifyProblemCollectionRevision } from "@wasm-oj/core";
|
|
6
|
+
import { isBuiltinLanguage } from "@wasm-oj/contracts";
|
|
5
7
|
import os from "node:os";
|
|
6
|
-
|
|
8
|
+
//#region src/core/canonical-json.ts
|
|
9
|
+
var encoder = new TextEncoder();
|
|
10
|
+
function canonicalValue(value, path, ancestors) {
|
|
11
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
|
|
12
|
+
if (typeof value === "number") {
|
|
13
|
+
if (!Number.isSafeInteger(value)) throw new TypeError(`${path} must be a safe integer.`);
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
if (typeof value !== "object" || value === void 0) throw new TypeError(`${path} is not canonical JSON data.`);
|
|
17
|
+
if (ancestors.has(value)) throw new TypeError(`${path} contains a cycle.`);
|
|
18
|
+
const nextAncestors = new Set(ancestors).add(value);
|
|
19
|
+
if (Array.isArray(value)) return value.map((item, index) => canonicalValue(item, `${path}[${index}]`, nextAncestors));
|
|
20
|
+
const prototype = Object.getPrototypeOf(value);
|
|
21
|
+
if (prototype !== Object.prototype && prototype !== null) throw new TypeError(`${path} must be a plain object.`);
|
|
22
|
+
const record = value;
|
|
23
|
+
return Object.fromEntries(Object.keys(record).sort().map((key) => [key, canonicalValue(record[key], `${path}.${key}`, nextAncestors)]));
|
|
24
|
+
}
|
|
25
|
+
/** WASM-OJ canonical JSON: sorted object keys, safe integers, UTF-8, and one trailing newline. */
|
|
26
|
+
function canonicalJsonBytes$1(value) {
|
|
27
|
+
return encoder.encode(`${JSON.stringify(canonicalValue(value, "$", /* @__PURE__ */ new Set()))}\n`);
|
|
28
|
+
}
|
|
29
|
+
//#endregion
|
|
30
|
+
//#region src/online-judge/contest-public.ts
|
|
31
|
+
var CONTEST_PUBLIC_PROJECTION_SCHEMA = "wasm-oj-platform/contest-public-problem-projection/v1";
|
|
32
|
+
/** The single deterministic hidden-data redaction used by author CI and platform validation. */
|
|
33
|
+
function deriveContestPublic$1(practice) {
|
|
34
|
+
if (practice.judgeCases.some((testCase) => testCase.kind !== "sample")) throw new TypeError("Practice-public input contains non-sample judge data.");
|
|
35
|
+
return {
|
|
36
|
+
...structuredClone(practice),
|
|
37
|
+
editorial: {
|
|
38
|
+
"zh-TW": "",
|
|
39
|
+
en: ""
|
|
40
|
+
},
|
|
41
|
+
judgeCases: practice.judgeCases.filter((testCase) => testCase.kind === "sample").map((testCase) => structuredClone(testCase))
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function createContestPublicProjection(practice) {
|
|
45
|
+
return {
|
|
46
|
+
schema: CONTEST_PUBLIC_PROJECTION_SCHEMA,
|
|
47
|
+
problem: deriveContestPublic$1(practice)
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function contestPublicProjectionBytes(practice) {
|
|
51
|
+
return canonicalJsonBytes$1(createContestPublicProjection(practice));
|
|
52
|
+
}
|
|
53
|
+
//#endregion
|
|
54
|
+
//#region src/online-judge/contracts.ts
|
|
55
|
+
var SUBMISSION_SOURCE_LIMITS = Object.freeze({
|
|
56
|
+
totalBytes: 1048576,
|
|
57
|
+
maximumFiles: 128,
|
|
58
|
+
fileBytes: 262144
|
|
59
|
+
});
|
|
60
|
+
var SUBMISSION_VERDICTS = [
|
|
61
|
+
"accepted",
|
|
62
|
+
"wrong-answer",
|
|
63
|
+
"runtime-error",
|
|
64
|
+
"instruction-limit",
|
|
65
|
+
"memory-limit",
|
|
66
|
+
"output-limit",
|
|
67
|
+
"filesystem-limit",
|
|
68
|
+
"logical-time-limit",
|
|
69
|
+
"wall-time-limit",
|
|
70
|
+
"compile-error",
|
|
71
|
+
"judge-error",
|
|
72
|
+
"cancelled"
|
|
73
|
+
];
|
|
74
|
+
var MAX_PROMPT_BYTES = 16384;
|
|
75
|
+
var SHA256$2 = /^[0-9a-f]{64}$/;
|
|
76
|
+
var SLUG$2 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
77
|
+
var IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
78
|
+
var SOURCE_PATH$1 = /^(?!\/)(?!.*(?:^|\/)\.{1,2}(?:\/|$))(?!.*\/\/)(?!.*\\)(?!.*\/$)[^\u0000-\u001f\u007f]+$/;
|
|
79
|
+
var MAX_DURATION_SECONDS = 31622400;
|
|
80
|
+
var MAX_RULE_INTEGER = 1e9;
|
|
81
|
+
var PENALIZED_ICPC_VERDICTS = /* @__PURE__ */ new Set([
|
|
82
|
+
"wrong-answer",
|
|
83
|
+
"runtime-error",
|
|
84
|
+
"instruction-limit",
|
|
85
|
+
"memory-limit",
|
|
86
|
+
"output-limit",
|
|
87
|
+
"filesystem-limit",
|
|
88
|
+
"logical-time-limit",
|
|
89
|
+
"wall-time-limit",
|
|
90
|
+
"compile-error"
|
|
91
|
+
]);
|
|
92
|
+
function record$4(value, label) {
|
|
93
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new TypeError(`${label} must be an object.`);
|
|
94
|
+
return value;
|
|
95
|
+
}
|
|
96
|
+
function exact$3(value, keys, label) {
|
|
97
|
+
const actual = Object.keys(value).sort();
|
|
98
|
+
const expected = [...keys].sort();
|
|
99
|
+
if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) throw new TypeError(`${label} has an invalid shape.`);
|
|
100
|
+
}
|
|
101
|
+
function integer(value, label, minimum, maximum = MAX_RULE_INTEGER) {
|
|
102
|
+
if (!Number.isSafeInteger(value) || value < minimum || value > maximum) throw new TypeError(`${label} must be an integer between ${minimum} and ${maximum}.`);
|
|
103
|
+
return value;
|
|
104
|
+
}
|
|
105
|
+
function finiteNumber(value, label, minimum, maximum = MAX_RULE_INTEGER) {
|
|
106
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < minimum || value > maximum) throw new TypeError(`${label} must be a finite number between ${minimum} and ${maximum}.`);
|
|
107
|
+
return value;
|
|
108
|
+
}
|
|
109
|
+
function contestSlug(value, label) {
|
|
110
|
+
if (typeof value !== "string" || value.length > 128 || !SLUG$2.test(value)) throw new TypeError(`${label} is invalid.`);
|
|
111
|
+
return value;
|
|
112
|
+
}
|
|
113
|
+
function timestamp(value, label) {
|
|
114
|
+
if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/.test(value) || Number.isNaN(Date.parse(value))) throw new TypeError(`${label} must be an RFC 3339 UTC timestamp.`);
|
|
115
|
+
return value;
|
|
116
|
+
}
|
|
117
|
+
function parseClock(value, label) {
|
|
118
|
+
const input = record$4(value, label);
|
|
119
|
+
if (input.kind === "global") {
|
|
120
|
+
exact$3(input, [
|
|
121
|
+
"durationSeconds",
|
|
122
|
+
"kind",
|
|
123
|
+
"registrationClosesAt",
|
|
124
|
+
"registrationOpensAt",
|
|
125
|
+
"startsAt"
|
|
126
|
+
], label);
|
|
127
|
+
const registrationOpensAt = timestamp(input.registrationOpensAt, `${label}.registrationOpensAt`);
|
|
128
|
+
const registrationClosesAt = timestamp(input.registrationClosesAt, `${label}.registrationClosesAt`);
|
|
129
|
+
const startsAt = timestamp(input.startsAt, `${label}.startsAt`);
|
|
130
|
+
const durationSeconds = integer(input.durationSeconds, `${label}.durationSeconds`, 1, MAX_DURATION_SECONDS);
|
|
131
|
+
const opensMs = Date.parse(registrationOpensAt);
|
|
132
|
+
const closesMs = Date.parse(registrationClosesAt);
|
|
133
|
+
const startsMs = Date.parse(startsAt);
|
|
134
|
+
if (opensMs >= closesMs || opensMs > startsMs || closesMs > startsMs + durationSeconds * 1e3) throw new TypeError(`${label} registration window must open no later than start and close before the contest ends.`);
|
|
135
|
+
return {
|
|
136
|
+
kind: "global",
|
|
137
|
+
registrationOpensAt,
|
|
138
|
+
registrationClosesAt,
|
|
139
|
+
startsAt,
|
|
140
|
+
durationSeconds
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
if (input.kind === "individual") {
|
|
144
|
+
exact$3(input, [
|
|
145
|
+
"durationSeconds",
|
|
146
|
+
"enrollmentClosesAt",
|
|
147
|
+
"enrollmentOpensAt",
|
|
148
|
+
"kind"
|
|
149
|
+
], label);
|
|
150
|
+
const enrollmentOpensAt = timestamp(input.enrollmentOpensAt, `${label}.enrollmentOpensAt`);
|
|
151
|
+
const enrollmentClosesAt = timestamp(input.enrollmentClosesAt, `${label}.enrollmentClosesAt`);
|
|
152
|
+
if (Date.parse(enrollmentOpensAt) >= Date.parse(enrollmentClosesAt)) throw new TypeError(`${label} enrollment window is invalid.`);
|
|
153
|
+
return {
|
|
154
|
+
kind: "individual",
|
|
155
|
+
enrollmentOpensAt,
|
|
156
|
+
enrollmentClosesAt,
|
|
157
|
+
durationSeconds: integer(input.durationSeconds, `${label}.durationSeconds`, 1, MAX_DURATION_SECONDS)
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
throw new TypeError(`${label}.kind is invalid.`);
|
|
161
|
+
}
|
|
162
|
+
function parseCompilerPin(value, label) {
|
|
163
|
+
const input = record$4(value, label);
|
|
164
|
+
exact$3(input, ["configDigest", "configId"], label);
|
|
165
|
+
if (typeof input.configId !== "string" || !IDENTIFIER.test(input.configId)) throw new TypeError(`${label}.configId is invalid.`);
|
|
166
|
+
if (typeof input.configDigest !== "string" || !SHA256$2.test(input.configDigest)) throw new TypeError(`${label}.configDigest must be a lowercase SHA-256 digest.`);
|
|
167
|
+
return {
|
|
168
|
+
configId: input.configId,
|
|
169
|
+
configDigest: input.configDigest
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
function parsePromptLimits(value, label) {
|
|
173
|
+
const input = record$4(value, label);
|
|
174
|
+
exact$3(input, [
|
|
175
|
+
"generatedSourceBytes",
|
|
176
|
+
"inputTokens",
|
|
177
|
+
"outputTokens",
|
|
178
|
+
"promptBytes",
|
|
179
|
+
"timeoutSeconds"
|
|
180
|
+
], label);
|
|
181
|
+
return {
|
|
182
|
+
promptBytes: integer(input.promptBytes, `${label}.promptBytes`, 1, MAX_PROMPT_BYTES),
|
|
183
|
+
inputTokens: integer(input.inputTokens, `${label}.inputTokens`, 1, 1e6),
|
|
184
|
+
outputTokens: integer(input.outputTokens, `${label}.outputTokens`, 1, 1e6),
|
|
185
|
+
generatedSourceBytes: integer(input.generatedSourceBytes, `${label}.generatedSourceBytes`, 1, SUBMISSION_SOURCE_LIMITS.totalBytes),
|
|
186
|
+
timeoutSeconds: integer(input.timeoutSeconds, `${label}.timeoutSeconds`, 1, 3600)
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
function parseOfficialTrack(value, label) {
|
|
190
|
+
const input = record$4(value, label);
|
|
191
|
+
if (input.kind === "code") {
|
|
192
|
+
exact$3(input, ["aiAssist", "kind"], label);
|
|
193
|
+
if (input.aiAssist !== "allowed" && input.aiAssist !== "disabled") throw new TypeError(`${label}.aiAssist is invalid.`);
|
|
194
|
+
return {
|
|
195
|
+
kind: "code",
|
|
196
|
+
aiAssist: input.aiAssist
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
if (input.kind === "prompt-program") {
|
|
200
|
+
exact$3(input, [
|
|
201
|
+
"attemptPolicy",
|
|
202
|
+
"compiler",
|
|
203
|
+
"disclosure",
|
|
204
|
+
"kind",
|
|
205
|
+
"limits"
|
|
206
|
+
], label);
|
|
207
|
+
const attemptPolicy = record$4(input.attemptPolicy, `${label}.attemptPolicy`);
|
|
208
|
+
exact$3(attemptPolicy, ["consumeOn", "terminalInfrastructureFailure"], `${label}.attemptPolicy`);
|
|
209
|
+
if (attemptPolicy.consumeOn !== "model-response-received" || attemptPolicy.terminalInfrastructureFailure !== "release-reservation") throw new TypeError(`${label}.attemptPolicy is unsupported.`);
|
|
210
|
+
if (input.disclosure !== "private" && input.disclosure !== "best-after-end") throw new TypeError(`${label}.disclosure is invalid.`);
|
|
211
|
+
return {
|
|
212
|
+
kind: "prompt-program",
|
|
213
|
+
compiler: parseCompilerPin(input.compiler, `${label}.compiler`),
|
|
214
|
+
limits: parsePromptLimits(input.limits, `${label}.limits`),
|
|
215
|
+
attemptPolicy: {
|
|
216
|
+
consumeOn: "model-response-received",
|
|
217
|
+
terminalInfrastructureFailure: "release-reservation"
|
|
218
|
+
},
|
|
219
|
+
disclosure: input.disclosure
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
throw new TypeError(`${label}.kind is invalid.`);
|
|
223
|
+
}
|
|
224
|
+
function parseOutputProfile(value, label) {
|
|
225
|
+
const input = record$4(value, label);
|
|
226
|
+
exact$3(input, [
|
|
227
|
+
"entry",
|
|
228
|
+
"language",
|
|
229
|
+
"optimization",
|
|
230
|
+
"target"
|
|
231
|
+
], label);
|
|
232
|
+
if (typeof input.language !== "string" || !isBuiltinLanguage(input.language)) throw new TypeError(`${label}.language is unsupported.`);
|
|
233
|
+
if (input.target !== "wasip1" && input.target !== "wasix") throw new TypeError(`${label}.target is unsupported.`);
|
|
234
|
+
if (input.optimization !== "debug" && input.optimization !== "release") throw new TypeError(`${label}.optimization is unsupported.`);
|
|
235
|
+
if (typeof input.entry !== "string" || input.entry.length > 512 || !SOURCE_PATH$1.test(input.entry)) throw new TypeError(`${label}.entry is invalid.`);
|
|
236
|
+
return {
|
|
237
|
+
language: input.language,
|
|
238
|
+
target: input.target,
|
|
239
|
+
optimization: input.optimization,
|
|
240
|
+
entry: input.entry
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
function parseProblems(value, track, durationSeconds, label) {
|
|
244
|
+
if (!Array.isArray(value) || value.length < 1 || value.length > 100) throw new TypeError(`${label} must contain between 1 and 100 problems.`);
|
|
245
|
+
const slugs = /* @__PURE__ */ new Set();
|
|
246
|
+
const batchCounts = /* @__PURE__ */ new Map();
|
|
247
|
+
const batchReleases = /* @__PURE__ */ new Map();
|
|
248
|
+
return value.map((candidate, index) => {
|
|
249
|
+
const problemLabel = `${label}[${index}]`;
|
|
250
|
+
const input = record$4(candidate, problemLabel);
|
|
251
|
+
exact$3(input, track.kind === "code" ? [
|
|
252
|
+
"attemptLimit",
|
|
253
|
+
"batch",
|
|
254
|
+
"points",
|
|
255
|
+
"releaseAfterSeconds",
|
|
256
|
+
"slug",
|
|
257
|
+
"submissionClosesAfterSeconds"
|
|
258
|
+
] : [
|
|
259
|
+
"attemptLimit",
|
|
260
|
+
"batch",
|
|
261
|
+
"output",
|
|
262
|
+
"points",
|
|
263
|
+
"releaseAfterSeconds",
|
|
264
|
+
"slug",
|
|
265
|
+
"submissionClosesAfterSeconds"
|
|
266
|
+
], problemLabel);
|
|
267
|
+
const slug = contestSlug(input.slug, `${problemLabel}.slug`);
|
|
268
|
+
if (slugs.has(slug)) throw new TypeError(`${label} contains duplicate problem '${slug}'.`);
|
|
269
|
+
slugs.add(slug);
|
|
270
|
+
const batch = integer(input.batch, `${problemLabel}.batch`, 1, 100);
|
|
271
|
+
const releaseAfterSeconds = integer(input.releaseAfterSeconds, `${problemLabel}.releaseAfterSeconds`, 0, durationSeconds);
|
|
272
|
+
const submissionClosesAfterSeconds = integer(input.submissionClosesAfterSeconds, `${problemLabel}.submissionClosesAfterSeconds`, 0, durationSeconds);
|
|
273
|
+
if (submissionClosesAfterSeconds <= releaseAfterSeconds) throw new TypeError(`${problemLabel} submission window is empty.`);
|
|
274
|
+
const batchCount = (batchCounts.get(batch) ?? 0) + 1;
|
|
275
|
+
if (batchCount > 8) throw new TypeError(`${label} batch ${batch} exceeds 8 problems.`);
|
|
276
|
+
batchCounts.set(batch, batchCount);
|
|
277
|
+
const existingRelease = batchReleases.get(batch);
|
|
278
|
+
if (existingRelease !== void 0 && existingRelease !== releaseAfterSeconds) throw new TypeError(`${label} batch ${batch} has inconsistent release offsets.`);
|
|
279
|
+
batchReleases.set(batch, releaseAfterSeconds);
|
|
280
|
+
const base = {
|
|
281
|
+
slug,
|
|
282
|
+
batch,
|
|
283
|
+
releaseAfterSeconds,
|
|
284
|
+
submissionClosesAfterSeconds,
|
|
285
|
+
points: finiteNumber(input.points, `${problemLabel}.points`, Number.EPSILON),
|
|
286
|
+
attemptLimit: integer(input.attemptLimit, `${problemLabel}.attemptLimit`, 1, 1e6)
|
|
287
|
+
};
|
|
288
|
+
if (track.kind === "code") return base;
|
|
289
|
+
return {
|
|
290
|
+
...base,
|
|
291
|
+
output: parseOutputProfile(input.output, `${problemLabel}.output`)
|
|
292
|
+
};
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
function uniqueEnum(value, allowed, label) {
|
|
296
|
+
if (!Array.isArray(value)) throw new TypeError(`${label} must be an array.`);
|
|
297
|
+
const result = value.map((candidate, index) => {
|
|
298
|
+
if (typeof candidate !== "string" || !allowed.has(candidate)) throw new TypeError(`${label}[${index}] is invalid.`);
|
|
299
|
+
return candidate;
|
|
300
|
+
});
|
|
301
|
+
if (new Set(result).size !== result.length) throw new TypeError(`${label} contains a duplicate.`);
|
|
302
|
+
return result;
|
|
303
|
+
}
|
|
304
|
+
var SCORE_TIE_BREAKS = /* @__PURE__ */ new Set([
|
|
305
|
+
"fully-passed-cases",
|
|
306
|
+
"deterministic-cost",
|
|
307
|
+
"peak-memory",
|
|
308
|
+
"final-best-achieved-at"
|
|
309
|
+
]);
|
|
310
|
+
var ICPC_TIE_BREAKS = /* @__PURE__ */ new Set([
|
|
311
|
+
"last-solve-at",
|
|
312
|
+
"deterministic-cost",
|
|
313
|
+
"peak-memory"
|
|
314
|
+
]);
|
|
315
|
+
var PROGRESS_TIE_BREAKS = /* @__PURE__ */ new Set([
|
|
316
|
+
"fully-passed-cases",
|
|
317
|
+
"deterministic-cost",
|
|
318
|
+
"peak-memory",
|
|
319
|
+
"final-best-achieved-at"
|
|
320
|
+
]);
|
|
321
|
+
function parseScoring$1(value, label) {
|
|
322
|
+
const input = record$4(value, label);
|
|
323
|
+
if (input.kind === "score") {
|
|
324
|
+
exact$3(input, ["kind", "tieBreaks"], label);
|
|
325
|
+
return {
|
|
326
|
+
kind: "score",
|
|
327
|
+
tieBreaks: uniqueEnum(input.tieBreaks, SCORE_TIE_BREAKS, `${label}.tieBreaks`)
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
if (input.kind === "icpc") {
|
|
331
|
+
exact$3(input, [
|
|
332
|
+
"kind",
|
|
333
|
+
"penalizedVerdicts",
|
|
334
|
+
"tieBreaks",
|
|
335
|
+
"wrongAttemptPenaltyMinutes"
|
|
336
|
+
], label);
|
|
337
|
+
const penalizedVerdicts = uniqueEnum(input.penalizedVerdicts, new Set(SUBMISSION_VERDICTS), `${label}.penalizedVerdicts`);
|
|
338
|
+
if (penalizedVerdicts.some((verdict) => !PENALIZED_ICPC_VERDICTS.has(verdict))) throw new TypeError(`${label}.penalizedVerdicts may contain only contestant-fault verdicts.`);
|
|
339
|
+
return {
|
|
340
|
+
kind: "icpc",
|
|
341
|
+
wrongAttemptPenaltyMinutes: integer(input.wrongAttemptPenaltyMinutes, `${label}.wrongAttemptPenaltyMinutes`, 0, 10080),
|
|
342
|
+
penalizedVerdicts,
|
|
343
|
+
tieBreaks: uniqueEnum(input.tieBreaks, ICPC_TIE_BREAKS, `${label}.tieBreaks`)
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
if (input.kind === "progress") {
|
|
347
|
+
exact$3(input, ["kind", "tieBreaks"], label);
|
|
348
|
+
return {
|
|
349
|
+
kind: "progress",
|
|
350
|
+
tieBreaks: uniqueEnum(input.tieBreaks, PROGRESS_TIE_BREAKS, `${label}.tieBreaks`)
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
throw new TypeError(`${label}.kind is invalid.`);
|
|
354
|
+
}
|
|
355
|
+
function parseLeaderboard(value, clock, label) {
|
|
356
|
+
const input = record$4(value, label);
|
|
357
|
+
if (input.kind === "live" || input.kind === "hidden-until-end") {
|
|
358
|
+
exact$3(input, ["kind"], label);
|
|
359
|
+
return { kind: input.kind };
|
|
360
|
+
}
|
|
361
|
+
if (input.kind === "freeze") {
|
|
362
|
+
exact$3(input, ["atSeconds", "kind"], label);
|
|
363
|
+
if (clock.kind !== "global") throw new TypeError(`${label} freeze is only valid for a global clock.`);
|
|
364
|
+
return {
|
|
365
|
+
kind: "freeze",
|
|
366
|
+
atSeconds: integer(input.atSeconds, `${label}.atSeconds`, 1, clock.durationSeconds - 1)
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
throw new TypeError(`${label}.kind is invalid.`);
|
|
370
|
+
}
|
|
371
|
+
function scopedProblems(scope, atSeconds, problems) {
|
|
372
|
+
if (scope.kind === "all-released") return problems.filter((problem) => problem.releaseAfterSeconds <= atSeconds);
|
|
373
|
+
if (scope.kind === "batch") return problems.filter((problem) => problem.batch === scope.batch);
|
|
374
|
+
const requested = new Set(scope.slugs);
|
|
375
|
+
return problems.filter((problem) => requested.has(problem.slug));
|
|
376
|
+
}
|
|
377
|
+
function parseCheckpoint(value, label) {
|
|
378
|
+
const input = record$4(value, label);
|
|
379
|
+
exact$3(input, [
|
|
380
|
+
"atSeconds",
|
|
381
|
+
"id",
|
|
382
|
+
"ranking",
|
|
383
|
+
"scope",
|
|
384
|
+
"settlement",
|
|
385
|
+
"threshold"
|
|
386
|
+
], label);
|
|
387
|
+
const scopeInput = record$4(input.scope, `${label}.scope`);
|
|
388
|
+
let scope;
|
|
389
|
+
if (scopeInput.kind === "all-released") {
|
|
390
|
+
exact$3(scopeInput, ["kind"], `${label}.scope`);
|
|
391
|
+
scope = { kind: "all-released" };
|
|
392
|
+
} else if (scopeInput.kind === "batch") {
|
|
393
|
+
exact$3(scopeInput, ["batch", "kind"], `${label}.scope`);
|
|
394
|
+
scope = {
|
|
395
|
+
kind: "batch",
|
|
396
|
+
batch: integer(scopeInput.batch, `${label}.scope.batch`, 1, 100)
|
|
397
|
+
};
|
|
398
|
+
} else if (scopeInput.kind === "problems") {
|
|
399
|
+
exact$3(scopeInput, ["kind", "slugs"], `${label}.scope`);
|
|
400
|
+
if (!Array.isArray(scopeInput.slugs) || scopeInput.slugs.length < 1 || scopeInput.slugs.length > 100) throw new TypeError(`${label}.scope.slugs is invalid.`);
|
|
401
|
+
const slugs = scopeInput.slugs.map((slug, index) => contestSlug(slug, `${label}.scope.slugs[${index}]`));
|
|
402
|
+
if (new Set(slugs).size !== slugs.length) throw new TypeError(`${label}.scope.slugs contains a duplicate.`);
|
|
403
|
+
scope = {
|
|
404
|
+
kind: "problems",
|
|
405
|
+
slugs
|
|
406
|
+
};
|
|
407
|
+
} else throw new TypeError(`${label}.scope.kind is invalid.`);
|
|
408
|
+
const thresholdInput = record$4(input.threshold, `${label}.threshold`);
|
|
409
|
+
exact$3(thresholdInput, ["minimumScore", "minimumSolved"], `${label}.threshold`);
|
|
410
|
+
const minimumSolved = thresholdInput.minimumSolved === null ? null : integer(thresholdInput.minimumSolved, `${label}.threshold.minimumSolved`, 0, 100);
|
|
411
|
+
const minimumScore = thresholdInput.minimumScore === null ? null : finiteNumber(thresholdInput.minimumScore, `${label}.threshold.minimumScore`, 0);
|
|
412
|
+
if (minimumSolved === null && minimumScore === null) throw new TypeError(`${label}.threshold must declare at least one minimum.`);
|
|
413
|
+
let ranking;
|
|
414
|
+
if (input.ranking === null) ranking = null;
|
|
415
|
+
else {
|
|
416
|
+
const rankInput = record$4(input.ranking, `${label}.ranking`);
|
|
417
|
+
if (rankInput.kind === "top-k") {
|
|
418
|
+
exact$3(rankInput, ["count", "kind"], `${label}.ranking`);
|
|
419
|
+
ranking = {
|
|
420
|
+
kind: "top-k",
|
|
421
|
+
count: integer(rankInput.count, `${label}.ranking.count`, 1, MAX_RULE_INTEGER)
|
|
422
|
+
};
|
|
423
|
+
} else if (rankInput.kind === "top-percent") {
|
|
424
|
+
exact$3(rankInput, ["kind", "percent"], `${label}.ranking`);
|
|
425
|
+
ranking = {
|
|
426
|
+
kind: "top-percent",
|
|
427
|
+
percent: finiteNumber(rankInput.percent, `${label}.ranking.percent`, Number.EPSILON, 100)
|
|
428
|
+
};
|
|
429
|
+
} else throw new TypeError(`${label}.ranking.kind is invalid.`);
|
|
430
|
+
}
|
|
431
|
+
if (input.settlement !== "provisional" && input.settlement !== "pause-until-terminal") throw new TypeError(`${label}.settlement is invalid.`);
|
|
432
|
+
return {
|
|
433
|
+
id: contestSlug(input.id, `${label}.id`),
|
|
434
|
+
atSeconds: integer(input.atSeconds, `${label}.atSeconds`, 0, MAX_DURATION_SECONDS),
|
|
435
|
+
scope,
|
|
436
|
+
threshold: {
|
|
437
|
+
minimumSolved,
|
|
438
|
+
minimumScore
|
|
439
|
+
},
|
|
440
|
+
ranking,
|
|
441
|
+
settlement: input.settlement
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
function parsePresetProblemSlugs(value, label) {
|
|
445
|
+
if (!Array.isArray(value) || value.length < 1 || value.length > 100) throw new TypeError(`${label} must contain between 1 and 100 problem slugs.`);
|
|
446
|
+
const slugs = value.map((candidate, index) => contestSlug(candidate, `${label}[${index}]`));
|
|
447
|
+
if (new Set(slugs).size !== slugs.length) throw new TypeError(`${label} contains a duplicate.`);
|
|
448
|
+
return slugs;
|
|
449
|
+
}
|
|
450
|
+
function parseAiAssist(value, label) {
|
|
451
|
+
if (value !== "allowed" && value !== "disabled") throw new TypeError(`${label} is invalid.`);
|
|
452
|
+
return value;
|
|
453
|
+
}
|
|
454
|
+
function parsePresetLeaderboard(value, clockValue, label) {
|
|
455
|
+
return parseLeaderboard(value, parseClock(clockValue, `${label} clock`), label);
|
|
456
|
+
}
|
|
457
|
+
function parseContestRules(value, label = "contest rules") {
|
|
458
|
+
const input = record$4(value, label);
|
|
459
|
+
exact$3(input, [
|
|
460
|
+
"checkpoints",
|
|
461
|
+
"clock",
|
|
462
|
+
"evidenceAt",
|
|
463
|
+
"leaderboard",
|
|
464
|
+
"officialTrack",
|
|
465
|
+
"problems",
|
|
466
|
+
"scoring"
|
|
467
|
+
], label);
|
|
468
|
+
const clock = parseClock(input.clock, `${label}.clock`);
|
|
469
|
+
const officialTrack = parseOfficialTrack(input.officialTrack, `${label}.officialTrack`);
|
|
470
|
+
if (input.evidenceAt !== "input-admitted" && input.evidenceAt !== "generated-source-ready" && input.evidenceAt !== "judge-terminal") throw new TypeError(`${label}.evidenceAt is invalid.`);
|
|
471
|
+
if (input.evidenceAt === "generated-source-ready" && officialTrack.kind !== "prompt-program") throw new TypeError(`${label}.evidenceAt generated-source-ready requires the prompt-program track.`);
|
|
472
|
+
const evidenceAt = input.evidenceAt;
|
|
473
|
+
const problems = parseProblems(input.problems, officialTrack, clock.durationSeconds, `${label}.problems`);
|
|
474
|
+
const scoring = parseScoring$1(input.scoring, `${label}.scoring`);
|
|
475
|
+
if (!Array.isArray(input.checkpoints) || input.checkpoints.length > 100) throw new TypeError(`${label}.checkpoints is invalid.`);
|
|
476
|
+
const checkpoints = input.checkpoints.map((checkpoint, index) => parseCheckpoint(checkpoint, `${label}.checkpoints[${index}]`));
|
|
477
|
+
if (scoring.kind === "progress" && checkpoints.length < 1) throw new TypeError(`${label}.scoring progress requires at least one checkpoint.`);
|
|
478
|
+
const checkpointIds = /* @__PURE__ */ new Set();
|
|
479
|
+
let priorCheckpointAt = -1;
|
|
480
|
+
for (const checkpoint of checkpoints) {
|
|
481
|
+
if (checkpointIds.has(checkpoint.id)) throw new TypeError(`${label}.checkpoints contains duplicate id '${checkpoint.id}'.`);
|
|
482
|
+
checkpointIds.add(checkpoint.id);
|
|
483
|
+
if (checkpoint.atSeconds <= priorCheckpointAt || checkpoint.atSeconds > clock.durationSeconds) throw new TypeError(`${label}.checkpoints must have strictly increasing offsets within the contest duration.`);
|
|
484
|
+
priorCheckpointAt = checkpoint.atSeconds;
|
|
485
|
+
if (clock.kind === "individual" && (checkpoint.ranking !== null || checkpoint.settlement !== "provisional")) throw new TypeError(`${label}.checkpoints for an individual clock must be provisional and cannot rank entrants.`);
|
|
486
|
+
const scoped = scopedProblems(checkpoint.scope, checkpoint.atSeconds, problems);
|
|
487
|
+
if (scoped.length < 1 || scoped.some((problem) => problem.releaseAfterSeconds > checkpoint.atSeconds)) throw new TypeError(`${label}.checkpoint '${checkpoint.id}' scope must contain released problems.`);
|
|
488
|
+
if (checkpoint.scope.kind === "problems" && scoped.length !== checkpoint.scope.slugs.length) throw new TypeError(`${label}.checkpoint '${checkpoint.id}' references an unknown problem.`);
|
|
489
|
+
if (checkpoint.threshold.minimumSolved !== null && checkpoint.threshold.minimumSolved > scoped.length) throw new TypeError(`${label}.checkpoint '${checkpoint.id}' minimumSolved exceeds its scope.`);
|
|
490
|
+
const maximumScore = scoped.reduce((total, problem) => total + problem.points, 0);
|
|
491
|
+
if (checkpoint.threshold.minimumScore !== null && checkpoint.threshold.minimumScore > maximumScore) throw new TypeError(`${label}.checkpoint '${checkpoint.id}' minimumScore exceeds its scope.`);
|
|
492
|
+
}
|
|
493
|
+
const base = {
|
|
494
|
+
clock,
|
|
495
|
+
evidenceAt,
|
|
496
|
+
scoring,
|
|
497
|
+
checkpoints,
|
|
498
|
+
leaderboard: parseLeaderboard(input.leaderboard, clock, `${label}.leaderboard`)
|
|
499
|
+
};
|
|
500
|
+
return officialTrack.kind === "code" ? {
|
|
501
|
+
...base,
|
|
502
|
+
officialTrack,
|
|
503
|
+
problems
|
|
504
|
+
} : {
|
|
505
|
+
...base,
|
|
506
|
+
officialTrack,
|
|
507
|
+
problems
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
function parseContestRulesPreset(value, label = "contest rules preset") {
|
|
511
|
+
const input = record$4(value, label);
|
|
512
|
+
const preset = input.preset;
|
|
513
|
+
if (preset === "classic-score") {
|
|
514
|
+
exact$3(input, [
|
|
515
|
+
"aiAssist",
|
|
516
|
+
"attemptLimit",
|
|
517
|
+
"clock",
|
|
518
|
+
"leaderboard",
|
|
519
|
+
"pointsPerProblem",
|
|
520
|
+
"preset",
|
|
521
|
+
"problemSlugs"
|
|
522
|
+
], label);
|
|
523
|
+
return {
|
|
524
|
+
preset,
|
|
525
|
+
clock: parseClock(input.clock, `${label}.clock`),
|
|
526
|
+
problemSlugs: parsePresetProblemSlugs(input.problemSlugs, `${label}.problemSlugs`),
|
|
527
|
+
pointsPerProblem: finiteNumber(input.pointsPerProblem, `${label}.pointsPerProblem`, Number.EPSILON),
|
|
528
|
+
attemptLimit: integer(input.attemptLimit, `${label}.attemptLimit`, 1, 1e6),
|
|
529
|
+
aiAssist: parseAiAssist(input.aiAssist, `${label}.aiAssist`),
|
|
530
|
+
leaderboard: parsePresetLeaderboard(input.leaderboard, input.clock, `${label}.leaderboard`)
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
if (preset === "icpc") {
|
|
534
|
+
exact$3(input, [
|
|
535
|
+
"aiAssist",
|
|
536
|
+
"attemptLimit",
|
|
537
|
+
"clock",
|
|
538
|
+
"leaderboard",
|
|
539
|
+
"penalizedVerdicts",
|
|
540
|
+
"preset",
|
|
541
|
+
"problemSlugs",
|
|
542
|
+
"wrongAttemptPenaltyMinutes"
|
|
543
|
+
], label);
|
|
544
|
+
const penalizedVerdicts = uniqueEnum(input.penalizedVerdicts, new Set(SUBMISSION_VERDICTS), `${label}.penalizedVerdicts`);
|
|
545
|
+
if (penalizedVerdicts.some((verdict) => !PENALIZED_ICPC_VERDICTS.has(verdict))) throw new TypeError(`${label}.penalizedVerdicts is invalid.`);
|
|
546
|
+
return {
|
|
547
|
+
preset,
|
|
548
|
+
clock: parseClock(input.clock, `${label}.clock`),
|
|
549
|
+
problemSlugs: parsePresetProblemSlugs(input.problemSlugs, `${label}.problemSlugs`),
|
|
550
|
+
attemptLimit: integer(input.attemptLimit, `${label}.attemptLimit`, 1, 1e6),
|
|
551
|
+
aiAssist: parseAiAssist(input.aiAssist, `${label}.aiAssist`),
|
|
552
|
+
wrongAttemptPenaltyMinutes: integer(input.wrongAttemptPenaltyMinutes, `${label}.wrongAttemptPenaltyMinutes`, 0, 10080),
|
|
553
|
+
penalizedVerdicts,
|
|
554
|
+
leaderboard: parsePresetLeaderboard(input.leaderboard, input.clock, `${label}.leaderboard`)
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
if (preset === "blitz-batches") {
|
|
558
|
+
exact$3(input, [
|
|
559
|
+
"aiAssist",
|
|
560
|
+
"attemptLimit",
|
|
561
|
+
"batchSize",
|
|
562
|
+
"clock",
|
|
563
|
+
"leaderboard",
|
|
564
|
+
"minimumSolvedPerBatch",
|
|
565
|
+
"pointsPerProblem",
|
|
566
|
+
"preset",
|
|
567
|
+
"problemSlugs",
|
|
568
|
+
"releaseIntervalSeconds"
|
|
569
|
+
], label);
|
|
570
|
+
const problemSlugs = parsePresetProblemSlugs(input.problemSlugs, `${label}.problemSlugs`);
|
|
571
|
+
const batchSize = integer(input.batchSize, `${label}.batchSize`, 1, 8);
|
|
572
|
+
return {
|
|
573
|
+
preset,
|
|
574
|
+
clock: parseClock(input.clock, `${label}.clock`),
|
|
575
|
+
problemSlugs,
|
|
576
|
+
batchSize,
|
|
577
|
+
releaseIntervalSeconds: integer(input.releaseIntervalSeconds, `${label}.releaseIntervalSeconds`, 1, MAX_DURATION_SECONDS),
|
|
578
|
+
pointsPerProblem: finiteNumber(input.pointsPerProblem, `${label}.pointsPerProblem`, Number.EPSILON),
|
|
579
|
+
attemptLimit: integer(input.attemptLimit, `${label}.attemptLimit`, 1, 1e6),
|
|
580
|
+
minimumSolvedPerBatch: integer(input.minimumSolvedPerBatch, `${label}.minimumSolvedPerBatch`, 0, Math.min(batchSize, problemSlugs.length)),
|
|
581
|
+
aiAssist: parseAiAssist(input.aiAssist, `${label}.aiAssist`),
|
|
582
|
+
leaderboard: parsePresetLeaderboard(input.leaderboard, input.clock, `${label}.leaderboard`)
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
if (preset === "prompt-five-by-three") {
|
|
586
|
+
exact$3(input, [
|
|
587
|
+
"clock",
|
|
588
|
+
"compiler",
|
|
589
|
+
"disclosure",
|
|
590
|
+
"leaderboard",
|
|
591
|
+
"limits",
|
|
592
|
+
"preset",
|
|
593
|
+
"problems"
|
|
594
|
+
], label);
|
|
595
|
+
if (!Array.isArray(input.problems) || input.problems.length !== 5) throw new TypeError(`${label}.problems must contain exactly five problems.`);
|
|
596
|
+
const problems = input.problems.map((candidate, index) => {
|
|
597
|
+
const problem = record$4(candidate, `${label}.problems[${index}]`);
|
|
598
|
+
exact$3(problem, ["output", "slug"], `${label}.problems[${index}]`);
|
|
599
|
+
return {
|
|
600
|
+
slug: contestSlug(problem.slug, `${label}.problems[${index}].slug`),
|
|
601
|
+
output: parseOutputProfile(problem.output, `${label}.problems[${index}].output`)
|
|
602
|
+
};
|
|
603
|
+
});
|
|
604
|
+
if (new Set(problems.map((problem) => problem.slug)).size !== problems.length) throw new TypeError(`${label}.problems contains a duplicate.`);
|
|
605
|
+
if (input.disclosure !== "private" && input.disclosure !== "best-after-end") throw new TypeError(`${label}.disclosure is invalid.`);
|
|
606
|
+
return {
|
|
607
|
+
preset,
|
|
608
|
+
clock: parseClock(input.clock, `${label}.clock`),
|
|
609
|
+
problems,
|
|
610
|
+
compiler: parseCompilerPin(input.compiler, `${label}.compiler`),
|
|
611
|
+
limits: parsePromptLimits(input.limits, `${label}.limits`),
|
|
612
|
+
disclosure: input.disclosure,
|
|
613
|
+
leaderboard: parsePresetLeaderboard(input.leaderboard, input.clock, `${label}.leaderboard`)
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
throw new TypeError(`${label}.preset is invalid.`);
|
|
617
|
+
}
|
|
618
|
+
function expandContestRulesPreset(preset) {
|
|
619
|
+
const close = preset.clock.durationSeconds;
|
|
620
|
+
if (preset.preset === "classic-score") return parseContestRules({
|
|
621
|
+
clock: preset.clock,
|
|
622
|
+
officialTrack: {
|
|
623
|
+
kind: "code",
|
|
624
|
+
aiAssist: preset.aiAssist
|
|
625
|
+
},
|
|
626
|
+
evidenceAt: "input-admitted",
|
|
627
|
+
problems: preset.problemSlugs.map((slug) => ({
|
|
628
|
+
slug,
|
|
629
|
+
batch: 1,
|
|
630
|
+
releaseAfterSeconds: 0,
|
|
631
|
+
submissionClosesAfterSeconds: close,
|
|
632
|
+
points: preset.pointsPerProblem,
|
|
633
|
+
attemptLimit: preset.attemptLimit
|
|
634
|
+
})),
|
|
635
|
+
scoring: {
|
|
636
|
+
kind: "score",
|
|
637
|
+
tieBreaks: [
|
|
638
|
+
"fully-passed-cases",
|
|
639
|
+
"deterministic-cost",
|
|
640
|
+
"peak-memory",
|
|
641
|
+
"final-best-achieved-at"
|
|
642
|
+
]
|
|
643
|
+
},
|
|
644
|
+
checkpoints: [],
|
|
645
|
+
leaderboard: preset.leaderboard
|
|
646
|
+
});
|
|
647
|
+
if (preset.preset === "icpc") return parseContestRules({
|
|
648
|
+
clock: preset.clock,
|
|
649
|
+
officialTrack: {
|
|
650
|
+
kind: "code",
|
|
651
|
+
aiAssist: preset.aiAssist
|
|
652
|
+
},
|
|
653
|
+
evidenceAt: "judge-terminal",
|
|
654
|
+
problems: preset.problemSlugs.map((slug) => ({
|
|
655
|
+
slug,
|
|
656
|
+
batch: 1,
|
|
657
|
+
releaseAfterSeconds: 0,
|
|
658
|
+
submissionClosesAfterSeconds: close,
|
|
659
|
+
points: 100,
|
|
660
|
+
attemptLimit: preset.attemptLimit
|
|
661
|
+
})),
|
|
662
|
+
scoring: {
|
|
663
|
+
kind: "icpc",
|
|
664
|
+
wrongAttemptPenaltyMinutes: preset.wrongAttemptPenaltyMinutes,
|
|
665
|
+
penalizedVerdicts: preset.penalizedVerdicts,
|
|
666
|
+
tieBreaks: []
|
|
667
|
+
},
|
|
668
|
+
checkpoints: [],
|
|
669
|
+
leaderboard: preset.leaderboard
|
|
670
|
+
});
|
|
671
|
+
if (preset.preset === "blitz-batches") {
|
|
672
|
+
const batchCount = Math.ceil(preset.problemSlugs.length / preset.batchSize);
|
|
673
|
+
const checkpoints = Array.from({ length: Math.max(0, batchCount - 1) }, (_, index) => ({
|
|
674
|
+
id: `batch-${index + 1}`,
|
|
675
|
+
atSeconds: (index + 1) * preset.releaseIntervalSeconds,
|
|
676
|
+
scope: {
|
|
677
|
+
kind: "batch",
|
|
678
|
+
batch: index + 1
|
|
679
|
+
},
|
|
680
|
+
threshold: {
|
|
681
|
+
minimumSolved: preset.minimumSolvedPerBatch,
|
|
682
|
+
minimumScore: null
|
|
683
|
+
},
|
|
684
|
+
ranking: null,
|
|
685
|
+
settlement: "provisional"
|
|
686
|
+
}));
|
|
687
|
+
return parseContestRules({
|
|
688
|
+
clock: preset.clock,
|
|
689
|
+
officialTrack: {
|
|
690
|
+
kind: "code",
|
|
691
|
+
aiAssist: preset.aiAssist
|
|
692
|
+
},
|
|
693
|
+
evidenceAt: "judge-terminal",
|
|
694
|
+
problems: preset.problemSlugs.map((slug, index) => ({
|
|
695
|
+
slug,
|
|
696
|
+
batch: Math.floor(index / preset.batchSize) + 1,
|
|
697
|
+
releaseAfterSeconds: Math.floor(index / preset.batchSize) * preset.releaseIntervalSeconds,
|
|
698
|
+
submissionClosesAfterSeconds: close,
|
|
699
|
+
points: preset.pointsPerProblem,
|
|
700
|
+
attemptLimit: preset.attemptLimit
|
|
701
|
+
})),
|
|
702
|
+
scoring: {
|
|
703
|
+
kind: "progress",
|
|
704
|
+
tieBreaks: [
|
|
705
|
+
"fully-passed-cases",
|
|
706
|
+
"deterministic-cost",
|
|
707
|
+
"final-best-achieved-at"
|
|
708
|
+
]
|
|
709
|
+
},
|
|
710
|
+
checkpoints,
|
|
711
|
+
leaderboard: preset.leaderboard
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
return parseContestRules({
|
|
715
|
+
clock: preset.clock,
|
|
716
|
+
officialTrack: {
|
|
717
|
+
kind: "prompt-program",
|
|
718
|
+
compiler: preset.compiler,
|
|
719
|
+
limits: preset.limits,
|
|
720
|
+
attemptPolicy: {
|
|
721
|
+
consumeOn: "model-response-received",
|
|
722
|
+
terminalInfrastructureFailure: "release-reservation"
|
|
723
|
+
},
|
|
724
|
+
disclosure: preset.disclosure
|
|
725
|
+
},
|
|
726
|
+
evidenceAt: "generated-source-ready",
|
|
727
|
+
problems: preset.problems.map((problem) => ({
|
|
728
|
+
...problem,
|
|
729
|
+
batch: 1,
|
|
730
|
+
releaseAfterSeconds: 0,
|
|
731
|
+
submissionClosesAfterSeconds: close,
|
|
732
|
+
points: 100,
|
|
733
|
+
attemptLimit: 3
|
|
734
|
+
})),
|
|
735
|
+
scoring: {
|
|
736
|
+
kind: "score",
|
|
737
|
+
tieBreaks: ["deterministic-cost", "final-best-achieved-at"]
|
|
738
|
+
},
|
|
739
|
+
checkpoints: [],
|
|
740
|
+
leaderboard: preset.leaderboard
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
function logicalContestSeconds(snapshot, now, durationSeconds) {
|
|
744
|
+
integer(snapshot.generation, "contest clock generation", 1);
|
|
745
|
+
const duration = integer(durationSeconds, "contest durationSeconds", 1, MAX_DURATION_SECONDS);
|
|
746
|
+
const capturedAt = timestamp(snapshot.capturedAt, "contest clock capturedAt");
|
|
747
|
+
const nowTimestamp = timestamp(now, "contest clock now");
|
|
748
|
+
if (snapshot.state !== "running" && snapshot.state !== "paused") throw new TypeError("contest clock state is invalid.");
|
|
749
|
+
if (typeof snapshot.logicalSeconds !== "number" || !Number.isFinite(snapshot.logicalSeconds)) throw new TypeError("contest clock logicalSeconds is invalid.");
|
|
750
|
+
const projected = snapshot.state === "paused" ? snapshot.logicalSeconds : snapshot.logicalSeconds + (Date.parse(nowTimestamp) - Date.parse(capturedAt)) / 1e3;
|
|
751
|
+
return Math.min(duration, Math.max(0, projected));
|
|
752
|
+
}
|
|
753
|
+
function projectContestRules(input) {
|
|
754
|
+
const { rules, clock, entrant } = input;
|
|
755
|
+
const observedAt = timestamp(input.observedAt, "contest projection observedAt");
|
|
756
|
+
const scheduleShiftSeconds = integer(input.scheduleShiftSeconds ?? 0, "contest projection scheduleShiftSeconds", 0);
|
|
757
|
+
const observedMs = Date.parse(observedAt) - scheduleShiftSeconds * 1e3;
|
|
758
|
+
if (clock !== null && (!Number.isSafeInteger(clock.generation) || clock.generation < 1)) throw new TypeError("contest clock generation is invalid.");
|
|
759
|
+
let phase;
|
|
760
|
+
let logicalSeconds = clock === null ? null : logicalContestSeconds(clock, observedAt, rules.clock.durationSeconds);
|
|
761
|
+
if (input.contestEnded === true || entrant?.completed === true) phase = "ended";
|
|
762
|
+
else if (entrant === null || !entrant.joined) {
|
|
763
|
+
const opensAt = rules.clock.kind === "global" ? rules.clock.registrationOpensAt : rules.clock.enrollmentOpensAt;
|
|
764
|
+
const closesAt = rules.clock.kind === "global" ? rules.clock.registrationClosesAt : rules.clock.enrollmentClosesAt;
|
|
765
|
+
if (observedMs >= Date.parse(opensAt) && observedMs < Date.parse(closesAt)) phase = "registration";
|
|
766
|
+
else if (rules.clock.kind === "global" && observedMs >= Date.parse(rules.clock.startsAt) + rules.clock.durationSeconds * 1e3) phase = "ended";
|
|
767
|
+
else phase = "upcoming";
|
|
768
|
+
} else if (entrant.eliminatedAtSeconds !== null) {
|
|
769
|
+
phase = "eliminated";
|
|
770
|
+
logicalSeconds ??= entrant.eliminatedAtSeconds;
|
|
771
|
+
} else if (clock?.state === "paused") phase = "paused";
|
|
772
|
+
else if (rules.clock.kind === "individual" && !entrant.started) phase = "awaiting-start";
|
|
773
|
+
else if (clock === null) phase = "upcoming";
|
|
774
|
+
else if (logicalSeconds !== null && logicalSeconds >= rules.clock.durationSeconds) phase = "ended";
|
|
775
|
+
else phase = "running";
|
|
776
|
+
const attempted = (slug) => {
|
|
777
|
+
const count = input.attemptedByProblem[slug] ?? 0;
|
|
778
|
+
if (!Number.isSafeInteger(count) || count < 0) throw new TypeError(`attempt count for '${slug}' is invalid.`);
|
|
779
|
+
return count;
|
|
780
|
+
};
|
|
781
|
+
const problems = rules.problems.map((problem) => {
|
|
782
|
+
let availability;
|
|
783
|
+
if (entrant === null || !entrant.joined || logicalSeconds === null || logicalSeconds < problem.releaseAfterSeconds) availability = "locked";
|
|
784
|
+
else if (phase === "eliminated" || logicalSeconds >= problem.submissionClosesAfterSeconds || phase === "ended") availability = "closed";
|
|
785
|
+
else availability = "open";
|
|
786
|
+
return {
|
|
787
|
+
slug: problem.slug,
|
|
788
|
+
availability,
|
|
789
|
+
releaseAfterSeconds: problem.releaseAfterSeconds,
|
|
790
|
+
submissionClosesAfterSeconds: problem.submissionClosesAfterSeconds,
|
|
791
|
+
attemptsRemaining: Math.max(0, problem.attemptLimit - attempted(problem.slug))
|
|
792
|
+
};
|
|
793
|
+
});
|
|
794
|
+
let nextBoundarySeconds = null;
|
|
795
|
+
if (logicalSeconds !== null && phase !== "ended" && phase !== "eliminated") {
|
|
796
|
+
const candidates = [
|
|
797
|
+
rules.clock.durationSeconds,
|
|
798
|
+
...rules.problems.flatMap((problem) => [problem.releaseAfterSeconds, problem.submissionClosesAfterSeconds]),
|
|
799
|
+
...rules.checkpoints.map((checkpoint) => checkpoint.atSeconds),
|
|
800
|
+
...rules.leaderboard.kind === "freeze" ? [rules.leaderboard.atSeconds] : []
|
|
801
|
+
].filter((boundary) => boundary > logicalSeconds);
|
|
802
|
+
nextBoundarySeconds = candidates.length === 0 ? null : Math.min(...candidates);
|
|
803
|
+
}
|
|
804
|
+
return {
|
|
805
|
+
generation: clock?.generation ?? 0,
|
|
806
|
+
phase,
|
|
807
|
+
logicalSeconds,
|
|
808
|
+
nextBoundarySeconds,
|
|
809
|
+
problems
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
function decideContestAdmission(input, problemSlug) {
|
|
813
|
+
const problem = input.rules.problems.find((candidate) => candidate.slug === problemSlug);
|
|
814
|
+
if (!problem) return {
|
|
815
|
+
allowed: false,
|
|
816
|
+
reason: "unknown-problem"
|
|
817
|
+
};
|
|
818
|
+
if (input.entrant === null || !input.entrant.joined) return {
|
|
819
|
+
allowed: false,
|
|
820
|
+
reason: "not-joined"
|
|
821
|
+
};
|
|
822
|
+
if (input.rules.clock.kind === "individual" && !input.entrant.started) return {
|
|
823
|
+
allowed: false,
|
|
824
|
+
reason: "not-started"
|
|
825
|
+
};
|
|
826
|
+
if (input.clock?.state === "paused") return {
|
|
827
|
+
allowed: false,
|
|
828
|
+
reason: "paused"
|
|
829
|
+
};
|
|
830
|
+
if (input.entrant.eliminatedAtSeconds !== null) return {
|
|
831
|
+
allowed: false,
|
|
832
|
+
reason: "eliminated"
|
|
833
|
+
};
|
|
834
|
+
const projectedProblem = projectContestRules(input).problems.find((candidate) => candidate.slug === problemSlug);
|
|
835
|
+
if (projectedProblem.availability === "locked") return {
|
|
836
|
+
allowed: false,
|
|
837
|
+
reason: "problem-locked"
|
|
838
|
+
};
|
|
839
|
+
if (projectedProblem.availability === "closed") return {
|
|
840
|
+
allowed: false,
|
|
841
|
+
reason: "problem-closed"
|
|
842
|
+
};
|
|
843
|
+
if (projectedProblem.attemptsRemaining < 1) return {
|
|
844
|
+
allowed: false,
|
|
845
|
+
reason: "attempt-limit"
|
|
846
|
+
};
|
|
847
|
+
return {
|
|
848
|
+
allowed: true,
|
|
849
|
+
problem
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
function compareNumber(left, right, direction) {
|
|
853
|
+
return direction === "asc" ? left - right : right - left;
|
|
854
|
+
}
|
|
855
|
+
function compareFactMetric(left, right, tieBreak) {
|
|
856
|
+
switch (tieBreak) {
|
|
857
|
+
case "fully-passed-cases": return compareNumber(left.fullyPassedCases, right.fullyPassedCases, "desc");
|
|
858
|
+
case "deterministic-cost": return compareNumber(left.deterministicCost, right.deterministicCost, "asc");
|
|
859
|
+
case "peak-memory": return compareNumber(left.peakMemoryBytes, right.peakMemoryBytes, "asc");
|
|
860
|
+
case "final-best-achieved-at": return compareNumber(left.logicalSeconds, right.logicalSeconds, "asc");
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
function bestScoreFact(facts, tieBreaks) {
|
|
864
|
+
let selected = null;
|
|
865
|
+
for (const fact of facts) {
|
|
866
|
+
if (!fact.eligible) continue;
|
|
867
|
+
if (selected === null) {
|
|
868
|
+
selected = fact;
|
|
869
|
+
continue;
|
|
870
|
+
}
|
|
871
|
+
let comparison = compareNumber(fact.score, selected.score, "desc");
|
|
872
|
+
for (const tieBreak of tieBreaks) {
|
|
873
|
+
if (comparison !== 0) break;
|
|
874
|
+
comparison = compareFactMetric(fact, selected, tieBreak);
|
|
875
|
+
}
|
|
876
|
+
if (comparison < 0 || comparison === 0 && fact.logicalSeconds < selected.logicalSeconds) selected = fact;
|
|
877
|
+
}
|
|
878
|
+
return selected;
|
|
879
|
+
}
|
|
880
|
+
function validateResultFacts(rules, entrantIds, results) {
|
|
881
|
+
if (new Set(entrantIds).size !== entrantIds.length || entrantIds.some((entrantId) => !entrantId)) throw new TypeError("entrantIds must be unique non-empty strings.");
|
|
882
|
+
const entrants = new Set(entrantIds);
|
|
883
|
+
const problems = new Set(rules.problems.map((problem) => problem.slug));
|
|
884
|
+
for (const result of results) {
|
|
885
|
+
if (!entrants.has(result.entrantId)) throw new TypeError(`result references unknown entrant '${result.entrantId}'.`);
|
|
886
|
+
if (!problems.has(result.problemSlug)) throw new TypeError(`result references unknown problem '${result.problemSlug}'.`);
|
|
887
|
+
if (!SUBMISSION_VERDICTS.includes(result.verdict)) throw new TypeError("result verdict is invalid.");
|
|
888
|
+
finiteNumber(result.score, "result score", 0, 100);
|
|
889
|
+
integer(result.fullyPassedCases, "result fullyPassedCases", 0);
|
|
890
|
+
integer(result.deterministicCost, "result deterministicCost", 0);
|
|
891
|
+
integer(result.peakMemoryBytes, "result peakMemoryBytes", 0);
|
|
892
|
+
finiteNumber(result.logicalSeconds, "result logicalSeconds", 0, rules.clock.durationSeconds);
|
|
893
|
+
if (typeof result.eligible !== "boolean") throw new TypeError("result eligible must be boolean.");
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
function standingMetric(standing, tieBreak) {
|
|
897
|
+
switch (tieBreak) {
|
|
898
|
+
case "fully-passed-cases": return standing.fullyPassedCases;
|
|
899
|
+
case "deterministic-cost": return standing.deterministicCost;
|
|
900
|
+
case "peak-memory": return standing.peakMemoryBytes;
|
|
901
|
+
case "final-best-achieved-at":
|
|
902
|
+
case "last-solve-at": return standing.achievedAtSeconds;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
function compareStandingMetric(left, right, tieBreak) {
|
|
906
|
+
const direction = tieBreak === "fully-passed-cases" ? "desc" : "asc";
|
|
907
|
+
return compareNumber(standingMetric(left, tieBreak), standingMetric(right, tieBreak), direction);
|
|
908
|
+
}
|
|
909
|
+
function compareCompetitiveStandings(scoring, left, right) {
|
|
910
|
+
if (scoring.kind === "icpc") {
|
|
911
|
+
let comparison = compareNumber(left.solved, right.solved, "desc") || compareNumber(left.penaltyMinutes, right.penaltyMinutes, "asc");
|
|
912
|
+
for (const tieBreak of scoring.tieBreaks) {
|
|
913
|
+
if (comparison !== 0) break;
|
|
914
|
+
comparison = compareStandingMetric(left, right, tieBreak);
|
|
915
|
+
}
|
|
916
|
+
return comparison;
|
|
917
|
+
}
|
|
918
|
+
let comparison = scoring.kind === "progress" ? compareNumber(left.furthestCheckpoint, right.furthestCheckpoint, "desc") || compareNumber(left.solved, right.solved, "desc") || compareNumber(left.score, right.score, "desc") : compareNumber(left.score, right.score, "desc");
|
|
919
|
+
for (const tieBreak of scoring.tieBreaks) {
|
|
920
|
+
if (comparison !== 0) break;
|
|
921
|
+
comparison = compareStandingMetric(left, right, tieBreak);
|
|
922
|
+
}
|
|
923
|
+
return comparison;
|
|
924
|
+
}
|
|
925
|
+
function rankContestResults(rules, entrantIds, results, passedCheckpointCounts) {
|
|
926
|
+
validateResultFacts(rules, entrantIds, results);
|
|
927
|
+
const factsByEntrant = /* @__PURE__ */ new Map();
|
|
928
|
+
for (const entrantId of entrantIds) factsByEntrant.set(entrantId, []);
|
|
929
|
+
for (const result of results) factsByEntrant.get(result.entrantId).push(result);
|
|
930
|
+
const unranked = entrantIds.map((entrantId) => {
|
|
931
|
+
const entrantFacts = factsByEntrant.get(entrantId);
|
|
932
|
+
let solved = 0;
|
|
933
|
+
let score = 0;
|
|
934
|
+
let penaltyMinutes = 0;
|
|
935
|
+
let fullyPassedCases = 0;
|
|
936
|
+
let deterministicCost = 0;
|
|
937
|
+
let peakMemoryBytes = 0;
|
|
938
|
+
let achievedAtSeconds = 0;
|
|
939
|
+
if (rules.scoring.kind === "icpc") {
|
|
940
|
+
const penalized = new Set(rules.scoring.penalizedVerdicts);
|
|
941
|
+
for (const problem of rules.problems) {
|
|
942
|
+
const chronological = entrantFacts.filter((fact) => fact.problemSlug === problem.slug && fact.eligible).sort((left, right) => left.logicalSeconds - right.logicalSeconds);
|
|
943
|
+
let wrongAttempts = 0;
|
|
944
|
+
for (const fact of chronological) {
|
|
945
|
+
if (fact.score === 100) {
|
|
946
|
+
solved += 1;
|
|
947
|
+
score += problem.points;
|
|
948
|
+
penaltyMinutes += Math.floor(fact.logicalSeconds / 60) + wrongAttempts * rules.scoring.wrongAttemptPenaltyMinutes;
|
|
949
|
+
fullyPassedCases += fact.fullyPassedCases;
|
|
950
|
+
deterministicCost += fact.deterministicCost;
|
|
951
|
+
peakMemoryBytes = Math.max(peakMemoryBytes, fact.peakMemoryBytes);
|
|
952
|
+
achievedAtSeconds = Math.max(achievedAtSeconds, fact.logicalSeconds);
|
|
953
|
+
break;
|
|
954
|
+
}
|
|
955
|
+
if (penalized.has(fact.verdict)) wrongAttempts += 1;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
} else {
|
|
959
|
+
const tieBreaks = rules.scoring.tieBreaks;
|
|
960
|
+
for (const problem of rules.problems) {
|
|
961
|
+
const selected = bestScoreFact(entrantFacts.filter((fact) => fact.problemSlug === problem.slug), tieBreaks);
|
|
962
|
+
if (!selected) continue;
|
|
963
|
+
score += problem.points * selected.score / 100;
|
|
964
|
+
if (selected.score === 100) solved += 1;
|
|
965
|
+
fullyPassedCases += selected.fullyPassedCases;
|
|
966
|
+
deterministicCost += selected.deterministicCost;
|
|
967
|
+
peakMemoryBytes = Math.max(peakMemoryBytes, selected.peakMemoryBytes);
|
|
968
|
+
achievedAtSeconds = Math.max(achievedAtSeconds, selected.logicalSeconds);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
const furthestCheckpoint = passedCheckpointCounts?.[entrantId] ?? 0;
|
|
972
|
+
integer(furthestCheckpoint, `passed checkpoint count for '${entrantId}'`, 0, rules.checkpoints.length);
|
|
973
|
+
return {
|
|
974
|
+
entrantId,
|
|
975
|
+
rank: 0,
|
|
976
|
+
solved,
|
|
977
|
+
score,
|
|
978
|
+
penaltyMinutes,
|
|
979
|
+
furthestCheckpoint,
|
|
980
|
+
fullyPassedCases,
|
|
981
|
+
deterministicCost,
|
|
982
|
+
peakMemoryBytes,
|
|
983
|
+
achievedAtSeconds
|
|
984
|
+
};
|
|
985
|
+
});
|
|
986
|
+
unranked.sort((left, right) => compareCompetitiveStandings(rules.scoring, left, right) || left.entrantId.localeCompare(right.entrantId));
|
|
987
|
+
let rank = 1;
|
|
988
|
+
return unranked.map((standing, index) => {
|
|
989
|
+
if (index > 0 && compareCompetitiveStandings(rules.scoring, unranked[index - 1], standing) !== 0) rank = index + 1;
|
|
990
|
+
return {
|
|
991
|
+
...standing,
|
|
992
|
+
rank
|
|
993
|
+
};
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
function evaluateContestCheckpoint(rules, checkpoint, standings, candidates) {
|
|
997
|
+
if (rules.clock.kind === "individual" && checkpoint.ranking !== null) throw new TypeError("individual checkpoints cannot rank entrants.");
|
|
998
|
+
if (checkpoint.settlement === "pause-until-terminal" && candidates.some((candidate) => candidate.pending)) throw new TypeError("pause-until-terminal checkpoint cannot settle while bounded work is pending.");
|
|
999
|
+
const standingByEntrant = new Map(standings.map((standing) => [standing.entrantId, standing]));
|
|
1000
|
+
if (standingByEntrant.size !== standings.length) throw new TypeError("checkpoint standings contain a duplicate entrant.");
|
|
1001
|
+
const candidateIds = /* @__PURE__ */ new Set();
|
|
1002
|
+
for (const candidate of candidates) {
|
|
1003
|
+
if (!candidate.entrantId || candidateIds.has(candidate.entrantId)) throw new TypeError("checkpoint candidates must have unique entrant ids.");
|
|
1004
|
+
candidateIds.add(candidate.entrantId);
|
|
1005
|
+
if (!standingByEntrant.has(candidate.entrantId)) throw new TypeError(`checkpoint candidate '${candidate.entrantId}' has no standing.`);
|
|
1006
|
+
integer(candidate.solved, `checkpoint candidate '${candidate.entrantId}' solved`, 0, 100);
|
|
1007
|
+
finiteNumber(candidate.score, `checkpoint candidate '${candidate.entrantId}' score`, 0);
|
|
1008
|
+
if (typeof candidate.pending !== "boolean") throw new TypeError("checkpoint candidate pending must be boolean.");
|
|
1009
|
+
}
|
|
1010
|
+
const seats = checkpoint.ranking === null ? candidates.length : checkpoint.ranking.kind === "top-k" ? Math.min(candidates.length, checkpoint.ranking.count) : Math.ceil(candidates.length * checkpoint.ranking.percent / 100);
|
|
1011
|
+
return candidates.map((candidate) => {
|
|
1012
|
+
const thresholdPassed = (checkpoint.threshold.minimumSolved === null || candidate.solved >= checkpoint.threshold.minimumSolved) && (checkpoint.threshold.minimumScore === null || candidate.score >= checkpoint.threshold.minimumScore);
|
|
1013
|
+
const rankingPassed = checkpoint.ranking === null || standingByEntrant.get(candidate.entrantId).rank <= seats;
|
|
1014
|
+
const provisional = checkpoint.settlement === "provisional" && candidate.pending;
|
|
1015
|
+
return {
|
|
1016
|
+
entrantId: candidate.entrantId,
|
|
1017
|
+
advances: provisional || thresholdPassed && rankingPassed,
|
|
1018
|
+
provisional
|
|
1019
|
+
};
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
Object.freeze({
|
|
1023
|
+
project: projectContestRules,
|
|
1024
|
+
admission: decideContestAdmission,
|
|
1025
|
+
rank: rankContestResults,
|
|
1026
|
+
checkpoint: evaluateContestCheckpoint
|
|
1027
|
+
});
|
|
1028
|
+
//#endregion
|
|
1029
|
+
//#region src/core/resources.ts
|
|
1030
|
+
var MAX_LOGICAL_TIME_LIMIT_MS = Math.floor(Number.MAX_SAFE_INTEGER / 1e6);
|
|
1031
|
+
Object.freeze({
|
|
1032
|
+
instructionBudget: 1e10,
|
|
1033
|
+
logicalTimeLimitMs: 6e4,
|
|
1034
|
+
memoryLimitBytes: 268435456,
|
|
1035
|
+
outputLimitBytes: 4194304,
|
|
1036
|
+
filesystemWriteLimitBytes: 67108864,
|
|
1037
|
+
filesystemEntryLimit: 4096,
|
|
1038
|
+
wallTimeLimitMs: 6e4
|
|
1039
|
+
});
|
|
1040
|
+
//#endregion
|
|
1041
|
+
//#region src/judge/problem-model.ts
|
|
1042
|
+
var PROBLEM_LOCALES = ["zh-TW", "en"];
|
|
1043
|
+
//#endregion
|
|
1044
|
+
//#region src/judge/problem-catalog-loader.ts
|
|
1045
|
+
var BROWSER_PROBLEM_SCHEMA$2 = "wasm-oj-browser-problem-v4";
|
|
1046
|
+
var PROBLEM_STARTER_LIMITS = Object.freeze({
|
|
1047
|
+
filesPerLanguage: 128,
|
|
1048
|
+
bytesPerFile: 262144,
|
|
1049
|
+
totalBytesPerLanguage: 1048576
|
|
1050
|
+
});
|
|
1051
|
+
Object.freeze({
|
|
1052
|
+
provider: "github",
|
|
1053
|
+
owner: "wasm-oj",
|
|
1054
|
+
repository: "problems",
|
|
1055
|
+
ref: "main",
|
|
1056
|
+
indexPath: "collection/index.json"
|
|
1057
|
+
});
|
|
1058
|
+
var LANGUAGES = [
|
|
1059
|
+
"c",
|
|
1060
|
+
"cpp",
|
|
1061
|
+
"rust",
|
|
1062
|
+
"go",
|
|
1063
|
+
"python",
|
|
1064
|
+
"javascript",
|
|
1065
|
+
"typescript"
|
|
1066
|
+
];
|
|
1067
|
+
var POLICY_IDS = [
|
|
1068
|
+
"baseline",
|
|
1069
|
+
"efficient",
|
|
1070
|
+
"optimal"
|
|
1071
|
+
];
|
|
1072
|
+
var CALIBRATION_METHOD = "wasm-oj-v2/compiled-average-optimal-rounded/v1";
|
|
1073
|
+
var ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
1074
|
+
var UTF8_ENCODER = new TextEncoder();
|
|
1075
|
+
var ProblemCollectionError = class extends Error {
|
|
1076
|
+
kind;
|
|
1077
|
+
constructor(message, kind, options) {
|
|
1078
|
+
super(message, options);
|
|
1079
|
+
this.kind = kind;
|
|
1080
|
+
this.name = "ProblemCollectionError";
|
|
1081
|
+
}
|
|
1082
|
+
};
|
|
1083
|
+
function parseStandaloneProblemBundle$2(value) {
|
|
1084
|
+
if (!isRecord(value) || !hasExactKeys(value, ["schema", "problem"]) || value.schema !== "wasm-oj-browser-problem-v4") throw schemaError("The problem bundle uses an unsupported schema.");
|
|
1085
|
+
return parseJudgeProblem(value.problem);
|
|
1086
|
+
}
|
|
1087
|
+
function parseJudgeProblem(value) {
|
|
1088
|
+
if (!isRecord(value) || !hasExactKeys(value, [
|
|
1089
|
+
"id",
|
|
1090
|
+
"number",
|
|
1091
|
+
"title",
|
|
1092
|
+
"trackId",
|
|
1093
|
+
"track",
|
|
1094
|
+
"difficulty",
|
|
1095
|
+
"tags",
|
|
1096
|
+
"statement",
|
|
1097
|
+
"editorial",
|
|
1098
|
+
"starterTemplates",
|
|
1099
|
+
"judgeCases",
|
|
1100
|
+
"scoring",
|
|
1101
|
+
"complexities"
|
|
1102
|
+
])) throw schemaError("The judge problem has an invalid shape.");
|
|
1103
|
+
if (typeof value.id !== "string" || !ID_PATTERN.test(value.id) || !Number.isSafeInteger(value.number) || value.number < 1) throw schemaError("The judge problem has an invalid identity.");
|
|
1104
|
+
const id = value.id;
|
|
1105
|
+
const title = parseLocalizedText(value.title, `problem '${id}' title`);
|
|
1106
|
+
if (typeof value.trackId !== "string" || !ID_PATTERN.test(value.trackId)) throw schemaError(`Problem '${id}' has an invalid track ID.`);
|
|
1107
|
+
const track = parseLocalizedText(value.track, `problem '${id}' track`);
|
|
1108
|
+
const statement = parseLocalizedText(value.statement, `problem '${id}' statement`, false);
|
|
1109
|
+
const editorial = parseLocalizedText(value.editorial, `problem '${id}' editorial`, false);
|
|
1110
|
+
const starterTemplates = parseStarterTemplates(value.starterTemplates, id);
|
|
1111
|
+
const difficulty = parseDifficulty(value.difficulty, `problem '${id}'`);
|
|
1112
|
+
const tags = parseTags(value.tags, `problem '${id}'`);
|
|
1113
|
+
if (!Array.isArray(value.judgeCases) || value.judgeCases.length < 1 || value.judgeCases.length > 1e4) throw schemaError(`Problem '${value.id}' has an invalid case inventory.`);
|
|
1114
|
+
const caseIds = /* @__PURE__ */ new Set();
|
|
1115
|
+
const judgeCases = value.judgeCases.map((testCase, index) => {
|
|
1116
|
+
if (!isRecord(testCase) || !hasExactKeys(testCase, [
|
|
1117
|
+
"id",
|
|
1118
|
+
"kind",
|
|
1119
|
+
"input",
|
|
1120
|
+
"output"
|
|
1121
|
+
]) || typeof testCase.id !== "string" || !ID_PATTERN.test(testCase.id) || caseIds.has(testCase.id) || ![
|
|
1122
|
+
"sample",
|
|
1123
|
+
"adversarial",
|
|
1124
|
+
"regression"
|
|
1125
|
+
].includes(String(testCase.kind)) || typeof testCase.input !== "string" || typeof testCase.output !== "string") throw schemaError(`Problem '${value.id}' has an invalid case at position ${index + 1}.`);
|
|
1126
|
+
caseIds.add(testCase.id);
|
|
1127
|
+
return {
|
|
1128
|
+
id: testCase.id,
|
|
1129
|
+
kind: testCase.kind,
|
|
1130
|
+
input: testCase.input,
|
|
1131
|
+
output: testCase.output
|
|
1132
|
+
};
|
|
1133
|
+
});
|
|
1134
|
+
if (judgeCases.filter((testCase) => testCase.kind === "sample").length !== 3) throw schemaError(`Problem '${value.id}' must contain exactly three sample cases.`);
|
|
1135
|
+
const scoring = parseScoring(value.scoring, id);
|
|
1136
|
+
if (!Array.isArray(value.complexities) || value.complexities.length < 2 || value.complexities.length > 32) throw schemaError(`Problem '${value.id}' has an invalid complexity inventory.`);
|
|
1137
|
+
const complexities = value.complexities.map((complexity, index) => parseComplexity(complexity, id, index));
|
|
1138
|
+
if (!complexities.at(-1)?.accepted) throw schemaError(`Problem '${id}' must end with its accepted complexity.`);
|
|
1139
|
+
return {
|
|
1140
|
+
id,
|
|
1141
|
+
number: value.number,
|
|
1142
|
+
title,
|
|
1143
|
+
trackId: value.trackId,
|
|
1144
|
+
track,
|
|
1145
|
+
difficulty,
|
|
1146
|
+
tags,
|
|
1147
|
+
statement,
|
|
1148
|
+
editorial,
|
|
1149
|
+
starterTemplates,
|
|
1150
|
+
judgeCases,
|
|
1151
|
+
scoring,
|
|
1152
|
+
complexities
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
function parseStarterTemplates(value, problemId) {
|
|
1156
|
+
if (!isRecord(value) || !hasExactKeys(value, LANGUAGES)) throw schemaError(`Problem '${problemId}' must provide starter templates for every built-in language.`);
|
|
1157
|
+
return Object.fromEntries(LANGUAGES.map((language) => [language, parseStarterTemplate(value[language], problemId, language)]));
|
|
1158
|
+
}
|
|
1159
|
+
function parseStarterTemplate(value, problemId, language) {
|
|
1160
|
+
if (!isRecord(value) || !hasExactKeys(value, ["entry", "files"]) || !isRecord(value.files)) throw schemaError(`Problem '${problemId}' has an invalid '${language}' starter template.`);
|
|
1161
|
+
const entry = parseStarterPath(value.entry, `problem '${problemId}' '${language}' starter entry`);
|
|
1162
|
+
const paths = Object.keys(value.files).sort();
|
|
1163
|
+
if (paths.length < 1 || paths.length > PROBLEM_STARTER_LIMITS.filesPerLanguage) throw schemaError(`Problem '${problemId}' '${language}' starter must contain between 1 and ${PROBLEM_STARTER_LIMITS.filesPerLanguage} files.`);
|
|
1164
|
+
let totalBytes = 0;
|
|
1165
|
+
const entries = [];
|
|
1166
|
+
for (const rawPath of paths) {
|
|
1167
|
+
const path = parseStarterPath(rawPath, `problem '${problemId}' '${language}' starter file`);
|
|
1168
|
+
const content = value.files[rawPath];
|
|
1169
|
+
if (typeof content !== "string" || hasUnpairedSurrogate(content)) throw schemaError(`Problem '${problemId}' '${language}' starter file '${path}' is not valid Unicode source text.`);
|
|
1170
|
+
const bytes = UTF8_ENCODER.encode(content).byteLength;
|
|
1171
|
+
if (bytes > PROBLEM_STARTER_LIMITS.bytesPerFile) throw schemaError(`Problem '${problemId}' '${language}' starter file '${path}' exceeds ${PROBLEM_STARTER_LIMITS.bytesPerFile} UTF-8 bytes.`);
|
|
1172
|
+
totalBytes += bytes;
|
|
1173
|
+
if (totalBytes > PROBLEM_STARTER_LIMITS.totalBytesPerLanguage) throw schemaError(`Problem '${problemId}' '${language}' starter files exceed ${PROBLEM_STARTER_LIMITS.totalBytesPerLanguage} UTF-8 bytes in total.`);
|
|
1174
|
+
entries.push([path, content]);
|
|
1175
|
+
}
|
|
1176
|
+
const files = Object.fromEntries(entries);
|
|
1177
|
+
if (!Object.hasOwn(files, entry) || UTF8_ENCODER.encode(files[entry]).byteLength === 0) throw schemaError(`Problem '${problemId}' '${language}' starter entry must name a non-empty file in its file map.`);
|
|
1178
|
+
return {
|
|
1179
|
+
entry,
|
|
1180
|
+
files
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
function parseStarterPath(value, label) {
|
|
1184
|
+
if (typeof value !== "string" || !value || value !== value.trim() || value.length > 4096 || value.startsWith("/") || value.endsWith("/") || value.includes("\\") || value.includes("\0") || value.split("/").some((segment) => !segment || segment === "." || segment === "..")) throw schemaError(`The ${label} is not a normalized relative path.`);
|
|
1185
|
+
return value;
|
|
1186
|
+
}
|
|
1187
|
+
function hasUnpairedSurrogate(value) {
|
|
1188
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
1189
|
+
const unit = value.charCodeAt(index);
|
|
1190
|
+
if (unit >= 55296 && unit <= 56319) {
|
|
1191
|
+
const next = value.charCodeAt(index + 1);
|
|
1192
|
+
if (!(next >= 56320 && next <= 57343)) return true;
|
|
1193
|
+
index += 1;
|
|
1194
|
+
} else if (unit >= 56320 && unit <= 57343) return true;
|
|
1195
|
+
}
|
|
1196
|
+
return false;
|
|
1197
|
+
}
|
|
1198
|
+
function parseScoring(value, problemId) {
|
|
1199
|
+
if (!isRecord(value) || !hasExactKeys(value, [
|
|
1200
|
+
"maximumPoints",
|
|
1201
|
+
"calibration",
|
|
1202
|
+
"policies",
|
|
1203
|
+
"safetyLimits"
|
|
1204
|
+
]) || value.maximumPoints !== 100) throw schemaError(`Problem '${problemId}' has invalid scoring metadata.`);
|
|
1205
|
+
const calibration = value.calibration;
|
|
1206
|
+
if (!isRecord(calibration) || !hasExactKeys(calibration, ["method", "profiles"]) || calibration.method !== CALIBRATION_METHOD) throw schemaError(`Problem '${problemId}' has invalid calibration metadata.`);
|
|
1207
|
+
const profiles = calibration.profiles;
|
|
1208
|
+
if (!isRecord(profiles) || !hasExactKeys(profiles, LANGUAGES) || LANGUAGES.some((language) => typeof profiles[language] !== "string" || !profiles[language] || profiles[language] !== profiles[language].trim())) throw schemaError(`Problem '${problemId}' has invalid calibration profiles.`);
|
|
1209
|
+
if (!Array.isArray(value.policies) || value.policies.length !== POLICY_IDS.length) throw schemaError(`Problem '${problemId}' has invalid scoring policies.`);
|
|
1210
|
+
const policies = value.policies.map((policy, index) => parsePolicy(policy, problemId, POLICY_IDS[index]));
|
|
1211
|
+
if (policies.reduce((total, policy) => total + policy.points, 0) !== 100) throw schemaError(`Problem '${problemId}' policy points must sum to 100.`);
|
|
1212
|
+
for (let index = 1; index < policies.length; index += 1) {
|
|
1213
|
+
const broad = policies[index - 1].limits;
|
|
1214
|
+
const strict = policies[index].limits;
|
|
1215
|
+
const logicalInvalid = broad.logicalTimeLimitMs !== void 0 && (strict.logicalTimeLimitMs === void 0 || strict.logicalTimeLimitMs > broad.logicalTimeLimitMs);
|
|
1216
|
+
const anyStricter = strict.instructionBudget < broad.instructionBudget || strict.memoryLimitBytes < broad.memoryLimitBytes || broad.logicalTimeLimitMs === void 0 && strict.logicalTimeLimitMs !== void 0 || broad.logicalTimeLimitMs !== void 0 && strict.logicalTimeLimitMs !== void 0 && strict.logicalTimeLimitMs < broad.logicalTimeLimitMs;
|
|
1217
|
+
if (strict.instructionBudget > broad.instructionBudget || strict.memoryLimitBytes > broad.memoryLimitBytes || logicalInvalid || !anyStricter) throw schemaError(`Problem '${problemId}' policies are not broad-to-strict.`);
|
|
1218
|
+
}
|
|
1219
|
+
if (!isRecord(value.safetyLimits) || !hasExactKeys(value.safetyLimits, ["wallTimeLimitMs"]) || !isPositiveSafeInteger(value.safetyLimits.wallTimeLimitMs) || value.safetyLimits.wallTimeLimitMs > 6e5) throw schemaError(`Problem '${problemId}' has invalid safety limits.`);
|
|
1220
|
+
return {
|
|
1221
|
+
maximumPoints: 100,
|
|
1222
|
+
calibration: {
|
|
1223
|
+
method: CALIBRATION_METHOD,
|
|
1224
|
+
profiles
|
|
1225
|
+
},
|
|
1226
|
+
policies,
|
|
1227
|
+
safetyLimits: { wallTimeLimitMs: value.safetyLimits.wallTimeLimitMs }
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
function parsePolicy(value, problemId, expectedId) {
|
|
1231
|
+
if (!isRecord(value) || !hasExactKeys(value, [
|
|
1232
|
+
"id",
|
|
1233
|
+
"title",
|
|
1234
|
+
"points",
|
|
1235
|
+
"limits"
|
|
1236
|
+
]) || value.id !== expectedId || !isPositiveSafeInteger(value.points) || !isRecord(value.limits)) throw schemaError(`Problem '${problemId}' has an invalid '${expectedId}' policy.`);
|
|
1237
|
+
const keys = Object.keys(value.limits).sort();
|
|
1238
|
+
if (JSON.stringify(keys) !== JSON.stringify(keys.includes("logicalTimeLimitMs") ? [
|
|
1239
|
+
"instructionBudget",
|
|
1240
|
+
"logicalTimeLimitMs",
|
|
1241
|
+
"memoryLimitBytes"
|
|
1242
|
+
] : ["instructionBudget", "memoryLimitBytes"])) throw schemaError(`Problem '${problemId}' policy '${expectedId}' has invalid limits.`);
|
|
1243
|
+
if (!isPositiveSafeInteger(value.limits.instructionBudget) || !isPositiveSafeInteger(value.limits.memoryLimitBytes) || value.limits.memoryLimitBytes < 65536 || value.limits.memoryLimitBytes > 4294967296 || value.limits.memoryLimitBytes % 65536 !== 0 || value.limits.logicalTimeLimitMs !== void 0 && (!isPositiveSafeInteger(value.limits.logicalTimeLimitMs) || value.limits.logicalTimeLimitMs > MAX_LOGICAL_TIME_LIMIT_MS)) throw schemaError(`Problem '${problemId}' policy '${expectedId}' has invalid resource values.`);
|
|
1244
|
+
const limits = {
|
|
1245
|
+
instructionBudget: value.limits.instructionBudget,
|
|
1246
|
+
memoryLimitBytes: value.limits.memoryLimitBytes,
|
|
1247
|
+
...value.limits.logicalTimeLimitMs === void 0 ? {} : { logicalTimeLimitMs: value.limits.logicalTimeLimitMs }
|
|
1248
|
+
};
|
|
1249
|
+
return {
|
|
1250
|
+
id: expectedId,
|
|
1251
|
+
title: parseLocalizedText(value.title, `problem '${problemId}' policy '${expectedId}' title`),
|
|
1252
|
+
points: value.points,
|
|
1253
|
+
limits
|
|
1254
|
+
};
|
|
1255
|
+
}
|
|
1256
|
+
function parseComplexity(value, problemId, index) {
|
|
1257
|
+
if (!isRecord(value) || !hasExactKeys(value, [
|
|
1258
|
+
"name",
|
|
1259
|
+
"time",
|
|
1260
|
+
"space",
|
|
1261
|
+
"accepted"
|
|
1262
|
+
]) || typeof value.time !== "string" || !value.time || typeof value.space !== "string" || !value.space || typeof value.accepted !== "boolean") throw schemaError(`Problem '${problemId}' has an invalid complexity at position ${index + 1}.`);
|
|
1263
|
+
return {
|
|
1264
|
+
name: parseLocalizedText(value.name, `problem '${problemId}' complexity name`),
|
|
1265
|
+
time: value.time,
|
|
1266
|
+
space: value.space,
|
|
1267
|
+
accepted: value.accepted
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
1270
|
+
function parseLocalizedText(value, label, trimmed = true) {
|
|
1271
|
+
if (!isRecord(value) || !hasExactKeys(value, PROBLEM_LOCALES)) throw schemaError(`${label} is invalid.`);
|
|
1272
|
+
for (const locale of PROBLEM_LOCALES) if (typeof value[locale] !== "string" || !value[locale] || trimmed && value[locale] !== value[locale].trim()) throw schemaError(`${label}[${locale}] is invalid.`);
|
|
1273
|
+
return value;
|
|
1274
|
+
}
|
|
1275
|
+
function parseDifficulty(value, label) {
|
|
1276
|
+
if (value !== "easy" && value !== "medium" && value !== "hard") throw schemaError(`${label} has an invalid difficulty.`);
|
|
1277
|
+
return value;
|
|
1278
|
+
}
|
|
1279
|
+
function parseTags(value, label) {
|
|
1280
|
+
if (!Array.isArray(value) || value.length < 1 || value.length > 32 || value.some((tag) => typeof tag !== "string" || !tag || tag !== tag.trim()) || new Set(value).size !== value.length) throw schemaError(`${label} has invalid tags.`);
|
|
1281
|
+
return value;
|
|
1282
|
+
}
|
|
1283
|
+
function hasExactKeys(value, expected) {
|
|
1284
|
+
const actual = Object.keys(value).sort();
|
|
1285
|
+
const expectedKeys = [...expected].sort();
|
|
1286
|
+
return actual.length === expectedKeys.length && actual.every((key, index) => key === expectedKeys[index]);
|
|
1287
|
+
}
|
|
1288
|
+
function isRecord(value) {
|
|
1289
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1290
|
+
}
|
|
1291
|
+
function isPositiveSafeInteger(value) {
|
|
1292
|
+
return Number.isSafeInteger(value) && value > 0;
|
|
1293
|
+
}
|
|
1294
|
+
function schemaError(message) {
|
|
1295
|
+
return new ProblemCollectionError(message, "schema");
|
|
1296
|
+
}
|
|
1297
|
+
//#endregion
|
|
1298
|
+
//#region src/online-judge/public-projection.ts
|
|
1299
|
+
/** Parses the redacted contest object. Its repository descriptor supplies the content identity. */
|
|
1300
|
+
function parseContestPublicProblemProjection(value) {
|
|
1301
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new TypeError("Contest-public projection must be an object.");
|
|
1302
|
+
const record = value;
|
|
1303
|
+
if (Object.keys(record).sort().join("\0") !== "problem\0schema" || record.schema !== "wasm-oj-platform/contest-public-problem-projection/v1") throw new TypeError("Contest-public projection has an invalid shape or schema.");
|
|
1304
|
+
if (!record.problem || typeof record.problem !== "object" || Array.isArray(record.problem)) throw new TypeError("Contest-public projection contains an invalid problem.");
|
|
1305
|
+
const rawProblem = record.problem;
|
|
1306
|
+
const editorial = rawProblem.editorial;
|
|
1307
|
+
if (!editorial || typeof editorial !== "object" || Array.isArray(editorial) || Object.keys(editorial).sort().join("\0") !== "en\0zh-TW" || editorial["zh-TW"] !== "" || editorial.en !== "") throw new TypeError("Contest-public projection contains non-public problem data.");
|
|
1308
|
+
const parsed = parseStandaloneProblemBundle$2({
|
|
1309
|
+
schema: BROWSER_PROBLEM_SCHEMA$2,
|
|
1310
|
+
problem: {
|
|
1311
|
+
...rawProblem,
|
|
1312
|
+
editorial: {
|
|
1313
|
+
"zh-TW": "redacted",
|
|
1314
|
+
en: "redacted"
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
});
|
|
1318
|
+
if (parsed.judgeCases.some((testCase) => testCase.kind !== "sample")) throw new TypeError("Contest-public projection contains non-public problem data.");
|
|
1319
|
+
return {
|
|
1320
|
+
schema: CONTEST_PUBLIC_PROJECTION_SCHEMA,
|
|
1321
|
+
problem: {
|
|
1322
|
+
...parsed,
|
|
1323
|
+
editorial: {
|
|
1324
|
+
"zh-TW": "",
|
|
1325
|
+
en: ""
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1330
|
+
//#endregion
|
|
1331
|
+
//#region src/online-judge/compile-profiles.ts
|
|
1332
|
+
function record$3(value, label) {
|
|
1333
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new TypeError(`${label} must be an object.`);
|
|
1334
|
+
return value;
|
|
1335
|
+
}
|
|
1336
|
+
function parseJudgeAllowedProfiles(value, label = "allowedProfiles") {
|
|
1337
|
+
const profiles = record$3(value, label);
|
|
1338
|
+
const entries = Object.entries(profiles).sort(([left], [right]) => left.localeCompare(right));
|
|
1339
|
+
if (entries.length < 1) throw new TypeError(`${label} must contain at least one compile profile.`);
|
|
1340
|
+
const result = {};
|
|
1341
|
+
for (const [language, candidate] of entries) {
|
|
1342
|
+
if (!isBuiltinLanguage(language)) throw new TypeError(`${label} language '${language}' is unsupported.`);
|
|
1343
|
+
const profile = record$3(candidate, `${label}.${language}`);
|
|
1344
|
+
if (JSON.stringify(Object.keys(profile).sort()) !== JSON.stringify(["optimization", "target"])) throw new TypeError(`${label}.${language} has an invalid shape.`);
|
|
1345
|
+
if (profile.target !== "wasip1" && profile.target !== "wasix") throw new TypeError(`${label}.${language}.target is unsupported.`);
|
|
1346
|
+
if (profile.optimization !== "debug" && profile.optimization !== "release") throw new TypeError(`${label}.${language}.optimization is unsupported.`);
|
|
1347
|
+
result[language] = {
|
|
1348
|
+
target: profile.target,
|
|
1349
|
+
optimization: profile.optimization
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
1352
|
+
return result;
|
|
1353
|
+
}
|
|
1354
|
+
//#endregion
|
|
1355
|
+
//#region src/online-judge/trusted-judge-wasm.ts
|
|
1356
|
+
var TRUSTED_JUDGE_WASM_MAX_BYTES = 8388608;
|
|
1357
|
+
var I32 = 127;
|
|
1358
|
+
var I64 = 126;
|
|
1359
|
+
var errno = (parameters) => ({
|
|
1360
|
+
parameters,
|
|
1361
|
+
results: [I32]
|
|
1362
|
+
});
|
|
1363
|
+
Object.freeze({
|
|
1364
|
+
args_get: errno([I32, I32]),
|
|
1365
|
+
args_sizes_get: errno([I32, I32]),
|
|
1366
|
+
clock_res_get: errno([I32, I32]),
|
|
1367
|
+
clock_time_get: errno([
|
|
1368
|
+
I32,
|
|
1369
|
+
I64,
|
|
1370
|
+
I32
|
|
1371
|
+
]),
|
|
1372
|
+
environ_get: errno([I32, I32]),
|
|
1373
|
+
environ_sizes_get: errno([I32, I32]),
|
|
1374
|
+
fd_advise: errno([
|
|
1375
|
+
I32,
|
|
1376
|
+
I64,
|
|
1377
|
+
I64,
|
|
1378
|
+
I32
|
|
1379
|
+
]),
|
|
1380
|
+
fd_allocate: errno([
|
|
1381
|
+
I32,
|
|
1382
|
+
I64,
|
|
1383
|
+
I64
|
|
1384
|
+
]),
|
|
1385
|
+
fd_close: errno([I32]),
|
|
1386
|
+
fd_datasync: errno([I32]),
|
|
1387
|
+
fd_fdstat_get: errno([I32, I32]),
|
|
1388
|
+
fd_fdstat_set_flags: errno([I32, I32]),
|
|
1389
|
+
fd_fdstat_set_rights: errno([
|
|
1390
|
+
I32,
|
|
1391
|
+
I64,
|
|
1392
|
+
I64
|
|
1393
|
+
]),
|
|
1394
|
+
fd_filestat_get: errno([I32, I32]),
|
|
1395
|
+
fd_filestat_set_size: errno([I32, I64]),
|
|
1396
|
+
fd_filestat_set_times: errno([
|
|
1397
|
+
I32,
|
|
1398
|
+
I64,
|
|
1399
|
+
I64,
|
|
1400
|
+
I32
|
|
1401
|
+
]),
|
|
1402
|
+
fd_pread: errno([
|
|
1403
|
+
I32,
|
|
1404
|
+
I32,
|
|
1405
|
+
I32,
|
|
1406
|
+
I64,
|
|
1407
|
+
I32
|
|
1408
|
+
]),
|
|
1409
|
+
fd_prestat_dir_name: errno([
|
|
1410
|
+
I32,
|
|
1411
|
+
I32,
|
|
1412
|
+
I32
|
|
1413
|
+
]),
|
|
1414
|
+
fd_prestat_get: errno([I32, I32]),
|
|
1415
|
+
fd_pwrite: errno([
|
|
1416
|
+
I32,
|
|
1417
|
+
I32,
|
|
1418
|
+
I32,
|
|
1419
|
+
I64,
|
|
1420
|
+
I32
|
|
1421
|
+
]),
|
|
1422
|
+
fd_read: errno([
|
|
1423
|
+
I32,
|
|
1424
|
+
I32,
|
|
1425
|
+
I32,
|
|
1426
|
+
I32
|
|
1427
|
+
]),
|
|
1428
|
+
fd_readdir: errno([
|
|
1429
|
+
I32,
|
|
1430
|
+
I32,
|
|
1431
|
+
I32,
|
|
1432
|
+
I64,
|
|
1433
|
+
I32
|
|
1434
|
+
]),
|
|
1435
|
+
fd_renumber: errno([I32, I32]),
|
|
1436
|
+
fd_seek: errno([
|
|
1437
|
+
I32,
|
|
1438
|
+
I64,
|
|
1439
|
+
I32,
|
|
1440
|
+
I32
|
|
1441
|
+
]),
|
|
1442
|
+
fd_sync: errno([I32]),
|
|
1443
|
+
fd_tell: errno([I32, I32]),
|
|
1444
|
+
fd_write: errno([
|
|
1445
|
+
I32,
|
|
1446
|
+
I32,
|
|
1447
|
+
I32,
|
|
1448
|
+
I32
|
|
1449
|
+
]),
|
|
1450
|
+
path_create_directory: errno([
|
|
1451
|
+
I32,
|
|
1452
|
+
I32,
|
|
1453
|
+
I32
|
|
1454
|
+
]),
|
|
1455
|
+
path_filestat_get: errno([
|
|
1456
|
+
I32,
|
|
1457
|
+
I32,
|
|
1458
|
+
I32,
|
|
1459
|
+
I32,
|
|
1460
|
+
I32
|
|
1461
|
+
]),
|
|
1462
|
+
path_filestat_set_times: errno([
|
|
1463
|
+
I32,
|
|
1464
|
+
I32,
|
|
1465
|
+
I32,
|
|
1466
|
+
I32,
|
|
1467
|
+
I64,
|
|
1468
|
+
I64,
|
|
1469
|
+
I32
|
|
1470
|
+
]),
|
|
1471
|
+
path_link: errno([
|
|
1472
|
+
I32,
|
|
1473
|
+
I32,
|
|
1474
|
+
I32,
|
|
1475
|
+
I32,
|
|
1476
|
+
I32,
|
|
1477
|
+
I32,
|
|
1478
|
+
I32
|
|
1479
|
+
]),
|
|
1480
|
+
path_open: errno([
|
|
1481
|
+
I32,
|
|
1482
|
+
I32,
|
|
1483
|
+
I32,
|
|
1484
|
+
I32,
|
|
1485
|
+
I32,
|
|
1486
|
+
I64,
|
|
1487
|
+
I64,
|
|
1488
|
+
I32,
|
|
1489
|
+
I32
|
|
1490
|
+
]),
|
|
1491
|
+
path_readlink: errno([
|
|
1492
|
+
I32,
|
|
1493
|
+
I32,
|
|
1494
|
+
I32,
|
|
1495
|
+
I32,
|
|
1496
|
+
I32,
|
|
1497
|
+
I32
|
|
1498
|
+
]),
|
|
1499
|
+
path_remove_directory: errno([
|
|
1500
|
+
I32,
|
|
1501
|
+
I32,
|
|
1502
|
+
I32
|
|
1503
|
+
]),
|
|
1504
|
+
path_rename: errno([
|
|
1505
|
+
I32,
|
|
1506
|
+
I32,
|
|
1507
|
+
I32,
|
|
1508
|
+
I32,
|
|
1509
|
+
I32,
|
|
1510
|
+
I32
|
|
1511
|
+
]),
|
|
1512
|
+
path_symlink: errno([
|
|
1513
|
+
I32,
|
|
1514
|
+
I32,
|
|
1515
|
+
I32,
|
|
1516
|
+
I32,
|
|
1517
|
+
I32
|
|
1518
|
+
]),
|
|
1519
|
+
path_unlink_file: errno([
|
|
1520
|
+
I32,
|
|
1521
|
+
I32,
|
|
1522
|
+
I32
|
|
1523
|
+
]),
|
|
1524
|
+
poll_oneoff: errno([
|
|
1525
|
+
I32,
|
|
1526
|
+
I32,
|
|
1527
|
+
I32,
|
|
1528
|
+
I32
|
|
1529
|
+
]),
|
|
1530
|
+
proc_exit: {
|
|
1531
|
+
parameters: [I32],
|
|
1532
|
+
results: []
|
|
1533
|
+
},
|
|
1534
|
+
proc_raise: errno([I32]),
|
|
1535
|
+
random_get: errno([I32, I32]),
|
|
1536
|
+
sched_yield: errno([])
|
|
1537
|
+
});
|
|
1538
|
+
Object.freeze(/* @__PURE__ */ new Set([
|
|
1539
|
+
"args_get",
|
|
1540
|
+
"args_sizes_get",
|
|
1541
|
+
"clock_res_get",
|
|
1542
|
+
"clock_time_get",
|
|
1543
|
+
"environ_get",
|
|
1544
|
+
"environ_sizes_get",
|
|
1545
|
+
"fd_advise",
|
|
1546
|
+
"fd_allocate",
|
|
1547
|
+
"fd_close",
|
|
1548
|
+
"fd_datasync",
|
|
1549
|
+
"fd_fdstat_get",
|
|
1550
|
+
"fd_fdstat_set_flags",
|
|
1551
|
+
"fd_fdstat_set_rights",
|
|
1552
|
+
"fd_filestat_get",
|
|
1553
|
+
"fd_filestat_set_size",
|
|
1554
|
+
"fd_filestat_set_times",
|
|
1555
|
+
"fd_pread",
|
|
1556
|
+
"fd_prestat_dir_name",
|
|
1557
|
+
"fd_prestat_get",
|
|
1558
|
+
"fd_pwrite",
|
|
1559
|
+
"fd_read",
|
|
1560
|
+
"fd_readdir",
|
|
1561
|
+
"fd_renumber",
|
|
1562
|
+
"fd_seek",
|
|
1563
|
+
"fd_sync",
|
|
1564
|
+
"fd_tell",
|
|
1565
|
+
"fd_write",
|
|
1566
|
+
"path_create_directory",
|
|
1567
|
+
"path_filestat_get",
|
|
1568
|
+
"path_filestat_set_times",
|
|
1569
|
+
"path_link",
|
|
1570
|
+
"path_open",
|
|
1571
|
+
"path_readlink",
|
|
1572
|
+
"path_remove_directory",
|
|
1573
|
+
"path_rename",
|
|
1574
|
+
"path_symlink",
|
|
1575
|
+
"path_unlink_file",
|
|
1576
|
+
"poll_oneoff",
|
|
1577
|
+
"proc_exit",
|
|
1578
|
+
"proc_raise",
|
|
1579
|
+
"random_get",
|
|
1580
|
+
"sched_yield"
|
|
1581
|
+
]));
|
|
1582
|
+
var TRUSTED_JUDGE_RUNTIME_PROFILES = Object.freeze(/* @__PURE__ */ new Set([
|
|
1583
|
+
"c-wasip1-release",
|
|
1584
|
+
"cpp-wasip1-release",
|
|
1585
|
+
"rust-wasip1-release",
|
|
1586
|
+
"go-wasip1-release"
|
|
1587
|
+
]));
|
|
1588
|
+
//#endregion
|
|
1589
|
+
//#region src/online-judge/repository-authoring.ts
|
|
1590
|
+
var REPOSITORY_AUTHORING_JUDGES_SCHEMA = "wasm-oj-platform/repository-authoring-judges/v1";
|
|
1591
|
+
var SHA256$1 = /^[0-9a-f]{64}$/;
|
|
1592
|
+
var SLUG$1 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
1593
|
+
var PATH$1 = /^(?!\/)(?!.*(?:^|\/)\.{1,2}(?:\/|$))(?!.*\/\/)(?!.*\\)(?!.*\/$)[^\u0000]+$/;
|
|
1594
|
+
var GUEST_PATH = /^\/(?!.*(?:^|\/)\.{1,2}(?:\/|$))(?!.*\/\/)(?!.*\\)(?!.*\/$)[^\u0000]+$/;
|
|
1595
|
+
var MAX_ASSET_BYTES = 4194304;
|
|
1596
|
+
var MAX_ASSET_TOTAL_BYTES = 4194304;
|
|
1597
|
+
function record$2(value, label) {
|
|
1598
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new TypeError(`${label} must be an object.`);
|
|
1599
|
+
return value;
|
|
1600
|
+
}
|
|
1601
|
+
function exact$2(value, keys, label) {
|
|
1602
|
+
if (Object.keys(value).sort().join("\0") !== [...keys].sort().join("\0")) throw new TypeError(`${label} has an invalid shape.`);
|
|
1603
|
+
}
|
|
1604
|
+
function sourceObject(value, label, maximum) {
|
|
1605
|
+
const object = record$2(value, label);
|
|
1606
|
+
exact$2(object, [
|
|
1607
|
+
"bytes",
|
|
1608
|
+
"path",
|
|
1609
|
+
"sha256"
|
|
1610
|
+
], label);
|
|
1611
|
+
if (typeof object.path !== "string" || object.path.length < 1 || object.path.length > 512 || !PATH$1.test(object.path)) throw new TypeError(`${label}.path must be a normalized repository-relative POSIX path.`);
|
|
1612
|
+
if (!Number.isSafeInteger(object.bytes) || object.bytes < 1 || object.bytes > maximum) throw new TypeError(`${label}.bytes is outside its limit.`);
|
|
1613
|
+
if (typeof object.sha256 !== "string" || !SHA256$1.test(object.sha256)) throw new TypeError(`${label}.sha256 is invalid.`);
|
|
1614
|
+
return {
|
|
1615
|
+
path: object.path,
|
|
1616
|
+
bytes: object.bytes,
|
|
1617
|
+
sha256: object.sha256
|
|
1618
|
+
};
|
|
1619
|
+
}
|
|
1620
|
+
function judge(value, slug) {
|
|
1621
|
+
const input = record$2(value, `repository authoring '${slug}' judge`);
|
|
1622
|
+
if (input.kind === "text") {
|
|
1623
|
+
exact$2(input, ["kind"], `repository authoring '${slug}' text judge`);
|
|
1624
|
+
return { kind: "text" };
|
|
1625
|
+
}
|
|
1626
|
+
if (input.kind !== "checker" && input.kind !== "interactive") throw new TypeError(`Repository authoring '${slug}' judge kind is unsupported.`);
|
|
1627
|
+
exact$2(input, input.kind === "checker" ? [
|
|
1628
|
+
"args",
|
|
1629
|
+
"artifact",
|
|
1630
|
+
"assets",
|
|
1631
|
+
"kind"
|
|
1632
|
+
] : [
|
|
1633
|
+
"args",
|
|
1634
|
+
"artifact",
|
|
1635
|
+
"assets",
|
|
1636
|
+
"inputPath",
|
|
1637
|
+
"kind"
|
|
1638
|
+
], `repository authoring '${slug}' ${input.kind}`);
|
|
1639
|
+
const artifactValue = record$2(input.artifact, `repository authoring '${slug}' ${input.kind} artifact`);
|
|
1640
|
+
exact$2(artifactValue, [
|
|
1641
|
+
"bytes",
|
|
1642
|
+
"path",
|
|
1643
|
+
"runtimeProfile",
|
|
1644
|
+
"sha256"
|
|
1645
|
+
], `repository authoring '${slug}' ${input.kind} artifact`);
|
|
1646
|
+
if (typeof artifactValue.runtimeProfile !== "string" || !TRUSTED_JUDGE_RUNTIME_PROFILES.has(artifactValue.runtimeProfile)) throw new TypeError(`Repository authoring '${slug}' ${input.kind} runtimeProfile is unsupported.`);
|
|
1647
|
+
const artifact = {
|
|
1648
|
+
...sourceObject({
|
|
1649
|
+
bytes: artifactValue.bytes,
|
|
1650
|
+
path: artifactValue.path,
|
|
1651
|
+
sha256: artifactValue.sha256
|
|
1652
|
+
}, `repository authoring '${slug}' ${input.kind} artifact`, TRUSTED_JUDGE_WASM_MAX_BYTES),
|
|
1653
|
+
runtimeProfile: artifactValue.runtimeProfile
|
|
1654
|
+
};
|
|
1655
|
+
if (!artifact.path.endsWith(".wasm")) throw new TypeError(`Repository authoring '${slug}' ${input.kind} artifact path must end in '.wasm'.`);
|
|
1656
|
+
if (!Array.isArray(input.assets) || input.assets.length > 256) throw new TypeError(`Repository authoring '${slug}' ${input.kind} assets are invalid.`);
|
|
1657
|
+
const namespace = input.kind === "checker" ? "/checker/assets/" : "/interactor/assets/";
|
|
1658
|
+
const assets = input.assets.map((candidate, index) => {
|
|
1659
|
+
const asset = record$2(candidate, `repository authoring '${slug}' ${input.kind} asset ${index}`);
|
|
1660
|
+
exact$2(asset, [
|
|
1661
|
+
"bytes",
|
|
1662
|
+
"guestPath",
|
|
1663
|
+
"path",
|
|
1664
|
+
"sha256"
|
|
1665
|
+
], `repository authoring '${slug}' ${input.kind} asset ${index}`);
|
|
1666
|
+
if (typeof asset.guestPath !== "string" || !GUEST_PATH.test(asset.guestPath) || !asset.guestPath.startsWith(namespace)) throw new TypeError(`Repository authoring '${slug}' ${input.kind} asset ${index} guestPath must be inside '${namespace}'.`);
|
|
1667
|
+
return {
|
|
1668
|
+
...sourceObject({
|
|
1669
|
+
bytes: asset.bytes,
|
|
1670
|
+
path: asset.path,
|
|
1671
|
+
sha256: asset.sha256
|
|
1672
|
+
}, `repository authoring '${slug}' ${input.kind} asset ${index}`, MAX_ASSET_BYTES),
|
|
1673
|
+
guestPath: asset.guestPath
|
|
1674
|
+
};
|
|
1675
|
+
});
|
|
1676
|
+
const guestPaths = assets.map((asset) => asset.guestPath);
|
|
1677
|
+
const repositoryPaths = [artifact.path, ...assets.map((asset) => asset.path)];
|
|
1678
|
+
if (new Set(guestPaths).size !== guestPaths.length || new Set(repositoryPaths).size !== repositoryPaths.length) throw new TypeError(`Repository authoring '${slug}' ${input.kind} repeats an asset or repository path.`);
|
|
1679
|
+
if (assets.reduce((total, asset) => total + asset.bytes, 0) > MAX_ASSET_TOTAL_BYTES) throw new TypeError(`Repository authoring '${slug}' ${input.kind} assets exceed 4 MiB.`);
|
|
1680
|
+
if (!Array.isArray(input.args) || input.args.length > 64 || input.args.some((argument) => typeof argument !== "string" || argument.includes("\0") || new TextEncoder().encode(argument).byteLength > 4096)) throw new TypeError(`Repository authoring '${slug}' ${input.kind} args are invalid.`);
|
|
1681
|
+
const args = [...input.args];
|
|
1682
|
+
if (input.kind === "checker") return {
|
|
1683
|
+
kind: "checker",
|
|
1684
|
+
artifact,
|
|
1685
|
+
assets,
|
|
1686
|
+
args
|
|
1687
|
+
};
|
|
1688
|
+
if (typeof input.inputPath !== "string" || !GUEST_PATH.test(input.inputPath) || !input.inputPath.startsWith("/interactor/input/")) throw new TypeError(`Repository authoring '${slug}' interactive inputPath must be inside '/interactor/input/'.`);
|
|
1689
|
+
return {
|
|
1690
|
+
kind: "interactive",
|
|
1691
|
+
artifact,
|
|
1692
|
+
assets,
|
|
1693
|
+
args,
|
|
1694
|
+
inputPath: input.inputPath
|
|
1695
|
+
};
|
|
1696
|
+
}
|
|
1697
|
+
/** Author-only input used by collection build; never accepted by the platform sync boundary. */
|
|
1698
|
+
function parseRepositoryAuthoringJudges(value) {
|
|
1699
|
+
const source = record$2(value, "repository authoring judges");
|
|
1700
|
+
exact$2(source, ["problems", "schema"], "repository authoring judges");
|
|
1701
|
+
if (source.schema !== "wasm-oj-platform/repository-authoring-judges/v1") throw new TypeError(`Repository authoring judge schema must be '${REPOSITORY_AUTHORING_JUDGES_SCHEMA}'.`);
|
|
1702
|
+
if (!Array.isArray(source.problems) || source.problems.length < 1 || source.problems.length > 1e3) throw new TypeError("Repository authoring judges must contain between 1 and 1000 problems.");
|
|
1703
|
+
const slugs = /* @__PURE__ */ new Set();
|
|
1704
|
+
return {
|
|
1705
|
+
schema: REPOSITORY_AUTHORING_JUDGES_SCHEMA,
|
|
1706
|
+
problems: source.problems.map((candidate, index) => {
|
|
1707
|
+
const problem = record$2(candidate, `repository authoring judge problem ${index + 1}`);
|
|
1708
|
+
exact$2(problem, [
|
|
1709
|
+
"allowedProfiles",
|
|
1710
|
+
"judge",
|
|
1711
|
+
"slug"
|
|
1712
|
+
], `repository authoring judge problem ${index + 1}`);
|
|
1713
|
+
if (typeof problem.slug !== "string" || !SLUG$1.test(problem.slug) || slugs.has(problem.slug)) throw new TypeError(`Repository authoring judge problem ${index + 1} has an invalid or duplicate slug.`);
|
|
1714
|
+
slugs.add(problem.slug);
|
|
1715
|
+
return {
|
|
1716
|
+
slug: problem.slug,
|
|
1717
|
+
allowedProfiles: parseJudgeAllowedProfiles(problem.allowedProfiles, `repository authoring '${problem.slug}' allowedProfiles`),
|
|
1718
|
+
judge: judge(problem.judge, problem.slug)
|
|
1719
|
+
};
|
|
1720
|
+
})
|
|
1721
|
+
};
|
|
1722
|
+
}
|
|
1723
|
+
//#endregion
|
|
1724
|
+
//#region src/online-judge/repository-contract.ts
|
|
1725
|
+
var REPOSITORY_SCHEMA = "wasm-oj-platform/repository/v1";
|
|
1726
|
+
var PROBLEMS_SCHEMA = "wasm-oj-platform/problems/v1";
|
|
1727
|
+
var CONTESTS_SCHEMA = "wasm-oj-platform/contests/v2";
|
|
1728
|
+
var MAX_PUBLIC_BUNDLE_BYTES = 8388608;
|
|
1729
|
+
var MAX_JUDGE_PACKAGE_BYTES = 33554432;
|
|
1730
|
+
var SHA256 = /^[0-9a-f]{64}$/;
|
|
1731
|
+
var SLUG = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
1732
|
+
var PATH = /^(?!\/)(?!.*(?:^|\/)\.{1,2}(?:\/|$))(?!.*\/\/)(?!.*\\)(?!.*\/$)[^\u0000-\u001f\u007f]+$/;
|
|
1733
|
+
var decoder = new TextDecoder("utf-8", { fatal: true });
|
|
1734
|
+
function record$1(value, label) {
|
|
1735
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new TypeError(`${label} must be an object.`);
|
|
1736
|
+
return value;
|
|
1737
|
+
}
|
|
1738
|
+
function exact$1(value, keys, label) {
|
|
1739
|
+
const actual = Object.keys(value).sort();
|
|
1740
|
+
const expected = [...keys].sort();
|
|
1741
|
+
if (actual.join("\0") !== expected.join("\0")) throw new TypeError(`${label} has an invalid shape.`);
|
|
1742
|
+
}
|
|
1743
|
+
function repositoryPath(value, label) {
|
|
1744
|
+
if (typeof value !== "string" || value.length < 1 || value.length > 512 || !PATH.test(value)) throw new TypeError(`${label} must be a normalized repository-relative POSIX path.`);
|
|
1745
|
+
return value;
|
|
1746
|
+
}
|
|
1747
|
+
function slug(value, label) {
|
|
1748
|
+
if (typeof value !== "string" || value.length > 128 || !SLUG.test(value)) throw new TypeError(`${label} is invalid.`);
|
|
1749
|
+
return value;
|
|
1750
|
+
}
|
|
1751
|
+
function boundedText(value, label, maximumBytes) {
|
|
1752
|
+
if (typeof value !== "string" || new TextEncoder().encode(value).byteLength > maximumBytes) throw new TypeError(`${label} exceeds its UTF-8 byte limit.`);
|
|
1753
|
+
return value;
|
|
1754
|
+
}
|
|
1755
|
+
function localizedText(value, label, maximumBytes) {
|
|
1756
|
+
const input = record$1(value, label);
|
|
1757
|
+
exact$1(input, ["en", "zh-TW"], label);
|
|
1758
|
+
return {
|
|
1759
|
+
"zh-TW": boundedText(input["zh-TW"], `${label}.zh-TW`, maximumBytes),
|
|
1760
|
+
en: boundedText(input.en, `${label}.en`, maximumBytes)
|
|
1761
|
+
};
|
|
1762
|
+
}
|
|
1763
|
+
function descriptor(value, label, maximumBytes) {
|
|
1764
|
+
const input = record$1(value, label);
|
|
1765
|
+
exact$1(input, [
|
|
1766
|
+
"bytes",
|
|
1767
|
+
"path",
|
|
1768
|
+
"sha256"
|
|
1769
|
+
], label);
|
|
1770
|
+
if (!Number.isSafeInteger(input.bytes) || input.bytes < 1 || input.bytes > maximumBytes) throw new TypeError(`${label}.bytes is outside its limit.`);
|
|
1771
|
+
if (typeof input.sha256 !== "string" || !SHA256.test(input.sha256)) throw new TypeError(`${label}.sha256 must be a lowercase SHA-256 digest.`);
|
|
1772
|
+
return {
|
|
1773
|
+
path: repositoryPath(input.path, `${label}.path`),
|
|
1774
|
+
bytes: input.bytes,
|
|
1775
|
+
sha256: input.sha256
|
|
1776
|
+
};
|
|
1777
|
+
}
|
|
1778
|
+
function parseJsonBytes(bytes, label) {
|
|
1779
|
+
if (!(bytes instanceof Uint8Array) || bytes.byteLength < 2 || bytes.byteLength > 2097152) throw new TypeError(`${label} bytes are outside the 2 MiB limit.`);
|
|
1780
|
+
let text;
|
|
1781
|
+
try {
|
|
1782
|
+
text = decoder.decode(bytes);
|
|
1783
|
+
} catch (error) {
|
|
1784
|
+
throw new TypeError(`${label} must be valid UTF-8.`, { cause: error });
|
|
1785
|
+
}
|
|
1786
|
+
try {
|
|
1787
|
+
return JSON.parse(text);
|
|
1788
|
+
} catch (error) {
|
|
1789
|
+
throw new TypeError(`${label} must be valid JSON.`, { cause: error });
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
function parseRepositoryRootValue(value) {
|
|
1793
|
+
const input = record$1(value, "repository manifest");
|
|
1794
|
+
exact$1(input, [
|
|
1795
|
+
"contests",
|
|
1796
|
+
"problems",
|
|
1797
|
+
"schema"
|
|
1798
|
+
], "repository manifest");
|
|
1799
|
+
if (input.schema !== "wasm-oj-platform/repository/v1") throw new TypeError(`Repository schema must be '${REPOSITORY_SCHEMA}'.`);
|
|
1800
|
+
const problems = repositoryPath(input.problems, "repository manifest problems");
|
|
1801
|
+
const contests = repositoryPath(input.contests, "repository manifest contests");
|
|
1802
|
+
if (problems === contests || problems === "wasm-oj.json" || contests === "wasm-oj.json") throw new TypeError("Repository manifest paths must be distinct from each other and the root manifest.");
|
|
1803
|
+
return {
|
|
1804
|
+
schema: REPOSITORY_SCHEMA,
|
|
1805
|
+
problems,
|
|
1806
|
+
contests
|
|
1807
|
+
};
|
|
1808
|
+
}
|
|
1809
|
+
function parseRepositoryRoot(bytes) {
|
|
1810
|
+
return parseRepositoryRootValue(parseJsonBytes(bytes, "repository manifest"));
|
|
1811
|
+
}
|
|
1812
|
+
function parseRepositoryProblemsValue(value) {
|
|
1813
|
+
const input = record$1(value, "problems manifest");
|
|
1814
|
+
exact$1(input, ["problems", "schema"], "problems manifest");
|
|
1815
|
+
if (input.schema !== "wasm-oj-platform/problems/v1") throw new TypeError(`Problems schema must be '${PROBLEMS_SCHEMA}'.`);
|
|
1816
|
+
if (!Array.isArray(input.problems) || input.problems.length < 1 || input.problems.length > 1e3) throw new TypeError("Problems manifest must contain between 1 and 1000 problems.");
|
|
1817
|
+
const slugs = /* @__PURE__ */ new Set();
|
|
1818
|
+
const orders = /* @__PURE__ */ new Set();
|
|
1819
|
+
const paths = /* @__PURE__ */ new Set();
|
|
1820
|
+
const problems = input.problems.map((candidate, index) => {
|
|
1821
|
+
const problem = record$1(candidate, `problem ${index + 1}`);
|
|
1822
|
+
exact$1(problem, [
|
|
1823
|
+
"contestBundle",
|
|
1824
|
+
"judgePackage",
|
|
1825
|
+
"order",
|
|
1826
|
+
"practiceBundle",
|
|
1827
|
+
"practiceEnabled",
|
|
1828
|
+
"slug",
|
|
1829
|
+
"summary",
|
|
1830
|
+
"title"
|
|
1831
|
+
], `problem ${index + 1}`);
|
|
1832
|
+
const problemSlug = slug(problem.slug, `problem ${index + 1}.slug`);
|
|
1833
|
+
if (slugs.has(problemSlug)) throw new TypeError(`Problem slug '${problemSlug}' is duplicated.`);
|
|
1834
|
+
if (!Number.isSafeInteger(problem.order) || problem.order < 1 || problem.order > 1e3 || orders.has(problem.order)) throw new TypeError(`Problem '${problemSlug}' order is invalid or duplicated.`);
|
|
1835
|
+
if (typeof problem.practiceEnabled !== "boolean") throw new TypeError(`Problem '${problemSlug}' practiceEnabled must be boolean.`);
|
|
1836
|
+
const practiceBundle = descriptor(problem.practiceBundle, `problem '${problemSlug}' practiceBundle`, MAX_PUBLIC_BUNDLE_BYTES);
|
|
1837
|
+
const contestBundle = descriptor(problem.contestBundle, `problem '${problemSlug}' contestBundle`, MAX_PUBLIC_BUNDLE_BYTES);
|
|
1838
|
+
const judgePackage = descriptor(problem.judgePackage, `problem '${problemSlug}' judgePackage`, MAX_JUDGE_PACKAGE_BYTES);
|
|
1839
|
+
for (const object of [
|
|
1840
|
+
practiceBundle,
|
|
1841
|
+
contestBundle,
|
|
1842
|
+
judgePackage
|
|
1843
|
+
]) {
|
|
1844
|
+
if (paths.has(object.path)) throw new TypeError(`Repository object path '${object.path}' is declared more than once.`);
|
|
1845
|
+
paths.add(object.path);
|
|
1846
|
+
}
|
|
1847
|
+
slugs.add(problemSlug);
|
|
1848
|
+
orders.add(problem.order);
|
|
1849
|
+
return {
|
|
1850
|
+
slug: problemSlug,
|
|
1851
|
+
order: problem.order,
|
|
1852
|
+
title: localizedText(problem.title, `problem '${problemSlug}' title`, 4096),
|
|
1853
|
+
summary: localizedText(problem.summary, `problem '${problemSlug}' summary`, 16384),
|
|
1854
|
+
practiceEnabled: problem.practiceEnabled,
|
|
1855
|
+
practiceBundle,
|
|
1856
|
+
contestBundle,
|
|
1857
|
+
judgePackage
|
|
1858
|
+
};
|
|
1859
|
+
});
|
|
1860
|
+
problems.sort((left, right) => left.order - right.order);
|
|
1861
|
+
return {
|
|
1862
|
+
schema: PROBLEMS_SCHEMA,
|
|
1863
|
+
problems
|
|
1864
|
+
};
|
|
1865
|
+
}
|
|
1866
|
+
function parseRepositoryProblems(bytes) {
|
|
1867
|
+
return parseRepositoryProblemsValue(parseJsonBytes(bytes, "problems manifest"));
|
|
1868
|
+
}
|
|
1869
|
+
function parseRepositoryContestsValue(value) {
|
|
1870
|
+
const input = record$1(value, "contests manifest");
|
|
1871
|
+
exact$1(input, ["contests", "schema"], "contests manifest");
|
|
1872
|
+
if (input.schema !== "wasm-oj-platform/contests/v2") throw new TypeError(`Contests schema must be '${CONTESTS_SCHEMA}'.`);
|
|
1873
|
+
if (!Array.isArray(input.contests) || input.contests.length > 1e3) throw new TypeError("Contests manifest may contain at most 1000 contests.");
|
|
1874
|
+
const slugs = /* @__PURE__ */ new Set();
|
|
1875
|
+
return {
|
|
1876
|
+
schema: CONTESTS_SCHEMA,
|
|
1877
|
+
contests: input.contests.map((candidate, index) => {
|
|
1878
|
+
const contest = record$1(candidate, `contest ${index + 1}`);
|
|
1879
|
+
exact$1(contest, [
|
|
1880
|
+
"accessMode",
|
|
1881
|
+
"description",
|
|
1882
|
+
"rules",
|
|
1883
|
+
"slug",
|
|
1884
|
+
"status",
|
|
1885
|
+
"title"
|
|
1886
|
+
], `contest ${index + 1}`);
|
|
1887
|
+
const contestSlug = slug(contest.slug, `contest ${index + 1}.slug`);
|
|
1888
|
+
if (slugs.has(contestSlug)) throw new TypeError(`Contest slug '${contestSlug}' is duplicated.`);
|
|
1889
|
+
if (contest.status !== "draft" && contest.status !== "published" && contest.status !== "archived") throw new TypeError(`Contest '${contestSlug}' status is invalid.`);
|
|
1890
|
+
if (contest.accessMode !== "public" && contest.accessMode !== "invite") throw new TypeError(`Contest '${contestSlug}' accessMode is invalid.`);
|
|
1891
|
+
const rules = parseContestRules(contest.rules, `contest '${contestSlug}' rules`);
|
|
1892
|
+
slugs.add(contestSlug);
|
|
1893
|
+
return {
|
|
1894
|
+
slug: contestSlug,
|
|
1895
|
+
status: contest.status,
|
|
1896
|
+
title: boundedText(contest.title, `contest '${contestSlug}' title`, 4096),
|
|
1897
|
+
description: boundedText(contest.description, `contest '${contestSlug}' description`, 65536),
|
|
1898
|
+
accessMode: contest.accessMode,
|
|
1899
|
+
rules
|
|
1900
|
+
};
|
|
1901
|
+
})
|
|
1902
|
+
};
|
|
1903
|
+
}
|
|
1904
|
+
function parseRepositoryContests(bytes) {
|
|
1905
|
+
return parseRepositoryContestsValue(parseJsonBytes(bytes, "contests manifest"));
|
|
1906
|
+
}
|
|
1907
|
+
function validateRepositoryCatalog(root, problems, contests) {
|
|
1908
|
+
const problemSlugs = new Set(problems.problems.map((problem) => problem.slug));
|
|
1909
|
+
for (const contest of contests.contests) for (const problem of contest.rules.problems) if (!problemSlugs.has(problem.slug)) throw new TypeError(`Contest '${contest.slug}' references unknown problem '${problem.slug}'.`);
|
|
1910
|
+
return {
|
|
1911
|
+
root,
|
|
1912
|
+
problems,
|
|
1913
|
+
contests
|
|
1914
|
+
};
|
|
1915
|
+
}
|
|
1916
|
+
//#endregion
|
|
7
1917
|
//#region src/path-safety.ts
|
|
8
1918
|
/** Resolve only the operating system's own temporary-directory alias. */
|
|
9
1919
|
async function canonicalizeSystemTemporaryPrefix(value) {
|
|
@@ -31,516 +1941,338 @@ async function anchoredPathHasNoSymlink(value) {
|
|
|
31
1941
|
}
|
|
32
1942
|
//#endregion
|
|
33
1943
|
//#region src/collection-cli.ts
|
|
34
|
-
var
|
|
35
|
-
var
|
|
36
|
-
var
|
|
37
|
-
var
|
|
38
|
-
var
|
|
39
|
-
var
|
|
40
|
-
var
|
|
41
|
-
var
|
|
42
|
-
var MANAGED_MAX_BYTES = 2097152;
|
|
43
|
-
var JUDGE_PACKAGE_MAX_BYTES = 33554432;
|
|
1944
|
+
var AUTHORING_SCHEMA = "wasm-oj-platform/repository-authoring/v1";
|
|
1945
|
+
var SOURCE_PATH = "collection/source.json";
|
|
1946
|
+
var ROOT_PATH = "wasm-oj.json";
|
|
1947
|
+
var PROBLEMS_PATH = "collection/problems.json";
|
|
1948
|
+
var CONTESTS_PATH = "collection/contests.json";
|
|
1949
|
+
var JSON_MAX_BYTES = 2097152;
|
|
1950
|
+
var PUBLIC_MAX_BYTES = 8388608;
|
|
1951
|
+
var PACKAGE_MAX_BYTES = 33554432;
|
|
44
1952
|
function fail(message) {
|
|
45
1953
|
throw new Error(message);
|
|
46
1954
|
}
|
|
47
|
-
function
|
|
48
|
-
|
|
49
|
-
if (value.split("/").some((segment) => !segment || segment === "." || segment === "..")) return fail(`${label} must be a normalized relative POSIX path.`);
|
|
50
|
-
return value;
|
|
51
|
-
}
|
|
52
|
-
function portableOutputPath(value, label) {
|
|
53
|
-
const relative = normalizedRelativePath(value, label);
|
|
54
|
-
if (relative.split("/").some((segment) => !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(segment))) return fail(`${label} must use only portable ASCII path segments.`);
|
|
55
|
-
return relative;
|
|
56
|
-
}
|
|
57
|
-
function resolveInside(root, relativeValue, label) {
|
|
58
|
-
const relative = normalizedRelativePath(relativeValue, label);
|
|
59
|
-
return path.join(root, ...relative.split("/"));
|
|
60
|
-
}
|
|
61
|
-
function collisionKey(value) {
|
|
62
|
-
return value.normalize("NFC").toLocaleLowerCase("en-US");
|
|
63
|
-
}
|
|
64
|
-
async function assertRepositoryRoot(root) {
|
|
65
|
-
const anchoredRoot = await canonicalizeSystemTemporaryPrefix(path.resolve(root));
|
|
66
|
-
if (!await anchoredPathHasNoSymlink(anchoredRoot)) fail("repository root must not traverse a symbolic link.");
|
|
67
|
-
const metadata = await lstat(anchoredRoot);
|
|
68
|
-
if (metadata.isSymbolicLink() || !metadata.isDirectory()) fail("repository root must be a real directory.");
|
|
69
|
-
}
|
|
70
|
-
async function readBoundedRepositoryFile(options, relativeValue, label, maximumBytes, expectedBytes) {
|
|
71
|
-
const file = resolveInside(options.root, relativeValue, label);
|
|
72
|
-
if (!await anchoredPathHasNoSymlink(file)) fail(`${label} must not traverse a symbolic link.`);
|
|
73
|
-
let handle;
|
|
74
|
-
try {
|
|
75
|
-
handle = await open(file, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
76
|
-
} catch (error) {
|
|
77
|
-
throw new Error(`${label} must be a readable regular file.`, { cause: error });
|
|
78
|
-
}
|
|
79
|
-
try {
|
|
80
|
-
const metadata = await handle.stat();
|
|
81
|
-
if (!metadata.isFile() || metadata.size > maximumBytes || expectedBytes !== void 0 && metadata.size !== expectedBytes) fail(`${label} is outside its allowed byte limit.`);
|
|
82
|
-
return new Uint8Array(await handle.readFile());
|
|
83
|
-
} finally {
|
|
84
|
-
await handle.close();
|
|
85
|
-
}
|
|
1955
|
+
function sameLocalizedText(left, right) {
|
|
1956
|
+
return left["zh-TW"] === right["zh-TW"] && left.en === right.en;
|
|
86
1957
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
let metadata;
|
|
91
|
-
try {
|
|
92
|
-
metadata = await lstat(output);
|
|
93
|
-
} catch (error) {
|
|
94
|
-
if (error.code === "ENOENT") return;
|
|
95
|
-
throw error;
|
|
96
|
-
}
|
|
97
|
-
if (metadata.isSymbolicLink() || !metadata.isFile()) fail(`${label} must replace only a regular file.`);
|
|
1958
|
+
function normalizedPath(value, label) {
|
|
1959
|
+
if (typeof value !== "string" || value.length < 1 || value.length > 512 || value.startsWith("/") || value.endsWith("/") || value.includes("\\") || value.includes("\0") || value.split("/").some((part) => !part || part === "." || part === "..")) fail(`${label} must be a normalized repository-relative POSIX path.`);
|
|
1960
|
+
return value;
|
|
98
1961
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
try {
|
|
103
|
-
const metadata = await lstat(file, { bigint: true });
|
|
104
|
-
if (metadata.isSymbolicLink()) fail(`${label} must not be a symbolic link.`);
|
|
105
|
-
return `${metadata.dev}:${metadata.ino}`;
|
|
106
|
-
} catch (error) {
|
|
107
|
-
if (error.code === "ENOENT") return void 0;
|
|
108
|
-
throw error;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
async function assertNoFilesystemAliases(options, inputs, outputs) {
|
|
112
|
-
const inputIdentities = /* @__PURE__ */ new Map();
|
|
113
|
-
for (const input of inputs) {
|
|
114
|
-
const identity = await existingPathIdentity(options, input, `declared input '${input}'`);
|
|
115
|
-
if (identity !== void 0 && !inputIdentities.has(identity)) inputIdentities.set(identity, input);
|
|
116
|
-
}
|
|
117
|
-
const outputIdentities = /* @__PURE__ */ new Map();
|
|
118
|
-
for (const output of outputs) {
|
|
119
|
-
const identity = await existingPathIdentity(options, output, `generated output '${output}'`);
|
|
120
|
-
if (identity === void 0) continue;
|
|
121
|
-
const input = inputIdentities.get(identity);
|
|
122
|
-
if (input !== void 0) fail(`generated output '${output}' aliases declared input '${input}' on this filesystem.`);
|
|
123
|
-
const priorOutput = outputIdentities.get(identity);
|
|
124
|
-
if (priorOutput !== void 0) fail(`generated outputs '${priorOutput}' and '${output}' alias on this filesystem.`);
|
|
125
|
-
outputIdentities.set(identity, output);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
async function ensureOutputParent(options, relativeValue) {
|
|
129
|
-
const relativeParent = path.posix.dirname(normalizedRelativePath(relativeValue, "output path"));
|
|
130
|
-
if (relativeParent === ".") return;
|
|
131
|
-
let current = options.root;
|
|
132
|
-
for (const segment of relativeParent.split("/")) {
|
|
133
|
-
current = path.join(current, segment);
|
|
134
|
-
try {
|
|
135
|
-
const metadata = await lstat(current);
|
|
136
|
-
if (metadata.isSymbolicLink() || !metadata.isDirectory()) fail(`output directory '${relativeParent}' must contain only real directories.`);
|
|
137
|
-
} catch (error) {
|
|
138
|
-
if (error.code !== "ENOENT") throw error;
|
|
139
|
-
try {
|
|
140
|
-
await mkdir(current);
|
|
141
|
-
} catch (mkdirError) {
|
|
142
|
-
if (mkdirError.code !== "EEXIST") throw mkdirError;
|
|
143
|
-
const metadata = await lstat(current);
|
|
144
|
-
if (metadata.isSymbolicLink() || !metadata.isDirectory()) fail(`output directory '${relativeParent}' must contain only real directories.`);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
1962
|
+
function record(value, label) {
|
|
1963
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) fail(`${label} must be an object.`);
|
|
1964
|
+
return value;
|
|
148
1965
|
}
|
|
149
|
-
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
try {
|
|
154
|
-
await writeFile(temporary, bytes, {
|
|
155
|
-
flag: "wx",
|
|
156
|
-
mode: 420
|
|
157
|
-
});
|
|
158
|
-
await rename(temporary, output);
|
|
159
|
-
} finally {
|
|
160
|
-
await rm(temporary, { force: true });
|
|
161
|
-
}
|
|
1966
|
+
function exact(value, keys, label) {
|
|
1967
|
+
const actual = Object.keys(value).sort();
|
|
1968
|
+
const expected = [...keys].sort();
|
|
1969
|
+
if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) fail(`${label} has an invalid shape.`);
|
|
162
1970
|
}
|
|
163
1971
|
function parseOptions(arguments_) {
|
|
164
|
-
const [
|
|
165
|
-
if (
|
|
1972
|
+
const [command, ...rest] = arguments_;
|
|
1973
|
+
if (command !== "build" && command !== "verify") fail("Usage: woj organizer collection <build|verify> [repository-root] [--source path]");
|
|
166
1974
|
let root = ".";
|
|
167
|
-
let
|
|
168
|
-
let
|
|
169
|
-
let managedPath;
|
|
170
|
-
let managedSourcePath;
|
|
171
|
-
let sawRoot = false;
|
|
1975
|
+
let sourcePath = SOURCE_PATH;
|
|
1976
|
+
let rootSeen = false;
|
|
172
1977
|
for (let index = 0; index < rest.length; index += 1) {
|
|
173
1978
|
const argument = rest[index];
|
|
174
|
-
if (argument === "--
|
|
1979
|
+
if (argument === "--source") {
|
|
175
1980
|
const value = rest[index + 1];
|
|
176
|
-
if (!value)
|
|
177
|
-
|
|
178
|
-
else if (argument === "--source") sourcePath = normalizedRelativePath(value, "source path");
|
|
179
|
-
else if (argument === "--managed") managedPath = portableOutputPath(value, "managed contract path");
|
|
180
|
-
else managedSourcePath = normalizedRelativePath(value, "managed source path");
|
|
1981
|
+
if (!value) fail("--source requires a path.");
|
|
1982
|
+
sourcePath = normalizedPath(value, "source path");
|
|
181
1983
|
index += 1;
|
|
182
|
-
|
|
1984
|
+
} else if (argument?.startsWith("-")) fail(`Unknown option '${argument}'.`);
|
|
1985
|
+
else if (!argument || rootSeen) fail("Only one repository root may be provided.");
|
|
1986
|
+
else {
|
|
1987
|
+
root = argument;
|
|
1988
|
+
rootSeen = true;
|
|
183
1989
|
}
|
|
184
|
-
|
|
185
|
-
if (sawRoot || !argument) return fail("Only one repository root may be provided.");
|
|
186
|
-
root = argument;
|
|
187
|
-
sawRoot = true;
|
|
188
|
-
}
|
|
189
|
-
if (commandValue !== "build" && managedSourcePath) return fail("--managed-source is only valid for build.");
|
|
190
|
-
if (commandValue === "build" && managedPath && !managedSourcePath) return fail("--managed requires --managed-source when building.");
|
|
191
|
-
if (commandValue === "build" && collisionKey(indexPath) === collisionKey(sourcePath)) return fail("--index and --source must not be the same input and output path.");
|
|
192
|
-
if (commandValue === "build" && managedPath !== void 0 && managedSourcePath !== void 0 && collisionKey(managedPath) === collisionKey(managedSourcePath)) return fail("--managed and --managed-source must not be the same input and output path.");
|
|
1990
|
+
}
|
|
193
1991
|
return {
|
|
194
|
-
command
|
|
1992
|
+
command,
|
|
195
1993
|
root: path.resolve(root),
|
|
196
|
-
|
|
197
|
-
sourcePath,
|
|
198
|
-
...managedPath ? { managedPath } : {},
|
|
199
|
-
...managedSourcePath ? { managedSourcePath } : {}
|
|
1994
|
+
sourcePath
|
|
200
1995
|
};
|
|
201
1996
|
}
|
|
202
|
-
function
|
|
203
|
-
|
|
1997
|
+
async function assertRoot(root) {
|
|
1998
|
+
const anchored = await canonicalizeSystemTemporaryPrefix(root);
|
|
1999
|
+
if (!await anchoredPathHasNoSymlink(anchored)) fail("repository root must not traverse a symbolic link.");
|
|
2000
|
+
const metadata = await lstat(anchored);
|
|
2001
|
+
if (!metadata.isDirectory() || metadata.isSymbolicLink()) fail("repository root must be a real directory.");
|
|
204
2002
|
}
|
|
205
|
-
function
|
|
206
|
-
|
|
207
|
-
const sortedExpected = [...expected].sort();
|
|
208
|
-
if (JSON.stringify(actual) !== JSON.stringify(sortedExpected)) fail(`${label} must contain exactly: ${sortedExpected.join(", ")}.`);
|
|
209
|
-
}
|
|
210
|
-
function parseAuthoredCollection(value) {
|
|
211
|
-
if (!isRecord(value)) return fail("collection/source.json must be an object.");
|
|
212
|
-
exactKeys(value, [
|
|
213
|
-
"schema",
|
|
214
|
-
"localization",
|
|
215
|
-
"problems"
|
|
216
|
-
], "collection source");
|
|
217
|
-
if (value.schema !== SOURCE_SCHEMA) return fail(`collection source schema must be '${SOURCE_SCHEMA}'.`);
|
|
218
|
-
if (!isRecord(value.localization)) return fail("collection localization must be an object.");
|
|
219
|
-
exactKeys(value.localization, ["defaultLocale", "supportedLocales"], "collection localization");
|
|
220
|
-
if (value.localization.defaultLocale !== "zh-TW" || JSON.stringify(value.localization.supportedLocales) !== JSON.stringify(["zh-TW", "en"])) return fail("collection localization must declare zh-TW followed by en.");
|
|
221
|
-
if (!Array.isArray(value.problems) || value.problems.length < 1 || value.problems.length > 1e3) return fail("collection source must contain between 1 and 1000 problems.");
|
|
222
|
-
return {
|
|
223
|
-
schema: SOURCE_SCHEMA,
|
|
224
|
-
localization: {
|
|
225
|
-
defaultLocale: "zh-TW",
|
|
226
|
-
supportedLocales: ["zh-TW", "en"]
|
|
227
|
-
},
|
|
228
|
-
problems: value.problems.map((problemValue, index) => {
|
|
229
|
-
if (!isRecord(problemValue)) return fail(`collection source problem ${index + 1} must be an object.`);
|
|
230
|
-
exactKeys(problemValue, ["statementPaths", "bundlePath"], `collection source problem ${index + 1}`);
|
|
231
|
-
if (!isRecord(problemValue.statementPaths)) return fail(`problem ${index + 1} statementPaths must be an object.`);
|
|
232
|
-
exactKeys(problemValue.statementPaths, ["zh-TW", "en"], `problem ${index + 1} statementPaths`);
|
|
233
|
-
const statementPaths = {
|
|
234
|
-
"zh-TW": normalizedRelativePath(problemValue.statementPaths["zh-TW"], `problem ${index + 1} zh-TW statement`),
|
|
235
|
-
en: normalizedRelativePath(problemValue.statementPaths.en, `problem ${index + 1} English statement`)
|
|
236
|
-
};
|
|
237
|
-
if (!statementPaths["zh-TW"].endsWith(".md") || !statementPaths.en.endsWith(".md")) return fail(`problem ${index + 1} statements must be Markdown files.`);
|
|
238
|
-
return {
|
|
239
|
-
statementPaths,
|
|
240
|
-
bundlePath: normalizedRelativePath(problemValue.bundlePath, `problem ${index + 1} bundle`)
|
|
241
|
-
};
|
|
242
|
-
})
|
|
243
|
-
};
|
|
2003
|
+
function resolveInside(root, relative) {
|
|
2004
|
+
return path.join(root, ...normalizedPath(relative, "repository path").split("/"));
|
|
244
2005
|
}
|
|
245
|
-
function
|
|
246
|
-
|
|
2006
|
+
async function readFile(root, relative, maximum, expected) {
|
|
2007
|
+
const absolute = resolveInside(root, relative);
|
|
2008
|
+
if (!await anchoredPathHasNoSymlink(absolute)) fail(`'${relative}' must not traverse a symbolic link.`);
|
|
2009
|
+
const handle = await open(absolute, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
247
2010
|
try {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
2011
|
+
const metadata = await handle.stat();
|
|
2012
|
+
if (!metadata.isFile() || metadata.size < 1 || metadata.size > maximum || expected !== void 0 && metadata.size !== expected) fail(`'${relative}' has an invalid size.`);
|
|
2013
|
+
return new Uint8Array(await handle.readFile());
|
|
2014
|
+
} finally {
|
|
2015
|
+
await handle.close();
|
|
251
2016
|
}
|
|
2017
|
+
}
|
|
2018
|
+
function parseJson(bytes, label) {
|
|
252
2019
|
try {
|
|
253
|
-
return JSON.parse(
|
|
2020
|
+
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
254
2021
|
} catch (error) {
|
|
255
|
-
throw new Error(`${label}
|
|
2022
|
+
throw new Error(`${label} must be valid UTF-8 JSON.`, { cause: error });
|
|
256
2023
|
}
|
|
257
2024
|
}
|
|
258
|
-
async function
|
|
2025
|
+
async function sha256(bytes) {
|
|
259
2026
|
const digest = await crypto.subtle.digest("SHA-256", bytes.slice().buffer);
|
|
260
|
-
return
|
|
261
|
-
}
|
|
262
|
-
async function validatePublishedCollection(options, strict) {
|
|
263
|
-
const indexBytes = await readBoundedRepositoryFile(options, options.indexPath, "index path", INDEX_MAX_BYTES);
|
|
264
|
-
const index = parseProblemCollectionIndex$1(parseJson(indexBytes, options.indexPath));
|
|
265
|
-
await verifyProblemCollectionRevision$1(index);
|
|
266
|
-
if (strict && !Buffer.from(indexBytes).equals(Buffer.from(canonicalJsonBytes(index)))) fail(`${options.indexPath} is not canonical; run woj organizer collection build.`);
|
|
267
|
-
const indexDirectory = path.posix.dirname(options.indexPath);
|
|
268
|
-
for (const entry of index.problems) {
|
|
269
|
-
for (const [locale, statementPath] of Object.entries(entry.statementPaths)) if ((await readBoundedRepositoryFile(options, statementPath, `problem '${entry.id}' ${locale} statement`, STATEMENT_MAX_BYTES)).byteLength < 1) fail(`problem '${entry.id}' ${locale} statement must contain between 1 byte and 2 MiB.`);
|
|
270
|
-
const repositoryPath = path.posix.join(indexDirectory, entry.bundle.path);
|
|
271
|
-
const bytes = await readBoundedRepositoryFile(options, repositoryPath, `problem '${entry.id}' bundle`, PUBLIC_BUNDLE_MAX_BYTES, entry.bundle.bytes);
|
|
272
|
-
const problem = await verifyProblemBundleBytes$1(bytes, entry);
|
|
273
|
-
if (strict && !Buffer.from(bytes).equals(Buffer.from(canonicalJsonBytes({
|
|
274
|
-
schema: BROWSER_PROBLEM_SCHEMA$1,
|
|
275
|
-
problem
|
|
276
|
-
})))) fail(`${repositoryPath} is not canonical; run woj organizer collection build.`);
|
|
277
|
-
}
|
|
278
|
-
if (strict) await rejectUndeclaredContentAddressedBundles(options, index);
|
|
279
|
-
if (options.managedPath) await validateManagedContract(options, index);
|
|
280
|
-
return index;
|
|
281
|
-
}
|
|
282
|
-
async function validateManagedContract(options, index) {
|
|
283
|
-
const managedPath = options.managedPath;
|
|
284
|
-
if (!managedPath) fail("managed contract path is required.");
|
|
285
|
-
const contractBytes = await readBoundedRepositoryFile(options, managedPath, "managed contract path", MANAGED_MAX_BYTES);
|
|
286
|
-
const contract = parseManagedCollectionV2$1(contractBytes);
|
|
287
|
-
if (contract.collectionRevision !== index.revision) fail("managed collection revision does not match collection/index.json.");
|
|
288
|
-
if (JSON.stringify(contract.problems.map((problem) => problem.slug)) !== JSON.stringify(index.problems.map((problem) => problem.id))) fail("managed collection problems must exactly match index order.");
|
|
289
|
-
const indexDirectory = path.posix.dirname(options.indexPath);
|
|
290
|
-
for (const [position, publication] of contract.problems.entries()) {
|
|
291
|
-
const entry = index.problems[position];
|
|
292
|
-
const practiceBytes = await readBoundedRepositoryFile(options, path.posix.join(indexDirectory, entry.bundle.path), `problem '${entry.id}' bundle`, PUBLIC_BUNDLE_MAX_BYTES, entry.bundle.bytes);
|
|
293
|
-
const practice = await verifyProblemBundleBytes$1(practiceBytes, entry);
|
|
294
|
-
const contestBytes = await readPublishedObject(options, indexDirectory, publication.contestPublic, `contest-public '${publication.slug}'`, PUBLIC_BUNDLE_MAX_BYTES);
|
|
295
|
-
const expectedContestBytes = contestPublicProjectionBytes(practice, entry.bundle.sha256);
|
|
296
|
-
if (!Buffer.from(contestBytes).equals(Buffer.from(expectedContestBytes))) fail(`contest-public '${publication.slug}' is not the deterministic projection of its practice bundle.`);
|
|
297
|
-
const packageBytes = await readPublishedObject(options, indexDirectory, publication.judgePackage, `judge package '${publication.slug}'`, JUDGE_PACKAGE_MAX_BYTES);
|
|
298
|
-
const validatedPackage = await validateJudgePackage(packageBytes, {
|
|
299
|
-
expectedBytes: publication.judgePackage.bytes,
|
|
300
|
-
expectedSha256: publication.judgePackage.sha256,
|
|
301
|
-
memoryLimitBytes: Math.max(...practice.scoring.policies.map((policy) => policy.limits.memoryLimitBytes))
|
|
302
|
-
});
|
|
303
|
-
if (JSON.stringify(validatedPackage.manifest.allowedProfiles) !== JSON.stringify(publication.allowedProfiles)) fail(`judge package '${publication.slug}' allowedProfiles disagree with collection/managed.json.`);
|
|
304
|
-
assertJudgeDataMatchesPracticePublic(validatedPackage.judgeData, practice, Object.keys(publication.allowedProfiles));
|
|
305
|
-
}
|
|
2027
|
+
return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
306
2028
|
}
|
|
307
|
-
async function
|
|
308
|
-
const bytes = await
|
|
309
|
-
if (await
|
|
2029
|
+
async function readDeclared(root, object, label) {
|
|
2030
|
+
const bytes = await readFile(root, object.path, PACKAGE_MAX_BYTES, object.bytes);
|
|
2031
|
+
if (await sha256(bytes) !== object.sha256) fail(`${label} failed digest verification.`);
|
|
310
2032
|
return bytes;
|
|
311
2033
|
}
|
|
312
|
-
async function
|
|
313
|
-
|
|
314
|
-
const
|
|
315
|
-
const directory = resolveInside(options.root, relativeDirectory, "bundle directory");
|
|
316
|
-
if (!await anchoredPathHasNoSymlink(directory)) fail("bundle directory must not traverse a symbolic link.");
|
|
317
|
-
const metadata = await lstat(directory);
|
|
318
|
-
if (metadata.isSymbolicLink() || !metadata.isDirectory()) fail("bundle directory must be a real directory.");
|
|
319
|
-
const declared = new Set(index.problems.map((entry) => path.posix.basename(entry.bundle.path)));
|
|
320
|
-
const entries = await readdir(directory, { withFileTypes: true });
|
|
321
|
-
const digestPattern = /\.[0-9a-f]{64}\.json$/;
|
|
322
|
-
const undeclared = entries.filter((entry) => entry.isFile() && digestPattern.test(entry.name) && !declared.has(entry.name)).map((entry) => entry.name).sort();
|
|
323
|
-
const unsafe = entries.filter((entry) => entry.isSymbolicLink() || !entry.isFile() && !entry.isDirectory()).map((entry) => entry.name).sort();
|
|
324
|
-
if (unsafe.length > 0) fail(`bundle directory contains unsafe entries: ${unsafe.join(", ")}.`);
|
|
325
|
-
if (undeclared.length > 0) fail(`undeclared content-addressed bundles: ${undeclared.join(", ")}.`);
|
|
326
|
-
}
|
|
327
|
-
function assertGeneratedSize(bytes, maximumBytes, label) {
|
|
328
|
-
if (bytes.byteLength > maximumBytes) fail(`${label} exceeds its allowed byte limit.`);
|
|
329
|
-
}
|
|
330
|
-
async function prepareCollection(options) {
|
|
331
|
-
const source = parseAuthoredCollection(parseJson(await readBoundedRepositoryFile(options, options.sourcePath, "source path", COLLECTION_SOURCE_MAX_BYTES), options.sourcePath));
|
|
332
|
-
const indexDirectory = path.posix.dirname(options.indexPath);
|
|
333
|
-
const entries = [];
|
|
334
|
-
const authoredProblems = [];
|
|
335
|
-
const practiceProblems = [];
|
|
336
|
-
const inputs = /* @__PURE__ */ new Set([options.sourcePath]);
|
|
337
|
-
const outputs = [];
|
|
338
|
-
for (const [position, authored] of source.problems.entries()) {
|
|
339
|
-
inputs.add(authored.bundlePath);
|
|
340
|
-
const authoredBytes = await readBoundedRepositoryFile(options, authored.bundlePath, `problem ${position + 1} source bundle`, AUTHORING_BUNDLE_MAX_BYTES);
|
|
341
|
-
const problem = parseStandaloneProblemBundle$1(parseJson(authoredBytes, authored.bundlePath));
|
|
342
|
-
if (problem.number !== position + 1) fail(`problem '${problem.id}' must have number ${position + 1}.`);
|
|
343
|
-
for (const [locale, statementPath] of Object.entries(authored.statementPaths)) {
|
|
344
|
-
inputs.add(statementPath);
|
|
345
|
-
if ((await readBoundedRepositoryFile(options, statementPath, `problem '${problem.id}' ${locale} statement`, STATEMENT_MAX_BYTES)).byteLength < 1) fail(`problem '${problem.id}' ${locale} statement must contain between 1 byte and 2 MiB.`);
|
|
346
|
-
}
|
|
347
|
-
authoredProblems.push(problem);
|
|
348
|
-
const practice = derivePracticePublic(problem);
|
|
349
|
-
practiceProblems.push(practice);
|
|
350
|
-
const bundleBytes = canonicalJsonBytes({
|
|
351
|
-
schema: BROWSER_PROBLEM_SCHEMA$1,
|
|
352
|
-
problem: practice
|
|
353
|
-
});
|
|
354
|
-
assertGeneratedSize(bundleBytes, PUBLIC_BUNDLE_MAX_BYTES, `problem '${problem.id}' public bundle`);
|
|
355
|
-
const digest = await sha256Hex(bundleBytes);
|
|
356
|
-
const bundleName = `${String(problem.number).padStart(3, "0")}-${problem.id}.${digest}.json`;
|
|
357
|
-
outputs.push({
|
|
358
|
-
path: path.posix.join(indexDirectory, "problems", bundleName),
|
|
359
|
-
label: `problem '${problem.id}' public bundle output`,
|
|
360
|
-
bytes: bundleBytes
|
|
361
|
-
});
|
|
362
|
-
entries.push({
|
|
363
|
-
id: problem.id,
|
|
364
|
-
number: problem.number,
|
|
365
|
-
title: problem.title,
|
|
366
|
-
trackId: problem.trackId,
|
|
367
|
-
track: problem.track,
|
|
368
|
-
statementPaths: authored.statementPaths,
|
|
369
|
-
difficulty: problem.difficulty,
|
|
370
|
-
tags: problem.tags,
|
|
371
|
-
caseCount: practice.judgeCases.length,
|
|
372
|
-
bundle: {
|
|
373
|
-
path: `problems/${bundleName}`,
|
|
374
|
-
sha256: digest,
|
|
375
|
-
bytes: bundleBytes.byteLength
|
|
376
|
-
}
|
|
377
|
-
});
|
|
378
|
-
}
|
|
379
|
-
const withoutRevision = {
|
|
380
|
-
schema: BROWSER_COLLECTION_SCHEMA$1,
|
|
381
|
-
problemSchema: BROWSER_PROBLEM_SCHEMA$1,
|
|
382
|
-
localization: source.localization,
|
|
383
|
-
problems: entries
|
|
384
|
-
};
|
|
385
|
-
const index = parseProblemCollectionIndex$1({
|
|
386
|
-
...withoutRevision,
|
|
387
|
-
revision: await problemCollectionRevision$1(withoutRevision)
|
|
388
|
-
});
|
|
389
|
-
const indexBytes = canonicalJsonBytes(index);
|
|
390
|
-
assertGeneratedSize(indexBytes, INDEX_MAX_BYTES, "collection index output");
|
|
391
|
-
outputs.push({
|
|
392
|
-
path: options.indexPath,
|
|
393
|
-
label: "collection index output",
|
|
394
|
-
bytes: indexBytes
|
|
395
|
-
});
|
|
396
|
-
return {
|
|
397
|
-
index,
|
|
398
|
-
authoredProblems,
|
|
399
|
-
practiceProblems,
|
|
400
|
-
inputs,
|
|
401
|
-
outputs
|
|
402
|
-
};
|
|
403
|
-
}
|
|
404
|
-
async function readDeclaredManagedSourceObject(options, object, label, inputs) {
|
|
405
|
-
inputs.add(object.path);
|
|
406
|
-
const bytes = await readBoundedRepositoryFile(options, object.path, label, JUDGE_PACKAGE_MAX_BYTES, object.bytes);
|
|
407
|
-
if (await sha256Hex(bytes) !== object.sha256) fail(`${label} failed declared size or digest verification.`);
|
|
408
|
-
return bytes;
|
|
409
|
-
}
|
|
410
|
-
async function managedJudgeInput(options, problem, inputs) {
|
|
411
|
-
if (problem.judge.kind === "text") return { kind: "text" };
|
|
412
|
-
const artifact = await readDeclaredManagedSourceObject(options, problem.judge.artifact, `${problem.judge.kind} artifact '${problem.slug}'`, inputs);
|
|
2034
|
+
async function judgeInput(root, judge) {
|
|
2035
|
+
if (judge.kind === "text") return { kind: "text" };
|
|
2036
|
+
const artifact = await readDeclared(root, judge.artifact, `${judge.kind} artifact`);
|
|
413
2037
|
const assets = [];
|
|
414
|
-
for (const asset of
|
|
2038
|
+
for (const asset of judge.assets) assets.push({
|
|
415
2039
|
guestPath: asset.guestPath,
|
|
416
|
-
contents: await
|
|
2040
|
+
contents: await readDeclared(root, asset, `${judge.kind} asset`)
|
|
417
2041
|
});
|
|
418
|
-
return
|
|
2042
|
+
return judge.kind === "checker" ? {
|
|
419
2043
|
kind: "checker",
|
|
420
|
-
runtimeProfile:
|
|
2044
|
+
runtimeProfile: judge.artifact.runtimeProfile,
|
|
421
2045
|
artifact,
|
|
422
2046
|
assets,
|
|
423
|
-
args:
|
|
2047
|
+
args: judge.args
|
|
424
2048
|
} : {
|
|
425
2049
|
kind: "interactive",
|
|
426
|
-
runtimeProfile:
|
|
2050
|
+
runtimeProfile: judge.artifact.runtimeProfile,
|
|
427
2051
|
artifact,
|
|
428
2052
|
assets,
|
|
429
|
-
args:
|
|
430
|
-
inputPath:
|
|
2053
|
+
args: judge.args,
|
|
2054
|
+
inputPath: judge.inputPath
|
|
2055
|
+
};
|
|
2056
|
+
}
|
|
2057
|
+
function localized(value, label, empty) {
|
|
2058
|
+
const input = record(value, label);
|
|
2059
|
+
exact(input, ["en", "zh-TW"], label);
|
|
2060
|
+
if (typeof input.en !== "string" || typeof input["zh-TW"] !== "string" || !empty && (!input.en.trim() || !input["zh-TW"].trim())) fail(`${label} must contain zh-TW and en strings.`);
|
|
2061
|
+
return {
|
|
2062
|
+
"zh-TW": input["zh-TW"],
|
|
2063
|
+
en: input.en
|
|
2064
|
+
};
|
|
2065
|
+
}
|
|
2066
|
+
function parseAuthoringSource(value) {
|
|
2067
|
+
const source = record(value, "repository authoring source");
|
|
2068
|
+
exact(source, [
|
|
2069
|
+
"contests",
|
|
2070
|
+
"problems",
|
|
2071
|
+
"schema"
|
|
2072
|
+
], "repository authoring source");
|
|
2073
|
+
if (source.schema !== AUTHORING_SCHEMA || !Array.isArray(source.problems)) fail(`repository authoring schema must be '${AUTHORING_SCHEMA}'.`);
|
|
2074
|
+
const judgeSource = parseRepositoryAuthoringJudges({
|
|
2075
|
+
schema: "wasm-oj-platform/repository-authoring-judges/v1",
|
|
2076
|
+
problems: source.problems.map((candidate) => {
|
|
2077
|
+
const problem = record(candidate, "authoring problem");
|
|
2078
|
+
return {
|
|
2079
|
+
slug: problem.slug,
|
|
2080
|
+
allowedProfiles: problem.allowedProfiles,
|
|
2081
|
+
judge: problem.judge
|
|
2082
|
+
};
|
|
2083
|
+
})
|
|
2084
|
+
});
|
|
2085
|
+
const problems = source.problems.map((candidate, index) => {
|
|
2086
|
+
const problem = record(candidate, `authoring problem ${index + 1}`);
|
|
2087
|
+
exact(problem, [
|
|
2088
|
+
"allowedProfiles",
|
|
2089
|
+
"authoringBundle",
|
|
2090
|
+
"judge",
|
|
2091
|
+
"order",
|
|
2092
|
+
"practiceEnabled",
|
|
2093
|
+
"slug",
|
|
2094
|
+
"summary",
|
|
2095
|
+
"title"
|
|
2096
|
+
], `authoring problem ${index + 1}`);
|
|
2097
|
+
if (!Number.isSafeInteger(problem.order) || problem.order !== index + 1 || typeof problem.practiceEnabled !== "boolean") fail(`authoring problem ${index + 1} order or practiceEnabled is invalid.`);
|
|
2098
|
+
const parsedJudge = judgeSource.problems[index];
|
|
2099
|
+
return {
|
|
2100
|
+
slug: parsedJudge.slug,
|
|
2101
|
+
order: problem.order,
|
|
2102
|
+
title: localized(problem.title, `authoring problem '${parsedJudge.slug}' title`, false),
|
|
2103
|
+
summary: localized(problem.summary, `authoring problem '${parsedJudge.slug}' summary`, true),
|
|
2104
|
+
practiceEnabled: problem.practiceEnabled,
|
|
2105
|
+
authoringBundle: normalizedPath(problem.authoringBundle, `authoring problem '${parsedJudge.slug}' bundle`),
|
|
2106
|
+
allowedProfiles: parsedJudge.allowedProfiles,
|
|
2107
|
+
judge: parsedJudge.judge
|
|
2108
|
+
};
|
|
2109
|
+
});
|
|
2110
|
+
if (!Array.isArray(source.contests)) fail("repository authoring source contests must be an array.");
|
|
2111
|
+
const contests = parseRepositoryContestsValue({
|
|
2112
|
+
schema: "wasm-oj-platform/contests/v2",
|
|
2113
|
+
contests: source.contests.map((candidate, index) => {
|
|
2114
|
+
const contest = record(candidate, `authoring contest ${index + 1}`);
|
|
2115
|
+
exact(contest, [
|
|
2116
|
+
"accessMode",
|
|
2117
|
+
"description",
|
|
2118
|
+
"rules",
|
|
2119
|
+
"slug",
|
|
2120
|
+
"status",
|
|
2121
|
+
"title"
|
|
2122
|
+
], `authoring contest ${index + 1}`);
|
|
2123
|
+
const rulesInput = record(contest.rules, `authoring contest ${index + 1} rules`);
|
|
2124
|
+
const rules = Object.hasOwn(rulesInput, "preset") ? expandContestRulesPreset(parseContestRulesPreset(rulesInput, `authoring contest ${index + 1} rules`)) : parseContestRules(rulesInput, `authoring contest ${index + 1} rules`);
|
|
2125
|
+
return {
|
|
2126
|
+
...contest,
|
|
2127
|
+
rules
|
|
2128
|
+
};
|
|
2129
|
+
})
|
|
2130
|
+
}).contests;
|
|
2131
|
+
const known = new Set(problems.map((problem) => problem.slug));
|
|
2132
|
+
for (const contest of contests) for (const problem of contest.rules.problems) if (!known.has(problem.slug)) fail(`contest '${contest.slug}' references unknown problem '${problem.slug}'.`);
|
|
2133
|
+
return {
|
|
2134
|
+
problems,
|
|
2135
|
+
contests
|
|
431
2136
|
};
|
|
432
2137
|
}
|
|
433
|
-
async function
|
|
434
|
-
const
|
|
435
|
-
|
|
436
|
-
|
|
2138
|
+
async function atomicWrite(root, output) {
|
|
2139
|
+
const absolute = resolveInside(root, output.path);
|
|
2140
|
+
const parent = path.dirname(absolute);
|
|
2141
|
+
await mkdir(parent, { recursive: true });
|
|
2142
|
+
if (!await anchoredPathHasNoSymlink(parent)) fail(`output parent for '${output.path}' must not traverse a symbolic link.`);
|
|
2143
|
+
const temporary = `${absolute}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
2144
|
+
try {
|
|
2145
|
+
await writeFile(temporary, output.bytes, {
|
|
2146
|
+
flag: "wx",
|
|
2147
|
+
mode: 420
|
|
2148
|
+
});
|
|
2149
|
+
await rename(temporary, absolute);
|
|
2150
|
+
} finally {
|
|
2151
|
+
await rm(temporary, { force: true });
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
async function build(options) {
|
|
2155
|
+
const source = parseAuthoringSource(parseJson(await readFile(options.root, options.sourcePath, JSON_MAX_BYTES), options.sourcePath));
|
|
2156
|
+
const problems = [];
|
|
437
2157
|
const outputs = [];
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
const entry = built.index.problems[position];
|
|
446
|
-
const practice = built.practiceProblems[position];
|
|
447
|
-
const authored = built.authoredProblems[position];
|
|
448
|
-
if (!authored || authored.id !== entry.id) fail(`authoring source for '${entry.id}' is unavailable.`);
|
|
449
|
-
const contestBytes = contestPublicProjectionBytes(practice, entry.bundle.sha256);
|
|
450
|
-
assertGeneratedSize(contestBytes, PUBLIC_BUNDLE_MAX_BYTES, `contest-public '${entry.id}' output`);
|
|
451
|
-
const contestSha256 = await sha256Hex(contestBytes);
|
|
452
|
-
const contestName = `${String(entry.number).padStart(3, "0")}-${entry.id}.${contestSha256}.contest.json`;
|
|
453
|
-
outputs.push({
|
|
454
|
-
path: path.posix.join(publicationDirectory, contestName),
|
|
455
|
-
label: `contest-public '${entry.id}' output`,
|
|
456
|
-
bytes: contestBytes
|
|
2158
|
+
for (const authored of source.problems) {
|
|
2159
|
+
const bundle = parseStandaloneProblemBundle$1(parseJson(await readFile(options.root, authored.authoringBundle, PACKAGE_MAX_BYTES), authored.authoringBundle));
|
|
2160
|
+
if (bundle.id !== authored.slug || bundle.number !== authored.order || !sameLocalizedText(bundle.title, authored.title)) fail(`authoring bundle '${authored.slug}' disagrees with its declarative identity.`);
|
|
2161
|
+
const practice = derivePracticePublic(bundle);
|
|
2162
|
+
const practiceBytes = canonicalJsonBytes({
|
|
2163
|
+
schema: BROWSER_PROBLEM_SCHEMA$1,
|
|
2164
|
+
problem: practice
|
|
457
2165
|
});
|
|
2166
|
+
const practiceDigest = await sha256(practiceBytes);
|
|
2167
|
+
const contestBytes = contestPublicProjectionBytes(practice);
|
|
458
2168
|
const encoded = await encodeJudgePackage({
|
|
459
|
-
judgeData: deriveJudgeData(
|
|
460
|
-
allowedProfiles:
|
|
461
|
-
judge: await
|
|
2169
|
+
judgeData: deriveJudgeData(bundle, Object.keys(authored.allowedProfiles)),
|
|
2170
|
+
allowedProfiles: authored.allowedProfiles,
|
|
2171
|
+
judge: await judgeInput(options.root, authored.judge)
|
|
462
2172
|
});
|
|
463
|
-
|
|
464
|
-
const
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
memoryLimitBytes: Math.max(...practice.scoring.policies.map((policy) => policy.limits.memoryLimitBytes))
|
|
468
|
-
});
|
|
469
|
-
assertJudgeDataMatchesPracticePublic(validated.judgeData, practice, Object.keys(sourceProblem.allowedProfiles));
|
|
470
|
-
const packageName = `${String(entry.number).padStart(3, "0")}-${entry.id}.${encoded.executionSemanticSha256}.wasmojjudge`;
|
|
2173
|
+
const base = `collection/problems/${String(authored.order).padStart(3, "0")}-${authored.slug}`;
|
|
2174
|
+
const practicePath = `${base}.practice.json`;
|
|
2175
|
+
const contestPath = `${base}.contest.json`;
|
|
2176
|
+
const judgePath = `${base}.wasmojjudge`;
|
|
471
2177
|
outputs.push({
|
|
472
|
-
path:
|
|
473
|
-
|
|
2178
|
+
path: practicePath,
|
|
2179
|
+
bytes: practiceBytes
|
|
2180
|
+
}, {
|
|
2181
|
+
path: contestPath,
|
|
2182
|
+
bytes: contestBytes
|
|
2183
|
+
}, {
|
|
2184
|
+
path: judgePath,
|
|
474
2185
|
bytes: encoded.bytes
|
|
475
2186
|
});
|
|
476
|
-
|
|
477
|
-
slug:
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
2187
|
+
problems.push({
|
|
2188
|
+
slug: authored.slug,
|
|
2189
|
+
order: authored.order,
|
|
2190
|
+
title: authored.title,
|
|
2191
|
+
summary: authored.summary,
|
|
2192
|
+
practiceEnabled: authored.practiceEnabled,
|
|
2193
|
+
practiceBundle: {
|
|
2194
|
+
path: practicePath,
|
|
2195
|
+
bytes: practiceBytes.byteLength,
|
|
2196
|
+
sha256: practiceDigest
|
|
2197
|
+
},
|
|
2198
|
+
contestBundle: {
|
|
2199
|
+
path: contestPath,
|
|
481
2200
|
bytes: contestBytes.byteLength,
|
|
482
|
-
sha256:
|
|
2201
|
+
sha256: await sha256(contestBytes)
|
|
483
2202
|
},
|
|
484
2203
|
judgePackage: {
|
|
485
|
-
|
|
2204
|
+
path: judgePath,
|
|
486
2205
|
bytes: encoded.bytes.byteLength,
|
|
487
2206
|
sha256: encoded.executionSemanticSha256
|
|
488
2207
|
}
|
|
489
2208
|
});
|
|
490
2209
|
}
|
|
491
|
-
const
|
|
492
|
-
schema:
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
const
|
|
497
|
-
|
|
498
|
-
|
|
2210
|
+
const problemManifest = {
|
|
2211
|
+
schema: "wasm-oj-platform/problems/v1",
|
|
2212
|
+
problems
|
|
2213
|
+
};
|
|
2214
|
+
parseRepositoryProblemsValue(problemManifest);
|
|
2215
|
+
const contestManifest = {
|
|
2216
|
+
schema: "wasm-oj-platform/contests/v2",
|
|
2217
|
+
contests: source.contests
|
|
2218
|
+
};
|
|
2219
|
+
parseRepositoryContestsValue(contestManifest);
|
|
2220
|
+
const root = {
|
|
2221
|
+
schema: "wasm-oj-platform/repository/v1",
|
|
2222
|
+
problems: PROBLEMS_PATH,
|
|
2223
|
+
contests: CONTESTS_PATH
|
|
2224
|
+
};
|
|
2225
|
+
parseRepositoryRootValue(root);
|
|
499
2226
|
outputs.push({
|
|
500
|
-
path:
|
|
501
|
-
|
|
502
|
-
|
|
2227
|
+
path: PROBLEMS_PATH,
|
|
2228
|
+
bytes: canonicalJsonBytes(problemManifest)
|
|
2229
|
+
}, {
|
|
2230
|
+
path: CONTESTS_PATH,
|
|
2231
|
+
bytes: canonicalJsonBytes(contestManifest)
|
|
2232
|
+
}, {
|
|
2233
|
+
path: ROOT_PATH,
|
|
2234
|
+
bytes: canonicalJsonBytes(root)
|
|
503
2235
|
});
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
outputs
|
|
507
|
-
};
|
|
2236
|
+
for (const output of outputs) await atomicWrite(options.root, output);
|
|
2237
|
+
return problems.length;
|
|
508
2238
|
}
|
|
509
|
-
function
|
|
510
|
-
const
|
|
511
|
-
|
|
512
|
-
return
|
|
2239
|
+
async function verifiedObject(root, descriptor, maximum) {
|
|
2240
|
+
const bytes = await readFile(root, descriptor.path, maximum, descriptor.bytes);
|
|
2241
|
+
if (await sha256(bytes) !== descriptor.sha256) fail(`'${descriptor.path}' failed digest verification.`);
|
|
2242
|
+
return bytes;
|
|
513
2243
|
}
|
|
514
|
-
async function
|
|
515
|
-
const
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
2244
|
+
async function verify(options) {
|
|
2245
|
+
const root = parseRepositoryRootValue(parseJson(await readFile(options.root, ROOT_PATH, JSON_MAX_BYTES), ROOT_PATH));
|
|
2246
|
+
const problems = parseRepositoryProblemsValue(parseJson(await readFile(options.root, root.problems, JSON_MAX_BYTES), root.problems));
|
|
2247
|
+
const contests = parseRepositoryContestsValue(parseJson(await readFile(options.root, root.contests, JSON_MAX_BYTES), root.contests));
|
|
2248
|
+
const known = new Set(problems.problems.map((problem) => problem.slug));
|
|
2249
|
+
for (const contest of contests.contests) for (const problem of contest.rules.problems) if (!known.has(problem.slug)) fail(`contest '${contest.slug}' references unknown problem '${problem.slug}'.`);
|
|
2250
|
+
for (const problem of problems.problems) {
|
|
2251
|
+
const practiceBytes = await verifiedObject(options.root, problem.practiceBundle, PUBLIC_MAX_BYTES);
|
|
2252
|
+
const practice = parseStandaloneProblemBundle$1(parseJson(practiceBytes, problem.practiceBundle.path));
|
|
2253
|
+
if (practice.id !== problem.slug || practice.number !== problem.order || !sameLocalizedText(practice.title, problem.title)) fail(`practice bundle '${problem.slug}' disagrees with problems.json.`);
|
|
2254
|
+
const contest = parseContestPublicProblemProjection(parseJson(await verifiedObject(options.root, problem.contestBundle, PUBLIC_MAX_BYTES), problem.contestBundle.path));
|
|
2255
|
+
const expectedContest = canonicalJsonBytes(deriveContestPublic(practice));
|
|
2256
|
+
if (!Buffer.from(canonicalJsonBytes(contest.problem)).equals(Buffer.from(expectedContest))) fail(`contest bundle '${problem.slug}' is not the public projection of its practice bundle.`);
|
|
2257
|
+
const packageBytes = await verifiedObject(options.root, problem.judgePackage, PACKAGE_MAX_BYTES);
|
|
2258
|
+
const validated = await validateJudgePackage(packageBytes, {
|
|
2259
|
+
expectedBytes: problem.judgePackage.bytes,
|
|
2260
|
+
expectedSha256: problem.judgePackage.sha256,
|
|
2261
|
+
memoryLimitBytes: Math.max(...practice.scoring.policies.map((policy) => policy.limits.memoryLimitBytes))
|
|
2262
|
+
});
|
|
2263
|
+
assertJudgeDataMatchesPracticePublic(validated.judgeData, practice, Object.keys(validated.manifest.allowedProfiles));
|
|
522
2264
|
}
|
|
523
|
-
|
|
524
|
-
await Promise.all(outputs.map((output) => assertOutputPathSafe(options, output.path, output.label)));
|
|
525
|
-
for (const output of outputs) await atomicRepositoryWrite(options, output.path, output.bytes);
|
|
2265
|
+
return problems.problems.length;
|
|
526
2266
|
}
|
|
527
2267
|
async function runCollectionCli(arguments_) {
|
|
528
2268
|
const options = parseOptions(arguments_);
|
|
529
|
-
await
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
const initialManagedOutput = options.managedSourcePath ? options.managedPath ?? path.posix.join(path.posix.dirname(options.indexPath), "managed.json") : void 0;
|
|
533
|
-
await assertNoFilesystemAliases(options, /* @__PURE__ */ new Set([options.sourcePath, ...options.managedSourcePath ? [options.managedSourcePath] : []]), [options.indexPath, ...initialManagedOutput ? [initialManagedOutput] : []]);
|
|
534
|
-
const built = await prepareCollection(options);
|
|
535
|
-
index = built.index;
|
|
536
|
-
const managed = options.managedSourcePath ? await prepareManagedCollection(options, built) : void 0;
|
|
537
|
-
await writePreparedCollection(options, /* @__PURE__ */ new Set([...built.inputs, ...managed?.inputs ?? []]), [...built.outputs, ...managed?.outputs ?? []]);
|
|
538
|
-
} else index = await validatePublishedCollection(options, true);
|
|
539
|
-
process.stdout.write(`${options.command} ok: ${index.problems.length} problems, revision ${index.revision}\n`);
|
|
2269
|
+
await assertRoot(options.root);
|
|
2270
|
+
const count = options.command === "build" ? await build(options) : await verify(options);
|
|
2271
|
+
process.stdout.write(`${options.command} ok: ${count} problems\n`);
|
|
540
2272
|
}
|
|
541
2273
|
if (process.argv[1] ? import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href : false) runCollectionCli(process.argv.slice(2)).catch((error) => {
|
|
542
2274
|
process.stderr.write(`woj organizer collection: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
543
2275
|
process.exitCode = 1;
|
|
544
2276
|
});
|
|
545
2277
|
//#endregion
|
|
546
|
-
export { BROWSER_COLLECTION_SCHEMA, BROWSER_PROBLEM_SCHEMA,
|
|
2278
|
+
export { BROWSER_COLLECTION_SCHEMA, BROWSER_PROBLEM_SCHEMA, CONTESTS_SCHEMA, PROBLEMS_SCHEMA, REPOSITORY_AUTHORING_JUDGES_SCHEMA, REPOSITORY_SCHEMA, parseProblemCollectionIndex, parseRepositoryAuthoringJudges, parseRepositoryContests, parseRepositoryContestsValue, parseRepositoryProblems, parseRepositoryProblemsValue, parseRepositoryRoot, parseRepositoryRootValue, parseStandaloneProblemBundle, problemCollectionRevision, runCollectionCli, validateRepositoryCatalog, verifyProblemBundleBytes, verifyProblemCollectionRevision };
|