1use anyhow::Context;
7use mesh::payload::Protobuf;
8use mesh::payload::Timestamp;
9use std::path::Path;
10
11pub const MANIFEST_VERSION: u32 = 1;
13
14#[derive(Clone, Protobuf)]
16#[mesh(package = "openvmm.snapshot")]
17pub struct SnapshotManifest {
18 #[mesh(1)]
20 pub version: u32,
21 #[mesh(2)]
23 pub created_at: Timestamp,
24 #[mesh(3)]
26 pub openvmm_version: String,
27 #[mesh(4)]
29 pub memory_size_bytes: u64,
30 #[mesh(5)]
32 pub vp_count: u32,
33 #[mesh(6)]
35 pub page_size: u32,
36 #[mesh(7)]
38 pub architecture: String,
39}
40
41pub fn write_snapshot(
48 dir: &Path,
49 manifest: &SnapshotManifest,
50 saved_state_bytes: &[u8],
51 memory_file_path: &Path,
52) -> anyhow::Result<()> {
53 fs_err::create_dir_all(dir)?;
54
55 let manifest_bytes = mesh::payload::encode(manifest.clone());
57 fs_err::write(dir.join("manifest.bin"), &manifest_bytes)?;
58
59 fs_err::write(dir.join("state.bin"), saved_state_bytes)?;
61
62 let memory_bin_path = dir.join("memory.bin");
64 let canonical_source = fs_err::canonicalize(memory_file_path)?;
65
66 let needs_link = if memory_bin_path.exists() {
69 let canonical_target = fs_err::canonicalize(&memory_bin_path)?;
70 if canonical_source == canonical_target {
71 false
72 } else {
73 fs_err::remove_file(&memory_bin_path)?;
76 true
77 }
78 } else {
79 true
80 };
81
82 if needs_link {
83 if let Err(err) = std::fs::hard_link(&canonical_source, &memory_bin_path) {
84 if err.kind() == std::io::ErrorKind::CrossesDevices {
85 anyhow::bail!(
86 "memory backing file ({}) must be on the same filesystem as the snapshot \
87 directory ({}); consider placing the backing file inside the snapshot \
88 directory",
89 memory_file_path.display(),
90 dir.display(),
91 );
92 }
93 return Err(err).with_context(|| {
94 format!(
95 "failed to hard-link {} -> {}",
96 canonical_source.display(),
97 memory_bin_path.display()
98 )
99 });
100 }
101 }
102
103 Ok(())
104}
105
106pub fn read_snapshot(dir: &Path) -> anyhow::Result<(SnapshotManifest, Vec<u8>)> {
111 let manifest_bytes =
112 fs_err::read(dir.join("manifest.bin")).context("failed to read manifest.bin")?;
113 let manifest: SnapshotManifest =
114 mesh::payload::decode(&manifest_bytes).context("failed to decode snapshot manifest")?;
115
116 let state_bytes = fs_err::read(dir.join("state.bin")).context("failed to read state.bin")?;
117
118 Ok((manifest, state_bytes))
119}
120
121pub fn validate_manifest(
127 manifest: &SnapshotManifest,
128 expected_arch: &str,
129 expected_memory_size: u64,
130 expected_vp_count: u32,
131 expected_page_size: u32,
132) -> anyhow::Result<()> {
133 if manifest.version != MANIFEST_VERSION {
134 anyhow::bail!(
135 "snapshot manifest version {} is not supported (expected {})",
136 manifest.version,
137 MANIFEST_VERSION,
138 );
139 }
140
141 if manifest.architecture != expected_arch {
142 anyhow::bail!(
143 "snapshot architecture '{}' doesn't match expected '{}'",
144 manifest.architecture,
145 expected_arch,
146 );
147 }
148
149 if manifest.memory_size_bytes != expected_memory_size {
150 anyhow::bail!(
151 "snapshot memory size ({} bytes) doesn't match expected ({} bytes)",
152 manifest.memory_size_bytes,
153 expected_memory_size,
154 );
155 }
156
157 if manifest.vp_count != expected_vp_count {
158 anyhow::bail!(
159 "snapshot VP count ({}) doesn't match expected ({})",
160 manifest.vp_count,
161 expected_vp_count,
162 );
163 }
164
165 if manifest.page_size != expected_page_size {
166 anyhow::bail!(
167 "snapshot page size ({}) doesn't match expected ({})",
168 manifest.page_size,
169 expected_page_size,
170 );
171 }
172
173 Ok(())
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179
180 fn test_manifest() -> SnapshotManifest {
182 SnapshotManifest {
183 version: MANIFEST_VERSION,
184 created_at: Timestamp {
185 seconds: 1234567890,
186 nanos: 0,
187 },
188 openvmm_version: "test-0.1.0".to_string(),
189 memory_size_bytes: 1024,
190 vp_count: 2,
191 page_size: 4096,
192 architecture: "x86_64".to_string(),
193 }
194 }
195
196 #[test]
197 fn write_read_roundtrip() {
198 let dir = tempfile::tempdir().unwrap();
199 let snap_dir = dir.path().join("snap");
200
201 let mem_path = dir.path().join("memory.bin");
203 std::fs::write(&mem_path, b"FAKEMEM").unwrap();
204
205 let manifest = test_manifest();
206 let state = b"saved-state-data";
207
208 write_snapshot(&snap_dir, &manifest, state, &mem_path).unwrap();
209
210 let (read_manifest, read_state) = read_snapshot(&snap_dir).unwrap();
211 assert_eq!(read_manifest.version, manifest.version);
212 assert_eq!(read_manifest.memory_size_bytes, manifest.memory_size_bytes);
213 assert_eq!(read_manifest.vp_count, manifest.vp_count);
214 assert_eq!(read_manifest.architecture, manifest.architecture);
215 assert_eq!(read_state, state);
216
217 assert!(snap_dir.join("memory.bin").exists());
219 }
220
221 #[test]
222 fn write_snapshot_creates_dir() {
223 let dir = tempfile::tempdir().unwrap();
224 let snap_dir = dir.path().join("a").join("b").join("c");
225
226 let mem_path = dir.path().join("memory.bin");
227 std::fs::write(&mem_path, b"MEM").unwrap();
228
229 write_snapshot(&snap_dir, &test_manifest(), b"state", &mem_path).unwrap();
230
231 assert!(snap_dir.join("manifest.bin").exists());
232 assert!(snap_dir.join("state.bin").exists());
233 assert!(snap_dir.join("memory.bin").exists());
234 }
235
236 #[test]
237 fn write_snapshot_same_memory_path() {
238 let dir = tempfile::tempdir().unwrap();
241 let snap_dir = dir.path().join("snap");
242 std::fs::create_dir_all(&snap_dir).unwrap();
243
244 let mem_path = snap_dir.join("memory.bin");
245 std::fs::write(&mem_path, b"SAMEFILE").unwrap();
246
247 write_snapshot(&snap_dir, &test_manifest(), b"state", &mem_path).unwrap();
249
250 assert_eq!(std::fs::read(&mem_path).unwrap(), b"SAMEFILE");
252 }
253
254 #[test]
255 fn read_snapshot_missing_file() {
256 let dir = tempfile::tempdir().unwrap();
257 let result = read_snapshot(dir.path());
259 assert!(result.is_err());
260 }
261
262 #[test]
263 fn validate_manifest_ok() {
264 let manifest = test_manifest();
265 validate_manifest(&manifest, "x86_64", 1024, 2, 4096).unwrap();
266 }
267
268 #[test]
269 fn validate_manifest_wrong_arch() {
270 let manifest = test_manifest();
271 let err = validate_manifest(&manifest, "aarch64", 1024, 2, 4096).unwrap_err();
272 assert!(
273 err.to_string().contains("architecture"),
274 "unexpected error: {err}"
275 );
276 }
277
278 #[test]
279 fn validate_manifest_wrong_memory_size() {
280 let manifest = test_manifest();
281 let err = validate_manifest(&manifest, "x86_64", 9999, 2, 4096).unwrap_err();
282 assert!(
283 err.to_string().contains("memory size"),
284 "unexpected error: {err}"
285 );
286 }
287
288 #[test]
289 fn validate_manifest_wrong_vp_count() {
290 let manifest = test_manifest();
291 let err = validate_manifest(&manifest, "x86_64", 1024, 99, 4096).unwrap_err();
292 assert!(
293 err.to_string().contains("VP count"),
294 "unexpected error: {err}"
295 );
296 }
297
298 #[test]
299 fn validate_manifest_wrong_page_size() {
300 let manifest = test_manifest();
301 let err = validate_manifest(&manifest, "x86_64", 1024, 2, 65536).unwrap_err();
302 assert!(
303 err.to_string().contains("page size"),
304 "unexpected error: {err}"
305 );
306 }
307
308 #[test]
309 fn validate_manifest_wrong_version() {
310 let mut manifest = test_manifest();
311 manifest.version = 999;
312 let err = validate_manifest(&manifest, "x86_64", 1024, 2, 4096).unwrap_err();
313 assert!(
314 err.to_string().contains("version"),
315 "unexpected error: {err}"
316 );
317 }
318}