microsandbox_types/snapshot/
owned.rs1use std::collections::BTreeSet;
4use std::path::{Path, PathBuf};
5
6use crate::{HostPermissions, MountOptions, OwnedVolumeStorage, StatVirtualization, VolumeMount};
7use serde::{Deserialize, Serialize};
8
9use super::Manifest;
10use super::disk::DiskGenerationManifest;
11use crate::error::{SnapshotManifestError, SnapshotManifestResult};
12
13pub const OWNED_VOLUMES_EXTENSION: &str = "microsandbox.owned-volumes";
19
20#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
26#[serde(deny_unknown_fields)]
27pub struct OwnedDirectoryPayload {
28 pub digest: String,
30 pub bytes: u64,
32}
33
34#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
36#[serde(tag = "kind", rename_all = "lowercase", deny_unknown_fields)]
37pub enum OwnedVolumeData {
38 Disk {
40 generation: DiskGenerationManifest,
42 },
43 Directory {
45 descriptor: OwnedDirectoryPayload,
47 files: Vec<OwnedDirectoryPayload>,
49 },
50}
51
52#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
54#[serde(deny_unknown_fields)]
55pub struct OwnedVolumeCapture {
56 pub mount_id: String,
58 pub mount: OwnedMountSnapshot,
60 pub data: OwnedVolumeData,
62}
63
64#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
66#[serde(deny_unknown_fields)]
67pub struct OwnedMountSnapshot {
68 pub guest: String,
70 pub storage: OwnedVolumeStorage,
72 pub options: MountOptions,
74 pub stat_virtualization: StatVirtualization,
76 pub host_permissions: HostPermissions,
78}
79
80impl Manifest {
85 pub fn set_owned_volumes(
87 &mut self,
88 volumes: Vec<OwnedVolumeCapture>,
89 ) -> SnapshotManifestResult<()> {
90 validate_owned_volumes(&volumes)?;
91 self.requires.retain(|key| key != OWNED_VOLUMES_EXTENSION);
92 if volumes.is_empty() {
93 self.extensions.remove(OWNED_VOLUMES_EXTENSION);
94 } else {
95 self.extensions.insert(
96 OWNED_VOLUMES_EXTENSION.into(),
97 serde_json::to_value(volumes)
98 .map_err(|error| SnapshotManifestError::ManifestParse(error.to_string()))?,
99 );
100 self.requires.push(OWNED_VOLUMES_EXTENSION.into());
101 self.requires.sort();
102 }
103 Ok(())
104 }
105
106 pub fn owned_volumes(&self) -> SnapshotManifestResult<Vec<OwnedVolumeCapture>> {
108 let Some(value) = self.extensions.get(OWNED_VOLUMES_EXTENSION) else {
109 return Ok(Vec::new());
110 };
111 if !self
112 .requires
113 .iter()
114 .any(|key| key == OWNED_VOLUMES_EXTENSION)
115 {
116 return invalid("owned volumes must be a required snapshot extension");
117 }
118 let volumes: Vec<OwnedVolumeCapture> =
119 serde_json::from_value(value.clone()).map_err(|error| {
120 SnapshotManifestError::ManifestParse(format!("owned volumes: {error}"))
121 })?;
122 validate_owned_volumes(&volumes)?;
123 Ok(volumes)
124 }
125}
126
127impl OwnedVolumeCapture {
128 pub fn directory_path(&self) -> PathBuf {
130 Path::new("owned").join(&self.mount_id)
131 }
132
133 pub fn directory_payloads(&self) -> Vec<(PathBuf, &OwnedDirectoryPayload)> {
135 match &self.data {
136 OwnedVolumeData::Disk { .. } => Vec::new(),
137 OwnedVolumeData::Directory { descriptor, files } => {
138 let directory = self.directory_path();
139 std::iter::once((directory.join("directory.bin"), descriptor))
140 .chain(
141 files
142 .iter()
143 .map(|file| (directory.join("files").join(&file.digest), file)),
144 )
145 .collect()
146 }
147 }
148 }
149}
150
151impl OwnedMountSnapshot {
152 pub fn from_mount(mount: &VolumeMount) -> SnapshotManifestResult<Self> {
154 let VolumeMount::Owned {
155 guest,
156 storage,
157 options,
158 stat_virtualization,
159 host_permissions,
160 } = mount
161 else {
162 return invalid("owned volume inventory contains an external mount");
163 };
164 Ok(Self {
165 guest: guest.clone(),
166 storage: storage.clone(),
167 options: *options,
168 stat_virtualization: *stat_virtualization,
169 host_permissions: *host_permissions,
170 })
171 }
172
173 pub fn to_mount(&self) -> VolumeMount {
175 VolumeMount::Owned {
176 guest: self.guest.clone(),
177 storage: self.storage.clone(),
178 options: self.options,
179 stat_virtualization: self.stat_virtualization,
180 host_permissions: self.host_permissions,
181 }
182 }
183}
184
185pub fn validate_owned_volumes(volumes: &[OwnedVolumeCapture]) -> SnapshotManifestResult<()> {
191 if volumes.len() > 256 {
192 return invalid("owned volume count exceeds the format bound");
193 }
194 let mut ids = BTreeSet::new();
195 let mut guests = BTreeSet::new();
196 for volume in volumes {
197 if volume.mount_id.is_empty()
198 || volume.mount_id.len() > 128
199 || !volume
200 .mount_id
201 .bytes()
202 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-'))
203 || !ids.insert(&volume.mount_id)
204 || !guests.insert(volume.mount.guest.as_str())
205 {
206 return invalid("owned volume has an invalid or repeated mount identity");
207 }
208 let guest = &volume.mount.guest;
209 let storage = &volume.mount.storage;
210 if crate::owned_volume_mount_id(guest) != volume.mount_id {
211 return invalid("owned volume mount identity differs from its guest path");
212 }
213 if guest == "/"
214 || !guest.starts_with('/')
215 || guest
216 .split('/')
217 .skip(1)
218 .any(|part| matches!(part, "" | "." | ".."))
219 {
220 return invalid("owned volume guest path is not canonical");
221 }
222 match (storage, &volume.data) {
223 (OwnedVolumeStorage::Disk { capacity_mib }, OwnedVolumeData::Disk { generation }) => {
224 generation.validate()?;
225 if *capacity_mib == 0
226 || generation.device_id != volume.mount_id
227 || generation
228 .layers
229 .iter()
230 .any(|layer| layer.virtual_size != u64::from(*capacity_mib) * 1024 * 1024)
231 {
232 return invalid("owned disk generation differs from its storage specification");
233 }
234 }
235 (
236 OwnedVolumeStorage::Directory { .. },
237 OwnedVolumeData::Directory { descriptor, files },
238 ) => {
239 validate_payload(descriptor)?;
240 let mut previous = None;
241 for payload in files {
242 validate_payload(payload)?;
243 if previous.is_some_and(|digest: &str| digest >= payload.digest.as_str()) {
244 return invalid("owned directory payloads must be sorted and unique");
245 }
246 previous = Some(payload.digest.as_str());
247 }
248 }
249 _ => return invalid("owned volume storage and captured backing kinds differ"),
250 }
251 }
252 Ok(())
253}
254
255fn validate_payload(payload: &OwnedDirectoryPayload) -> SnapshotManifestResult<()> {
256 if payload.digest.len() != 64
257 || !payload
258 .digest
259 .bytes()
260 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
261 {
262 return invalid("owned directory payload has an invalid SHA-256 identity");
263 }
264 Ok(())
265}
266
267fn invalid<T>(message: &str) -> SnapshotManifestResult<T> {
268 Err(SnapshotManifestError::ManifestParse(message.into()))
269}