use std::{fs::File, io::BufReader, path::Path};
use crate::{
envelope::{SNAPSHOT_VERSION, Snapshot},
error::{Result, SnapshotError},
state::MicrovmState,
};
#[derive(Debug, Clone, PartialEq)]
pub struct LoadedSnapshot {
pub state: MicrovmState,
pub source_version: semver::Version,
}
pub fn load(state_path: &Path) -> Result<LoadedSnapshot> {
let file = File::open(state_path)?;
let mut reader = BufReader::new(file);
let snapshot = Snapshot::<MicrovmState>::load(&mut reader)?;
snapshot.data.verify_compatible()?;
Ok(LoadedSnapshot {
state: snapshot.data,
source_version: snapshot.header.version,
})
}
#[derive(Debug, Clone)]
#[allow(clippy::struct_excessive_bools)] pub struct SnapshotDescription {
pub state_path: std::path::PathBuf,
pub magic: u64,
pub version: semver::Version,
pub expected_version: semver::Version,
pub vcpu_count: usize,
pub mem_size_mib: u64,
pub track_dirty_pages: bool,
pub devices: Vec<(String, String, u32)>,
pub mmds_present: bool,
pub inferred_memory_path: Option<std::path::PathBuf>,
pub inferred_memory_exists: bool,
pub inferred_memory_size: Option<u64>,
pub crc_ok: bool,
}
impl SnapshotDescription {
pub fn to_json(&self) -> serde_json::Value {
serde_json::json!({
"state_path": self.state_path.display().to_string(),
"magic": format!("{:#018x}", self.magic),
"version": self.version.to_string(),
"expected_version": self.expected_version.to_string(),
"vcpu_count": self.vcpu_count,
"mem_size_mib": self.mem_size_mib,
"track_dirty_pages": self.track_dirty_pages,
"devices": self
.devices
.iter()
.map(|(k, i, s)| serde_json::json!({"kind": k, "id": i, "mmio_slot": s}))
.collect::<Vec<_>>(),
"mmds_present": self.mmds_present,
"inferred_memory_path": self
.inferred_memory_path
.as_ref()
.map(|p| p.display().to_string()),
"inferred_memory_exists": self.inferred_memory_exists,
"inferred_memory_size": self.inferred_memory_size,
"crc_ok": self.crc_ok,
})
}
pub fn human(&self) -> String {
use std::fmt::Write;
let mut s = String::new();
let _ = writeln!(s, "snapshot: {}", self.state_path.display());
let _ = writeln!(s, " magic: {:#018x}", self.magic);
let _ = writeln!(s, " version: {}", self.version);
if self.version != self.expected_version {
let _ = writeln!(s, " expected (squib): {}", self.expected_version);
}
let _ = writeln!(s, " vcpu_count: {}", self.vcpu_count);
let _ = writeln!(s, " mem_size_mib: {}", self.mem_size_mib);
let _ = writeln!(s, " track_dirty_pages: {}", self.track_dirty_pages);
let _ = writeln!(s, " devices: {}", self.devices.len());
for (kind, id, slot) in &self.devices {
let _ = writeln!(s, " - {kind}: {id} (slot {slot})");
}
let _ = writeln!(s, " mmds_present: {}", self.mmds_present);
if let Some(p) = &self.inferred_memory_path {
let _ = writeln!(
s,
" memory file: {} ({})",
p.display(),
if self.inferred_memory_exists {
self.inferred_memory_size
.map_or_else(|| "present".into(), |b| format!("{b} bytes"))
} else {
"MISSING".into()
}
);
}
let _ = writeln!(
s,
" crc_ok: {}",
if self.crc_ok { "yes" } else { "NO" }
);
s
}
}
pub fn describe(state_path: &Path) -> Result<SnapshotDescription> {
let bytes = std::fs::read(state_path)?;
let (snapshot, crc_ok) = match Snapshot::<MicrovmState>::load_from_slice(&bytes) {
Ok(snap) => (snap, true),
Err(SnapshotError::CrcMismatch) => {
if bytes.len() < 8 {
return Err(SnapshotError::TooShort);
}
let body = &bytes[..bytes.len() - 8];
(
Snapshot::<MicrovmState>::load_without_crc_check(body)?,
false,
)
}
Err(e) => return Err(e),
};
let inferred_memory_path = infer_memory_path(state_path);
let (inferred_memory_exists, inferred_memory_size) = match &inferred_memory_path {
Some(p) => match std::fs::metadata(p) {
Ok(m) => (true, Some(m.len())),
Err(_) => (false, None),
},
None => (false, None),
};
let devices = snapshot
.data
.device_states
.devices
.iter()
.map(|d| (d.kind.clone(), d.id.clone(), d.mmio_slot))
.collect();
Ok(SnapshotDescription {
state_path: state_path.to_path_buf(),
magic: snapshot.header.magic,
version: snapshot.header.version,
expected_version: SNAPSHOT_VERSION,
vcpu_count: snapshot.data.vcpu_states.len(),
mem_size_mib: snapshot.data.vm_info.mem_size_mib,
track_dirty_pages: snapshot.data.vm_info.track_dirty_pages,
devices,
mmds_present: snapshot.data.mmds_state.is_some(),
inferred_memory_path,
inferred_memory_exists,
inferred_memory_size,
crc_ok,
})
}
fn infer_memory_path(state_path: &Path) -> Option<std::path::PathBuf> {
state_path.extension()?;
let mut p = state_path.to_path_buf();
p.set_extension("mem");
Some(p)
}
#[cfg(test)]
mod tests {
use std::path::Path;
use tempfile::TempDir;
use super::*;
use crate::{
envelope::Snapshot,
memory::VecPageReader,
save::{SaveRequest, SnapshotKind, save},
state::{DeviceState, DeviceStates, GicState, MicrovmState, VcpuState, VmInfo},
};
fn build_state() -> MicrovmState {
MicrovmState {
vm_info: VmInfo {
mem_size_mib: 256,
smt: false,
cpu_template: String::new(),
kernel_image_path: "/k".into(),
initrd_path: None,
boot_args: String::new(),
track_dirty_pages: false,
},
vcpu_states: vec![VcpuState::new(0), VcpuState::new(1)],
device_states: DeviceStates::from_devices(vec![DeviceState {
kind: "virtio-block".into(),
id: "rootfs".into(),
mmio_slot: 0,
blob: vec![],
}]),
gic_state: GicState::from_bytes(vec![0xAA; 32]),
mmds_state: None,
}
}
fn dest_in(dir: &Path, name: &str) -> std::path::PathBuf {
dir.join(name)
}
fn save_pair(dir: &Path, name: &str) -> (std::path::PathBuf, std::path::PathBuf) {
let snap = dest_in(dir, &format!("{name}.snap"));
let mem = dest_in(dir, &format!("{name}.mem"));
let reader = VecPageReader::new(vec![0u8; 16 * 1024]);
save(SaveRequest {
state_path: &snap,
memory_path: &mem,
kind: SnapshotKind::Full,
state: build_state(),
memory: &reader,
ram_size: 16 * 1024,
memory_page_size: 16 * 1024,
dirty: None,
})
.unwrap();
(snap, mem)
}
#[test]
fn test_should_round_trip_save_then_load() {
let dir = TempDir::new().unwrap();
let (snap, _mem) = save_pair(dir.path(), "x");
let loaded = load(&snap).unwrap();
assert_eq!(loaded.state.vcpu_states.len(), 2);
assert_eq!(loaded.source_version, SNAPSHOT_VERSION);
}
#[test]
fn test_should_describe_state_file_in_human_form() {
let dir = TempDir::new().unwrap();
let (snap, _mem) = save_pair(dir.path(), "x");
let desc = describe(&snap).unwrap();
let h = desc.human();
assert!(h.contains("vcpu_count: 2"));
assert!(h.contains("mem_size_mib: 256"));
assert!(h.contains("crc_ok: yes"));
}
#[test]
fn test_should_describe_state_file_in_json_form() {
let dir = TempDir::new().unwrap();
let (snap, _mem) = save_pair(dir.path(), "x");
let desc = describe(&snap).unwrap();
let j = desc.to_json();
assert_eq!(j["vcpu_count"], 2);
assert_eq!(j["mem_size_mib"], 256);
assert_eq!(j["crc_ok"], true);
}
#[test]
fn test_should_describe_corrupt_file_with_crc_warning() {
let dir = TempDir::new().unwrap();
let (snap, _mem) = save_pair(dir.path(), "x");
let mut bytes = std::fs::read(&snap).unwrap();
let last = bytes.len() - 1;
bytes[last] ^= 0x01;
std::fs::write(&snap, &bytes).unwrap();
let desc = describe(&snap).unwrap();
assert!(!desc.crc_ok);
assert!(desc.human().contains("crc_ok: NO"));
}
#[test]
fn test_should_infer_memory_path_from_state_path() {
assert_eq!(
infer_memory_path(Path::new("/tmp/x.snap")),
Some(std::path::PathBuf::from("/tmp/x.mem"))
);
assert_eq!(
infer_memory_path(Path::new("/tmp/x.snapshot")),
Some(std::path::PathBuf::from("/tmp/x.mem"))
);
assert_eq!(
infer_memory_path(Path::new("/tmp/x.bin")),
Some(std::path::PathBuf::from("/tmp/x.mem"))
);
assert_eq!(infer_memory_path(Path::new("/tmp/x")), None);
}
#[test]
fn test_should_report_missing_memory_file() {
let dir = TempDir::new().unwrap();
let (snap, mem) = save_pair(dir.path(), "x");
std::fs::remove_file(&mem).unwrap();
let desc = describe(&snap).unwrap();
assert!(!desc.inferred_memory_exists);
assert!(desc.human().contains("MISSING"));
}
#[test]
fn test_should_reject_load_on_incompatible_state() {
let dir = TempDir::new().unwrap();
let snap = dest_in(dir.path(), "x.snap");
let mut bad = build_state();
bad.vcpu_states.clear();
let envelope = Snapshot::new(bad);
let mut buf = Vec::new();
envelope.save(&mut buf).unwrap();
std::fs::write(&snap, &buf).unwrap();
let res = load(&snap);
assert!(matches!(res, Err(SnapshotError::Incompatible)));
}
#[test]
fn test_should_surface_truncated_file_as_too_short() {
let dir = TempDir::new().unwrap();
let snap = dest_in(dir.path(), "x.snap");
std::fs::write(&snap, b"abc").unwrap();
let res = describe(&snap);
assert!(matches!(
res,
Err(SnapshotError::TooShort | SnapshotError::CrcMismatch)
));
}
}