snapshot/
lib.rs

1// Copyright 2025 The ChromiumOS Authors
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use std::fmt::Debug;
6use std::fmt::Formatter;
7use std::fs::File;
8use std::io::BufReader;
9use std::io::Read;
10use std::io::Write;
11use std::path::Path;
12use std::path::PathBuf;
13
14use anyhow::Context;
15use anyhow::Result;
16use crypto::CryptKey;
17
18mod any_snapshot;
19
20pub use any_snapshot::AnySnapshot;
21
22// Use 4kB encrypted chunks by default (if encryption is used).
23const DEFAULT_ENCRYPTED_CHUNK_SIZE_BYTES: usize = 1024 * 4;
24
25/// Writer of serialized VM snapshots.
26///
27/// Each fragment is an opaque byte blob. Namespaces can be used to avoid fragment naming
28/// collisions between devices.
29///
30/// In the current implementation, fragments are files and namespaces are directories, but the API
31/// is kept abstract so that we can potentially support something like a single file archive
32/// output.
33#[derive(Clone, serde::Serialize, serde::Deserialize)]
34pub struct SnapshotWriter {
35    dir: PathBuf,
36    /// If encryption is used, the plaintext key will be stored here.
37    key: Option<CryptKey>,
38}
39
40impl Debug for SnapshotWriter {
41    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
42        f.debug_struct("SnapshotWriter")
43            .field("dir", &format!("{:?}", self.dir))
44            .field("key", if self.key.is_some() { &"Some" } else { &"None" })
45            .finish()
46    }
47}
48
49impl SnapshotWriter {
50    /// Creates a new `SnapshotWriter` that will writes its data to a dir at `root`. The path must
51    /// not exist yet. If encryption is desired, set encrypt (Note: only supported downstream on
52    /// Windows).
53    // TODO(b/268094487): If the snapshot fails, we leave incomplete snapshot files at the
54    // requested path. Consider building up the snapshot dir somewhere else and moving it into
55    // place at the end.
56    pub fn new(root: PathBuf, encrypt: bool) -> Result<Self> {
57        std::fs::create_dir(&root)
58            .with_context(|| format!("failed to create snapshot root dir: {}", root.display()))?;
59
60        if encrypt {
61            let key = crypto::generate_random_key();
62            // Creating an empty CryptWriter will still write header information
63            // to the file, and that header information is what we need. This
64            // ensures we use a single key for *all* snapshot files.
65            let mut writer = crypto::CryptWriter::new_from_key(
66                File::create(root.join("enc_metadata")).context("failed to create enc_metadata")?,
67                1024,
68                &key,
69            )
70            .context("failed to create enc_metadata writer")?;
71            writer.flush().context("flush of enc_metadata failed")?;
72            return Ok(Self {
73                dir: root,
74                key: Some(key),
75            });
76        }
77
78        Ok(Self {
79            dir: root,
80            key: None,
81        })
82    }
83
84    /// Creates a snapshot fragment and get access to the `Write` impl representing it.
85    pub fn raw_fragment(&self, name: &str) -> Result<Box<dyn Write>> {
86        self.raw_fragment_with_chunk_size(name, DEFAULT_ENCRYPTED_CHUNK_SIZE_BYTES)
87    }
88
89    /// When encryption is used, allows direct control of the encrypted chunk size.
90    pub fn raw_fragment_with_chunk_size(
91        &self,
92        name: &str,
93        chunk_size_bytes: usize,
94    ) -> Result<Box<dyn Write>> {
95        let path = self.dir.join(name);
96        let file = File::options()
97            .write(true)
98            .create_new(true)
99            .open(&path)
100            .with_context(|| {
101                format!(
102                    "failed to create snapshot fragment {name:?} at {}",
103                    path.display()
104                )
105            })?;
106
107        if let Some(key) = self.key.as_ref() {
108            return Ok(Box::new(crypto::CryptWriter::new_from_key(
109                file,
110                chunk_size_bytes,
111                key,
112            )?));
113        }
114
115        Ok(Box::new(file))
116    }
117
118    /// Creates a snapshot fragment from a serialized representation of `v`.
119    pub fn write_fragment<T: serde::Serialize>(&self, name: &str, v: &T) -> Result<()> {
120        let mut w = std::io::BufWriter::new(self.raw_fragment(name)?);
121        ciborium::into_writer(v, &mut w)?;
122        w.flush()?;
123        Ok(())
124    }
125
126    /// Creates new namespace and returns a `SnapshotWriter` that writes to it. Namespaces can be
127    /// nested.
128    pub fn add_namespace(&self, name: &str) -> Result<Self> {
129        let dir = self.dir.join(name);
130        std::fs::create_dir(&dir).with_context(|| {
131            format!(
132                "failed to create nested snapshot writer {name:?} at {}",
133                dir.display()
134            )
135        })?;
136        Ok(Self {
137            dir,
138            key: self.key.clone(),
139        })
140    }
141}
142
143/// Reads snapshots created by `SnapshotWriter`.
144#[derive(Clone, serde::Serialize, serde::Deserialize)]
145pub struct SnapshotReader {
146    dir: PathBuf,
147    /// If encryption is used, the plaintext key will be stored here.
148    key: Option<CryptKey>,
149}
150
151impl Debug for SnapshotReader {
152    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
153        f.debug_struct("SnapshotReader")
154            .field("dir", &format!("{:?}", self.dir))
155            .field("key", if self.key.is_some() { &"Some" } else { &"None" })
156            .finish()
157    }
158}
159
160impl SnapshotReader {
161    /// Reads a snapshot at `root`. Set require_encrypted to require an encrypted snapshot.
162    pub fn new(root: &Path, require_encrypted: bool) -> Result<Self> {
163        let enc_metadata_path = root.join("enc_metadata");
164        if Path::exists(&enc_metadata_path) {
165            let key = Some(
166                crypto::CryptReader::extract_key(
167                    File::open(&enc_metadata_path).context("failed to open encryption metadata")?,
168                )
169                .context("failed to load snapshot key")?,
170            );
171            return Ok(Self {
172                dir: root.to_path_buf(),
173                key,
174            });
175        } else if require_encrypted {
176            return Err(anyhow::anyhow!("snapshot was not encrypted"));
177        }
178
179        Ok(Self {
180            dir: root.to_path_buf(),
181            key: None,
182        })
183    }
184
185    /// Gets access to a `Read` impl that represents a fragment.
186    pub fn raw_fragment(&self, name: &str) -> Result<Box<dyn Read>> {
187        let path = self.dir.join(name);
188        let file = File::open(&path).with_context(|| {
189            format!(
190                "failed to open snapshot fragment {name:?} at {}",
191                path.display()
192            )
193        })?;
194        if let Some(key) = self.key.as_ref() {
195            return Ok(Box::new(crypto::CryptReader::from_file_and_key(file, key)?));
196        }
197
198        Ok(Box::new(file))
199    }
200
201    /// Reads a fragment.
202    pub fn read_fragment<T: serde::de::DeserializeOwned>(&self, name: &str) -> Result<T> {
203        Ok(ciborium::from_reader(BufReader::new(
204            self.raw_fragment(name)?,
205        ))?)
206    }
207
208    /// Reads the names of all fragments in this namespace.
209    pub fn list_fragments(&self) -> Result<Vec<String>> {
210        let mut result = Vec::new();
211        for entry in std::fs::read_dir(&self.dir)? {
212            let entry = entry?;
213            if entry.file_type()?.is_file() {
214                result.push(entry.file_name().to_string_lossy().into_owned());
215            }
216        }
217        Ok(result)
218    }
219
220    /// Open a namespace.
221    pub fn namespace(&self, name: &str) -> Result<Self> {
222        let dir = self.dir.join(name);
223        Ok(Self {
224            dir,
225            key: self.key.clone(),
226        })
227    }
228
229    /// Reads the names of all child namespaces
230    pub fn list_namespaces(&self) -> Result<Vec<String>> {
231        let mut result = Vec::new();
232        for entry in std::fs::read_dir(&self.dir)? {
233            let entry = entry?;
234            if entry.path().is_dir() {
235                if let Some(file_name) = entry.path().file_name() {
236                    result.push(file_name.to_string_lossy().into_owned());
237                }
238            }
239        }
240        Ok(result)
241    }
242}