maelstrom_base/
lib.rs

1//! Core structs used by the broker, worker, and clients. Everything in this crate must be usable
2//! from wasm.
3
4pub 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/// ID of a client connection. These share the same ID space as [`WorkerId`] and [`MonitorId`].
32#[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/// A client-relative job ID. Clients can assign these however they like.
44#[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    /// A .tar file
71    Tar,
72    /// A serialized `Manifest`
73    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/// An absolute job ID that includes a [`ClientId`] for disambiguation.
91#[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/// ID of a user. This should be compatible with uid_t.
333#[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/// ID of a group. This should be compatible with gid_t.
358#[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/// A count of seconds.
383#[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/// The size of a terminal in characters.
411#[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/// The parameters for a TTY for a job.
425#[pocket_definition(export)]
426#[derive(Copy, Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
427pub struct JobTty {
428    /// A Unix domain socket abstract address. We use exactly 6 bytes because that's how many bytes
429    /// the autobind feature in Linux uses. The first byte will always be 0.
430    pub socket_address: [u8; 6],
431
432    /// The initial window size of the TTY. Window size updates may follow.
433    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/// All necessary information for the worker to execute a job.
447#[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/// How a job's process terminated. A process can either exit of its own accord or be killed by a
556/// signal.
557#[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/// The result for stdout or stderr for a job.
565#[pocket_definition(export)]
566#[derive(Clone, Debug, Display, Deserialize, PartialEq, Eq, PartialOrd, Ord, Serialize)]
567pub enum JobOutputResult {
568    /// There was no output.
569    None,
570
571    /// The output is contained in the provided slice.
572    #[display("{}", String::from_utf8_lossy(_0))]
573    Inline(#[debug("{}", String::from_utf8_lossy(_0))] Box<[u8]>),
574
575    /// The output was truncated to the provided slice, the size of which is based on the job
576    /// request. The actual size of the output is also provided, though the remaining bytes will
577    /// have been thrown away.
578    #[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    /*
585     * To come:
586    /// The output was stored in a digest, and is of the provided size.
587    External(Sha256Digest, u64),
588    */
589}
590
591/// The output and duration of a job that ran for some amount of time. This is generated regardless
592/// of how the job terminated. From our point of view, it doesn't matter. We ran the job until it
593/// was terminated, and gathered its output.
594#[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/// The outcome of a completed job. That is, a job that ran to completion, instead of timing out,
603/// being canceled, etc.
604#[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/// The outcome of a job. This doesn't include error outcomes, which are handled with JobError.
612#[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/// A job failed to execute for some reason. We separate the universe of errors into "execution"
620/// errors and "system" errors.
621#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
622pub enum JobError<T> {
623    /// There was something wrong with the job that made it unable to be executed. This error
624    /// indicates that there was something wrong with the job itself, and thus is obstensibly the
625    /// fault of the client. An error of this type might happen if the execution path wasn't found, or
626    /// if the binary couldn't be executed because it was for the wrong architecture.
627    Execution(T),
628
629    /// There was something wrong with the system that made it impossible to execute the job. There
630    /// isn't anything different the client could do to mitigate this error. An error of this type
631    /// might happen if the broker ran out of disk space, or there was a software error.
632    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
644/// A common Result type in the worker.
645pub type JobResult<T, E> = Result<T, JobError<E>>;
646
647/// All relevant information about the outcome of a job. This is what's sent around between the
648/// Worker, Broker, and Client.
649pub 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/// ID of a worker connection. These share the same ID space as [`ClientId`] and [`MonitorId`].
668#[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/// ID of a monitor connection. These share the same ID space as [`ClientId`] and [`WorkerId`].
688#[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/// A SHA-256 digest.
706#[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    /// Verify that two digests match. If not, return a [`Sha256DigestVerificationError`].
713    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/// Error indicating that two digests that should have matched didn't.
784#[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}