use std::path::PathBuf;
use semver::Version;
use thiserror::Error;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum SnapshotError {
#[error("a vCPU did not ack quiesce within the timeout")]
QuiesceTimeout,
#[error("snapshot magic mismatch (file: {found:#x}, expected: {expected:#x})")]
MagicMismatch {
found: u64,
expected: u64,
},
#[error("snapshot version {found} is incompatible with squib's {expected}")]
VersionMismatch {
found: Version,
expected: Version,
},
#[error("snapshot CRC64 mismatch")]
CrcMismatch,
#[error("snapshot file is too short to contain a CRC trailer")]
TooShort,
#[error("snapshot is from a different VMM (sysreg or GIC blob shape mismatch)")]
Incompatible,
#[error("atomic commit (rename) failed: {0}")]
AtomicCommitFailed(#[source] std::io::Error),
#[error(
"snapshot temp-file path is on a different filesystem from the destination \
(dest={dest:?}, temp_dir={temp_dir:?}); rename(2) cannot be atomic across mounts"
)]
AtomicCommitCrossFs {
dest: PathBuf,
temp_dir: PathBuf,
},
#[error("invalid snapshot path: {0}")]
InvalidPath(String),
#[error("snapshot encoding error: {0}")]
Bitcode(String),
#[error("snapshot exceeds {limit} byte deserialization limit")]
SizeLimitExceeded {
limit: usize,
},
#[error("memory file I/O error: {0}")]
MemoryIo(#[source] std::io::Error),
#[error("snapshot I/O error: {0}")]
Io(#[source] std::io::Error),
#[error("snapshot capture/restore failure: {0}")]
Capture(String),
}
impl SnapshotError {
#[must_use]
pub fn wire_message(&self) -> String {
self.to_string()
}
}
impl From<bitcode::Error> for SnapshotError {
fn from(err: bitcode::Error) -> Self {
Self::Bitcode(err.to_string())
}
}
impl From<std::io::Error> for SnapshotError {
fn from(err: std::io::Error) -> Self {
Self::Io(err)
}
}
pub type Result<T, E = SnapshotError> = core::result::Result<T, E>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_should_render_quiesce_timeout_with_stable_message() {
let err = SnapshotError::QuiesceTimeout;
assert_eq!(err.to_string(), err.wire_message());
assert_eq!(
err.wire_message(),
"a vCPU did not ack quiesce within the timeout"
);
}
#[test]
fn test_should_format_magic_mismatch_with_hex() {
let err = SnapshotError::MagicMismatch {
found: 0xDEAD_BEEF,
expected: 0x0710_1984_AAAA_0000,
};
let s = err.wire_message();
assert!(s.contains("0xdeadbeef"), "msg = {s}");
assert!(s.contains("0x7101984aaaa0000"), "msg = {s}");
}
#[test]
fn test_should_classify_io_errors_under_io_variant() {
let io = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "no");
let err: SnapshotError = io.into();
assert!(matches!(err, SnapshotError::Io(_)));
}
#[test]
fn test_should_route_atomic_commit_failure_into_dedicated_variant() {
let io = std::io::Error::other("rename failed");
let err = SnapshotError::AtomicCommitFailed(io);
assert!(
err.wire_message()
.starts_with("atomic commit (rename) failed:")
);
}
#[test]
fn test_should_describe_cross_fs_with_paths() {
let err = SnapshotError::AtomicCommitCrossFs {
dest: PathBuf::from("/dest/x.snap"),
temp_dir: PathBuf::from("/other-fs"),
};
let s = err.wire_message();
assert!(s.contains("/dest/x.snap"));
assert!(s.contains("/other-fs"));
}
}