use std::path::PathBuf;
use std::sync::OnceLock;
struct Glob
{
weight: u32,
mime: String,
pattern: String,
case_sensitive: bool,
}
pub fn mime_type( name: &str ) -> Option<&'static str>
{
static GLOBS: OnceLock<Vec<Glob>> = OnceLock::new();
best( GLOBS.get_or_init( load ), name )
}
fn load() -> Vec<Glob>
{
let bundled = crate::resource_dir().map( |dir| dir.join( "mime" ) );
let home = crate::dirs::data_home();
let system = std::env::var( "XDG_DATA_DIRS" ).ok().filter( |dirs| !dirs.is_empty() ).unwrap_or_else( || "/usr/local/share:/usr/share".into() );
let dirs = bundled.into_iter()
.chain( home.map( |dir| dir.join( "mime" ) ) )
.chain( system.split( ':' ).map( |dir| PathBuf::from( dir ).join( "mime" ) ) );
let mut globs = Vec::new();
for dir in dirs
{
if let Ok( text ) = crate::vfs::read_to_string( dir.join( "globs2" ) ) { globs.extend( parse( &text ) ); }
}
globs
}
fn parse( text: &str ) -> impl Iterator<Item = Glob> + '_
{
text.lines().filter( |line| !line.starts_with( '#' ) ).filter_map( |line|
{
let mut fields = line.splitn( 4, ':' );
let weight = fields.next()?.parse().ok()?;
let mime = fields.next()?.to_string();
let pattern = fields.next()?.to_string();
let flags = fields.next().unwrap_or_default();
let case_sensitive = flags.split( ',' ).any( |flag| flag == "cs" );
Some( Glob { weight, mime, pattern: if case_sensitive { pattern } else { pattern.to_lowercase() }, case_sensitive } )
} )
}
fn best<'a>( globs: &'a [ Glob ], name: &str ) -> Option<&'a str>
{
let lower = name.to_lowercase();
let mut found: Option<&Glob> = None;
for glob in globs
{
let subject = if glob.case_sensitive { name } else { &lower };
if !matches( &glob.pattern, subject ) { continue; }
let better = found.is_none_or( |f| ( glob.weight, glob.case_sensitive, glob.pattern.len() ) > ( f.weight, f.case_sensitive, f.pattern.len() ) );
if better { found = Some( glob ); }
}
found.map( |glob| glob.mime.as_str() )
}
fn matches( pattern: &str, name: &str ) -> bool
{
let pattern: Vec<char> = pattern.chars().collect();
let name: Vec<char> = name.chars().collect();
matches_at( &pattern, &name )
}
fn matches_at( pattern: &[ char ], name: &[ char ] ) -> bool
{
match pattern.first()
{
None => name.is_empty(),
Some( '*' ) => ( 0..=name.len() ).any( |skip| matches_at( &pattern[ 1.. ], &name[ skip.. ] ) ),
Some( '?' ) => !name.is_empty() && matches_at( &pattern[ 1.. ], &name[ 1.. ] ),
Some( '[' ) =>
{
let Some( ( &c, rest ) ) = name.split_first() else { return false };
match class( &pattern[ 1.. ], c )
{
Some( ( true, after ) ) => matches_at( after, rest ),
Some( ( false, _ ) ) => false,
None => c == '[' && matches_at( &pattern[ 1.. ], rest ),
}
}
Some( &p ) => name.first() == Some( &p ) && matches_at( &pattern[ 1.. ], &name[ 1.. ] ),
}
}
fn class( pattern: &[ char ], c: char ) -> Option<( bool, &[ char ] )>
{
let ( negated, mut i ) = match pattern.first() { Some( '!' ) | Some( '^' ) => ( true, 1 ), _ => ( false, 0 ) };
let mut hit = false;
let mut first = true;
while i < pattern.len()
{
let p = pattern[ i ];
if p == ']' && !first { return Some( ( hit != negated, &pattern[ i + 1.. ] ) ); }
first = false;
if pattern.get( i + 1 ) == Some( &'-' ) && pattern.get( i + 2 ).is_some_and( |&end| end != ']' )
{
hit |= ( p..=pattern[ i + 2 ] ).contains( &c );
i += 3;
} else {
hit |= p == c;
i += 1;
}
}
None
}
#[ cfg( test ) ]
mod tests
{
use super::*;
const GLOBS2: &str = "\
# This file was automatically generated
50:text/x-csrc:*.c
50:text/x-c++src:*.C:cs
50:image/jpeg:*.jpg
50:image/jpeg:*.jpeg
50:application/x-compressed-tar:*.tar.gz
50:application/gzip:*.gz
10:text/x-readme:readme*
50:text/x-chdr:*.[ch]h
";
fn globs() -> Vec<Glob>
{
parse( GLOBS2 ).collect()
}
#[ test ]
fn extensions_ignore_case_unless_told()
{
let globs = globs();
assert_eq!( best( &globs, "photo.JPG" ), Some( "image/jpeg" ) );
assert_eq!( best( &globs, "main.c" ), Some( "text/x-csrc" ) );
assert_eq!( best( &globs, "main.C" ), Some( "text/x-c++src" ) );
}
#[ test ]
fn the_longest_pattern_of_a_weight_wins()
{
assert_eq!( best( &globs(), "src.tar.gz" ), Some( "application/x-compressed-tar" ) );
assert_eq!( best( &globs(), "log.gz" ), Some( "application/gzip" ) );
}
#[ test ]
fn wildcards_and_classes_match()
{
let globs = globs();
assert_eq!( best( &globs, "README.md" ), Some( "text/x-readme" ) );
assert_eq!( best( &globs, "util.hh" ), Some( "text/x-chdr" ) );
assert_eq!( best( &globs, "util.ch" ), Some( "text/x-chdr" ) );
assert_eq!( best( &globs, "util.xh" ), None );
assert_eq!( best( &globs, "noextension" ), None );
}
#[ test ]
fn classes_take_ranges_and_negation()
{
assert!( matches( "file[0-9]", "file7" ) );
assert!( !matches( "file[!0-9]", "file7" ) );
assert!( matches( "a[]]b", "a]b" ) );
assert!( matches( "a[b", "a[b" ) );
}
}