Skip to main content

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    ///
601    /// ```
602    /// # use magic::cookie::DatabasePaths;
603    /// // can also be cloned
604    /// let database: DatabasePaths = Default::default();
605    /// let another_database = database.clone();
606    /// ```
607    #[derive(Clone)]
608    pub struct DatabasePaths {
609        filenames: Option<CString>,
610    }
611
612    impl std::fmt::Debug for DatabasePaths {
613        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
614            let mut list = f.debug_list();
615
616            if let Some(cstr) = &self.filenames {
617                cstr.as_bytes()
618                    .split(|&b| b == DATABASE_FILENAME_SEPARATOR_U8)
619                    .for_each(|filename| {
620                        if let Ok(s) = std::str::from_utf8(filename) {
621                            list.entry(&s as _);
622                        } else {
623                            list.entry(&"<non utf8 string>" as _);
624                        }
625                    });
626            }
627
628            list.finish()
629        }
630    }
631
632    const DATABASE_FILENAME_SEPARATOR: &str = ":";
633    const DATABASE_FILENAME_SEPARATOR_U8: u8 = b':';
634
635    impl DatabasePaths {
636        /// Create a new database paths instance
637        ///
638        /// Using one of the `TryFrom` implementations is recommended instead, see [`DatabasePaths`] examples.
639        ///
640        /// Empty `paths` returns [`Default::default()`](DatabasePaths::default).
641        ///
642        /// # Errors
643        ///
644        /// If the `paths` contain a ":" (colon), a [`cookie::InvalidDatabasePathError`](InvalidDatabasePathError) will be returned.
645        ///
646        pub fn new<I, P>(paths: I) -> Result<Self, InvalidDatabasePathError>
647        where
648            I: IntoIterator<Item = P>,
649            P: AsRef<Path>,
650        {
651            // this is not the most efficient nor correct for Windows, but consistent with previous behaviour
652
653            let filename = paths
654                .into_iter()
655                .map(|f| f.as_ref().to_string_lossy().into_owned())
656                .collect::<Vec<String>>()
657                .join(DATABASE_FILENAME_SEPARATOR);
658
659            Ok(Self {
660                filenames: match filename.is_empty() {
661                    true => None,
662                    _ => Some(CString::new(filename).map_err(|_| InvalidDatabasePathError {})?),
663                },
664            })
665        }
666    }
667
668    impl Default for DatabasePaths {
669        /// Returns the path for the default unnamed database/s
670        ///
671        /// Note that the default database/s can be overwritten by setting the "MAGIC" environment variable
672        /// to a colon-separated text of database file paths:
673        /// ```shell
674        /// $ export MAGIC='data/tests/db-python:data/tests/db-images-png-precompiled.mgc'
675        /// $ # file-ish uses `DatabasePaths::default()`
676        /// $ cargo run --example file-ish -- data/tests/rust-logo-128x128-blk.png
677        /// ```
678        /// This is a feature of `libmagic` itself, not of this Rust crate.
679        ///
680        /// Note that the `file` CLI (which uses `libmagic`) prints the location of its default database with:
681        /// ```shell
682        /// $ file --version
683        /// file-5.38
684        /// magic file from /etc/magic:/usr/share/misc/magic
685        ///
686        /// $ export MAGIC='data/tests/db-python:data/tests/db-images-png-precompiled.mgc'
687        /// $ file --version
688        /// file-5.39
689        /// magic file from data/tests/db-python:data/tests/db-images-png-precompiled.mgc
690        /// ```
691        fn default() -> Self {
692            Self { filenames: None }
693        }
694    }
695
696    impl<P: AsRef<std::path::Path>, const N: usize> TryFrom<[P; N]> for DatabasePaths {
697        type Error = InvalidDatabasePathError;
698
699        /// Invokes [`DatabasePaths::new()`](DatabasePaths::new)
700        fn try_from(value: [P; N]) -> Result<Self, <Self as TryFrom<[P; N]>>::Error> {
701            Self::new(value)
702        }
703    }
704
705    impl<P: AsRef<std::path::Path>> TryFrom<Vec<P>> for DatabasePaths {
706        type Error = InvalidDatabasePathError;
707
708        /// Invokes [`DatabasePaths::new()`](DatabasePaths::new)
709        fn try_from(value: Vec<P>) -> Result<Self, <Self as TryFrom<Vec<P>>>::Error> {
710            Self::new(value)
711        }
712    }
713
714    impl<P: AsRef<std::path::Path>> TryFrom<&'_ [P]> for DatabasePaths {
715        type Error = InvalidDatabasePathError;
716
717        /// Invokes [`DatabasePaths::new()`](DatabasePaths::new)
718        fn try_from(value: &[P]) -> Result<Self, <Self as TryFrom<&[P]>>::Error> {
719            Self::new(value)
720        }
721    }
722
723    macro_rules! databasepaths_try_from_impl {
724        ($t:ty) => {
725            impl TryFrom<$t> for DatabasePaths {
726                type Error = InvalidDatabasePathError;
727
728                /// Invokes [`DatabasePaths::new()`](DatabasePaths::new)
729                fn try_from(value: $t) -> Result<Self, <Self as TryFrom<$t>>::Error> {
730                    DatabasePaths::new(std::iter::once(value))
731                }
732            }
733        };
734    }
735
736    // missing for now are:
737    // - Cow<'_, OsStr>
738    // - std::path::Component<'_>
739    // - std::path::Components<'_>
740    // - std::path::Iter<'_>
741    databasepaths_try_from_impl!(&str);
742    databasepaths_try_from_impl!(&std::ffi::OsStr);
743    databasepaths_try_from_impl!(std::ffi::OsString);
744    databasepaths_try_from_impl!(&std::path::Path);
745    databasepaths_try_from_impl!(std::path::PathBuf);
746    databasepaths_try_from_impl!(String);
747
748    /// Error within several [`Cookie`] functions
749    ///
750    /// Most functions on a [`Cookie`] can return an error from `libmagic`,
751    /// which unfortunately is not very structured.
752    #[derive(Debug)]
753    pub struct Error {
754        function: &'static str,
755        //#[backtrace]
756        source: crate::ffi::CookieError,
757    }
758
759    impl std::error::Error for Error {
760        fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
761            Some(&self.source)
762        }
763    }
764
765    impl std::fmt::Display for Error {
766        fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
767            let Self { function, .. } = self;
768            write!(f, "magic cookie error in `libmagic` function {0}", function,)
769        }
770    }
771
772    /// Typestate marker of opened [`Cookie`]
773    ///
774    /// In this state, a cookie has only been [opened](Cookie::open) but databases are not [loaded](Cookie::load) yet.
775    #[derive(Debug)]
776    pub enum Open {}
777
778    /// Typestate marker of loaded [`Cookie`]
779    ///
780    /// In this state, a cookie has both been [opened](Cookie::open) and databases are [loaded](Cookie::load).
781    #[derive(Debug)]
782    pub enum Load {}
783
784    mod private {
785        pub trait Sealed {}
786
787        impl Sealed for super::Open {}
788        impl Sealed for super::Load {}
789    }
790
791    /// Typestate marker for [`Cookie`]
792    ///
793    /// A cookie can either be in the [`Open`] or [`Load`] state
794    pub trait State: private::Sealed {}
795
796    impl State for Open {}
797    impl State for Load {}
798
799    /// Combined configuration of [`Flags`], parameters and databases
800    ///
801    /// A "cookie" is `libmagic` lingo for a combined configuration of
802    /// - [`cookie::Flags`](crate::cookie::Flags)
803    /// - parameters (not implemented yet)
804    /// - loaded datbases, e.g. [`cookie::DatabasePaths`](crate::cookie::DatabasePaths)
805    ///
806    /// A cookie advances through 2 states: opened, then loaded.
807    /// ```
808    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
809    /// // create new cookie in initial opened state with given flags
810    /// let cookie = magic::Cookie::open(magic::cookie::Flags::default())?;
811    ///
812    /// // advance cookie into loaded state
813    /// let cookie = cookie.load(&magic::cookie::DatabasePaths::default())?;
814    /// # Ok(())
815    /// # }
816    /// ```
817    ///
818    /// In either state, you can use operations that do not require
819    /// already loaded magic databases:
820    /// - [`Cookie::load()`](Cookie::load), [`Cookie::load_buffers()`](Cookie::load_buffers) to load databases and transition into the loaded state
821    /// - [`Cookie::set_flags()`](Cookie::set_flags) to overwrite the initial flags given in [`Cookie::open()`](Cookie::open)
822    /// - [`Cookie::compile()`](Cookie::compile), [`Cookie::check()`](Cookie::check), [`Cookie::list()`](Cookie::list) to operate on magic database files
823    ///
824    /// Once in the loaded state, you can perform magic "queries":
825    /// - [`Cookie::file()`](Cookie::file), [`Cookie::buffer()`](Cookie::buffer)
826    #[derive(Debug)]
827    #[doc(alias = "magic_t")]
828    #[doc(alias = "magic_set")]
829    pub struct Cookie<S: State> {
830        cookie: crate::ffi::Cookie,
831        marker: std::marker::PhantomData<S>,
832    }
833
834    /// Error within [`Cookie::load()`](Cookie::load) or [`Cookie::load_buffers()`](Cookie::load_buffers)
835    ///
836    /// This is like [`cookie:Error`](Error) but also has the cookie in its original state.
837    ///
838    /// # Examples
839    ///
840    /// ```
841    /// # use std::convert::TryInto;
842    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
843    /// let cookie = magic::Cookie::open(Default::default())?;
844    /// let database = "data/tests/db-images-png".try_into()?;
845    /// // try to load an existing database, consuming and returning early
846    /// let cookie = cookie.load(&database)?;
847    ///
848    /// let database = "doesntexist.mgc".try_into()?;
849    /// // load a database that does not exist
850    /// let cookie = match cookie.load(&database) {
851    ///     Err(err) => {
852    ///         println!("whoopsie: {:?}", err);
853    ///         // recover the loaded cookie without dropping it
854    ///         err.cookie()
855    ///     },
856    ///     Ok(cookie) => cookie,
857    /// };
858    ///
859    /// let database = "data/tests/db-python".try_into()?;
860    /// // try to load another existing database
861    /// let cookie = cookie.load(&database)?;
862    /// # Ok(())
863    /// # }
864    /// ```
865    #[derive(Debug)]
866    pub struct LoadError<S: State> {
867        function: &'static str,
868        //#[backtrace]
869        source: crate::ffi::CookieError,
870        cookie: Cookie<S>,
871    }
872
873    impl<S: State> std::error::Error for LoadError<S>
874    where
875        Self: std::fmt::Debug,
876    {
877        fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
878            Some(&self.source)
879        }
880    }
881
882    impl<S: State> std::fmt::Display for LoadError<S> {
883        fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
884            let Self { function, .. } = self;
885            write!(f, "magic cookie error in `libmagic` function {0}", function,)
886        }
887    }
888
889    impl<S: State> LoadError<S> {
890        /// Returns the cookie in its original state
891        pub fn cookie(self) -> Cookie<S> {
892            self.cookie
893        }
894    }
895
896    impl<S: State> Drop for Cookie<S> {
897        /// Closes the loaded magic database files and deallocates any resources used
898        #[doc(alias = "magic_close")]
899        fn drop(&mut self) {
900            crate::ffi::close(&mut self.cookie);
901        }
902    }
903
904    /// Operations that are valid in the [`Open`] state
905    ///
906    /// A new cookie created with [`Cookie::open`](Cookie::open) does not have any databases [loaded](Cookie::load).
907    impl Cookie<Open> {
908        /// Creates a new configuration cookie, `flags` specify how other operations on this cookie should behave
909        ///
910        /// This does not [`load()`](Cookie::load) any databases yet.
911        ///
912        /// The `flags` can be changed lateron with [`set_flags()`](Cookie::set_flags).
913        ///
914        /// # Examples
915        ///
916        /// ```
917        /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
918        /// // open a new cookie with default flags
919        /// let cookie = magic::Cookie::open(Default::default())?;
920        ///
921        /// // open a new cookie with custom flags
922        /// let flags = magic::cookie::Flags::COMPRESS | magic::cookie::Flags::DEVICES;
923        /// let cookie = magic::Cookie::open(flags)?;
924        /// # Ok(())
925        /// # }
926        /// ```
927        ///
928        /// # Errors
929        ///
930        /// If there was an `libmagic` internal error allocating a new cookie, a [`cookie::OpenError`](OpenError) will be returned.
931        ///
932        /// If the given `flags` are unsupported on the current platform, a [`cookie::OpenError`](OpenError) will be returned.
933        #[doc(alias = "magic_open")]
934        pub fn open(flags: Flags) -> Result<Cookie<Open>, OpenError> {
935            match crate::ffi::open(flags.bits()) {
936                Err(err) => Err(OpenError {
937                    flags,
938                    kind: match err.errno().kind() {
939                        std::io::ErrorKind::InvalidInput => OpenErrorKind::UnsupportedFlags,
940                        _ => OpenErrorKind::Errno,
941                    },
942                    source: err,
943                }),
944                Ok(cookie) => {
945                    let cookie = Cookie {
946                        cookie,
947                        marker: std::marker::PhantomData,
948                    };
949                    Ok(cookie)
950                }
951            }
952        }
953    }
954
955    /// Operations that are valid in the [`Load`] state
956    ///
957    /// An opened cookie with [loaded](Cookie::load) databases can inspect [files](Cookie::file) and [buffers](Cookie::buffer).
958    impl Cookie<Load> {
959        /// Returns a textual description of the contents of the file `filename`
960        ///
961        /// Requires to [`load()`](Cookie::load) databases before calling.
962        ///
963        /// # Examples
964        ///
965        /// ```
966        /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
967        /// // open a new cookie with default flags and database
968        /// let cookie = magic::Cookie::open(Default::default())?.load(&Default::default())?;
969        ///
970        /// let file_description = cookie.file("data/tests/rust-logo-128x128-blk.png");
971        /// # Ok(())
972        /// # }
973        /// ```
974        ///
975        /// # Errors
976        ///
977        /// If there was an `libmagic` internal error, a [`cookie::Error`](Error) will be returned.
978        ///
979        /// If `libmagic` violates its API contract, e.g. by not setting the last error, a [`cookie::Error`](Error) will be returned.
980        #[doc(alias = "magic_file")]
981        pub fn file<P: AsRef<Path>>(&self, filename: P) -> Result<String, Error> {
982            let c_string = CString::new(filename.as_ref().to_string_lossy().into_owned()).unwrap();
983            match crate::ffi::file(&self.cookie, c_string.as_c_str()) {
984                Ok(res) => Ok(res.to_string_lossy().to_string()),
985                Err(err) => Err(Error {
986                    function: "magic_file",
987                    source: err,
988                }),
989            }
990        }
991
992        /// Returns a textual description of the contents of the `buffer`
993        ///
994        /// Requires to [`load()`](Cookie::load) databases before calling.
995        ///
996        /// # Examples
997        ///
998        /// ```
999        /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1000        /// // open a new cookie with default flags and database
1001        /// let cookie = magic::Cookie::open(Default::default())?.load(&Default::default())?;
1002        ///
1003        /// let buffer = b"%PDF-\xE2\x80\xA6";
1004        /// let buffer_description = cookie.buffer(buffer);
1005        /// # Ok(())
1006        /// # }
1007        /// ```
1008        ///
1009        /// # Errors
1010        ///
1011        /// If there was an `libmagic` internal error, a [`cookie::Error`](Error) will be returned.
1012        ///
1013        /// If `libmagic` violates its API contract, e.g. by not setting the last error, a [`cookie::Error`](Error) will be returned.
1014        #[doc(alias = "magic_buffer")]
1015        pub fn buffer(&self, buffer: &[u8]) -> Result<String, Error> {
1016            match crate::ffi::buffer(&self.cookie, buffer) {
1017                Ok(res) => Ok(res.to_string_lossy().to_string()),
1018                Err(err) => Err(Error {
1019                    function: "magic_buffer",
1020                    source: err,
1021                }),
1022            }
1023        }
1024    }
1025
1026    /// Operations that are valid in any state
1027    impl<S: State> Cookie<S> {
1028        /// Loads the given database `filenames` for further queries
1029        ///
1030        /// Adds ".mgc" to the database filenames as appropriate.
1031        ///
1032        /// Calling `load()` or [`load_buffers()`](Cookie::load_buffers) replaces the previously loaded database/s.
1033        ///
1034        /// This is equivalent to the using the `file` CLI:
1035        /// ```shell
1036        /// $ file --magic-file 'data/tests/db-images-png:data/tests/db-python' --version
1037        /// file-5.39
1038        /// magic file from data/tests/db-images-png:data/tests/db-python
1039        /// ```
1040        ///
1041        /// # Examples
1042        /// ```rust
1043        /// # use std::convert::TryInto;
1044        /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1045        /// // open a new cookie with default flags
1046        /// let cookie = magic::Cookie::open(Default::default())?;
1047        ///
1048        /// // load the default unnamed database
1049        /// let database = Default::default();
1050        /// let cookie = cookie.load(&database)?;
1051        ///
1052        /// // load databases from files
1053        /// let databases = ["data/tests/db-images-png", "data/tests/db-python"].try_into()?;
1054        /// let cookie = cookie.load(&databases)?;
1055        ///
1056        /// // load precompiled database from file
1057        /// let database = "data/tests/db-images-png-precompiled.mgc".try_into()?;
1058        /// let cookie = cookie.load(&database)?;
1059        /// # Ok(())
1060        /// # }
1061        /// ```
1062        ///
1063        /// # Errors
1064        ///
1065        /// If there was an `libmagic` internal error, a [`cookie::LoadError`](LoadError) will be returned,
1066        /// which contains the cookie in its original state.
1067        ///
1068        /// If `libmagic` violates its API contract, e.g. by not setting the last error, a [`cookie::Error`](Error) will be returned.
1069        #[doc(alias = "magic_load")]
1070        #[doc(alias = "--magic-file")]
1071        pub fn load(self, filenames: &DatabasePaths) -> Result<Cookie<Load>, LoadError<S>> {
1072            match crate::ffi::load(&self.cookie, filenames.filenames.as_deref()) {
1073                Err(err) => Err(LoadError {
1074                    function: "magic_load",
1075                    source: err,
1076                    cookie: self,
1077                }),
1078                Ok(_) => {
1079                    let mut cookie = std::mem::ManuallyDrop::new(self);
1080
1081                    let cookie = Cookie {
1082                        cookie: crate::ffi::Cookie::new(&mut cookie.cookie),
1083                        marker: std::marker::PhantomData,
1084                    };
1085                    Ok(cookie)
1086                }
1087            }
1088        }
1089
1090        /// Loads the given compiled databases `buffers` for further queries
1091        ///
1092        /// Databases need to be compiled with a compatible `libmagic` version.
1093        ///
1094        /// This function can be used in environments where `libmagic` does
1095        /// not have direct access to the filesystem, but can access the magic
1096        /// database via shared memory or other IPC means.
1097        ///
1098        /// Calling `load_buffers()` or [`load()`](Cookie::load) replaces the previously loaded database/s.
1099        ///
1100        /// # Errors
1101        ///
1102        /// If there was an `libmagic` internal error, a [`cookie::LoadError`](LoadError) will be returned,
1103        /// which contains the cookie in its original state.
1104        ///
1105        /// If `libmagic` violates its API contract, e.g. by not setting the last error, a [`cookie::Error`](Error) will be returned.
1106        #[doc(alias = "magic_load_buffers")]
1107        pub fn load_buffers(self, buffers: &[&[u8]]) -> Result<Cookie<Load>, LoadError<S>> {
1108            match crate::ffi::load_buffers(&self.cookie, buffers) {
1109                Err(err) => Err(LoadError {
1110                    function: "magic_load_buffers",
1111                    source: err,
1112                    cookie: self,
1113                }),
1114                Ok(_) => {
1115                    let mut cookie = std::mem::ManuallyDrop::new(self);
1116
1117                    let cookie = Cookie {
1118                        cookie: crate::ffi::Cookie::new(&mut cookie.cookie),
1119                        marker: std::marker::PhantomData,
1120                    };
1121                    Ok(cookie)
1122                }
1123            }
1124        }
1125
1126        /// Sets the `flags` to use for this configuration
1127        ///
1128        /// Overwrites any previously set flags, e.g. those from [`load()`](Cookie::load).
1129        ///
1130        /// # Examples
1131        /// ```rust
1132        /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1133        /// // open a new cookie with initial default flags
1134        /// let cookie = magic::Cookie::open(Default::default())?;
1135        ///
1136        /// // overwrite the initial flags
1137        /// let flags = magic::cookie::Flags::COMPRESS | magic::cookie::Flags::DEVICES;
1138        /// cookie.set_flags(flags)?;
1139        /// # Ok(())
1140        /// # }
1141        /// ```
1142        ///
1143        /// # Errors
1144        ///
1145        /// If the given `flags` are unsupported on the current platform, an [`cookie::SetFlagsError`](SetFlagsError) will be returned.
1146        #[doc(alias = "magic_setflags")]
1147        pub fn set_flags(&self, flags: Flags) -> Result<(), SetFlagsError> {
1148            let ret = crate::ffi::setflags(&self.cookie, flags.bits());
1149            match ret {
1150                // according to `libmagic` man page this is the only flag that could be unsupported
1151                Err(err) => Err(SetFlagsError {
1152                    flags: Flags::PRESERVE_ATIME,
1153                    source: err,
1154                }),
1155                Ok(_) => Ok(()),
1156            }
1157        }
1158
1159        // TODO: check, compile, list and load mostly do the same, refactor!
1160
1161        /// Compiles the given database files `filenames` for faster access
1162        ///
1163        /// The compiled files created are named from the `basename` of each file argument with ".mgc" appended to it.
1164        ///
1165        /// This is equivalent to the following `file` CLI command:
1166        /// ```shell
1167        /// $ file --compile --magic-file data/tests/db-images-png:data/tests/db-python
1168        /// ```
1169        ///
1170        /// # Errors
1171        ///
1172        /// If there was an `libmagic` internal error, a [`cookie::Error`](Error) will be returned.
1173        ///
1174        /// If `libmagic` violates its API contract, e.g. by not setting the last error, a [`cookie::Error`](Error) will be returned.
1175        #[doc(alias = "magic_compile")]
1176        #[doc(alias = "--compile")]
1177        pub fn compile(&self, filenames: &DatabasePaths) -> Result<(), Error> {
1178            match crate::ffi::compile(&self.cookie, filenames.filenames.as_deref()) {
1179                Err(err) => Err(Error {
1180                    function: "magic_compile",
1181                    source: err,
1182                }),
1183                Ok(_) => Ok(()),
1184            }
1185        }
1186
1187        /// Checks the validity of entries in the database files `filenames`
1188        ///
1189        /// # Errors
1190        ///
1191        /// If there was an `libmagic` internal error, a [`cookie::Error`](Error) will be returned.
1192        ///
1193        /// If `libmagic` violates its API contract, e.g. by not setting the last error, a [`cookie::Error`](Error) will be returned.
1194        #[doc(alias = "magic_check")]
1195        pub fn check(&self, filenames: &DatabasePaths) -> Result<(), Error> {
1196            match crate::ffi::check(&self.cookie, filenames.filenames.as_deref()) {
1197                Err(err) => Err(Error {
1198                    function: "magic_check",
1199                    source: err,
1200                }),
1201                Ok(_) => Ok(()),
1202            }
1203        }
1204
1205        /// Dumps all magic entries in the given database files `filenames` in a human readable format
1206        ///
1207        /// This is equivalent to the following `file` CLI command:
1208        /// ```shell
1209        /// $ file --checking-printout --magic-file data/tests/db-images-png:data/tests/db-python
1210        /// ```
1211        ///
1212        /// # Errors
1213        ///
1214        /// If there was an `libmagic` internal error, a [`cookie::Error`](Error) will be returned.
1215        ///
1216        /// If `libmagic` violates its API contract, e.g. by not setting the last error, a [`cookie::Error`](Error) will be returned.
1217        #[doc(alias = "magic_list")]
1218        #[doc(alias = "--checking-printout")]
1219        pub fn list(&self, filenames: &DatabasePaths) -> Result<(), Error> {
1220            match crate::ffi::list(&self.cookie, filenames.filenames.as_deref()) {
1221                Err(err) => Err(Error {
1222                    function: "magic_list",
1223                    source: err,
1224                }),
1225                Ok(_) => Ok(()),
1226            }
1227        }
1228    }
1229
1230    /// Error within [`Cookie::open()`](Cookie::open)
1231    ///
1232    /// Note that a similar [`cookie::SetFlagsError`](SetFlagsError) can also occur
1233    #[derive(Debug)]
1234    pub struct OpenError {
1235        flags: Flags,
1236        kind: OpenErrorKind,
1237        //#[backtrace]
1238        source: crate::ffi::OpenError,
1239    }
1240
1241    impl std::error::Error for OpenError {
1242        fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1243            Some(&self.source)
1244        }
1245    }
1246
1247    impl std::fmt::Display for OpenError {
1248        fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1249            let Self { flags, kind, .. } = self;
1250            write!(
1251                f,
1252                "could not open magic cookie: {0}",
1253                match kind {
1254                    OpenErrorKind::UnsupportedFlags => format!("unsupported flags {0}", flags),
1255                    OpenErrorKind::Errno => "other error".to_string(),
1256                }
1257            )
1258        }
1259    }
1260
1261    /// Kind of [`OpenError`]
1262    #[derive(Debug)]
1263    enum OpenErrorKind {
1264        /// Unsupported flags given
1265        UnsupportedFlags,
1266        /// Other kind
1267        Errno,
1268    }
1269
1270    /// Error within [`Cookie::set_flags()`](Cookie::set_flags)
1271    ///
1272    /// Note that a similar [`cookie::OpenError`](OpenError) can also occur
1273    #[derive(Debug)]
1274    pub struct SetFlagsError {
1275        flags: Flags,
1276        //#[backtrace]
1277        source: crate::ffi::SetFlagsError,
1278    }
1279
1280    impl std::error::Error for SetFlagsError {
1281        fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1282            Some(&self.source)
1283        }
1284    }
1285
1286    impl std::fmt::Display for SetFlagsError {
1287        fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1288            let Self { flags, .. } = self;
1289            write!(f, "could not set magic cookie flags {0}", flags)
1290        }
1291    }
1292} // mod cookie
1293
1294pub use crate::cookie::Cookie;
1295
1296#[cfg(test)]
1297mod tests {
1298    use super::cookie::Flags;
1299    use super::cookie::{
1300        DatabasePaths, Error, InvalidDatabasePathError, LoadError, OpenError, SetFlagsError,
1301    };
1302    use super::cookie::{Load, Open};
1303    use super::Cookie;
1304    use std::convert::TryInto;
1305
1306    fn assert_impl_debug<T: std::fmt::Debug>() {}
1307    fn assert_impl_display<T: std::fmt::Display>() {}
1308    fn assert_impl_error<T: std::error::Error>() {}
1309
1310    // Using relative paths to test files should be fine, since cargo doc
1311    // https://doc.rust-lang.org/cargo/reference/build-scripts.html#inputs-to-the-build-script
1312    // states that cwd == CARGO_MANIFEST_DIR
1313
1314    #[test]
1315    fn file() {
1316        let cookie = Cookie::open(Flags::ERROR).unwrap();
1317        let databases = &["data/tests/db-images-png"].try_into().unwrap();
1318        let cookie = cookie.load(databases).unwrap();
1319
1320        let path = "data/tests/rust-logo-128x128-blk.png";
1321
1322        assert_eq!(
1323            cookie.file(path).ok().unwrap(),
1324            "PNG image data, 128 x 128, 8-bit/color RGBA, non-interlaced"
1325        );
1326
1327        cookie.set_flags(Flags::MIME_TYPE).unwrap();
1328        assert_eq!(cookie.file(path).ok().unwrap(), "image/png");
1329
1330        cookie
1331            .set_flags(Flags::MIME_TYPE | Flags::MIME_ENCODING)
1332            .unwrap();
1333        assert_eq!(cookie.file(path).ok().unwrap(), "image/png; charset=binary");
1334    }
1335
1336    #[test]
1337    fn buffer() {
1338        let cookie = Cookie::open(Flags::ERROR).unwrap();
1339        let databases = &["data/tests/db-python"].try_into().unwrap();
1340        let cookie = cookie.load(databases).unwrap();
1341
1342        let s = b"#!/usr/bin/env python\nprint('Hello, world!')";
1343        assert_eq!(
1344            cookie.buffer(s).ok().unwrap(),
1345            "Python script, ASCII text executable"
1346        );
1347
1348        cookie.set_flags(Flags::MIME_TYPE).unwrap();
1349        assert_eq!(cookie.buffer(s).ok().unwrap(), "text/x-python");
1350    }
1351
1352    #[test]
1353    fn file_error() {
1354        let cookie = Cookie::open(Flags::ERROR).unwrap();
1355        let cookie = cookie.load(&Default::default()).unwrap();
1356
1357        let ret = cookie.file("non-existent_file.txt");
1358        assert!(ret.is_err());
1359    }
1360
1361    #[test]
1362    fn load_default() {
1363        let cookie = Cookie::open(Flags::ERROR).unwrap();
1364        assert!(cookie.load(&Default::default()).is_ok());
1365    }
1366
1367    #[test]
1368    fn load_one() {
1369        let cookie = Cookie::open(Flags::ERROR).unwrap();
1370        let databases = &["data/tests/db-images-png"].try_into().unwrap();
1371        assert!(cookie.load(databases).is_ok());
1372    }
1373
1374    #[test]
1375    fn load_multiple() {
1376        let cookie = Cookie::open(Flags::ERROR).unwrap();
1377        let databases = &["data/tests/db-images-png", "data/tests/db-python"]
1378            .try_into()
1379            .unwrap();
1380        assert!(cookie.load(databases).is_ok());
1381    }
1382
1383    #[test]
1384    fn cookie_impl_debug() {
1385        assert_impl_debug::<Cookie<Open>>();
1386        assert_impl_debug::<Cookie<Load>>();
1387    }
1388
1389    #[test]
1390    #[ignore] // FIXME: the binary .mgc file depends on libmagic (internal format) version
1391    fn load_buffers_file() {
1392        let cookie = Cookie::open(Flags::ERROR).unwrap();
1393        // file --compile --magic-file data/tests/db-images-png
1394        let magic_database = std::fs::read("data/tests/db-images-png-precompiled.mgc").unwrap();
1395        let buffers = vec![magic_database.as_slice()];
1396        let cookie = cookie.load_buffers(&buffers).unwrap();
1397
1398        let path = "data/tests/rust-logo-128x128-blk.png";
1399        assert_eq!(
1400            cookie.file(path).ok().unwrap(),
1401            "PNG image data, 128 x 128, 8-bit/color RGBA, non-interlaced"
1402        );
1403    }
1404
1405    #[test]
1406    fn libmagic_version() {
1407        let version = super::libmagic_version();
1408
1409        assert!(version > 500);
1410    }
1411
1412    #[test]
1413    fn error_impls() {
1414        assert_impl_debug::<Error>();
1415        assert_impl_display::<Error>();
1416        assert_impl_error::<Error>();
1417    }
1418
1419    #[test]
1420    fn databasepath_impls() {
1421        assert_impl_debug::<DatabasePaths>();
1422    }
1423
1424    #[test]
1425    fn invaliddatabasepatherror_impls() {
1426        assert_impl_debug::<InvalidDatabasePathError>();
1427        assert_impl_display::<InvalidDatabasePathError>();
1428        assert_impl_error::<InvalidDatabasePathError>();
1429    }
1430
1431    #[test]
1432    fn loaderror_impls() {
1433        assert_impl_debug::<LoadError<Open>>();
1434        assert_impl_debug::<LoadError<Load>>();
1435        assert_impl_display::<LoadError<Open>>();
1436        assert_impl_display::<LoadError<Load>>();
1437        assert_impl_error::<LoadError<Open>>();
1438        assert_impl_error::<LoadError<Load>>();
1439    }
1440
1441    #[test]
1442    fn openerror_impls() {
1443        assert_impl_debug::<OpenError>();
1444        assert_impl_display::<OpenError>();
1445        assert_impl_error::<OpenError>();
1446    }
1447
1448    #[test]
1449    fn setflagserror_impls() {
1450        assert_impl_debug::<SetFlagsError>();
1451        assert_impl_display::<SetFlagsError>();
1452        assert_impl_error::<SetFlagsError>();
1453    }
1454}
1455
1456#[cfg(doctest)]
1457#[doc=include_str!("../README-crate.md")]
1458mod readme {}