1use std::{fs::File, io::BufReader, path::Path};
8
9use crate::{
10 envelope::{SNAPSHOT_VERSION, Snapshot},
11 error::{Result, SnapshotError},
12 state::MicrovmState,
13};
14
15#[derive(Debug, Clone, PartialEq)]
17pub struct LoadedSnapshot {
18 pub state: MicrovmState,
20 pub source_version: semver::Version,
22}
23
24pub fn load(state_path: &Path) -> Result<LoadedSnapshot> {
29 let file = File::open(state_path)?;
30 let mut reader = BufReader::new(file);
31 let snapshot = Snapshot::<MicrovmState>::load(&mut reader)?;
32 snapshot.data.verify_compatible()?;
33 Ok(LoadedSnapshot {
34 state: snapshot.data,
35 source_version: snapshot.header.version,
36 })
37}
38
39#[derive(Debug, Clone)]
49#[allow(clippy::struct_excessive_bools)] pub struct SnapshotDescription {
51 pub state_path: std::path::PathBuf,
53 pub magic: u64,
55 pub version: semver::Version,
57 pub expected_version: semver::Version,
59 pub vcpu_count: usize,
61 pub mem_size_mib: u64,
63 pub track_dirty_pages: bool,
65 pub devices: Vec<(String, String, u32)>,
67 pub mmds_present: bool,
69 pub inferred_memory_path: Option<std::path::PathBuf>,
71 pub inferred_memory_exists: bool,
73 pub inferred_memory_size: Option<u64>,
75 pub crc_ok: bool,
77}
78
79impl SnapshotDescription {
80 pub fn to_json(&self) -> serde_json::Value {
82 serde_json::json!({
83 "state_path": self.state_path.display().to_string(),
84 "magic": format!("{:#018x}", self.magic),
85 "version": self.version.to_string(),
86 "expected_version": self.expected_version.to_string(),
87 "vcpu_count": self.vcpu_count,
88 "mem_size_mib": self.mem_size_mib,
89 "track_dirty_pages": self.track_dirty_pages,
90 "devices": self
91 .devices
92 .iter()
93 .map(|(k, i, s)| serde_json::json!({"kind": k, "id": i, "mmio_slot": s}))
94 .collect::<Vec<_>>(),
95 "mmds_present": self.mmds_present,
96 "inferred_memory_path": self
97 .inferred_memory_path
98 .as_ref()
99 .map(|p| p.display().to_string()),
100 "inferred_memory_exists": self.inferred_memory_exists,
101 "inferred_memory_size": self.inferred_memory_size,
102 "crc_ok": self.crc_ok,
103 })
104 }
105
106 pub fn human(&self) -> String {
108 use std::fmt::Write;
109 let mut s = String::new();
110 let _ = writeln!(s, "snapshot: {}", self.state_path.display());
111 let _ = writeln!(s, " magic: {:#018x}", self.magic);
112 let _ = writeln!(s, " version: {}", self.version);
113 if self.version != self.expected_version {
114 let _ = writeln!(s, " expected (squib): {}", self.expected_version);
115 }
116 let _ = writeln!(s, " vcpu_count: {}", self.vcpu_count);
117 let _ = writeln!(s, " mem_size_mib: {}", self.mem_size_mib);
118 let _ = writeln!(s, " track_dirty_pages: {}", self.track_dirty_pages);
119 let _ = writeln!(s, " devices: {}", self.devices.len());
120 for (kind, id, slot) in &self.devices {
121 let _ = writeln!(s, " - {kind}: {id} (slot {slot})");
122 }
123 let _ = writeln!(s, " mmds_present: {}", self.mmds_present);
124 if let Some(p) = &self.inferred_memory_path {
125 let _ = writeln!(
126 s,
127 " memory file: {} ({})",
128 p.display(),
129 if self.inferred_memory_exists {
130 self.inferred_memory_size
131 .map_or_else(|| "present".into(), |b| format!("{b} bytes"))
132 } else {
133 "MISSING".into()
134 }
135 );
136 }
137 let _ = writeln!(
138 s,
139 " crc_ok: {}",
140 if self.crc_ok { "yes" } else { "NO" }
141 );
142 s
143 }
144}
145
146pub fn describe(state_path: &Path) -> Result<SnapshotDescription> {
155 let bytes = std::fs::read(state_path)?;
156 let (snapshot, crc_ok) = match Snapshot::<MicrovmState>::load_from_slice(&bytes) {
157 Ok(snap) => (snap, true),
158 Err(SnapshotError::CrcMismatch) => {
159 if bytes.len() < 8 {
161 return Err(SnapshotError::TooShort);
162 }
163 let body = &bytes[..bytes.len() - 8];
164 (
165 Snapshot::<MicrovmState>::load_without_crc_check(body)?,
166 false,
167 )
168 }
169 Err(e) => return Err(e),
170 };
171
172 let inferred_memory_path = infer_memory_path(state_path);
173 let (inferred_memory_exists, inferred_memory_size) = match &inferred_memory_path {
174 Some(p) => match std::fs::metadata(p) {
175 Ok(m) => (true, Some(m.len())),
176 Err(_) => (false, None),
177 },
178 None => (false, None),
179 };
180
181 let devices = snapshot
182 .data
183 .device_states
184 .devices
185 .iter()
186 .map(|d| (d.kind.clone(), d.id.clone(), d.mmio_slot))
187 .collect();
188
189 Ok(SnapshotDescription {
190 state_path: state_path.to_path_buf(),
191 magic: snapshot.header.magic,
192 version: snapshot.header.version,
193 expected_version: SNAPSHOT_VERSION,
194 vcpu_count: snapshot.data.vcpu_states.len(),
195 mem_size_mib: snapshot.data.vm_info.mem_size_mib,
196 track_dirty_pages: snapshot.data.vm_info.track_dirty_pages,
197 devices,
198 mmds_present: snapshot.data.mmds_state.is_some(),
199 inferred_memory_path,
200 inferred_memory_exists,
201 inferred_memory_size,
202 crc_ok,
203 })
204}
205
206fn infer_memory_path(state_path: &Path) -> Option<std::path::PathBuf> {
215 state_path.extension()?;
216 let mut p = state_path.to_path_buf();
217 p.set_extension("mem");
218 Some(p)
219}
220
221#[cfg(test)]
222mod tests {
223 use std::path::Path;
224
225 use tempfile::TempDir;
226
227 use super::*;
228 use crate::{
229 envelope::Snapshot,
230 memory::VecPageReader,
231 save::{SaveRequest, SnapshotKind, save},
232 state::{DeviceState, DeviceStates, GicState, MicrovmState, VcpuState, VmInfo},
233 };
234
235 fn build_state() -> MicrovmState {
236 MicrovmState {
237 vm_info: VmInfo {
238 mem_size_mib: 256,
239 smt: false,
240 cpu_template: String::new(),
241 kernel_image_path: "/k".into(),
242 initrd_path: None,
243 boot_args: String::new(),
244 track_dirty_pages: false,
245 },
246 vcpu_states: vec![VcpuState::new(0), VcpuState::new(1)],
247 device_states: DeviceStates::from_devices(vec![DeviceState {
248 kind: "virtio-block".into(),
249 id: "rootfs".into(),
250 mmio_slot: 0,
251 blob: vec![],
252 }]),
253 gic_state: GicState::from_bytes(vec![0xAA; 32]),
254 mmds_state: None,
255 }
256 }
257
258 fn dest_in(dir: &Path, name: &str) -> std::path::PathBuf {
259 dir.join(name)
260 }
261
262 fn save_pair(dir: &Path, name: &str) -> (std::path::PathBuf, std::path::PathBuf) {
263 let snap = dest_in(dir, &format!("{name}.snap"));
264 let mem = dest_in(dir, &format!("{name}.mem"));
265 let reader = VecPageReader::new(vec![0u8; 16 * 1024]);
266 save(SaveRequest {
267 state_path: &snap,
268 memory_path: &mem,
269 kind: SnapshotKind::Full,
270 state: build_state(),
271 memory: &reader,
272 ram_size: 16 * 1024,
273 memory_page_size: 16 * 1024,
274 dirty: None,
275 })
276 .unwrap();
277 (snap, mem)
278 }
279
280 #[test]
281 fn test_should_round_trip_save_then_load() {
282 let dir = TempDir::new().unwrap();
283 let (snap, _mem) = save_pair(dir.path(), "x");
284 let loaded = load(&snap).unwrap();
285 assert_eq!(loaded.state.vcpu_states.len(), 2);
286 assert_eq!(loaded.source_version, SNAPSHOT_VERSION);
287 }
288
289 #[test]
290 fn test_should_describe_state_file_in_human_form() {
291 let dir = TempDir::new().unwrap();
292 let (snap, _mem) = save_pair(dir.path(), "x");
293 let desc = describe(&snap).unwrap();
294 let h = desc.human();
295 assert!(h.contains("vcpu_count: 2"));
296 assert!(h.contains("mem_size_mib: 256"));
297 assert!(h.contains("crc_ok: yes"));
298 }
299
300 #[test]
301 fn test_should_describe_state_file_in_json_form() {
302 let dir = TempDir::new().unwrap();
303 let (snap, _mem) = save_pair(dir.path(), "x");
304 let desc = describe(&snap).unwrap();
305 let j = desc.to_json();
306 assert_eq!(j["vcpu_count"], 2);
307 assert_eq!(j["mem_size_mib"], 256);
308 assert_eq!(j["crc_ok"], true);
309 }
310
311 #[test]
312 fn test_should_describe_corrupt_file_with_crc_warning() {
313 let dir = TempDir::new().unwrap();
318 let (snap, _mem) = save_pair(dir.path(), "x");
319 let mut bytes = std::fs::read(&snap).unwrap();
320 let last = bytes.len() - 1;
321 bytes[last] ^= 0x01;
322 std::fs::write(&snap, &bytes).unwrap();
323 let desc = describe(&snap).unwrap();
324 assert!(!desc.crc_ok);
325 assert!(desc.human().contains("crc_ok: NO"));
326 }
327
328 #[test]
329 fn test_should_infer_memory_path_from_state_path() {
330 assert_eq!(
332 infer_memory_path(Path::new("/tmp/x.snap")),
333 Some(std::path::PathBuf::from("/tmp/x.mem"))
334 );
335 assert_eq!(
338 infer_memory_path(Path::new("/tmp/x.snapshot")),
339 Some(std::path::PathBuf::from("/tmp/x.mem"))
340 );
341 assert_eq!(
342 infer_memory_path(Path::new("/tmp/x.bin")),
343 Some(std::path::PathBuf::from("/tmp/x.mem"))
344 );
345 assert_eq!(infer_memory_path(Path::new("/tmp/x")), None);
348 }
349
350 #[test]
351 fn test_should_report_missing_memory_file() {
352 let dir = TempDir::new().unwrap();
353 let (snap, mem) = save_pair(dir.path(), "x");
354 std::fs::remove_file(&mem).unwrap();
355 let desc = describe(&snap).unwrap();
356 assert!(!desc.inferred_memory_exists);
357 assert!(desc.human().contains("MISSING"));
358 }
359
360 #[test]
361 fn test_should_reject_load_on_incompatible_state() {
362 let dir = TempDir::new().unwrap();
363 let snap = dest_in(dir.path(), "x.snap");
364 let mut bad = build_state();
367 bad.vcpu_states.clear();
368 let envelope = Snapshot::new(bad);
369 let mut buf = Vec::new();
370 envelope.save(&mut buf).unwrap();
371 std::fs::write(&snap, &buf).unwrap();
372 let res = load(&snap);
373 assert!(matches!(res, Err(SnapshotError::Incompatible)));
374 }
375
376 #[test]
377 fn test_should_surface_truncated_file_as_too_short() {
378 let dir = TempDir::new().unwrap();
379 let snap = dest_in(dir.path(), "x.snap");
380 std::fs::write(&snap, b"abc").unwrap();
381 let res = describe(&snap);
382 assert!(matches!(
383 res,
384 Err(SnapshotError::TooShort | SnapshotError::CrcMismatch)
385 ));
386 }
387}