Skip to main content

squib_api/schemas/
snapshot.rs

1//! `/snapshot/create` and `/snapshot/load` bodies.
2
3use serde::{Deserialize, Serialize};
4
5use super::common::{SafePath, UdsPath};
6
7/// Snapshot type for `/snapshot/create`.
8#[derive(Debug, Clone, Copy, Eq, PartialEq, Default, Serialize, Deserialize)]
9pub enum SnapshotType {
10    /// Full snapshot — the entire memory file is dumped.
11    #[default]
12    Full,
13    /// Diff snapshot — only dirty pages (requires `track_dirty_pages`).
14    Diff,
15}
16
17/// Raw `/snapshot/create` PUT body off the wire.
18#[derive(Debug, Clone, Deserialize)]
19#[serde(deny_unknown_fields)]
20pub struct RawSnapshotCreateConfig {
21    /// Output path for the state file (`<id>.snap`).
22    pub snapshot_path: String,
23    /// Output path for the memory file (`<id>.mem`).
24    pub mem_file_path: String,
25    /// Snapshot type (Full or Diff).
26    #[serde(default)]
27    pub snapshot_type: SnapshotType,
28    /// Optional version override (currently ignored; we follow upstream version).
29    #[serde(default)]
30    pub version: Option<String>,
31}
32
33/// Validated `/snapshot/create` PUT body.
34#[derive(Debug, Clone, Serialize)]
35#[non_exhaustive]
36pub struct SnapshotCreateConfig {
37    /// Validated state-file output path.
38    pub snapshot_path: SafePath,
39    /// Validated memory-file output path.
40    pub mem_file_path: SafePath,
41    /// Snapshot type.
42    pub snapshot_type: SnapshotType,
43    /// Optional version override.
44    pub version: Option<String>,
45}
46
47impl TryFrom<RawSnapshotCreateConfig> for SnapshotCreateConfig {
48    type Error = String;
49
50    fn try_from(raw: RawSnapshotCreateConfig) -> Result<Self, Self::Error> {
51        let snapshot_path =
52            SafePath::new(raw.snapshot_path).map_err(|e| format!("Invalid snapshot_path: {e}"))?;
53        let mem_file_path =
54            SafePath::new(raw.mem_file_path).map_err(|e| format!("Invalid mem_file_path: {e}"))?;
55        if let Some(v) = raw.version.as_deref()
56            && (v.is_empty() || v.len() > 32)
57        {
58            return Err("Invalid version: must be 1..=32 bytes".into());
59        }
60        Ok(Self {
61            snapshot_path,
62            mem_file_path,
63            snapshot_type: raw.snapshot_type,
64            version: raw.version,
65        })
66    }
67}
68
69/// Memory backend type for `/snapshot/load`.
70#[derive(Debug, Clone, Copy, Eq, PartialEq, Default, Serialize, Deserialize)]
71pub enum MemBackendType {
72    /// Memory file is loaded eagerly from disk.
73    #[default]
74    File,
75    /// Memory is faulted in via a userfaultfd-style backend (Mach exception ports on
76    /// Darwin); `backend_path` is a UDS the page server connects to.
77    Uffd,
78}
79
80/// `mem_backend` sub-object on `/snapshot/load`.
81#[derive(Debug, Clone, Deserialize)]
82#[serde(deny_unknown_fields)]
83pub struct RawMemBackend {
84    /// Backend kind.
85    pub backend_type: MemBackendType,
86    /// Path to the memory file (`File`) or to the page-server UDS (`Uffd`).
87    pub backend_path: String,
88}
89
90/// Validated `mem_backend`.
91#[derive(Debug, Clone, Serialize)]
92#[non_exhaustive]
93pub struct MemBackend {
94    /// Backend kind.
95    pub backend_type: MemBackendType,
96    /// Validated backend path. The cap depends on the backend type — UDS for `Uffd`,
97    /// regular file path for `File`.
98    pub backend_path_file: Option<SafePath>,
99    /// UDS path when `backend_type` is Uffd.
100    pub backend_path_uds: Option<UdsPath>,
101}
102
103impl TryFrom<RawMemBackend> for MemBackend {
104    type Error = String;
105
106    fn try_from(raw: RawMemBackend) -> Result<Self, Self::Error> {
107        match raw.backend_type {
108            MemBackendType::File => {
109                let p = SafePath::new(raw.backend_path)
110                    .map_err(|e| format!("Invalid mem_backend.backend_path: {e}"))?;
111                Ok(Self {
112                    backend_type: raw.backend_type,
113                    backend_path_file: Some(p),
114                    backend_path_uds: None,
115                })
116            }
117            MemBackendType::Uffd => {
118                let p = UdsPath::new(raw.backend_path)
119                    .map_err(|e| format!("Invalid mem_backend.backend_path: {e}"))?;
120                Ok(Self {
121                    backend_type: raw.backend_type,
122                    backend_path_file: None,
123                    backend_path_uds: Some(p),
124                })
125            }
126        }
127    }
128}
129
130/// Raw `/snapshot/load` PUT body off the wire.
131#[derive(Debug, Clone, Deserialize)]
132#[serde(deny_unknown_fields)]
133pub struct RawSnapshotLoadConfig {
134    /// Path to the state file.
135    pub snapshot_path: String,
136    /// Memory backend descriptor (post-1.5 upstream API).
137    #[serde(default)]
138    pub mem_backend: Option<RawMemBackend>,
139    /// Deprecated single-string memory file path (pre-1.5 upstream API; squib accepts
140    /// for back-compat).
141    #[serde(default)]
142    pub mem_file_path: Option<String>,
143    /// Whether to track dirty pages after restore (for subsequent diff snapshots).
144    #[serde(default)]
145    pub track_dirty_pages: bool,
146    /// Whether to resume vCPUs immediately after restore.
147    #[serde(default)]
148    pub resume_vm: bool,
149    /// `clock_realtime` — x86-only field; squib accept-and-ignore (A row).
150    #[serde(default)]
151    pub clock_realtime: Option<u64>,
152    /// Per-NIC overrides (passthrough).
153    #[serde(default)]
154    pub network_overrides: Option<Vec<serde_json::Value>>,
155    /// Vsock override (passthrough).
156    #[serde(default)]
157    pub vsock_override: Option<serde_json::Value>,
158}
159
160/// Validated `/snapshot/load` PUT body.
161#[derive(Debug, Clone, Serialize)]
162#[non_exhaustive]
163pub struct SnapshotLoadConfig {
164    /// Validated state-file path.
165    pub snapshot_path: SafePath,
166    /// Validated memory backend (or back-compat `mem_file_path`).
167    pub mem_backend: MemBackend,
168    /// Whether to track dirty pages after restore.
169    pub track_dirty_pages: bool,
170    /// Whether to resume vCPUs immediately after restore.
171    pub resume_vm: bool,
172    /// `clock_realtime` — recorded but ignored (warning emitted).
173    pub clock_realtime: Option<u64>,
174    /// Per-NIC overrides (passthrough).
175    pub network_overrides: Vec<serde_json::Value>,
176    /// Vsock override (passthrough).
177    pub vsock_override: Option<serde_json::Value>,
178}
179
180impl TryFrom<RawSnapshotLoadConfig> for SnapshotLoadConfig {
181    type Error = String;
182
183    fn try_from(raw: RawSnapshotLoadConfig) -> Result<Self, Self::Error> {
184        let snapshot_path =
185            SafePath::new(raw.snapshot_path).map_err(|e| format!("Invalid snapshot_path: {e}"))?;
186        let mem_backend = match (raw.mem_backend, raw.mem_file_path) {
187            (Some(mb), None) => MemBackend::try_from(mb)?,
188            (None, Some(p)) => {
189                // Back-compat single-string form maps to backend_type=File.
190                let p = SafePath::new(p).map_err(|e| format!("Invalid mem_file_path: {e}"))?;
191                MemBackend {
192                    backend_type: MemBackendType::File,
193                    backend_path_file: Some(p),
194                    backend_path_uds: None,
195                }
196            }
197            (Some(_), Some(_)) => {
198                return Err(
199                    "Invalid snapshot/load: provide exactly one of mem_backend or mem_file_path"
200                        .into(),
201                );
202            }
203            (None, None) => {
204                return Err(
205                    "Invalid snapshot/load: must provide mem_backend or mem_file_path".into(),
206                );
207            }
208        };
209        Ok(Self {
210            snapshot_path,
211            mem_backend,
212            track_dirty_pages: raw.track_dirty_pages,
213            resume_vm: raw.resume_vm,
214            clock_realtime: raw.clock_realtime,
215            network_overrides: raw.network_overrides.unwrap_or_default(),
216            vsock_override: raw.vsock_override,
217        })
218    }
219}
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224
225    #[test]
226    fn test_should_validate_snapshot_create_paths() {
227        let cfg = SnapshotCreateConfig::try_from(RawSnapshotCreateConfig {
228            snapshot_path: "/tmp/x.snap".into(),
229            mem_file_path: "/tmp/x.mem".into(),
230            snapshot_type: SnapshotType::Full,
231            version: None,
232        })
233        .unwrap();
234        assert_eq!(cfg.snapshot_path.as_path().as_os_str(), "/tmp/x.snap");
235    }
236
237    #[test]
238    fn test_should_default_snapshot_type_to_full() {
239        let json = r#"{"snapshot_path":"/tmp/x.snap","mem_file_path":"/tmp/x.mem"}"#;
240        let raw: RawSnapshotCreateConfig = serde_json::from_str(json).unwrap();
241        assert_eq!(raw.snapshot_type, SnapshotType::Full);
242    }
243
244    #[test]
245    fn test_should_accept_back_compat_mem_file_path() {
246        let cfg = SnapshotLoadConfig::try_from(RawSnapshotLoadConfig {
247            snapshot_path: "/tmp/x.snap".into(),
248            mem_backend: None,
249            mem_file_path: Some("/tmp/x.mem".into()),
250            track_dirty_pages: false,
251            resume_vm: false,
252            clock_realtime: None,
253            network_overrides: None,
254            vsock_override: None,
255        })
256        .unwrap();
257        assert_eq!(cfg.mem_backend.backend_type, MemBackendType::File);
258    }
259
260    #[test]
261    fn test_should_reject_both_mem_backend_and_mem_file_path() {
262        let mb = RawMemBackend {
263            backend_type: MemBackendType::File,
264            backend_path: "/tmp/x.mem".into(),
265        };
266        let res = SnapshotLoadConfig::try_from(RawSnapshotLoadConfig {
267            snapshot_path: "/tmp/x.snap".into(),
268            mem_backend: Some(mb),
269            mem_file_path: Some("/tmp/y.mem".into()),
270            track_dirty_pages: false,
271            resume_vm: false,
272            clock_realtime: None,
273            network_overrides: None,
274            vsock_override: None,
275        });
276        assert!(res.is_err());
277    }
278
279    #[test]
280    fn test_should_validate_uffd_backend_uds_path() {
281        let mb = RawMemBackend {
282            backend_type: MemBackendType::Uffd,
283            backend_path: "/tmp/pager.sock".into(),
284        };
285        let cfg = SnapshotLoadConfig::try_from(RawSnapshotLoadConfig {
286            snapshot_path: "/tmp/x.snap".into(),
287            mem_backend: Some(mb),
288            mem_file_path: None,
289            track_dirty_pages: true,
290            resume_vm: true,
291            clock_realtime: None,
292            network_overrides: None,
293            vsock_override: None,
294        })
295        .unwrap();
296        assert_eq!(cfg.mem_backend.backend_type, MemBackendType::Uffd);
297        assert!(cfg.mem_backend.backend_path_uds.is_some());
298    }
299}