Skip to main content

kernel/config/
container.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4// Copyright (c) 2026 2youg1 and the sprawling contributors
5
6//! Explicit container limits and a local immutable image identity
7//! (`crates/kernel/spec/Config.lean`, container input contract).
8
9use std::num::{NonZeroU32, NonZeroU64};
10
11use serde::{Deserialize, Serialize};
12
13use crate::{AxCode, AxError};
14
15/// A locally installed image ID. Registry references and mutable tags
16/// cannot reach an execution request through this type.
17#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
18#[serde(try_from = "String", into = "String")]
19pub struct ContainerImage(String);
20
21impl ContainerImage {
22    pub(crate) const PREFIX: &str = "sha256:";
23    pub(crate) const DIGEST_LENGTH: usize = 64;
24    pub(crate) const HEX_DIGITS: &str = "0123456789abcdef";
25    /// Parses an immutable image ID without consulting a registry.
26    ///
27    /// # Errors
28    /// `E_CONFIG_INVALID` for anything except a full lowercase sha256 ID.
29    pub fn parse(image: &str) -> Result<Self, AxError> {
30        if image.strip_prefix(Self::PREFIX).is_some_and(|digest| {
31            digest.len() == Self::DIGEST_LENGTH
32                && digest
33                    .bytes()
34                    .all(|byte| Self::HEX_DIGITS.as_bytes().contains(&byte))
35        }) {
36            return Ok(Self(image.to_owned()));
37        }
38        Err(AxError::failure(
39            AxCode::ConfigInvalid,
40            "read the container image",
41            "the image is not a full local sha256 image ID",
42        )
43        .with_recovery(
44            "prepare an image yourself and use its sha256 ID from image inspect; exec never pulls an image",
45        ))
46    }
47
48    #[must_use]
49    pub fn as_str(&self) -> &str {
50        &self.0
51    }
52}
53
54impl TryFrom<String> for ContainerImage {
55    type Error = AxError;
56
57    fn try_from(image: String) -> Result<Self, Self::Error> {
58        Self::parse(&image)
59    }
60}
61
62impl From<ContainerImage> for String {
63    fn from(image: ContainerImage) -> Self {
64        image.0
65    }
66}
67
68/// Every limit is explicit and nonzero. CPU is in millicpu, memory is
69/// in bytes, and the process count bounds descendants as well as PID 1.
70#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
71#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
72#[serde(deny_unknown_fields)]
73pub struct ContainerLimits {
74    pub image: ContainerImage,
75    pub user: NonZeroU32,
76    pub cpu_millis: NonZeroU32,
77    pub memory_bytes: NonZeroU64,
78    pub pids: NonZeroU32,
79}
80
81#[cfg(test)]
82#[allow(clippy::unwrap_used, reason = "test code")]
83mod tests {
84    use super::*;
85
86    #[test]
87    fn configuration_and_serde_share_the_immutable_image_admission() {
88        for image in [
89            "",
90            "alpine:latest",
91            "--privileged",
92            "sha256:abc",
93            "sha256:fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffF",
94        ] {
95            assert!(ContainerImage::parse(image).is_err());
96            assert!(serde_json::from_value::<ContainerImage>(serde_json::json!(image)).is_err());
97        }
98        let image = format!("sha256:{}", "a".repeat(64));
99        let parsed = ContainerImage::parse(&image).unwrap();
100        assert_eq!(
101            serde_json::from_value::<ContainerImage>(serde_json::json!(image)).unwrap(),
102            parsed
103        );
104        assert_eq!(
105            serde_json::to_value(parsed).unwrap(),
106            serde_json::json!(image)
107        );
108    }
109}