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 {}