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