Skip to main content

qubit_mime/detector/
mime_detector_backend.rs

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