Skip to main content

provenant/utils/
magic.rs

1//! File magic byte detection utilities.
2//!
3//! Provides low-level file format detection by reading and checking magic bytes
4//! at the beginning of files. Used by parsers to disambiguate file types that
5//! share the same extension (e.g., Alpine .apk vs Android .apk).
6
7use std::fs::File;
8use std::io::Read;
9use std::path::Path;
10
11/// Check if file starts with ZIP magic bytes (PK\x03\x04).
12///
13/// ZIP format is used by many file types including Android APK, JAR, InstallShield installers, etc.
14///
15/// # Returns
16/// `true` if the file starts with the ZIP signature, `false` otherwise or on IO error.
17pub fn is_zip(path: &Path) -> bool {
18    check_magic_bytes(path, &[0x50, 0x4B, 0x03, 0x04])
19}
20
21pub fn is_gzip(path: &Path) -> bool {
22    check_magic_bytes(path, &[0x1F, 0x8B])
23}
24
25/// Check if file starts with Squashfs magic bytes.
26///
27/// Squashfs filesystems can be either little-endian (hsqs) or big-endian (sqsh).
28/// This function checks for both variants.
29///
30/// # Returns
31/// `true` if the file starts with either Squashfs signature, `false` otherwise or on IO error.
32pub fn is_squashfs(path: &Path) -> bool {
33    // Little-endian: hsqs (0x68, 0x73, 0x71, 0x73)
34    // Big-endian: sqsh (0x73, 0x71, 0x73, 0x68)
35    check_magic_bytes(path, &[0x68, 0x73, 0x71, 0x73])
36        || check_magic_bytes(path, &[0x73, 0x71, 0x73, 0x68])
37}
38
39/// Check if file contains NSIS installer signature.
40///
41/// NSIS installers are Windows executables that contain a specific signature string.
42/// This function searches the first 8KB of the file for "Nullsoft.NSIS.exehead".
43///
44/// # Returns
45/// `true` if the NSIS signature is found within the first 8KB, `false` otherwise or on IO error.
46pub fn is_nsis_installer(path: &Path) -> bool {
47    const SEARCH_SIZE: usize = 8192; // 8KB
48    const NSIS_SIGNATURE: &[u8] = b"Nullsoft.NSIS.exehead";
49
50    let mut file = match File::open(path) {
51        Ok(f) => f,
52        Err(_) => return false,
53    };
54
55    let mut buffer = vec![0u8; SEARCH_SIZE];
56    let bytes_read = match file.read(&mut buffer) {
57        Ok(n) => n,
58        Err(_) => return false,
59    };
60
61    buffer.truncate(bytes_read);
62
63    // Search for NSIS signature in the buffer
64    buffer
65        .windows(NSIS_SIGNATURE.len())
66        .any(|window| window == NSIS_SIGNATURE)
67}
68
69/// Helper function to check if a file starts with specific magic bytes.
70///
71/// Reads only the minimum number of bytes needed for comparison.
72fn check_magic_bytes(path: &Path, magic: &[u8]) -> bool {
73    let mut file = match File::open(path) {
74        Ok(f) => f,
75        Err(_) => return false,
76    };
77
78    let mut buffer = vec![0u8; magic.len()];
79    match file.read_exact(&mut buffer) {
80        Ok(()) => buffer == magic,
81        Err(_) => false,
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88    use std::io::Write;
89    use tempfile::NamedTempFile;
90
91    #[test]
92    fn test_is_zip() {
93        // Create a file with ZIP magic bytes
94        let mut file = NamedTempFile::new().unwrap();
95        file.write_all(&[0x50, 0x4B, 0x03, 0x04, 0x00, 0x00])
96            .unwrap();
97        assert!(is_zip(file.path()));
98
99        // Create a file without ZIP magic bytes
100        let mut file2 = NamedTempFile::new().unwrap();
101        file2.write_all(&[0x1F, 0x8B, 0x08, 0x00]).unwrap();
102        assert!(!is_zip(file2.path()));
103
104        // Non-existent file
105        assert!(!is_zip(Path::new("/nonexistent/file.zip")));
106    }
107
108    #[test]
109    fn test_is_gzip() {
110        let mut file = NamedTempFile::new().unwrap();
111        file.write_all(&[0x1F, 0x8B, 0x08, 0x00]).unwrap();
112        assert!(is_gzip(file.path()));
113
114        let mut file2 = NamedTempFile::new().unwrap();
115        file2.write_all(&[0x50, 0x4B, 0x03, 0x04]).unwrap();
116        assert!(!is_gzip(file2.path()));
117    }
118
119    #[test]
120    fn test_is_squashfs_little_endian() {
121        // Create a file with Squashfs little-endian magic (hsqs)
122        let mut file = NamedTempFile::new().unwrap();
123        file.write_all(&[0x68, 0x73, 0x71, 0x73, 0x00, 0x00])
124            .unwrap();
125        assert!(is_squashfs(file.path()));
126    }
127
128    #[test]
129    fn test_is_squashfs_big_endian() {
130        // Create a file with Squashfs big-endian magic (sqsh)
131        let mut file = NamedTempFile::new().unwrap();
132        file.write_all(&[0x73, 0x71, 0x73, 0x68, 0x00, 0x00])
133            .unwrap();
134        assert!(is_squashfs(file.path()));
135    }
136
137    #[test]
138    fn test_is_squashfs_negative() {
139        // Create a file without Squashfs magic
140        let mut file = NamedTempFile::new().unwrap();
141        file.write_all(&[0x50, 0x4B, 0x03, 0x04]).unwrap();
142        assert!(!is_squashfs(file.path()));
143
144        // Non-existent file
145        assert!(!is_squashfs(Path::new("/nonexistent/file.squashfs")));
146    }
147
148    #[test]
149    fn test_is_nsis_installer() {
150        // Create a file with NSIS signature at the beginning
151        let mut file = NamedTempFile::new().unwrap();
152        file.write_all(b"MZ\x90\x00").unwrap(); // DOS header
153        file.write_all(b"Nullsoft.NSIS.exehead").unwrap();
154        file.write_all(&[0u8; 100]).unwrap();
155        assert!(is_nsis_installer(file.path()));
156
157        // Create a file with NSIS signature in the middle
158        let mut file2 = NamedTempFile::new().unwrap();
159        file2.write_all(&vec![0u8; 1000]).unwrap();
160        file2.write_all(b"Nullsoft.NSIS.exehead").unwrap();
161        assert!(is_nsis_installer(file2.path()));
162
163        // Create a file without NSIS signature
164        let mut file3 = NamedTempFile::new().unwrap();
165        file3.write_all(b"This is not an NSIS installer").unwrap();
166        assert!(!is_nsis_installer(file3.path()));
167
168        // Non-existent file
169        assert!(!is_nsis_installer(Path::new("/nonexistent/setup.exe")));
170    }
171
172    #[test]
173    fn test_is_nsis_installer_beyond_8kb() {
174        // Create a file with NSIS signature beyond 8KB - should NOT match
175        let mut file = NamedTempFile::new().unwrap();
176        file.write_all(&vec![0u8; 8500]).unwrap();
177        file.write_all(b"Nullsoft.NSIS.exehead").unwrap();
178        assert!(!is_nsis_installer(file.path()));
179    }
180
181    #[test]
182    fn test_check_magic_bytes_short_file() {
183        // File shorter than expected magic bytes
184        let mut file = NamedTempFile::new().unwrap();
185        file.write_all(&[0x50, 0x4B]).unwrap(); // Only 2 bytes
186        assert!(!check_magic_bytes(file.path(), &[0x50, 0x4B, 0x03, 0x04]));
187    }
188
189    #[test]
190    fn test_check_magic_bytes_empty_file() {
191        // Empty file
192        let file = NamedTempFile::new().unwrap();
193        assert!(!check_magic_bytes(file.path(), &[0x50, 0x4B]));
194    }
195}