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