Skip to main content

libmagic_rs/parser/
format.rs

1// Copyright (c) 2025-2026 the libmagic-rs contributors
2// SPDX-License-Identifier: Apache-2.0
3
4//! Format detection for magic files.
5//!
6//! Detects whether a path points to a text magic file, a directory of magic files,
7//! or a binary compiled magic file (.mgc format).
8
9use crate::error::ParseError;
10use std::io::Read;
11use std::path::Path;
12
13const MGC_MAGIC: u32 = 0xF11E_041C;
14
15pub(super) fn has_binary_magic_header(bytes: &[u8]) -> bool {
16    bytes.starts_with(&MGC_MAGIC.to_le_bytes()) || bytes.starts_with(&MGC_MAGIC.to_be_bytes())
17}
18
19/// Represents the format of a magic file or directory
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum MagicFileFormat {
22    /// Text-based magic file (human-readable)
23    Text,
24    /// Directory containing multiple magic files (Magdir pattern)
25    Directory,
26    /// Binary compiled magic file (.mgc format)
27    Binary,
28}
29
30/// Detect the format of a magic file or directory
31///
32/// This function examines the filesystem metadata and file contents to determine
33/// whether the path points to a text magic file, a directory, or a binary .mgc file.
34///
35/// # Detection Logic
36///
37/// 1. Check if path is a directory -> `MagicFileFormat::Directory`
38/// 2. Read the first 4 bytes and check for the binary magic number
39///    `0xF11E041C` in either byte order -> `MagicFileFormat::Binary`
40/// 3. Otherwise -> `MagicFileFormat::Text`
41///
42/// # Arguments
43///
44/// * `path` - Path to the magic file or directory to detect
45///
46/// # Errors
47///
48/// Returns `ParseError::IoError` if the path doesn't exist or cannot be read.
49///
50/// # Notes
51///
52/// This function only detects the format and returns it. It does not validate whether
53/// the format is supported by the parser. Higher-level code should check the returned
54/// format and decide how to handle unsupported formats (e.g., binary .mgc files).
55///
56/// # Examples
57///
58/// ```rust,no_run
59/// use libmagic_rs::parser::detect_format;
60/// use std::path::Path;
61///
62/// let format = detect_format(Path::new("/usr/share/file/magic"))?;
63/// # Ok::<(), libmagic_rs::ParseError>(())
64/// ```
65pub fn detect_format(path: &Path) -> Result<MagicFileFormat, ParseError> {
66    // Check if path exists and is accessible
67    let metadata = std::fs::metadata(path)?;
68
69    // Check if it's a directory
70    if metadata.is_dir() {
71        return Ok(MagicFileFormat::Directory);
72    }
73
74    // Read first 4 bytes to check for binary magic number
75    let mut file = std::fs::File::open(path)?;
76
77    let mut magic_bytes = [0u8; 4];
78
79    match file.read_exact(&mut magic_bytes) {
80        Ok(()) => {
81            if has_binary_magic_header(&magic_bytes) {
82                return Ok(MagicFileFormat::Binary);
83            }
84            // Not a binary magic file, assume text
85            Ok(MagicFileFormat::Text)
86        }
87        Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
88            // File is too small to be a binary magic file, assume text
89            Ok(MagicFileFormat::Text)
90        }
91        Err(e) => Err(ParseError::IoError(e)),
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98    use std::fs;
99    use std::io::Write;
100
101    #[test]
102    fn test_detect_format_text_file() {
103        let temp_dir = std::env::temp_dir();
104        let text_file = temp_dir.join("test_text_magic.txt");
105        fs::write(&text_file, "# Magic file\n0 string test Test").unwrap();
106
107        let format = detect_format(&text_file).unwrap();
108        assert_eq!(format, MagicFileFormat::Text);
109
110        fs::remove_file(&text_file).unwrap();
111    }
112
113    #[test]
114    fn test_detect_format_directory() {
115        let temp_dir = std::env::temp_dir().join("test_magic_dir");
116        fs::create_dir_all(&temp_dir).unwrap();
117
118        let format = detect_format(&temp_dir).unwrap();
119        assert_eq!(format, MagicFileFormat::Directory);
120
121        fs::remove_dir_all(&temp_dir).unwrap();
122    }
123
124    #[test]
125    fn test_detect_format_binary_mgc() {
126        let temp_dir = std::env::temp_dir();
127        let headers = [
128            ("little-endian", MGC_MAGIC.to_le_bytes()),
129            ("big-endian", MGC_MAGIC.to_be_bytes()),
130        ];
131
132        for (byte_order, header) in headers {
133            let binary_file = temp_dir.join(format!("test_binary_{byte_order}.mgc"));
134            let mut file = fs::File::create(&binary_file).unwrap();
135            file.write_all(&header).unwrap();
136            file.write_all(b"additional binary data").unwrap();
137
138            let result = detect_format(&binary_file);
139            assert!(result.is_ok(), "failed to detect {byte_order} header");
140
141            match result.unwrap() {
142                MagicFileFormat::Binary => {}
143                other => panic!("Expected Binary format for {byte_order}, got {other:?}"),
144            }
145
146            fs::remove_file(&binary_file).unwrap();
147        }
148    }
149
150    #[test]
151    fn test_detect_format_nonexistent_path() {
152        let nonexistent = std::env::temp_dir().join("nonexistent_magic_file.txt");
153
154        let result = detect_format(&nonexistent);
155        assert!(result.is_err());
156
157        match result.unwrap_err() {
158            ParseError::IoError(e) => {
159                assert_eq!(e.kind(), std::io::ErrorKind::NotFound);
160            }
161            other => panic!("Expected IoError, got: {other:?}"),
162        }
163    }
164
165    #[test]
166    fn test_detect_format_empty_file() {
167        let temp_dir = std::env::temp_dir();
168        let empty_file = temp_dir.join("test_empty_magic.txt");
169        fs::write(&empty_file, "").unwrap();
170
171        // Empty files should be detected as text (too small for binary magic)
172        let format = detect_format(&empty_file).unwrap();
173        assert_eq!(format, MagicFileFormat::Text);
174
175        fs::remove_file(&empty_file).unwrap();
176    }
177
178    #[test]
179    fn test_detect_format_small_file() {
180        let temp_dir = std::env::temp_dir();
181        let small_file = temp_dir.join("test_small_magic.txt");
182        fs::write(&small_file, "ab").unwrap(); // Only 2 bytes
183
184        // Small files should be detected as text
185        let format = detect_format(&small_file).unwrap();
186        assert_eq!(format, MagicFileFormat::Text);
187
188        fs::remove_file(&small_file).unwrap();
189    }
190
191    #[test]
192    fn test_detect_format_text_with_binary_content() {
193        let temp_dir = std::env::temp_dir();
194        let binary_text_file = temp_dir.join("test_binary_text.txt");
195
196        // Write binary data that's NOT the magic number
197        let mut file = fs::File::create(&binary_text_file).unwrap();
198        file.write_all(&[0xFF, 0xFE, 0xFD, 0xFC]).unwrap();
199        file.write_all(b"some text").unwrap();
200
201        // Should be detected as text (wrong magic number)
202        let format = detect_format(&binary_text_file).unwrap();
203        assert_eq!(format, MagicFileFormat::Text);
204
205        fs::remove_file(&binary_text_file).unwrap();
206    }
207
208    #[test]
209    fn test_magic_file_format_enum_equality() {
210        assert_eq!(MagicFileFormat::Text, MagicFileFormat::Text);
211        assert_eq!(MagicFileFormat::Directory, MagicFileFormat::Directory);
212        assert_eq!(MagicFileFormat::Binary, MagicFileFormat::Binary);
213
214        assert_ne!(MagicFileFormat::Text, MagicFileFormat::Directory);
215        assert_ne!(MagicFileFormat::Text, MagicFileFormat::Binary);
216        assert_ne!(MagicFileFormat::Directory, MagicFileFormat::Binary);
217    }
218
219    #[test]
220    fn test_magic_file_format_debug() {
221        let text_format = MagicFileFormat::Text;
222        let debug_str = format!("{text_format:?}");
223        assert!(debug_str.contains("Text"));
224    }
225
226    #[test]
227    fn test_magic_file_format_clone() {
228        let original = MagicFileFormat::Directory;
229        let cloned = original;
230        assert_eq!(original, cloned);
231    }
232
233    #[test]
234    fn test_magic_file_format_copy() {
235        let original = MagicFileFormat::Binary;
236        let copied = original; // Copy trait allows this
237        assert_eq!(original, copied);
238    }
239}