Skip to main content

qubit_mime/detector/
mime_detector_backend.rs

1/*******************************************************************************
2 *
3 *    Copyright (c) 2026 Haixing Hu.
4 *
5 *    SPDX-License-Identifier: Apache-2.0
6 *
7 *    Licensed under the Apache License, Version 2.0.
8 *
9 ******************************************************************************/
10//! Backend contract for MIME detector implementations.
11
12use std::fmt::Debug;
13use std::fs::File;
14use std::path::Path;
15
16use qubit_io::ReadSeek;
17
18use crate::{
19    DetectionSource,
20    MimeDetectionPolicy,
21    MimeDetector,
22    MimeDetectorCore,
23    MimeResult,
24    StreamBasedMimeDetector,
25};
26
27use super::stream_based_mime_detector::read_prefix;
28
29/// Core implementation contract for MIME detectors.
30pub trait MimeDetectorBackend: Debug + Send + Sync {
31    /// Gets the shared detector core.
32    ///
33    /// # Returns
34    /// Shared detector configuration and merge/refinement behavior.
35    fn core(&self) -> &MimeDetectorCore;
36
37    /// Gets the maximum number of bytes needed for content inspection.
38    ///
39    /// # Returns
40    /// Content prefix length to read from files and readers.
41    fn max_test_bytes(&self) -> usize;
42
43    /// Guesses MIME type names from a filename.
44    ///
45    /// # Parameters
46    /// - `filename`: File path or basename.
47    ///
48    /// # Returns
49    /// Candidate MIME type names ordered by backend confidence.
50    fn guess_from_filename(&self, filename: &str) -> Vec<String>;
51
52    /// Guesses MIME type names from content bytes.
53    ///
54    /// # Parameters
55    /// - `content`: Content bytes.
56    ///
57    /// # Returns
58    /// Candidate MIME type names ordered by backend confidence.
59    ///
60    /// # Errors
61    /// Returns an error when a backend cannot inspect the supplied content.
62    fn guess_from_content(&self, content: &[u8]) -> MimeResult<Vec<String>>;
63
64    /// Guesses MIME type names from a seekable reader.
65    ///
66    /// # Parameters
67    /// - `reader`: Reader to inspect. The original position is restored.
68    ///
69    /// # Returns
70    /// Candidate MIME type names and the content prefix used for refinement.
71    ///
72    /// # Errors
73    /// Returns an error when reading, seeking, or backend inspection fails.
74    fn guess_from_reader(&self, reader: &mut dyn ReadSeek) -> MimeResult<(Vec<String>, Vec<u8>)> {
75        let content = read_prefix(reader, self.max_test_bytes())?;
76        let candidates = self.guess_from_content(&content)?;
77        Ok((candidates, content))
78    }
79
80    /// Guesses MIME type names from a local file.
81    ///
82    /// # Parameters
83    /// - `file`: Local file path.
84    ///
85    /// # Returns
86    /// Candidate MIME type names and the content prefix used for refinement.
87    ///
88    /// # Errors
89    /// Returns an error when opening, reading, seeking, or backend inspection fails.
90    fn guess_from_file(&self, file: &Path) -> MimeResult<(Vec<String>, Vec<u8>)> {
91        let mut reader = File::open(file)?;
92        self.guess_from_reader(&mut reader)
93    }
94}
95
96impl<T> MimeDetector for T
97where
98    T: MimeDetectorBackend,
99{
100    /// Detects a MIME type from filename candidates.
101    fn detect_by_filename(&self, filename: &str) -> Option<String> {
102        self.guess_from_filename(filename).first().map(|mime_type| {
103            self.core()
104                .refine_detected_mime_type(mime_type, Some(filename), DetectionSource::None)
105        })
106    }
107
108    /// Detects a MIME type from content candidates.
109    fn detect_by_content(&self, content: &[u8]) -> Option<String> {
110        self.guess_from_content(content)
111            .ok()?
112            .first()
113            .map(|mime_type| {
114                self.core().refine_detected_mime_type(
115                    mime_type,
116                    None,
117                    DetectionSource::Content(content),
118                )
119            })
120    }
121
122    /// Detects a MIME type from content bytes and an optional filename.
123    fn detect(
124        &self,
125        content: &[u8],
126        filename: Option<&str>,
127        policy: MimeDetectionPolicy,
128    ) -> Option<String> {
129        let from_filename = filename
130            .map(|filename| self.guess_from_filename(filename))
131            .unwrap_or_default();
132        let from_content =
133            if from_filename.len() == 1 && policy == MimeDetectionPolicy::PreferFilename {
134                Vec::new()
135            } else {
136                self.guess_from_content(content).unwrap_or_default()
137            };
138        self.core().select_result(
139            &from_filename,
140            &from_content,
141            filename,
142            policy,
143            DetectionSource::Content(content),
144        )
145    }
146
147    /// Detects a MIME type from a seekable reader.
148    fn detect_reader(
149        &self,
150        reader: &mut dyn ReadSeek,
151        filename: Option<&str>,
152        policy: MimeDetectionPolicy,
153    ) -> MimeResult<Option<String>> {
154        let from_filename = filename
155            .map(|filename| self.guess_from_filename(filename))
156            .unwrap_or_default();
157        let (from_content, content) =
158            if from_filename.len() == 1 && policy == MimeDetectionPolicy::PreferFilename {
159                (Vec::new(), Vec::new())
160            } else {
161                self.guess_from_reader(reader)?
162            };
163        Ok(self.core().select_result(
164            &from_filename,
165            &from_content,
166            filename,
167            policy,
168            DetectionSource::Content(&content),
169        ))
170    }
171
172    /// Detects a MIME type from a local file.
173    fn detect_file(&self, file: &Path, policy: MimeDetectionPolicy) -> MimeResult<Option<String>> {
174        let filename = file.to_string_lossy();
175        let from_filename = self.guess_from_filename(&filename);
176        let (from_content, _content) =
177            if from_filename.len() == 1 && policy == MimeDetectionPolicy::PreferFilename {
178                (Vec::new(), Vec::new())
179            } else {
180                self.guess_from_file(file)?
181            };
182        Ok(self.core().select_result(
183            &from_filename,
184            &from_content,
185            Some(&filename),
186            policy,
187            DetectionSource::Path(file),
188        ))
189    }
190}
191
192impl<T> MimeDetectorBackend for T
193where
194    T: StreamBasedMimeDetector,
195{
196    /// Gets the shared detector core.
197    fn core(&self) -> &MimeDetectorCore {
198        StreamBasedMimeDetector::core(self)
199    }
200
201    /// Gets the maximum content prefix length needed by this detector.
202    fn max_test_bytes(&self) -> usize {
203        StreamBasedMimeDetector::max_test_bytes(self)
204    }
205
206    /// Guesses MIME type names from filename rules.
207    fn guess_from_filename(&self, filename: &str) -> Vec<String> {
208        StreamBasedMimeDetector::guess_from_filename(self, filename)
209    }
210
211    /// Guesses MIME type names from content bytes.
212    fn guess_from_content(&self, content: &[u8]) -> MimeResult<Vec<String>> {
213        StreamBasedMimeDetector::guess_from_content_bytes(self, content)
214    }
215
216    /// Delegates reader inspection to the stream-based hook.
217    fn guess_from_reader(&self, reader: &mut dyn ReadSeek) -> MimeResult<(Vec<String>, Vec<u8>)> {
218        StreamBasedMimeDetector::guess_from_reader_stream(self, reader)
219    }
220
221    /// Delegates local-file inspection to the stream-based hook.
222    fn guess_from_file(&self, file: &Path) -> MimeResult<(Vec<String>, Vec<u8>)> {
223        StreamBasedMimeDetector::guess_from_file_stream(self, file)
224    }
225}