Skip to main content

qubit_mime/detector/
file_based_mime_detector.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//! File-backed MIME detector helpers.
11
12use std::fmt::Debug;
13use std::fs;
14use std::path::{
15    Path,
16    PathBuf,
17};
18use std::sync::atomic::{
19    AtomicU64,
20    Ordering,
21};
22
23use crate::{
24    MimeDetectorCore,
25    MimeResult,
26    StreamBasedMimeDetector,
27};
28
29/// Core implementation contract for detectors that only inspect local files.
30pub trait FileBasedMimeDetector: 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 stage for byte and reader inputs.
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 one local file.
53    ///
54    /// # Parameters
55    /// - `file`: Local file path readable by the backend.
56    ///
57    /// # Returns
58    /// Candidate MIME type names ordered by backend confidence.
59    ///
60    /// # Errors
61    /// Returns an error when local-file inspection fails.
62    fn guess_from_local_file(&self, file: &Path) -> MimeResult<Vec<String>>;
63}
64
65impl<T> StreamBasedMimeDetector for T
66where
67    T: FileBasedMimeDetector,
68{
69    /// Gets the shared detector core.
70    fn core(&self) -> &MimeDetectorCore {
71        FileBasedMimeDetector::core(self)
72    }
73
74    /// Gets the maximum content prefix length needed by this detector.
75    fn max_test_bytes(&self) -> usize {
76        FileBasedMimeDetector::max_test_bytes(self)
77    }
78
79    /// Guesses MIME type names from filename rules.
80    fn guess_from_filename(&self, filename: &str) -> Vec<String> {
81        FileBasedMimeDetector::guess_from_filename(self, filename)
82    }
83
84    /// Stages content to a temporary local file before inspection.
85    fn guess_from_content_bytes(&self, content: &[u8]) -> MimeResult<Vec<String>> {
86        with_temp_file(content, |path| {
87            FileBasedMimeDetector::guess_from_local_file(self, path)
88        })
89    }
90
91    /// Delegates local-file inspection to the file-based hook.
92    fn guess_from_file_stream(&self, file: &Path) -> MimeResult<(Vec<String>, Vec<u8>)> {
93        Ok((
94            FileBasedMimeDetector::guess_from_local_file(self, file)?,
95            Vec::new(),
96        ))
97    }
98}
99
100/// Stages content into a temporary file for file-based detectors.
101///
102/// # Parameters
103/// - `content`: Content bytes to stage.
104/// - `detect`: Callback receiving the temporary path.
105///
106/// # Returns
107/// The callback result.
108///
109/// # Errors
110/// Returns [`MimeError::Io`](crate::MimeError::Io) when the temporary file cannot be written.
111pub(crate) fn with_temp_file<T>(
112    content: &[u8],
113    detect: impl FnOnce(&PathBuf) -> MimeResult<T>,
114) -> MimeResult<T> {
115    let path = unique_temp_path("MimeDetectorTemp", ".tmp");
116    fs::write(&path, content)?;
117    let result = detect(&path);
118    let _ = fs::remove_file(&path);
119    result
120}
121
122/// Builds a best-effort unique temporary path.
123///
124/// # Parameters
125/// - `prefix`: Filename prefix.
126/// - `suffix`: Filename suffix.
127///
128/// # Returns
129/// Path under the OS temporary directory.
130fn unique_temp_path(prefix: &str, suffix: &str) -> PathBuf {
131    static COUNTER: AtomicU64 = AtomicU64::new(0);
132    let counter = COUNTER.fetch_add(1, Ordering::Relaxed);
133    std::env::temp_dir().join(format!("{prefix}-{}-{counter}{suffix}", std::process::id()))
134}