Skip to main content

smolvm_pack/
detect.rs

1//! Packed binary auto-detection.
2//!
3//! Determines whether the current executable is a packed binary and what mode
4//! it is running in: section-embedded (macOS), append-embedded, sidecar, or
5//! normal CLI.
6//!
7//! Called at the very start of `main()` before clap parsing so that a packed
8//! binary (e.g. `./my-app echo hello`) shows packed-binary help rather
9//! than the full smolvm CLI.
10
11use crate::format::{PackFooter, FOOTER_SIZE};
12use crate::packer::{read_footer_from_sidecar, sidecar_path_for};
13use std::path::{Path, PathBuf};
14
15/// The detected packed binary mode.
16pub enum PackedMode {
17    /// Assets embedded in a Mach-O `__SMOLVM,__smolvm` section (macOS single-file).
18    #[cfg(target_os = "macos")]
19    Section {
20        /// Parsed manifest from the section.
21        manifest: Box<crate::format::PackManifest>,
22        /// CRC32 checksum from the section header.
23        checksum: u32,
24        /// Pointer to compressed assets in the section (valid for process lifetime).
25        assets_ptr: *const u8,
26        /// Size of compressed assets.
27        assets_size: usize,
28    },
29    /// Assets appended to the binary (Linux single-file, or macOS fallback).
30    Embedded {
31        /// Path to the current executable.
32        exe_path: PathBuf,
33        /// Footer read from end of binary.
34        footer: PackFooter,
35    },
36    /// Assets in a `.smolmachine` sidecar file alongside the binary.
37    Sidecar {
38        /// Path to the `.smolmachine` file.
39        sidecar_path: PathBuf,
40        /// Footer read from end of sidecar.
41        footer: PackFooter,
42    },
43}
44
45/// Detect whether this process is running as a packed binary.
46///
47/// Checks in order:
48/// 1. macOS Mach-O section (`__SMOLVM,__smolvm`) with `SMOLSECT` magic
49/// 2. `<exe>.smolmachine` sidecar file with valid footer
50/// 3. `SMOLPACK` footer appended to own binary with `assets_offset > 0`
51///
52/// Returns `None` for normal `smolvm` invocations (no false positives).
53pub fn detect_packed_mode() -> Option<PackedMode> {
54    let exe_path = std::env::current_exe().ok()?;
55
56    // 1. macOS section mode
57    #[cfg(target_os = "macos")]
58    {
59        if let Some(mode) = try_section_mode() {
60            return Some(mode);
61        }
62    }
63
64    // 2. Sidecar mode: <exe>.smolmachine next to binary
65    if let Some(mode) = try_sidecar_mode(&exe_path) {
66        return Some(mode);
67    }
68
69    // 3. Embedded-append mode: SMOLPACK footer at end of own binary
70    if let Some(mode) = try_embedded_mode(&exe_path) {
71        return Some(mode);
72    }
73
74    None
75}
76
77fn try_sidecar_mode(exe_path: &Path) -> Option<PackedMode> {
78    let sidecar = sidecar_path_for(exe_path);
79    if !sidecar.exists() {
80        return None;
81    }
82    let footer = read_footer_from_sidecar(&sidecar).ok()?;
83    Some(PackedMode::Sidecar {
84        sidecar_path: sidecar,
85        footer,
86    })
87}
88
89fn try_embedded_mode(exe_path: &Path) -> Option<PackedMode> {
90    let footer = read_footer_direct(exe_path).ok()?;
91    // assets_offset > 0 means assets are appended after the stub in the binary
92    if footer.assets_offset > 0 {
93        Some(PackedMode::Embedded {
94            exe_path: exe_path.to_path_buf(),
95            footer,
96        })
97    } else {
98        None
99    }
100}
101
102/// Read footer directly from end of file without trying sidecar first.
103///
104/// Unlike `packer::read_footer()` which checks for a sidecar, this reads
105/// the last 64 bytes of the given file and parses them as a `PackFooter`.
106fn read_footer_direct(path: &Path) -> crate::Result<PackFooter> {
107    use std::fs::File;
108    use std::io::{Read, Seek, SeekFrom};
109
110    let mut file = File::open(path)?;
111    let file_size = file.metadata()?.len();
112    if file_size < FOOTER_SIZE as u64 {
113        return Err(crate::PackError::InvalidMagic);
114    }
115    file.seek(SeekFrom::End(-(FOOTER_SIZE as i64)))?;
116    let mut buf = [0u8; FOOTER_SIZE];
117    file.read_exact(&mut buf)?;
118    PackFooter::from_bytes(&buf)
119}
120
121// ---------------------------------------------------------------------------
122// macOS Mach-O section reading
123// ---------------------------------------------------------------------------
124
125#[cfg(target_os = "macos")]
126fn try_section_mode() -> Option<PackedMode> {
127    let embedded = read_embedded_section()?;
128    Some(PackedMode::Section {
129        manifest: Box::new(embedded.manifest),
130        checksum: embedded.header.checksum,
131        assets_ptr: embedded.assets_ptr,
132        assets_size: embedded.assets_size,
133    })
134}
135
136/// Data read from the `__SMOLVM,__smolvm` Mach-O section.
137#[cfg(target_os = "macos")]
138struct EmbeddedData {
139    header: crate::format::SectionHeader,
140    manifest: crate::format::PackManifest,
141    assets_ptr: *const u8,
142    assets_size: usize,
143}
144
145/// Try to read embedded data from the `__SMOLVM,__smolvm` Mach-O section.
146///
147/// Returns `None` if:
148/// - Not running on macOS
149/// - Section doesn't exist
150/// - Section contains only the build-time placeholder (not `SMOLSECT` magic)
151#[cfg(target_os = "macos")]
152fn read_embedded_section() -> Option<EmbeddedData> {
153    use crate::format::{PackManifest, SectionHeader, SECTION_HEADER_SIZE, SECTION_MAGIC};
154
155    extern "C" {
156        fn getsectiondata(
157            mhp: *const MachHeader64,
158            segname: *const i8,
159            sectname: *const i8,
160            size: *mut usize,
161        ) -> *const u8;
162    }
163
164    #[repr(C)]
165    struct MachHeader64 {
166        magic: u32,
167        cputype: i32,
168        cpusubtype: i32,
169        filetype: u32,
170        ncmds: u32,
171        sizeofcmds: u32,
172        flags: u32,
173        reserved: u32,
174    }
175
176    extern "C" {
177        fn _dyld_get_image_header(image_index: u32) -> *const MachHeader64;
178    }
179
180    unsafe {
181        let header = _dyld_get_image_header(0);
182        if header.is_null() {
183            return None;
184        }
185
186        let segname = c"__SMOLVM";
187        let sectname = c"__smolvm";
188        let mut size: usize = 0;
189
190        let data_ptr = getsectiondata(header, segname.as_ptr(), sectname.as_ptr(), &mut size);
191
192        if data_ptr.is_null() || size < SECTION_HEADER_SIZE {
193            return None;
194        }
195
196        // Check for SMOLSECT magic (placeholder contains different marker)
197        let magic_bytes = std::slice::from_raw_parts(data_ptr, 8);
198        if magic_bytes != SECTION_MAGIC {
199            return None;
200        }
201
202        // Parse section header
203        let header_bytes = std::slice::from_raw_parts(data_ptr, SECTION_HEADER_SIZE);
204        let section_header = SectionHeader::from_bytes(header_bytes).ok()?;
205
206        // Validate sizes
207        let expected_size = SECTION_HEADER_SIZE
208            + section_header.manifest_size as usize
209            + section_header.assets_size as usize;
210        if size < expected_size {
211            return None;
212        }
213
214        // Parse manifest
215        let manifest_start = data_ptr.add(SECTION_HEADER_SIZE);
216        let manifest_bytes =
217            std::slice::from_raw_parts(manifest_start, section_header.manifest_size as usize);
218        let manifest = PackManifest::from_json(manifest_bytes).ok()?;
219
220        // Assets follow the manifest
221        let assets_ptr = manifest_start.add(section_header.manifest_size as usize);
222
223        Some(EmbeddedData {
224            header: section_header,
225            manifest,
226            assets_ptr,
227            assets_size: section_header.assets_size as usize,
228        })
229    }
230}
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235
236    #[test]
237    fn test_detect_returns_none_for_normal_binary() {
238        // The test binary is a normal Rust executable, not a packed binary.
239        assert!(detect_packed_mode().is_none());
240    }
241
242    #[test]
243    fn test_read_footer_direct_rejects_short_file() {
244        let dir = tempfile::tempdir().unwrap();
245        let path = dir.path().join("short.bin");
246        std::fs::write(&path, b"too short").unwrap();
247        assert!(read_footer_direct(&path).is_err());
248    }
249
250    #[test]
251    fn test_read_footer_direct_rejects_invalid_magic() {
252        let dir = tempfile::tempdir().unwrap();
253        let path = dir.path().join("no_magic.bin");
254        std::fs::write(&path, &[0u8; 128]).unwrap();
255        assert!(read_footer_direct(&path).is_err());
256    }
257}