1pub mod manifest;
5pub mod proto;
6pub mod ring_buffer;
7pub mod stats;
8pub mod tty;
9
10pub use camino::{Utf8Component, Utf8Path, Utf8PathBuf};
11pub use enumset::{enum_set, EnumSet};
12pub use nonempty::{nonempty, NonEmpty};
13
14use derive_more::{with_trait::Debug, Constructor, Display, From, Into};
15use enumset::EnumSetType;
16use get_size::GetSize;
17use hex::{self, FromHexError};
18use maelstrom_macro::pocket_definition;
19use serde::{Deserialize, Serialize};
20use std::{
21 error::Error,
22 fmt::{self, Formatter},
23 hash::Hash,
24 num::NonZeroU32,
25 result::Result,
26 str::{self, FromStr},
27 time::Duration,
28};
29use strum::{EnumCount, EnumIter};
30
31#[derive(
33 Copy, Clone, Debug, Deserialize, Display, Eq, From, Hash, Ord, PartialEq, PartialOrd, Serialize,
34)]
35pub struct ClientId(u32);
36
37impl ClientId {
38 pub fn as_u32(&self) -> u32 {
39 self.0
40 }
41}
42
43#[pocket_definition(export)]
45#[derive(
46 Copy,
47 Clone,
48 Debug,
49 Deserialize,
50 Display,
51 Eq,
52 From,
53 Hash,
54 Ord,
55 PartialEq,
56 PartialOrd,
57 Serialize,
58 Into,
59)]
60pub struct ClientJobId(u32);
61
62impl ClientJobId {
63 pub fn from_u32(v: u32) -> Self {
64 Self(v)
65 }
66}
67
68#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, PartialOrd, Ord, Serialize)]
69pub enum ArtifactType {
70 Tar,
72 Manifest,
74}
75
76#[macro_export]
77macro_rules! tar_digest {
78 ($digest:expr) => {
79 ($crate::digest!($digest), $crate::ArtifactType::Tar)
80 };
81}
82
83#[macro_export]
84macro_rules! manifest_digest {
85 ($digest:expr) => {
86 ($crate::digest!($digest), $crate::ArtifactType::Manifest)
87 };
88}
89
90#[derive(
92 Copy, Clone, Debug, Deserialize, Display, Eq, From, Hash, Ord, PartialEq, PartialOrd, Serialize,
93)]
94#[display("{cid}.{cjid}")]
95#[from(forward)]
96pub struct JobId {
97 pub cid: ClientId,
98 pub cjid: ClientJobId,
99}
100
101#[pocket_definition(export)]
102#[derive(Debug, Deserialize, EnumIter, EnumSetType, Serialize)]
103pub enum JobDevice {
104 Full,
105 Fuse,
106 Null,
107 Random,
108 Shm,
109 Tty,
110 Urandom,
111 Zero,
112}
113
114#[derive(Debug, Deserialize, EnumCount, EnumSetType, Serialize)]
115#[serde(rename_all = "kebab-case")]
116#[enumset(serialize_repr = "list")]
117pub enum JobDeviceForTomlAndJson {
118 Full,
119 Fuse,
120 Null,
121 Random,
122 Shm,
123 Tty,
124 Urandom,
125 Zero,
126}
127
128impl From<JobDeviceForTomlAndJson> for JobDevice {
129 fn from(value: JobDeviceForTomlAndJson) -> JobDevice {
130 match value {
131 JobDeviceForTomlAndJson::Full => JobDevice::Full,
132 JobDeviceForTomlAndJson::Fuse => JobDevice::Fuse,
133 JobDeviceForTomlAndJson::Null => JobDevice::Null,
134 JobDeviceForTomlAndJson::Random => JobDevice::Random,
135 JobDeviceForTomlAndJson::Shm => JobDevice::Shm,
136 JobDeviceForTomlAndJson::Tty => JobDevice::Tty,
137 JobDeviceForTomlAndJson::Urandom => JobDevice::Urandom,
138 JobDeviceForTomlAndJson::Zero => JobDevice::Zero,
139 }
140 }
141}
142
143#[derive(Clone, Debug, PartialEq, Serialize, Into)]
144#[serde(transparent)]
145pub struct NonRootUtf8PathBuf(Utf8PathBuf);
146
147impl<'de> Deserialize<'de> for NonRootUtf8PathBuf {
148 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
149 where
150 D: serde::de::Deserializer<'de>,
151 {
152 use serde::de::Error as _;
153 let path = Utf8PathBuf::deserialize(deserializer)?;
154 path.try_into()
155 .map_err(|e: NonRootUtf8PathBufTryFromError| D::Error::custom(e.to_string()))
156 }
157}
158
159#[derive(Debug, Display)]
160#[display("a path of \"/\" is not allowed")]
161pub struct NonRootUtf8PathBufTryFromError;
162
163impl Error for NonRootUtf8PathBufTryFromError {}
164
165impl TryFrom<Utf8PathBuf> for NonRootUtf8PathBuf {
166 type Error = NonRootUtf8PathBufTryFromError;
167
168 fn try_from(v: Utf8PathBuf) -> Result<Self, Self::Error> {
169 if v == "/" {
170 return Err(NonRootUtf8PathBufTryFromError);
171 }
172 Ok(Self(v))
173 }
174}
175
176#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
177#[serde(tag = "type")]
178#[serde(rename_all = "kebab-case")]
179#[serde(deny_unknown_fields)]
180pub enum JobMountForTomlAndJson {
181 Bind {
182 mount_point: NonRootUtf8PathBuf,
183 local_path: Utf8PathBuf,
184 #[serde(default)]
185 read_only: bool,
186 },
187 Devices {
188 devices: EnumSet<JobDeviceForTomlAndJson>,
189 },
190 Devpts {
191 mount_point: NonRootUtf8PathBuf,
192 },
193 Mqueue {
194 mount_point: NonRootUtf8PathBuf,
195 },
196 Proc {
197 mount_point: NonRootUtf8PathBuf,
198 },
199 Sys {
200 mount_point: NonRootUtf8PathBuf,
201 },
202 Tmp {
203 mount_point: NonRootUtf8PathBuf,
204 },
205}
206
207#[pocket_definition(export)]
208#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
209pub enum JobMount {
210 Bind {
211 mount_point: Utf8PathBuf,
212 local_path: Utf8PathBuf,
213 read_only: bool,
214 },
215 Devices {
216 devices: EnumSet<JobDevice>,
217 },
218 Devpts {
219 mount_point: Utf8PathBuf,
220 },
221 Mqueue {
222 mount_point: Utf8PathBuf,
223 },
224 Proc {
225 mount_point: Utf8PathBuf,
226 },
227 Sys {
228 mount_point: Utf8PathBuf,
229 },
230 Tmp {
231 mount_point: Utf8PathBuf,
232 },
233}
234
235#[macro_export]
236macro_rules! sys_mount {
237 ($mount_point:expr) => {
238 $crate::JobMount::Sys {
239 mount_point: $mount_point.into(),
240 }
241 };
242}
243
244#[macro_export]
245macro_rules! proc_mount {
246 ($mount_point:expr) => {
247 $crate::JobMount::Proc {
248 mount_point: $mount_point.into(),
249 }
250 };
251}
252
253#[macro_export]
254macro_rules! tmp_mount {
255 ($mount_point:expr) => {
256 $crate::JobMount::Tmp {
257 mount_point: $mount_point.into(),
258 }
259 };
260}
261
262#[macro_export]
263macro_rules! devices_mount {
264 ($devices:expr) => {
265 $crate::JobMount::Devices {
266 devices: $devices.into_iter().map($crate::JobDevice::from).collect(),
267 }
268 };
269}
270
271impl From<JobMountForTomlAndJson> for JobMount {
272 fn from(job_mount: JobMountForTomlAndJson) -> JobMount {
273 match job_mount {
274 JobMountForTomlAndJson::Bind {
275 mount_point,
276 local_path,
277 read_only,
278 } => JobMount::Bind {
279 mount_point: mount_point.into(),
280 local_path,
281 read_only,
282 },
283 JobMountForTomlAndJson::Devices { devices } => JobMount::Devices {
284 devices: devices.into_iter().map(JobDevice::from).collect(),
285 },
286 JobMountForTomlAndJson::Devpts { mount_point } => JobMount::Devpts {
287 mount_point: mount_point.into(),
288 },
289 JobMountForTomlAndJson::Mqueue { mount_point } => JobMount::Mqueue {
290 mount_point: mount_point.into(),
291 },
292 JobMountForTomlAndJson::Proc { mount_point } => JobMount::Proc {
293 mount_point: mount_point.into(),
294 },
295 JobMountForTomlAndJson::Sys { mount_point } => JobMount::Sys {
296 mount_point: mount_point.into(),
297 },
298 JobMountForTomlAndJson::Tmp { mount_point } => JobMount::Tmp {
299 mount_point: mount_point.into(),
300 },
301 }
302 }
303}
304
305#[pocket_definition(export)]
306#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
307#[serde(rename_all = "kebab-case")]
308pub enum JobNetwork {
309 #[default]
310 Disabled,
311 Loopback,
312 Local,
313}
314
315#[pocket_definition(export)]
316#[derive(Clone, Debug, Default, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
317pub struct CaptureFileSystemChanges {
318 pub upper: Utf8PathBuf,
319 pub work: Utf8PathBuf,
320}
321
322#[pocket_definition(export)]
323#[derive(Clone, Debug, Default, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
324#[serde(rename_all = "kebab-case")]
325pub enum JobRootOverlay {
326 #[default]
327 None,
328 Tmp,
329 Local(CaptureFileSystemChanges),
330}
331
332#[pocket_definition(export)]
334#[derive(
335 Copy,
336 Clone,
337 Debug,
338 Deserialize,
339 Display,
340 Eq,
341 From,
342 Hash,
343 Ord,
344 PartialEq,
345 PartialOrd,
346 Serialize,
347 Into,
348)]
349pub struct UserId(u32);
350
351impl UserId {
352 pub fn new(v: u32) -> Self {
353 Self(v)
354 }
355}
356
357#[pocket_definition(export)]
359#[derive(
360 Copy,
361 Clone,
362 Debug,
363 Deserialize,
364 Display,
365 Eq,
366 From,
367 Hash,
368 Ord,
369 PartialEq,
370 PartialOrd,
371 Serialize,
372 Into,
373)]
374pub struct GroupId(u32);
375
376impl GroupId {
377 pub fn new(v: u32) -> Self {
378 Self(v)
379 }
380}
381
382#[pocket_definition(export)]
384#[derive(
385 Copy, Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Into,
386)]
387#[into(u32)]
388pub struct Timeout(NonZeroU32);
389
390impl Timeout {
391 pub fn new(timeout: u32) -> Option<Self> {
392 NonZeroU32::new(timeout).map(Self)
393 }
394}
395
396impl TryFrom<u32> for Timeout {
397 type Error = std::num::TryFromIntError;
398
399 fn try_from(timeout: u32) -> std::result::Result<Self, std::num::TryFromIntError> {
400 Ok(Self(timeout.try_into()?))
401 }
402}
403
404impl From<Timeout> for Duration {
405 fn from(timeout: Timeout) -> Duration {
406 Duration::from_secs(timeout.0.get().into())
407 }
408}
409
410#[pocket_definition(export)]
412#[derive(Copy, Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
413pub struct WindowSize {
414 pub rows: u16,
415 pub columns: u16,
416}
417
418impl WindowSize {
419 pub fn new(rows: u16, columns: u16) -> Self {
420 Self { rows, columns }
421 }
422}
423
424#[pocket_definition(export)]
426#[derive(Copy, Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
427pub struct JobTty {
428 pub socket_address: [u8; 6],
431
432 pub window_size: WindowSize,
434}
435
436impl JobTty {
437 pub fn new(socket_address: &[u8; 6], window_size: WindowSize) -> Self {
438 let socket_address = *socket_address;
439 Self {
440 socket_address,
441 window_size,
442 }
443 }
444}
445
446#[derive(Clone, Debug, Deserialize, PartialEq, Eq, PartialOrd, Ord, Serialize)]
448pub struct JobSpec {
449 pub program: Utf8PathBuf,
450 pub arguments: Vec<String>,
451 pub environment: Vec<String>,
452 pub layers: NonEmpty<(Sha256Digest, ArtifactType)>,
453 pub mounts: Vec<JobMount>,
454 pub network: JobNetwork,
455 pub root_overlay: JobRootOverlay,
456 pub working_directory: Utf8PathBuf,
457 pub user: UserId,
458 pub group: GroupId,
459 pub timeout: Option<Timeout>,
460 pub estimated_duration: Option<Duration>,
461 pub allocate_tty: Option<JobTty>,
462 pub priority: i8,
463}
464
465impl JobSpec {
466 pub fn must_be_run_locally(&self) -> bool {
467 self.network == JobNetwork::Local
468 || self
469 .mounts
470 .iter()
471 .any(|mount| matches!(mount, JobMount::Bind { .. }))
472 || matches!(&self.root_overlay, JobRootOverlay::Local { .. })
473 || self.allocate_tty.is_some()
474 }
475}
476
477#[macro_export]
478macro_rules! job_spec {
479 (@expand [$program:expr, [$($layer:expr),+ $(,)?]] [] -> [$($($fields:tt)+)?]) => {
480 $crate::JobSpec {
481 $($($fields)+,)?
482 .. $crate::JobSpec {
483 program: $program.into(),
484 arguments: Default::default(),
485 environment: Default::default(),
486 layers: $crate::nonempty![$($layer),+],
487 mounts: Default::default(),
488 network: Default::default(),
489 root_overlay: Default::default(),
490 working_directory: "/".into(),
491 user: 0.into(),
492 group: 0.into(),
493 timeout: Default::default(),
494 estimated_duration: Default::default(),
495 allocate_tty: Default::default(),
496 priority: Default::default(),
497 }
498 }
499 };
500 (@expand [$($required:tt)+] [arguments: [$($($argument:expr),+ $(,)?)?] $(,$($field_in:tt)*)?] -> [$($($field_out:tt)+)?]) => {
501 $crate::job_spec!(@expand [$($required)+] [$($($field_in)*)?] ->
502 [$($($field_out)+,)? arguments: vec![$($($argument.into()),+)?]
503 ])
504 };
505 (@expand [$($required:tt)+] [environment: [$($($var:expr),+ $(,)?)?] $(,$($field_in:tt)*)?] -> [$($($field_out:tt)+)?]) => {
506 $crate::job_spec!(@expand [$($required)+] [$($($field_in)*)?] ->
507 [$($($field_out)+,)? environment: vec![$($($var.into()),+)?]
508 ])
509 };
510 (@expand [$($required:tt)+] [mounts: [$($mount:tt)*] $(,$($field_in:tt)*)?] -> [$($($field_out:tt)+)?]) => {
511 $crate::job_spec!(@expand [$($required)+] [$($($field_in)*)?] ->
512 [$($($field_out)+,)? mounts: vec![$($mount)*]])
513 };
514 (@expand [$($required:tt)+] [network: $network:expr $(,$($field_in:tt)*)?] -> [$($($field_out:tt)+)?]) => {
515 $crate::job_spec!(@expand [$($required)+] [$($($field_in)*)?] ->
516 [$($($field_out)+,)? network: $network])
517 };
518 (@expand [$($required:tt)+] [root_overlay: $root_overlay:expr $(,$($field_in:tt)*)?] -> [$($($field_out:tt)+)?]) => {
519 $crate::job_spec!(@expand [$($required)+] [$($($field_in)*)?] ->
520 [$($($field_out)+,)? root_overlay: $root_overlay])
521 };
522 (@expand [$($required:tt)+] [working_directory: $working_directory:expr $(,$($field_in:tt)*)?] -> [$($($field_out:tt)+)?]) => {
523 $crate::job_spec!(@expand [$($required)+] [$($($field_in)*)?] ->
524 [$($($field_out)+,)? working_directory: $working_directory.into()])
525 };
526 (@expand [$($required:tt)+] [user: $user:expr $(,$($field_in:tt)*)?] -> [$($($field_out:tt)+)?]) => {
527 $crate::job_spec!(@expand [$($required)+] [$($($field_in)*)?] ->
528 [$($($field_out)+,)? user: $user.into()])
529 };
530 (@expand [$($required:tt)+] [group: $group:expr $(,$($field_in:tt)*)?] -> [$($($field_out:tt)+)?]) => {
531 $crate::job_spec!(@expand [$($required)+] [$($($field_in)*)?] ->
532 [$($($field_out)+,)? group: $group.into()])
533 };
534 (@expand [$($required:tt)+] [timeout: $timeout:expr $(,$($field_in:tt)*)?] -> [$($($field_out:tt)+)?]) => {
535 $crate::job_spec!(@expand [$($required)+] [$($($field_in)*)?] ->
536 [$($($field_out)+,)? timeout: $crate::Timeout::new($timeout)])
537 };
538 (@expand [$($required:tt)+] [estimated_duration: $duration:expr $(,$($field_in:tt)*)?] -> [$($($field_out:tt)+)?]) => {
539 $crate::job_spec!(@expand [$($required)+] [$($($field_in)*)?] ->
540 [$($($field_out)+,)? estimated_duration: Some($duration)])
541 };
542 (@expand [$($required:tt)+] [allocate_tty: $tty:expr $(,$($field_in:tt)*)?] -> [$($($field_out:tt)+)?]) => {
543 $crate::job_spec!(@expand [$($required)+] [$($($field_in)*)?] ->
544 [$($($field_out)+,)? allocate_tty: Some($tty)])
545 };
546 (@expand [$($required:tt)+] [priority: $priority:expr $(,$($field_in:tt)*)?] -> [$($($field_out:tt)+)?]) => {
547 $crate::job_spec!(@expand [$($required)+] [$($($field_in)*)?] ->
548 [$($($field_out)+,)? priority: $priority])
549 };
550 ($program:expr, [$($layer:expr),+ $(,)?] $(,$($field_in:tt)*)?) => {
551 $crate::job_spec!(@expand [$program, [$($layer),+]] [$($($field_in)*)?] -> [])
552 };
553}
554
555#[pocket_definition(export)]
558#[derive(Clone, Debug, Deserialize, PartialEq, Eq, PartialOrd, Ord, Serialize)]
559pub enum JobTerminationStatus {
560 Exited(u8),
561 Signaled(u8),
562}
563
564#[pocket_definition(export)]
566#[derive(Clone, Debug, Display, Deserialize, PartialEq, Eq, PartialOrd, Ord, Serialize)]
567pub enum JobOutputResult {
568 None,
570
571 #[display("{}", String::from_utf8_lossy(_0))]
573 Inline(#[debug("{}", String::from_utf8_lossy(_0))] Box<[u8]>),
574
575 #[display("{}<{truncated} bytes truncated>", String::from_utf8_lossy(first))]
579 Truncated {
580 #[debug("{}", String::from_utf8_lossy(first))]
581 first: Box<[u8]>,
582 truncated: u64,
583 },
584 }
590
591#[pocket_definition(export)]
595#[derive(Clone, Debug, Deserialize, PartialEq, Eq, PartialOrd, Ord, Serialize)]
596pub struct JobEffects {
597 pub stdout: JobOutputResult,
598 pub stderr: JobOutputResult,
599 pub duration: Duration,
600}
601
602#[pocket_definition(export)]
605#[derive(Clone, Debug, Deserialize, PartialEq, Eq, PartialOrd, Ord, Serialize)]
606pub struct JobCompleted {
607 pub status: JobTerminationStatus,
608 pub effects: JobEffects,
609}
610
611#[pocket_definition(export)]
613#[derive(Clone, Debug, Deserialize, PartialEq, Eq, PartialOrd, Ord, Serialize)]
614pub enum JobOutcome {
615 Completed(JobCompleted),
616 TimedOut(JobEffects),
617}
618
619#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
622pub enum JobError<T> {
623 Execution(T),
628
629 System(T),
633}
634
635impl<T> JobError<T> {
636 pub fn map<U>(self, f: impl FnOnce(T) -> U) -> JobError<U> {
637 match self {
638 JobError::Execution(e) => JobError::Execution(f(e)),
639 JobError::System(e) => JobError::System(f(e)),
640 }
641 }
642}
643
644pub type JobResult<T, E> = Result<T, JobError<E>>;
646
647pub type JobOutcomeResult = JobResult<JobOutcome, String>;
650
651#[pocket_definition(export)]
652#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
653pub enum JobWorkerStatus {
654 WaitingForLayers,
655 WaitingToExecute,
656 Executing,
657}
658
659#[pocket_definition(export)]
660#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
661pub enum JobBrokerStatus {
662 WaitingForLayers,
663 WaitingForWorker,
664 AtWorker(WorkerId, JobWorkerStatus),
665}
666
667#[pocket_definition(export)]
669#[derive(
670 Clone,
671 Copy,
672 Debug,
673 Default,
674 Deserialize,
675 Display,
676 Eq,
677 From,
678 Hash,
679 Into,
680 Ord,
681 PartialEq,
682 PartialOrd,
683 Serialize,
684)]
685pub struct WorkerId(u32);
686
687#[derive(
689 Copy, Clone, Debug, Deserialize, Display, Eq, From, Hash, Ord, PartialEq, PartialOrd, Serialize,
690)]
691pub struct MonitorId(u32);
692
693impl MonitorId {
694 pub fn as_u32(&self) -> u32 {
695 self.0
696 }
697}
698
699#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
700pub enum ArtifactUploadLocation {
701 TcpUpload,
702 Remote,
703}
704
705#[derive(
707 Clone, Constructor, Debug, Deserialize, Eq, GetSize, Hash, Ord, PartialEq, PartialOrd, Serialize,
708)]
709pub struct Sha256Digest(#[debug("{self}")] [u8; 32]);
710
711impl Sha256Digest {
712 pub fn verify(&self, expected: &Self) -> Result<(), Sha256DigestVerificationError> {
714 if *self != *expected {
715 Err(Sha256DigestVerificationError::new(
716 self.clone(),
717 expected.clone(),
718 ))
719 } else {
720 Ok(())
721 }
722 }
723
724 pub fn as_bytes(&self) -> &[u8] {
725 self.0.as_ref()
726 }
727}
728
729#[derive(Debug, Display)]
730#[display("failed to convert to SHA-256 digest")]
731pub struct Sha256DigestTryFromError;
732
733impl Error for Sha256DigestTryFromError {}
734
735impl TryFrom<Vec<u8>> for Sha256Digest {
736 type Error = Sha256DigestTryFromError;
737
738 fn try_from(bytes: Vec<u8>) -> Result<Self, Self::Error> {
739 Ok(Self(
740 bytes.try_into().map_err(|_| Sha256DigestTryFromError)?,
741 ))
742 }
743}
744
745impl From<Sha256Digest> for Vec<u8> {
746 fn from(d: Sha256Digest) -> Self {
747 d.0.to_vec()
748 }
749}
750
751impl From<u64> for Sha256Digest {
752 fn from(input: u64) -> Self {
753 let mut bytes = [0; 32];
754 bytes[24..].copy_from_slice(&input.to_be_bytes());
755 Sha256Digest(bytes)
756 }
757}
758
759impl FromStr for Sha256Digest {
760 type Err = FromHexError;
761
762 fn from_str(value: &str) -> Result<Self, Self::Err> {
763 let mut bytes = [0; 32];
764 hex::decode_to_slice(value, &mut bytes).map(|_| Sha256Digest(bytes))
765 }
766}
767
768impl fmt::Display for Sha256Digest {
769 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
770 let mut bytes = [0; 64];
771 hex::encode_to_slice(self.0, &mut bytes).unwrap();
772 f.pad(unsafe { str::from_utf8_unchecked(&bytes) })
773 }
774}
775
776#[macro_export]
777macro_rules! digest {
778 ($n:expr) => {
779 $crate::Sha256Digest::from(u64::try_from($n).unwrap())
780 };
781}
782
783#[derive(Debug, Display)]
785#[display("mismatched SHA-256 digest (expected {expected}, found {actual})")]
786pub struct Sha256DigestVerificationError {
787 pub actual: Sha256Digest,
788 pub expected: Sha256Digest,
789}
790
791impl Sha256DigestVerificationError {
792 pub fn new(actual: Sha256Digest, expected: Sha256Digest) -> Self {
793 Sha256DigestVerificationError { actual, expected }
794 }
795}
796
797impl Error for Sha256DigestVerificationError {}
798
799#[cfg(test)]
800mod tests {
801 use super::*;
802 use enumset::enum_set;
803 use heck::ToKebabCase;
804 use indoc::indoc;
805 use strum::IntoEnumIterator as _;
806
807 #[test]
808 fn client_id_display() {
809 assert_eq!(format!("{}", ClientId::from(100)), "100");
810 assert_eq!(format!("{}", ClientId::from(0)), "0");
811 assert_eq!(format!("{:03}", ClientId::from(0)), "000");
812 assert_eq!(format!("{:3}", ClientId::from(43)), " 43");
813 }
814
815 #[test]
816 fn client_job_id_display() {
817 assert_eq!(format!("{}", ClientJobId::from(100)), "100");
818 assert_eq!(format!("{}", ClientJobId::from(0)), "0");
819 assert_eq!(format!("{:03}", ClientJobId::from(0)), "000");
820 assert_eq!(format!("{:3}", ClientJobId::from(43)), " 43");
821 }
822
823 #[test]
824 fn user_id_display() {
825 assert_eq!(format!("{}", UserId::from(100)), "100");
826 assert_eq!(format!("{}", UserId::from(0)), "0");
827 assert_eq!(format!("{:03}", UserId::from(0)), "000");
828 assert_eq!(format!("{:3}", UserId::from(43)), " 43");
829 }
830
831 #[test]
832 fn group_id_display() {
833 assert_eq!(format!("{}", GroupId::from(100)), "100");
834 assert_eq!(format!("{}", GroupId::from(0)), "0");
835 assert_eq!(format!("{:03}", GroupId::from(0)), "000");
836 assert_eq!(format!("{:3}", GroupId::from(43)), " 43");
837 }
838
839 #[test]
840 fn worker_id_display() {
841 assert_eq!(format!("{}", WorkerId::from(100)), "100");
842 assert_eq!(format!("{}", WorkerId::from(0)), "0");
843 assert_eq!(format!("{:03}", WorkerId::from(0)), "000");
844 assert_eq!(format!("{:3}", WorkerId::from(43)), " 43");
845 }
846
847 #[test]
848 fn job_id_from() {
849 assert_eq!(
850 JobId {
851 cid: ClientId::from(1),
852 cjid: ClientJobId::from(2)
853 },
854 JobId::from((1, 2))
855 );
856 }
857
858 #[test]
859 fn job_id_display() {
860 assert_eq!(format!("{}", JobId::from((0, 0))), "0.0");
861 }
862
863 #[test]
864 fn sha256_digest_from_u64() {
865 assert_eq!(
866 Sha256Digest::from(0x123456789ABCDEF0u64),
867 Sha256Digest([
868 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x12, 0x34,
869 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0,
870 ])
871 );
872 }
873
874 #[test]
875 fn sha256_digest_from_str_ok() {
876 assert_eq!(
877 "101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f"
878 .parse::<Sha256Digest>()
879 .unwrap(),
880 Sha256Digest([
881 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d,
882 0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b,
883 0x2c, 0x2d, 0x2e, 0x2f,
884 ])
885 );
886 }
887
888 #[test]
889 fn sha256_digest_from_str_wrong_length() {
890 assert_eq!(
891 "101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f0"
892 .parse::<Sha256Digest>()
893 .unwrap_err(),
894 FromHexError::OddLength
895 );
896 assert_eq!(
897 "101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f0f"
898 .parse::<Sha256Digest>()
899 .unwrap_err(),
900 FromHexError::InvalidStringLength
901 );
902 assert_eq!(
903 "101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e"
904 .parse::<Sha256Digest>()
905 .unwrap_err(),
906 FromHexError::InvalidStringLength
907 );
908 }
909
910 #[test]
911 fn sha256_digest_from_str_bad_chars() {
912 assert_eq!(
913 " 01112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f"
914 .parse::<Sha256Digest>()
915 .unwrap_err(),
916 FromHexError::InvalidHexCharacter { c: ' ', index: 0 }
917 );
918 assert_eq!(
919 "101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2g"
920 .parse::<Sha256Digest>()
921 .unwrap_err(),
922 FromHexError::InvalidHexCharacter { c: 'g', index: 63 }
923 );
924 }
925
926 #[test]
927 fn sha256_digest_display_round_trip() {
928 let s = "101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f";
929 assert_eq!(s, s.parse::<Sha256Digest>().unwrap().to_string());
930 }
931
932 #[test]
933 fn sha256_digest_display_padding() {
934 let d = "101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f"
935 .parse::<Sha256Digest>()
936 .unwrap();
937 assert_eq!(
938 format!("{d:<70}"),
939 "101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f "
940 );
941 assert_eq!(
942 format!("{d:0>70}"),
943 "000000101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f"
944 );
945 }
946
947 #[test]
948 fn sha256_digest_debug() {
949 let d = "101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f"
950 .parse::<Sha256Digest>()
951 .unwrap();
952 assert_eq!(
953 format!("{d:?}"),
954 "Sha256Digest(101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f)"
955 );
956 assert_eq!(
957 format!("{d:80?}"),
958 "Sha256Digest(101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f)"
959 );
960 assert_eq!(
961 format!("{d:#?}"),
962 indoc! {
963 "Sha256Digest(
964 101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f,
965 )"
966 }
967 );
968 }
969
970 trait AssertError {
971 fn assert_error(&self, expected: &str);
972 }
973
974 impl AssertError for toml::de::Error {
975 fn assert_error(&self, expected: &str) {
976 let message = self.message();
977 assert!(message.starts_with(expected), "message: {message}");
978 }
979 }
980
981 #[track_caller]
982 fn deserialize_value<T: for<'a> Deserialize<'a>>(file: &str) -> T {
983 T::deserialize(toml::de::ValueDeserializer::new(file)).unwrap()
984 }
985
986 #[track_caller]
987 fn deserialize_value_error<T: for<'a> Deserialize<'a> + Debug>(file: &str) -> toml::de::Error {
988 match T::deserialize(toml::de::ValueDeserializer::new(file)) {
989 Err(err) => err,
990 Ok(val) => panic!("expected a toml error but instead got value: {val:?}"),
991 }
992 }
993
994 #[test]
995 fn job_device_for_toml_and_json_enum_set_deserializes_from_list() {
996 let devices: EnumSet<JobDeviceForTomlAndJson> = deserialize_value(r#"["full", "null"]"#);
997 let devices: EnumSet<_> = devices.into_iter().map(Into::<JobDevice>::into).collect();
998 assert_eq!(devices, enum_set!(JobDevice::Full | JobDevice::Null));
999 }
1000
1001 #[test]
1002 fn job_device_for_toml_and_json_enum_set_deserialize_unknown_field() {
1003 deserialize_value_error::<EnumSet<JobDeviceForTomlAndJson>>(r#"["bull", "null"]"#)
1004 .assert_error("unknown variant `bull`");
1005 }
1006
1007 #[test]
1008 fn job_device_for_toml_and_json_and_job_device_match() {
1009 for job_device in JobDevice::iter() {
1010 let repr = format!(r#""{}""#, format!("{job_device:?}").to_kebab_case());
1011 assert_eq!(
1012 JobDevice::from(deserialize_value::<JobDeviceForTomlAndJson>(&repr)),
1013 job_device
1014 );
1015 }
1016 assert_eq!(JobDevice::iter().count(), JobDeviceForTomlAndJson::COUNT);
1017 }
1018
1019 #[test]
1020 fn non_root_utf8_path_buf_deserialize_not_root() {
1021 let path_buf: NonRootUtf8PathBuf = deserialize_value(r#""foo""#);
1022 assert_eq!(path_buf, NonRootUtf8PathBuf("foo".into()));
1023 }
1024
1025 #[test]
1026 fn non_root_utf8_path_buf_deserialize_root() {
1027 deserialize_value_error::<NonRootUtf8PathBuf>(r#""/""#)
1028 .assert_error(r#"a path of "/" is not allowed"#);
1029 }
1030
1031 #[test]
1032 fn job_mount_for_toml_and_json_deserialize() {
1033 let job_mounts: Vec<JobMountForTomlAndJson> = deserialize_value(
1034 r#"[
1035 { type = "bind", mount_point = "/mnt", local_path = "/a", read_only = true },
1036 { type = "devices", devices = [ "tty", "shm" ] },
1037 { type = "devpts", mount_point = "/dev/pts" },
1038 { type = "mqueue", mount_point = "/dev/mqueue" },
1039 { type = "proc", mount_point = "/proc" },
1040 { type = "sys", mount_point = "/sys" },
1041 { type = "tmp", mount_point = "/tmp" },
1042 ]"#,
1043 );
1044 let job_mounts: Vec<JobMount> = job_mounts.into_iter().map(|mount| mount.into()).collect();
1045 assert_eq!(
1046 job_mounts,
1047 vec![
1048 JobMount::Bind {
1049 mount_point: "/mnt".into(),
1050 local_path: "/a".into(),
1051 read_only: true,
1052 },
1053 JobMount::Devices {
1054 devices: enum_set!(JobDevice::Tty | JobDevice::Shm),
1055 },
1056 JobMount::Devpts {
1057 mount_point: "/dev/pts".into(),
1058 },
1059 JobMount::Mqueue {
1060 mount_point: "/dev/mqueue".into(),
1061 },
1062 JobMount::Proc {
1063 mount_point: "/proc".into(),
1064 },
1065 JobMount::Sys {
1066 mount_point: "/sys".into(),
1067 },
1068 JobMount::Tmp {
1069 mount_point: "/tmp".into(),
1070 },
1071 ]
1072 );
1073 }
1074
1075 #[test]
1076 fn job_mount_for_toml_and_json_deserialize_invalid() {
1077 deserialize_value_error::<JobMountForTomlAndJson>(
1078 r#"{ type = "foo", mount_point = "/mnt" }"#,
1079 )
1080 .assert_error("unknown variant `foo`");
1081 }
1082
1083 #[test]
1084 fn job_mount_for_toml_and_json_deserialize_bind_mount_missing_read_only() {
1085 let job_mount: JobMountForTomlAndJson =
1086 deserialize_value(r#"{ type = "bind", mount_point = "/mnt", local_path = "/a" }"#);
1087 let job_mount: JobMount = job_mount.into();
1088 assert_eq!(
1089 job_mount,
1090 JobMount::Bind {
1091 mount_point: "/mnt".into(),
1092 local_path: "/a".into(),
1093 read_only: false,
1094 },
1095 );
1096 }
1097
1098 #[test]
1099 fn job_mount_for_toml_and_json_deserialize_root_mount_point() {
1100 let mounts = [
1101 r#"{ type = "bind", mount_point = "/", local_path = "/a" }"#,
1102 r#"{ type = "devpts", mount_point = "/" }"#,
1103 r#"{ type = "mqueue", mount_point = "/" }"#,
1104 r#"{ type = "proc", mount_point = "/" }"#,
1105 r#"{ type = "sys", mount_point = "/" }"#,
1106 r#"{ type = "tmp", mount_point = "/" }"#,
1107 ];
1108 for mount in mounts {
1109 deserialize_value_error::<JobMountForTomlAndJson>(mount)
1110 .assert_error(r#"a path of "/" is not allowed"#);
1111 }
1112 }
1113
1114 #[test]
1115 fn job_network_deserialize() {
1116 assert_eq!(
1117 deserialize_value::<JobNetwork>(r#""disabled""#),
1118 JobNetwork::Disabled
1119 );
1120 assert_eq!(
1121 deserialize_value::<JobNetwork>(r#""loopback""#),
1122 JobNetwork::Loopback
1123 );
1124 assert_eq!(
1125 deserialize_value::<JobNetwork>(r#""local""#),
1126 JobNetwork::Local
1127 );
1128 deserialize_value_error::<JobNetwork>(r#""foo""#).assert_error("unknown variant `foo`");
1129 }
1130
1131 #[test]
1132 fn job_spec_must_be_run_locally_network() {
1133 let spec = job_spec!("foo", [tar_digest!(0)]);
1134 assert!(!spec.must_be_run_locally());
1135
1136 let spec = job_spec!("foo", [tar_digest!(0)], network: JobNetwork::Loopback);
1137 assert!(!spec.must_be_run_locally());
1138
1139 let spec = job_spec!("foo", [tar_digest!(0)], network: JobNetwork::Local);
1140 assert!(spec.must_be_run_locally());
1141
1142 let spec = job_spec!("foo", [tar_digest!(0)], network: JobNetwork::Disabled);
1143 assert!(!spec.must_be_run_locally());
1144 }
1145
1146 #[test]
1147 fn job_spec_must_be_run_locally_mounts() {
1148 let spec = job_spec!("foo", [tar_digest!(0)]);
1149 assert!(!spec.must_be_run_locally());
1150
1151 let spec = job_spec! {
1152 "foo",
1153 [tar_digest!(0)],
1154 mounts: [
1155 JobMount::Sys {
1156 mount_point: Utf8PathBuf::from("/sys"),
1157 },
1158 JobMount::Bind {
1159 mount_point: Utf8PathBuf::from("/bind"),
1160 local_path: Utf8PathBuf::from("/a"),
1161 read_only: false,
1162 },
1163 ],
1164 };
1165 assert!(spec.must_be_run_locally());
1166 }
1167
1168 #[test]
1169 fn job_spec_must_be_run_locally_root_overlay() {
1170 let spec = job_spec!("foo", [tar_digest!(0)]);
1171 assert!(!spec.must_be_run_locally());
1172
1173 let spec = job_spec!("foo", [tar_digest!(0)], root_overlay: JobRootOverlay::None);
1174 assert!(!spec.must_be_run_locally());
1175
1176 let spec = job_spec!("foo", [tar_digest!(0)], root_overlay: JobRootOverlay::Tmp);
1177 assert!(!spec.must_be_run_locally());
1178
1179 let spec = job_spec! {
1180 "foo",
1181 [tar_digest!(0)],
1182 root_overlay: JobRootOverlay::Local(CaptureFileSystemChanges {
1183 upper: "upper".into(),
1184 work: "work".into(),
1185 }),
1186 };
1187 assert!(spec.must_be_run_locally());
1188 }
1189
1190 #[test]
1191 fn job_spec_must_be_run_locally_allocate_tty() {
1192 let spec = job_spec!("foo", [tar_digest!(0)]);
1193 assert!(!spec.must_be_run_locally());
1194
1195 let spec = job_spec! {
1196 "foo",
1197 [tar_digest!(0)],
1198 allocate_tty: JobTty::new(b"\0abcde", WindowSize::new(20, 80)),
1199 };
1200 assert!(spec.must_be_run_locally());
1201 }
1202}