magic/
lib.rs

1// SPDX-FileCopyrightText: © The `magic` Rust crate authors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! # About
5//!
6//! This crate provides bindings for the `libmagic` C library, which recognizes the
7//! type of data contained in a file (or buffer).
8//!
9//! You might be familiar with `libmagic`'s command-line-interface; [`file`](https://www.darwinsys.com/file/):
10//!
11//! ```shell
12//! $ file data/tests/rust-logo-128x128-blk.png
13//! data/tests/rust-logo-128x128-blk.png: PNG image data, 128 x 128, 8-bit/color RGBA, non-interlaced
14//! ```
15//!
16//! ## `libmagic`
17//!
18//! Understanding how the `libmagic` C library and thus this Rust crate works requires a bit of glossary.
19//!
20//! `libmagic` at its core can analyze a file or buffer and return a mostly unstructured text that describes the analysis result.
21//! There are built-in tests for special cases such as symlinks and compressed files
22//! and there are magic databases with signatures which can be supplied by the user for the generic cases
23//! ("if those bytes look like this, it's a PNG image file").
24//!
25//! The analysis behaviour can be influenced by so-called flags and parameters.
26//! Flags are either set or unset and do not have a value, parameters have a value.
27//!
28//! Databases can be in text form or compiled binary form for faster access. They can be loaded from files on disk or from in-memory buffers.
29//! A regular `libmagic` / `file` installation contains a default database file that includes a plethora of file formats.
30//!
31//! Most `libmagic` functionality requires a configured instance which is called a "magic cookie".
32//! Creating a cookie instance requires initial flags and usually loaded databases.
33//!
34//! # Usage example
35//!
36//! ```rust
37//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
38//! # use std::convert::TryInto;
39//! // open a new configuration with flags
40//! let cookie = magic::Cookie::open(magic::cookie::Flags::ERROR)?;
41//!
42//! // load a specific database
43//! // (so exact test text assertion below works regardless of the system's default database version)
44//! let database = ["data/tests/db-images-png"].try_into()?;
45//! // you can instead load the default database
46//! //let database = Default::default();
47//!
48//! let cookie = cookie.load(&database)?;
49//!
50//! // analyze a test file
51//! let file_to_analyze = "data/tests/rust-logo-128x128-blk.png";
52//! let expected_analysis_result = "PNG image data, 128 x 128, 8-bit/color RGBA, non-interlaced";
53//! assert_eq!(cookie.file(file_to_analyze)?, expected_analysis_result);
54//! # Ok(())
55//! # }
56//! ```
57//!
58//! See further examples in [`examples/`](https://github.com/robo9k/rust-magic/tree/main/examples).
59//!
60//! # MIME example
61//!
62//! Return a MIME type with "charset" encoding parameter:
63//!
64//! ```shell
65//! $ file --mime data/tests/rust-logo-128x128-blk.png
66//! data/tests/rust-logo-128x128-blk.png: image/png; charset=binary
67//! ```
68//! ```rust
69//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
70//! # use std::convert::TryInto;
71//! // open a new configuration with flags for mime type and encoding
72//! let flags = magic::cookie::Flags::MIME_TYPE | magic::cookie::Flags::MIME_ENCODING;
73//! let cookie = magic::Cookie::open(flags)?;
74//!
75//! // load a specific database
76//! let database = ["data/tests/db-images-png"].try_into()?;
77//! let cookie = cookie.load(&database)?;
78//!
79//! // analyze a test file
80//! let file_to_analyze = "data/tests/rust-logo-128x128-blk.png";
81//! let expected_analysis_result = "image/png; charset=binary";
82//! assert_eq!(cookie.file(file_to_analyze)?, expected_analysis_result);
83//! # Ok(())
84//! # }
85//! ```
86//!
87//! See [`magic::cookie::Flags::MIME`](crate::cookie::Flags::MIME).
88//!
89//! # Filename extensions example
90//!
91//! Return slash-separated filename extensions (the ".png" in "example.png")
92//! from file contents (the input filename is not used for detection):
93//!
94//! ```shell
95//! $ file --extension data/tests/rust-logo-128x128-blk.png
96//! data/tests/rust-logo-128x128-blk.png: png
97//! ```
98//! ```rust
99//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
100//! # use std::convert::TryInto;
101//! // open a new configuration with flags for filename extension
102//! let flags = magic::cookie::Flags::EXTENSION;
103//! let cookie = magic::Cookie::open(flags)?;
104//!
105//! // load a specific database
106//! let database = ["data/tests/db-images-png"].try_into()?;
107//! let cookie = cookie.load(&database)?;
108//!
109//! // analyze a test file
110//! let file_to_analyze = "data/tests/rust-logo-128x128-blk.png";
111//! let expected_analysis_result = "png";
112//! assert_eq!(cookie.file(file_to_analyze)?, expected_analysis_result);
113//! # Ok(())
114//! # }
115//! ```
116//!
117//! See [`magic::cookie::Flags::EXTENSION`](crate::cookie::Flags::EXTENSION).
118//!
119//! # Further reading
120//!
121//! * [`Cookie::open()`][Cookie::open]
122//! * cookie [`Flags`](crate::cookie::Flags), in particular:
123//!     * [`Flags::ERROR`](crate::cookie::Flags::ERROR)
124//!     * [`Flags::MIME`](crate::cookie::Flags::MIME)
125//!     * [`Flags::EXTENSION`](crate::cookie::Flags::EXTENSION)
126//!     * [`Flags::CONTINUE`](crate::cookie::Flags::CONTINUE)
127//!     * [`Flags::NO_CHECK_BUILTIN`](crate::cookie::Flags::NO_CHECK_BUILTIN)
128//! * [`Cookie::load()`](Cookie::load), [`Cookie::load_buffers()`](Cookie::load_buffers)
129//! * [`Cookie::file()`](Cookie::file), [`Cookie::buffer()`](Cookie::buffer)
130//!
131//! Note that while some `libmagic` functions return somewhat structured text, e.g. MIME types and file extensions,
132//! the `magic` crate does not attempt to parse them into Rust data types since the format is not guaranteed by the C FFI API.
133//!
134//! Check the [crate README](https://crates.io/crates/magic) for required dependencies and MSRV.
135//!
136//! # Features
137//!
138//! ## `libmagic` API features
139//! - `libmagic+v5-40` — Enable `libmagic` v5.40 API
140//! - `libmagic+v5-44` — Enable `libmagic` v5.44 API
141//! - `libmagic+v5-45` — Enable `libmagic` v5.45 API
142//!
143//! # Safety
144//!
145//! This crate is a binding to the `libmagic` C library and as such subject to its security problems.
146//! Please note that `libmagic` has several CVEs, listed on e.g. [Repology](https://repology.org/project/file/cves).
147//! Make sure that you are using an up-to-date version of `libmagic` and ideally
148//! add additional security layers such as sandboxing (which this crate does _not_ provide)
149//! and __do not use it on untrusted input__ e.g. from users on the internet!
150//!
151//! The Rust code of this crate needs to use some `unsafe` for interacting with the `libmagic` C FFI.
152//!
153//! This crate has not been audited nor is it ready for production use.
154//!
155//! This Rust project / crate is not affiliated with the original `file` / `libmagic` C project.
156//!
157//! # Use cases
158//!
159//! `libmagic` can help to identify unknown content. It does this by looking at byte patterns, among other things.
160//! This does not guarantee that e.g. a file which is detected as a PNG image is indeed a valid PNG image.
161//!
162//! Maybe you just want a mapping from file name extensions to MIME types instead, e.g. ".png" ↔ "image/png"?
163//! In this case you do not even need to look at file contents and could use e.g. the [`mime_guess` crate](https://crates.io/crates/mime_guess).
164//!
165//! Maybe you want to be certain that a file is valid for a kown format, e.g. a PNG image?
166//! In this case you should use a parser for that format specifically, e.g. the [`image` crate](https://crates.io/crates/image).
167//!
168//! Maybe you want to know if a file contains other, malicious content?
169//! In this case you should use an anti-virus software, e.g. [ClamAV](https://www.clamav.net/), [Virus Total](https://www.virustotal.com/).
170//!
171//! Maybe you need to support platforms that are tricky with the `libmagic` C library, e.g. wasm32?
172//! In this case you could use pure Rust alternatives, e.g. the [`file_type` crate](https://crates.io/crates/file_type).
173
174#![cfg_attr(docsrs, feature(doc_cfg))]
175#![cfg_attr(docsrs, doc(auto_cfg(hide(docsrs))))]
176#![deny(unsafe_code)]
177
178use core::ffi::c_int;
179
180mod ffi;
181
182/// Returns the version of the `libmagic` C library as reported by itself.
183///
184/// # Examples
185/// A version of "5.41" is returned as `541`.
186#[doc(alias = "magic_version")]
187pub fn libmagic_version() -> c_int {
188    crate::ffi::version()
189}
190
191/// Functionality for [`Cookie`]
192pub mod cookie {
193    use std::convert::TryFrom;
194    use std::ffi::{c_int, CString};
195    use std::path::Path;
196
197    use magic_sys as libmagic;
198
199    bitflags::bitflags! {
200        /// Configuration bits for [`Cookie`]
201        ///
202        /// A bitflags instance is a combined set of individual flags.
203        /// `cookie::Flags` are configuration bits for `Cookie` instances that specify how the cookie should behave.
204        ///
205        /// `cookie::Flags` influence several functions, e.g. [`Cookie::file()`](Cookie::file)
206        /// but also [`Cookie::load()`](Cookie::load).
207        ///
208        /// Flags are initially set when a new cookie is created with [`Cookie::open()`](Cookie::open)
209        /// and can be overwritten lateron with [`Cookie::set_flags()`](Cookie::set_flags).
210        ///
211        /// Flags of particular interest:
212        /// - [`ERROR`](Flags::ERROR)
213        /// - [`MIME`](Flags::MIME)
214        /// - [`EXTENSION`](Flags::EXTENSION)
215        /// - [`CONTINUE`](Flags::CONTINUE)
216        /// - [`NO_CHECK_BUILTIN`](Flags::NO_CHECK_BUILTIN)
217        ///
218        /// # Examples
219        ///
220        /// ```
221        /// // default flags
222        /// // `: Flags` type annotation is only needed for this example
223        /// // if you pass it to Cookie::open() etc., Rust will figure it out
224        /// let flags: magic::cookie::Flags = Default::default();
225        ///
226        /// // custom flags combination
227        /// let flags = magic::cookie::Flags::COMPRESS | magic::cookie::Flags::DEVICES;
228        /// ```
229        ///
230        /// # Errors
231        ///
232        /// Some flags might not be supported on all platforms, i.e.
233        /// - [`Cookie::open()`](Cookie::open) might return a [`cookie::OpenError`](OpenError)
234        /// - [`Cookie::set_flags()`](Cookie::set_flags) might return a [`cookie::SetFlagsError`](SetFlagsError)
235        ///
236        /// # Misc
237        ///
238        /// NOTE: The flag descriptions are mostly copied from `man libmagic 3`.
239        ///
240        /// `MAGIC_NONE` is the default, meaning "No special handling" and has no constant.
241        /// ```
242        /// let default_flags: magic::cookie::Flags = Default::default();
243        /// assert_eq!(default_flags, magic::cookie::Flags::empty());
244        /// ```
245        #[derive(std::default::Default, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone, Copy)]
246        pub struct Flags: c_int {
247            // MAGIC_NONE is 0/default, see https://docs.rs/bitflags/latest/bitflags/#zero-bit-flags
248
249            // Define unnamed flag for all other bits https://docs.rs/bitflags/latest/bitflags/#externally-defined-flags
250            const _                 = !0;
251
252            /// Print debugging messages to `stderr`
253            ///
254            /// This is equivalent to the `file` CLI option `--debug`.
255            ///
256            /// NOTE: Those messages are printed by `libmagic` itself, no this Rust crate.
257            #[doc(alias = "MAGIC_DEBUG")]
258            #[doc(alias = "--debug")]
259            const DEBUG             = libmagic::MAGIC_DEBUG;
260
261            /// If the file queried is a symlink, follow it
262            ///
263            /// This is equivalent to the `file` CLI option `--dereference`.
264            #[doc(alias = "MAGIC_SYMLINK")]
265            #[doc(alias = "--dereference")]
266            const SYMLINK           = libmagic::MAGIC_SYMLINK;
267
268            /// If the file is compressed, unpack it and look at the contents
269            ///
270            /// This is equivalent to the `file` CLI option `--uncompress`.
271            #[doc(alias = "MAGIC_COMPRESS")]
272            #[doc(alias = "--uncompress")]
273            const COMPRESS          = libmagic::MAGIC_COMPRESS;
274
275            /// If the file is a block or character special device, then open the device and try to look in its contents
276            ///
277            /// This is equivalent to the `file` CLI option `--special-files`.
278            #[doc(alias = "MAGIC_DEVICES")]
279            #[doc(alias = "--special-files")]
280            const DEVICES           = libmagic::MAGIC_DEVICES;
281
282            /// Return a MIME type string, instead of a textual description
283            ///
284            /// See also: [`Flags::MIME`]
285            ///
286            /// This is equivalent to the `file` CLI option `--mime-type`.
287            ///
288            /// NOTE: `libmagic` uses non-standard MIME types for at least some built-in checks,
289            /// e.g. `inode/*` (also see [`Flags::SYMLINK`], [`Flags::DEVICES`]):
290            /// ```shell
291            /// $ file --mime-type /proc/self/exe
292            /// /proc/self/exe: inode/symlink
293            ///
294            /// $ file --mime-type /dev/sda
295            /// /dev/sda: inode/blockdevice
296            /// ```
297            #[doc(alias = "MAGIC_MIME_TYPE")]
298            #[doc(alias = "--mime-type")]
299            const MIME_TYPE         = libmagic::MAGIC_MIME_TYPE;
300
301            /// Return all matches, not just the first
302            ///
303            /// This is equivalent to the `file` CLI option `--keep-going`.
304            #[doc(alias = "MAGIC_CONTINUE")]
305            #[doc(alias = "--keep-going")]
306            const CONTINUE          = libmagic::MAGIC_CONTINUE;
307
308            /// Check the magic database for consistency and print warnings to `stderr`
309            ///
310            /// NOTE: Those warnings are printed by `libmagic` itself, no this Rust crate.
311            #[doc(alias = "MAGIC_CHECK")]
312            const CHECK             = libmagic::MAGIC_CHECK;
313
314            /// On systems that support `utime(2)` or `utimes(2)`, attempt to preserve the access time of files analyzed
315            ///
316            /// This is equivalent to the `file` CLI option `--preserve-date`.
317            #[doc(alias = "MAGIC_PRESERVE_ATIME")]
318            #[doc(alias = "--preserve-date")]
319            const PRESERVE_ATIME    = libmagic::MAGIC_PRESERVE_ATIME;
320
321            /// Don't translate unprintable characters to a `\\ooo` octal representation
322            ///
323            /// This is equivalent to the `file` CLI option `--raw`.
324            #[doc(alias = "MAGIC_RAW")]
325            #[doc(alias = "--raw")]
326            const RAW               = libmagic::MAGIC_RAW;
327
328            /// Treat operating system errors while trying to open files and follow symlinks as real errors, instead of printing them in the magic buffer
329            #[doc(alias = "MAGIC_ERROR")]
330            const ERROR             = libmagic::MAGIC_ERROR;
331
332            /// Return a MIME encoding, instead of a textual description
333            ///
334            /// See also: [`Flags::MIME`]
335            ///
336            /// This is equivalent to the `file` CLI option `--mime-encoding`.
337            ///
338            /// NOTE: `libmagic` uses non-standard MIME `charset` values, e.g. for binary files:
339            /// ```shell
340            /// $ file --mime-encoding /proc/self/exe
341            /// binary
342            /// ```
343            #[doc(alias = "MAGIC_MIME_ENCODING")]
344            #[doc(alias = "--mime-encoding")]
345            const MIME_ENCODING     = libmagic::MAGIC_MIME_ENCODING;
346
347            /// A shorthand for `MIME_TYPE | MIME_ENCODING`
348            ///
349            /// See also: [`Flags::MIME_TYPE`], [`Flags::MIME_ENCODING`]
350            ///
351            /// This is equivalent to the `file` CLI option `--mime`.
352            ///
353            /// NOTE: `libmagic` returns a parseable MIME type with a `charset` field:
354            /// ```shell
355            /// $ file --mime /proc/self/exe
356            /// /proc/self/exe: inode/symlink; charset=binary
357            /// ```
358            #[doc(alias = "MAGIC_MIME")]
359            #[doc(alias = "--mime")]
360            const MIME              = Self::MIME_TYPE.bits()
361                                    | Self::MIME_ENCODING.bits();
362
363            /// Return the Apple creator and type
364            ///
365            /// This is equivalent to the `file` CLI option `--apple`.
366            #[doc(alias = "MAGIC_APPLE")]
367            #[doc(alias = "--apple")]
368            const APPLE             = libmagic::MAGIC_APPLE;
369
370            /// Return a slash-separated list of extensions for this file type
371            ///
372            /// This is equivalent to the `file` CLI option `--extension`.
373            ///
374            /// NOTE: `libmagic` returns a list with one or more extensions without a leading "." (dot):
375            /// ```shell
376            /// $ file --extension example.jpg
377            /// example.jpg: jpeg/jpg/jpe/jfif
378            ///
379            /// $ file --extension /proc/self/exe
380            /// /proc/self/exe: ???
381            /// ```
382            #[doc(alias = "MAGIC_EXTENSION")]
383            #[doc(alias = "--extension")]
384            const EXTENSION         = libmagic::MAGIC_EXTENSION;
385
386            /// Don't report on compression, only report about the uncompressed data
387            ///
388            /// This is equivalent to the `file` CLI option `--uncompress-noreport`.
389            #[doc(alias = "MAGIC_COMPRESS_TRANSP")]
390            #[doc(alias = "--uncompress-noreport")]
391            const COMPRESS_TRANSP   = libmagic::MAGIC_COMPRESS_TRANSP;
392
393            /// A shorthand for `EXTENSION | MIME | APPLE`
394            #[doc(alias = "MAGIC_NODESC")]
395            const NODESC            = Self::EXTENSION.bits()
396                                    | Self::MIME.bits()
397                                    | Self::APPLE.bits();
398
399            /// Don't look inside compressed files
400            ///
401            /// This is equivalent to the `file` CLI option `--exclude compress`.
402            #[doc(alias = "MAGIC_NO_CHECK_COMPRESS")]
403            #[doc(alias = "--exclude compress")]
404            const NO_CHECK_COMPRESS = libmagic::MAGIC_NO_CHECK_COMPRESS;
405
406            /// Don't examine tar files
407            ///
408            /// This is equivalent to the `file` CLI option `--exclude tar`.
409            #[doc(alias = "MAGIC_NO_CHECK_TAR")]
410            #[doc(alias = "--exclude tar")]
411            const NO_CHECK_TAR      = libmagic::MAGIC_NO_CHECK_TAR;
412
413            /// Don't consult magic files
414            ///
415            /// This is equivalent to the `file` CLI option `--exclude soft`.
416            #[doc(alias = "MAGIC_NO_CHECK_SOFT")]
417            #[doc(alias = "--exclude soft")]
418            const NO_CHECK_SOFT     = libmagic::MAGIC_NO_CHECK_SOFT;
419
420            /// Check for EMX application type (only on EMX)
421            ///
422            /// This is equivalent to the `file` CLI option `--exclude apptype`.
423            #[doc(alias = "MAGIC_NO_CHECK_APPTYPE")]
424            #[doc(alias = "--exclude apptype")]
425            const NO_CHECK_APPTYPE  = libmagic::MAGIC_NO_CHECK_APPTYPE;
426
427            /// Don't print ELF details
428            ///
429            /// This is equivalent to the `file` CLI option `--exclude elf`.
430            #[doc(alias = "MAGIC_NO_CHECK_ELF")]
431            #[doc(alias = "--exclude elf")]
432            const NO_CHECK_ELF      = libmagic::MAGIC_NO_CHECK_ELF;
433
434            /// Don't check for various types of text files
435            ///
436            /// This is equivalent to the `file` CLI option `--exclude text`.
437            #[doc(alias = "MAGIC_NO_CHECK_TEXT")]
438            #[doc(alias = "--exclude text")]
439            const NO_CHECK_TEXT     = libmagic::MAGIC_NO_CHECK_TEXT;
440
441            /// Don't get extra information on MS Composite Document Files
442            ///
443            /// This is equivalent to the `file` CLI option `--exclude cdf`.
444            #[doc(alias = "MAGIC_NO_CHECK_CDF")]
445            #[doc(alias = "--exclude cdf")]
446            const NO_CHECK_CDF      = libmagic::MAGIC_NO_CHECK_CDF;
447
448            /// Don't examine CSV files
449            ///
450            /// This is equivalent to the `file` CLI option `--exclude csv`.
451            #[doc(alias = "MAGIC_NO_CHECK_CSV")]
452            #[doc(alias = "--exclude csv")]
453            const NO_CHECK_CSV      = libmagic::MAGIC_NO_CHECK_CSV;
454
455            /// Don't look for known tokens inside ascii files
456            ///
457            /// This is equivalent to the `file` CLI option `--exclude tokens`.
458            #[doc(alias = "MAGIC_NO_CHECK_TOKENS")]
459            #[doc(alias = "--exclude tokens")]
460            const NO_CHECK_TOKENS   = libmagic::MAGIC_NO_CHECK_TOKENS;
461
462            /// Don't check text encodings
463            ///
464            /// This is equivalent to the `file` CLI option `--exclude encoding`.
465            #[doc(alias = "MAGIC_NO_CHECK_ENCODING")]
466            #[doc(alias = "--exclude encoding")]
467            const NO_CHECK_ENCODING = libmagic::MAGIC_NO_CHECK_ENCODING;
468
469            /// Don't examine JSON files
470            ///
471            /// This is equivalent to the `file` CLI option `--exclude json`.
472            #[doc(alias = "MAGIC_NO_CHECK_JSON")]
473            #[doc(alias = "--exclude json")]
474            const NO_CHECK_JSON     = libmagic::MAGIC_NO_CHECK_JSON;
475
476            #[cfg(feature = "libmagic+v5-44")]
477            #[cfg_attr(docsrs, doc(cfg(feature = "libmagic+v5-44")))]
478            /// Don't allow decompressors that use fork
479            ///
480            /// The enabled-by-default sandboxing of the `file` CLI implicitly activates this flag if available.
481            #[doc(alias = "MAGIC_NO_COMPRESS_FORK")]
482            const NO_COMPRESS_FORK  = libmagic::MAGIC_NO_COMPRESS_FORK;
483
484            #[cfg(feature = "libmagic+v5-45")]
485            #[cfg_attr(docsrs, doc(cfg(feature = "libmagic+v5-45")))]
486            ///  Don't examine SIMH tape files
487            ///
488            /// This is equivalent to the `file` CLI option `--exclude simh`.
489            #[doc(alias = "MAGIC_NO_CHECK_SIMH")]
490            const NO_CHECK_SIMH     = libmagic::MAGIC_NO_CHECK_SIMH;
491
492            /// No built-in tests; only consult the magic file
493            #[doc(alias = "MAGIC_NO_CHECK_BUILTIN")]
494            #[cfg(not(feature = "libmagic+v5-45"))]
495            #[cfg_attr(docsrs, doc(auto_cfg = false))]
496            const NO_CHECK_BUILTIN  = Self::NO_CHECK_COMPRESS.bits()
497                                    | Self::NO_CHECK_TAR.bits()
498                                    | Self::NO_CHECK_APPTYPE.bits()
499                                    | Self::NO_CHECK_ELF.bits()
500                                    | Self::NO_CHECK_TEXT.bits()
501                                    | Self::NO_CHECK_CSV.bits()
502                                    | Self::NO_CHECK_CDF.bits()
503                                    | Self::NO_CHECK_TOKENS.bits()
504                                    | Self::NO_CHECK_ENCODING.bits()
505                                    | Self::NO_CHECK_JSON.bits();
506            /// No built-in tests; only consult the magic file
507            #[cfg(feature = "libmagic+v5-45")]
508            #[cfg_attr(docsrs, doc(auto_cfg = false))]
509            const NO_CHECK_BUILTIN  = Self::NO_CHECK_COMPRESS.bits()
510                                    | Self::NO_CHECK_TAR.bits()
511                                    | Self::NO_CHECK_APPTYPE.bits()
512                                    | Self::NO_CHECK_ELF.bits()
513                                    | Self::NO_CHECK_TEXT.bits()
514                                    | Self::NO_CHECK_CSV.bits()
515                                    | Self::NO_CHECK_CDF.bits()
516                                    | Self::NO_CHECK_TOKENS.bits()
517                                    | Self::NO_CHECK_ENCODING.bits()
518                                    | Self::NO_CHECK_JSON.bits()
519                                    | Self::NO_CHECK_SIMH.bits();
520        }
521    }
522
523    impl std::fmt::Display for Flags {
524        fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
525            bitflags::parser::to_writer(self, f)
526        }
527    }
528
529    /// Invalid [`DatabasePaths`]
530    ///
531    /// This is returned from [`DatabasePaths::new()`](DatabasePaths::new)
532    #[derive(Debug)]
533    pub struct InvalidDatabasePathError {}
534
535    impl std::error::Error for InvalidDatabasePathError {}
536
537    impl std::fmt::Display for InvalidDatabasePathError {
538        fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
539            write!(f, "invalid database files path")
540        }
541    }
542
543    /// Magic database file paths
544    ///
545    /// `libmagic` requires database file paths for certain operations on a [`Cookie`] that must:
546    /// - be a valid C string
547    /// - not contain ":" (colon), since that is used to separate multiple file paths (on all platforms)
548    ///
549    /// Those operations are [`Cookie::load()`](Cookie::load), [`Cookie::compile()`](Cookie::compile), [`Cookie::check()`](Cookie::check), [`Cookie::list()`](Cookie::list).\
550    /// [`Cookie::file()`](Cookie::file) does not take database file paths but the single file to inspect instead.
551    ///
552    /// The default unnamed database can be constructed with [`Default::default()`](DatabasePaths::default).
553    /// Explicit paths can be constructed manually with [`new()`](DatabasePaths::new) or by fallible conversion from an array, slice or Vec
554    /// containing something convertible as [`std::path::Path`], or a single something.
555    ///
556    /// Note that this only ensures the paths themselves are valid.
557    /// Operating on those database file paths can still fail,
558    /// for example if they refer to files that do not exist, can not be opened or do not have the required format.
559    ///
560    /// # Examples
561    ///
562    /// ```
563    /// # use std::convert::TryInto;
564    /// # use magic::cookie::DatabasePaths;
565    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
566    /// // `: DatabasePaths` type annotation is only needed for these examples
567    /// // if you pass it to Cookie::load() etc., Rust will figure it out
568    ///
569    /// // construct default unnamed database paths
570    /// let database: DatabasePaths = Default::default();
571    ///
572    /// // construct from single path
573    /// let database: DatabasePaths = "first-directory/first-database".try_into()?;
574    /// let database: DatabasePaths =
575    ///     std::path::Path::new("second-directory/second-database").try_into()?;
576    ///
577    /// // construct from multiple paths in array
578    /// let array: [&'static str; 2] = [
579    ///     "first-directory/first-database",
580    ///     "second-directory/second-database",
581    /// ];
582    /// let database: DatabasePaths = array.try_into()?;
583    ///
584    /// // construct from multiple paths in slice
585    /// let database: DatabasePaths = [
586    ///     "first-directory/first-database".as_ref(),
587    ///     std::path::Path::new("second-directory/second-database"),
588    /// ]
589    /// .try_into()?;
590    ///
591    /// // construct from multiple paths in Vec
592    /// let database: DatabasePaths = vec![
593    ///     std::ffi::OsStr::new("first-directory/first-database"),
594    ///     "second-directory/second-database".as_ref(),
595    /// ]
596    /// .try_into()?;
597    /// # Ok(())
598    /// # }
599    /// ```
600    pub struct DatabasePaths {
601        filenames: Option<CString>,
602    }
603
604    const DATABASE_FILENAME_SEPARATOR: &str = ":";
605
606    impl DatabasePaths {
607        /// Create a new database paths instance
608        ///
609        /// Using one of the `TryFrom` implementations is recommended instead, see [`DatabasePaths`] examples.
610        ///
611        /// Empty `paths` returns [`Default::default()`](DatabasePaths::default).
612        ///
613        /// # Errors
614        ///
615        /// If the `paths` contain a ":" (colon), a [`cookie::InvalidDatabasePathError`](InvalidDatabasePathError) will be returned.
616        ///
617        pub fn new<I, P>(paths: I) -> Result<Self, InvalidDatabasePathError>
618        where
619            I: IntoIterator<Item = P>,
620            P: AsRef<Path>,
621        {
622            // this is not the most efficient nor correct for Windows, but consistent with previous behaviour
623
624            let filename = paths
625                .into_iter()
626                .map(|f| f.as_ref().to_string_lossy().into_owned())
627                .collect::<Vec<String>>()
628                .join(DATABASE_FILENAME_SEPARATOR);
629
630            Ok(Self {
631                filenames: match filename.is_empty() {
632                    true => None,
633                    _ => Some(CString::new(filename).map_err(|_| InvalidDatabasePathError {})?),
634                },
635            })
636        }
637    }
638
639    impl Default for DatabasePaths {
640        /// Returns the path for the default unnamed database/s
641        ///
642        /// Note that the default database/s can be overwritten by setting the "MAGIC" environment variable
643        /// to a colon-separated text of database file paths:
644        /// ```shell
645        /// $ export MAGIC='data/tests/db-python:data/tests/db-images-png-precompiled.mgc'
646        /// $ # file-ish uses `DatabasePaths::default()`
647        /// $ cargo run --example file-ish -- data/tests/rust-logo-128x128-blk.png
648        /// ```
649        /// This is a feature of `libmagic` itself, not of this Rust crate.
650        ///
651        /// Note that the `file` CLI (which uses `libmagic`) prints the location of its default database with:
652        /// ```shell
653        /// $ file --version
654        /// file-5.38
655        /// magic file from /etc/magic:/usr/share/misc/magic
656        ///
657        /// $ export MAGIC='data/tests/db-python:data/tests/db-images-png-precompiled.mgc'
658        /// $ file --version
659        /// file-5.39
660        /// magic file from data/tests/db-python:data/tests/db-images-png-precompiled.mgc
661        /// ```
662        fn default() -> Self {
663            Self { filenames: None }
664        }
665    }
666
667    impl<P: AsRef<std::path::Path>, const N: usize> TryFrom<[P; N]> for DatabasePaths {
668        type Error = InvalidDatabasePathError;
669
670        /// Invokes [`DatabasePaths::new()`](DatabasePaths::new)
671        fn try_from(value: [P; N]) -> Result<Self, <Self as TryFrom<[P; N]>>::Error> {
672            Self::new(value)
673        }
674    }
675
676    impl<P: AsRef<std::path::Path>> TryFrom<Vec<P>> for DatabasePaths {
677        type Error = InvalidDatabasePathError;
678
679        /// Invokes [`DatabasePaths::new()`](DatabasePaths::new)
680        fn try_from(value: Vec<P>) -> Result<Self, <Self as TryFrom<Vec<P>>>::Error> {
681            Self::new(value)
682        }
683    }
684
685    impl<P: AsRef<std::path::Path>> TryFrom<&'_ [P]> for DatabasePaths {
686        type Error = InvalidDatabasePathError;
687
688        /// Invokes [`DatabasePaths::new()`](DatabasePaths::new)
689        fn try_from(value: &[P]) -> Result<Self, <Self as TryFrom<&[P]>>::Error> {
690            Self::new(value)
691        }
692    }
693
694    macro_rules! databasepaths_try_from_impl {
695        ($t:ty) => {
696            impl TryFrom<$t> for DatabasePaths {
697                type Error = InvalidDatabasePathError;
698
699                /// Invokes [`DatabasePaths::new()`](DatabasePaths::new)
700                fn try_from(value: $t) -> Result<Self, <Self as TryFrom<$t>>::Error> {
701                    DatabasePaths::new(std::iter::once(value))
702                }
703            }
704        };
705    }
706
707    // missing for now are:
708    // - Cow<'_, OsStr>
709    // - std::path::Component<'_>
710    // - std::path::Components<'_>
711    // - std::path::Iter<'_>
712    databasepaths_try_from_impl!(&str);
713    databasepaths_try_from_impl!(&std::ffi::OsStr);
714    databasepaths_try_from_impl!(std::ffi::OsString);
715    databasepaths_try_from_impl!(&std::path::Path);
716    databasepaths_try_from_impl!(std::path::PathBuf);
717    databasepaths_try_from_impl!(String);
718
719    /// Error within several [`Cookie`] functions
720    ///
721    /// Most functions on a [`Cookie`] can return an error from `libmagic`,
722    /// which unfortunately is not very structured.
723    #[derive(Debug)]
724    pub struct Error {
725        function: &'static str,
726        //#[backtrace]
727        source: crate::ffi::CookieError,
728    }
729
730    impl std::error::Error for Error {
731        fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
732            Some(&self.source)
733        }
734    }
735
736    impl std::fmt::Display for Error {
737        fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
738            let Self { function, .. } = self;
739            write!(f, "magic cookie error in `libmagic` function {0}", function,)
740        }
741    }
742
743    /// Typestate marker of opened [`Cookie`]
744    ///
745    /// In this state, a cookie has only been [opened](Cookie::open) but databases are not [loaded](Cookie::load) yet.
746    #[derive(Debug)]
747    pub enum Open {}
748
749    /// Typestate marker of loaded [`Cookie`]
750    ///
751    /// In this state, a cookie has both been [opened](Cookie::open) and databases are [loaded](Cookie::load).
752    #[derive(Debug)]
753    pub enum Load {}
754
755    mod private {
756        pub trait Sealed {}
757
758        impl Sealed for super::Open {}
759        impl Sealed for super::Load {}
760    }
761
762    /// Typestate marker for [`Cookie`]
763    ///
764    /// A cookie can either be in the [`Open`] or [`Load`] state
765    pub trait State: private::Sealed {}
766
767    impl State for Open {}
768    impl State for Load {}
769
770    /// Combined configuration of [`Flags`], parameters and databases
771    ///
772    /// A "cookie" is `libmagic` lingo for a combined configuration of
773    /// - [`cookie::Flags`](crate::cookie::Flags)
774    /// - parameters (not implemented yet)
775    /// - loaded datbases, e.g. [`cookie::DatabasePaths`](crate::cookie::DatabasePaths)
776    ///
777    /// A cookie advances through 2 states: opened, then loaded.
778    /// ```
779    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
780    /// // create new cookie in initial opened state with given flags
781    /// let cookie = magic::Cookie::open(magic::cookie::Flags::default())?;
782    ///
783    /// // advance cookie into loaded state
784    /// let cookie = cookie.load(&magic::cookie::DatabasePaths::default())?;
785    /// # Ok(())
786    /// # }
787    /// ```
788    ///
789    /// In either state, you can use operations that do not require
790    /// already loaded magic databases:
791    /// - [`Cookie::load()`](Cookie::load), [`Cookie::load_buffers()`](Cookie::load_buffers) to load databases and transition into the loaded state
792    /// - [`Cookie::set_flags()`](Cookie::set_flags) to overwrite the initial flags given in [`Cookie::open()`](Cookie::open)
793    /// - [`Cookie::compile()`](Cookie::compile), [`Cookie::check()`](Cookie::check), [`Cookie::list()`](Cookie::list) to operate on magic database files
794    ///
795    /// Once in the loaded state, you can perform magic "queries":
796    /// - [`Cookie::file()`](Cookie::file), [`Cookie::buffer()`](Cookie::buffer)
797    #[derive(Debug)]
798    #[doc(alias = "magic_t")]
799    #[doc(alias = "magic_set")]
800    pub struct Cookie<S: State> {
801        cookie: crate::ffi::Cookie,
802        marker: std::marker::PhantomData<S>,
803    }
804
805    /// Error within [`Cookie::load()`](Cookie::load) or [`Cookie::load_buffers()`](Cookie::load_buffers)
806    ///
807    /// This is like [`cookie:Error`](Error) but also has the cookie in its original state.
808    ///
809    /// # Examples
810    ///
811    /// ```
812    /// # use std::convert::TryInto;
813    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
814    /// let cookie = magic::Cookie::open(Default::default())?;
815    /// let database = "data/tests/db-images-png".try_into()?;
816    /// // try to load an existing database, consuming and returning early
817    /// let cookie = cookie.load(&database)?;
818    ///
819    /// let database = "doesntexist.mgc".try_into()?;
820    /// // load a database that does not exist
821    /// let cookie = match cookie.load(&database) {
822    ///     Err(err) => {
823    ///         println!("whoopsie: {:?}", err);
824    ///         // recover the loaded cookie without dropping it
825    ///         err.cookie()
826    ///     },
827    ///     Ok(cookie) => cookie,
828    /// };
829    ///
830    /// let database = "data/tests/db-python".try_into()?;
831    /// // try to load another existing database
832    /// let cookie = cookie.load(&database)?;
833    /// # Ok(())
834    /// # }
835    /// ```
836    #[derive(Debug)]
837    pub struct LoadError<S: State> {
838        function: &'static str,
839        //#[backtrace]
840        source: crate::ffi::CookieError,
841        cookie: Cookie<S>,
842    }
843
844    impl<S: State> std::error::Error for LoadError<S>
845    where
846        Self: std::fmt::Debug,
847    {
848        fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
849            Some(&self.source)
850        }
851    }
852
853    impl<S: State> std::fmt::Display for LoadError<S> {
854        fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
855            let Self { function, .. } = self;
856            write!(f, "magic cookie error in `libmagic` function {0}", function,)
857        }
858    }
859
860    impl<S: State> LoadError<S> {
861        /// Returns the cookie in its original state
862        pub fn cookie(self) -> Cookie<S> {
863            self.cookie
864        }
865    }
866
867    impl<S: State> Drop for Cookie<S> {
868        /// Closes the loaded magic database files and deallocates any resources used
869        #[doc(alias = "magic_close")]
870        fn drop(&mut self) {
871            crate::ffi::close(&mut self.cookie);
872        }
873    }
874
875    /// Operations that are valid in the [`Open`] state
876    ///
877    /// A new cookie created with [`Cookie::open`](Cookie::open) does not have any databases [loaded](Cookie::load).
878    impl Cookie<Open> {
879        /// Creates a new configuration cookie, `flags` specify how other operations on this cookie should behave
880        ///
881        /// This does not [`load()`](Cookie::load) any databases yet.
882        ///
883        /// The `flags` can be changed lateron with [`set_flags()`](Cookie::set_flags).
884        ///
885        /// # Examples
886        ///
887        /// ```
888        /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
889        /// // open a new cookie with default flags
890        /// let cookie = magic::Cookie::open(Default::default())?;
891        ///
892        /// // open a new cookie with custom flags
893        /// let flags = magic::cookie::Flags::COMPRESS | magic::cookie::Flags::DEVICES;
894        /// let cookie = magic::Cookie::open(flags)?;
895        /// # Ok(())
896        /// # }
897        /// ```
898        ///
899        /// # Errors
900        ///
901        /// If there was an `libmagic` internal error allocating a new cookie, a [`cookie::OpenError`](OpenError) will be returned.
902        ///
903        /// If the given `flags` are unsupported on the current platform, a [`cookie::OpenError`](OpenError) will be returned.
904        #[doc(alias = "magic_open")]
905        pub fn open(flags: Flags) -> Result<Cookie<Open>, OpenError> {
906            match crate::ffi::open(flags.bits()) {
907                Err(err) => Err(OpenError {
908                    flags,
909                    kind: match err.errno().kind() {
910                        std::io::ErrorKind::InvalidInput => OpenErrorKind::UnsupportedFlags,
911                        _ => OpenErrorKind::Errno,
912                    },
913                    source: err,
914                }),
915                Ok(cookie) => {
916                    let cookie = Cookie {
917                        cookie,
918                        marker: std::marker::PhantomData,
919                    };
920                    Ok(cookie)
921                }
922            }
923        }
924    }
925
926    /// Operations that are valid in the [`Load`] state
927    ///
928    /// An opened cookie with [loaded](Cookie::load) databases can inspect [files](Cookie::file) and [buffers](Cookie::buffer).
929    impl Cookie<Load> {
930        /// Returns a textual description of the contents of the file `filename`
931        ///
932        /// Requires to [`load()`](Cookie::load) databases before calling.
933        ///
934        /// # Examples
935        ///
936        /// ```
937        /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
938        /// // open a new cookie with default flags and database
939        /// let cookie = magic::Cookie::open(Default::default())?.load(&Default::default())?;
940        ///
941        /// let file_description = cookie.file("data/tests/rust-logo-128x128-blk.png");
942        /// # Ok(())
943        /// # }
944        /// ```
945        ///
946        /// # Errors
947        ///
948        /// If there was an `libmagic` internal error, a [`cookie::Error`](Error) will be returned.
949        ///
950        /// If `libmagic` violates its API contract, e.g. by not setting the last error, a [`cookie::Error`](Error) will be returned.
951        #[doc(alias = "magic_file")]
952        pub fn file<P: AsRef<Path>>(&self, filename: P) -> Result<String, Error> {
953            let c_string = CString::new(filename.as_ref().to_string_lossy().into_owned()).unwrap();
954            match crate::ffi::file(&self.cookie, c_string.as_c_str()) {
955                Ok(res) => Ok(res.to_string_lossy().to_string()),
956                Err(err) => Err(Error {
957                    function: "magic_file",
958                    source: err,
959                }),
960            }
961        }
962
963        /// Returns a textual description of the contents of the `buffer`
964        ///
965        /// Requires to [`load()`](Cookie::load) databases before calling.
966        ///
967        /// # Examples
968        ///
969        /// ```
970        /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
971        /// // open a new cookie with default flags and database
972        /// let cookie = magic::Cookie::open(Default::default())?.load(&Default::default())?;
973        ///
974        /// let buffer = b"%PDF-\xE2\x80\xA6";
975        /// let buffer_description = cookie.buffer(buffer);
976        /// # Ok(())
977        /// # }
978        /// ```
979        ///
980        /// # Errors
981        ///
982        /// If there was an `libmagic` internal error, a [`cookie::Error`](Error) will be returned.
983        ///
984        /// If `libmagic` violates its API contract, e.g. by not setting the last error, a [`cookie::Error`](Error) will be returned.
985        #[doc(alias = "magic_buffer")]
986        pub fn buffer(&self, buffer: &[u8]) -> Result<String, Error> {
987            match crate::ffi::buffer(&self.cookie, buffer) {
988                Ok(res) => Ok(res.to_string_lossy().to_string()),
989                Err(err) => Err(Error {
990                    function: "magic_buffer",
991                    source: err,
992                }),
993            }
994        }
995    }
996
997    /// Operations that are valid in any state
998    impl<S: State> Cookie<S> {
999        /// Loads the given database `filenames` for further queries
1000        ///
1001        /// Adds ".mgc" to the database filenames as appropriate.
1002        ///
1003        /// Calling `load()` or [`load_buffers()`](Cookie::load_buffers) replaces the previously loaded database/s.
1004        ///
1005        /// This is equivalent to the using the `file` CLI:
1006        /// ```shell
1007        /// $ file --magic-file 'data/tests/db-images-png:data/tests/db-python' --version
1008        /// file-5.39
1009        /// magic file from data/tests/db-images-png:data/tests/db-python
1010        /// ```
1011        ///
1012        /// # Examples
1013        /// ```rust
1014        /// # use std::convert::TryInto;
1015        /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1016        /// // open a new cookie with default flags
1017        /// let cookie = magic::Cookie::open(Default::default())?;
1018        ///
1019        /// // load the default unnamed database
1020        /// let database = Default::default();
1021        /// let cookie = cookie.load(&database)?;
1022        ///
1023        /// // load databases from files
1024        /// let databases = ["data/tests/db-images-png", "data/tests/db-python"].try_into()?;
1025        /// let cookie = cookie.load(&databases)?;
1026        ///
1027        /// // load precompiled database from file
1028        /// let database = "data/tests/db-images-png-precompiled.mgc".try_into()?;
1029        /// let cookie = cookie.load(&database)?;
1030        /// # Ok(())
1031        /// # }
1032        /// ```
1033        ///
1034        /// # Errors
1035        ///
1036        /// If there was an `libmagic` internal error, a [`cookie::LoadError`](LoadError) will be returned,
1037        /// which contains the cookie in its original state.
1038        ///
1039        /// If `libmagic` violates its API contract, e.g. by not setting the last error, a [`cookie::Error`](Error) will be returned.
1040        #[doc(alias = "magic_load")]
1041        #[doc(alias = "--magic-file")]
1042        pub fn load(self, filenames: &DatabasePaths) -> Result<Cookie<Load>, LoadError<S>> {
1043            match crate::ffi::load(&self.cookie, filenames.filenames.as_deref()) {
1044                Err(err) => Err(LoadError {
1045                    function: "magic_load",
1046                    source: err,
1047                    cookie: self,
1048                }),
1049                Ok(_) => {
1050                    let mut cookie = std::mem::ManuallyDrop::new(self);
1051
1052                    let cookie = Cookie {
1053                        cookie: crate::ffi::Cookie::new(&mut cookie.cookie),
1054                        marker: std::marker::PhantomData,
1055                    };
1056                    Ok(cookie)
1057                }
1058            }
1059        }
1060
1061        /// Loads the given compiled databases `buffers` for further queries
1062        ///
1063        /// Databases need to be compiled with a compatible `libmagic` version.
1064        ///
1065        /// This function can be used in environments where `libmagic` does
1066        /// not have direct access to the filesystem, but can access the magic
1067        /// database via shared memory or other IPC means.
1068        ///
1069        /// Calling `load_buffers()` or [`load()`](Cookie::load) replaces the previously loaded database/s.
1070        ///
1071        /// # Errors
1072        ///
1073        /// If there was an `libmagic` internal error, a [`cookie::LoadError`](LoadError) will be returned,
1074        /// which contains the cookie in its original state.
1075        ///
1076        /// If `libmagic` violates its API contract, e.g. by not setting the last error, a [`cookie::Error`](Error) will be returned.
1077        #[doc(alias = "magic_load_buffers")]
1078        pub fn load_buffers(self, buffers: &[&[u8]]) -> Result<Cookie<Load>, LoadError<S>> {
1079            match crate::ffi::load_buffers(&self.cookie, buffers) {
1080                Err(err) => Err(LoadError {
1081                    function: "magic_load_buffers",
1082                    source: err,
1083                    cookie: self,
1084                }),
1085                Ok(_) => {
1086                    let mut cookie = std::mem::ManuallyDrop::new(self);
1087
1088                    let cookie = Cookie {
1089                        cookie: crate::ffi::Cookie::new(&mut cookie.cookie),
1090                        marker: std::marker::PhantomData,
1091                    };
1092                    Ok(cookie)
1093                }
1094            }
1095        }
1096
1097        /// Sets the `flags` to use for this configuration
1098        ///
1099        /// Overwrites any previously set flags, e.g. those from [`load()`](Cookie::load).
1100        ///
1101        /// # Examples
1102        /// ```rust
1103        /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1104        /// // open a new cookie with initial default flags
1105        /// let cookie = magic::Cookie::open(Default::default())?;
1106        ///
1107        /// // overwrite the initial flags
1108        /// let flags = magic::cookie::Flags::COMPRESS | magic::cookie::Flags::DEVICES;
1109        /// cookie.set_flags(flags)?;
1110        /// # Ok(())
1111        /// # }
1112        /// ```
1113        ///
1114        /// # Errors
1115        ///
1116        /// If the given `flags` are unsupported on the current platform, an [`cookie::SetFlagsError`](SetFlagsError) will be returned.
1117        #[doc(alias = "magic_setflags")]
1118        pub fn set_flags(&self, flags: Flags) -> Result<(), SetFlagsError> {
1119            let ret = crate::ffi::setflags(&self.cookie, flags.bits());
1120            match ret {
1121                // according to `libmagic` man page this is the only flag that could be unsupported
1122                Err(err) => Err(SetFlagsError {
1123                    flags: Flags::PRESERVE_ATIME,
1124                    source: err,
1125                }),
1126                Ok(_) => Ok(()),
1127            }
1128        }
1129
1130        // TODO: check, compile, list and load mostly do the same, refactor!
1131
1132        /// Compiles the given database files `filenames` for faster access
1133        ///
1134        /// The compiled files created are named from the `basename` of each file argument with ".mgc" appended to it.
1135        ///
1136        /// This is equivalent to the following `file` CLI command:
1137        /// ```shell
1138        /// $ file --compile --magic-file data/tests/db-images-png:data/tests/db-python
1139        /// ```
1140        ///
1141        /// # Errors
1142        ///
1143        /// If there was an `libmagic` internal error, a [`cookie::Error`](Error) will be returned.
1144        ///
1145        /// If `libmagic` violates its API contract, e.g. by not setting the last error, a [`cookie::Error`](Error) will be returned.
1146        #[doc(alias = "magic_compile")]
1147        #[doc(alias = "--compile")]
1148        pub fn compile(&self, filenames: &DatabasePaths) -> Result<(), Error> {
1149            match crate::ffi::compile(&self.cookie, filenames.filenames.as_deref()) {
1150                Err(err) => Err(Error {
1151                    function: "magic_compile",
1152                    source: err,
1153                }),
1154                Ok(_) => Ok(()),
1155            }
1156        }
1157
1158        /// Checks the validity of entries in the database files `filenames`
1159        ///
1160        /// # Errors
1161        ///
1162        /// If there was an `libmagic` internal error, a [`cookie::Error`](Error) will be returned.
1163        ///
1164        /// If `libmagic` violates its API contract, e.g. by not setting the last error, a [`cookie::Error`](Error) will be returned.
1165        #[doc(alias = "magic_check")]
1166        pub fn check(&self, filenames: &DatabasePaths) -> Result<(), Error> {
1167            match crate::ffi::check(&self.cookie, filenames.filenames.as_deref()) {
1168                Err(err) => Err(Error {
1169                    function: "magic_check",
1170                    source: err,
1171                }),
1172                Ok(_) => Ok(()),
1173            }
1174        }
1175
1176        /// Dumps all magic entries in the given database files `filenames` in a human readable format
1177        ///
1178        /// This is equivalent to the following `file` CLI command:
1179        /// ```shell
1180        /// $ file --checking-printout --magic-file data/tests/db-images-png:data/tests/db-python
1181        /// ```
1182        ///
1183        /// # Errors
1184        ///
1185        /// If there was an `libmagic` internal error, a [`cookie::Error`](Error) will be returned.
1186        ///
1187        /// If `libmagic` violates its API contract, e.g. by not setting the last error, a [`cookie::Error`](Error) will be returned.
1188        #[doc(alias = "magic_list")]
1189        #[doc(alias = "--checking-printout")]
1190        pub fn list(&self, filenames: &DatabasePaths) -> Result<(), Error> {
1191            match crate::ffi::list(&self.cookie, filenames.filenames.as_deref()) {
1192                Err(err) => Err(Error {
1193                    function: "magic_list",
1194                    source: err,
1195                }),
1196                Ok(_) => Ok(()),
1197            }
1198        }
1199    }
1200
1201    /// Error within [`Cookie::open()`](Cookie::open)
1202    ///
1203    /// Note that a similar [`cookie::SetFlagsError`](SetFlagsError) can also occur
1204    #[derive(Debug)]
1205    pub struct OpenError {
1206        flags: Flags,
1207        kind: OpenErrorKind,
1208        //#[backtrace]
1209        source: crate::ffi::OpenError,
1210    }
1211
1212    impl std::error::Error for OpenError {
1213        fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1214            Some(&self.source)
1215        }
1216    }
1217
1218    impl std::fmt::Display for OpenError {
1219        fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1220            let Self { flags, kind, .. } = self;
1221            write!(
1222                f,
1223                "could not open magic cookie: {0}",
1224                match kind {
1225                    OpenErrorKind::UnsupportedFlags => format!("unsupported flags {0}", flags),
1226                    OpenErrorKind::Errno => "other error".to_string(),
1227                }
1228            )
1229        }
1230    }
1231
1232    /// Kind of [`OpenError`]
1233    #[derive(Debug)]
1234    enum OpenErrorKind {
1235        /// Unsupported flags given
1236        UnsupportedFlags,
1237        /// Other kind
1238        Errno,
1239    }
1240
1241    /// Error within [`Cookie::set_flags()`](Cookie::set_flags)
1242    ///
1243    /// Note that a similar [`cookie::OpenError`](OpenError) can also occur
1244    #[derive(Debug)]
1245    pub struct SetFlagsError {
1246        flags: Flags,
1247        //#[backtrace]
1248        source: crate::ffi::SetFlagsError,
1249    }
1250
1251    impl std::error::Error for SetFlagsError {
1252        fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1253            Some(&self.source)
1254        }
1255    }
1256
1257    impl std::fmt::Display for SetFlagsError {
1258        fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1259            let Self { flags, .. } = self;
1260            write!(f, "could not set magic cookie flags {0}", flags)
1261        }
1262    }
1263} // mod cookie
1264
1265pub use crate::cookie::Cookie;
1266
1267#[cfg(test)]
1268mod tests {
1269    use super::cookie::Flags;
1270    use super::cookie::{Error, InvalidDatabasePathError, LoadError, OpenError, SetFlagsError};
1271    use super::cookie::{Load, Open};
1272    use super::Cookie;
1273    use std::convert::TryInto;
1274
1275    fn assert_impl_debug<T: std::fmt::Debug>() {}
1276    fn assert_impl_display<T: std::fmt::Display>() {}
1277    fn assert_impl_error<T: std::error::Error>() {}
1278
1279    // Using relative paths to test files should be fine, since cargo doc
1280    // https://doc.rust-lang.org/cargo/reference/build-scripts.html#inputs-to-the-build-script
1281    // states that cwd == CARGO_MANIFEST_DIR
1282
1283    #[test]
1284    fn file() {
1285        let cookie = Cookie::open(Flags::ERROR).unwrap();
1286        let databases = &["data/tests/db-images-png"].try_into().unwrap();
1287        let cookie = cookie.load(databases).unwrap();
1288
1289        let path = "data/tests/rust-logo-128x128-blk.png";
1290
1291        assert_eq!(
1292            cookie.file(path).ok().unwrap(),
1293            "PNG image data, 128 x 128, 8-bit/color RGBA, non-interlaced"
1294        );
1295
1296        cookie.set_flags(Flags::MIME_TYPE).unwrap();
1297        assert_eq!(cookie.file(path).ok().unwrap(), "image/png");
1298
1299        cookie
1300            .set_flags(Flags::MIME_TYPE | Flags::MIME_ENCODING)
1301            .unwrap();
1302        assert_eq!(cookie.file(path).ok().unwrap(), "image/png; charset=binary");
1303    }
1304
1305    #[test]
1306    fn buffer() {
1307        let cookie = Cookie::open(Flags::ERROR).unwrap();
1308        let databases = &["data/tests/db-python"].try_into().unwrap();
1309        let cookie = cookie.load(databases).unwrap();
1310
1311        let s = b"#!/usr/bin/env python\nprint('Hello, world!')";
1312        assert_eq!(
1313            cookie.buffer(s).ok().unwrap(),
1314            "Python script, ASCII text executable"
1315        );
1316
1317        cookie.set_flags(Flags::MIME_TYPE).unwrap();
1318        assert_eq!(cookie.buffer(s).ok().unwrap(), "text/x-python");
1319    }
1320
1321    #[test]
1322    fn file_error() {
1323        let cookie = Cookie::open(Flags::ERROR).unwrap();
1324        let cookie = cookie.load(&Default::default()).unwrap();
1325
1326        let ret = cookie.file("non-existent_file.txt");
1327        assert!(ret.is_err());
1328    }
1329
1330    #[test]
1331    fn load_default() {
1332        let cookie = Cookie::open(Flags::ERROR).unwrap();
1333        assert!(cookie.load(&Default::default()).is_ok());
1334    }
1335
1336    #[test]
1337    fn load_one() {
1338        let cookie = Cookie::open(Flags::ERROR).unwrap();
1339        let databases = &["data/tests/db-images-png"].try_into().unwrap();
1340        assert!(cookie.load(databases).is_ok());
1341    }
1342
1343    #[test]
1344    fn load_multiple() {
1345        let cookie = Cookie::open(Flags::ERROR).unwrap();
1346        let databases = &["data/tests/db-images-png", "data/tests/db-python"]
1347            .try_into()
1348            .unwrap();
1349        assert!(cookie.load(databases).is_ok());
1350    }
1351
1352    #[test]
1353    fn cookie_impl_debug() {
1354        assert_impl_debug::<Cookie<Open>>();
1355        assert_impl_debug::<Cookie<Load>>();
1356    }
1357
1358    #[test]
1359    #[ignore] // FIXME: the binary .mgc file depends on libmagic (internal format) version
1360    fn load_buffers_file() {
1361        let cookie = Cookie::open(Flags::ERROR).unwrap();
1362        // file --compile --magic-file data/tests/db-images-png
1363        let magic_database = std::fs::read("data/tests/db-images-png-precompiled.mgc").unwrap();
1364        let buffers = vec![magic_database.as_slice()];
1365        let cookie = cookie.load_buffers(&buffers).unwrap();
1366
1367        let path = "data/tests/rust-logo-128x128-blk.png";
1368        assert_eq!(
1369            cookie.file(path).ok().unwrap(),
1370            "PNG image data, 128 x 128, 8-bit/color RGBA, non-interlaced"
1371        );
1372    }
1373
1374    #[test]
1375    fn libmagic_version() {
1376        let version = super::libmagic_version();
1377
1378        assert!(version > 500);
1379    }
1380
1381    #[test]
1382    fn error_impls() {
1383        assert_impl_debug::<Error>();
1384        assert_impl_display::<Error>();
1385        assert_impl_error::<Error>();
1386    }
1387
1388    #[test]
1389    fn invaliddatabasepatherror_impls() {
1390        assert_impl_debug::<InvalidDatabasePathError>();
1391        assert_impl_display::<InvalidDatabasePathError>();
1392        assert_impl_error::<InvalidDatabasePathError>();
1393    }
1394
1395    #[test]
1396    fn loaderror_impls() {
1397        assert_impl_debug::<LoadError<Open>>();
1398        assert_impl_debug::<LoadError<Load>>();
1399        assert_impl_display::<LoadError<Open>>();
1400        assert_impl_display::<LoadError<Load>>();
1401        assert_impl_error::<LoadError<Open>>();
1402        assert_impl_error::<LoadError<Load>>();
1403    }
1404
1405    #[test]
1406    fn openerror_impls() {
1407        assert_impl_debug::<OpenError>();
1408        assert_impl_display::<OpenError>();
1409        assert_impl_error::<OpenError>();
1410    }
1411
1412    #[test]
1413    fn setflagserror_impls() {
1414        assert_impl_debug::<SetFlagsError>();
1415        assert_impl_display::<SetFlagsError>();
1416        assert_impl_error::<SetFlagsError>();
1417    }
1418}
1419
1420#[cfg(doctest)]
1421#[doc=include_str!("../README-crate.md")]
1422mod readme {}