blob: d4446ac8cc77f968d5b6b971714416ee56e49222 [file]
// Copyright 2025 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use std::fmt::Debug;
use std::fmt::Formatter;
use std::fs::File;
use std::io::BufReader;
use std::io::Read;
use std::io::Write;
use std::path::Path;
use std::path::PathBuf;
use anyhow::Context;
use anyhow::Result;
use crypto::CryptKey;
mod any_snapshot;
pub use any_snapshot::AnySnapshot;
// Use 4kB encrypted chunks by default (if encryption is used).
const DEFAULT_ENCRYPTED_CHUNK_SIZE_BYTES: usize = 1024 * 4;
/// Writer of serialized VM snapshots.
///
/// Each fragment is an opaque byte blob. Namespaces can be used to avoid fragment naming
/// collisions between devices.
///
/// In the current implementation, fragments are files and namespaces are directories, but the API
/// is kept abstract so that we can potentially support something like a single file archive
/// output.
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct SnapshotWriter {
dir: PathBuf,
/// If encryption is used, the plaintext key will be stored here.
key: Option<CryptKey>,
}
impl Debug for SnapshotWriter {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SnapshotWriter")
.field("dir", &format!("{:?}", self.dir))
.field("key", if self.key.is_some() { &"Some" } else { &"None" })
.finish()
}
}
impl SnapshotWriter {
/// Creates a new `SnapshotWriter` that will writes its data to a dir at `root`. The path must
/// not exist yet. If encryption is desired, set encrypt (Note: only supported downstream on
/// Windows).
// TODO(b/268094487): If the snapshot fails, we leave incomplete snapshot files at the
// requested path. Consider building up the snapshot dir somewhere else and moving it into
// place at the end.
pub fn new(root: PathBuf, encrypt: bool) -> Result<Self> {
std::fs::create_dir(&root)
.with_context(|| format!("failed to create snapshot root dir: {}", root.display()))?;
if encrypt {
let key = crypto::generate_random_key();
// Creating an empty CryptWriter will still write header information
// to the file, and that header information is what we need. This
// ensures we use a single key for *all* snapshot files.
let mut writer = crypto::CryptWriter::new_from_key(
File::create(root.join("enc_metadata")).context("failed to create enc_metadata")?,
1024,
&key,
)
.context("failed to create enc_metadata writer")?;
writer.flush().context("flush of enc_metadata failed")?;
return Ok(Self {
dir: root,
key: Some(key),
});
}
Ok(Self {
dir: root,
key: None,
})
}
/// Creates a snapshot fragment and get access to the `Write` impl representing it.
pub fn raw_fragment(&self, name: &str) -> Result<Box<dyn Write>> {
self.raw_fragment_with_chunk_size(name, DEFAULT_ENCRYPTED_CHUNK_SIZE_BYTES)
}
/// When encryption is used, allows direct control of the encrypted chunk size.
pub fn raw_fragment_with_chunk_size(
&self,
name: &str,
chunk_size_bytes: usize,
) -> Result<Box<dyn Write>> {
let path = self.dir.join(name);
let file = File::options()
.write(true)
.create_new(true)
.open(&path)
.with_context(|| {
format!(
"failed to create snapshot fragment {name:?} at {}",
path.display()
)
})?;
if let Some(key) = self.key.as_ref() {
return Ok(Box::new(crypto::CryptWriter::new_from_key(
file,
chunk_size_bytes,
key,
)?));
}
Ok(Box::new(file))
}
/// Creates a snapshot fragment from a serialized representation of `v`.
pub fn write_fragment<T: serde::Serialize>(&self, name: &str, v: &T) -> Result<()> {
let mut w = std::io::BufWriter::new(self.raw_fragment(name)?);
ciborium::into_writer(v, &mut w)?;
w.flush()?;
Ok(())
}
/// Creates new namespace and returns a `SnapshotWriter` that writes to it. Namespaces can be
/// nested.
pub fn add_namespace(&self, name: &str) -> Result<Self> {
let dir = self.dir.join(name);
std::fs::create_dir(&dir).with_context(|| {
format!(
"failed to create nested snapshot writer {name:?} at {}",
dir.display()
)
})?;
Ok(Self {
dir,
key: self.key.clone(),
})
}
}
/// Reads snapshots created by `SnapshotWriter`.
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct SnapshotReader {
dir: PathBuf,
/// If encryption is used, the plaintext key will be stored here.
key: Option<CryptKey>,
}
impl Debug for SnapshotReader {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SnapshotReader")
.field("dir", &format!("{:?}", self.dir))
.field("key", if self.key.is_some() { &"Some" } else { &"None" })
.finish()
}
}
impl SnapshotReader {
/// Reads a snapshot at `root`. Set require_encrypted to require an encrypted snapshot.
pub fn new(root: &Path, require_encrypted: bool) -> Result<Self> {
let enc_metadata_path = root.join("enc_metadata");
if Path::exists(&enc_metadata_path) {
let key = Some(
crypto::CryptReader::extract_key(
File::open(&enc_metadata_path).context("failed to open encryption metadata")?,
)
.context("failed to load snapshot key")?,
);
return Ok(Self {
dir: root.to_path_buf(),
key,
});
} else if require_encrypted {
return Err(anyhow::anyhow!("snapshot was not encrypted"));
}
Ok(Self {
dir: root.to_path_buf(),
key: None,
})
}
/// Gets access to a `Read` impl that represents a fragment.
pub fn raw_fragment(&self, name: &str) -> Result<Box<dyn Read>> {
let path = self.dir.join(name);
let file = File::open(&path).with_context(|| {
format!(
"failed to open snapshot fragment {name:?} at {}",
path.display()
)
})?;
if let Some(key) = self.key.as_ref() {
return Ok(Box::new(crypto::CryptReader::from_file_and_key(file, key)?));
}
Ok(Box::new(file))
}
/// Reads a fragment.
pub fn read_fragment<T: serde::de::DeserializeOwned>(&self, name: &str) -> Result<T> {
Ok(ciborium::from_reader(BufReader::new(
self.raw_fragment(name)?,
))?)
}
/// Reads the names of all fragments in this namespace.
pub fn list_fragments(&self) -> Result<Vec<String>> {
let mut result = Vec::new();
for entry in std::fs::read_dir(&self.dir)? {
let entry = entry?;
if entry.file_type()?.is_file() {
result.push(entry.file_name().to_string_lossy().into_owned());
}
}
Ok(result)
}
/// Open a namespace.
pub fn namespace(&self, name: &str) -> Result<Self> {
let dir = self.dir.join(name);
Ok(Self {
dir,
key: self.key.clone(),
})
}
/// Reads the names of all child namespaces
pub fn list_namespaces(&self) -> Result<Vec<String>> {
let mut result = Vec::new();
for entry in std::fs::read_dir(&self.dir)? {
let entry = entry?;
if entry.path().is_dir() {
if let Some(file_name) = entry.path().file_name() {
result.push(file_name.to_string_lossy().into_owned());
}
}
}
Ok(result)
}
}