use crate::globs::Globs;
use crate::magic::Magic;
use crate::types::{self, Types};
use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum How {
Inode,
StrongMagic,
Name,
Magic,
Fallback,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Found {
pub mime: String,
pub how: How,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Lookup {
pub globs: Globs,
pub magic: Magic,
pub types: Types,
}
pub const TEXT: &str = "text/plain";
pub const BYTES: &str = "application/octet-stream";
const STRONG: u32 = 80;
impl Lookup {
pub fn load_from(data_dirs: &[std::path::PathBuf]) -> Lookup {
Lookup {
globs: Globs::load_from(data_dirs),
magic: Magic::load_from(data_dirs),
types: Types::load_from(data_dirs),
}
}
pub fn of_file(&self, path: &Path, follow: bool) -> Found {
if let Some(inode) = types::inode_type(path, follow) {
return Found { mime: inode.to_string(), how: How::Inode };
}
match crate::magic::head(path).map(|data| self.of_data_and_name(&data, Some(path))) {
Some(found) => found,
None => match self.globs.type_of(path) {
Some(mime) => Found { mime: mime.to_string(), how: How::Name },
None => Found { mime: BYTES.to_string(), how: How::Fallback },
},
}
}
pub fn of_data_and_name(&self, data: &[u8], path: Option<&Path>) -> Found {
if let Some(found) = self.magic.of_data(data).filter(|m| m.priority >= STRONG) {
return Found { mime: found.mime, how: How::StrongMagic };
}
if let Some(mime) = path.and_then(|path| self.globs.type_of(path)) {
return Found { mime: mime.to_string(), how: How::Name };
}
if let Some(found) = self.magic.of_data(data) {
return Found { mime: found.mime, how: How::Magic };
}
Found { mime: fallback(data).to_string(), how: How::Fallback }
}
pub fn is_subclass_of(&self, mime: &str, parent: &str) -> bool {
self.types.is_subclass_of(mime, parent)
}
}
pub fn fallback(data: &[u8]) -> &'static str {
if data.is_empty() || looks_like_text(data) {
return TEXT;
}
BYTES
}
fn looks_like_text(data: &[u8]) -> bool {
data.iter().take(32).all(|byte| match byte {
b' ' | b'\t' | b'\n' | b'\r' | 0x0b | 0x0c => true,
0x00..=0x1f | 0x7f => false,
_ => true,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn lookup() -> Lookup {
let mut magic = Vec::new();
magic.extend(b"MIME-Magic\0\n");
magic.extend(b"[90:image/png]\n>0=");
magic.extend(4u16.to_be_bytes());
magic.extend(b"\x89PNG\n");
magic.extend(b"[50:model/stl]\n>0=");
magic.extend(6u16.to_be_bytes());
magic.extend(b"solid \n");
let mut types = Types::default();
types.add_subclasses("model/3mf application/zip\n");
Lookup {
globs: Globs::parse("50:model/3mf:*.3mf\n50:text/plain:*.txt\n50:image/jpeg:*.jpg\n"),
magic: Magic::parse(&magic),
types,
}
}
fn write(dir: &tempfile::TempDir, name: &str, contents: &[u8]) -> std::path::PathBuf {
let path = dir.path().join(name);
std::fs::write(&path, contents).unwrap();
path
}
#[test]
fn a_name_beats_a_weak_guess_about_the_bytes() {
let dir = tempfile::tempdir().unwrap();
let found = lookup().of_file(&write(&dir, "part.3mf", b"PK\x03\x04zip data"), true);
assert_eq!(found, Found { mime: "model/3mf".to_string(), how: How::Name });
}
#[test]
fn strong_magic_beats_a_name_that_lies() {
let dir = tempfile::tempdir().unwrap();
let found = lookup().of_file(&write(&dir, "notes.txt", b"\x89PNG\r\n\x1a\n"), true);
assert_eq!(found, Found { mime: "image/png".to_string(), how: How::StrongMagic });
}
#[test]
fn a_weak_rule_yields_to_the_name() {
let dir = tempfile::tempdir().unwrap();
let found = lookup().of_file(&write(&dir, "notes.txt", b"solid ground underfoot"), true);
assert_eq!(found.mime, "text/plain");
assert_eq!(found.how, How::Name);
}
#[test]
fn a_file_with_no_useful_name_falls_back_to_its_contents() {
let dir = tempfile::tempdir().unwrap();
let found = lookup().of_file(&write(&dir, "download", b"solid model\n"), true);
assert_eq!(found, Found { mime: "model/stl".to_string(), how: How::Magic });
}
#[test]
fn something_that_is_not_a_file_is_answered_first() {
let dir = tempfile::tempdir().unwrap();
let folder = dir.path().join("photos.jpg");
std::fs::create_dir(&folder).unwrap();
assert_eq!(
lookup().of_file(&folder, true),
Found { mime: "inode/directory".to_string(), how: How::Inode },
"a folder called photos.jpg is still a folder"
);
}
#[test]
fn nothing_recognised_is_text_or_bytes() {
let dir = tempfile::tempdir().unwrap();
assert_eq!(
lookup().of_file(&write(&dir, "nothing", b""), true).mime,
TEXT,
"an empty file is text, not a type nothing can open"
);
assert_eq!(lookup().of_file(&write(&dir, "readme", b"hello there\n"), true).mime, TEXT);
assert_eq!(lookup().of_file(&write(&dir, "blob", b"\x00\x01\x02binary"), true).mime, BYTES);
}
#[test]
fn an_empty_file_still_has_the_type_its_name_gives() {
let dir = tempfile::tempdir().unwrap();
let found = lookup().of_file(&write(&dir, "empty.jpg", b""), true);
assert_eq!(found, Found { mime: "image/jpeg".to_string(), how: How::Name });
}
#[test]
fn text_with_accents_is_still_text() {
assert!(looks_like_text("café — naïve\n".as_bytes()));
assert!(looks_like_text(&[0xe9, 0xe8, b'\n']), "latin-1, not utf-8, still text");
assert!(!looks_like_text(b"\x00\x01"));
assert!(!looks_like_text(b"\x1b[0;31mred\x1b[0m"), "escape means binary, not whitespace");
}
#[test]
fn a_stray_control_byte_later_on_does_not_make_a_file_binary() {
let mut data = b"a text file, for the first thirty-two bytes at least".to_vec();
data.push(0x00);
assert!(looks_like_text(&data));
}
#[test]
fn an_unreadable_file_is_typed_by_its_name() {
let dir = tempfile::tempdir().unwrap();
let path = write(&dir, "secret.3mf", b"PK\x03\x04");
let mut perms = std::fs::metadata(&path).unwrap().permissions();
std::os::unix::fs::PermissionsExt::set_mode(&mut perms, 0o000);
std::fs::set_permissions(&path, perms).unwrap();
if std::fs::read(&path).is_ok() {
eprintln!("HYPRFORGE-SKIP: this user can read a mode 000 file (root?)");
return;
}
assert_eq!(lookup().of_file(&path, true).mime, "model/3mf");
}
#[test]
fn data_without_a_name_is_answered_too() {
let found = lookup().of_data_and_name(b"\x89PNG\r\n", None);
assert_eq!(found.mime, "image/png");
assert_eq!(lookup().of_data_and_name(b"plain words", None).mime, TEXT);
}
}