Skip to main content

qubit_mime/detector/
stream_based_mime_detector.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//! Helpers for stream-backed MIME detectors.
9
10use std::fmt::Debug;
11use std::path::Path;
12
13use qubit_io::{ReadSeek, ReadSeekExt};
14use qubit_local_files::{FileReadOptions, LocalFiles};
15
16use crate::{MimeDetectorCore, MimeError, MimeResult};
17
18/// Core implementation contract for detectors that can inspect content bytes.
19pub trait StreamBasedMimeDetector: Debug + Send + Sync {
20    /// Gets the shared detector core.
21    ///
22    /// # Returns
23    /// Shared detector configuration and merge/refinement behavior.
24    fn core(&self) -> &MimeDetectorCore;
25
26    /// Gets the maximum number of bytes needed for content inspection.
27    ///
28    /// # Returns
29    /// Content prefix length to read from files and readers.
30    fn max_test_bytes(&self) -> usize;
31
32    /// Guesses MIME type names from a filename.
33    ///
34    /// # Parameters
35    /// - `filename`: File path or basename.
36    ///
37    /// # Returns
38    /// Candidate MIME type names ordered by backend confidence.
39    fn guess_from_filename(&self, filename: &str) -> Vec<String>;
40
41    /// Guesses MIME type names from content bytes.
42    ///
43    /// # Parameters
44    /// - `content`: Content bytes.
45    ///
46    /// # Returns
47    /// Candidate MIME type names ordered by backend confidence.
48    ///
49    /// # Errors
50    /// Returns an error when a backend cannot inspect the supplied content.
51    fn guess_from_content_bytes(&self, content: &[u8]) -> MimeResult<Vec<String>>;
52
53    /// Guesses MIME type names from a seekable reader.
54    ///
55    /// # Parameters
56    /// - `reader`: Reader to inspect. The original position is restored.
57    ///
58    /// # Returns
59    /// Candidate MIME type names and the content prefix used for refinement.
60    ///
61    /// # Errors
62    /// Returns an error when reading, seeking, or backend inspection fails.
63    fn guess_from_reader_stream(
64        &self,
65        reader: &mut dyn ReadSeek,
66    ) -> MimeResult<(Vec<String>, Vec<u8>)> {
67        let content = read_prefix(reader, self.max_test_bytes(), self.core().max_buffer_size())?;
68        let candidates = self.guess_from_content_bytes(&content)?;
69        Ok((candidates, content))
70    }
71
72    /// Guesses MIME type names from a local file.
73    ///
74    /// # Parameters
75    /// - `file`: Local file path.
76    ///
77    /// # Returns
78    /// Candidate MIME type names and the content prefix used for refinement.
79    ///
80    /// # Errors
81    /// Returns an error when opening, reading, seeking, or backend inspection
82    /// fails.
83    fn guess_from_file_stream(&self, file: &Path) -> MimeResult<(Vec<String>, Vec<u8>)> {
84        let mut reader = LocalFiles::open_reader(file, FileReadOptions::buffered())?;
85        self.guess_from_reader_stream(&mut reader)
86    }
87}
88
89/// Reads a prefix from a stream and restores the original position.
90///
91/// # Parameters
92/// - `reader`: Stream to inspect.
93/// - `max_bytes`: Maximum number of bytes to read.
94/// - `max_buffer_size`: Maximum allowed byte buffer allocation.
95///
96/// # Returns
97/// Bytes read from the stream.
98///
99/// # Errors
100/// Returns [`MimeError::BufferLimitExceeded`](crate::MimeError::BufferLimitExceeded) when
101/// `max_bytes` exceeds `max_buffer_size`. Returns
102/// [`MimeError::Io`](crate::MimeError::Io) when reading or seeking fails.
103pub(crate) fn read_prefix(
104    reader: &mut dyn ReadSeek,
105    max_bytes: usize,
106    max_buffer_size: usize,
107) -> MimeResult<Vec<u8>> {
108    if max_bytes > max_buffer_size {
109        return Err(MimeError::BufferLimitExceeded {
110            requested: max_bytes,
111            limit: max_buffer_size,
112        });
113    }
114    let mut buffer = vec![0; max_bytes];
115    let bytes_read = reader.peek_exact_or_eof(&mut buffer)?;
116    buffer.truncate(bytes_read);
117    Ok(buffer)
118}