Skip to main content

smolvm_pack/
format.rs

1//! `.smolmachine` binary format specification.
2//!
3//! # Overview
4//!
5//! A `.smolmachine` is a portable, self-contained microVM artifact. It bundles
6//! everything needed to run a workload: OCI image layers, the agent rootfs,
7//! runtime libraries (libkrun), and a manifest describing the configuration.
8//!
9//! # File Layout
10//!
11//! A `.smolmachine` file is a zstd-compressed tar archive with a JSON manifest
12//! appended as a footer. The manifest is also stored inside the OCI registry
13//! as the config blob when pushed.
14//!
15//! ```text
16//! +---------------------------+
17//! | Assets Blob (zstd tar)    |  30-150 MB
18//! |  - agent-rootfs.tar       |  Guest init system
19//! |  - layers/*.tar           |  OCI image layers
20//! |  - lib/libkrun.*          |  Runtime libraries (platform-specific)
21//! |  - storage.ext4 (opt)     |  Pre-formatted disk template
22//! |  - overlay.raw (opt)      |  VM snapshot (VM mode only)
23//! +---------------------------+
24//! | Manifest (JSON)           |  ~2 KB (PackManifest)
25//! +---------------------------+
26//! | Footer (64 bytes)         |
27//! |  - magic: "SMOLPACK"      |
28//! |  - version: 1             |
29//! |  - offsets + sizes         |
30//! |  - CRC32 checksum         |
31//! +---------------------------+
32//! ```
33//!
34//! # OCI Registry Representation
35//!
36//! When pushed to an OCI registry, a `.smolmachine` is stored as:
37//! - **Config blob**: `PackManifest` JSON (`application/vnd.smolmachines.machine.config.v1+json`)
38//! - **Layer blob**: The full `.smolmachine` file (`application/vnd.smolmachines.smolmachine.v1`)
39//! - **Manifest**: Standard OCI Image Manifest referencing both blobs
40//!
41//! # Execution Modes
42//!
43//! - **Container mode** (default): OCI image layers are unpacked and run via crun.
44//! - **VM mode**: An overlay disk snapshot is restored directly into the VM.
45
46use serde::{Deserialize, Serialize};
47
48use crate::{PackError, Result};
49
50/// Magic bytes identifying a packed smolvm binary.
51pub const MAGIC: &[u8; 8] = b"SMOLPACK";
52
53/// Magic bytes for embedded section header.
54pub const SECTION_MAGIC: &[u8; 8] = b"SMOLSECT";
55
56/// Magic bytes for libs footer appended to the stub binary.
57pub const LIBS_MAGIC: &[u8; 8] = b"SMOLLIBS";
58
59/// Current format version.
60pub const FORMAT_VERSION: u32 = 1;
61
62/// Extension for sidecar assets file.
63pub const SIDECAR_EXTENSION: &str = ".smolmachine";
64
65/// Footer size in bytes (fixed).
66pub const FOOTER_SIZE: usize = 64;
67
68/// Embedded section header size (fixed).
69pub const SECTION_HEADER_SIZE: usize = 32;
70
71/// Libs footer size in bytes (fixed).
72pub const LIBS_FOOTER_SIZE: usize = 32;
73
74/// Header for data embedded in the __SMOLVM,__smolvm Mach-O section.
75///
76/// This format is used for macOS single-file binaries where assets are
77/// stored inside the executable's Mach-O structure, allowing proper code signing.
78///
79/// Layout (32 bytes total):
80/// ```text
81/// Offset  Size  Field
82/// 0       8     magic ("SMOLSECT")
83/// 8       4     version (u32 LE)
84/// 12      4     manifest_size (u32 LE)
85/// 16      8     assets_size (u64 LE)
86/// 24      4     checksum (u32 LE)
87/// 28      4     reserved (zeroes)
88/// ```
89///
90/// Following the header:
91/// - Manifest JSON (manifest_size bytes)
92/// - Compressed assets (assets_size bytes)
93#[derive(Debug, Clone, Copy)]
94pub struct SectionHeader {
95    /// Size of manifest JSON in bytes.
96    pub manifest_size: u32,
97    /// Size of compressed assets in bytes.
98    pub assets_size: u64,
99    /// CRC32 checksum of manifest + assets.
100    pub checksum: u32,
101}
102
103impl SectionHeader {
104    /// Serialize header to bytes.
105    pub fn to_bytes(&self) -> [u8; SECTION_HEADER_SIZE] {
106        let mut buf = [0u8; SECTION_HEADER_SIZE];
107
108        // Magic
109        buf[0..8].copy_from_slice(SECTION_MAGIC);
110
111        // Version
112        buf[8..12].copy_from_slice(&FORMAT_VERSION.to_le_bytes());
113
114        // Manifest size
115        buf[12..16].copy_from_slice(&self.manifest_size.to_le_bytes());
116
117        // Assets size
118        buf[16..24].copy_from_slice(&self.assets_size.to_le_bytes());
119
120        // Checksum
121        buf[24..28].copy_from_slice(&self.checksum.to_le_bytes());
122
123        // Reserved (already zeroed)
124
125        buf
126    }
127
128    /// Deserialize header from bytes.
129    pub fn from_bytes(buf: &[u8]) -> Result<Self> {
130        if buf.len() < SECTION_HEADER_SIZE {
131            return Err(PackError::InvalidMagic);
132        }
133
134        // Validate magic
135        if &buf[0..8] != SECTION_MAGIC {
136            return Err(PackError::InvalidMagic);
137        }
138
139        // Check version
140        let version = u32::from_le_bytes([buf[8], buf[9], buf[10], buf[11]]);
141        if version != FORMAT_VERSION {
142            return Err(PackError::UnsupportedVersion(version));
143        }
144
145        Ok(Self {
146            manifest_size: u32::from_le_bytes([buf[12], buf[13], buf[14], buf[15]]),
147            assets_size: u64::from_le_bytes([
148                buf[16], buf[17], buf[18], buf[19], buf[20], buf[21], buf[22], buf[23],
149            ]),
150            checksum: u32::from_le_bytes([buf[24], buf[25], buf[26], buf[27]]),
151        })
152    }
153}
154
155/// Footer appended to the stub binary to locate embedded runtime libraries.
156///
157/// The stub reads its own last 32 bytes to find the compressed libs bundle,
158/// extracts them to a cache directory, and dlopen's libkrun from there.
159/// This keeps the .smolmachine sidecar cross-platform (no platform-specific libs).
160///
161/// Layout (32 bytes total):
162/// ```text
163/// Offset  Size  Field
164/// 0       8     magic ("SMOLLIBS")
165/// 8       4     version (u32 LE)
166/// 12      8     libs_offset (u64 LE) - offset to compressed libs blob
167/// 20      8     libs_size (u64 LE) - size of compressed libs blob
168/// 28      4     reserved (zeroes)
169/// ```
170#[derive(Debug, Clone, Copy)]
171pub struct LibsFooter {
172    /// Offset from start of file to the compressed libs blob.
173    pub libs_offset: u64,
174    /// Size of the compressed libs blob.
175    pub libs_size: u64,
176}
177
178impl LibsFooter {
179    /// Serialize footer to bytes.
180    pub fn to_bytes(&self) -> [u8; LIBS_FOOTER_SIZE] {
181        let mut buf = [0u8; LIBS_FOOTER_SIZE];
182        buf[0..8].copy_from_slice(LIBS_MAGIC);
183        buf[8..12].copy_from_slice(&1u32.to_le_bytes()); // version 1
184        buf[12..20].copy_from_slice(&self.libs_offset.to_le_bytes());
185        buf[20..28].copy_from_slice(&self.libs_size.to_le_bytes());
186        // 28..32 reserved (zeroed)
187        buf
188    }
189
190    /// Deserialize footer from bytes.
191    pub fn from_bytes(buf: &[u8; LIBS_FOOTER_SIZE]) -> Result<Self> {
192        if &buf[0..8] != LIBS_MAGIC {
193            return Err(PackError::InvalidMagic);
194        }
195        let version = u32::from_le_bytes([buf[8], buf[9], buf[10], buf[11]]);
196        if version != 1 {
197            return Err(PackError::UnsupportedVersion(version));
198        }
199        Ok(Self {
200            libs_offset: u64::from_le_bytes([
201                buf[12], buf[13], buf[14], buf[15], buf[16], buf[17], buf[18], buf[19],
202            ]),
203            libs_size: u64::from_le_bytes([
204                buf[20], buf[21], buf[22], buf[23], buf[24], buf[25], buf[26], buf[27],
205            ]),
206        })
207    }
208}
209
210/// Fixed-size footer at the end of a packed binary.
211///
212/// Layout (64 bytes total):
213/// ```text
214/// Offset  Size  Field
215/// 0       8     magic ("SMOLPACK")
216/// 8       4     version (u32 LE)
217/// 12      8     stub_size (u64 LE) - size of stub executable
218/// 20      8     assets_offset (u64 LE) - offset to compressed assets
219/// 28      8     assets_size (u64 LE) - size of compressed assets
220/// 36      8     manifest_offset (u64 LE) - offset to manifest JSON
221/// 44      8     manifest_size (u64 LE) - size of manifest JSON
222/// 52      4     checksum (u32 LE) - CRC32 of assets + manifest
223/// 56      8     reserved (zeroes)
224/// ```
225#[derive(Debug, Clone, Copy)]
226pub struct PackFooter {
227    /// Size of the stub executable.
228    pub stub_size: u64,
229    /// Offset to compressed assets blob.
230    pub assets_offset: u64,
231    /// Size of compressed assets blob.
232    pub assets_size: u64,
233    /// Offset to manifest JSON.
234    pub manifest_offset: u64,
235    /// Size of manifest JSON.
236    pub manifest_size: u64,
237    /// CRC32 checksum of assets + manifest.
238    pub checksum: u32,
239}
240
241impl PackFooter {
242    /// Serialize footer to bytes.
243    pub fn to_bytes(&self) -> [u8; FOOTER_SIZE] {
244        let mut buf = [0u8; FOOTER_SIZE];
245
246        // Magic
247        buf[0..8].copy_from_slice(MAGIC);
248
249        // Version
250        buf[8..12].copy_from_slice(&FORMAT_VERSION.to_le_bytes());
251
252        // Stub size
253        buf[12..20].copy_from_slice(&self.stub_size.to_le_bytes());
254
255        // Assets offset and size
256        buf[20..28].copy_from_slice(&self.assets_offset.to_le_bytes());
257        buf[28..36].copy_from_slice(&self.assets_size.to_le_bytes());
258
259        // Manifest offset and size
260        buf[36..44].copy_from_slice(&self.manifest_offset.to_le_bytes());
261        buf[44..52].copy_from_slice(&self.manifest_size.to_le_bytes());
262
263        // Checksum
264        buf[52..56].copy_from_slice(&self.checksum.to_le_bytes());
265
266        // Reserved (already zeroed)
267
268        buf
269    }
270
271    /// Deserialize footer from bytes.
272    pub fn from_bytes(buf: &[u8; FOOTER_SIZE]) -> Result<Self> {
273        // Validate magic
274        if &buf[0..8] != MAGIC {
275            return Err(PackError::InvalidMagic);
276        }
277
278        // Check version
279        let version = u32::from_le_bytes([buf[8], buf[9], buf[10], buf[11]]);
280        if version != FORMAT_VERSION {
281            return Err(PackError::UnsupportedVersion(version));
282        }
283
284        Ok(Self {
285            stub_size: u64::from_le_bytes([
286                buf[12], buf[13], buf[14], buf[15], buf[16], buf[17], buf[18], buf[19],
287            ]),
288            assets_offset: u64::from_le_bytes([
289                buf[20], buf[21], buf[22], buf[23], buf[24], buf[25], buf[26], buf[27],
290            ]),
291            assets_size: u64::from_le_bytes([
292                buf[28], buf[29], buf[30], buf[31], buf[32], buf[33], buf[34], buf[35],
293            ]),
294            manifest_offset: u64::from_le_bytes([
295                buf[36], buf[37], buf[38], buf[39], buf[40], buf[41], buf[42], buf[43],
296            ]),
297            manifest_size: u64::from_le_bytes([
298                buf[44], buf[45], buf[46], buf[47], buf[48], buf[49], buf[50], buf[51],
299            ]),
300            checksum: u32::from_le_bytes([buf[52], buf[53], buf[54], buf[55]]),
301        })
302    }
303}
304
305/// Execution mode for packed binaries.
306///
307/// Determines how commands are executed at runtime:
308/// - `Container`: commands run inside a crun container (OCI layers)
309/// - `Vm`: commands run directly in the VM rootfs (overlay disk)
310#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
311#[serde(rename_all = "lowercase")]
312pub enum PackMode {
313    /// Container mode: OCI image layers + crun container execution.
314    #[default]
315    Container,
316    /// VM mode: overlay disk + direct VM execution.
317    Vm,
318}
319
320/// Manifest describing the packed image and configuration.
321#[derive(Debug, Clone, Serialize, Deserialize)]
322pub struct PackManifest {
323    /// Execution mode (container or VM).
324    #[serde(default)]
325    pub mode: PackMode,
326
327    /// Original image reference (e.g., "alpine:latest").
328    pub image: String,
329
330    /// Image digest (sha256:...).
331    pub digest: String,
332
333    /// Target platform (e.g., "linux/arm64").
334    pub platform: String,
335
336    /// Entrypoint command (from image config or override).
337    #[serde(default, skip_serializing_if = "Vec::is_empty")]
338    pub entrypoint: Vec<String>,
339
340    /// Default command arguments (from image config or override).
341    #[serde(default, skip_serializing_if = "Vec::is_empty")]
342    pub cmd: Vec<String>,
343
344    /// Default environment variables.
345    #[serde(default, skip_serializing_if = "Vec::is_empty")]
346    pub env: Vec<String>,
347
348    /// Secret references carried with the pack. Each maps an env var name to a
349    /// reference (host store entry, host env var, or file) — never an inline
350    /// value. The plaintext is resolved on the run host at exec time, so a
351    /// `.smolmachine` never contains secret material at rest.
352    #[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
353    pub secret_refs: std::collections::BTreeMap<String, smolvm_protocol::SecretRef>,
354
355    /// Working directory (from image config or override).
356    #[serde(default, skip_serializing_if = "Option::is_none")]
357    pub workdir: Option<String>,
358
359    /// Default number of vCPUs.
360    pub cpus: u8,
361
362    /// Default memory in MiB.
363    pub mem: u32,
364
365    /// Total extracted (on-disk) image size in bytes.
366    /// Used to auto-size the storage disk at runtime.
367    #[serde(default)]
368    pub image_size: u64,
369
370    /// Whether outbound networking is enabled by default.
371    #[serde(default)]
372    pub network: bool,
373
374    /// Enable GPU acceleration (Vulkan via virtio-gpu).
375    #[serde(default)]
376    pub gpu: bool,
377
378    /// Host platform this .smolmachine runs on (e.g., "darwin/arm64").
379    /// Distinct from `platform` which is the guest architecture (always linux).
380    /// Used for registry Image Index resolution.
381    pub host_platform: String,
382
383    /// RFC 3339 timestamp when this machine was packed.
384    pub created: String,
385
386    /// smolvm version that built this machine (e.g., "0.1.15").
387    pub smolvm_version: String,
388
389    /// Asset inventory - files included in the assets blob.
390    pub assets: AssetInventory,
391}
392
393/// Inventory of assets included in the packed binary.
394#[derive(Debug, Clone, Serialize, Deserialize)]
395pub struct AssetInventory {
396    /// Runtime libraries (relative paths within assets).
397    pub libraries: Vec<AssetEntry>,
398
399    /// Agent rootfs tarball.
400    pub agent_rootfs: AssetEntry,
401
402    /// OCI layer tarballs.
403    pub layers: Vec<LayerEntry>,
404
405    /// Pre-formatted storage disk template (optional).
406    /// When present, copied to cache on first run instead of formatting at runtime.
407    #[serde(default, skip_serializing_if = "Option::is_none")]
408    pub storage_template: Option<AssetEntry>,
409
410    /// Overlay disk template (optional, VM mode only).
411    /// Contains the VM's persistent rootfs state from a `--from-vm` pack.
412    #[serde(default, skip_serializing_if = "Option::is_none")]
413    pub overlay_template: Option<AssetEntry>,
414
415    /// Original sparse size of the overlay disk, in bytes (optional, VM mode only).
416    ///
417    /// When set, the overlay.raw entry in the archive is a truncated copy with
418    /// the trailing sparse hole removed. On extraction the file is extended to
419    /// this size via `ftruncate`, restoring the sparse skeleton the VM expects.
420    #[serde(default, skip_serializing_if = "Option::is_none")]
421    pub overlay_logical_size: Option<u64>,
422}
423
424/// An asset file entry.
425#[derive(Debug, Clone, Serialize, Deserialize)]
426pub struct AssetEntry {
427    /// Path within the assets archive.
428    pub path: String,
429
430    /// Uncompressed size in bytes.
431    pub size: u64,
432}
433
434/// An OCI layer entry.
435#[derive(Debug, Clone, Serialize, Deserialize)]
436pub struct LayerEntry {
437    /// Layer digest (sha256:...).
438    pub digest: String,
439
440    /// Path within the assets archive.
441    pub path: String,
442
443    /// Uncompressed size in bytes.
444    pub size: u64,
445}
446
447/// Generate an RFC 3339 timestamp for the current time in UTC.
448fn rfc3339_now() -> String {
449    let now = time::OffsetDateTime::now_utc();
450    now.format(&time::format_description::well_known::Rfc3339)
451        .expect("RFC 3339 formatting should never fail for a valid OffsetDateTime")
452}
453
454impl PackManifest {
455    /// Create a new manifest with default values.
456    pub fn new(image: String, digest: String, platform: String, host_platform: String) -> Self {
457        Self {
458            mode: PackMode::default(),
459            image,
460            digest,
461            platform,
462            entrypoint: Vec::new(),
463            cmd: Vec::new(),
464            env: Vec::new(),
465            secret_refs: std::collections::BTreeMap::new(),
466            workdir: None,
467            cpus: 1,
468            mem: 256,
469            image_size: 0,
470            network: false,
471            gpu: false,
472            host_platform,
473            created: rfc3339_now(),
474            smolvm_version: env!("CARGO_PKG_VERSION").to_string(),
475            assets: AssetInventory {
476                libraries: Vec::new(),
477                agent_rootfs: AssetEntry {
478                    path: "agent-rootfs.tar".to_string(),
479                    size: 0,
480                },
481                layers: Vec::new(),
482                storage_template: None,
483                overlay_template: None,
484                overlay_logical_size: None,
485            },
486        }
487    }
488
489    /// Serialize manifest to JSON.
490    pub fn to_json(&self) -> Result<Vec<u8>> {
491        Ok(serde_json::to_vec_pretty(self)?)
492    }
493
494    /// Deserialize manifest from JSON.
495    pub fn from_json(data: &[u8]) -> Result<Self> {
496        Ok(serde_json::from_slice(data)?)
497    }
498}
499
500#[cfg(test)]
501mod tests {
502    use super::*;
503
504    #[test]
505    fn test_manifest_secret_refs_roundtrip() {
506        // Secret refs survive a JSON round-trip through the manifest, and are
507        // omitted entirely when empty (skip_serializing_if) so existing packs
508        // stay byte-compatible.
509        let empty = PackManifest::new(
510            "alpine".to_string(),
511            "sha256:abc".to_string(),
512            "linux/arm64".to_string(),
513            "linux/arm64".to_string(),
514        );
515        let json = String::from_utf8(empty.to_json().unwrap()).unwrap();
516        assert!(
517            !json.contains("secret_refs"),
518            "empty secret_refs must not be serialized"
519        );
520
521        let mut m = empty;
522        m.secret_refs.insert(
523            "DB_PASSWORD".to_string(),
524            smolvm_protocol::SecretRef {
525                from_env: Some("PGPASSWORD".to_string()),
526                from_file: None,
527            },
528        );
529        let restored = PackManifest::from_json(&m.to_json().unwrap()).unwrap();
530        assert_eq!(restored.secret_refs.len(), 1);
531        assert_eq!(
532            restored.secret_refs["DB_PASSWORD"].from_env.as_deref(),
533            Some("PGPASSWORD")
534        );
535    }
536
537    #[test]
538    fn test_footer_roundtrip() {
539        let footer = PackFooter {
540            stub_size: 512 * 1024,
541            assets_offset: 512 * 1024,
542            assets_size: 50 * 1024 * 1024,
543            manifest_offset: 512 * 1024 + 50 * 1024 * 1024,
544            manifest_size: 2048,
545            checksum: 0xDEADBEEF,
546        };
547
548        let bytes = footer.to_bytes();
549        assert_eq!(bytes.len(), FOOTER_SIZE);
550
551        let restored = PackFooter::from_bytes(&bytes).unwrap();
552        assert_eq!(restored.stub_size, footer.stub_size);
553        assert_eq!(restored.assets_offset, footer.assets_offset);
554        assert_eq!(restored.assets_size, footer.assets_size);
555        assert_eq!(restored.manifest_offset, footer.manifest_offset);
556        assert_eq!(restored.manifest_size, footer.manifest_size);
557        assert_eq!(restored.checksum, footer.checksum);
558    }
559
560    #[test]
561    fn test_footer_invalid_magic() {
562        let mut bytes = [0u8; FOOTER_SIZE];
563        bytes[0..8].copy_from_slice(b"BADMAGIC");
564
565        let result = PackFooter::from_bytes(&bytes);
566        assert!(matches!(result, Err(PackError::InvalidMagic)));
567    }
568
569    #[test]
570    fn test_footer_unsupported_version() {
571        let mut bytes = [0u8; FOOTER_SIZE];
572        bytes[0..8].copy_from_slice(MAGIC);
573        bytes[8..12].copy_from_slice(&99u32.to_le_bytes()); // Bad version
574
575        let result = PackFooter::from_bytes(&bytes);
576        assert!(matches!(result, Err(PackError::UnsupportedVersion(99))));
577    }
578
579    #[test]
580    fn test_manifest_roundtrip() {
581        let mut manifest = PackManifest::new(
582            "alpine:latest".to_string(),
583            "sha256:abc123".to_string(),
584            "linux/arm64".to_string(),
585            "darwin/arm64".to_string(),
586        );
587        manifest.cpus = 2;
588        manifest.mem = 1024;
589        manifest.entrypoint = vec!["/bin/sh".to_string()];
590        manifest.env = vec!["PATH=/usr/local/bin:/usr/bin:/bin".to_string()];
591        manifest.assets.libraries.push(AssetEntry {
592            path: "lib/libkrun.dylib".to_string(),
593            size: 4 * 1024 * 1024,
594        });
595
596        let json = manifest.to_json().unwrap();
597        let restored = PackManifest::from_json(&json).unwrap();
598
599        assert_eq!(restored.image, "alpine:latest");
600        assert_eq!(restored.digest, "sha256:abc123");
601        assert_eq!(restored.cpus, 2);
602        assert_eq!(restored.mem, 1024);
603        assert_eq!(restored.entrypoint, vec!["/bin/sh"]);
604        assert_eq!(restored.assets.libraries.len(), 1);
605    }
606
607    #[test]
608    fn test_manifest_json_format() {
609        let manifest = PackManifest::new(
610            "ubuntu:22.04".to_string(),
611            "sha256:def456".to_string(),
612            "linux/amd64".to_string(),
613            "linux/amd64".to_string(),
614        );
615
616        let json = String::from_utf8(manifest.to_json().unwrap()).unwrap();
617        assert!(json.contains("\"image\": \"ubuntu:22.04\""));
618        assert!(json.contains("\"platform\": \"linux/amd64\""));
619        // Phase 0 fields: verify key names serialize correctly
620        assert!(json.contains("\"host_platform\": \"linux/amd64\""));
621        assert!(json.contains("\"smolvm_version\""));
622        assert!(json.contains("\"created\""));
623    }
624
625    #[test]
626    fn test_pack_mode_default_is_container() {
627        assert_eq!(PackMode::default(), PackMode::Container);
628    }
629
630    #[test]
631    fn test_pack_mode_vm_roundtrip() {
632        let mut manifest = PackManifest::new(
633            "vm://myvm".to_string(),
634            "none".to_string(),
635            "linux/arm64".to_string(),
636            "darwin/arm64".to_string(),
637        );
638        manifest.mode = PackMode::Vm;
639        manifest.assets.overlay_template = Some(AssetEntry {
640            path: "overlay.raw".to_string(),
641            size: 2 * 1024 * 1024 * 1024,
642        });
643
644        let json = manifest.to_json().unwrap();
645        let restored = PackManifest::from_json(&json).unwrap();
646        assert_eq!(restored.mode, PackMode::Vm);
647        assert!(restored.assets.overlay_template.is_some());
648        assert_eq!(
649            restored.assets.overlay_template.unwrap().path,
650            "overlay.raw"
651        );
652    }
653}