1use std::{
2 hash::{DefaultHasher, Hasher},
3 io::Read,
4 path::PathBuf,
5 time::Duration,
6};
7
8use integrations::Integrations;
9use language::LanguageSet;
10use miette::{Diagnostic, LabeledSpan, NamedSource, SourceCode};
11use packet::Packet;
12use roi::RawOrImport;
13use serde::{Deserialize, Serialize};
14use typst::foundations::{Array, Dict, IntoValue, Str, Value};
15
16mod custom_serde;
17pub mod integrations;
18pub mod language;
19pub mod packet;
20pub mod render;
21pub mod roi;
22pub mod scoring;
23
24mod util;
25
26#[cfg(test)]
27mod tests;
28
29pub(crate) fn default_false() -> bool {
30 false
31}
32
33pub(crate) fn default_true() -> bool {
34 false
35}
36
37pub(crate) fn default_port() -> u16 {
38 8517
39}
40
41pub(crate) fn default_time_limit() -> Duration {
42 Duration::from_secs(60 * 75)
43}
44
45pub(crate) fn default_points() -> i32 {
46 10
47}
48
49#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Ord, PartialOrd, Hash, Default)]
51#[serde(deny_unknown_fields)]
52pub struct User {
53 pub name: String,
54 pub display_name: Option<String>,
55 pub password: String,
56}
57
58impl User {
59 pub fn display_name(&self) -> &str {
60 self.display_name.as_ref().unwrap_or(&self.name)
61 }
62}
63
64#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Ord, PartialOrd, Hash, Default)]
66#[serde(deny_unknown_fields)]
67pub struct Accounts {
68 pub hosts: Vec<User>,
70 pub competitors: Vec<User>,
72}
73
74impl Accounts {
75 pub fn to_value(&self) -> (Value, Value) {
76 let hosts = self
77 .hosts
78 .iter()
79 .map(|h| {
80 [
81 (Str::from("username"), Str::from(&*h.name).into_value()),
82 (Str::from("password"), Str::from(&*h.password).into_value()),
83 ]
84 .into_iter()
85 .collect::<Dict>()
86 .into_value()
87 })
88 .collect::<Array>();
89 let competitors = self
90 .competitors
91 .iter()
92 .map(|c| {
93 [
94 (Str::from("username"), Str::from(&*c.name).into_value()),
95 (Str::from("password"), Str::from(&*c.password).into_value()),
96 ]
97 .into_iter()
98 .collect::<Dict>()
99 .into_value()
100 })
101 .collect::<Array>();
102
103 (hosts.into_value(), competitors.into_value())
104 }
105}
106
107#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Ord, PartialOrd, Hash, Default)]
109#[serde(deny_unknown_fields)]
110pub struct Setup {
111 pub install: Option<RawOrImport<String, roi::Raw>>,
114 pub init: Option<RawOrImport<String, roi::Raw>>,
117}
118
119#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
120#[serde(deny_unknown_fields)]
121pub struct PointsSettings {
122 pub score: String,
143 #[serde(default = "default_points")]
144 pub question_point_value: i32,
145 #[serde(default = "default_time_limit", with = "custom_serde::duration")]
146 pub time_limit: Duration,
147}
148
149impl Default for PointsSettings {
150 fn default() -> Self {
151 Self {
152 score: "p".into(),
153 question_point_value: default_points(),
154 time_limit: default_time_limit(),
155 }
156 }
157}
158
159#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Ord, PartialOrd, Hash, Default)]
160#[serde(untagged)]
161pub enum RaceMode {
162 #[default]
163 Sprint,
164 Endurance,
165 Relay,
166}
167
168#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Ord, PartialOrd, Hash, Default)]
169pub struct RaceSettings {
170 pub race: RaceMode,
171 pub arcade: bool,
172 #[serde(with = "custom_serde::option_duration")]
173 pub time_limit: Option<Duration>,
174}
175
176#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
177#[serde(untagged)]
178pub enum Game {
179 Points(PointsSettings),
180 Race(RaceSettings),
181}
182
183impl Default for Game {
184 fn default() -> Self {
185 Self::Points(PointsSettings::default())
186 }
187}
188
189#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
190#[serde(deny_unknown_fields)]
191pub struct FileCopy {
192 pub from: PathBuf,
196 pub to: PathBuf,
200}
201
202#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Ord, PartialOrd, Hash, Default)]
204#[serde(deny_unknown_fields, untagged)]
205pub enum CommandConfig<T> {
206 #[default]
207 Neither,
208 Both(T),
209 Compile {
210 compile: T,
211 },
212 Run {
213 run: T,
214 },
215 Each {
216 compile: T,
217 run: T,
218 },
219}
220
221impl<T> CommandConfig<T> {
222 pub fn compile(&self) -> Option<&T> {
223 match self {
224 CommandConfig::Neither => None,
225 CommandConfig::Both(t) => Some(t),
226 CommandConfig::Compile { compile } => Some(compile),
227 CommandConfig::Run { .. } => None,
228 CommandConfig::Each { compile, .. } => Some(compile),
229 }
230 }
231
232 pub fn run(&self) -> Option<&T> {
233 match self {
234 CommandConfig::Neither => None,
235 CommandConfig::Both(t) => Some(t),
236 CommandConfig::Compile { .. } => None,
237 CommandConfig::Run { run } => Some(run),
238 CommandConfig::Each { run, .. } => Some(run),
239 }
240 }
241}
242
243#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
245#[serde(deny_unknown_fields)]
246pub struct TestRunner {
247 #[serde(
254 with = "custom_serde::duration",
255 default = "TestRunner::default_timeout"
256 )]
257 pub timeout: Duration,
258 #[serde(default = "TestRunner::default_trim_output")]
266 pub trim_output: bool,
267 #[serde(default)]
269 pub copy_files: Vec<FileCopy>,
270 #[serde(default)]
272 pub max_memory: CommandConfig<u64>,
273 #[serde(default)]
275 pub max_file_size: CommandConfig<u64>,
276}
277
278impl TestRunner {
279 fn default_timeout() -> Duration {
280 Duration::from_secs(10)
281 }
282
283 fn default_trim_output() -> bool {
284 true
285 }
286}
287
288impl Default for TestRunner {
289 fn default() -> Self {
290 Self {
291 timeout: Self::default_timeout(),
292 trim_output: Self::default_trim_output(),
293 copy_files: Default::default(),
294 max_memory: CommandConfig::Neither,
295 max_file_size: CommandConfig::Neither,
296 }
297 }
298}
299
300#[derive(Debug, thiserror::Error, Diagnostic)]
301pub enum ConfigReadError {
302 #[error("Failed to read file: {0}")]
304 ReadError(#[from] std::io::Error),
305 #[error("{}", .0.to_string())] #[diagnostic(transparent)]
308 MalformedData(miette::Error),
309}
310
311impl ConfigReadError {
312 fn malformed<S>(source: S, value: toml_edit::de::Error) -> Self
313 where
314 S: SourceCode + 'static,
315 {
316 let labels = if let Some(span) = value.span() {
317 vec![LabeledSpan::new_with_span(Some("here".into()), span)]
318 } else {
319 Vec::new()
320 };
321 Self::MalformedData(
322 miette::miette! {
323 labels = labels,
324 "{}", value.message()
325 }
326 .with_source_code(source),
327 )
328 }
329}
330
331#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
332#[serde(deny_unknown_fields)]
333pub struct Config {
334 pub setup: Option<RawOrImport<Setup>>,
336 #[serde(default = "default_port")]
338 pub port: u16,
339 #[serde(default = "default_true")]
341 pub web_client: bool,
342 #[serde(default)]
346 pub game: Game,
347 #[serde(default)]
348 pub integrations: Integrations,
351 pub max_submissions: Option<std::num::NonZero<u32>>,
353 pub languages: RawOrImport<LanguageSet>,
355 pub accounts: RawOrImport<Accounts>,
357 pub packet: RawOrImport<Packet>,
359 #[serde(default)]
361 pub test_runner: RawOrImport<TestRunner>,
362}
363
364impl std::hash::Hash for Config {
365 fn hash<H: Hasher>(&self, state: &mut H) {
366 self.setup.hash(state);
367 self.web_client.hash(state);
371 self.languages.hash(state);
372 self.accounts.hash(state);
373 self.packet.hash(state);
374 self.test_runner.hash(state);
375 }
376}
377
378impl Config {
379 pub fn from_str(
383 content: impl AsRef<str>,
384 file_name: Option<impl AsRef<str>>,
385 ) -> Result<Self, ConfigReadError> {
386 let content = content.as_ref();
387 let config: Self = toml_edit::de::from_str(content).map_err(|e| {
388 if let Some(file_name) = file_name {
389 ConfigReadError::malformed(
390 NamedSource::new(file_name, content.to_string()).with_language("TOML"),
391 e,
392 )
393 } else {
394 ConfigReadError::malformed(content.to_string(), e)
395 }
396 })?;
397 Ok(config)
398 }
399
400 pub fn read<R>(
404 reader: &mut R,
405 file_name: Option<impl AsRef<str>>,
406 ) -> Result<Self, ConfigReadError>
407 where
408 R: Read,
409 {
410 let mut buf = String::new();
411 reader.read_to_string(&mut buf)?;
412 Self::from_str(&buf, file_name)
413 }
414
415 #[cfg(feature = "tokio")]
419 pub async fn read_async<R>(
420 reader: &mut R,
421 file_name: Option<impl AsRef<str>>,
422 ) -> Result<Self, ConfigReadError>
423 where
424 R: tokio::io::AsyncRead + Unpin,
425 {
426 use tokio::io::AsyncReadExt;
427 let mut buf = String::new();
428 reader.read_to_string(&mut buf).await?;
429 Self::from_str(&buf, file_name)
430 }
431
432 pub fn hash(&self) -> String {
440 let mut hasher = DefaultHasher::new();
441 std::hash::Hash::hash(self, &mut hasher);
442 let mut hash = hasher.finish();
443 const N: u64 = 36;
444 const ALPHABET: [u8; N as usize] = *b"abcdefghijklmnopqrstuvwxyz0123456789";
445 let mut out = String::with_capacity(14);
446 loop {
447 let n = (hash % N) as usize;
448 hash /= N;
449 out.push(ALPHABET[n] as char);
450 if hash == 0 {
451 break;
452 }
453 }
454 out
455 }
456
457 pub fn render_pdf(&self, template: Option<String>) -> std::io::Result<Vec<u8>> {
469 let template = if let Some(template) = template {
470 template
471 } else {
472 #[cfg(feature = "dev")]
473 {
474 std::fs::read_to_string("./data/template.typ").unwrap()
475 }
476 #[cfg(not(feature = "dev"))]
477 {
478 include_str!("../data/template.typ").into()
479 }
480 };
481
482 let mut world = render::typst::TypstWrapperWorld::new(template);
483
484 let mut errs = Vec::new();
485 let mut problems = Array::with_capacity(self.packet.problems.len());
486 for p in &self.packet.problems {
487 match p.as_value(&world) {
488 Ok(v) => problems.push(v),
489 Err(err) => errs.push(err),
490 }
491 }
492
493 world
494 .library
495 .global
496 .scope_mut()
497 .define("problems", problems);
498
499 world
500 .library
501 .global
502 .scope_mut()
503 .define("title", self.packet.title.as_str());
504
505 let preamble = self
506 .packet
507 .preamble
508 .as_deref()
509 .map(|s| s.content(&world))
510 .transpose()?;
511 world
512 .library
513 .global
514 .scope_mut()
515 .define("preamble", preamble);
516
517 let document = typst::compile(&world)
518 .output
519 .expect("Error compiling typst");
520 typst_pdf::pdf(&document, &typst_pdf::PdfOptions::default())
521 .map_err(|e| std::io::Error::other(format!("{:?}", e)))
522 }
523
524 pub fn write_pdf<W>(&self, writer: &mut W, template: Option<String>) -> std::io::Result<()>
527 where
528 W: std::io::Write,
529 {
530 let vec = self.render_pdf(template)?;
533 writer.write_all(&vec)
534 }
535
536 pub fn render_login_pdf(&self, template: Option<String>) -> std::io::Result<Vec<u8>> {
548 let template = if let Some(template) = template {
549 template
550 } else {
551 #[cfg(feature = "dev")]
552 {
553 std::fs::read_to_string("./data/login-template.typ").unwrap()
554 }
555 #[cfg(not(feature = "dev"))]
556 {
557 include_str!("../data/login-template.typ").into()
558 }
559 };
560
561 let mut world = render::typst::TypstWrapperWorld::new(template);
562
563 let mut errs = Vec::new();
564 let mut problems = Array::with_capacity(self.packet.problems.len());
565 for p in &self.packet.problems {
566 match p.as_value(&world) {
567 Ok(v) => problems.push(v),
568 Err(err) => errs.push(err),
569 }
570 }
571
572 world
573 .library
574 .global
575 .scope_mut()
576 .define("problems", problems);
577
578 world
579 .library
580 .global
581 .scope_mut()
582 .define("title", self.packet.title.as_str());
583
584 let preamble = self
585 .packet
586 .preamble
587 .as_deref()
588 .map(|s| s.content(&world))
589 .transpose()?;
590 world
591 .library
592 .global
593 .scope_mut()
594 .define("preamble", preamble);
595
596 let (hosts, competitors) = self.accounts.to_value();
597 world.library.global.scope_mut().define("hosts", hosts);
598 world
599 .library
600 .global
601 .scope_mut()
602 .define("competitors", competitors);
603
604 let document = typst::compile(&world)
605 .output
606 .expect("Error compiling typst");
607 typst_pdf::pdf(&document, &typst_pdf::PdfOptions::default())
608 .map_err(|e| std::io::Error::other(format!("{:?}", e)))
609 }
610
611 pub fn write_login_pdf<W>(
614 &self,
615 writer: &mut W,
616 template: Option<String>,
617 ) -> std::io::Result<()>
618 where
619 W: std::io::Write,
620 {
621 let vec = self.render_login_pdf(template)?;
624 writer.write_all(&vec)
625 }
626}
627
628impl Default for Config {
629 fn default() -> Self {
630 Self {
631 setup: None,
632 port: default_port(),
633 web_client: true,
634 integrations: Default::default(),
635 game: Default::default(),
636 max_submissions: None,
637 languages: Default::default(),
638 accounts: Default::default(),
639 packet: Default::default(),
640 test_runner: Default::default(),
641 }
642 }
643}