xdg_mime/
lib.rs

1#![cfg(any(unix, target_os = "redox"))]
2#![doc(html_root_url = "https://docs.rs/xdg_mime/0.4.0")]
3#![allow(dead_code)]
4
5//! `xdg_mime` allows to look up the MIME type associated to a file name
6//! or to the contents of a file, using the [Freedesktop.org Shared MIME
7//! database specification][xdg-mime].
8//!
9//! Alongside the MIME type, the shared MIME database contains other ancillary
10//! information, like the icon associated to the MIME type; the aliases for
11//! a given MIME type; and the various sub-classes of a MIME type.
12//!
13//! [xdg-mime]: https://specifications.freedesktop.org/shared-mime-info-spec/shared-mime-info-spec-latest.html
14//!
15//! ## Loading the Shared MIME database
16//!
17//! The [`SharedMimeInfo`](struct.SharedMimeInfo.html) type will automatically
18//! load all the instances of shared MIME databases available in the following
19//! directories, in this specified order:
20//!
21//!  - `$XDG_DATA_HOME/mime`
22//!    - if `XDG_DATA_HOME` is unset, this corresponds to `$HOME/.local/share/mime`
23//!  - `$XDG_DATA_DIRS/mime`
24//!    - if `XDG_DATA_DIRS` is unset, this corresponds to `/usr/local/share/mime`
25//!      and `/usr/share/mime`
26//!
27//! For more information on the `XDG_DATA_HOME` and `XDG_DATA_DIRS` environment
28//! variables, see the [XDG base directory specification][xdg-basedir].
29//!
30//! [xdg-basedir]: https://specifications.freedesktop.org/basedir-spec/latest/
31//!
32//! The MIME data in each directory will be coalesced into a single database.
33//!
34//! ## Retrieving the MIME type of a file
35//!
36//! If you want to know the MIME type of a file, you typically have two
37//! options at your disposal:
38//!
39//!  - guess from the file name, using the [`get_mime_types_from_file_name`]
40//!    method
41//!  - use an appropriately sized chunk of the file contents and
42//!    perform "content sniffing", using the [`get_mime_type_for_data`] method
43//!
44//! The former step does not come with performance penalties, or even requires
45//! the file to exist in the first place, but it may return a list of potential
46//! matches; the latter can be an arbitrarily expensive operation to perform,
47//! but its result is going to be certain. It is recommended to always guess the
48//! MIME type from the file name first, and only use content sniffing lazily and,
49//! possibly, asynchronously.
50//!
51//! [`get_mime_types_from_file_name`]: struct.SharedMimeInfo.html#method.get_mime_types_from_file_name
52//! [`get_mime_type_for_data`]: struct.SharedMimeInfo.html#method.get_mime_type_for_data
53//!
54//! ## Guessing the MIME type
55//!
56//! If you have access to a file name or its contents, it's possible to use
57//! the [`guess_mime_type`] method to create a [`GuessBuilder`] instance, and
58//! populate it with the file name, its contents, or the full path to the file;
59//! then, call the [`guess`] method to guess the MIME type depending on the
60//! available information.
61//!
62//! [`GuessBuilder`]: struct.GuessBuilder.html
63//! [`guess_mime_type`]: struct.SharedMimeInfo.html#method.guess_mime_type
64//! [`guess`]: struct.GuessBuilder.html#method.guess
65
66use mime::Mime;
67use std::env;
68use std::fs;
69use std::fs::File;
70use std::io::prelude::*;
71use std::path::{Path, PathBuf};
72use std::time::SystemTime;
73
74extern crate dirs_next;
75extern crate nom;
76
77mod alias;
78mod glob;
79mod icon;
80mod magic;
81mod parent;
82
83#[derive(Clone, PartialEq)]
84struct MimeDirectory {
85    path: PathBuf,
86    mtime: SystemTime,
87}
88
89/// The shared MIME info database.
90pub struct SharedMimeInfo {
91    aliases: alias::AliasesList,
92    parents: parent::ParentsMap,
93    icons: Vec<icon::Icon>,
94    generic_icons: Vec<icon::Icon>,
95    globs: glob::GlobMap,
96    magic: Vec<magic::MagicEntry>,
97    mime_dirs: Vec<MimeDirectory>,
98}
99
100/// A builder type to specify the parameters for guessing a MIME type.
101///
102/// Each instance of `GuessBuilder` is tied to the lifetime of the
103/// [`SharedMimeInfo`] instance that created it.
104///
105/// The `GuessBuilder` returned by the [`guess_mime_type`] method is
106/// empty, and will always return a `mime::APPLICATION_OCTET_STREAM`
107/// guess.
108///
109/// You can use the builder methods to specify the file name, the data,
110/// or both, to be used to guess the MIME type:
111///
112/// ```rust
113/// # use std::error::Error;
114/// # use std::str::FromStr;
115/// # use mime::Mime;
116/// #
117/// # fn main() -> Result<(), Box<dyn Error>> {
118/// # let mime_db = xdg_mime::SharedMimeInfo::new();
119/// // let mime_db = ...
120/// let mut guess_builder = mime_db.guess_mime_type();
121/// let guess = guess_builder.file_name("foo.png").guess();
122/// assert_eq!(guess.mime_type(), &Mime::from_str("image/png")?);
123/// #
124/// # Ok(())
125/// # }
126/// ```
127///
128/// The guessed MIME type can have a degree of uncertainty; for instance,
129/// if you only set the [`file_name`] there can be multiple matching MIME
130/// types to choose from. Alternatively, if you only set the [`data`], the
131/// content might not match any existing rule. Even in the case of setting
132/// both the file name and the data the match can be uncertain. This
133/// information is preserved by the [`Guess`] type, and can be retrieved
134/// using the [`uncertain`] method.
135///
136/// [`SharedMimeInfo`]: struct.SharedMimeInfo.html
137/// [`guess_mime_type`]: struct.SharedMimeInfo.html#method.guess_mime_type
138/// [`file_name`]: #method.file_name
139/// [`data`]: #method.data
140/// [`Guess`]: struct.Guess.html
141/// [`uncertain`]: struct.Guess.html#method.uncertain
142pub struct GuessBuilder<'a> {
143    db: &'a SharedMimeInfo,
144    file_name: Option<String>,
145    data: Vec<u8>,
146    metadata: Option<fs::Metadata>,
147    path: Option<PathBuf>,
148}
149
150/// The result of the [`guess`] method of [`GuessBuilder`].
151///
152/// [`guess`]: struct.GuessBuilder.html#method.guess
153/// [`GuessBuilder`]: struct.GuessBuilder.html
154pub struct Guess {
155    mime: mime::Mime,
156    uncertain: bool,
157}
158
159impl<'a> GuessBuilder<'a> {
160    /// Sets the file name to be used to guess its MIME type.
161    ///
162    /// If you have a full path, you should extract the last component,
163    /// for instance using the [`Path::file_name()`][path_file_name]
164    /// method.
165    ///
166    /// [path_file_name]: https://doc.rust-lang.org/std/path/struct.Path.html#method.file_name
167    pub fn file_name(&mut self, name: &str) -> &mut Self {
168        self.file_name = Some(name.to_string());
169
170        self
171    }
172
173    /// Sets the data for which you want to guess the MIME type.
174    pub fn data(&mut self, data: &[u8]) -> &mut Self {
175        // If we have enough data, just copy the largest chunk
176        // necessary to match any rule in the magic entries
177        let max_data_size = magic::max_extents(&self.db.magic);
178        if data.len() > max_data_size {
179            self.data.extend_from_slice(&data[..max_data_size]);
180        } else {
181            self.data.extend(data.iter().cloned());
182        }
183
184        self
185    }
186
187    /// Sets the metadata of the file for which you want to get the MIME type.
188    ///
189    /// The metadata can be used to match an existing file or path, for instance:
190    ///
191    /// ```rust
192    /// # use std::error::Error;
193    /// use std::fs;
194    /// use std::str::FromStr;
195    /// use mime::Mime;
196    /// #
197    /// # fn main() -> Result<(), Box<dyn Error>> {
198    /// # let mime_db = xdg_mime::SharedMimeInfo::new();
199    /// // let mime_db = ...
200    /// # let metadata = fs::metadata("src/lib.rs")?;
201    /// // let metadata = fs::metadata("/path/to/lib.rs")?;
202    /// let mut guess_builder = mime_db.guess_mime_type();
203    /// let guess = guess_builder
204    ///     .file_name("lib.rs")
205    ///     .metadata(metadata)
206    ///     .guess();
207    /// assert_eq!(guess.mime_type(), &Mime::from_str("text/rust")?);
208    /// #
209    /// # Ok(())
210    /// # }
211    /// ```
212    pub fn metadata(&mut self, metadata: fs::Metadata) -> &mut Self {
213        self.metadata = Some(metadata);
214
215        self
216    }
217
218    /// Sets the path of the file for which you want to get the MIME type.
219    ///
220    /// The `path` will be used by the [`guess`] method to extract the
221    /// file name, metadata, and contents, unless you called the [`file_name`],
222    /// [`metadata`], and [`data`] methods, respectively.
223    ///
224    /// ```rust
225    /// # use std::error::Error;
226    /// use std::fs;
227    /// use std::str::FromStr;
228    /// use mime::Mime;
229    /// #
230    /// # fn main() -> Result<(), Box<dyn Error>> {
231    /// # let mime_db = xdg_mime::SharedMimeInfo::new();
232    /// // let mime_db = ...
233    /// let mut guess_builder = mime_db.guess_mime_type();
234    /// let guess = guess_builder
235    ///     .path("src")
236    ///     .guess();
237    /// assert_eq!(guess.mime_type(), &Mime::from_str("inode/directory")?);
238    /// #
239    /// # Ok(())
240    /// # }
241    /// ```
242    ///
243    /// [`guess`]: #method.guess
244    /// [`file_name`]: #method.file_name
245    /// [`metadata`]: #method.metadata
246    /// [`data`]: #method.data
247    pub fn path<P: AsRef<Path>>(&mut self, path: P) -> &mut Self {
248        let mut buf = PathBuf::new();
249        buf.push(path);
250
251        self.path = Some(buf);
252
253        self
254    }
255
256    /// Guesses the MIME type using the data set on the builder. The result is
257    /// a [`Guess`] instance that contains both the guessed MIME type, and whether
258    /// the result of the guess is certain.
259    ///
260    /// [`Guess`]: struct.Guess.html
261    pub fn guess(&mut self) -> Guess {
262        if let Some(path) = &self.path {
263            // Fill out the metadata
264            if self.metadata.is_none() {
265                self.metadata = match fs::metadata(path) {
266                    Ok(m) => Some(m),
267                    Err(_) => None,
268                };
269            }
270
271            fn load_data_chunk<P: AsRef<Path>>(path: P, chunk_size: usize) -> Option<Vec<u8>> {
272                if chunk_size == 0 {
273                    return None;
274                }
275
276                let mut f = match File::open(&path) {
277                    Ok(file) => file,
278                    Err(_) => return None,
279                };
280
281                let mut buf = vec![0u8; chunk_size];
282
283                if f.read_exact(&mut buf).is_err() {
284                    return None;
285                }
286
287                Some(buf)
288            }
289
290            // Load the minimum amount of data necessary for a match
291            if self.data.is_empty() {
292                let mut max_data_size = magic::max_extents(&self.db.magic);
293
294                if let Some(metadata) = &self.metadata {
295                    let file_size: usize = metadata.len() as usize;
296                    if file_size < max_data_size {
297                        max_data_size = file_size;
298                    }
299                }
300
301                match load_data_chunk(path, max_data_size) {
302                    Some(v) => self.data.extend(v),
303                    None => self.data.clear(),
304                }
305            }
306
307            // Set the file name
308            if self.file_name.is_none() {
309                if let Some(file_name) = path.file_name() {
310                    self.file_name = match file_name.to_os_string().into_string() {
311                        Ok(v) => Some(v),
312                        Err(_) => None,
313                    };
314                }
315            }
316        }
317
318        if let Some(metadata) = &self.metadata {
319            let file_type = metadata.file_type();
320
321            // Special type for directories
322            if file_type.is_dir() {
323                return Guess {
324                    mime: "inode/directory".parse::<mime::Mime>().unwrap(),
325                    uncertain: true,
326                };
327            }
328
329            // Special type for symbolic links
330            if file_type.is_symlink() {
331                return Guess {
332                    mime: "inode/symlink".parse::<mime::Mime>().unwrap(),
333                    uncertain: true,
334                };
335            }
336
337            // Special type for empty files
338            if metadata.len() == 0 {
339                return Guess {
340                    mime: "application/x-zerosize".parse::<mime::Mime>().unwrap(),
341                    uncertain: true,
342                };
343            }
344        }
345
346        let name_mime_types: Vec<mime::Mime> = match &self.file_name {
347            Some(file_name) => self.db.get_mime_types_from_file_name(file_name),
348            None => Vec::new(),
349        };
350
351        // File name match, and no conflicts
352        if name_mime_types.len() == 1 && name_mime_types[0] != mime::APPLICATION_OCTET_STREAM {
353            return Guess {
354                mime: name_mime_types[0].clone(),
355                uncertain: false,
356            };
357        }
358
359        let sniffed_mime = self
360            .db
361            .get_mime_type_for_data(&self.data)
362            .unwrap_or((mime::APPLICATION_OCTET_STREAM, 80));
363
364        if name_mime_types.is_empty() {
365            // No names and no data => unknown MIME type
366            if self.data.is_empty() {
367                return Guess {
368                    mime: mime::APPLICATION_OCTET_STREAM,
369                    uncertain: true,
370                };
371            }
372
373            return Guess {
374                mime: sniffed_mime.0.clone(),
375                uncertain: sniffed_mime.0 == mime::APPLICATION_OCTET_STREAM,
376            };
377        } else {
378            let (mut mime, priority) = sniffed_mime;
379
380            // "If no magic rule matches the data (or if the content is not
381            // available), use the default type of application/octet-stream
382            // for binary data, or text/plain for textual data."
383            // -- shared-mime-info, "Recommended checking order"
384            if mime == mime::APPLICATION_OCTET_STREAM
385                && !self.data.is_empty()
386                && looks_like_text(&self.data)
387            {
388                mime = mime::TEXT_PLAIN;
389            }
390
391            // From the content type guessing implementation in GIO:
392            //
393            // For security reasons we don't ever want to sniff desktop files
394            // where we know the filename and it doesn't have a .desktop extension.
395            // This is because desktop files allow executing any application and
396            // we don't want to make it possible to hide them looking like something
397            // else.
398            if self.file_name.is_some() {
399                let x_desktop = "application/x-desktop".parse::<mime::Mime>().unwrap();
400
401                if mime == x_desktop {
402                    mime = mime::TEXT_PLAIN;
403                }
404            }
405
406            if mime != mime::APPLICATION_OCTET_STREAM {
407                // We found a match with a high confidence value
408                if priority >= 80 {
409                    return Guess {
410                        mime,
411                        uncertain: false,
412                    };
413                }
414
415                // We have possible conflicts, but the data matches the
416                // file name, so let's see if the sniffed MIME type is
417                // a subclass of the MIME type associated to the file name,
418                // and use that as a tie breaker.
419                if name_mime_types
420                    .iter()
421                    .any(|m| self.db.mime_type_subclass(&mime, m))
422                {
423                    return Guess {
424                        mime,
425                        uncertain: false,
426                    };
427                }
428            }
429
430            // If there are conflicts, and the data does not help us,
431            // we just pick the first result
432            if let Some(mime_type) = name_mime_types.first() {
433                return Guess {
434                    mime: mime_type.clone(),
435                    uncertain: true,
436                };
437            }
438        }
439
440        // Okay, we give up
441        Guess {
442            mime: mime::APPLICATION_OCTET_STREAM,
443            uncertain: true,
444        }
445    }
446}
447
448fn looks_like_text(data: &[u8]) -> bool {
449    // "Checking the first 128 bytes of the file for ASCII
450    // control characters is a good way to guess whether a
451    // file is binary or text."
452    // -- shared-mime-info, "Recommended checking order"
453    !data
454        .iter()
455        .take(128)
456        .any(|ch| ch.is_ascii_control() && !ch.is_ascii_whitespace())
457}
458
459impl Guess {
460    /// The guessed MIME type.
461    pub fn mime_type(&self) -> &mime::Mime {
462        &self.mime
463    }
464
465    /// Whether the guessed MIME type is uncertain.
466    ///
467    /// If the MIME type was guessed only from its file name there can be
468    /// multiple matches, but the [`mime_type`] method will return just the
469    /// first match.
470    ///
471    /// If you only have a file name, and you want to gather all potential
472    /// matches, you should use the [`get_mime_types_from_file_name`] method
473    /// instead of performing a guess.
474    ///
475    /// [`mime_type`]: #method.mime_type
476    /// [`get_mime_types_from_file_name`]: struct.SharedMimeInfo.html#method.get_mime_types_from_file_name
477    pub fn uncertain(&self) -> bool {
478        self.uncertain
479    }
480}
481
482impl Default for SharedMimeInfo {
483    fn default() -> Self {
484        Self::new()
485    }
486}
487
488impl SharedMimeInfo {
489    fn create() -> SharedMimeInfo {
490        SharedMimeInfo {
491            aliases: alias::AliasesList::new(),
492            parents: parent::ParentsMap::new(),
493            icons: Vec::new(),
494            generic_icons: Vec::new(),
495            globs: glob::GlobMap::new(),
496            magic: Vec::new(),
497            mime_dirs: Vec::new(),
498        }
499    }
500
501    fn load_directory<P: AsRef<Path>>(&mut self, directory: P) {
502        let mut mime_path = PathBuf::new();
503        mime_path.push(directory);
504        mime_path.push("mime");
505
506        let aliases = alias::read_aliases_from_dir(&mime_path);
507        self.aliases.add_aliases(aliases);
508
509        let icons = icon::read_icons_from_dir(&mime_path, false);
510        self.icons.extend(icons);
511
512        let generic_icons = icon::read_icons_from_dir(&mime_path, true);
513        self.generic_icons.extend(generic_icons);
514
515        let subclasses = parent::read_subclasses_from_dir(&mime_path);
516        self.parents.add_subclasses(subclasses);
517
518        let globs = glob::read_globs_from_dir(&mime_path);
519        self.globs.add_globs(&globs);
520
521        let magic_entries = magic::read_magic_from_dir(&mime_path);
522        self.magic.extend(magic_entries);
523
524        let mime_dir = match fs::metadata(&mime_path) {
525            Ok(v) => {
526                let mtime = v.modified().unwrap_or_else(|_| SystemTime::now());
527
528                MimeDirectory {
529                    path: mime_path,
530                    mtime,
531                }
532            }
533            Err(_) => MimeDirectory {
534                path: mime_path,
535                mtime: SystemTime::now(),
536            },
537        };
538
539        self.mime_dirs.push(mime_dir);
540    }
541
542    /// Creates a new `SharedMimeInfo` instance containing all MIME information
543    /// under the [standard XDG base directories][xdg-basedir].
544    ///
545    /// [xdg-basedir]: http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html
546    pub fn new() -> SharedMimeInfo {
547        let mut db = SharedMimeInfo::create();
548
549        let data_home = dirs_next::data_dir().expect("Data directory is unset");
550        db.load_directory(data_home);
551
552        let data_dirs = match env::var_os("XDG_DATA_DIRS") {
553            Some(v) => env::split_paths(&v).collect(),
554            None => vec![
555                PathBuf::from("/usr/local/share"),
556                PathBuf::from("/usr/share"),
557            ],
558        };
559
560        for dir in data_dirs {
561            db.load_directory(dir)
562        }
563
564        db
565    }
566
567    /// Loads all the MIME information under `directory`, and creates a new
568    /// [`SharedMimeInfo`] instance for it.
569    ///
570    /// This method is only really useful for testing purposes; you should
571    /// always use the [`new`] method, instead.
572    ///
573    /// [`SharedMimeInfo`]: struct.SharedMimeInfo.html
574    /// [`new`]: #method.new
575    pub fn new_for_directory<P: AsRef<Path>>(directory: P) -> SharedMimeInfo {
576        let mut db = SharedMimeInfo::create();
577
578        db.load_directory(directory);
579
580        db
581    }
582
583    /// Reloads the contents of the [`SharedMimeInfo`] type from the directories
584    /// used to populate it at construction time. You should use this method
585    /// if you're planning to keep the database around for long running operations
586    /// or applications.
587    ///
588    /// This method does not do anything if the directories haven't changed
589    /// since the time they were loaded last.
590    ///
591    /// This method will return `true` if the contents of the shared MIME
592    /// database were updated.
593    ///
594    /// [`SharedMimeInfo`]: struct.SharedMimeInfo.html
595    pub fn reload(&mut self) -> bool {
596        let mut dropped_db = false;
597
598        // Do not reload the data if nothing has changed
599        for dir in &self.mime_dirs {
600            let mtime = match fs::metadata(&dir.path) {
601                Ok(v) => v.modified().unwrap_or(dir.mtime),
602                Err(_) => dir.mtime,
603            };
604
605            // Drop everything if a directory was changed since
606            // the last time we looked into it
607            if dir.mtime < mtime {
608                dropped_db = true;
609
610                self.aliases.clear();
611                self.parents.clear();
612                self.globs.clear();
613                self.icons.clear();
614                self.generic_icons.clear();
615                self.magic.clear();
616
617                break;
618            }
619        }
620
621        if dropped_db {
622            let mime_dirs: Vec<MimeDirectory> = self.mime_dirs.to_vec();
623
624            self.mime_dirs.clear();
625
626            for dir in &mime_dirs {
627                // Pop the `mime` chunk, since load_directory() will
628                // automatically add it back
629                let mut base_dir = PathBuf::new();
630                base_dir.push(&dir.path);
631                base_dir.pop();
632
633                self.load_directory(base_dir);
634            }
635        }
636
637        dropped_db
638    }
639
640    /// Retrieves the MIME type aliased by a MIME type, if any.
641    pub fn unalias_mime_type(&self, mime_type: &Mime) -> Option<Mime> {
642        self.aliases.unalias_mime_type(mime_type)
643    }
644
645    /// Looks up the icons associated to a MIME type.
646    ///
647    /// The icons can be looked up within the current [icon theme][xdg-icon-theme].
648    ///
649    /// [xdg-icon-theme]: https://specifications.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html
650    pub fn lookup_icon_names(&self, mime_type: &Mime) -> Vec<String> {
651        let mut res = Vec::new();
652
653        if let Some(v) = icon::find_icon(&self.icons, mime_type) {
654            res.push(v);
655        };
656
657        res.push(mime_type.essence_str().replace('/', "-"));
658
659        match icon::find_icon(&self.generic_icons, mime_type) {
660            Some(v) => res.push(v),
661            None => {
662                let generic = format!("{}-x-generic", mime_type.type_());
663                res.push(generic);
664            }
665        };
666
667        res
668    }
669
670    /// Looks up the generic icon associated to a MIME type.
671    ///
672    /// The icon can be looked up within the current [icon theme][xdg-icon-theme].
673    ///
674    /// [xdg-icon-theme]: https://specifications.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html
675    pub fn lookup_generic_icon_name(&self, mime_type: &Mime) -> Option<String> {
676        let res = match icon::find_icon(&self.generic_icons, mime_type) {
677            Some(v) => v,
678            None => format!("{}-x-generic", mime_type.type_()),
679        };
680
681        Some(res)
682    }
683
684    /// Retrieves all the parent MIME types associated to `mime_type`.
685    pub fn get_parents(&self, mime_type: &Mime) -> Option<Vec<Mime>> {
686        let unaliased = match self.aliases.unalias_mime_type(mime_type) {
687            Some(v) => v,
688            None => return None,
689        };
690
691        let mut res = vec![unaliased.clone()];
692
693        if let Some(parents) = self.parents.lookup(&unaliased) {
694            for parent in parents {
695                res.push(parent.clone());
696            }
697        };
698
699        Some(res)
700    }
701
702    /// Retrieves the list of matching MIME types for the given file name,
703    /// without looking at the data inside the file.
704    ///
705    /// If no specific MIME-type can be determined, returns a single
706    /// element vector containing the `application/octet-stream` MIME type.
707    ///
708    /// ```rust
709    /// # use std::error::Error;
710    /// # use std::str::FromStr;
711    /// # use mime::Mime;
712    /// #
713    /// # fn main() -> Result<(), Box<dyn Error>> {
714    /// # let mime_db = xdg_mime::SharedMimeInfo::new();
715    /// // let mime_db = ...
716    /// let mime_types: Vec<Mime> = mime_db.get_mime_types_from_file_name("file.txt");
717    /// assert_eq!(mime_types, vec![Mime::from_str("text/plain")?]);
718    /// #
719    /// # Ok(())
720    /// # }
721    /// ```
722    pub fn get_mime_types_from_file_name(&self, file_name: &str) -> Vec<Mime> {
723        match self.globs.lookup_mime_type_for_file_name(file_name) {
724            Some(v) => v,
725            None => {
726                vec![mime::APPLICATION_OCTET_STREAM.clone()]
727            }
728        }
729    }
730
731    /// Retrieves the MIME type for the given data, and the priority of the
732    /// match. A priority above 80 means a certain match.
733    pub fn get_mime_type_for_data(&self, data: &[u8]) -> Option<(Mime, u32)> {
734        if data.is_empty() {
735            let empty_mime: mime::Mime = "application/x-zerosize".parse().unwrap();
736            return Some((empty_mime, 100));
737        }
738
739        magic::lookup_data(&self.magic, data)
740    }
741
742    /// Checks whether two MIME types are equal, taking into account
743    /// eventual aliases.
744    ///
745    /// ```rust
746    /// # use std::error::Error;
747    /// # use std::str::FromStr;
748    /// # use mime::Mime;
749    /// #
750    /// # fn main() -> Result<(), Box<dyn Error>> {
751    /// # let mime_db = xdg_mime::SharedMimeInfo::new();
752    /// // let mime_db = ...
753    /// let x_markdown: Mime = "text/x-markdown".parse()?;
754    /// let markdown: Mime = "text/markdown".parse()?;
755    /// assert!(mime_db.mime_type_equal(&x_markdown, &markdown));
756    /// #
757    /// # Ok(())
758    /// # }
759    /// ```
760    pub fn mime_type_equal(&self, mime_a: &Mime, mime_b: &Mime) -> bool {
761        let unaliased_a = self
762            .unalias_mime_type(mime_a)
763            .unwrap_or_else(|| mime_a.clone());
764        let unaliased_b = self
765            .unalias_mime_type(mime_b)
766            .unwrap_or_else(|| mime_b.clone());
767
768        unaliased_a == unaliased_b
769    }
770
771    /// Checks whether a MIME type is a subclass of another MIME type.
772    ///
773    /// ```rust
774    /// # use std::error::Error;
775    /// # use std::str::FromStr;
776    /// # use mime::Mime;
777    /// #
778    /// # fn main() -> Result<(), Box<dyn Error>> {
779    /// # let mime_db = xdg_mime::SharedMimeInfo::new();
780    /// // let mime_db = ...
781    /// let rust: Mime = "text/rust".parse()?;
782    /// let text: Mime = "text/plain".parse()?;
783    /// assert!(mime_db.mime_type_subclass(&rust, &text));
784    /// #
785    /// # Ok(())
786    /// # }
787    /// ```
788    pub fn mime_type_subclass(&self, mime_type: &Mime, base: &Mime) -> bool {
789        let unaliased_mime = self
790            .unalias_mime_type(mime_type)
791            .unwrap_or_else(|| mime_type.clone());
792        let unaliased_base = self.unalias_mime_type(base).unwrap_or_else(|| base.clone());
793
794        if unaliased_mime == unaliased_base {
795            return true;
796        }
797
798        // Handle super-types
799        if unaliased_base.subtype() == mime::STAR {
800            let base_type = unaliased_base.type_();
801            let unaliased_type = unaliased_mime.type_();
802
803            if base_type == unaliased_type {
804                return true;
805            }
806        }
807
808        // The text/plain and application/octet-stream require some
809        // special handling:
810        //
811        //  - All text/* types are subclasses of text/plain.
812        //  - All streamable types (ie, everything except the
813        //    inode/* types) are subclasses of application/octet-stream
814        //
815        // https://specifications.freedesktop.org/shared-mime-info-spec/shared-mime-info-spec-latest.html#subclassing
816        if unaliased_base == mime::TEXT_PLAIN && unaliased_mime.type_() == mime::TEXT {
817            return true;
818        }
819
820        if unaliased_base == mime::APPLICATION_OCTET_STREAM && unaliased_mime.type_() != "inode" {
821            return true;
822        }
823
824        if let Some(parents) = self.parents.lookup(&unaliased_mime) {
825            if parents
826                .iter()
827                .any(|p| self.mime_type_subclass(p, &unaliased_base))
828            {
829                return true;
830            }
831        }
832
833        false
834    }
835
836    /// Creates a new [`GuessBuilder`] that can be used to guess the MIME type
837    /// of a file name, its contents, or a path.
838    ///
839    /// ```rust
840    /// # use std::error::Error;
841    /// # use std::str::FromStr;
842    /// # use mime::Mime;
843    /// #
844    /// # fn main() -> Result<(), Box<dyn Error>> {
845    /// # let mime_db = xdg_mime::SharedMimeInfo::new();
846    /// // let mime_db = ...
847    /// let mut gb = mime_db.guess_mime_type();
848    /// let guess = gb.file_name("foo.txt").guess();
849    /// assert_eq!(guess.mime_type(), &mime::TEXT_PLAIN);
850    /// assert_eq!(guess.uncertain(), false);
851    /// #
852    /// # Ok(())
853    /// # }
854    /// ```
855    ///
856    /// [`GuessBuilder`]: struct.GuessBuilder.html
857    pub fn guess_mime_type(&self) -> GuessBuilder {
858        GuessBuilder {
859            db: self,
860            file_name: None,
861            data: Vec::new(),
862            metadata: None,
863            path: None,
864        }
865    }
866}
867
868#[cfg(test)]
869mod tests {
870    use super::*;
871    use std::env;
872    use std::str::FromStr;
873
874    fn load_test_data() -> SharedMimeInfo {
875        let cwd = env::current_dir().unwrap().to_string_lossy().into_owned();
876        let dir = PathBuf::from(&format!("{}/test_files", cwd));
877        SharedMimeInfo::new_for_directory(dir)
878    }
879
880    #[test]
881    fn load_from_directory() {
882        let cwd = env::current_dir().unwrap().to_string_lossy().into_owned();
883        let dir = PathBuf::from(&format!("{}/test_files", cwd));
884        SharedMimeInfo::new_for_directory(dir);
885    }
886
887    #[test]
888    fn load_system() {
889        let _db = SharedMimeInfo::new();
890    }
891
892    #[test]
893    fn load_default() {
894        let _db: SharedMimeInfo = Default::default();
895    }
896
897    #[test]
898    fn reload() {
899        // We don't load the system data in the, admittedly, remote case the system
900        // is getting updated *while* we run the test suite.
901        let mut _db = load_test_data();
902
903        assert_eq!(_db.reload(), false);
904    }
905
906    #[test]
907    fn lookup_generic_icons() {
908        let mime_db = load_test_data();
909
910        assert_eq!(
911            mime_db.lookup_generic_icon_name(&mime::APPLICATION_JSON),
912            Some("text-x-script".to_string())
913        );
914        assert_eq!(
915            mime_db.lookup_generic_icon_name(&mime::TEXT_PLAIN),
916            Some("text-x-generic".to_string())
917        );
918    }
919
920    #[test]
921    fn unalias() {
922        let mime_db = load_test_data();
923
924        assert_eq!(
925            mime_db.unalias_mime_type(&Mime::from_str("application/ics").unwrap()),
926            Some(Mime::from_str("text/calendar").unwrap())
927        );
928        assert_eq!(
929            mime_db.unalias_mime_type(&Mime::from_str("text/plain").unwrap()),
930            None
931        );
932    }
933
934    #[test]
935    fn mime_type_equal() {
936        let mime_db = load_test_data();
937
938        assert_eq!(
939            mime_db.mime_type_equal(
940                &Mime::from_str("application/wordperfect").unwrap(),
941                &Mime::from_str("application/vnd.wordperfect").unwrap(),
942            ),
943            true
944        );
945        assert_eq!(
946            mime_db.mime_type_equal(
947                &Mime::from_str("application/x-gnome-app-info").unwrap(),
948                &Mime::from_str("application/x-desktop").unwrap(),
949            ),
950            true
951        );
952        assert_eq!(
953            mime_db.mime_type_equal(
954                &Mime::from_str("application/x-wordperfect").unwrap(),
955                &Mime::from_str("application/vnd.wordperfect").unwrap(),
956            ),
957            true
958        );
959        assert_eq!(
960            mime_db.mime_type_equal(
961                &Mime::from_str("application/x-wordperfect").unwrap(),
962                &Mime::from_str("audio/x-midi").unwrap(),
963            ),
964            false
965        );
966        assert_eq!(
967            mime_db.mime_type_equal(
968                &Mime::from_str("application/octet-stream").unwrap(),
969                &Mime::from_str("text/plain").unwrap(),
970            ),
971            false
972        );
973        assert_eq!(
974            mime_db.mime_type_equal(
975                &Mime::from_str("text/plain").unwrap(),
976                &Mime::from_str("text/*").unwrap(),
977            ),
978            false
979        );
980    }
981
982    #[test]
983    fn mime_type_for_file_name() {
984        let mime_db = load_test_data();
985
986        assert_eq!(
987            mime_db.get_mime_types_from_file_name("foo.txt"),
988            vec![Mime::from_str("text/plain").unwrap()]
989        );
990
991        assert_eq!(
992            mime_db.get_mime_types_from_file_name("bar.gif"),
993            vec![Mime::from_str("image/gif").unwrap()]
994        );
995
996        assert_eq!(
997            mime_db.get_mime_types_from_file_name("baz.mod"),
998            vec![Mime::from_str("audio/x-mod").unwrap()]
999        );
1000    }
1001
1002    #[test]
1003    fn mime_type_for_file_data() {
1004        let mime_db = load_test_data();
1005
1006        let svg_data = include_bytes!("../test_files/files/rust-logo.svg");
1007        assert_eq!(
1008            mime_db.get_mime_type_for_data(svg_data),
1009            Some((Mime::from_str("image/svg+xml").unwrap(), 80))
1010        );
1011
1012        let png_data = include_bytes!("../test_files/files/rust-logo.png");
1013        assert_eq!(
1014            mime_db.get_mime_type_for_data(png_data),
1015            Some((Mime::from_str("image/png").unwrap(), 50))
1016        );
1017    }
1018
1019    #[test]
1020    fn mime_type_subclass() {
1021        let mime_db = load_test_data();
1022
1023        assert_eq!(
1024            mime_db.mime_type_subclass(
1025                &Mime::from_str("application/rtf").unwrap(),
1026                &Mime::from_str("text/plain").unwrap(),
1027            ),
1028            true
1029        );
1030        assert_eq!(
1031            mime_db.mime_type_subclass(
1032                &Mime::from_str("message/news").unwrap(),
1033                &Mime::from_str("text/plain").unwrap(),
1034            ),
1035            true
1036        );
1037        assert_eq!(
1038            mime_db.mime_type_subclass(
1039                &Mime::from_str("message/news").unwrap(),
1040                &Mime::from_str("message/*").unwrap(),
1041            ),
1042            true
1043        );
1044        assert_eq!(
1045            mime_db.mime_type_subclass(
1046                &Mime::from_str("message/news").unwrap(),
1047                &Mime::from_str("text/*").unwrap(),
1048            ),
1049            true
1050        );
1051        assert_eq!(
1052            mime_db.mime_type_subclass(
1053                &Mime::from_str("message/news").unwrap(),
1054                &Mime::from_str("application/octet-stream").unwrap(),
1055            ),
1056            true
1057        );
1058        assert_eq!(
1059            mime_db.mime_type_subclass(
1060                &Mime::from_str("application/rtf").unwrap(),
1061                &Mime::from_str("application/octet-stream").unwrap(),
1062            ),
1063            true
1064        );
1065        assert_eq!(
1066            mime_db.mime_type_subclass(
1067                &Mime::from_str("application/x-gnome-app-info").unwrap(),
1068                &Mime::from_str("text/plain").unwrap(),
1069            ),
1070            true
1071        );
1072        assert_eq!(
1073            mime_db.mime_type_subclass(
1074                &Mime::from_str("image/x-djvu").unwrap(),
1075                &Mime::from_str("image/vnd.djvu").unwrap(),
1076            ),
1077            true
1078        );
1079        assert_eq!(
1080            mime_db.mime_type_subclass(
1081                &Mime::from_str("image/vnd.djvu").unwrap(),
1082                &Mime::from_str("image/x-djvu").unwrap(),
1083            ),
1084            true
1085        );
1086        assert_eq!(
1087            mime_db.mime_type_subclass(
1088                &Mime::from_str("image/vnd.djvu").unwrap(),
1089                &Mime::from_str("text/plain").unwrap(),
1090            ),
1091            false
1092        );
1093        assert_eq!(
1094            mime_db.mime_type_subclass(
1095                &Mime::from_str("image/vnd.djvu").unwrap(),
1096                &Mime::from_str("text/*").unwrap(),
1097            ),
1098            false
1099        );
1100        assert_eq!(
1101            mime_db.mime_type_subclass(
1102                &Mime::from_str("text/*").unwrap(),
1103                &Mime::from_str("text/plain").unwrap(),
1104            ),
1105            true
1106        );
1107        assert_eq!(
1108            mime_db.mime_type_subclass(
1109                &Mime::from_str("application/x-shellscript").unwrap(),
1110                &mime::APPLICATION_OCTET_STREAM
1111            ),
1112            true
1113        );
1114    }
1115
1116    #[test]
1117    fn guess_none() {
1118        let mime_db = load_test_data();
1119
1120        let mut gb = mime_db.guess_mime_type();
1121        let guess = gb.guess();
1122        assert_eq!(guess.mime_type(), &mime::APPLICATION_OCTET_STREAM);
1123        assert_eq!(guess.uncertain(), true);
1124    }
1125
1126    #[test]
1127    fn guess_filename() {
1128        let mime_db = load_test_data();
1129        let mut gb = mime_db.guess_mime_type();
1130        let guess = gb.file_name("foo.txt").guess();
1131        assert_eq!(guess.mime_type(), &mime::TEXT_PLAIN);
1132        assert_eq!(guess.uncertain(), false);
1133    }
1134
1135    #[test]
1136    fn guess_data() {
1137        let svg_data = include_bytes!("../test_files/files/rust-logo.svg");
1138        let mime_db = load_test_data();
1139        let mut gb = mime_db.guess_mime_type();
1140        let guess = gb.data(svg_data).guess();
1141        assert_eq!(guess.mime_type(), &Mime::from_str("image/svg+xml").unwrap());
1142        assert_eq!(guess.uncertain(), false);
1143    }
1144
1145    #[test]
1146    fn guess_both() {
1147        let png_data = include_bytes!("../test_files/files/rust-logo.png");
1148        let mime_db = load_test_data();
1149        let mut gb = mime_db.guess_mime_type();
1150        let guess = gb.file_name("rust-logo.png").data(png_data).guess();
1151        assert_eq!(guess.mime_type(), &Mime::from_str("image/png").unwrap());
1152        assert_eq!(guess.uncertain(), false);
1153    }
1154
1155    #[test]
1156    fn guess_script() {
1157        let sh_data = include_bytes!("../test_files/files/script");
1158        let mime_db = load_test_data();
1159        let mut gb = mime_db.guess_mime_type();
1160        let guess = gb.data(sh_data).guess();
1161        assert_eq!(
1162            guess.mime_type(),
1163            &Mime::from_str("application/x-shellscript").unwrap()
1164        );
1165    }
1166
1167    #[test]
1168    fn guess_script_with_name() {
1169        let sh_data = include_bytes!("../test_files/files/gp");
1170        let mime_db = load_test_data();
1171        let mut gb = mime_db.guess_mime_type();
1172        let guess = gb.file_name("gp").data(sh_data).guess();
1173        assert_eq!(
1174            guess.mime_type(),
1175            &Mime::from_str("application/x-shellscript").unwrap()
1176        );
1177    }
1178
1179    #[test]
1180    fn guess_empty() {
1181        let mime_db = load_test_data();
1182        let mut gb = mime_db.guess_mime_type();
1183        let cwd = env::current_dir().unwrap().to_string_lossy().into_owned();
1184        let file = PathBuf::from(&format!("{}/test_files/files/empty", cwd));
1185        let guess = gb.path(file).guess();
1186        assert_ne!(guess.mime_type(), &mime::TEXT_PLAIN);
1187        assert_eq!(
1188            guess.mime_type(),
1189            &Mime::from_str("application/x-zerosize").unwrap()
1190        );
1191    }
1192
1193    #[test]
1194    fn guess_text() {
1195        let mime_db = load_test_data();
1196        let mut gb = mime_db.guess_mime_type();
1197        let cwd = env::current_dir().unwrap().to_string_lossy().into_owned();
1198        let file = PathBuf::from(&format!("{}/test_files/files/text", cwd));
1199        let guess = gb.path(file).guess();
1200        assert_eq!(guess.mime_type(), &mime::TEXT_PLAIN);
1201    }
1202
1203    #[test]
1204    fn looks_like_text_works() {
1205        assert!(looks_like_text(&[]));
1206        assert!(looks_like_text(b"hello"));
1207        assert!(!looks_like_text(b"hello\x00"));
1208        assert!(!looks_like_text(&[0, 1, 2]));
1209    }
1210
1211    #[test]
1212    fn guess_turtle() {
1213        let cwd = env::current_dir().unwrap().to_string_lossy().into_owned();
1214        let ttl_file = PathBuf::from(&format!("{}/test_files/files/example.ttl", cwd));
1215        let ttl_data = include_bytes!("../test_files/files/example.ttl");
1216        let ttl_meta = std::fs::metadata(ttl_file).unwrap();
1217        let mime_db = load_test_data();
1218        let mut gb = mime_db.guess_mime_type();
1219        let guess = gb
1220            .file_name("example.ttl")
1221            .metadata(ttl_meta)
1222            .data(ttl_data)
1223            .guess();
1224        assert_eq!(guess.mime_type(), &Mime::from_str("text/turtle").unwrap());
1225    }
1226
1227    #[test]
1228    fn guess_dodgy_desktop_file() {
1229        let cwd = env::current_dir().unwrap().to_string_lossy().into_owned();
1230        let desktop_file = PathBuf::from(&format!("{}/test_files/files/launcher", cwd));
1231        let desktop_data = include_bytes!("../test_files/files/launcher");
1232        let desktop_meta = std::fs::metadata(desktop_file).unwrap();
1233        let mime_db = load_test_data();
1234        let mut gb = mime_db.guess_mime_type();
1235        let guess = gb
1236            .file_name("launcher")
1237            .metadata(desktop_meta)
1238            .data(desktop_data)
1239            .guess();
1240        assert_eq!(guess.mime_type(), &Mime::from_str("text/plain").unwrap());
1241    }
1242
1243    #[test]
1244    fn guess_html_with_no_html_tags() {
1245        let mime_db = load_test_data();
1246        let mut gb = mime_db.guess_mime_type();
1247        let cwd = env::current_dir().unwrap().to_string_lossy().into_owned();
1248        let file = PathBuf::from(&format!("{}/test_files/files/no_html_tags.html", cwd));
1249        let guess = gb.path(file).guess();
1250        assert_eq!(guess.mime_type(), &mime::TEXT_HTML);
1251    }
1252}