shared_mime/mimedb/
query.rs

1//! MIME DB querying implementation.
2use std::ffi::OsStr;
3use std::fs::Metadata;
4#[cfg(unix)]
5use std::os::unix::fs::FileTypeExt;
6use std::os::unix::fs::MetadataExt;
7
8use log::*;
9
10use crate::{query::FileQuery, Answer, QueryError};
11
12use super::MimeDB;
13
14enum MetaAnswer {
15    Inode(&'static str),
16    File(u64),
17}
18
19fn type_for_meta(meta: &Metadata) -> MetaAnswer {
20    let ft = meta.file_type();
21    if ft.is_dir() {
22        return MetaAnswer::Inode("inode/directory");
23    } else if ft.is_symlink() {
24        return MetaAnswer::Inode("inode/symlink");
25    }
26
27    #[cfg(unix)]
28    if ft.is_block_device() {
29        return MetaAnswer::Inode("inode/blockdevice");
30    } else if ft.is_char_device() {
31        return MetaAnswer::Inode("inode/chardevice");
32    } else if ft.is_fifo() {
33        return MetaAnswer::Inode("inode/fifo");
34    } else if ft.is_socket() {
35        return MetaAnswer::Inode("inode/socket");
36    }
37
38    MetaAnswer::File(meta.size())
39}
40
41impl MimeDB {
42    /// Query the MIME database.
43    pub fn query(&self, query: &FileQuery<'_>) -> Result<Answer<'_>, QueryError> {
44        let dbg_name = if let Some(name) = query.filename {
45            name.to_string_lossy()
46        } else {
47            "⟨unnamed⟩".into()
48        };
49        // first step: check for special files, if we have metadata
50        let size = if let Some(meta) = &query.metadata {
51            debug!("{}: looking up with metadata", dbg_name);
52            match type_for_meta(meta) {
53                // if we have a special title, all done!
54                MetaAnswer::Inode(tstr) => return Ok(Answer::definite(tstr)),
55                MetaAnswer::File(size) => Some(size),
56            }
57        } else {
58            None
59        };
60
61        // next step: look up based on filename
62        let mut ans = Answer::unknown();
63        if let Some(name) = query.filename {
64            debug!("{}: looking up with file name", dbg_name);
65            ans = self.query_filename(name);
66        }
67
68        if ans.is_unknown() && size == Some(0) {
69            ans = Answer::definite("application/x-zerosize")
70        }
71
72        // TODO: detect text files
73        if ans.is_unknown() {
74            ans = Answer::definite("application/octet-stream")
75        }
76
77        Ok(ans)
78    }
79
80    /// Use metadata to detect file types.
81    ///
82    /// This function can only detect the `inode/` types and `application/octet-stream`.
83    pub fn query_meta(&self, meta: &Metadata) -> Answer<'_> {
84        match type_for_meta(meta) {
85            MetaAnswer::Inode(mt) => Answer::definite(mt),
86            MetaAnswer::File(_) => Answer::definite("application/octet-stream"),
87        }
88    }
89
90    /// Look up MIME type information based only on a filename.
91    pub fn query_filename<S: AsRef<OsStr>>(&self, name: S) -> Answer<'_> {
92        let name = name.as_ref();
93        let display = name.to_string_lossy();
94        debug!("looking up filename {}", display);
95        let mut sw = None;
96        let mut matches = Vec::new();
97        let pbs = name.as_encoded_bytes();
98        for glob in self.globs.iter() {
99            if let Some((s, w)) = sw {
100                if s > glob.sequence || w > glob.weight {
101                    // done searching
102                    break;
103                }
104            }
105            if glob.matcher.matches(pbs) {
106                sw = Some((glob.sequence, glob.weight));
107                matches.push(glob.mimetype.as_str());
108            }
109        }
110        let ambiguous = self.coalesce_fn_matches(&display, &mut matches);
111        Answer::new(matches, ambiguous)
112    }
113
114    fn coalesce_fn_matches(&self, name: &str, matches: &mut Vec<&str>) -> bool {
115        let mut ambiguous = matches.len() > 1;
116        // TODO: prefer matching literals
117        // TODO: disambiguate by match length
118        if ambiguous {
119            // this is our own addition to the match logic
120            // if we have multiple matches, but one is the supertype of the others, use it
121            debug!("{}: {} matches, sorting", name, matches.len());
122            // put supertype first
123            matches.sort_by(|a, b| self.compare_types(a, b).reverse());
124            let root = matches[0];
125            ambiguous = !matches[1..].iter().all(|t| self.is_subtype(t, root));
126            if ambiguous {
127                debug!("{}: ambiguous match", name)
128            } else {
129                debug!("{}: best match {}", name, root)
130            }
131        }
132        ambiguous
133    }
134}