Skip to main content

provenant/utils/
magic.rs

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