Skip to main content

microsandbox_types/snapshot/
disk.rs

1//! Pure canonical disk generations shared by snapshots and local checkpoint storage.
2
3use crate::error::{SnapshotManifestError, SnapshotManifestResult};
4use serde::{Deserialize, Serialize};
5use sha2::{Digest, Sha256};
6use std::fmt;
7
8/// Algorithm-qualified immutable object identity.
9#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
10#[serde(try_from = "String", into = "String")]
11pub struct ObjectId(String);
12
13/// One immutable disk layer in a complete oldest-first dependency closure.
14#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
15#[serde(deny_unknown_fields)]
16pub struct DiskLayerRef {
17    /// Stable layer identity.
18    pub layer_id: String,
19    /// Physical format (`raw` or `qcow2`).
20    pub format: String,
21    /// Guest-visible virtual size.
22    pub virtual_size: u64,
23    /// Exact physical file length; checked even when content integrity is not recorded.
24    pub file_size: u64,
25    /// Immediate predecessor when present.
26    pub predecessor: Option<String>,
27    /// Optional content integrity of the exact physical layer, independent of layer identity.
28    pub integrity_root: Option<String>,
29}
30
31/// Immutable sealed disk generation.
32#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
33#[serde(deny_unknown_fields)]
34pub struct DiskGenerationManifest {
35    /// Schema identifier.
36    pub schema: String,
37    /// Logical writable volume identity.
38    pub volume_id: String,
39    /// Stable guest-visible block device whose bytes this generation captures.
40    #[serde(
41        default = "default_root_disk_device_id",
42        skip_serializing_if = "is_default_root_disk_device_id"
43    )]
44    pub device_id: String,
45    /// Monotonic immutable generation.
46    pub generation: u64,
47    /// Complete oldest-first physical closure.
48    pub layers: Vec<DiskLayerRef>,
49    /// Layer identity of the sealed head.
50    pub head: String,
51    /// VM-wide pause boundary at which the writable head was sealed.
52    pub pause_generation: u64,
53}
54
55impl ObjectId {
56    /// Compute an identity from exact bytes.
57    pub fn from_bytes(bytes: &[u8]) -> SnapshotManifestResult<Self> {
58        let mut hasher = Sha256::new();
59        hasher.update(bytes);
60        Self::new(format!("sha256:{}", hex::encode(hasher.finalize())))
61    }
62
63    /// Parse and validate an algorithm-qualified identity.
64    pub fn new(value: impl Into<String>) -> SnapshotManifestResult<Self> {
65        let value = value.into();
66        let Some(encoded) = value.strip_prefix("sha256:") else {
67            return Err(SnapshotManifestError::ManifestParse(
68                "object identity must use sha256".into(),
69            ));
70        };
71        if encoded.len() != 64
72            || !encoded
73                .bytes()
74                .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
75        {
76            return Err(SnapshotManifestError::ManifestParse(format!(
77                "invalid object identity: {value}"
78            )));
79        }
80        Ok(Self(value))
81    }
82
83    /// Return the qualified identity.
84    pub fn as_str(&self) -> &str {
85        &self.0
86    }
87
88    /// Return the unqualified SHA-256 hexadecimal digest.
89    pub fn hex(&self) -> &str {
90        self.0.strip_prefix("sha256:").expect("validated identity")
91    }
92}
93
94impl fmt::Display for ObjectId {
95    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
96        self.0.fmt(formatter)
97    }
98}
99
100impl TryFrom<String> for ObjectId {
101    type Error = SnapshotManifestError;
102
103    fn try_from(value: String) -> Result<Self, Self::Error> {
104        Self::new(value)
105    }
106}
107
108impl From<ObjectId> for String {
109    fn from(value: ObjectId) -> Self {
110        value.0
111    }
112}
113
114impl DiskGenerationManifest {
115    fn validate_body(&self) -> SnapshotManifestResult<()> {
116        if !portable_member_id(&self.volume_id)
117            || !portable_member_id(&self.device_id)
118            || self.generation == 0
119            || self.layers.is_empty()
120        {
121            return manifest_error("disk generation has invalid identity, generation, or layers");
122        }
123        if self.layers.len() > 256 {
124            return manifest_error("disk generation exceeds 256 layers");
125        }
126        if self.layers.last().map(|layer| layer.layer_id.as_str()) != Some(self.head.as_str()) {
127            return manifest_error("disk head does not name the final layer");
128        }
129        for (index, layer) in self.layers.iter().enumerate() {
130            if !portable_member_id(&layer.layer_id)
131                || layer.virtual_size == 0
132                || layer.file_size == 0
133            {
134                return manifest_error("disk layer has invalid identity or zero virtual size");
135            }
136            if let Some(root) = &layer.integrity_root {
137                validate_blake3_root(root)?;
138            }
139            match (index, layer.format.as_str(), layer.predecessor.as_deref()) {
140                (0, "raw" | "qcow2", None) => {}
141                (_, "qcow2", Some(parent))
142                    if parent == self.layers[index - 1].layer_id.as_str() => {}
143                _ => return manifest_error("disk layer closure is not a valid oldest-first chain"),
144            }
145        }
146        Ok(())
147    }
148}
149
150fn validate_blake3_root(root: &str) -> SnapshotManifestResult<()> {
151    let Some(encoded) = root.strip_prefix("blake3:") else {
152        return manifest_error("disk layer integrity must use blake3");
153    };
154    if encoded.len() != 64
155        || !encoded
156            .bytes()
157            .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
158    {
159        return manifest_error("disk layer integrity has an invalid digest");
160    }
161    Ok(())
162}
163
164fn portable_member_id(value: &str) -> bool {
165    !value.is_empty()
166        && value.len() <= 128
167        && value != "."
168        && value != ".."
169        && value
170            .bytes()
171            .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-'))
172}
173
174fn default_root_disk_device_id() -> String {
175    "vdb".into()
176}
177
178fn is_default_root_disk_device_id(value: &str) -> bool {
179    value == "vdb"
180}
181
182impl DiskGenerationManifest {
183    /// Validate the exact released disk generation contract.
184    pub fn validate(&self) -> SnapshotManifestResult<()> {
185        if self.schema != "microsandbox.disk-generation/1" {
186            return manifest_error(format!(
187                "unsupported schema {} (expected microsandbox.disk-generation/1)",
188                self.schema
189            ));
190        }
191        self.validate_body()
192    }
193    /// Serialize using the bounded canonical checkpoint encoding.
194    pub fn to_canonical_bytes(&self) -> SnapshotManifestResult<Vec<u8>> {
195        self.validate()?;
196        let value = serde_json::to_value(self)
197            .map_err(|error| manifest_error_value(format!("serialize failed: {error}")))?;
198        let mut output = Vec::new();
199        super::manifest::write_canonical_json(&value, &mut output)?;
200        if output.len() > 8 * 1024 * 1024 {
201            return manifest_error("manifest exceeds the encoded-size bound");
202        }
203        Ok(output)
204    }
205    /// Parse one complete, canonical disk generation.
206    pub fn from_bytes(bytes: &[u8]) -> SnapshotManifestResult<Self> {
207        if bytes.len() > 8 * 1024 * 1024 {
208            return manifest_error("manifest exceeds the encoded-size bound");
209        }
210        super::manifest::reject_duplicate_json_keys(bytes)?;
211        let manifest: Self = serde_json::from_slice(bytes)
212            .map_err(|error| manifest_error_value(format!("parse failed: {error}")))?;
213        manifest.validate()?;
214        if manifest.to_canonical_bytes()? != bytes {
215            return manifest_error("stored manifest bytes are not canonical");
216        }
217        Ok(manifest)
218    }
219    /// Compute the immutable SHA-256 identity of canonical bytes.
220    pub fn digest(&self) -> SnapshotManifestResult<ObjectId> {
221        ObjectId::from_bytes(&self.to_canonical_bytes()?)
222    }
223}
224fn manifest_error<T>(message: impl Into<String>) -> SnapshotManifestResult<T> {
225    Err(manifest_error_value(message))
226}
227fn manifest_error_value(message: impl Into<String>) -> SnapshotManifestError {
228    SnapshotManifestError::ManifestParse(format!("checkpoint manifest: {}", message.into()))
229}