microsandbox_types/snapshot/
disk.rs1use crate::error::{SnapshotManifestError, SnapshotManifestResult};
4use serde::{Deserialize, Serialize};
5use sha2::{Digest, Sha256};
6use std::fmt;
7
8#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
10#[serde(try_from = "String", into = "String")]
11pub struct ObjectId(String);
12
13#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
15#[serde(deny_unknown_fields)]
16pub struct DiskLayerRef {
17 pub layer_id: String,
19 pub format: String,
21 pub virtual_size: u64,
23 pub file_size: u64,
25 pub predecessor: Option<String>,
27 pub integrity_root: Option<String>,
29}
30
31#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
33#[serde(deny_unknown_fields)]
34pub struct DiskGenerationManifest {
35 pub schema: String,
37 pub volume_id: String,
39 #[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 pub generation: u64,
47 pub layers: Vec<DiskLayerRef>,
49 pub head: String,
51 pub pause_generation: u64,
53}
54
55impl ObjectId {
56 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 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 pub fn as_str(&self) -> &str {
85 &self.0
86 }
87
88 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 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 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 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 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}