//! Functions for getting the mime type and extension of a file. use std::fs::File; use std::io; use std::io::{Read, Seek, SeekFrom}; use std::path::Path; use std::str::FromStr; use cached::cached; use mime::Mime; use crate::mime_db::MimeDb; use crate::string_type::String; /// The number of bytes to read initially. /// /// Rather than reading the entire file all at once into a [`BUF_SIZE`] buffer, it tends to be faster to read a small /// chunk of the file and trying to identify that, proceeding with the larger buffer if that fails. Many file formats /// can be identified with the first few dozen bytes, so the "happy path" will likely be taken in the majority of cases. pub const INITIAL_BUF_SIZE: usize = 128; /// The number of bytes to read if the file couldn't be identified from its first [`INITIAL_BUF_SIZE`] bytes. pub const BUF_SIZE: usize = 8192; /// Tries to identify the mimetype of a file from a given path. pub fn mime_type(db: &T, path: &Path) -> io::Result> { let mut buffer = [0; INITIAL_BUF_SIZE]; let mut file = File::open(path)?; // read a small amount to start with file.read(&mut buffer)?; let r = db.get_type(&buffer).filter(|mime| // some mime types should be investigated further, reading up to BUF_SIZE even if they've been determined already // one such type is XML - there's many more specific types that can be determined by reading further (such as SVG) mime != &mime::TEXT_XML // another is ZIP - many file formats (DOCX, ODT, JAR...) are just ZIP files with particular data structures. // determining that a file is in one of the MS office formats in particular requires looking quite far into the // file. && mime != &Mime::from_str("application/zip").unwrap() // doc/ppt/xls files are a subset of what's known as an "OLE2 compound document storage", at least according to // shared-mime-info. if a pre-OOXML era MS office file is scanned and identified as x-ole-storage, reading further // will allow it to be detected correctly as the appropriate filetype. && mime != &Mime::from_str("application/x-ole-storage").unwrap()); if r.is_some() { return Ok(r); } // attempt to read up to the BUF_SIZE bytes of the file. // we've already read the first 128 bytes into a buffer, but i can't see an obvious way to reuse them in a way that's // faster than simply moving the seek position back to the start of the file and re-reading the whole BUF_SIZE bytes. // for example, reading (BUF_SIZE - INITIAL_BUF_SIZE) bytes, then concatenating the original INITIAL_BUF_SIZE buffer // with this new one would presumably be faster - but it's not. i think it's more expensive to create the iterator, // collect the contents, etc. i'll have to look into this more. i don't at all doubt that there IS a way to do this // efficiently, and i can think of a way in principle, but i'm not sure how to express it in a way that is both // idiomatic/safe and fast. let mut buffer = [0; BUF_SIZE]; file.seek(SeekFrom::Start(0))?; file.read(&mut buffer)?; Ok(db.get_type(&buffer)) } cached! { MIMEXT; fn mime_extension_lookup(essence: String) -> Option> = { // Returns a list of known extensions for this mime type, if any. // This function uses the [Mime]'s "essence" rather than the [Mime] itself - mime_guess::get_mime_extensions ignores // the type suffix, treating "image/svg+xml" as "image/svg", and thus fails to find any extensions. Passing the // essence_str (which includes the suffix) fixes this. // ↑ this is supposed to be a doc comment, but the cached! macro doesn't support that... i would switch to the // proc_macro version of cached, but it has a huge number of deps :c let essence = essence.as_str(); let mut exts = mime_guess::get_mime_extensions_str(essence); if exts.is_none() { // no matches :c // mime_guess' database isn't exactly perfect... there are a lot of times where the db will return "some/x-thing" // but mime_guess only understands "some/thing", or vice-versa. // so, if there appear to be no extensions, try replacing "some/x-thing" with "some/thing", or "some/thing" with // "some/x-thing". if essence.contains("/x-") { // replace e.g. "application/x-gzip" with "application/gzip" exts = mime_guess::get_mime_extensions_str(&essence.replace("/x-", "/")); } else { // replace e.g. "video/mp2t" with "video/x-mp2t" exts = mime_guess::get_mime_extensions_str(&essence.replace("/", "/x-")); } } match exts { Some(exts) => { let possible_exts: Vec = exts.iter().map(|e| String::from(*e)).collect(); Some(if essence == mime::IMAGE_JPEG.essence_str() { // possible_exts starts with "jpe", because it's alphabetically before "jpeg" and "jpg". however, jpg/jpeg are // far more common than jpe, so it makes sense to suggest one of those rather than jpe. to do this, we can // add "jpg" to the start of the possible_exts list, ensuring that it will be the extension suggested by fif. [vec![String::from("jpg")], possible_exts].concat() } else if essence == mime::TEXT_XML.essence_str() || essence == "application/xml" { // a somewhat similar case arises with XML files - the first suggested extension is "asa", when it should // (in my opinion) be "xml". // there's also another problem: SVG files can easily be misidentified as XML files, because they usually // *are* valid XML - the more whitespace and comments an SVG file begins with, the more bytes must be read // before it's possible to determine that it's an SVG rather than an XML file. to "fix" this, we can add "svg" // as a valid extension for XML files, ensuring that SVG files misidentified as XML will still be considered // to have valid extensions. // TODO: if a file is detected as application/xml, but it has an extension like "xht" which corresponds to // "application/xhtml+xml", let it through - in other words, if it's identified as application/xml, but its // extension is classed as application/*+xml, consider it OK [vec![String::from("xml"), String::from("svg")], possible_exts].concat() } else if essence == "application/msword" { // classic office files considered harmful vec![String::from("doc"), String::from("xls"), String::from("ppt")] } else if essence == "application/zip" { // neither xdg-mime nor infer seem to be able to detect office XML files properly... [vec![String::from("zip"), String::from("docx"), String::from("xlsx"), String::from("pptx")], possible_exts].concat() } else if essence == "application/x-ms-dos-executable" { // both .dll and .exe files are given the same mime type... but you definitely don't want to rename one to the // other! [vec![String::from("dll"), String::from("exe")], possible_exts].concat() } else { possible_exts }) }, None => None } } }