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}