Skip to main content

pure_magic/
lib.rs

1#![forbid(unsafe_code)]
2#![deny(unused_imports)]
3#![deny(missing_docs)]
4//! # `pure-magic`: A pure and safe Rust Reimplementation of `libmagic`
5//!
6//! Unlike many file identification crates, `pure-magic` is highly compatible with the standard
7//! `magic` rule format, allowing seamless reuse of existing
8//! [rules](https://github.com/qjerome/magic-rs/tree/main/magic-db/src/magdir). This makes it an ideal
9//! drop-in replacement for crates relying on **`libmagic` C bindings**, where memory safety is critical.
10//!
11//! **Key Features:**
12//! - File type detection
13//! - MIME type inference
14//! - Custom magic rule parsing
15//!
16//! ## Installation
17//! Add `pure-magic` to your `Cargo.toml`:
18//!
19//! ```toml
20//! [dependencies]
21//! pure-magic = "0.1"  # Replace with the latest version
22//! ```
23//!
24//! Or add the latest version with cargo:
25//!
26//! ```sh
27//! cargo add pure-magic
28//! ```
29//!
30//! ## Quick Start
31//!
32//! ### Detect File Types Programmatically
33//! ```rust
34//! use pure_magic::{MagicDb, MagicSource, DataReader};
35//! use std::fs::File;
36//!
37//! fn main() -> Result<(), Box<dyn std::error::Error>> {
38//!     let mut db = MagicDb::new();
39//!     // Create a MagicSource from a file
40//!     let rust_magic = MagicSource::open("../magic-db/src/magdir/rust")?;
41//!     db.load(rust_magic);
42//!     // Verification is not mandatory
43//!     db.verify()?;
44//!
45//!     // Detect file type
46//!     let magic = db.first_magic_file("src/lib.rs")?;
47//!
48//!     println!(
49//!         "File type: {} (MIME: {}, strength: {})",
50//!         magic.message(),
51//!         magic.mime_type(),
52//!         magic.strength()
53//!     );
54//!     Ok(())
55//! }
56//! ```
57//!
58//! ### Get All Matching Rules
59//! ```rust
60//! use pure_magic::{MagicDb, MagicSource, DataReader};
61//! use std::fs::File;
62//!
63//! fn main() -> Result<(), Box<dyn std::error::Error>> {
64//!     let mut db = MagicDb::new();
65//!     // Create a MagicSource from a file
66//!     let rust_magic = MagicSource::open("../magic-db/src/magdir/rust")?;
67//!     db.load(rust_magic);
68//!
69//!     // Get all matching rules, sorted by strength
70//!     let magics = db.all_magics_file("src/lib.rs")?;
71//!
72//!     // Must contain rust file magic and default text magic
73//!     assert!(magics.len() > 1);
74//!
75//!     for magic in magics {
76//!         println!(
77//!             "Match: {} (strength: {}, source: {})",
78//!             magic.message(),
79//!             magic.strength(),
80//!             magic.source().unwrap_or("unknown")
81//!         );
82//!     }
83//!     Ok(())
84//! }
85//! ```
86//!
87//! ### Serialize a Database to Disk
88//! ```rust
89//! use pure_magic::{MagicDb, MagicSource};
90//! use std::fs::File;
91//!
92//! fn main() -> Result<(), Box<dyn std::error::Error>> {
93//!     let mut db = MagicDb::new();
94//!     // Create a MagicSource from a file
95//!     let rust_magic = MagicSource::open("../magic-db/src/magdir/rust")?;
96//!     db.load(rust_magic);
97//!
98//!     // Serialize the database to a file
99//!     let mut output = File::create("/tmp/compiled.db")?;
100//!     db.serialize(&mut output)?;
101//!
102//!     println!("Database saved to file");
103//!     Ok(())
104//! }
105//! ```
106//!
107//! ### Deserialize a Database
108//! ```rust
109//! use pure_magic::{MagicDb, MagicSource};
110//! use std::fs::File;
111//!
112//! fn main() -> Result<(), Box<dyn std::error::Error>> {
113//!     let mut db = MagicDb::new();
114//!     // Create a MagicSource from a file
115//!     let rust_magic = MagicSource::open("../magic-db/src/magdir/rust")?;
116//!     db.load(rust_magic);
117//!
118//!     // Serialize the database in a vector
119//!     let mut ser = vec![];
120//!     db.serialize(&mut ser)?;
121//!     println!("Database saved to vector");
122//!
123//!     // We deserialize from slice
124//!     let db = MagicDb::deserialize(&mut ser.as_slice())?;
125//!
126//!     assert!(!db.rules().is_empty());
127//!
128//!     Ok(())
129//! }
130//! ```
131//!
132//! ## License
133//! This project is dual-licensed under either:
134//! - **GPL-3.0**
135//! - **BSD-2-Clause**
136//!
137//! ## Contributing
138//! Contributions are welcome! Open an issue or submit a pull request.
139//!
140//! ## Acknowledgments
141//! - Inspired by the original `libmagic` (part of the `file` command).
142
143use dyf::{DynDisplay, FormatString, dformat};
144use flaglet::flags;
145use flate2::{Compression, read::GzDecoder, write::GzEncoder};
146use memchr::memchr;
147use pest::{Span, error::ErrorVariant};
148use regex::bytes::{self};
149use serde::{Deserialize, Serialize};
150use std::{
151    borrow::Cow,
152    cmp::max,
153    collections::{HashMap, HashSet},
154    fmt::{self, Debug, Display},
155    fs::File,
156    io::{self, Read, SeekFrom, Write},
157    mem::swap,
158    ops::{Add, BitAnd, BitOr, BitXor, Deref, Div, Mul, Rem, Sub},
159    path::Path,
160    sync::OnceLock,
161};
162use tar::Archive;
163use thiserror::Error;
164use tracing::{Level, debug, enabled, error, trace};
165
166use crate::{
167    numeric::{Float, FloatDataType, Scalar, ScalarDataType},
168    parser::{FileMagicParser, Rule},
169    readers::DataRead,
170    utils::{
171        debug_string_from_vec_u8, debug_string_from_vec_u16, decode_id3, find_json_boundaries,
172        run_utf8_validation,
173    },
174};
175
176mod numeric;
177mod parser;
178pub mod readers;
179pub use readers::DataReader;
180mod utils;
181
182const HARDCODED_MAGIC_STRENGTH: u64 = 2048;
183const HARDCODED_SOURCE: &str = "hardcoded";
184// corresponds to FILE_INDIR_MAX constant defined in libmagic
185const MAX_RECURSION: usize = 50;
186// constant found in libmagic. It is used to limit for regex tests
187const FILE_REGEX_MAX: usize = 8192;
188
189/// Maximum number of bytes to read for search tests.
190///
191/// This constant is derived from `libmagic` and is used to limit the number of bytes
192/// read during search tests to ensure performance and efficiency. The value is set
193/// to 7 megabytes.
194pub const FILE_BYTES_MAX: usize = 7 * 1024 * 1024;
195/// Default mimetype for un-identified binary data
196pub const DEFAULT_BIN_MIMETYPE: &str = "application/octet-stream";
197/// Default mimetype for un-identified text data
198pub const DEFAULT_TEXT_MIMETYPE: &str = "text/plain";
199
200pub(crate) const TIMESTAMP_FORMAT: &str = "%Y-%m-%d %H:%M:%S";
201
202macro_rules! debug_panic {
203    ($($arg:tt)*) => {
204        if cfg!(debug_assertions) {
205            panic!($($arg)*);
206        }
207    };
208}
209
210macro_rules! read {
211    ($r: expr, $ty: ty) => {{
212        let mut a = [0u8; std::mem::size_of::<$ty>()];
213        $r.read_exact_into(&mut a)?;
214        a
215    }};
216}
217
218macro_rules! read_le {
219    ($r:expr, $ty: ty ) => {{ <$ty>::from_le_bytes(read!($r, $ty)) }};
220}
221
222macro_rules! read_be {
223    ($r:expr, $ty: ty ) => {{ <$ty>::from_be_bytes(read!($r, $ty)) }};
224}
225
226macro_rules! read_me {
227    ($r: expr) => {{ ((read_le!($r, u16) as i32) << 16) | (read_le!($r, u16) as i32) }};
228}
229
230#[inline(always)]
231fn read_octal_u64<D: DataRead>(haystack: &mut D) -> Option<u64> {
232    let s = haystack
233        .read_while_or_limit(|b| matches!(b, b'0'..=b'7'), 22)
234        .map(|buf| str::from_utf8(buf))
235        .ok()?
236        .ok()?;
237
238    if !s.starts_with("0") {
239        return None;
240    }
241
242    u64::from_str_radix(s, 8).ok()
243}
244
245/// Represents all possible errors that can occur during file type detection and processing.
246#[derive(Debug, Error)]
247pub enum Error {
248    /// A generic error with a custom message.
249    #[error("{0}")]
250    Msg(String),
251
252    /// Indicate a rule load failure
253    #[error("source={0} line={1} error={2}")]
254    Verify(String, usize, Box<Error>),
255
256    /// An error with a source location and a nested error.
257    #[error("source={0} line={1} error={2}")]
258    Localized(String, usize, Box<Error>),
259
260    /// Indicates a required rule was not found.
261    #[error("missing rule: {0}")]
262    MissingRule(String),
263
264    /// Indicates the maximum recursion depth was reached.
265    #[error("maximum recursion reached: {0}")]
266    MaximumRecursion(usize),
267
268    /// Wraps an I/O error.
269    #[error("io: {0}")]
270    Io(#[from] io::Error),
271
272    /// Wraps a parsing error from the `pest` parser.
273    #[error("parser error: {0}")]
274    Parse(#[from] Box<pest::error::Error<Rule>>),
275
276    /// Wraps a formatting error from the `dyf` crate.
277    #[error("formatting: {0}")]
278    Format(#[from] dyf::Error),
279
280    /// Wraps a regex-related error.
281    #[error("regex: {0}")]
282    Regex(#[from] regex::Error),
283
284    /// Wraps a serialization error from `bincode`.
285    #[error("{0}")]
286    Serialize(#[from] bincode::error::EncodeError),
287
288    /// Wraps a deserialization error from `bincode`.
289    #[error("{0}")]
290    Deserialize(#[from] bincode::error::DecodeError),
291}
292
293impl Error {
294    #[inline]
295    fn parser<S: ToString>(msg: S, span: Span<'_>) -> Self {
296        Self::Parse(Box::new(pest::error::Error::new_from_span(
297            ErrorVariant::CustomError {
298                message: msg.to_string(),
299            },
300            span,
301        )))
302    }
303
304    fn msg<M: AsRef<str>>(msg: M) -> Self {
305        Self::Msg(msg.as_ref().into())
306    }
307
308    fn localized<S: AsRef<str>>(source: S, line: usize, err: Error) -> Self {
309        Self::Localized(source.as_ref().into(), line, err.into())
310    }
311
312    /// Unwraps the localized error
313    pub fn unwrap_localized(&self) -> &Self {
314        match self {
315            Self::Localized(_, _, e) => e,
316            _ => self,
317        }
318    }
319}
320
321#[derive(Debug, Clone, Serialize, Deserialize)]
322enum Message {
323    String(String),
324    Format {
325        printf_spec: String,
326        fs: FormatString,
327    },
328}
329
330impl Display for Message {
331    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
332        match self {
333            Self::String(s) => write!(f, "{s}"),
334            Self::Format { printf_spec: _, fs } => write!(f, "{}", fs.to_string_lossy()),
335        }
336    }
337}
338
339impl Message {
340    fn to_string_lossy(&self) -> Cow<'_, str> {
341        match self {
342            Message::String(s) => Cow::Borrowed(s),
343            Message::Format { printf_spec: _, fs } => fs.to_string_lossy(),
344        }
345    }
346
347    #[inline(always)]
348    fn format_with(&self, mr: Option<&MatchRes>) -> Result<Cow<'_, str>, Error> {
349        match self {
350            Self::String(s) => Ok(Cow::Borrowed(s.as_str())),
351            Self::Format {
352                printf_spec: c_spec,
353                fs,
354            } => {
355                if let Some(mr) = mr {
356                    match mr {
357                        MatchRes::Float(_, _) | MatchRes::Bytes(_, _, _, _) => {
358                            Ok(Cow::Owned(dformat!(fs, mr)?))
359                        }
360                        MatchRes::Scalar(_, scalar) => {
361                            // we want to print a byte as char
362                            if c_spec.as_str() == "c" {
363                                match scalar {
364                                    Scalar::byte(b) => {
365                                        let b = (*b as u8) as char;
366                                        Ok(Cow::Owned(dformat!(fs, b)?))
367                                    }
368                                    Scalar::ubyte(b) => {
369                                        let b = *b as char;
370                                        Ok(Cow::Owned(dformat!(fs, b)?))
371                                    }
372                                    _ => Ok(Cow::Owned(dformat!(fs, mr)?)),
373                                }
374                            } else {
375                                Ok(Cow::Owned(dformat!(fs, mr)?))
376                            }
377                        }
378                    }
379                } else {
380                    Ok(fs.to_string_lossy())
381                }
382            }
383        }
384    }
385}
386
387impl ScalarDataType {
388    #[inline(always)]
389    fn read<R: DataRead>(&self, from: &mut R, switch_endianness: bool) -> Result<Scalar, Error> {
390        macro_rules! _read_le {
391            ($ty: ty) => {{
392                if switch_endianness {
393                    <$ty>::from_be_bytes(read!(from, $ty))
394                } else {
395                    <$ty>::from_le_bytes(read!(from, $ty))
396                }
397            }};
398        }
399
400        macro_rules! _read_be {
401            ($ty: ty) => {{
402                if switch_endianness {
403                    <$ty>::from_le_bytes(read!(from, $ty))
404                } else {
405                    <$ty>::from_be_bytes(read!(from, $ty))
406                }
407            }};
408        }
409
410        macro_rules! _read_ne {
411            ($ty: ty) => {{
412                if cfg!(target_endian = "big") {
413                    _read_be!($ty)
414                } else {
415                    _read_le!($ty)
416                }
417            }};
418        }
419
420        macro_rules! _read_me {
421            () => {
422                ((_read_le!(u16) as i32) << 16) | (_read_le!(u16) as i32)
423            };
424        }
425
426        Ok(match self {
427            // signed
428            Self::byte => Scalar::byte(read!(from, u8)[0] as i8),
429            Self::short => Scalar::short(_read_ne!(i16)),
430            Self::long => Scalar::long(_read_ne!(i32)),
431            Self::date => Scalar::date(_read_ne!(i32)),
432            Self::ldate => Scalar::ldate(_read_ne!(i32)),
433            Self::qwdate => Scalar::qwdate(_read_ne!(i64)),
434            Self::leshort => Scalar::leshort(_read_le!(i16)),
435            Self::lelong => Scalar::lelong(_read_le!(i32)),
436            Self::lequad => Scalar::lequad(_read_le!(i64)),
437            Self::bequad => Scalar::bequad(_read_be!(i64)),
438            Self::belong => Scalar::belong(_read_be!(i32)),
439            Self::bedate => Scalar::bedate(_read_be!(i32)),
440            Self::beldate => Scalar::beldate(_read_be!(i32)),
441            Self::beqdate => Scalar::beqdate(_read_be!(i64)),
442            Self::beqldate => Scalar::beqldate(_read_be!(i64)),
443            // unsigned
444            Self::ubyte => Scalar::ubyte(read!(from, u8)[0]),
445            Self::ushort => Scalar::ushort(_read_ne!(u16)),
446            Self::uleshort => Scalar::uleshort(_read_le!(u16)),
447            Self::ulelong => Scalar::ulelong(_read_le!(u32)),
448            Self::uledate => Scalar::uledate(_read_le!(u32)),
449            Self::ulequad => Scalar::ulequad(_read_le!(u64)),
450            Self::offset => Scalar::offset(from.stream_position()),
451            Self::ubequad => Scalar::ubequad(_read_be!(u64)),
452            Self::medate => Scalar::medate(_read_me!()),
453            Self::meldate => Scalar::meldate(_read_me!()),
454            Self::melong => Scalar::melong(_read_me!()),
455            Self::beshort => Scalar::beshort(_read_be!(i16)),
456            Self::quad => Scalar::quad(_read_ne!(i64)),
457            Self::uquad => Scalar::uquad(_read_ne!(u64)),
458            Self::ledate => Scalar::ledate(_read_le!(i32)),
459            Self::leldate => Scalar::leldate(_read_le!(i32)),
460            Self::leqdate => Scalar::leqdate(_read_le!(i64)),
461            Self::leqldate => Scalar::leqldate(_read_le!(i64)),
462            Self::leqwdate => Scalar::leqwdate(_read_le!(i64)),
463            Self::ubelong => Scalar::ubelong(_read_be!(u32)),
464            Self::ulong => Scalar::ulong(_read_ne!(u32)),
465            Self::ubeshort => Scalar::ubeshort(_read_be!(u16)),
466            Self::ubeqdate => Scalar::ubeqdate(_read_be!(u64)),
467            Self::lemsdosdate => Scalar::lemsdosdate(_read_le!(u16)),
468            Self::lemsdostime => Scalar::lemsdostime(_read_le!(u16)),
469            // Microsoft mixed-endian GUID: data1/data2/data3 little-endian, data4 raw.
470            Self::guid => Scalar::guid(
471                (_read_le!(u32) as u128) << 96
472                    | (_read_le!(u16) as u128) << 80
473                    | (_read_le!(u16) as u128) << 64
474                    | (_read_be!(u64) as u128),
475            ),
476            Self::leguid => Scalar::leguid(
477                (_read_le!(u32) as u128) << 96
478                    | (_read_le!(u16) as u128) << 80
479                    | (_read_le!(u16) as u128) << 64
480                    | (_read_be!(u64) as u128),
481            ),
482            Self::beguid => Scalar::beguid(_read_be!(u128)),
483        })
484    }
485}
486
487impl FloatDataType {
488    #[inline(always)]
489    fn read<R: DataRead>(&self, from: &mut R, switch_endianness: bool) -> Result<Float, Error> {
490        macro_rules! _read_le {
491            ($ty: ty) => {{
492                if switch_endianness {
493                    <$ty>::from_be_bytes(read!(from, $ty))
494                } else {
495                    <$ty>::from_le_bytes(read!(from, $ty))
496                }
497            }};
498        }
499
500        macro_rules! _read_be {
501            ($ty: ty) => {{
502                if switch_endianness {
503                    <$ty>::from_le_bytes(read!(from, $ty))
504                } else {
505                    <$ty>::from_be_bytes(read!(from, $ty))
506                }
507            }};
508        }
509
510        macro_rules! _read_ne {
511            ($ty: ty) => {{
512                if cfg!(target_endian = "big") {
513                    _read_be!($ty)
514                } else {
515                    _read_le!($ty)
516                }
517            }};
518        }
519
520        macro_rules! _read_me {
521            () => {
522                ((_read_le!(u16) as i32) << 16) | (_read_le!(u16) as i32)
523            };
524        }
525
526        Ok(match self {
527            Self::lefloat => Float::lefloat(_read_le!(f32)),
528            Self::befloat => Float::befloat(_read_le!(f32)),
529            Self::ledouble => Float::ledouble(_read_le!(f64)),
530            Self::bedouble => Float::bedouble(_read_be!(f64)),
531        })
532    }
533}
534
535#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
536enum Op {
537    Mul,
538    Add,
539    Sub,
540    Div,
541    Mod,
542    And,
543    Xor,
544    Or,
545}
546
547impl Display for Op {
548    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
549        match self {
550            Op::Mul => write!(f, "*"),
551            Op::Add => write!(f, "+"),
552            Op::Sub => write!(f, "-"),
553            Op::Div => write!(f, "/"),
554            Op::Mod => write!(f, "%"),
555            Op::And => write!(f, "&"),
556            Op::Or => write!(f, "|"),
557            Op::Xor => write!(f, "^"),
558        }
559    }
560}
561
562#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
563enum CmpOp {
564    Eq,
565    Lt,
566    Gt,
567    BitAnd,
568    Neq, // ! operator
569    Xor,
570    Not, // ~ operator
571}
572
573impl CmpOp {
574    #[inline(always)]
575    fn is_neq(&self) -> bool {
576        matches!(self, Self::Neq)
577    }
578}
579
580#[derive(Debug, Clone, Serialize, Deserialize)]
581struct ScalarTransform {
582    op: Op,
583    num: Scalar,
584}
585
586impl ScalarTransform {
587    fn apply(&self, s: Scalar) -> Option<Scalar> {
588        match self.op {
589            Op::Add => s.checked_add(self.num),
590            Op::Sub => s.checked_sub(self.num),
591            Op::Mul => s.checked_mul(self.num),
592            Op::Div => s.checked_div(self.num),
593            Op::Mod => s.checked_rem(self.num),
594            Op::And => Some(s.bitand(self.num)),
595            Op::Xor => Some(s.bitxor(self.num)),
596            Op::Or => Some(s.bitor(self.num)),
597        }
598    }
599}
600
601#[derive(Debug, Clone, Serialize, Deserialize)]
602struct FloatTransform {
603    op: Op,
604    num: Float,
605}
606
607impl FloatTransform {
608    fn apply(&self, s: Float) -> Float {
609        match self.op {
610            Op::Add => s.add(self.num),
611            Op::Sub => s.sub(self.num),
612            Op::Mul => s.mul(self.num),
613            // returns inf when div by 0
614            Op::Div => s.div(self.num),
615            // returns NaN when rem by 0
616            Op::Mod => s.rem(self.num),
617            // parser makes sure those operators cannot be used
618            Op::And | Op::Xor | Op::Or => {
619                debug_panic!("unsupported operation");
620                s
621            }
622        }
623    }
624}
625
626#[derive(Clone, Serialize, Deserialize)]
627enum TestValue<T> {
628    Value(T),
629    Any,
630}
631
632impl Debug for TestValue<Vec<u8>> {
633    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
634        match self {
635            Self::Value(v) => write!(f, "\"{}\"", debug_string_from_vec_u8(v)),
636            Self::Any => write!(f, "ANY"),
637        }
638    }
639}
640
641impl Debug for TestValue<Vec<u16>> {
642    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
643        match self {
644            Self::Value(v) => write!(f, "\"{}\"", debug_string_from_vec_u16(v)),
645            Self::Any => write!(f, "ANY"),
646        }
647    }
648}
649
650impl Debug for TestValue<Scalar> {
651    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
652        match self {
653            Self::Value(s) => write!(f, "{s:?}"),
654            Self::Any => write!(f, "ANY"),
655        }
656    }
657}
658
659impl Debug for TestValue<Float> {
660    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
661        match self {
662            Self::Value(fl) => write!(f, "{fl:?}"),
663            Self::Any => write!(f, "ANY"),
664        }
665    }
666}
667
668impl<T> TestValue<T> {
669    #[inline(always)]
670    fn as_ref(&self) -> TestValue<&T> {
671        match self {
672            Self::Value(v) => TestValue::Value(v),
673            Self::Any => TestValue::Any,
674        }
675    }
676}
677
678#[flags(u8)]
679#[derive(Debug, Serialize, Deserialize)]
680enum ReMod {
681    CaseInsensitive = 1 << 0,
682    StartOffsetUpdate = 1 << 1,
683    LineLimit = 1 << 2,
684    ForceBin = 1 << 3,
685    ForceText = 1 << 4,
686    TrimMatch = 1 << 5,
687}
688
689#[derive(Debug, Clone)]
690struct Regex {
691    s: String,
692    captures_len: usize,
693    re: OnceLock<bytes::Regex>,
694}
695
696#[derive(Serialize, Deserialize)]
697struct SerializedRegex {
698    s: String,
699    captures_len: usize,
700}
701
702impl Serialize for Regex {
703    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
704    where
705        S: serde::Serializer,
706    {
707        SerializedRegex {
708            s: self.s.clone(),
709            captures_len: self.captures_len,
710        }
711        .serialize(serializer)
712    }
713}
714
715impl<'de> Deserialize<'de> for Regex {
716    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
717    where
718        D: serde::Deserializer<'de>,
719    {
720        let sr = SerializedRegex::deserialize(deserializer)?;
721        Ok(Self {
722            s: sr.s,
723            captures_len: sr.captures_len,
724            re: OnceLock::new(),
725        })
726    }
727}
728
729impl Regex {
730    #[inline]
731    fn new(s: String) -> Result<Self, regex::Error> {
732        let compiled = bytes::Regex::new(&s)?;
733        let captures_len = compiled.captures_len();
734        let re = OnceLock::from(compiled);
735        Ok(Self {
736            s,
737            captures_len,
738            re,
739        })
740    }
741
742    #[inline]
743    fn get_re(&self) -> Result<&bytes::Regex, regex::Error> {
744        match self.re.get() {
745            Some(re) => Ok(re),
746            None => {
747                let _ = self.re.set(bytes::Regex::new(self.s.as_str())?);
748                Ok(self
749                    .re
750                    .get()
751                    .expect("unreachable: cell is guaranteed to contain a value"))
752            }
753        }
754    }
755}
756
757#[derive(Debug, Clone, Serialize, Deserialize)]
758struct RegexTest {
759    re: Regex,
760    length: Option<usize>,
761    mods: ReModFlags,
762    str_mods: StringModFlags,
763    non_magic_len: usize,
764    binary: bool,
765    cmp_op: CmpOp,
766}
767
768impl RegexTest {
769    fn match_buf<'buf>(
770        &self,
771        off_buf: u64, // absolute buffer offset in content
772        stream_kind: StreamKind,
773        buf: &'buf [u8],
774    ) -> Option<MatchRes<'buf>> {
775        let mr = match stream_kind {
776            StreamKind::Text(_) => {
777                let mut off_txt = off_buf;
778
779                let mut line_limit = self.length.unwrap_or(usize::MAX);
780
781                for line in buf.split(|c| c == &b'\n') {
782                    // we don't need to break on offset
783                    // limit as buf contains the good amount
784                    // of bytes to match against
785                    if line_limit == 0 {
786                        break;
787                    }
788
789                    if let Some(re_match) = self
790                        .re
791                        .get_re()
792                        .inspect_err(|e| {
793                            error!(
794                                "deferred compile of regex pattern {:?} failed: {e}",
795                                self.re.s
796                            )
797                        })
798                        .ok()?
799                        .find(line)
800                    {
801                        // the offset of the string is computed from the start of the buffer
802                        let start_offset = off_txt + re_match.start() as u64;
803
804                        // if we matched until EOL we need to add one to include the delimiter removed from the split
805                        let stop_offset = if re_match.end() == line.len() {
806                            Some(start_offset + re_match.as_bytes().len() as u64 + 1)
807                        } else {
808                            None
809                        };
810
811                        return Some(MatchRes::Bytes(
812                            start_offset,
813                            stop_offset,
814                            re_match.as_bytes(),
815                            Encoding::Utf8,
816                        ));
817                    }
818
819                    off_txt += line.len() as u64;
820                    // we have to add one because lines do not contain splitting character
821                    off_txt += 1;
822                    line_limit = line_limit.saturating_sub(1)
823                }
824                None
825            }
826
827            StreamKind::Binary => {
828                self.re
829                    .get_re()
830                    .inspect_err(|e| {
831                        error!(
832                            "deferred compile of regex pattern {:?} failed: {e}",
833                            self.re.s
834                        )
835                    })
836                    .ok()?
837                    .find(buf)
838                    .map(|re_match| {
839                        MatchRes::Bytes(
840                            // the offset of the string is computed from the start of the buffer
841                            off_buf + re_match.start() as u64,
842                            None,
843                            re_match.as_bytes(),
844                            Encoding::Utf8,
845                        )
846                    })
847            }
848        };
849
850        // handle the case where we want the regex not to match
851        if self.cmp_op.is_neq() && mr.is_none() {
852            return Some(MatchRes::Bytes(off_buf, None, buf, Encoding::Utf8));
853        }
854
855        mr
856    }
857}
858
859impl From<RegexTest> for Test {
860    fn from(value: RegexTest) -> Self {
861        Self::Regex(value)
862    }
863}
864
865#[flags(u8)]
866#[derive(Debug, Serialize, Deserialize)]
867enum StringMod {
868    ForceBin = 1 << 0,
869    UpperInsensitive = 1 << 1,
870    LowerInsensitive = 1 << 2,
871    FullWordMatch = 1 << 3,
872    Trim = 1 << 4,
873    ForceText = 1 << 5,
874    CompactWhitespace = 1 << 6,
875    OptBlank = 1 << 7,
876}
877
878impl StringModFlags {
879    /// Whether a match's consumption can genuinely exceed the matched
880    /// pattern's own length
881    #[inline(always)]
882    fn has_unbounded_length(&self) -> bool {
883        !self.is_disjoint(StringMod::CompactWhitespace | StringMod::OptBlank)
884    }
885
886    /// FullWordMatch needs to peek one byte past the pattern's own
887    /// length, to check that byte isn't part of the same word.
888    #[inline(always)]
889    fn word_boundary_lookahead(&self) -> u64 {
890        self.contains(StringMod::FullWordMatch) as u64
891    }
892}
893
894#[derive(Debug, Clone, Serialize, Deserialize)]
895struct StringTest {
896    test_val: TestValue<Vec<u8>>,
897    cmp_op: CmpOp,
898    length: Option<usize>,
899    mods: StringModFlags,
900    binary: bool,
901}
902
903impl From<StringTest> for Test {
904    fn from(value: StringTest) -> Self {
905        Self::String(value)
906    }
907}
908
909#[inline(always)]
910fn string_match(str: &[u8], mods: StringModFlags, buf: &[u8]) -> (bool, usize) {
911    let mut consumed = 0;
912    // we can do a simple string comparison
913    if mods.is_disjoint(
914        StringMod::UpperInsensitive
915            | StringMod::LowerInsensitive
916            | StringMod::FullWordMatch
917            | StringMod::CompactWhitespace
918            | StringMod::OptBlank,
919    ) {
920        // we check if target contains
921        if buf.starts_with(str) {
922            (true, str.len())
923        } else {
924            (false, consumed)
925        }
926    } else {
927        let mut i_src = 0;
928        let mut iter = buf.iter().peekable();
929
930        macro_rules! consume_target {
931            () => {{
932                if iter.next().is_some() {
933                    consumed += 1;
934                }
935            }};
936        }
937
938        macro_rules! continue_next_iteration {
939            () => {{
940                consume_target!();
941                i_src += 1;
942                continue;
943            }};
944        }
945
946        while let Some(&&b) = iter.peek() {
947            let Some(&ref_byte) = str.get(i_src) else {
948                break;
949            };
950
951            if mods.contains(StringMod::OptBlank) && (b == b' ' || ref_byte == b' ') {
952                if b == b' ' {
953                    // we ignore whitespace in target
954                    consume_target!();
955                }
956
957                if ref_byte == b' ' {
958                    // we ignore whitespace in test
959                    i_src += 1;
960                }
961
962                continue;
963            }
964
965            if mods.contains(StringMod::UpperInsensitive) {
966                //upper case characters in the magic match both lower and upper case characters in the target
967                if ref_byte.is_ascii_uppercase() && ref_byte == b.to_ascii_uppercase()
968                    || ref_byte == b
969                {
970                    continue_next_iteration!()
971                }
972            }
973
974            if mods.contains(StringMod::LowerInsensitive)
975                && (ref_byte.is_ascii_lowercase() && ref_byte == b.to_ascii_lowercase()
976                    || ref_byte == b)
977            {
978                continue_next_iteration!()
979            }
980
981            if mods.contains(StringMod::CompactWhitespace) && ref_byte == b' ' {
982                let mut src_blk = 0;
983                while let Some(b' ') = str.get(i_src) {
984                    src_blk += 1;
985                    i_src += 1;
986                }
987
988                let mut tgt_blk = 0;
989                while let Some(b' ') = iter.peek() {
990                    tgt_blk += 1;
991                    consume_target!();
992                }
993
994                if src_blk > tgt_blk {
995                    return (false, consumed);
996                }
997
998                continue;
999            }
1000
1001            if ref_byte == b {
1002                continue_next_iteration!()
1003            } else {
1004                return (false, consumed);
1005            }
1006        }
1007
1008        if mods.contains(StringMod::FullWordMatch)
1009            && let Some(b) = iter.peek()
1010            && !b.is_ascii_whitespace()
1011        {
1012            return (false, consumed);
1013        }
1014
1015        (
1016            consumed > 0 && str.get(i_src).is_none() && consumed <= buf.len(),
1017            consumed,
1018        )
1019    }
1020}
1021
1022impl StringTest {
1023    fn has_length_mod(&self) -> bool {
1024        !self.mods.is_disjoint(
1025            StringMod::UpperInsensitive
1026                | StringMod::LowerInsensitive
1027                | StringMod::FullWordMatch
1028                | StringMod::CompactWhitespace
1029                | StringMod::OptBlank,
1030        )
1031    }
1032
1033    #[inline(always)]
1034    fn test_value_len(&self) -> usize {
1035        match self.test_val.as_ref() {
1036            TestValue::Value(s) => s.len(),
1037            TestValue::Any => 0,
1038        }
1039    }
1040}
1041
1042#[derive(Clone, Serialize, Deserialize)]
1043struct ByteVec(Vec<u8>);
1044
1045impl Debug for ByteVec {
1046    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1047        write!(f, "\"{}\"", debug_string_from_vec_u8(self))
1048    }
1049}
1050
1051impl From<Vec<u8>> for ByteVec {
1052    fn from(value: Vec<u8>) -> Self {
1053        Self(value)
1054    }
1055}
1056
1057impl Deref for ByteVec {
1058    type Target = Vec<u8>;
1059
1060    fn deref(&self) -> &Self::Target {
1061        &self.0
1062    }
1063}
1064
1065#[derive(Debug, Clone, Serialize, Deserialize)]
1066struct SearchTest {
1067    str: ByteVec,
1068    n_pos: Option<usize>,
1069    str_mods: StringModFlags,
1070    re_mods: ReModFlags,
1071    binary: bool,
1072    cmp_op: CmpOp,
1073}
1074
1075impl From<SearchTest> for Test {
1076    fn from(value: SearchTest) -> Self {
1077        Self::Search(value)
1078    }
1079}
1080
1081impl SearchTest {
1082    // off_buf: absolute buffer offset in content
1083    #[inline]
1084    fn match_buf<'buf>(&self, off_buf: u64, buf: &'buf [u8]) -> Option<MatchRes<'buf>> {
1085        let mut i = 0;
1086
1087        let needle = self.str.first()?;
1088
1089        // A match may only *start* within n_pos -- same as libmagic's own
1090        // `idx < str_range` candidate loop
1091        let scan_end = self
1092            .n_pos
1093            .map(|n| n.saturating_add(1).min(buf.len()))
1094            .unwrap_or(buf.len());
1095
1096        while i < scan_end {
1097            // we cannot match if the first character isn't the same
1098            // so we accelerate the search by finding potential matches
1099            let Some(k) = memchr(*needle, &buf[i..scan_end]) else {
1100                break;
1101            };
1102
1103            i += k;
1104
1105            // if we want a full word match
1106            if self.str_mods.contains(StringMod::FullWordMatch) {
1107                let prev_is_whitespace = buf
1108                    .get(i.saturating_sub(1))
1109                    .map(|c| c.is_ascii_whitespace())
1110                    .unwrap_or_default();
1111
1112                // if it is not the first character
1113                // and its previous character isn't
1114                // a whitespace. It cannot be a
1115                // fullword match
1116                if i > 0 && !prev_is_whitespace {
1117                    i += 1;
1118                    continue;
1119                }
1120            }
1121
1122            let pos = i;
1123            let (ok, consumed) = string_match(&self.str, self.str_mods, &buf[i..]);
1124
1125            if ok {
1126                return Some(MatchRes::Bytes(
1127                    off_buf.saturating_add(pos as u64),
1128                    None,
1129                    &buf[i..i + consumed],
1130                    Encoding::Utf8,
1131                ));
1132            } else {
1133                i += max(consumed, 1)
1134            }
1135        }
1136
1137        // handles the case where we want the string not to be found
1138        if self.cmp_op.is_neq() {
1139            return Some(MatchRes::Bytes(off_buf, None, buf, Encoding::Utf8));
1140        }
1141
1142        None
1143    }
1144}
1145
1146#[derive(Debug, Clone, Serialize, Deserialize)]
1147struct ScalarTest {
1148    ty: ScalarDataType,
1149    transform: Option<ScalarTransform>,
1150    cmp_op: CmpOp,
1151    test_val: TestValue<Scalar>,
1152}
1153
1154#[derive(Debug, Clone, Serialize, Deserialize)]
1155struct FloatTest {
1156    ty: FloatDataType,
1157    transform: Option<FloatTransform>,
1158    cmp_op: CmpOp,
1159    test_val: TestValue<Float>,
1160}
1161
1162// the value read from the haystack we want to match against
1163// 'buf is the lifetime of the buffer we are scanning
1164#[derive(PartialEq)]
1165enum ReadValue<'buf> {
1166    Float(u64, Float),
1167    Scalar(u64, Scalar),
1168    Bytes(u64, &'buf [u8]),
1169}
1170
1171impl<'buf> Debug for ReadValue<'buf> {
1172    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1173        match self {
1174            Self::Float(_, fl) => write!(f, "{fl:?}"),
1175            Self::Scalar(_, s) => write!(f, "{s:?}"),
1176            Self::Bytes(_, b) => {
1177                if b.len() <= 128 {
1178                    write!(f, "\"{}\"", debug_string_from_vec_u8(b))
1179                } else {
1180                    let limit = 128;
1181                    write!(
1182                        f,
1183                        "\"{}\" (first {limit} bytes)",
1184                        debug_string_from_vec_u8(&b[..limit])
1185                    )
1186                }
1187            }
1188        }
1189    }
1190}
1191
1192impl DynDisplay for ReadValue<'_> {
1193    fn dyn_fmt(&self, f: &mut dyf::Formatter<'_>) -> dyf::Result {
1194        use std::fmt::Write;
1195        match self {
1196            Self::Float(_, s) => DynDisplay::dyn_fmt(s, f),
1197            Self::Scalar(_, s) => DynDisplay::dyn_fmt(s, f),
1198            Self::Bytes(_, b) => Ok(write!(f, "{b:?}")?),
1199        }
1200    }
1201}
1202
1203impl DynDisplay for &ReadValue<'_> {
1204    fn dyn_fmt(&self, f: &mut dyf::Formatter<'_>) -> dyf::Result {
1205        // Dereference self to get the TestValue and call its fmt method
1206        DynDisplay::dyn_fmt(*self, f)
1207    }
1208}
1209
1210impl Display for ReadValue<'_> {
1211    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1212        match self {
1213            Self::Float(_, v) => write!(f, "{v}"),
1214            Self::Scalar(_, s) => write!(f, "{s}"),
1215            Self::Bytes(_, b) => write!(f, "{b:?}"),
1216        }
1217    }
1218}
1219
1220enum Encoding {
1221    Utf16(String16Encoding),
1222    Utf8,
1223}
1224
1225// Carry the offset of the start of the data in the stream
1226// and the data itself
1227enum MatchRes<'buf> {
1228    // Bytes.0: offset of the match
1229    // Bytes.1: optional end of match (to address the need of EOL adjustment in string regex)
1230    // Bytes.2: the bytes matching
1231    // Bytes.3: encoding of the buffer
1232    Bytes(u64, Option<u64>, &'buf [u8], Encoding),
1233    Scalar(u64, Scalar),
1234    Float(u64, Float),
1235}
1236
1237impl DynDisplay for &MatchRes<'_> {
1238    fn dyn_fmt(&self, f: &mut dyf::Formatter) -> dyf::Result {
1239        (*self).dyn_fmt(f)
1240    }
1241}
1242
1243impl DynDisplay for MatchRes<'_> {
1244    fn dyn_fmt(&self, f: &mut dyf::Formatter) -> dyf::Result {
1245        match self {
1246            Self::Scalar(_, v) => v.dyn_fmt(f),
1247            Self::Float(_, v) => v.dyn_fmt(f),
1248            Self::Bytes(_, _, v, enc) => match enc {
1249                Encoding::Utf8 => String::from_utf8_lossy(v).to_string().dyn_fmt(f),
1250                Encoding::Utf16(enc) => {
1251                    let utf16: Vec<u16> = slice_to_utf16_iter(v, *enc).collect();
1252                    String::from_utf16_lossy(&utf16).dyn_fmt(f)
1253                }
1254            },
1255        }
1256    }
1257}
1258
1259impl MatchRes<'_> {
1260    // start offset of the match
1261    #[inline]
1262    fn start_offset(&self) -> u64 {
1263        match self {
1264            MatchRes::Bytes(o, _, _, _) => *o,
1265            MatchRes::Scalar(o, _) => *o,
1266            MatchRes::Float(o, _) => *o,
1267        }
1268    }
1269
1270    // start offset of the match
1271    #[inline]
1272    fn end_offset(&self) -> u64 {
1273        match self {
1274            MatchRes::Bytes(start, end, buf, _) => match end {
1275                Some(end) => *end,
1276                None => start.saturating_add(buf.len() as u64),
1277            },
1278            MatchRes::Scalar(o, sc) => o.add(sc.size_of() as u64),
1279            MatchRes::Float(o, f) => o.add(f.size_of() as u64),
1280        }
1281    }
1282}
1283
1284fn slice_to_utf16_iter(read: &[u8], encoding: String16Encoding) -> impl Iterator<Item = u16> {
1285    let even = read
1286        .iter()
1287        .enumerate()
1288        .filter(|(i, _)| i % 2 == 0)
1289        .map(|t| t.1);
1290
1291    let odd = read
1292        .iter()
1293        .enumerate()
1294        .filter(|(i, _)| i % 2 != 0)
1295        .map(|t| t.1);
1296
1297    even.zip(odd).map(move |(e, o)| match encoding {
1298        String16Encoding::Le => u16::from_le_bytes([*e, *o]),
1299        String16Encoding::Be => u16::from_be_bytes([*e, *o]),
1300    })
1301}
1302
1303#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
1304enum String16Encoding {
1305    Le,
1306    Be,
1307}
1308
1309#[derive(Debug, Clone, Serialize, Deserialize)]
1310struct String16Test {
1311    orig: String,
1312    test_val: TestValue<Vec<u16>>,
1313    encoding: String16Encoding,
1314}
1315
1316impl String16Test {
1317    /// if the test value is a specific value this method returns
1318    /// the number of utf16 characters. To obtain the length in
1319    /// bytes the return value needs to be multiplied by two.
1320    #[inline(always)]
1321    fn test_value_len(&self) -> usize {
1322        match self.test_val.as_ref() {
1323            TestValue::Value(str16) => str16.len(),
1324            TestValue::Any => 0,
1325        }
1326    }
1327}
1328
1329#[flags(u8)]
1330#[derive(Debug, Serialize, Deserialize)]
1331enum IndirectMod {
1332    Relative = 1 << 0,
1333}
1334
1335#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
1336enum PStringLen {
1337    Byte,    // B
1338    ShortBe, // H
1339    ShortLe, // h
1340    LongBe,  // L
1341    LongLe,  // l
1342}
1343
1344impl PStringLen {
1345    #[inline(always)]
1346    const fn size_of_len(&self) -> usize {
1347        match self {
1348            PStringLen::Byte => 1,
1349            PStringLen::ShortBe => 2,
1350            PStringLen::ShortLe => 2,
1351            PStringLen::LongBe => 4,
1352            PStringLen::LongLe => 4,
1353        }
1354    }
1355}
1356
1357#[derive(Debug, Clone, Serialize, Deserialize)]
1358struct PStringTest {
1359    len: PStringLen,
1360    test_val: TestValue<Vec<u8>>,
1361    include_len: bool,
1362}
1363
1364impl PStringTest {
1365    #[inline]
1366    fn read<'cache, R: DataRead>(
1367        &self,
1368        haystack: &'cache mut R,
1369    ) -> Result<Option<&'cache [u8]>, Error> {
1370        let mut len = match self.len {
1371            PStringLen::Byte => read_le!(haystack, u8) as u32,
1372            PStringLen::ShortBe => read_be!(haystack, u16) as u32,
1373            PStringLen::ShortLe => read_le!(haystack, u16) as u32,
1374            PStringLen::LongBe => read_be!(haystack, u32),
1375            PStringLen::LongLe => read_le!(haystack, u32),
1376        } as usize;
1377
1378        if self.include_len {
1379            len = len.saturating_sub(self.len.size_of_len())
1380        }
1381
1382        if let TestValue::Value(s) = self.test_val.as_ref()
1383            && len != s.len()
1384        {
1385            return Ok(None);
1386        }
1387
1388        let read = haystack.read_exact_count(len as u64)?;
1389
1390        Ok(Some(read))
1391    }
1392
1393    #[inline(always)]
1394    fn test_value_len(&self) -> usize {
1395        match self.test_val.as_ref() {
1396            TestValue::Value(s) => s.len(),
1397            TestValue::Any => 0,
1398        }
1399    }
1400}
1401
1402#[derive(Debug, Clone, Serialize, Deserialize)]
1403enum Test {
1404    Name(String),
1405    Use(bool, String),
1406    Scalar(ScalarTest),
1407    Float(FloatTest),
1408    String(StringTest),
1409    Search(SearchTest),
1410    PString(PStringTest),
1411    Regex(RegexTest),
1412    Indirect(IndirectModFlags),
1413    String16(String16Test),
1414    // FIXME: placeholder for strength computation
1415    #[allow(dead_code)]
1416    Der,
1417    Clear,
1418    Default,
1419}
1420
1421impl Display for Test {
1422    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1423        match self {
1424            Test::Name(name) => write!(f, "name {name}"),
1425            Test::Use(flip, rule) => {
1426                if *flip {
1427                    write!(f, "use {rule}")
1428                } else {
1429                    write!(f, "use ^{rule}")
1430                }
1431            }
1432            Test::Scalar(st) => write!(f, "{st:?}"),
1433            Test::Float(ft) => write!(f, "{ft:?}"),
1434            Test::String(st) => write!(f, "{st:?}"),
1435            Test::Search(st) => write!(f, "{st:?}"),
1436            Test::PString(pt) => write!(f, "{pt:?}"),
1437            Test::Regex(rt) => write!(f, "{rt:?}"),
1438            Test::Indirect(fs) => write!(f, "indirect {fs:?}"),
1439            Test::String16(s16t) => write!(f, "{s16t:?}"),
1440            Test::Der => write!(f, "unimplemented der"),
1441            Test::Clear => write!(f, "clear"),
1442            Test::Default => write!(f, "default"),
1443        }
1444    }
1445}
1446
1447impl Test {
1448    // read the value to test from the haystack
1449    #[inline]
1450    fn read_test_value<'haystack, D: DataRead>(
1451        &self,
1452        haystack: &'haystack mut D,
1453        switch_endianness: bool,
1454    ) -> Result<Option<ReadValue<'haystack>>, Error> {
1455        let test_value_offset = haystack.stream_position();
1456
1457        match self {
1458            Self::Scalar(t) => {
1459                t.ty.read(haystack, switch_endianness)
1460                    .map(|s| Some(ReadValue::Scalar(test_value_offset, s)))
1461            }
1462
1463            Self::Float(t) => {
1464                t.ty.read(haystack, switch_endianness)
1465                    .map(|f| Some(ReadValue::Float(test_value_offset, f)))
1466            }
1467            Self::String(t) => {
1468                match t.test_val.as_ref() {
1469                    TestValue::Value(str) => {
1470                        let buf = if let Some(length) = t.length {
1471                            // if there is a length specified
1472                            haystack.read_exact_count(length as u64)?
1473                        } else {
1474                            // no length specified we read until end of string
1475
1476                            match t.cmp_op {
1477                                CmpOp::Eq | CmpOp::Neq => {
1478                                    if t.mods.has_unbounded_length() {
1479                                        haystack.read_count(FILE_BYTES_MAX as u64)?
1480                                    } else {
1481                                        let len =
1482                                            str.len() as u64 + t.mods.word_boundary_lookahead();
1483                                        haystack.read_count(len)?
1484                                    }
1485                                }
1486                                CmpOp::Lt | CmpOp::Gt => {
1487                                    let read =
1488                                        haystack.read_until_any_delim_or_limit(b"\n\0", 8092)?;
1489
1490                                    if read.ends_with(b"\0") || read.ends_with(b"\n") {
1491                                        &read[..read.len() - 1]
1492                                    } else {
1493                                        read
1494                                    }
1495                                }
1496                                _ => {
1497                                    return Err(Error::Msg(format!(
1498                                        "string test does not support {:?} operator",
1499                                        t.cmp_op
1500                                    )));
1501                                }
1502                            }
1503                        };
1504
1505                        Ok(Some(ReadValue::Bytes(test_value_offset, buf)))
1506                    }
1507                    TestValue::Any => {
1508                        let read = haystack.read_until_any_delim_or_limit(b"\0\n", 8192)?;
1509                        // we don't take last byte if it matches end of string
1510                        let bytes = if read.ends_with(b"\0") || read.ends_with(b"\n") {
1511                            &read[..read.len() - 1]
1512                        } else {
1513                            read
1514                        };
1515
1516                        Ok(Some(ReadValue::Bytes(test_value_offset, bytes)))
1517                    }
1518                }
1519            }
1520
1521            Self::String16(t) => {
1522                match t.test_val.as_ref() {
1523                    TestValue::Value(str16) => {
1524                        let read = haystack.read_exact_count((str16.len() * 2) as u64)?;
1525
1526                        Ok(Some(ReadValue::Bytes(test_value_offset, read)))
1527                    }
1528                    TestValue::Any => {
1529                        let read = haystack.read_until_utf16_or_limit(b"\x00\x00", 8192)?;
1530
1531                        // we make sure we have an even number of elements
1532                        let end = if read.len() % 2 == 0 {
1533                            read.len()
1534                        } else {
1535                            // we decide to read anyway even though
1536                            // length isn't even
1537                            read.len().saturating_sub(1)
1538                        };
1539
1540                        Ok(Some(ReadValue::Bytes(test_value_offset, &read[..end])))
1541                    }
1542                }
1543            }
1544
1545            Self::PString(t) => {
1546                let Some(read) = t.read(haystack)? else {
1547                    return Ok(None);
1548                };
1549                Ok(Some(ReadValue::Bytes(test_value_offset, read)))
1550            }
1551
1552            Self::Search(s) => {
1553                let length = if s.str_mods.has_unbounded_length() {
1554                    FILE_BYTES_MAX as u64
1555                } else {
1556                    s.n_pos
1557                        .map(|n| n as u64)
1558                        .unwrap_or(FILE_BYTES_MAX as u64)
1559                        .saturating_add(s.str.len() as u64)
1560                        .saturating_add(s.str_mods.word_boundary_lookahead())
1561                };
1562                let buf = haystack.read_count(length)?;
1563                Ok(Some(ReadValue::Bytes(test_value_offset, buf)))
1564            }
1565
1566            Self::Regex(r) => {
1567                let length = {
1568                    match r.length {
1569                        Some(len) => {
1570                            if r.mods.contains(ReMod::LineLimit) {
1571                                len * 80
1572                            } else {
1573                                len
1574                            }
1575                        }
1576
1577                        None => FILE_REGEX_MAX,
1578                    }
1579                };
1580
1581                let read = haystack.read_count(length as u64)?;
1582                Ok(Some(ReadValue::Bytes(test_value_offset, read)))
1583            }
1584
1585            Self::Name(_)
1586            | Self::Use(_, _)
1587            | Self::Indirect(_)
1588            | Self::Clear
1589            | Self::Default
1590            | Self::Der => Err(Error::msg("no value to read for this test")),
1591        }
1592    }
1593
1594    #[inline(always)]
1595    fn match_value<'s>(
1596        &'s self,
1597        tv: &ReadValue<'s>,
1598        stream_kind: StreamKind,
1599    ) -> Option<MatchRes<'s>> {
1600        match (self, tv) {
1601            (Self::Scalar(t), ReadValue::Scalar(o, ts)) => {
1602                let read_value: Scalar = match t.transform.as_ref() {
1603                    Some(t) => t.apply(*ts)?,
1604                    None => *ts,
1605                };
1606
1607                match t.test_val {
1608                    TestValue::Value(test_value) => {
1609                        let ok = match t.cmp_op {
1610                            // NOTE: this should not happen in practice because
1611                            // we convert it into Eq equivalent at parsing time
1612                            CmpOp::Not => read_value == !test_value,
1613                            CmpOp::Eq => read_value == test_value,
1614                            CmpOp::Lt => read_value < test_value,
1615                            CmpOp::Gt => read_value > test_value,
1616                            CmpOp::Neq => read_value != test_value,
1617                            CmpOp::BitAnd => read_value & test_value == test_value,
1618                            CmpOp::Xor => (read_value & test_value).is_zero(),
1619                        };
1620
1621                        if ok {
1622                            Some(MatchRes::Scalar(*o, read_value))
1623                        } else {
1624                            None
1625                        }
1626                    }
1627
1628                    TestValue::Any => Some(MatchRes::Scalar(*o, read_value)),
1629                }
1630            }
1631
1632            (Self::Float(t), ReadValue::Float(o, f)) => {
1633                let read_value: Float = t.transform.as_ref().map(|t| t.apply(*f)).unwrap_or(*f);
1634
1635                match t.test_val {
1636                    TestValue::Value(tf) => {
1637                        let ok = match t.cmp_op {
1638                            CmpOp::Eq => read_value == tf,
1639                            CmpOp::Lt => read_value < tf,
1640                            CmpOp::Gt => read_value > tf,
1641                            CmpOp::Neq => read_value != tf,
1642                            _ => {
1643                                // this should never be reached as we validate
1644                                // operator in parser
1645                                debug_panic!("unsupported float comparison");
1646                                debug!("unsupported float comparison");
1647                                false
1648                            }
1649                        };
1650
1651                        if ok {
1652                            Some(MatchRes::Float(*o, read_value))
1653                        } else {
1654                            None
1655                        }
1656                    }
1657                    TestValue::Any => Some(MatchRes::Float(*o, read_value)),
1658                }
1659            }
1660
1661            (Self::String(st), ReadValue::Bytes(o, buf)) => {
1662                macro_rules! trim_buf {
1663                    ($buf: expr) => {{
1664                        if st.mods.contains(StringMod::Trim) {
1665                            $buf.trim_ascii()
1666                        } else {
1667                            $buf
1668                        }
1669                    }};
1670                }
1671
1672                match st.test_val.as_ref() {
1673                    TestValue::Value(str) => {
1674                        match st.cmp_op {
1675                            CmpOp::Eq => {
1676                                if let (true, _) = string_match(str, st.mods, buf) {
1677                                    Some(MatchRes::Bytes(*o, None, trim_buf!(str), Encoding::Utf8))
1678                                } else {
1679                                    None
1680                                }
1681                            }
1682                            CmpOp::Neq => {
1683                                if let (false, _) = string_match(str, st.mods, buf) {
1684                                    Some(MatchRes::Bytes(*o, None, trim_buf!(str), Encoding::Utf8))
1685                                } else {
1686                                    None
1687                                }
1688                            }
1689                            CmpOp::Gt => {
1690                                if buf.len() > str.len() {
1691                                    Some(MatchRes::Bytes(*o, None, trim_buf!(buf), Encoding::Utf8))
1692                                } else {
1693                                    None
1694                                }
1695                            }
1696                            CmpOp::Lt => {
1697                                if buf.len() < str.len() {
1698                                    Some(MatchRes::Bytes(*o, None, trim_buf!(buf), Encoding::Utf8))
1699                                } else {
1700                                    None
1701                                }
1702                            }
1703
1704                            // unsupported for strings
1705                            _ => {
1706                                // this should never be reached as we validate
1707                                // operator in parser
1708                                debug_panic!("unsupported string comparison");
1709                                debug!("unsupported string comparison");
1710                                None
1711                            }
1712                        }
1713                    }
1714                    TestValue::Any => {
1715                        Some(MatchRes::Bytes(*o, None, trim_buf!(buf), Encoding::Utf8))
1716                    }
1717                }
1718            }
1719
1720            (Self::PString(m), ReadValue::Bytes(o, buf)) => match m.test_val.as_ref() {
1721                TestValue::Value(psv) => {
1722                    if buf == psv {
1723                        Some(MatchRes::Bytes(*o, None, buf, Encoding::Utf8))
1724                    } else {
1725                        None
1726                    }
1727                }
1728                TestValue::Any => Some(MatchRes::Bytes(*o, None, buf, Encoding::Utf8)),
1729            },
1730
1731            (Self::String16(t), ReadValue::Bytes(o, buf)) => {
1732                match t.test_val.as_ref() {
1733                    TestValue::Value(str16) => {
1734                        // strings cannot be equal
1735                        if str16.len() * 2 != buf.len() {
1736                            return None;
1737                        }
1738
1739                        // we check string equality
1740                        for (i, utf16_char) in slice_to_utf16_iter(buf, t.encoding).enumerate() {
1741                            if str16[i] != utf16_char {
1742                                return None;
1743                            }
1744                        }
1745
1746                        Some(MatchRes::Bytes(
1747                            *o,
1748                            None,
1749                            t.orig.as_bytes(),
1750                            Encoding::Utf16(t.encoding),
1751                        ))
1752                    }
1753
1754                    TestValue::Any => {
1755                        Some(MatchRes::Bytes(*o, None, buf, Encoding::Utf16(t.encoding)))
1756                    }
1757                }
1758            }
1759
1760            (Self::Regex(r), ReadValue::Bytes(o, buf)) => r.match_buf(*o, stream_kind, buf),
1761
1762            (Self::Search(t), ReadValue::Bytes(o, buf)) => t.match_buf(*o, buf),
1763
1764            _ => None,
1765        }
1766    }
1767
1768    #[inline(always)]
1769    fn strength(&self) -> u64 {
1770        const MULT: usize = 10;
1771
1772        let mut out = 2 * MULT;
1773
1774        // FIXME: octal is missing but it is not used in practice ...
1775        match self {
1776            Test::Scalar(s) => {
1777                out += s.ty.type_size() * MULT;
1778            }
1779
1780            Test::Float(t) => {
1781                out += t.ty.type_size() * MULT;
1782            }
1783
1784            Test::String(t) => out += t.test_value_len().saturating_mul(MULT),
1785
1786            Test::PString(t) => out += t.test_value_len().saturating_mul(MULT),
1787
1788            Test::Search(s) => {
1789                // NOTE: this implementation deviates from what is in
1790                // C libmagic. The purpose of this implementation is to
1791                // minimize the difference between similar tests,
1792                // implemented differently (ex: string test VS very localized search test).
1793                let n_pos = s.n_pos.unwrap_or(FILE_BYTES_MAX);
1794
1795                match n_pos {
1796                    // a search on one line should be equivalent to a string match
1797                    0..=80 => out += s.str.len().saturating_mul(MULT),
1798                    // search on the first 3 lines gets a little penalty
1799                    81..=240 => out += s.str.len() * s.str.len().clamp(0, MULT - 2),
1800                    // a search on more than 3 lines isn't considered very accurate
1801                    _ => out += s.str.len(),
1802                }
1803            }
1804
1805            Test::Regex(r) => {
1806                // NOTE: this implementation deviates from what is in
1807                // C libmagic. The purpose of this implementation is to
1808                // minimize the difference between similar tests,
1809                // implemented differently (ex: string test VS very localized regex test).
1810
1811                // we divide length by the number of capture group
1812                // which gives us a value close to he average string
1813                // length match in the regex.
1814                let v = r.non_magic_len / r.re.captures_len.max(1);
1815
1816                let len = r
1817                    .length
1818                    .map(|l| {
1819                        if r.mods.contains(ReMod::LineLimit) {
1820                            l * 80
1821                        } else {
1822                            l
1823                        }
1824                    })
1825                    .unwrap_or(FILE_BYTES_MAX);
1826
1827                match len {
1828                    // a search on one line should be equivalent to a string match
1829                    0..=80 => out += v.saturating_mul(MULT),
1830                    // search on the first 3 lines gets a little penalty
1831                    81..=240 => out += v * v.clamp(0, MULT - 2),
1832                    // a search on more than 3 lines isn't considered very accurate
1833                    _ => out += v,
1834                }
1835            }
1836
1837            Test::String16(t) => {
1838                // NOTE: in libmagic the result is div by 2
1839                // but I GUESS it is because the len is expressed
1840                // in number bytes. In our case length is expressed
1841                // in number of u16 so we shouldn't divide.
1842                out += t.test_value_len().saturating_mul(MULT);
1843            }
1844
1845            Test::Der => out += MULT,
1846
1847            Test::Default | Test::Name(_) | Test::Use(_, _) | Test::Indirect(_) | Test::Clear => {
1848                return 0;
1849            }
1850        }
1851
1852        // matching any output gets penalty
1853        if self.is_match_any() {
1854            return 0;
1855        }
1856
1857        if let Some(op) = self.cmp_op() {
1858            match op {
1859                // matching almost any gets penalty
1860                CmpOp::Neq => out = 0,
1861                CmpOp::Eq | CmpOp::Not => out += MULT,
1862                CmpOp::Lt | CmpOp::Gt => out -= 2 * MULT,
1863                CmpOp::Xor | CmpOp::BitAnd => out -= MULT,
1864            }
1865        }
1866
1867        out as u64
1868    }
1869
1870    #[inline(always)]
1871    fn cmp_op(&self) -> Option<CmpOp> {
1872        match self {
1873            Self::String(t) => Some(t.cmp_op),
1874            Self::Scalar(s) => Some(s.cmp_op),
1875            Self::Float(t) => Some(t.cmp_op),
1876            Self::Search(t) => Some(t.cmp_op),
1877            Self::Regex(t) => Some(t.cmp_op),
1878            Self::Name(_)
1879            | Self::Use(_, _)
1880            | Self::PString(_)
1881            | Self::Clear
1882            | Self::Default
1883            | Self::Indirect(_)
1884            | Self::String16(_)
1885            | Self::Der => None,
1886        }
1887    }
1888
1889    #[inline(always)]
1890    fn is_recursive(&self) -> bool {
1891        matches!(self, Test::Use(_, _) | Test::Indirect(_))
1892    }
1893
1894    #[inline(always)]
1895    fn is_match_any(&self) -> bool {
1896        match self {
1897            Test::Name(_) => false,
1898            Test::Use(_, _) => false,
1899            Test::Scalar(scalar_test) => matches!(scalar_test.test_val, TestValue::Any),
1900            Test::Float(float_test) => matches!(float_test.test_val, TestValue::Any),
1901            Test::String(string_test) => matches!(string_test.test_val, TestValue::Any),
1902            Test::Search(_) => false,
1903            Test::PString(pstring_test) => matches!(pstring_test.test_val, TestValue::Any),
1904            Test::Regex(_) => false,
1905            Test::Indirect(_) => false,
1906            Test::String16(string16_test) => matches!(string16_test.test_val, TestValue::Any),
1907            Test::Der => false,
1908            Test::Clear => false,
1909            Test::Default => false,
1910        }
1911    }
1912
1913    /// Whether this test carries an explicit `/b` (force-binary)
1914    /// modifier, as opposed to a type's default classification.
1915    #[inline(always)]
1916    fn has_explicit_bin_mod(&self) -> bool {
1917        match self {
1918            Self::String(t) => t.mods.contains(StringMod::ForceBin),
1919            Self::Search(t) => t.str_mods.contains(StringMod::ForceBin),
1920            Self::Regex(t) => {
1921                t.mods.contains(ReMod::ForceBin) || t.str_mods.contains(StringMod::ForceBin)
1922            }
1923            _ => false,
1924        }
1925    }
1926
1927    /// Same as [`Test::has_explicit_bin_mod`], for an explicit `/t`
1928    /// (force-text) modifier.
1929    #[inline(always)]
1930    fn has_explicit_text_mod(&self) -> bool {
1931        match self {
1932            Self::String(t) => t.mods.contains(StringMod::ForceText),
1933            Self::Search(t) => t.str_mods.contains(StringMod::ForceText),
1934            Self::Regex(t) => {
1935                t.mods.contains(ReMod::ForceText) || t.str_mods.contains(StringMod::ForceText)
1936            }
1937            _ => false,
1938        }
1939    }
1940
1941    /// Type-based binary/text default, ignoring explicit `/b`/`/t`.
1942    /// Numeric/string/pstring/string16/DER default to binary;
1943    /// `regex`/`search` are content-sniffed; structural types have no
1944    /// binary-or-text nature, hence `None`.
1945    #[inline(always)]
1946    fn type_default_is_binary(&self) -> Option<bool> {
1947        match self {
1948            Self::Scalar(_) | Self::Float(_) | Self::Der => Some(true),
1949            Self::String(_) | Self::PString(_) | Self::String16(_) => Some(true),
1950            Self::Search(t) => Some(t.binary),
1951            Self::Regex(t) => Some(t.binary),
1952            Self::Name(_) | Self::Use(_, _) | Self::Indirect(_) | Self::Clear | Self::Default => {
1953                None
1954            }
1955        }
1956    }
1957
1958    /// Classifies the runtime stream-kind gate for this entry (cached on
1959    /// [`Match`]). `/bt` together means never skip, not a contradiction.
1960    /// Binary-default entries never skip either way; only default-text
1961    /// skips, and only on a binary stream.
1962    #[inline(always)]
1963    fn stream_gate(&self) -> StreamGate {
1964        let explicit_bin = self.has_explicit_bin_mod();
1965        let explicit_text = self.has_explicit_text_mod();
1966        if explicit_bin && explicit_text {
1967            return StreamGate::Never;
1968        }
1969        if explicit_bin {
1970            return StreamGate::SkipOnText;
1971        }
1972        if explicit_text || self.type_default_is_binary() == Some(false) {
1973            return StreamGate::SkipOnBinary;
1974        }
1975        StreamGate::Never
1976    }
1977
1978    /// Returns true if the [`Test`] consumes bytes
1979    #[inline(always)]
1980    fn consumes_bytes(&self) -> bool {
1981        !matches!(
1982            self,
1983            Test::Use(_, _) | Test::Indirect(_) | Test::Default | Test::Clear | Test::Name(_)
1984        )
1985    }
1986}
1987
1988/// Cached classification of an entry's runtime stream-kind gate. See
1989/// [`Test::stream_gate`].
1990#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1991enum StreamGate {
1992    SkipOnText,
1993    SkipOnBinary,
1994    Never,
1995}
1996
1997impl StreamGate {
1998    #[inline(always)]
1999    fn should_skip(self, stream_kind: StreamKind) -> bool {
2000        match self {
2001            Self::SkipOnText => stream_kind.is_text(),
2002            Self::SkipOnBinary => stream_kind.is_binary(),
2003            Self::Never => false,
2004        }
2005    }
2006}
2007
2008#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
2009enum OffsetType {
2010    Byte,
2011    DoubleLe,
2012    DoubleBe,
2013    ShortLe,
2014    ShortBe,
2015    Id3Le,
2016    Id3Be,
2017    LongLe,
2018    LongBe,
2019    Middle,
2020    Octal,
2021    QuadBe,
2022    QuadLe,
2023}
2024
2025#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
2026enum Shift {
2027    Direct(u64),
2028    Indirect(i64),
2029}
2030
2031#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
2032struct IndOffset {
2033    // where to find the offset
2034    off_addr: DirOffset,
2035    // signed or unsigned
2036    signed: bool,
2037    // type of the offset
2038    ty: OffsetType,
2039    op: Option<Op>,
2040    shift: Option<Shift>,
2041}
2042
2043impl IndOffset {
2044    // if we overflow we must not return an offset
2045    fn read_offset<D: DataRead>(
2046        &self,
2047        haystack: &mut D,
2048        offset_base: Option<u64>,
2049        last_upper_match_offset: Option<u64>,
2050    ) -> Result<Option<u64>, io::Error> {
2051        let offset_address = match self.off_addr {
2052            DirOffset::Start(s) => {
2053                let Some(o) = s.checked_add(offset_base.unwrap_or_default()) else {
2054                    return Ok(None);
2055                };
2056
2057                haystack.seek(SeekFrom::Start(o))?
2058            }
2059            DirOffset::LastUpper(c) => haystack.seek(SeekFrom::Start(
2060                (last_upper_match_offset.unwrap_or_default() as i64 + c) as u64,
2061            ))?,
2062            DirOffset::End(e) => haystack.seek(SeekFrom::End(e))?,
2063        };
2064
2065        macro_rules! read_value {
2066            () => {
2067                match self.ty {
2068                    OffsetType::Byte => {
2069                        if self.signed {
2070                            read_le!(haystack, u8) as u64
2071                        } else {
2072                            read_le!(haystack, i8) as u64
2073                        }
2074                    }
2075                    OffsetType::DoubleLe => read_le!(haystack, f64) as u64,
2076                    OffsetType::DoubleBe => read_be!(haystack, f64) as u64,
2077                    OffsetType::ShortLe => {
2078                        if self.signed {
2079                            read_le!(haystack, i16) as u64
2080                        } else {
2081                            read_le!(haystack, u16) as u64
2082                        }
2083                    }
2084                    OffsetType::ShortBe => {
2085                        if self.signed {
2086                            read_be!(haystack, i16) as u64
2087                        } else {
2088                            read_be!(haystack, u16) as u64
2089                        }
2090                    }
2091                    OffsetType::Id3Le => decode_id3(read_le!(haystack, u32)) as u64,
2092                    OffsetType::Id3Be => decode_id3(read_be!(haystack, u32)) as u64,
2093                    OffsetType::LongLe => {
2094                        if self.signed {
2095                            read_le!(haystack, i32) as u64
2096                        } else {
2097                            read_le!(haystack, u32) as u64
2098                        }
2099                    }
2100                    OffsetType::LongBe => {
2101                        if self.signed {
2102                            read_be!(haystack, i32) as u64
2103                        } else {
2104                            read_be!(haystack, u32) as u64
2105                        }
2106                    }
2107                    OffsetType::Middle => read_me!(haystack) as u64,
2108                    OffsetType::Octal => {
2109                        if let Some(o) = read_octal_u64(haystack) {
2110                            o
2111                        } else {
2112                            debug!("failed to read octal offset @ {offset_address}");
2113                            return Ok(None);
2114                        }
2115                    }
2116                    OffsetType::QuadLe => {
2117                        if self.signed {
2118                            read_le!(haystack, i64) as u64
2119                        } else {
2120                            read_le!(haystack, u64)
2121                        }
2122                    }
2123                    OffsetType::QuadBe => {
2124                        if self.signed {
2125                            read_be!(haystack, i64) as u64
2126                        } else {
2127                            read_be!(haystack, u64)
2128                        }
2129                    }
2130                }
2131            };
2132        }
2133
2134        // in theory every offset read should end up in something seekable from start, so we can use u64 to store the result
2135        let o = read_value!();
2136
2137        trace!(
2138            "offset read @ {offset_address} value={o} op={:?} shift={:?}",
2139            self.op, self.shift
2140        );
2141
2142        // apply transformation
2143        if let (Some(op), Some(shift)) = (self.op, self.shift) {
2144            let shift = match shift {
2145                Shift::Direct(i) => i,
2146                Shift::Indirect(i) => {
2147                    let tmp = offset_address as i128 + i as i128;
2148                    if tmp.is_negative() {
2149                        return Ok(None);
2150                    } else {
2151                        haystack.seek(SeekFrom::Start(tmp as u64))?;
2152                    };
2153                    // NOTE: here we assume that the shift has the same
2154                    // type as the main offset !
2155                    read_value!()
2156                }
2157            };
2158
2159            match op {
2160                Op::Add => return Ok(o.checked_add(shift)),
2161                Op::Mul => return Ok(o.checked_mul(shift)),
2162                Op::Sub => return Ok(o.checked_sub(shift)),
2163                Op::Div => return Ok(o.checked_div(shift)),
2164                Op::Mod => return Ok(o.checked_rem(shift)),
2165                Op::And => return Ok(Some(o & shift)),
2166                Op::Or => return Ok(Some(o | shift)),
2167                Op::Xor => return Ok(Some(o ^ shift)),
2168            }
2169        }
2170
2171        Ok(Some(o))
2172    }
2173}
2174
2175#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
2176enum DirOffset {
2177    Start(u64),
2178    // relative to the last up-level field
2179    LastUpper(i64),
2180    End(i64),
2181}
2182
2183#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
2184enum Offset {
2185    Direct(DirOffset),
2186    Indirect(IndOffset),
2187}
2188
2189impl Offset {
2190    #[inline(always)]
2191    fn is_indirect(&self) -> bool {
2192        matches!(self, Self::Indirect(_))
2193    }
2194}
2195
2196impl From<DirOffset> for Offset {
2197    fn from(value: DirOffset) -> Self {
2198        Self::Direct(value)
2199    }
2200}
2201
2202impl From<IndOffset> for Offset {
2203    fn from(value: IndOffset) -> Self {
2204        Self::Indirect(value)
2205    }
2206}
2207
2208impl Display for DirOffset {
2209    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2210        match self {
2211            DirOffset::Start(i) => write!(f, "{i}"),
2212            DirOffset::LastUpper(c) => write!(f, "&{c}"),
2213            DirOffset::End(e) => write!(f, "-{e}"),
2214        }
2215    }
2216}
2217
2218impl Default for DirOffset {
2219    fn default() -> Self {
2220        Self::LastUpper(0)
2221    }
2222}
2223
2224#[derive(Debug, Clone, Serialize, Deserialize)]
2225struct Match {
2226    line: usize,
2227    depth: u8,
2228    offset: Offset,
2229    test: Test,
2230    test_strength: u64,
2231    message: Option<Message>,
2232    stream_gate: StreamGate,
2233}
2234
2235impl From<Use> for Match {
2236    fn from(value: Use) -> Self {
2237        let test = Test::Use(value.switch_endianness, value.rule_name);
2238        let test_strength = test.strength();
2239        let stream_gate = test.stream_gate();
2240        Self {
2241            line: value.line,
2242            depth: value.depth,
2243            offset: value.start_offset,
2244            test,
2245            test_strength,
2246            message: value.message,
2247            stream_gate,
2248        }
2249    }
2250}
2251
2252impl From<Name> for Match {
2253    fn from(value: Name) -> Self {
2254        let test = Test::Name(value.name);
2255        let test_strength = test.strength();
2256        let stream_gate = test.stream_gate();
2257        Self {
2258            line: value.line,
2259            depth: 0,
2260            offset: Offset::Direct(DirOffset::Start(0)),
2261            test,
2262            test_strength,
2263            message: value.message,
2264            stream_gate,
2265        }
2266    }
2267}
2268
2269impl Match {
2270    /// Whether the runtime stream-kind gate should skip this entry.
2271    #[inline(always)]
2272    fn should_skip_for_stream(&self, stream_kind: StreamKind) -> bool {
2273        self.stream_gate.should_skip(stream_kind)
2274    }
2275
2276    /// Turns the `Match`'s offset into an absolute offset from the start of the stream
2277    #[inline(always)]
2278    fn offset_from_start<D: DataRead>(
2279        &self,
2280        haystack: &mut D,
2281        offset_base: Option<u64>,
2282        last_level_offset: Option<u64>,
2283    ) -> Result<Option<u64>, io::Error> {
2284        match self.offset {
2285            Offset::Direct(dir_offset) => match dir_offset {
2286                DirOffset::Start(s) => Ok(Some(s)),
2287                DirOffset::LastUpper(shift) => {
2288                    let o = last_level_offset.unwrap_or_default() as i64 + shift;
2289
2290                    if o >= 0 { Ok(Some(o as u64)) } else { Ok(None) }
2291                }
2292                DirOffset::End(e) => Ok(Some(haystack.offset_from_start(SeekFrom::End(e)))),
2293            },
2294            Offset::Indirect(ind_offset) => {
2295                let Some(o) = ind_offset.read_offset(haystack, offset_base, last_level_offset)?
2296                else {
2297                    return Ok(None);
2298                };
2299
2300                Ok(Some(o))
2301            }
2302        }
2303    }
2304
2305    /// This entry's own absolute anchor position.
2306    #[inline(always)]
2307    fn anchor_offset<D: DataRead>(
2308        &self,
2309        haystack: &mut D,
2310        buf_base_offset: Option<u64>,
2311        offset_base: Option<u64>,
2312        last_level_offset: Option<u64>,
2313    ) -> Result<Option<u64>, io::Error> {
2314        let Some(offset) = self.offset_from_start(haystack, offset_base, last_level_offset)? else {
2315            return Ok(None);
2316        };
2317
2318        Ok(Some(match self.offset {
2319            // the result we get for an indirect offset is relative to
2320            // the start of the libmagic buffer so we need to add base to
2321            // make it absolute.
2322            Offset::Indirect(_) => buf_base_offset.unwrap_or_default().saturating_add(offset),
2323            // Bare offsets are relative to offset_base -- see
2324            // EntryNode::matches for how it's computed.
2325            Offset::Direct(DirOffset::Start(_)) => {
2326                offset_base.unwrap_or_default().saturating_add(offset)
2327            }
2328            _ => offset,
2329        }))
2330    }
2331
2332    /// this method emulates the buffer based matching
2333    /// logic implemented in libmagic. It needs some aweful
2334    /// and weird offset convertions to turn buffer
2335    /// relative offsets (libmagic is based on) into
2336    /// absolute offset in the file.
2337    ///
2338    /// this method shoud bubble up only critical errors
2339    /// all the other errors should make the match result
2340    /// false and be logged via debug!
2341    ///
2342    /// the function returns an error if the maximum recursion
2343    /// has been reached or if a dependency rule is missing.
2344    #[inline]
2345    #[allow(clippy::too_many_arguments)]
2346    fn matches<'a: 'h, 'h, D: DataRead>(
2347        &'a self,
2348        source: Option<&str>,
2349        magic: &mut Magic<'a>,
2350        stream_kind: StreamKind,
2351        state: &mut MatchState,
2352        offset: u64,
2353        haystack: &'h mut D,
2354        switch_endianness: bool,
2355        db: &'a MagicDb,
2356        rec_depth: usize,
2357        test_depth: usize,
2358    ) -> Result<(bool, Option<MatchRes<'h>>), Error> {
2359        let source = source.unwrap_or("unknown");
2360        let line = self.line;
2361
2362        if rec_depth >= MAX_RECURSION {
2363            return Err(Error::localized(
2364                source,
2365                line,
2366                Error::MaximumRecursion(MAX_RECURSION),
2367            ));
2368        }
2369
2370        match &self.test {
2371            Test::Clear => {
2372                trace!("source={source} line={line} clear");
2373                state.clear_continuation_level(&self.continuation_level());
2374                Ok((true, None))
2375            }
2376
2377            Test::Name(name) => {
2378                trace!(
2379                    "source={source} line={line} running rule {name} switch_endianness={switch_endianness}",
2380                );
2381                Ok((true, None))
2382            }
2383
2384            Test::Use(flip_endianness, rule_name) => {
2385                trace!(
2386                    "source={source} line={line} use {rule_name} switch_endianness={flip_endianness}",
2387                );
2388
2389                // switch_endianness must propagate down the rule call stack
2390                let switch_endianness = switch_endianness ^ flip_endianness;
2391
2392                let dr: &DependencyRule = db.dependencies.get(rule_name).ok_or(
2393                    Error::localized(source, line, Error::MissingRule(rule_name.clone())),
2394                )?;
2395
2396                let new_buf_base_off = if self.offset.is_indirect() {
2397                    Some(offset)
2398                } else {
2399                    None
2400                };
2401
2402                let nmatch = dr.rule.magic(
2403                    magic,
2404                    stream_kind,
2405                    new_buf_base_off,
2406                    Some(offset),
2407                    haystack,
2408                    db,
2409                    switch_endianness,
2410                    rec_depth.saturating_add(1),
2411                    test_depth,
2412                )?;
2413
2414                // The name is always true, so we consider there to be a match
2415                // if more than one test succeeded
2416                let matched = nmatch > 0;
2417                if matched {
2418                    state.set_continuation_level(self.continuation_level());
2419                }
2420
2421                Ok((matched, None))
2422            }
2423
2424            Test::Indirect(m) => {
2425                trace!(
2426                    "source={source} line={line} indirect mods={:?} offset={offset:#x}",
2427                    m
2428                );
2429
2430                let new_buf_base_off = if m.contains(IndirectMod::Relative) {
2431                    Some(offset)
2432                } else {
2433                    None
2434                };
2435
2436                // Own message prints only if the scan matches, spliced before its output.
2437                let msg_checkpoint = magic.message.len();
2438
2439                let mut nmatch = 0u64;
2440                for r in db.rules.iter() {
2441                    nmatch = nmatch.saturating_add(r.magic(
2442                        magic,
2443                        stream_kind,
2444                        new_buf_base_off,
2445                        Some(offset),
2446                        haystack,
2447                        db,
2448                        false,
2449                        rec_depth.saturating_add(1),
2450                        test_depth,
2451                    )?);
2452
2453                    if nmatch > 0 {
2454                        break;
2455                    }
2456                }
2457
2458                if nmatch > 0
2459                    && let Some(msg) = self.message.as_ref()
2460                {
2461                    let msg = msg.to_string_lossy();
2462                    if !msg.is_empty() {
2463                        debug!("pushing message: msg={msg} len={}", msg.len());
2464                        magic.message.insert(msg_checkpoint, msg);
2465                    }
2466                }
2467
2468                Ok((nmatch > 0, None))
2469            }
2470
2471            Test::Default => {
2472                // default matches if nothing else at the continuation level matched
2473                let ok = !state.get_continuation_level(&self.continuation_level());
2474
2475                trace!("source={source} line={line} default match={ok}");
2476                if ok {
2477                    state.set_continuation_level(self.continuation_level());
2478                }
2479
2480                Ok((ok, None))
2481            }
2482
2483            _ => {
2484                if let Err(e) = haystack.seek(SeekFrom::Start(offset)) {
2485                    debug!("source={source} line={line} failed to seek in haystack: {e}");
2486                    return Ok((false, None));
2487                }
2488
2489                let mut trace_msg = None;
2490
2491                if enabled!(Level::DEBUG) {
2492                    trace_msg = Some(vec![format!(
2493                        "source={source} line={line} depth={} stream_offset={:#x}",
2494                        self.depth,
2495                        haystack.stream_position()
2496                    )])
2497                }
2498
2499                // NOTE: we may have a way to optimize here. In case we do a Any
2500                // test and we don't use the value to format the message, we don't
2501                // need to read the value.
2502                match self.test.read_test_value(haystack, switch_endianness) {
2503                    Err(e) => {
2504                        debug!(
2505                            "source={source} line={line} error while reading test value @{offset}: {e}",
2506                        );
2507
2508                        // libmagic mget(): unreadable data satisfies a `!=` test.
2509                        if self.test.cmp_op().is_some_and(|op| op.is_neq()) {
2510                            trace!(
2511                                "source={source} line={line} unreadable data treated as match for negated test",
2512                            );
2513                            state.set_continuation_level(self.continuation_level());
2514                            return Ok((true, None));
2515                        }
2516                    }
2517                    Ok(opt_test_value) => {
2518                        if let Some(v) = trace_msg.as_mut() {
2519                            v.push(format!("test={}", self.test))
2520                        }
2521
2522                        if let Some(v) = trace_msg.as_mut() {
2523                            let drv = match opt_test_value.as_ref() {
2524                                Some(r) => format!("{r:?}"),
2525                                None => String::new(),
2526                            };
2527                            v.push(format!("read_in_stream={drv}"))
2528                        }
2529
2530                        let match_res =
2531                            opt_test_value.and_then(|tv| self.test.match_value(&tv, stream_kind));
2532
2533                        if let Some(v) = trace_msg.as_mut() {
2534                            v.push(format!(
2535                                "message=\"{}\" match={}",
2536                                self.message
2537                                    .as_ref()
2538                                    .map(|fs| fs.to_string_lossy())
2539                                    .unwrap_or_default(),
2540                                match_res.is_some()
2541                            ))
2542                        }
2543
2544                        // trace message
2545                        if enabled!(Level::DEBUG) && !enabled!(Level::TRACE) && match_res.is_some()
2546                        {
2547                            if let Some(m) = trace_msg {
2548                                debug!("{}", m.join(" "));
2549                            }
2550                        } else if enabled!(Level::TRACE)
2551                            && let Some(m) = trace_msg
2552                        {
2553                            trace!("{}", m.join(" "));
2554                        }
2555
2556                        if let Some(mr) = match_res {
2557                            state.set_continuation_level(self.continuation_level());
2558                            return Ok((true, Some(mr)));
2559                        }
2560                    }
2561                }
2562
2563                Ok((false, None))
2564            }
2565        }
2566    }
2567
2568    #[inline(always)]
2569    fn continuation_level(&self) -> ContinuationLevel {
2570        ContinuationLevel(self.depth)
2571    }
2572}
2573
2574#[derive(Debug, Clone)]
2575struct Use {
2576    line: usize,
2577    depth: u8,
2578    start_offset: Offset,
2579    rule_name: String,
2580    switch_endianness: bool,
2581    message: Option<Message>,
2582}
2583
2584#[derive(Debug, Clone, Serialize, Deserialize)]
2585struct StrengthMod {
2586    op: Op,
2587    by: u8,
2588}
2589
2590impl StrengthMod {
2591    #[inline(always)]
2592    fn apply(&self, strength: u64) -> u64 {
2593        let by = self.by as u64;
2594        debug!("applying strength modifier: {strength} {} {}", self.op, by);
2595        match self.op {
2596            Op::Mul => strength.saturating_mul(by),
2597            Op::Add => strength.saturating_add(by),
2598            Op::Sub => strength.saturating_sub(by),
2599            Op::Div => {
2600                if by > 0 {
2601                    strength.saturating_div(by)
2602                } else {
2603                    strength
2604                }
2605            }
2606            Op::Mod => strength % by,
2607            Op::And => strength & by,
2608            // this should never happen as strength operators
2609            // are enforced by our parser
2610            Op::Xor | Op::Or => {
2611                debug_panic!("unsupported strength operator");
2612                strength
2613            }
2614        }
2615    }
2616}
2617
2618#[derive(Debug, Clone)]
2619enum Flag {
2620    Mime(String),
2621    Ext(HashSet<String>),
2622    Strength(StrengthMod),
2623    Apple(String),
2624}
2625
2626#[derive(Debug, Clone)]
2627struct Name {
2628    line: usize,
2629    name: String,
2630    message: Option<Message>,
2631}
2632
2633#[derive(Debug, Clone)]
2634enum Entry<'span> {
2635    Match(Span<'span>, Match),
2636    Flag(Span<'span>, Flag),
2637}
2638
2639#[derive(Debug, Clone, Serialize, Deserialize)]
2640struct EntryNode {
2641    root: bool,
2642    entry: Match,
2643    children: Vec<EntryNode>,
2644    mimetype: Option<String>,
2645    apple: Option<String>,
2646    strength_mod: Option<StrengthMod>,
2647    exts: HashSet<String>,
2648}
2649
2650#[derive(Debug, Default)]
2651struct EntryNodeVisitor {
2652    exts: HashSet<String>,
2653    score: u64,
2654    max_score: u64,
2655}
2656
2657impl EntryNodeVisitor {
2658    fn new() -> Self {
2659        Self {
2660            ..Default::default()
2661        }
2662    }
2663
2664    fn merge(&mut self, other: Self) {
2665        self.exts.extend(other.exts);
2666        self.max_score += other.max_score;
2667    }
2668}
2669
2670impl EntryNode {
2671    #[inline]
2672    fn update_visitor(&self, v: &mut EntryNodeVisitor, depth: usize) {
2673        // update extensions
2674        for ext in self.exts.iter() {
2675            if !v.exts.contains(ext) {
2676                v.exts.insert(ext.clone());
2677            }
2678        }
2679
2680        if self.root {
2681            let mut score = self.entry.test_strength;
2682            if let Some(sm) = self.strength_mod.as_ref() {
2683                score = sm.apply(score);
2684            }
2685            // libmagic gives entries with no message/description a bonus,
2686            // since they rely on later continuations to print anything.
2687            if self.entry.message.is_none() {
2688                score += 1;
2689            }
2690            v.score = score;
2691
2692            v.max_score = v
2693                .max_score
2694                .saturating_add(score)
2695                .saturating_add(self.best_path_bonus(depth));
2696        }
2697    }
2698
2699    /// The strongest additional strength achievable from a single
2700    /// root-to-leaf path through this node's local continuation tree.
2701    fn best_path_bonus(&self, depth: usize) -> u64 {
2702        self.children
2703            .iter()
2704            // `Test::Use` is excluded -- handled separately via `EntryNodeVisitor::merge`.
2705            .filter(|c| !matches!(c.entry.test, Test::Use(_, _)))
2706            .map(|c| {
2707                let d = depth.saturating_add(1);
2708                // Mirror runtime computation
2709                (c.entry.test_strength >> d).saturating_add(c.best_path_bonus(d))
2710            })
2711            .max()
2712            .unwrap_or(0)
2713    }
2714
2715    fn visit(
2716        &self,
2717        v: &mut EntryNodeVisitor,
2718        deps: &HashMap<String, DependencyRule>,
2719        marked: &mut HashSet<String>,
2720        depth: usize,
2721    ) -> Result<(), Error> {
2722        // updating visitor
2723        self.update_visitor(v, depth);
2724
2725        // Merge the dependency's contribution too, without skipping
2726        // recursion into this entry's own children below.
2727        if let Test::Use(_, ref name) = self.entry.test
2728            && !marked.contains(name)
2729        {
2730            marked.insert(name.clone());
2731
2732            if let Some(r) = deps.get(name) {
2733                let dv = r.rule.visit_all_entries(deps, marked, depth)?;
2734                v.merge(dv);
2735            } else {
2736                return Err(Error::MissingRule(name.clone()));
2737            }
2738        }
2739
2740        // recursively visiting
2741        for c in self.children.iter() {
2742            c.visit(v, deps, marked, depth + 1)?;
2743        }
2744
2745        Ok(())
2746    }
2747
2748    /// Executes the magic matching logic recursively and returns the count of matches that produce messages.
2749    /// Matches that don't result in message appends are not counted, consistent with libmagic's behavior.
2750    #[inline]
2751    #[allow(clippy::too_many_arguments)]
2752    fn matches<'r, D: DataRead>(
2753        &'r self,
2754        opt_source: Option<&str>,
2755        magic: &mut Magic<'r>,
2756        state: &mut MatchState,
2757        stream_kind: StreamKind,
2758        buf_base_offset: Option<u64>,
2759        offset_base: Option<u64>,
2760        last_level_offset: Option<u64>,
2761        haystack: &mut D,
2762        db: &'r MagicDb,
2763        switch_endianness: bool,
2764        rec_depth: usize,  // recursion depth
2765        test_depth: usize, // test entry depth
2766    ) -> Result<u64, Error> {
2767        let mut nmatch = 0u64;
2768        let source = opt_source.unwrap_or("unknown");
2769        let line = self.entry.line;
2770
2771        if self.root && self.entry.should_skip_for_stream(stream_kind) {
2772            trace!("skip test source={source} line={line} stream_kind={stream_kind:?}");
2773            return Ok(0);
2774        }
2775
2776        // Entry's own anchor position
2777        let Some(own_anchor) = self
2778            .entry
2779            .anchor_offset(haystack, buf_base_offset, offset_base, last_level_offset)
2780            .inspect_err(|e| debug!("source={source} line={line} failed at computing offset: {e}"))
2781            .ok()
2782            .flatten()
2783        else {
2784            // we cannot resolve offset at which we should match
2785            return Ok(0);
2786        };
2787
2788        let (ok, opt_match_res) = self.entry.matches(
2789            opt_source,
2790            magic,
2791            stream_kind,
2792            state,
2793            own_anchor,
2794            haystack,
2795            switch_endianness,
2796            db,
2797            rec_depth,
2798            test_depth,
2799        )?;
2800
2801        if ok {
2802            // Update the magic with the message if the match is successful
2803            // Skip updating if the test is recursive, as it's already handled
2804            // in the Match::matches function
2805            if !self.entry.test.is_recursive()
2806                && let Some(msg) = self.entry.message.as_ref()
2807                && let Ok(msg) = msg.format_with(opt_match_res.as_ref()).inspect_err(|e| {
2808                    debug!("source={source} line={line} failed to format message: {e}")
2809                })
2810            {
2811                nmatch = nmatch.saturating_add(1);
2812                magic.push_message(msg);
2813            }
2814
2815            // we need to adjust stream offset in case of regex/search tests
2816            if let Some(mr) = opt_match_res {
2817                match &self.entry.test {
2818                    Test::String(t) if t.has_length_mod() => {
2819                        let o = mr.end_offset();
2820                        haystack.seek(SeekFrom::Start(o))?;
2821                    }
2822                    Test::Search(t) => {
2823                        if t.re_mods.contains(ReMod::StartOffsetUpdate) {
2824                            let o = mr.start_offset();
2825                            haystack.seek(SeekFrom::Start(o))?;
2826                        } else {
2827                            let o = mr.end_offset();
2828                            haystack.seek(SeekFrom::Start(o))?;
2829                        }
2830                    }
2831
2832                    Test::Regex(t) => {
2833                        if t.mods.contains(ReMod::StartOffsetUpdate) {
2834                            let o = mr.start_offset();
2835                            haystack.seek(SeekFrom::Start(o))?;
2836                        } else {
2837                            let o = mr.end_offset();
2838                            haystack.seek(SeekFrom::Start(o))?;
2839                        }
2840                    }
2841                    // other types do not need offset adjustement
2842                    _ => {}
2843                }
2844            }
2845
2846            if let Some(mimetype) = self.mimetype.as_ref() {
2847                magic.set_mime_type(Cow::Borrowed(mimetype));
2848            }
2849
2850            if let Some(apple_ty) = self.apple.as_ref() {
2851                magic.set_creator_code(Cow::Borrowed(apple_ty));
2852            }
2853
2854            if !self.exts.is_empty() {
2855                magic.insert_extensions(self.exts.iter().map(|s| s.as_str()));
2856            }
2857
2858            // NOTE: here we try to implement a similar logic as in file_magic_strength.
2859            // Sticking to the exact same strength computation logic is complicated due
2860            // to implementation differences. Let's wait and see if that is a real issue.
2861            let mut strength = self.entry.test_strength;
2862
2863            strength >>= test_depth;
2864
2865            // `strength_mod` is only ever `Some` on the root entry
2866            if let Some(sm) = self.strength_mod.as_ref() {
2867                strength = sm.apply(strength);
2868            }
2869
2870            // entries with no message get a bonus
2871            if self.entry.message.is_none() {
2872                strength += 1
2873            }
2874
2875            magic.update_strength(strength);
2876
2877            // Position handed to this entry's children as their `&`-relative
2878            // base: the shared stream cursor, unless this entry doesn't
2879            // consume bytes itself, in which case that cursor can't be
2880            // trusted (e.g. left wherever `use`'s sub-scan ended up).
2881            let end_upper_level = if self.entry.test.consumes_bytes() {
2882                haystack.stream_position()
2883            } else {
2884                own_anchor
2885            };
2886
2887            // Anchor point for this entry's children: what a bare offset
2888            // like `>>0` (or the "N" in an indirect `(N.b)`) counts from.
2889            let offset_base = match self.entry.offset {
2890                // A true end-relative top-level entry (`-N`) establishes a
2891                // fresh anchor for its children
2892                Offset::Direct(DirOffset::End(_)) if self.root => Some(own_anchor),
2893                // An `&`-relative entry (`>&N`) clears the anchor for its
2894                // own children, back to absolute file start.
2895                Offset::Direct(DirOffset::LastUpper(_)) => None,
2896                // Anything else (a plain `Start` continuation, or a
2897                // non-end-relative root) leaves the inherited anchor as-is.
2898                _ => offset_base,
2899            };
2900
2901            for e in self.children.iter() {
2902                nmatch = nmatch.saturating_add(e.matches(
2903                    opt_source,
2904                    magic,
2905                    state,
2906                    stream_kind,
2907                    buf_base_offset,
2908                    offset_base,
2909                    Some(end_upper_level),
2910                    haystack,
2911                    db,
2912                    switch_endianness,
2913                    rec_depth,
2914                    test_depth.saturating_add(1),
2915                )?);
2916            }
2917        }
2918
2919        Ok(nmatch)
2920    }
2921}
2922
2923/// Represents a parsed magic rule
2924#[derive(Debug, Clone, Serialize, Deserialize)]
2925pub struct MagicRule {
2926    id: usize,
2927    source: Option<String>,
2928    entries: EntryNode,
2929    extensions: HashSet<String>,
2930    /// score used for rule ranking
2931    score: u64,
2932    /// cached result of the (non-trivial) text/binary classification
2933    is_text: bool,
2934    finalized: bool,
2935    max_score: u64,
2936}
2937
2938impl MagicRule {
2939    #[inline(always)]
2940    fn set_id(&mut self, id: usize) {
2941        self.id = id
2942    }
2943
2944    fn visit_all_entries(
2945        &self,
2946        deps: &HashMap<String, DependencyRule>,
2947        marked: &mut HashSet<String>,
2948        depth: usize,
2949    ) -> Result<EntryNodeVisitor, Error> {
2950        let mut v = EntryNodeVisitor::new();
2951        self.entries.visit(&mut v, deps, marked, depth)?;
2952        Ok(v)
2953    }
2954
2955    /// Finalize a rule by searching for all extensions and computing its
2956    /// score and text/binary classification for ranking. If the
2957    /// `MagicRule` is already finalized it returns immediately.
2958    fn try_finalize(&mut self, deps: &HashMap<String, DependencyRule>) -> Result<(), Error> {
2959        if self.finalized {
2960            return Ok(());
2961        }
2962
2963        // rule can be finalized all deps are found
2964        let v = self.visit_all_entries(deps, &mut HashSet::new(), 0)?;
2965
2966        self.extensions.extend(v.exts);
2967        self.score = v.score;
2968        self.is_text = self.compute_is_text();
2969        self.finalized = true;
2970        self.max_score = v.max_score;
2971
2972        Ok(())
2973    }
2974
2975    #[inline]
2976    #[allow(clippy::too_many_arguments)]
2977    fn magic_entrypoint<'r, D: DataRead>(
2978        &'r self,
2979        magic: &mut Magic<'r>,
2980        stream_kind: StreamKind,
2981        haystack: &mut D,
2982        db: &'r MagicDb,
2983        switch_endianness: bool,
2984        rec_depth: usize,
2985        test_depth: usize,
2986    ) -> Result<u64, Error> {
2987        self.entries.matches(
2988            self.source.as_deref(),
2989            magic,
2990            &mut MatchState::empty(),
2991            stream_kind,
2992            None,
2993            None,
2994            None,
2995            haystack,
2996            db,
2997            switch_endianness,
2998            rec_depth,
2999            test_depth,
3000        )
3001    }
3002
3003    /// Executes the magic matching logic and returns the count of matches that produce messages.
3004    /// Matches that don't result in message appends are not counted, consistent with libmagic's behavior.
3005    #[inline]
3006    #[allow(clippy::too_many_arguments)]
3007    fn magic<'r, D: DataRead>(
3008        &'r self,
3009        magic: &mut Magic<'r>,
3010        stream_kind: StreamKind,
3011        buf_base_offset: Option<u64>,
3012        offset_base: Option<u64>,
3013        haystack: &mut D,
3014        db: &'r MagicDb,
3015        switch_endianness: bool,
3016        rec_depth: usize,
3017        test_depth: usize,
3018    ) -> Result<u64, Error> {
3019        self.entries.matches(
3020            self.source.as_deref(),
3021            magic,
3022            &mut MatchState::empty(),
3023            stream_kind,
3024            buf_base_offset,
3025            offset_base,
3026            None,
3027            haystack,
3028            db,
3029            switch_endianness,
3030            rec_depth,
3031            test_depth,
3032        )
3033    }
3034
3035    /// Checks if the rule is for matching against text content.
3036    ///
3037    /// # Returns
3038    ///
3039    /// * `bool` - True if the rule is for text files
3040    #[inline(always)]
3041    pub fn is_text(&self) -> bool {
3042        self.is_text
3043    }
3044
3045    /// Computes the rule's text/binary classification, used for rule
3046    /// ranking. Decided per rule group, not per entry: an explicit
3047    /// `/b`/`/t` on the top-level entry decides the whole group;
3048    /// otherwise it's an OR of each entry's own type default across the
3049    /// group (top + children), with binary taking priority over text if
3050    /// both appear.
3051    fn compute_is_text(&self) -> bool {
3052        let top = &self.entries.entry.test;
3053
3054        if top.has_explicit_bin_mod() {
3055            return false;
3056        }
3057        if top.has_explicit_text_mod() {
3058            return true;
3059        }
3060
3061        // Binary wins on conflict, so the first `Some(true)` already
3062        // pins the answer to `false` -- no need to scan further.
3063        let mut any_text = false;
3064        for t in std::iter::once(top).chain(self.entries.children.iter().map(|e| &e.entry.test)) {
3065            match t.type_default_is_binary() {
3066                Some(true) => return false,
3067                Some(false) => any_text = true,
3068                None => {}
3069            }
3070        }
3071
3072        any_text
3073    }
3074
3075    /// Gets the rule's score used for ranking rules between them
3076    ///
3077    /// # Returns
3078    ///
3079    /// * `u64` - The rule's score
3080    #[inline(always)]
3081    pub fn score(&self) -> u64 {
3082        self.score
3083    }
3084
3085    /// Gets the rule's filename if any
3086    ///
3087    /// # Returns
3088    ///
3089    /// * `Option<&str>` - The rule's source if available
3090    #[inline(always)]
3091    pub fn source(&self) -> Option<&str> {
3092        self.source.as_deref()
3093    }
3094
3095    /// Gets the line number at which the rule is defined
3096    ///
3097    /// # Returns
3098    ///
3099    /// * `usize` - The rule's line number
3100    #[inline(always)]
3101    pub fn line(&self) -> usize {
3102        self.entries.entry.line
3103    }
3104
3105    /// Gets all the file extensions associated to the rule
3106    ///
3107    /// # Returns
3108    ///
3109    /// * `&HashSet<String>` - The set of all associated extensions
3110    #[inline(always)]
3111    pub fn extensions(&self) -> &HashSet<String> {
3112        &self.extensions
3113    }
3114}
3115
3116#[derive(Debug, Clone, Serialize, Deserialize)]
3117struct DependencyRule {
3118    name: String,
3119    rule: MagicRule,
3120}
3121
3122/// A parsed source of magic rules
3123///
3124/// # Methods
3125///
3126/// * `open` - Opens a magic file from a path
3127#[derive(Debug, Clone, Serialize, Deserialize)]
3128pub struct MagicSource {
3129    rules: Vec<MagicRule>,
3130    dependencies: HashMap<String, DependencyRule>,
3131}
3132
3133impl MagicSource {
3134    /// Opens and parses a magic file from a path
3135    ///
3136    /// # Arguments
3137    ///
3138    /// * `p` - The path to the magic file
3139    ///
3140    /// # Returns
3141    ///
3142    /// * `Result<Self, Error>` - The parsed magic file or an error
3143    pub fn open<P: AsRef<Path>>(p: P) -> Result<Self, Error> {
3144        FileMagicParser::parse_file(p)
3145    }
3146}
3147
3148#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)]
3149struct ContinuationLevel(u8);
3150
3151/// The encoding encountered for a given `StreamKind::Text`
3152// FIXME: magic handles many more text encodings
3153#[derive(Debug, PartialEq, Eq, Clone, Copy)]
3154pub enum TextEncoding {
3155    /// ASCII
3156    Ascii,
3157    /// UTF-8
3158    Utf8,
3159    /// Unspecified
3160    Unknown,
3161}
3162
3163impl TextEncoding {
3164    const fn as_magic_str(&self) -> &'static str {
3165        match self {
3166            TextEncoding::Ascii => "ASCII",
3167            TextEncoding::Utf8 => "UTF-8",
3168            TextEncoding::Unknown => "Unknown",
3169        }
3170    }
3171}
3172
3173/// Represents the kind of encountered data
3174#[derive(Debug, PartialEq, Eq, Clone, Copy)]
3175pub enum StreamKind {
3176    /// A binary stream
3177    Binary,
3178    /// A text stream with encoding information
3179    Text(TextEncoding),
3180}
3181
3182impl StreamKind {
3183    /// Returns whether this is a text stream
3184    ///
3185    /// # Returns
3186    ///
3187    /// * `bool` - True if this is a StreamKind::Text
3188    #[inline(always)]
3189    pub const fn is_text(&self) -> bool {
3190        matches!(self, StreamKind::Text(_))
3191    }
3192
3193    /// Returns whether this is a binary stream
3194    ///
3195    /// # Returns
3196    ///
3197    /// * `bool` - True if this is a StreamKind::Binary
3198    #[inline(always)]
3199    pub const fn is_binary(&self) -> bool {
3200        matches!(self, StreamKind::Binary)
3201    }
3202
3203    /// Returns a stable, machine-readable string representation.
3204    ///
3205    /// # Returns
3206    ///
3207    /// * `&'static str` - One of `"binary"`, `"text/ascii"`,
3208    ///   `"text/utf-8"` or `"text/unknown"`
3209    #[inline(always)]
3210    pub const fn as_str(&self) -> &'static str {
3211        match self {
3212            StreamKind::Binary => "binary",
3213            StreamKind::Text(TextEncoding::Ascii) => "text/ascii",
3214            StreamKind::Text(TextEncoding::Utf8) => "text/utf-8",
3215            StreamKind::Text(TextEncoding::Unknown) => "text/unknown",
3216        }
3217    }
3218}
3219
3220#[derive(Debug)]
3221struct MatchState {
3222    continuation_levels: [bool; 256],
3223}
3224
3225impl MatchState {
3226    #[inline(always)]
3227    fn empty() -> Self {
3228        MatchState {
3229            continuation_levels: [false; 256],
3230        }
3231    }
3232
3233    #[inline(always)]
3234    fn get_continuation_level(&mut self, level: &ContinuationLevel) -> bool {
3235        self.continuation_levels
3236            .get(level.0 as usize)
3237            .cloned()
3238            .unwrap_or_default()
3239    }
3240
3241    #[inline(always)]
3242    fn set_continuation_level(&mut self, level: ContinuationLevel) {
3243        if let Some(b) = self.continuation_levels.get_mut(level.0 as usize) {
3244            *b = true
3245        }
3246    }
3247
3248    #[inline(always)]
3249    fn clear_continuation_level(&mut self, level: &ContinuationLevel) {
3250        if let Some(b) = self.continuation_levels.get_mut(level.0 as usize) {
3251            *b = false;
3252        }
3253    }
3254}
3255
3256/// Represents a file magic detection result
3257#[derive(Debug, Default)]
3258pub struct Magic<'m> {
3259    stream_kind: Option<StreamKind>,
3260    source: Option<Cow<'m, str>>,
3261    message: Vec<Cow<'m, str>>,
3262    mime_type: Option<Cow<'m, str>>,
3263    creator_code: Option<Cow<'m, str>>,
3264    strength: u64,
3265    exts: HashSet<Cow<'m, str>>,
3266    is_default: bool,
3267}
3268
3269impl<'m> Magic<'m> {
3270    #[inline(always)]
3271    fn set_source(&mut self, source: Option<&'m str>) {
3272        self.source = source.map(Cow::Borrowed);
3273    }
3274
3275    #[inline(always)]
3276    fn set_stream_kind(&mut self, stream_kind: StreamKind) {
3277        self.stream_kind = Some(stream_kind)
3278    }
3279
3280    /// Gets the detected `StreamKind` describing the type of detected data,
3281    /// or `None` if it was not determined.
3282    ///
3283    /// # Returns
3284    ///
3285    /// * `Option<StreamKind>` - The detected kind or `None`
3286    #[inline(always)]
3287    pub fn stream_kind(&self) -> Option<StreamKind> {
3288        self.stream_kind
3289    }
3290
3291    #[inline(always)]
3292    fn reset(&mut self) {
3293        self.stream_kind = None;
3294        self.source = None;
3295        self.message.clear();
3296        self.mime_type = None;
3297        self.creator_code = None;
3298        self.strength = 0;
3299        self.exts.clear();
3300        self.is_default = false;
3301    }
3302
3303    /// Converts borrowed data into owned data. This method involves
3304    /// data cloning, so you must use this method only if you need to
3305    /// extend the lifetime of a [`Magic`] struct.
3306    ///
3307    /// # Returns
3308    ///
3309    /// * `Magic<'owned>` - A new [`Magic`] with owned data
3310    #[inline]
3311    pub fn into_owned<'owned>(self) -> Magic<'owned> {
3312        Magic {
3313            stream_kind: self.stream_kind,
3314            source: self.source.map(|s| Cow::Owned(s.into_owned())),
3315            message: self
3316                .message
3317                .into_iter()
3318                .map(Cow::into_owned)
3319                .map(Cow::Owned)
3320                .collect(),
3321            mime_type: self.mime_type.map(|m| Cow::Owned(m.into_owned())),
3322            creator_code: self.creator_code.map(|m| Cow::Owned(m.into_owned())),
3323            strength: self.strength,
3324            exts: self
3325                .exts
3326                .into_iter()
3327                .map(|e| Cow::Owned(e.into_owned()))
3328                .collect(),
3329            is_default: self.is_default,
3330        }
3331    }
3332
3333    /// Gets the formatted message describing the file type
3334    ///
3335    /// # Returns
3336    ///
3337    /// * `String` - The formatted message
3338    ///
3339    /// # Note
3340    ///
3341    /// The returned string only ever contains printable ASCII (graphic
3342    /// characters and spaces). Any other byte, including non-ASCII UTF-8
3343    /// sequences, control characters and Unicode format/bidi characters, is
3344    /// escaped as `\NNN` (octal), the same way libmagic does. This makes the
3345    /// output safe to print directly to a terminal.
3346    #[inline(always)]
3347    pub fn message(&self) -> String {
3348        let mut out = String::new();
3349
3350        macro_rules! push_str {
3351            ($input: expr) => {{
3352                for c in $input.as_bytes() {
3353                    match c {
3354                        b'\r' => out.push_str("\\r"),
3355                        b'\t' => out.push_str("\\t"),
3356                        b'\n' => out.push_str("\\n"),
3357                        b' ' => out.push(' '),
3358                        c if c.is_ascii_graphic() => out.push(*c as char),
3359                        // the way libmagic handles non printable bytes
3360                        _ => out.push_str(&format!("\\{:03o}", *c as u8)),
3361                    }
3362                }
3363            }};
3364        }
3365
3366        for (i, m) in self.message.iter().enumerate() {
3367            if let Some(s) = m.strip_prefix(r#"\b"#) {
3368                push_str!(s);
3369            } else {
3370                // don't put space on first string
3371                if i > 0 {
3372                    out.push(' ');
3373                }
3374                push_str!(m);
3375            }
3376        }
3377        out
3378    }
3379
3380    /// Returns an iterator over the individual raw parts of the magic message
3381    ///
3382    /// A magic message is typically composed of multiple parts, each appended
3383    /// during successful magic tests. This method provides an efficient way to
3384    /// iterate over these parts without concatenating them into a new string,
3385    /// as done when calling [`Magic::message`].
3386    ///
3387    /// # Returns
3388    ///
3389    /// * `impl Iterator<Item = &str>` - An iterator yielding string slices of each message part
3390    ///
3391    /// # Note
3392    ///
3393    /// Unlike [`Magic::message`], the returned parts are **not** sanitized:
3394    /// they may contain raw control characters (e.g. `ESC`) extracted from
3395    /// the scanned data. Do not print them directly to a terminal, as this
3396    /// could result in terminal escape sequence injection. This method is
3397    /// intended for programmatic inspection only.
3398    #[inline]
3399    pub fn message_parts(&self) -> impl Iterator<Item = &str> {
3400        self.message.iter().map(|p| p.as_ref())
3401    }
3402
3403    #[inline(always)]
3404    fn update_strength(&mut self, value: u64) {
3405        debug!("update strength = {} + {value}", self.strength);
3406        self.strength = self.strength.saturating_add(value);
3407        debug!("updated strength = {}", self.strength)
3408    }
3409
3410    /// Gets the detected MIME type
3411    ///
3412    /// # Returns
3413    ///
3414    /// * `&str` - The MIME type or default based on stream kind
3415    #[inline(always)]
3416    pub fn mime_type(&self) -> &str {
3417        self.mime_type.as_deref().unwrap_or(match self.stream_kind {
3418            Some(StreamKind::Text(_)) => DEFAULT_TEXT_MIMETYPE,
3419            Some(StreamKind::Binary) | None => DEFAULT_BIN_MIMETYPE,
3420        })
3421    }
3422
3423    #[inline(always)]
3424    fn push_message<'a: 'm>(&mut self, msg: Cow<'a, str>) {
3425        if !msg.is_empty() {
3426            debug!("pushing message: msg={msg} len={}", msg.len());
3427            self.message.push(msg);
3428        }
3429    }
3430
3431    #[inline(always)]
3432    fn set_mime_type<'a: 'm>(&mut self, mime: Cow<'a, str>) {
3433        if self.mime_type.is_none() {
3434            debug!("insert mime: {:?}", mime);
3435            self.mime_type = Some(mime)
3436        }
3437    }
3438
3439    #[inline(always)]
3440    fn set_creator_code<'a: 'm>(&mut self, apple_ty: Cow<'a, str>) {
3441        if self.creator_code.is_none() {
3442            debug!("insert apple type: {apple_ty:?}");
3443            self.creator_code = Some(apple_ty)
3444        }
3445    }
3446
3447    #[inline(always)]
3448    fn insert_extensions<'a: 'm, I: Iterator<Item = &'a str>>(&mut self, exts: I) {
3449        if self.exts.is_empty() {
3450            self.exts.extend(exts.filter_map(|e| {
3451                if e.is_empty() {
3452                    None
3453                } else {
3454                    Some(Cow::Borrowed(e))
3455                }
3456            }));
3457        }
3458    }
3459
3460    /// Gets the confidence score of the detection. This
3461    /// value is used to sort [`Magic`] in [`MagicDb::best_magic`]
3462    /// and [`MagicDb::all_magics`].
3463    ///
3464    /// # Returns
3465    ///
3466    /// * `u64` - The confidence score attributed to that [`Magic`]
3467    #[inline(always)]
3468    pub fn strength(&self) -> u64 {
3469        self.strength
3470    }
3471
3472    /// Gets the filename where the magic rule was defined
3473    ///
3474    /// # Returns
3475    ///
3476    /// * `Option<&str>` - The source if available
3477    #[inline(always)]
3478    pub fn source(&self) -> Option<&str> {
3479        self.source.as_deref()
3480    }
3481
3482    /// Gets the Apple creator code if available
3483    ///
3484    /// # Returns
3485    ///
3486    /// * `Option<&str>` - The creator code if available
3487    #[inline(always)]
3488    pub fn creator_code(&self) -> Option<&str> {
3489        self.creator_code.as_deref()
3490    }
3491
3492    /// Gets the possible file extensions for the detected [`Magic`]
3493    ///
3494    /// # Returns
3495    ///
3496    /// * `&HashSet<Cow<'m, str>>` - The set of possible extensions
3497    #[inline(always)]
3498    pub fn extensions(&self) -> &HashSet<Cow<'m, str>> {
3499        &self.exts
3500    }
3501
3502    /// Checks if this is a default fallback detection
3503    ///
3504    /// # Returns
3505    ///
3506    /// * `bool` - True if this is a default detection
3507    #[inline(always)]
3508    pub fn is_default(&self) -> bool {
3509        self.is_default
3510    }
3511}
3512
3513/// Represents a database of [`MagicRule`]
3514#[derive(Debug, Default, Clone, Serialize, Deserialize)]
3515pub struct MagicDb {
3516    rule_id: usize,
3517    rules: Vec<MagicRule>,
3518    dependencies: HashMap<String, DependencyRule>,
3519    finalized: usize,
3520}
3521
3522#[inline(always)]
3523/// Returns `true` if the byte stream is likely text.
3524fn is_likely_text(bytes: &[u8]) -> bool {
3525    const CHUNK_SIZE: usize = std::mem::size_of::<usize>();
3526
3527    if bytes.is_empty() {
3528        return false;
3529    }
3530
3531    let mut printable = 0f64;
3532    let mut high_bytes = 0f64; // Bytes > 0x7F (non-ASCII)
3533
3534    let (chunks, remainder) = bytes.as_chunks::<CHUNK_SIZE>();
3535
3536    macro_rules! handle_byte {
3537        ($byte: expr) => {
3538            match $byte {
3539                0x00 => return false,
3540                0x09 | 0x0A | 0x0D => printable += 1.0, // Whitespace
3541                0x20..=0x7E => printable += 1.0,        // Printable ASCII
3542                _ => high_bytes += 1.0,
3543            }
3544        };
3545    }
3546
3547    for bytes in chunks {
3548        for b in bytes {
3549            handle_byte!(b)
3550        }
3551    }
3552
3553    for b in remainder {
3554        handle_byte!(b)
3555    }
3556
3557    let total = bytes.len() as f64;
3558    let printable_ratio = printable / total;
3559    let high_bytes_ratio = high_bytes / total;
3560
3561    // Heuristic thresholds (adjust as needed):
3562    printable_ratio > 0.85 && high_bytes_ratio < 0.20
3563}
3564
3565#[inline(always)]
3566fn guess_stream_kind<S: AsRef<[u8]>>(stream: S) -> StreamKind {
3567    let buf = stream.as_ref();
3568
3569    match run_utf8_validation(buf) {
3570        Ok(is_ascii) => {
3571            if is_ascii {
3572                StreamKind::Text(TextEncoding::Ascii)
3573            } else {
3574                StreamKind::Text(TextEncoding::Utf8)
3575            }
3576        }
3577        Err(e) => {
3578            if is_likely_text(&buf[e.valid_up_to..]) {
3579                StreamKind::Text(TextEncoding::Unknown)
3580            } else {
3581                StreamKind::Binary
3582            }
3583        }
3584    }
3585}
3586
3587impl MagicDb {
3588    /// Creates a new empty database
3589    ///
3590    /// # Returns
3591    ///
3592    /// * [`MagicDb`] - A new empty database
3593    pub fn new() -> Self {
3594        Self::default()
3595    }
3596
3597    #[inline(always)]
3598    fn next_rule_id(&mut self) -> usize {
3599        let t = self.rule_id;
3600        self.rule_id += 1;
3601        t
3602    }
3603
3604    #[inline(always)]
3605    fn try_json<D: DataRead>(
3606        haystack: &mut D,
3607        stream_kind: StreamKind,
3608        magic: &mut Magic,
3609    ) -> Result<bool, Error> {
3610        // cannot be json if content is binary
3611        if matches!(stream_kind, StreamKind::Binary) {
3612            return Ok(false);
3613        }
3614
3615        let buf = haystack.read_range(0..FILE_BYTES_MAX as u64)?.trim_ascii();
3616
3617        let Some((start, end)) = find_json_boundaries(buf) else {
3618            return Ok(false);
3619        };
3620
3621        // if anything else than whitespace before start
3622        // this is not json
3623        for c in buf[0..start].iter() {
3624            if !c.is_ascii_whitespace() {
3625                return Ok(false);
3626            }
3627        }
3628
3629        let mut is_ndjson = false;
3630
3631        trace!("maybe a json document");
3632        let ok = serde_json::from_slice::<serde_json::Value>(&buf[start..=end]).is_ok();
3633        if !ok {
3634            return Ok(false);
3635        }
3636
3637        // we are sure it is json now we must look if we are ndjson
3638        if end + 1 < buf.len() {
3639            // after first json
3640            let buf = &buf[end + 1..];
3641            if let Some((second_start, second_end)) = find_json_boundaries(buf) {
3642                // there is a new line between the two json docs
3643                if memchr(b'\n', &buf[..second_start]).is_some() {
3644                    trace!("might be ndjson");
3645                    is_ndjson = serde_json::from_slice::<serde_json::Value>(
3646                        &buf[second_start..=second_end],
3647                    )
3648                    .is_ok();
3649                }
3650            }
3651        }
3652
3653        if is_ndjson {
3654            magic.push_message(Cow::Borrowed("New Line Delimited"));
3655            magic.set_mime_type(Cow::Borrowed("application/x-ndjson"));
3656            magic.insert_extensions(["ndjson", "jsonl"].into_iter());
3657        } else {
3658            magic.set_mime_type(Cow::Borrowed("application/json"));
3659            magic.insert_extensions(["json"].into_iter());
3660        }
3661
3662        magic.push_message(Cow::Borrowed("JSON text data"));
3663        magic.set_source(Some(HARDCODED_SOURCE));
3664        magic.update_strength(HARDCODED_MAGIC_STRENGTH);
3665        Ok(true)
3666    }
3667
3668    #[inline(always)]
3669    fn try_csv<D: DataRead>(
3670        haystack: &mut D,
3671        stream_kind: StreamKind,
3672        magic: &mut Magic,
3673    ) -> Result<bool, Error> {
3674        // cannot be csv if content is binary
3675        let StreamKind::Text(enc) = stream_kind else {
3676            return Ok(false);
3677        };
3678
3679        let buf = haystack.read_range(0..FILE_BYTES_MAX as u64)?;
3680        let mut reader = csv::ReaderBuilder::new()
3681            .has_headers(false)
3682            .from_reader(io::Cursor::new(buf));
3683        let mut records = reader.records();
3684
3685        let Some(Ok(first)) = records.next() else {
3686            return Ok(false);
3687        };
3688
3689        // very not likely a CSV otherwise all programming
3690        // languages having ; line terminator would be
3691        // considered as CSV
3692        if first.len() <= 1 {
3693            return Ok(false);
3694        }
3695
3696        // we already parsed first line
3697        let mut n = 1;
3698        for i in records.take(9) {
3699            if let Ok(rec) = i {
3700                if first.len() != rec.len() {
3701                    return Ok(false);
3702                }
3703            } else {
3704                return Ok(false);
3705            }
3706            n += 1;
3707        }
3708
3709        // we need at least 2 lines (matches file command https://github.com/file/file/commit/b4e621d1d5b3e9d142dd23030cca09f6f198e18b)
3710        if n < 2 {
3711            return Ok(false);
3712        }
3713
3714        magic.set_mime_type(Cow::Borrowed("text/csv"));
3715        magic.push_message(Cow::Borrowed("CSV"));
3716        magic.push_message(Cow::Borrowed(enc.as_magic_str()));
3717        magic.push_message(Cow::Borrowed("text"));
3718        magic.insert_extensions(["csv"].into_iter());
3719        magic.set_source(Some(HARDCODED_SOURCE));
3720        magic.update_strength(HARDCODED_MAGIC_STRENGTH);
3721        Ok(true)
3722    }
3723
3724    #[inline(always)]
3725    fn try_tar<D: DataRead>(
3726        haystack: &mut D,
3727        stream_kind: StreamKind,
3728        magic: &mut Magic,
3729    ) -> Result<bool, Error> {
3730        // cannot be json if content is not binary
3731        if !matches!(stream_kind, StreamKind::Binary) {
3732            return Ok(false);
3733        }
3734
3735        let buf = haystack.read_range(0..FILE_BYTES_MAX as u64)?;
3736        let mut ar = Archive::new(io::Cursor::new(buf));
3737
3738        let Ok(mut entries) = ar.entries() else {
3739            return Ok(false);
3740        };
3741
3742        let Some(Ok(first)) = entries.next() else {
3743            return Ok(false);
3744        };
3745
3746        let header = first.header();
3747
3748        if header.as_ustar().is_some() {
3749            magic.push_message(Cow::Borrowed("POSIX tar archive"));
3750        } else if header.as_gnu().is_some() {
3751            magic.push_message(Cow::Borrowed("POSIX tar archive (GNU)"));
3752        } else {
3753            magic.push_message(Cow::Borrowed("tar archive"));
3754        }
3755
3756        magic.set_mime_type(Cow::Borrowed("application/x-tar"));
3757        magic.set_source(Some(HARDCODED_SOURCE));
3758        magic.update_strength(HARDCODED_MAGIC_STRENGTH);
3759        magic.insert_extensions(["tar"].into_iter());
3760        Ok(true)
3761    }
3762
3763    #[inline(always)]
3764    fn try_hard_magic<D: DataRead>(
3765        haystack: &mut D,
3766        stream_kind: StreamKind,
3767        magic: &mut Magic,
3768    ) -> Result<bool, Error> {
3769        Ok(Self::try_json(haystack, stream_kind, magic)?
3770            || Self::try_csv(haystack, stream_kind, magic)?
3771            || Self::try_tar(haystack, stream_kind, magic)?)
3772    }
3773
3774    #[inline(always)]
3775    fn magic_default<'m, D: DataRead>(
3776        cache: &mut D,
3777        stream_kind: StreamKind,
3778        magic: &mut Magic<'m>,
3779    ) {
3780        magic.set_source(Some(HARDCODED_SOURCE));
3781        magic.set_stream_kind(stream_kind);
3782        magic.is_default = true;
3783
3784        if cache.data_size() == 0 {
3785            magic.push_message(Cow::Borrowed("empty"));
3786            magic.set_mime_type(Cow::Borrowed(DEFAULT_BIN_MIMETYPE));
3787        }
3788
3789        match stream_kind {
3790            StreamKind::Binary => {
3791                magic.push_message(Cow::Borrowed("data"));
3792            }
3793            StreamKind::Text(e) => {
3794                magic.push_message(Cow::Borrowed(e.as_magic_str()));
3795                magic.push_message(Cow::Borrowed("text"));
3796            }
3797        }
3798    }
3799
3800    fn load_rules_no_prepare(&mut self, rules: Vec<MagicRule>) {
3801        for rule in rules.into_iter() {
3802            let mut rule = rule;
3803            rule.set_id(self.next_rule_id());
3804
3805            self.rules.push(rule);
3806        }
3807    }
3808
3809    /// Loads rules from a [`MagicSource`]
3810    ///
3811    /// # Arguments
3812    ///
3813    /// * `ms` - The [`MagicSource`] to load rules from
3814    pub fn load(&mut self, ms: MagicSource) -> &mut Self {
3815        self.load_rules_no_prepare(ms.rules);
3816        self.dependencies.extend(ms.dependencies);
3817        self.try_finalize();
3818        self
3819    }
3820
3821    /// Loads multiple [`MagicSource`] items efficiently in bulk.
3822    ///
3823    /// This is more efficient than loading each individually. After processing
3824    /// all sources, it applies finalization step only once.
3825    pub fn load_bulk<I: Iterator<Item = MagicSource>>(&mut self, it: I) -> &mut Self {
3826        for ms in it {
3827            self.load_rules_no_prepare(ms.rules);
3828            self.dependencies.extend(ms.dependencies);
3829        }
3830        self.try_finalize();
3831        self
3832    }
3833
3834    /// Gets all rules in the database
3835    ///
3836    /// # Returns
3837    ///
3838    /// * `&[MagicRule]` - A slice of all rules
3839    pub fn rules(&self) -> &[MagicRule] {
3840        &self.rules
3841    }
3842
3843    #[inline]
3844    fn first_magic_with_stream_kind<D: DataRead>(
3845        &self,
3846        haystack: &mut D,
3847        stream_kind: StreamKind,
3848        extension: Option<&str>,
3849    ) -> Result<Magic<'_>, Error> {
3850        // re-using magic makes this function faster
3851        let mut magic = Magic::default();
3852
3853        if Self::try_hard_magic(haystack, stream_kind, &mut magic)? {
3854            return Ok(magic);
3855        }
3856
3857        macro_rules! do_magic {
3858            ($rule: expr) => {{
3859                $rule.magic_entrypoint(&mut magic, stream_kind, haystack, &self, false, 0, 0)?;
3860
3861                if !magic.message.is_empty() {
3862                    magic.set_stream_kind(stream_kind);
3863                    magic.set_source($rule.source.as_deref());
3864                    return Ok(magic);
3865                }
3866
3867                magic.reset();
3868            }};
3869        }
3870
3871        if let Some(ext) = extension.map(|e| e.to_lowercase())
3872            && !ext.is_empty()
3873        {
3874            let mut marked = vec![false; self.rules.len()];
3875            for rule in self.rules.iter().filter(|r| r.extensions.contains(&ext)) {
3876                do_magic!(rule);
3877                if let Some(f) = marked.get_mut(rule.id) {
3878                    *f = true
3879                }
3880            }
3881            for rule in self
3882                .rules
3883                .iter()
3884                // we don't run again rules run by extension
3885                .filter(|r| !*marked.get(r.id).unwrap_or(&false))
3886            {
3887                do_magic!(rule)
3888            }
3889        } else {
3890            for rule in self.rules.iter() {
3891                do_magic!(rule)
3892            }
3893        }
3894
3895        Self::magic_default(haystack, stream_kind, &mut magic);
3896
3897        Ok(magic)
3898    }
3899
3900    /// Detects file [`Magic`] stopping at the first matching magic. Magic
3901    /// rules are evaluated from the best to the least relevant, so this method
3902    /// returns most of the time the best magic. For the rare cases where
3903    /// it doesn't or if the best result is always required, use [`MagicDb::best_magic`]
3904    ///
3905    /// # Arguments
3906    ///
3907    /// * `r` - A reader implementing [`DataRead`]
3908    /// * `extension` - Optional file extension to use for acceleration
3909    ///
3910    /// # Returns
3911    ///
3912    /// * `Result<Magic<'_>, Error>` - The detection result or an error
3913    ///
3914    /// # Notes
3915    ///
3916    /// * Use this method **only** if you need to re-use a `reader` for future **read** operations.
3917    /// * Use [`DataReader`] to create a generic `reader`
3918    ///
3919    /// # Warning
3920    ///
3921    /// File extension acceleration is made to evaluate rules faster by testing
3922    /// first the rules defining this extension with an `!:ext` entry.
3923    /// Whether you use `extension` acceleration or not with this function should not
3924    /// produce different results. Yet this makes the assumption rules are written
3925    /// correctly and every rule concerned defines `!:ext` when it is appropriate.
3926    /// If some rules are missing it, results might differ.
3927    pub fn first_magic<R: DataRead>(
3928        &self,
3929        r: &mut R,
3930        extension: Option<&str>,
3931    ) -> Result<Magic<'_>, Error> {
3932        let stream_kind = guess_stream_kind(r.read_range(0..FILE_BYTES_MAX as u64)?);
3933        self.first_magic_with_stream_kind(r, stream_kind, extension)
3934    }
3935
3936    /// Detects file [`Magic`] from a file path.
3937    ///
3938    /// This is a convenience method that opens the file and creates a [`DataReader::File`]
3939    /// internally. The file extension is automatically extracted and passed to
3940    /// [`MagicDb::first_magic`].
3941    ///
3942    /// # Errors
3943    ///
3944    /// Returns an error if the file cannot be opened or if magic detection fails.
3945    pub fn first_magic_file<P: AsRef<Path>>(&self, path: P) -> Result<Magic<'_>, Error> {
3946        let ext = path.as_ref().extension().and_then(|e| e.to_str());
3947        self.first_magic(&mut DataReader::from_file(File::open(path.as_ref())?)?, ext)
3948    }
3949
3950    /// Detects file [`Magic`] from an in-memory byte slice.
3951    ///
3952    /// This is a convenience method that creates a [`DataReader::Slice`] internally.
3953    ///
3954    /// # Errors
3955    ///
3956    /// Returns an error if magic detection fails.
3957    pub fn first_magic_slice<S: AsRef<[u8]>>(
3958        &self,
3959        s: S,
3960        extension: Option<&str>,
3961    ) -> Result<Magic<'_>, Error> {
3962        self.first_magic(&mut DataReader::from_slice(s.as_ref()), extension)
3963    }
3964
3965    #[inline(always)]
3966    fn all_magics_sort_with_stream_kind<R: DataRead>(
3967        &self,
3968        haystack: &mut R,
3969        stream_kind: StreamKind,
3970    ) -> Result<Vec<Magic<'_>>, Error> {
3971        let mut out = Vec::new();
3972
3973        let mut magic = Magic::default();
3974
3975        if Self::try_hard_magic(haystack, stream_kind, &mut magic)? {
3976            out.push(magic);
3977            magic = Magic::default();
3978        }
3979
3980        for rule in self.rules.iter() {
3981            rule.magic_entrypoint(&mut magic, stream_kind, haystack, self, false, 0, 0)?;
3982
3983            // it is possible we have a strength with no message
3984            if !magic.message.is_empty() {
3985                magic.set_stream_kind(stream_kind);
3986                magic.set_source(rule.source.as_deref());
3987                out.push(magic);
3988                magic = Magic::default();
3989            }
3990
3991            magic.reset();
3992        }
3993
3994        Self::magic_default(haystack, stream_kind, &mut magic);
3995        out.push(magic);
3996
3997        out.sort_by_key(|b| std::cmp::Reverse(b.strength()));
3998
3999        Ok(out)
4000    }
4001
4002    /// Detects all [`Magic`] matching a given content.
4003    ///
4004    /// # Arguments
4005    ///
4006    /// * `r` - A reader implementing [`DataRead`]
4007    ///
4008    /// # Returns
4009    ///
4010    /// * `Result<Vec<Magic<'_>>, Error>` - All detection results sorted by strength or an error
4011    ///
4012    /// # Notes
4013    ///
4014    /// * Use this method **only** if you need to re-use a `reader` for future **read** operations.
4015    /// * Use [`DataReader`] to create a generic `reader`
4016    #[inline]
4017    pub fn all_magics<R: DataRead>(&self, r: &mut R) -> Result<Vec<Magic<'_>>, Error> {
4018        let stream_kind = guess_stream_kind(r.read_range(0..FILE_BYTES_MAX as u64)?);
4019        self.all_magics_sort_with_stream_kind(r, stream_kind)
4020    }
4021
4022    /// Detects all matching [`Magic`] entries from a file path.
4023    ///
4024    /// This is a convenience method that opens the file and creates a [`DataReader::File`]
4025    /// internally, then calls [`MagicDb::all_magics`].
4026    ///
4027    /// # Errors
4028    ///
4029    /// Returns an error if the file cannot be opened or if magic detection fails.
4030    pub fn all_magics_file<P: AsRef<Path>>(&self, path: P) -> Result<Vec<Magic<'_>>, Error> {
4031        self.all_magics(&mut DataReader::from_file(File::open(path)?)?)
4032    }
4033
4034    /// Detects all matching [`Magic`] entries from an in-memory byte slice.
4035    ///
4036    /// This is a convenience method that creates a [`DataReader::Slice`] internally,
4037    /// then calls [`MagicDb::all_magics`].
4038    ///
4039    /// # Errors
4040    ///
4041    /// Returns an error if magic detection fails.
4042    pub fn all_magics_slice<S: AsRef<[u8]>>(&self, slice: S) -> Result<Vec<Magic<'_>>, Error> {
4043        self.all_magics(&mut DataReader::from_slice(slice.as_ref()))
4044    }
4045
4046    #[inline(always)]
4047    fn best_magic_with_stream_kind<R: DataRead>(
4048        &self,
4049        haystack: &mut R,
4050        stream_kind: StreamKind,
4051        extension: Option<&str>,
4052    ) -> Result<Magic<'_>, Error> {
4053        // re-using magic makes this function faster
4054        let mut magic = Magic::default();
4055        let mut best = Magic::default();
4056        let mut best_id = None;
4057
4058        if Self::try_hard_magic(haystack, stream_kind, &mut magic)? {
4059            swap(&mut magic, &mut best);
4060            magic.reset();
4061        }
4062
4063        macro_rules! do_best {
4064            ($rule: expr) => {{
4065                $rule.magic_entrypoint(&mut magic, stream_kind, haystack, &self, false, 0, 0)?;
4066
4067                if !magic.message.is_empty()
4068                    && (magic.strength > best.strength || best.message.is_empty())
4069                {
4070                    magic.set_stream_kind(stream_kind);
4071                    magic.set_source($rule.source.as_deref());
4072                    swap(&mut magic, &mut best);
4073                    let _ = best_id.insert($rule.id);
4074                }
4075
4076                magic.reset();
4077            }};
4078        }
4079
4080        let ext = extension.map(|e| e.to_lowercase());
4081        if let Some(ext) = ext.as_ref()
4082            && !ext.is_empty()
4083        {
4084            for rule in self.rules.iter().filter(|r| r.extensions.contains(ext)) {
4085                // don't prune while best is empty -- a 0-strength match still beats none
4086                if !best.message.is_empty() && rule.max_score <= best.strength {
4087                    continue;
4088                }
4089                do_best!(rule);
4090            }
4091        }
4092
4093        for rule in self.rules.iter() {
4094            // don't prune while best is empty -- a 0-strength match still beats none
4095            if (!best.message.is_empty() && rule.max_score <= best.strength)
4096                || best_id == Some(rule.id)
4097            {
4098                continue;
4099            }
4100
4101            do_best!(rule)
4102        }
4103
4104        if best.message.is_empty() {
4105            Self::magic_default(haystack, stream_kind, &mut best);
4106        }
4107
4108        Ok(best)
4109    }
4110
4111    /// Detects the best [`Magic`] matching a given content.
4112    ///
4113    /// # Arguments
4114    ///
4115    /// * `r` - A reader implementing [`DataRead`]
4116    ///
4117    /// # Returns
4118    ///
4119    /// * `Result<Magic<'_>, Error>` - The best detection result or an error
4120    ///
4121    /// # Notes
4122    ///
4123    /// * Use this method **only** if you need to re-use a `reader` for future **read** operations.
4124    /// * Use [`DataReader`] to create a generic `reader`
4125    #[inline]
4126    pub fn best_magic<R: DataRead>(
4127        &self,
4128        r: &mut R,
4129        extension: Option<&str>,
4130    ) -> Result<Magic<'_>, Error> {
4131        let stream_kind = guess_stream_kind(r.read_range(0..FILE_BYTES_MAX as u64)?);
4132        self.best_magic_with_stream_kind(r, stream_kind, extension)
4133    }
4134
4135    /// Detects the best matching [`Magic`] from a file path.
4136    ///
4137    /// This is a convenience method that opens the file and creates a [`DataReader::File`]
4138    /// internally, then calls [`MagicDb::best_magic`].
4139    ///
4140    /// # Errors
4141    ///
4142    /// Returns an error if the file cannot be opened or if magic detection fails.
4143    pub fn best_magic_file<P: AsRef<Path>>(&self, path: P) -> Result<Magic<'_>, Error> {
4144        let ext = path.as_ref().extension().and_then(|e| e.to_str());
4145        self.best_magic(&mut DataReader::from_file(File::open(&path)?)?, ext)
4146    }
4147
4148    /// Detects the best matching [`Magic`] from an in-memory byte slice.
4149    ///
4150    /// This is a convenience method that creates a [`DataReader::Slice`] internally,
4151    /// then calls [`MagicDb::best_magic`].
4152    ///
4153    /// # Errors
4154    ///
4155    /// Returns an error if magic detection fails.
4156    pub fn best_magic_slice<S: AsRef<[u8]>>(
4157        &self,
4158        slice: S,
4159        extension: Option<&str>,
4160    ) -> Result<Magic<'_>, Error> {
4161        self.best_magic(&mut DataReader::from_slice(slice.as_ref()), extension)
4162    }
4163
4164    /// Serializes the database to a generic writer implementing [`io::Write`]
4165    ///
4166    /// # Returns
4167    ///
4168    /// * `Result<(), Error>` - The serialized database or an error
4169    pub fn serialize<W: Write>(self, w: &mut W) -> Result<(), Error> {
4170        let mut encoder = GzEncoder::new(w, Compression::best());
4171
4172        bincode::serde::encode_into_std_write(&self, &mut encoder, bincode::config::standard())?;
4173        encoder.finish()?;
4174        Ok(())
4175    }
4176
4177    /// Deserializes the database from a generic reader implementing [`io::Read`]
4178    ///
4179    /// # Arguments
4180    ///
4181    /// * `r` - The reader to deserialize from
4182    ///
4183    /// # Returns
4184    ///
4185    /// * `Result<Self, Error>` - The deserialized database or an error
4186    pub fn deserialize<R: Read>(r: &mut R) -> Result<Self, Error> {
4187        let mut buf = vec![];
4188        let mut gz = GzDecoder::new(r);
4189        gz.read_to_end(&mut buf).map_err(|e| {
4190            bincode::error::DecodeError::OtherString(format!("failed to read: {e}"))
4191        })?;
4192        let (sdb, _): (MagicDb, usize) =
4193            bincode::serde::decode_from_slice(&buf, bincode::config::standard())?;
4194        Ok(sdb)
4195    }
4196
4197    /// Verifies the consistency of the [`MagicDb`] database.
4198    /// This method must be called when the database is built once and used later.
4199    /// It catches [`enum@Error`] that would raise at rule evaluation time.
4200    ///
4201    /// # Errors
4202    /// Returns an error if any rule fails verification
4203    pub fn verify(&mut self) -> Result<(), Error> {
4204        if self.rules.len() == self.finalized {
4205            return Ok(());
4206        }
4207
4208        for r in self.rules.iter_mut().filter(|r| !r.finalized) {
4209            // return at the first rule failing verification
4210            r.try_finalize(&self.dependencies).map_err(|e| {
4211                Error::Verify(
4212                    r.source.clone().unwrap_or(String::from("unknown")),
4213                    r.line(),
4214                    e.into(),
4215                )
4216            })?;
4217            self.finalized += 1;
4218        }
4219
4220        debug_assert!(self.finalized <= self.rules.len());
4221
4222        Ok(())
4223    }
4224
4225    #[inline(always)]
4226    fn try_finalize(&mut self) {
4227        if self.rules.len() == self.finalized {
4228            return;
4229        }
4230
4231        let mut finalized = 0usize;
4232        self.rules.iter_mut().for_each(|r| {
4233            if r.try_finalize(&self.dependencies).is_ok() {
4234                finalized += 1;
4235            }
4236        });
4237
4238        self.finalized = finalized;
4239
4240        debug_assert!(self.finalized <= self.rules.len());
4241
4242        // put text rules at the end
4243        self.rules.sort_by_key(|r| (r.is_text(), -(r.score as i64)));
4244    }
4245}
4246
4247#[cfg(test)]
4248mod tests {
4249
4250    use regex::bytes::Regex;
4251
4252    use crate::{readers::BufReader, utils::unix_local_time_to_string};
4253
4254    use super::*;
4255
4256    macro_rules! buf_reader {
4257        ($l: literal) => {
4258            BufReader::from_slice($l.as_bytes())
4259        };
4260    }
4261
4262    fn best_magic(
4263        rule: &str,
4264        content: &[u8],
4265        stream_kind: StreamKind,
4266    ) -> Result<Magic<'static>, Error> {
4267        let mut md = MagicDb::new();
4268        md.load(
4269            FileMagicParser::parse_str(rule, None)
4270                .inspect_err(|e| eprintln!("{e}"))
4271                .unwrap(),
4272        );
4273        let mut reader = BufReader::from_slice(content);
4274        let v = md.best_magic_with_stream_kind(&mut reader, stream_kind, None)?;
4275        Ok(v.into_owned())
4276    }
4277
4278    /// helper macro to debug tests
4279    #[allow(unused_macros)]
4280    macro_rules! enable_trace {
4281        () => {
4282            tracing_subscriber::fmt()
4283                .with_max_level(tracing_subscriber::filter::LevelFilter::TRACE)
4284                .try_init();
4285        };
4286    }
4287
4288    macro_rules! parse_assert {
4289        ($rule:expr) => {
4290            FileMagicParser::parse_str($rule, None)
4291                .inspect_err(|e| eprintln!("{e}"))
4292                .unwrap()
4293        };
4294    }
4295
4296    macro_rules! assert_magic_match_bin {
4297        ($rule: literal, $content:literal) => {{ best_magic($rule, $content, StreamKind::Binary).unwrap() }};
4298        ($rule: literal, $content:literal, $message:expr) => {{
4299            assert_eq!(
4300                best_magic($rule, $content, StreamKind::Binary)
4301                    .unwrap()
4302                    .message(),
4303                $message
4304            );
4305        }};
4306    }
4307
4308    macro_rules! assert_magic_match_text {
4309        ($rule: literal, $content:literal) => {{ best_magic($rule, $content, StreamKind::Text(TextEncoding::Utf8)).unwrap() }};
4310        ($rule: literal, $content:literal, $message:expr) => {{
4311            assert_eq!(
4312                best_magic($rule, $content, StreamKind::Text(TextEncoding::Utf8))
4313                    .unwrap()
4314                    .message(),
4315                $message
4316            );
4317        }};
4318    }
4319
4320    macro_rules! assert_magic_not_match_text {
4321        ($rule: literal, $content:literal) => {{
4322            assert!(
4323                best_magic($rule, $content, StreamKind::Text(TextEncoding::Utf8))
4324                    .unwrap()
4325                    .is_default()
4326            );
4327        }};
4328    }
4329
4330    macro_rules! assert_magic_not_match_bin {
4331        ($rule: literal, $content:literal) => {{
4332            assert!(
4333                best_magic($rule, $content, StreamKind::Binary)
4334                    .unwrap()
4335                    .is_default()
4336            );
4337        }};
4338    }
4339
4340    #[test]
4341    fn test_regex() {
4342        assert_magic_match_text!(
4343            r#"
43440	regex/1024 \^#![[:space:]]*/usr/bin/env[[:space:]]+
4345!:mime	text/x-shellscript
4346>&0  regex/64 .*($|\\b) %s shell script text executable
4347    "#,
4348            br#"#!/usr/bin/env bash
4349        echo hello world"#,
4350            // the magic generated
4351            "bash shell script text executable"
4352        );
4353
4354        let re = Regex::new(r"(?-u)\x42\x82").unwrap();
4355        assert!(re.is_match(b"\x42\x82"));
4356
4357        assert_magic_match_bin!(
4358            r#"0 regex \x42\x82 binary regex match"#,
4359            b"\x00\x00\x00\x00\x00\x00\x42\x82"
4360        );
4361
4362        // test regex continuation after match
4363        assert_magic_match_bin!(
4364            r#"
4365            0 regex \x42\x82
4366            >&0 string \xde\xad\xbe\xef it works
4367            "#,
4368            b"\x00\x00\x00\x00\x00\x00\x42\x82\xde\xad\xbe\xef"
4369        );
4370
4371        assert_magic_match_bin!(
4372            r#"
4373            0 regex/s \x42\x82
4374            >&0 string \x42\x82\xde\xad\xbe\xef it works
4375            "#,
4376            b"\x00\x00\x00\x00\x00\x00\x42\x82\xde\xad\xbe\xef"
4377        );
4378
4379        // ^ must match stat of line when matching text
4380        assert_magic_match_text!(
4381            r#"
43820	regex/1024 \^HelloWorld$ HelloWorld String"#,
4383            br#"
4384// this is a comment after an empty line
4385HelloWorld
4386            "#
4387        );
4388    }
4389
4390    #[test]
4391    fn test_string_with_mods() {
4392        assert_magic_match_text!(
4393            r#"0	string/w	#!\ \ \ /usr/bin/env\ bash	BASH
4394        "#,
4395            b"#! /usr/bin/env bash i
4396        echo hello world"
4397        );
4398
4399        // test uppercase insensitive
4400        assert_magic_match_text!(
4401            r#"0	string/C	HelloWorld	it works
4402        "#,
4403            b"helloworld"
4404        );
4405
4406        assert_magic_not_match_text!(
4407            r#"0	string/C	HelloWorld	it works
4408        "#,
4409            b"hELLOwORLD"
4410        );
4411
4412        // test lowercase insensitive
4413        assert_magic_match_text!(
4414            r#"0	string/c	HelloWorld	it works
4415        "#,
4416            b"HELLOWORLD"
4417        );
4418
4419        assert_magic_not_match_text!(
4420            r#"0	string/c	HelloWorld	it works
4421        "#,
4422            b"helloworld"
4423        );
4424
4425        // test full word match
4426        assert_magic_match_text!(
4427            r#"0	string/f	#!/usr/bin/env\ bash	BASH
4428        "#,
4429            b"#!/usr/bin/env bash"
4430        );
4431
4432        assert_magic_not_match_text!(
4433            r#"0	string/f	#!/usr/bin/python PYTHON"#,
4434            b"#!/usr/bin/pythonic"
4435        );
4436
4437        // testing whitespace compacting
4438        assert_magic_match_text!(
4439            r#"0	string/W	#!/usr/bin/env\ python  PYTHON"#,
4440            b"#!/usr/bin/env    python"
4441        );
4442
4443        assert_magic_not_match_text!(
4444            r#"0	string/W	#!/usr/bin/env\ \ python  PYTHON"#,
4445            b"#!/usr/bin/env python"
4446        );
4447    }
4448
4449    #[test]
4450    fn test_search_with_mods() {
4451        assert_magic_match_text!(
4452            r#"0	search/1/fwt	#!\ /usr/bin/luatex	LuaTex script text executable"#,
4453            b"#!          /usr/bin/luatex "
4454        );
4455
4456        // test matching from the beginning
4457        assert_magic_match_text!(
4458            r#"
4459            0	search/s	/usr/bin/env
4460            >&0 string /usr/bin/env it works
4461            "#,
4462            b"#!/usr/bin/env    python"
4463        );
4464
4465        assert_magic_not_match_text!(
4466            r#"
4467            0	search	/usr/bin/env
4468            >&0 string /usr/bin/env it works
4469            "#,
4470            b"#!/usr/bin/env    python"
4471        );
4472    }
4473
4474    #[test]
4475    fn test_pstring() {
4476        assert_magic_match_bin!(r#"0 pstring Toast it works"#, b"\x05Toast");
4477
4478        assert_magic_match_bin!(r#"0 pstring Toast %s"#, b"\x05Toast", "Toast");
4479
4480        assert_magic_not_match_bin!(r#"0 pstring Toast Doesn't work"#, b"\x07Toaster");
4481
4482        // testing with modifiers
4483        assert_magic_match_bin!(r#"0 pstring/H Toast it works"#, b"\x00\x05Toast");
4484
4485        assert_magic_match_bin!(r#"0 pstring/HJ Toast it works"#, b"\x00\x07Toast");
4486
4487        assert_magic_match_bin!(r#"0 pstring/HJ Toast %s"#, b"\x00\x07Toast", "Toast");
4488
4489        assert_magic_match_bin!(r#"0 pstring/h Toast it works"#, b"\x05\x00Toast");
4490
4491        assert_magic_match_bin!(r#"0 pstring/hJ Toast it works"#, b"\x07\x00Toast");
4492
4493        assert_magic_match_bin!(r#"0 pstring/L Toast it works"#, b"\x00\x00\x00\x05Toast");
4494
4495        assert_magic_match_bin!(r#"0 pstring/LJ Toast it works"#, b"\x00\x00\x00\x09Toast");
4496
4497        assert_magic_match_bin!(r#"0 pstring/l Toast it works"#, b"\x05\x00\x00\x00Toast");
4498
4499        assert_magic_match_bin!(r#"0 pstring/lJ Toast it works"#, b"\x09\x00\x00\x00Toast");
4500    }
4501
4502    #[test]
4503    fn test_max_recursion() {
4504        let res = best_magic(
4505            r#"0	indirect x"#,
4506            b"#!          /usr/bin/luatex ",
4507            StreamKind::Binary,
4508        );
4509        assert!(res.is_err());
4510        let _ = res.inspect_err(|e| {
4511            assert!(matches!(
4512                e.unwrap_localized(),
4513                Error::MaximumRecursion(MAX_RECURSION)
4514            ))
4515        });
4516    }
4517
4518    #[test]
4519    fn test_string_ops() {
4520        assert_magic_match_text!("0	string/b MZ MZ File", b"MZ\0");
4521        assert_magic_match_text!("0	string !MZ Not MZ File", b"AZ\0");
4522        assert_magic_match_text!("0	string >\0 Any String", b"A\0");
4523        assert_magic_match_text!("0	string >Test Any String", b"Test 1\0");
4524        assert_magic_match_text!("0	string <Test Any String", b"\0");
4525        assert_magic_not_match_text!("0	string >Test Any String", b"\0");
4526    }
4527
4528    #[test]
4529    fn test_lestring16() {
4530        assert_magic_match_bin!(
4531            "0 lestring16 abcd Little-endian UTF-16 string",
4532            b"\x61\x00\x62\x00\x63\x00\x64\x00"
4533        );
4534        assert_magic_match_bin!(
4535            "0 lestring16 x %s",
4536            b"\x61\x00\x62\x00\x63\x00\x64\x00\x00",
4537            "abcd"
4538        );
4539        assert_magic_not_match_bin!(
4540            "0 lestring16 abcd Little-endian UTF-16 string",
4541            b"\x00\x61\x00\x62\x00\x63\x00\x64"
4542        );
4543        assert_magic_match_bin!(
4544            "4 lestring16 abcd Little-endian UTF-16 string",
4545            b"\x00\x00\x00\x00\x61\x00\x62\x00\x63\x00\x64\x00"
4546        );
4547    }
4548
4549    #[test]
4550    fn test_bestring16() {
4551        assert_magic_match_bin!(
4552            "0 bestring16 abcd Big-endian UTF-16 string",
4553            b"\x00\x61\x00\x62\x00\x63\x00\x64"
4554        );
4555        assert_magic_match_bin!(
4556            "0 bestring16 x %s",
4557            b"\x00\x61\x00\x62\x00\x63\x00\x64",
4558            "abcd"
4559        );
4560        assert_magic_not_match_bin!(
4561            "0 bestring16 abcd Big-endian UTF-16 string",
4562            b"\x61\x00\x62\x00\x63\x00\x64\x00"
4563        );
4564        assert_magic_match_bin!(
4565            "4 bestring16 abcd Big-endian UTF-16 string",
4566            b"\x00\x00\x00\x00\x00\x61\x00\x62\x00\x63\x00\x64"
4567        );
4568    }
4569
4570    #[test]
4571    fn test_offset_from_end() {
4572        assert_magic_match_bin!("-1 ubyte 0x42 last byte ok", b"\x00\x00\x42");
4573        assert_magic_match_bin!("-2 ubyte 0x41 last byte ok", b"\x00\x41\x00");
4574    }
4575
4576    #[test]
4577    fn test_relative_offset() {
4578        assert_magic_match_bin!(
4579            "
4580            0 ubyte 0x42
4581            >&0 ubyte 0x00
4582            >>&0 ubyte 0x41 third byte ok
4583            ",
4584            b"\x42\x00\x41\x00"
4585        );
4586    }
4587
4588    #[test]
4589    fn test_indirect_offset() {
4590        assert_magic_match_bin!("(0.l) ubyte 0x42 it works", b"\x04\x00\x00\x00\x42");
4591        // adding fixed value to offset
4592        assert_magic_match_bin!("(0.l+3) ubyte 0x42 it works", b"\x01\x00\x00\x00\x42");
4593        // testing offset pair
4594        assert_magic_match_bin!(
4595            "(0.l+(4)) ubyte 0x42 it works",
4596            b"\x04\x00\x00\x00\x04\x00\x00\x00\x42"
4597        );
4598    }
4599
4600    // A `use` line's own message is never printed by libmagic (softmagic.c's
4601    // FILE_USE case never calls file_printf on its own `desc`) -- only the
4602    // named rule's own text (here the `name` line's "then second match")
4603    // shows up, confirmed against real `file`.
4604    #[test]
4605    fn test_use_with_message() {
4606        assert_magic_match_bin!(
4607            r#"
46080 string MZ
4609>0 use mz first match
4610
46110 name mz then second match
4612>0 string MZ
4613"#,
4614            b"MZ\0",
4615            "then second match"
4616        );
4617    }
4618
4619    // An `indirect` line's own message only prints if the recursive scan at
4620    // its target offset actually finds something (softmagic.c's
4621    // FILE_INDIRECT case prints `desc` conditionally, then the nested
4622    // match's own output), and in that order: own text first, nested second.
4623    #[test]
4624    fn test_indirect_message_only_on_nested_match() {
4625        let rule = r#"
46260 string AB
4627>2 indirect x contains:
4628
46290 string CD nested match
4630"#;
4631
4632        let m = best_magic(rule, b"ABCD", StreamKind::Binary).unwrap();
4633        assert_eq!(m.message(), "contains: nested match");
4634
4635        let m = best_magic(rule, b"ABXX", StreamKind::Binary).unwrap();
4636        assert!(m.is_default());
4637    }
4638
4639    #[test]
4640    fn test_scalar_transform() {
4641        assert_magic_match_bin!("0 ubyte+1 0x1 add works", b"\x00");
4642        assert_magic_match_bin!("0 ubyte-1 0xfe sub works", b"\xff");
4643        assert_magic_match_bin!("0 ubyte%2 0 mod works", b"\x0a");
4644        assert_magic_match_bin!("0 ubyte&0x0f 0x0f bitand works", b"\xff");
4645        assert_magic_match_bin!("0 ubyte|0x0f 0xff bitor works", b"\xf0");
4646        assert_magic_match_bin!("0 ubyte^0x0f 0xf0 bitxor works", b"\xff");
4647
4648        FileMagicParser::parse_str("0 ubyte%0 mod by zero", None)
4649            .expect_err("expect div by zero error");
4650        FileMagicParser::parse_str("0 ubyte/0 div by zero", None)
4651            .expect_err("expect div by zero error");
4652    }
4653
4654    #[test]
4655    fn test_strength_hoisted_to_root() {
4656        // `!:strength` declared on a nested continuation must be treated
4657        // exactly as if it had been declared on the group's root
4658        let base_rule = r"
46590	string	MAGIC	found it
4660>0	byte	x	x
4661        ";
4662        let with_strength_rule = r"
46630	string	MAGIC	found it
4664>0	byte	x	x
4665!:strength +100
4666        ";
4667
4668        let mut db = MagicDb::new();
4669        db.load(parse_assert!(base_rule));
4670        db.load(parse_assert!(with_strength_rule));
4671
4672        // reached the top at load time
4673        let modded_score = db.rules()[0].score();
4674        let base_score = db.rules()[1].score();
4675
4676        assert_eq!(
4677            modded_score,
4678            base_score + 100,
4679            "!:strength on a nested continuation must still affect the root's static score"
4680        );
4681
4682        let base_strength = best_magic(base_rule, b"MAGIC", StreamKind::Binary)
4683            .unwrap()
4684            .strength();
4685        let modded_strength = best_magic(with_strength_rule, b"MAGIC", StreamKind::Binary)
4686            .unwrap()
4687            .strength();
4688
4689        assert_eq!(
4690            modded_strength,
4691            base_strength + 100,
4692            "!:strength on a nested continuation must still affect the runtime strength"
4693        );
4694    }
4695
4696    #[test]
4697    fn test_duplicate_strength_rejected() {
4698        // real libmagic rejects a second !:strength in the same rule
4699        FileMagicParser::parse_str(
4700            r"
47010	string	MAGIC	found it
4702!:strength +10
4703>0	byte	x	x
4704!:strength +20
4705            ",
4706            None,
4707        )
4708        .expect_err("a second !:strength in the same rule must be rejected");
4709    }
4710
4711    #[test]
4712    fn test_strength_on_name_entry_rejected() {
4713        FileMagicParser::parse_str(
4714            r"
47150	name	dep
4716!:strength +10
4717>0	byte	x	x
4718            ",
4719            None,
4720        )
4721        .expect_err("!:strength directly on a name entry must be rejected");
4722
4723        FileMagicParser::parse_str(
4724            r"
47250	name	dep
4726>0	byte	x	x
4727!:strength +10
4728            ",
4729            None,
4730        )
4731        .expect_err("!:strength anywhere in a name-headed group must be rejected");
4732    }
4733
4734    #[test]
4735    fn test_search_binary_default_matches_libmagic_text_chars() {
4736        // libmagic counts BEL (0x07) as text, not binary. Both
4737        // assertions need a message, or a silent match looks identical
4738        // to a correctly-skipped one.
4739        assert_magic_not_match_bin!(r"0	search	\007test	found", b"\x07test");
4740        assert_magic_match_text!(r"0	search	\007test	found", b"\x07test", "found");
4741    }
4742
4743    #[test]
4744    fn test_search_binary_default_accepts_valid_utf8() {
4745        // A valid multi-byte UTF-8 escape sequence must default to
4746        // text, not binary just because its bytes are >= 0x80.
4747        assert_magic_not_match_bin!(r"0	search	caf\xc3\xa9	found", b"caf\xc3\xa9");
4748        assert_magic_match_text!(r"0	search	caf\xc3\xa9	found", b"caf\xc3\xa9", "found");
4749    }
4750
4751    #[test]
4752    fn test_belong() {
4753        // Test that a file with a four-byte value at offset 0 that matches the given value in big-endian byte order
4754        assert_magic_match_bin!("0 belong 0x12345678 Big-endian long", b"\x12\x34\x56\x78");
4755        // Test that a file with a four-byte value at offset 0 that does not match the given value in big-endian byte order
4756        assert_magic_not_match_bin!("0 belong 0x12345678 Big-endian long", b"\x78\x56\x34\x12");
4757        // Test that a file with a four-byte value at a non-zero offset that matches the given value in big-endian byte order
4758        assert_magic_match_bin!(
4759            "4 belong 0x12345678 Big-endian long",
4760            b"\x00\x00\x00\x00\x12\x34\x56\x78"
4761        );
4762        // Test < operator
4763        assert_magic_match_bin!("0 belong <0x12345678 Big-endian long", b"\x12\x34\x56\x77");
4764        assert_magic_not_match_bin!("0 belong <0x12345678 Big-endian long", b"\x12\x34\x56\x78");
4765
4766        // Test > operator
4767        assert_magic_match_bin!("0 belong >0x12345678 Big-endian long", b"\x12\x34\x56\x79");
4768        assert_magic_not_match_bin!("0 belong >0x12345678 Big-endian long", b"\x12\x34\x56\x78");
4769
4770        // Test & operator
4771        assert_magic_match_bin!("0 belong &0x5678 Big-endian long", b"\x00\x00\x56\x78");
4772        assert_magic_not_match_bin!("0 belong &0x0000FFFF Big-endian long", b"\x12\x34\x56\x78");
4773
4774        // Test ^ operator (bitwise AND with complement)
4775        assert_magic_match_bin!("0 belong ^0xFFFF0000 Big-endian long", b"\x00\x00\x56\x78");
4776        assert_magic_not_match_bin!("0 belong ^0xFFFF0000 Big-endian long", b"\x00\x01\x56\x78");
4777
4778        // Test ~ operator
4779        assert_magic_match_bin!("0 belong ~0x12345678 Big-endian long", b"\xed\xcb\xa9\x87");
4780        assert_magic_not_match_bin!("0 belong ~0x12345678 Big-endian long", b"\x12\x34\x56\x78");
4781
4782        // Test x operator
4783        assert_magic_match_bin!("0 belong x Big-endian long", b"\x12\x34\x56\x78");
4784        assert_magic_match_bin!("0 belong x Big-endian long", b"\x78\x56\x34\x12");
4785    }
4786
4787    #[test]
4788    fn test_parse_search() {
4789        parse_assert!("0 search test");
4790        parse_assert!("0 search/24/s test");
4791        parse_assert!("0 search/s/24 test");
4792    }
4793
4794    #[test]
4795    fn test_bedate() {
4796        assert_magic_match_bin!(
4797            "0 bedate 946684800 Unix date (Jan 1, 2000)",
4798            b"\x38\x6D\x43\x80"
4799        );
4800        assert_magic_not_match_bin!(
4801            "0 bedate 946684800 Unix date (Jan 1, 2000)",
4802            b"\x00\x00\x00\x00"
4803        );
4804        assert_magic_match_bin!(
4805            "4 bedate 946684800 %s",
4806            b"\x00\x00\x00\x00\x38\x6D\x43\x80",
4807            "2000-01-01 00:00:00"
4808        );
4809    }
4810    #[test]
4811    fn test_beldate() {
4812        assert_magic_match_bin!(
4813            "0 beldate 946684800 Local date (Jan 1, 2000)",
4814            b"\x38\x6D\x43\x80"
4815        );
4816        assert_magic_not_match_bin!(
4817            "0 beldate 946684800 Local date (Jan 1, 2000)",
4818            b"\x00\x00\x00\x00"
4819        );
4820
4821        assert_magic_match_bin!(
4822            "4 beldate 946684800 {}",
4823            b"\x00\x00\x00\x00\x38\x6D\x43\x80",
4824            unix_local_time_to_string(946684800)
4825        );
4826    }
4827
4828    #[test]
4829    fn test_beqdate() {
4830        assert_magic_match_bin!(
4831            "0 beqdate 946684800 Unix date (Jan 1, 2000)",
4832            b"\x00\x00\x00\x00\x38\x6D\x43\x80"
4833        );
4834
4835        assert_magic_not_match_bin!(
4836            "0 beqdate 946684800 Unix date (Jan 1, 2000)",
4837            b"\x00\x00\x00\x00\x00\x00\x00\x00"
4838        );
4839
4840        assert_magic_match_bin!(
4841            "0 beqdate 946684800 %s",
4842            b"\x00\x00\x00\x00\x38\x6D\x43\x80",
4843            "2000-01-01 00:00:00"
4844        );
4845    }
4846
4847    #[test]
4848    fn test_beqldate() {
4849        assert_magic_match_bin!(
4850            "0 beqldate 946684800 Local date (Jan 1, 2000)",
4851            b"\x00\x00\x00\x00\x38\x6D\x43\x80"
4852        );
4853
4854        assert_magic_not_match_bin!(
4855            "0 beqldate 946684800 Local date (Jan 1, 2000)",
4856            b"\x00\x00\x00\x00\x00\x00\x00\x00"
4857        );
4858
4859        assert_magic_match_bin!(
4860            "0 beqldate 946684800 %s",
4861            b"\x00\x00\x00\x00\x38\x6D\x43\x80",
4862            unix_local_time_to_string(946684800)
4863        );
4864    }
4865
4866    #[test]
4867    fn test_medate() {
4868        assert_magic_match_bin!(
4869            "0 medate 946684800 Unix date (Jan 1, 2000)",
4870            b"\x6D\x38\x80\x43"
4871        );
4872
4873        assert_magic_not_match_bin!(
4874            "0 medate 946684800 Unix date (Jan 1, 2000)",
4875            b"\x00\x00\x00\x00"
4876        );
4877
4878        assert_magic_match_bin!(
4879            "4 medate 946684800 %s",
4880            b"\x00\x00\x00\x00\x6D\x38\x80\x43",
4881            "2000-01-01 00:00:00"
4882        );
4883    }
4884
4885    #[test]
4886    fn test_meldate() {
4887        assert_magic_match_bin!(
4888            "0 meldate 946684800 Local date (Jan 1, 2000)",
4889            b"\x6D\x38\x80\x43"
4890        );
4891        assert_magic_not_match_bin!(
4892            "0 meldate 946684800 Local date (Jan 1, 2000)",
4893            b"\x00\x00\x00\x00"
4894        );
4895
4896        assert_magic_match_bin!(
4897            "4 meldate 946684800 %s",
4898            b"\x00\x00\x00\x00\x6D\x38\x80\x43",
4899            unix_local_time_to_string(946684800)
4900        );
4901    }
4902
4903    #[test]
4904    fn test_date() {
4905        assert_magic_match_bin!(
4906            "0 date 946684800 Local date (Jan 1, 2000)",
4907            b"\x80\x43\x6D\x38"
4908        );
4909        assert_magic_not_match_bin!(
4910            "0 date 946684800 Local date (Jan 1, 2000)",
4911            b"\x00\x00\x00\x00"
4912        );
4913        assert_magic_match_bin!(
4914            "4 date 946684800 {}",
4915            b"\x00\x00\x00\x00\x80\x43\x6D\x38",
4916            "2000-01-01 00:00:00"
4917        );
4918    }
4919
4920    #[test]
4921    fn test_leldate() {
4922        assert_magic_match_bin!(
4923            "0 leldate 946684800 Local date (Jan 1, 2000)",
4924            b"\x80\x43\x6D\x38"
4925        );
4926        assert_magic_not_match_bin!(
4927            "0 leldate 946684800 Local date (Jan 1, 2000)",
4928            b"\x00\x00\x00\x00"
4929        );
4930        assert_magic_match_bin!(
4931            "4 leldate 946684800 {}",
4932            b"\x00\x00\x00\x00\x80\x43\x6D\x38",
4933            unix_local_time_to_string(946684800)
4934        );
4935    }
4936
4937    #[test]
4938    fn test_leqdate() {
4939        assert_magic_match_bin!(
4940            "0 leqdate 1577836800 Unix date (Jan 1, 2020)",
4941            b"\x00\xe1\x0b\x5E\x00\x00\x00\x00"
4942        );
4943
4944        assert_magic_not_match_bin!(
4945            "0 leqdate 1577836800 Unix date (Jan 1, 2020)",
4946            b"\x00\x00\x00\x00\x00\x00\x00\x00"
4947        );
4948        assert_magic_match_bin!(
4949            "8 leqdate 1577836800 %s",
4950            b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\xE1\x0B\x5E\x00\x00\x00\x00",
4951            "2020-01-01 00:00:00"
4952        );
4953    }
4954
4955    #[test]
4956    fn test_leqldate() {
4957        assert_magic_match_bin!(
4958            "0 leqldate 1577836800 Unix date (Jan 1, 2020)",
4959            b"\x00\xe1\x0b\x5E\x00\x00\x00\x00"
4960        );
4961
4962        assert_magic_not_match_bin!(
4963            "0 leqldate 1577836800 Unix date (Jan 1, 2020)",
4964            b"\x00\x00\x00\x00\x00\x00\x00\x00"
4965        );
4966        assert_magic_match_bin!(
4967            "8 leqldate 1577836800 %s",
4968            b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\xE1\x0B\x5E\x00\x00\x00\x00",
4969            unix_local_time_to_string(1577836800)
4970        );
4971    }
4972
4973    #[test]
4974    fn test_melong() {
4975        // Test = operator
4976        assert_magic_match_bin!(
4977            "0 melong =0x12345678 Middle-endian long",
4978            b"\x34\x12\x78\x56"
4979        );
4980        assert_magic_not_match_bin!(
4981            "0 melong =0x12345678 Middle-endian long",
4982            b"\x00\x00\x00\x00"
4983        );
4984
4985        // Test < operator
4986        assert_magic_match_bin!(
4987            "0 melong <0x12345678 Middle-endian long",
4988            b"\x34\x12\x78\x55"
4989        ); // 0x12345677 in middle-endian
4990        assert_magic_not_match_bin!(
4991            "0 melong <0x12345678 Middle-endian long",
4992            b"\x34\x12\x78\x56"
4993        ); // 0x12345678 in middle-endian
4994
4995        // Test > operator
4996        assert_magic_match_bin!(
4997            "0 melong >0x12345678 Middle-endian long",
4998            b"\x34\x12\x78\x57"
4999        ); // 0x12345679 in middle-endian
5000        assert_magic_not_match_bin!(
5001            "0 melong >0x12345678 Middle-endian long",
5002            b"\x34\x12\x78\x56"
5003        ); // 0x12345678 in middle-endian
5004
5005        // Test & operator
5006        assert_magic_match_bin!("0 melong &0x5678 Middle-endian long", b"\xab\xcd\x78\x56"); // 0x00007856 in middle-endian
5007        assert_magic_not_match_bin!(
5008            "0 melong &0x0000FFFF Middle-endian long",
5009            b"\x34\x12\x78\x56"
5010        ); // 0x12347856 in middle-endian
5011
5012        // Test ^ operator (bitwise AND with complement)
5013        assert_magic_match_bin!(
5014            "0 melong ^0xFFFF0000 Middle-endian long",
5015            b"\x00\x00\x78\x56"
5016        ); // 0x00007856 in middle-endian
5017        assert_magic_not_match_bin!(
5018            "0 melong ^0xFFFF0000 Middle-endian long",
5019            b"\x00\x01\x78\x56"
5020        ); // 0x00017856 in middle-endian
5021
5022        // Test ~ operator
5023        assert_magic_match_bin!(
5024            "0 melong ~0x12345678 Middle-endian long",
5025            b"\xCB\xED\x87\xA9"
5026        );
5027        assert_magic_not_match_bin!(
5028            "0 melong ~0x12345678 Middle-endian long",
5029            b"\x34\x12\x78\x56"
5030        ); // The original value
5031
5032        // Test x operator
5033        assert_magic_match_bin!("0 melong x Middle-endian long", b"\x34\x12\x78\x56");
5034        assert_magic_match_bin!("0 melong x Middle-endian long", b"\x00\x00\x00\x00");
5035    }
5036
5037    #[test]
5038    fn test_uquad() {
5039        // Test = operator
5040        assert_magic_match_bin!(
5041            "0 uquad =0x123456789ABCDEF0 Unsigned quad",
5042            b"\xF0\xDE\xBC\x9A\x78\x56\x34\x12"
5043        );
5044        assert_magic_not_match_bin!(
5045            "0 uquad =0x123456789ABCDEF0 Unsigned quad",
5046            b"\x00\x00\x00\x00\x00\x00\x00\x00"
5047        );
5048
5049        // Test < operator
5050        assert_magic_match_bin!(
5051            "0 uquad <0x123456789ABCDEF0 Unsigned quad",
5052            b"\xF0\xDE\xBC\x9A\x78\x56\x34\x11"
5053        );
5054        assert_magic_not_match_bin!(
5055            "0 uquad <0x123456789ABCDEF0 Unsigned quad",
5056            b"\xF0\xDE\xBC\x9A\x78\x56\x34\x12"
5057        );
5058
5059        // Test > operator
5060        assert_magic_match_bin!(
5061            "0 uquad >0x123456789ABCDEF0 Unsigned quad",
5062            b"\xF0\xDE\xBC\x9A\x78\x56\x34\x13"
5063        );
5064        assert_magic_not_match_bin!(
5065            "0 uquad >0x123456789ABCDEF0 Unsigned quad",
5066            b"\xF0\xDE\xBC\x9A\x78\x56\x34\x12"
5067        );
5068
5069        // Test & operator
5070        assert_magic_match_bin!(
5071            "0 uquad &0xF0 Unsigned quad",
5072            b"\xF0\xDE\xBC\x9A\x78\x56\x34\x12"
5073        );
5074        assert_magic_not_match_bin!(
5075            "0 uquad &0xFF Unsigned quad",
5076            b"\xF0\xDE\xBC\x9A\x78\x56\x34\x12"
5077        );
5078
5079        // Test ^ operator (bitwise AND with complement)
5080        assert_magic_match_bin!(
5081            "0 uquad ^0xFFFFFFFFFFFFFFFF Unsigned quad",
5082            b"\x00\x00\x00\x00\x00\x00\x00\x00"
5083        ); // All bits clear
5084        assert_magic_not_match_bin!(
5085            "0 uquad ^0xFFFFFFFFFFFFFFFF Unsigned quad",
5086            b"\xF0\xDE\xBC\x9A\x78\x56\x34\x12"
5087        ); // Some bits set
5088
5089        // Test ~ operator
5090        assert_magic_match_bin!(
5091            "0 uquad ~0x123456789ABCDEF0 Unsigned quad",
5092            b"\x0F\x21\x43\x65\x87\xA9\xCB\xED"
5093        );
5094        assert_magic_not_match_bin!(
5095            "0 uquad ~0x123456789ABCDEF0 Unsigned quad",
5096            b"\xF0\xDE\xBC\x9A\x78\x56\x34\x12"
5097        ); // The original value
5098
5099        // Test x operator
5100        assert_magic_match_bin!(
5101            "0 uquad x {:#x}",
5102            b"\xF0\xDE\xBC\x9A\x78\x56\x34\x12",
5103            "0x123456789abcdef0"
5104        );
5105        assert_magic_match_bin!(
5106            "0 uquad x Unsigned quad",
5107            b"\x00\x00\x00\x00\x00\x00\x00\x00"
5108        );
5109    }
5110
5111    // guid stores data1/data2/data3 little-endian (the Microsoft
5112    // mixed-endian GUID layout), only data4 is raw bytes -- confirmed
5113    // against real `file`.
5114    #[test]
5115    fn test_guid() {
5116        assert_magic_match_bin!(
5117            "0 guid DDCCBBAA-FFEE-1100-2233-445566778899 It works",
5118            b"\xAA\xBB\xCC\xDD\xEE\xFF\x00\x11\x22\x33\x44\x55\x66\x77\x88\x99"
5119        );
5120
5121        assert_magic_not_match_bin!(
5122            "0 guid DDCCBBAA-FFEE-1100-2233-445566778899 It works",
5123            b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F"
5124        );
5125
5126        assert_magic_match_bin!(
5127            "0 guid x %s",
5128            b"\xAA\xBB\xCC\xDD\xEE\xFF\x00\x11\x22\x33\x44\x55\x66\x77\x88\x99",
5129            "DDCCBBAA-FFEE-1100-2233-445566778899"
5130        );
5131    }
5132
5133    // leguid has identical semantics to guid.
5134    #[test]
5135    fn test_leguid() {
5136        assert_magic_match_bin!(
5137            "0 leguid DDCCBBAA-FFEE-1100-2233-445566778899 It works",
5138            b"\xAA\xBB\xCC\xDD\xEE\xFF\x00\x11\x22\x33\x44\x55\x66\x77\x88\x99"
5139        );
5140
5141        assert_magic_not_match_bin!(
5142            "0 leguid DDCCBBAA-FFEE-1100-2233-445566778899 It works",
5143            b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F"
5144        );
5145
5146        assert_magic_match_bin!(
5147            "0 leguid x %s",
5148            b"\xAA\xBB\xCC\xDD\xEE\xFF\x00\x11\x22\x33\x44\x55\x66\x77\x88\x99",
5149            "DDCCBBAA-FFEE-1100-2233-445566778899"
5150        );
5151    }
5152
5153    // beguid has no on-disk mixed-endian convention, unlike guid/leguid: the
5154    // 16 bytes are one plain big-endian number, matching the string's
5155    // sequential digit order directly.
5156    #[test]
5157    fn test_beguid() {
5158        assert_magic_match_bin!(
5159            "0 beguid AABBCCDD-EEFF-0011-2233-445566778899 It works",
5160            b"\xAA\xBB\xCC\xDD\xEE\xFF\x00\x11\x22\x33\x44\x55\x66\x77\x88\x99"
5161        );
5162
5163        assert_magic_not_match_bin!(
5164            "0 beguid AABBCCDD-EEFF-0011-2233-445566778899 It works",
5165            b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F"
5166        );
5167
5168        assert_magic_match_bin!(
5169            "0 beguid x %s",
5170            b"\xAA\xBB\xCC\xDD\xEE\xFF\x00\x11\x22\x33\x44\x55\x66\x77\x88\x99",
5171            "AABBCCDD-EEFF-0011-2233-445566778899"
5172        );
5173    }
5174
5175    #[test]
5176    fn test_ubeqdate() {
5177        assert_magic_match_bin!(
5178            "0 ubeqdate 1633046400 It works",
5179            b"\x00\x00\x00\x00\x61\x56\x4f\x80"
5180        );
5181
5182        assert_magic_match_bin!(
5183            "0 ubeqdate x %s",
5184            b"\x00\x00\x00\x00\x61\x56\x4f\x80",
5185            "2021-10-01 00:00:00"
5186        );
5187
5188        assert_magic_not_match_bin!(
5189            "0 ubeqdate 1633046400 It should not work",
5190            b"\x00\x00\x00\x00\x00\x00\x00\x00"
5191        );
5192    }
5193
5194    #[test]
5195    fn test_ldate() {
5196        assert_magic_match_bin!("0 ldate 1640551520 It works", b"\x60\xd4\xC8\x61");
5197
5198        assert_magic_not_match_bin!("0 ldate 1633046400 It should not work", b"\x00\x00\x00\x00");
5199
5200        assert_magic_match_bin!(
5201            "0 ldate x %s",
5202            b"\x60\xd4\xC8\x61",
5203            unix_local_time_to_string(1640551520)
5204        );
5205    }
5206
5207    #[test]
5208    fn test_scalar_with_transform() {
5209        assert_magic_match_bin!("0 ubyte/10 2 {}", b"\x14", "2");
5210        assert_magic_match_bin!("0 ubyte/10 x {}", b"\x14", "2");
5211        assert_magic_match_bin!("0 ubyte%10 x {}", b"\x14", "0");
5212    }
5213
5214    #[test]
5215    fn test_float_with_transform() {
5216        assert_magic_match_bin!("0 lefloat/10 2 {}", b"\x00\x00\xa0\x41", "2");
5217        assert_magic_match_bin!("0 lefloat/10 x {}", b"\x00\x00\xa0\x41", "2");
5218        assert_magic_match_bin!("0 lefloat%10 x {}", b"\x00\x00\xa0\x41", "0");
5219    }
5220
5221    #[test]
5222    fn test_read_octal() {
5223        // Basic cases
5224        assert_eq!(read_octal_u64(&mut buf_reader!("0")), Some(0));
5225        assert_eq!(read_octal_u64(&mut buf_reader!("00")), Some(0));
5226        assert_eq!(read_octal_u64(&mut buf_reader!("01")), Some(1));
5227        assert_eq!(read_octal_u64(&mut buf_reader!("07")), Some(7));
5228        assert_eq!(read_octal_u64(&mut buf_reader!("010")), Some(8));
5229        assert_eq!(read_octal_u64(&mut buf_reader!("0123")), Some(83));
5230        assert_eq!(read_octal_u64(&mut buf_reader!("0755")), Some(493));
5231
5232        // With trailing non-octal characters
5233        assert_eq!(read_octal_u64(&mut buf_reader!("0ABC")), Some(0));
5234        assert_eq!(read_octal_u64(&mut buf_reader!("01ABC")), Some(1));
5235        assert_eq!(read_octal_u64(&mut buf_reader!("0755ABC")), Some(493));
5236        assert_eq!(read_octal_u64(&mut buf_reader!("0123ABC")), Some(83));
5237
5238        // Invalid octal digits
5239        assert_eq!(read_octal_u64(&mut buf_reader!("08")), Some(0)); // stops at '8'
5240        assert_eq!(read_octal_u64(&mut buf_reader!("01238")), Some(83)); // stops at '8'
5241
5242        // No leading '0'
5243        assert_eq!(read_octal_u64(&mut buf_reader!("123")), None);
5244        assert_eq!(read_octal_u64(&mut buf_reader!("755")), None);
5245
5246        // Empty string
5247        assert_eq!(read_octal_u64(&mut buf_reader!("")), None);
5248
5249        // Only non-octal characters
5250        assert_eq!(read_octal_u64(&mut buf_reader!("ABC")), None);
5251        assert_eq!(read_octal_u64(&mut buf_reader!("8ABC")), None); // first char is not '0'
5252
5253        // Longer valid octal (but within u64 range)
5254        assert_eq!(
5255            read_octal_u64(&mut buf_reader!("01777777777")),
5256            Some(268435455)
5257        );
5258    }
5259
5260    #[test]
5261    fn test_offset_bug_1() {
5262        // this tests the exact behaviour
5263        // expected by libmagic/file
5264        assert_magic_match_bin!(
5265            r"
52661	string		TEST Bread is
5267# offset computation is relative to
5268# rule start
5269>(5.b)	use toasted
5270
52710 name toasted
5272>0	string twice Toasted
5273>>0  use toasted_twice
5274
52750 name toasted_twice
5276>(6.b) string x %s
5277        ",
5278            b"\x00TEST\x06twice\x00\x06",
5279            "Bread is Toasted twice"
5280        );
5281    }
5282
5283    // this test implement the exact same logic as
5284    // test_offset_bug_1 except that the rule starts
5285    // matching from end. Surprisingly we need to
5286    // adjust indirect offsets so that it works in
5287    // libmagic/file
5288    #[test]
5289    fn test_offset_bug_2() {
5290        // this tests the exact behaviour
5291        // expected by libmagic/file
5292        assert_magic_match_bin!(
5293            r"
5294-12	string		TEST Bread is
5295>(4.b)	use toasted
5296
52970 name toasted
5298>0	string twice Toasted
5299>>0  use toasted_twice
5300
53010 name toasted_twice
5302>(6.b) string x %
5303        ",
5304            b"\x00TEST\x06twice\x00\x06",
5305            "Bread is Toasted twice"
5306        )
5307    }
5308
5309    #[test]
5310    fn test_offset_bug_3() {
5311        // this tests the exact behaviour
5312        // expected by libmagic/file
5313        assert_magic_match_bin!(
5314            r"
53151	string		TEST Bread is
5316>(5.b) indirect/r x
5317
53180	string twice Toasted
5319>0  use toasted_twice
5320
53210 name toasted_twice
5322>0 string x %s
5323        ",
5324            b"\x00TEST\x06twice\x00\x08",
5325            "Bread is Toasted twice"
5326        )
5327    }
5328
5329    #[test]
5330    fn test_offset_bug_4() {
5331        // this tests the exact behaviour
5332        // expected by libmagic/file
5333        assert_magic_match_bin!(
5334            r"
53351	string		Bread %s
5336>(6.b) indirect/r x
5337
5338# this one uses a based offset
5339# computed at indirection
53401	string is\ Toasted %s
5341>(11.b)  use toasted_twice
5342
5343# this one is using a new base
5344# offset being previous base
5345# offset + offset of use
53460 name toasted_twice
5347>0 string x %s
5348            ",
5349            b"\x00Bread\x06is Toasted\x0ctwice\x00",
5350            "Bread is Toasted twice"
5351        )
5352    }
5353
5354    #[test]
5355    fn test_offset_bug_5() {
5356        assert_magic_match_bin!(
5357            r"
53581	string		TEST Bread is
5359>(5.b) indirect/r x
5360
53610	string twice Toasted
5362>0  use toasted_twice
5363
53640 name toasted_twice
5365>0 string twice
5366>>&1 byte 0x08 twice
5367            ",
5368            b"\x00TEST\x06twice\x00\x08",
5369            "Bread is Toasted twice"
5370        )
5371    }
5372
5373    #[test]
5374    fn test_bug_6() {
5375        // An indirect use test should not be successful
5376        // even if a match with no message occurs
5377
5378        assert_magic_match_bin!(
5379            r"
53801	string		TEST Bread is toasted
5381>&0 use toasted
5382>>&0 default x but not burnt
5383
53840 name toasted
5385>1 string toasted
5386            ",
5387            b"\x00TEST\x06toasted",
5388            "Bread is toasted"
5389        )
5390    }
5391
5392    #[test]
5393    fn test_offset_bug_7() {
5394        // Bug: nested 'use' directives with indirect offsets don't properly
5395        // adjust offsets during recursion. This test encodes the behavior
5396        // libmagic has when dealing with such scenarios.
5397        assert_magic_match_bin!(
5398            r"
53991	string		TEST Bread is
5400# offset computation is relative to
5401# rule start
5402>(5.b)	use toasted
5403
54040 name toasted
5405>0	string toast Toasted
5406>>(6.b)  use toasted_twice
5407
54080 name toasted_twice
5409>1 string x %s
5410        ",
5411            b"\x00TEST\x06toast\x00\x06twice\x00",
5412            "Bread is Toasted twice"
5413        );
5414    }
5415
5416    #[test]
5417    fn test_offset_bug_8() {
5418        // A bare (non-'&') nested offset under an '&'-relative sibling
5419        // must resolve as absolute from file start, not relative to the
5420        // enclosing end-relative top-level anchor.
5421        //   -2 uleshort 0
5422        //   >&-22 string PK\005\006
5423        //   >>0 string SIG  <- must check absolute offset 0, not
5424        //                      "top-level anchor" + 0.
5425        assert_magic_match_bin!(
5426            r"
5427-2	uleshort	0
5428>&-22	string	PK\005\006
5429>>0	string	SIG	found SIG at absolute start
5430            ",
5431            b"SIG\x00\x00\x00\x00\x00PK\x05\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
5432            "found SIG at absolute start"
5433        );
5434    }
5435
5436    #[test]
5437    fn test_offset_bug_9() {
5438        // Same root cause as test_offset_bug_8, but for the "N" in an
5439        // indirect `(N.b)` offset expression instead of a bare `>>N`
5440        // continuation. Byte 0 holds 3: if `(0.b)` resolves absolute
5441        // (correct, matching real file), it reads that 3 and dispatches
5442        // `whichbyte` at offset 3, where "YES" sits. If it wrongly
5443        // resolves relative to the stale end-relative anchor (byte 28,
5444        // part of the `-2 uleshort 0` test, always 0 here), it
5445        // dispatches at offset 0 instead, landing back on the literal
5446        // byte 3 itself.
5447        assert_magic_match_bin!(
5448            r"
5449-2	uleshort	0
5450>&-22	string	PK\005\006
5451>>(0.b)	use	whichbyte
5452
54530	name	whichbyte
5454>0	byte	0x03	found byte 0x03 (wrong: stale anchor)
5455>0	string	YES	found YES marker (correct: absolute)
5456            ",
5457            b"\x03\x00\x00YES\x00\x00PK\x05\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
5458            "found YES marker (correct: absolute)"
5459        );
5460    }
5461
5462    #[test]
5463    fn test_offset_bug_10() {
5464        // Regression (real-world case: magic-db/src/magdir/msooxml
5465        // failing to detect .xlsx/.docx/.pptx). `inner`'s failed
5466        // `search/1000` for "NEEDLE" scans all the way to EOF, leaving
5467        // the shared stream cursor there. The sibling `default` entry
5468        // reads nothing of its own, so it must use its own anchor
5469        // (0x10) for its children's `&`-relative offsets instead of
5470        // that leftover EOF cursor -- otherwise `>>&5` computes
5471        // 40 + 5 = 45 (out of bounds) instead of landing on "TARGET"
5472        // at byte 21.
5473        assert_magic_match_bin!(
5474            r"
54750	string	MAGIC
5476>0x10	use	inner
5477>0x10	default	x
5478>>&5	string	TARGET	found target via correct anchor
5479
54800	name	inner
5481>0	search/1000	NEEDLE
5482            ",
5483            b"MAGIC\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00TARGET\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
5484            "found target via correct anchor"
5485        );
5486    }
5487
5488    #[test]
5489    fn test_offset_bug_11() {
5490        // Same root cause as test_offset_bug_10, confirmed independently
5491        // for `clear` (a `default` sibling isn't the only entry type that
5492        // reads nothing of its own and so is vulnerable to inheriting a
5493        // preceding `use`'s leftover stream position).
5494        assert_magic_match_bin!(
5495            r"
54960	string	MAGIC
5497>0x10	use	inner
5498>0x10	clear	x
5499>>&5	string	TARGET	found target via correct anchor
5500
55010	name	inner
5502>0	search/1000	NEEDLE
5503            ",
5504            b"MAGIC\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00TARGET\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
5505            "found target via correct anchor"
5506        );
5507    }
5508
5509    #[test]
5510    fn test_message_parts() {
5511        let m = best_magic(
5512            r#"0	string/W	#!/usr/bin/env\ python  PYTHON"#,
5513            b"#!/usr/bin/env    python",
5514            StreamKind::Text(TextEncoding::Ascii),
5515        )
5516        .unwrap();
5517
5518        assert!(m.message_parts().any(|p| p.eq_ignore_ascii_case("python")))
5519    }
5520
5521    #[test]
5522    fn test_magic_stream_kind_reflects_detection() {
5523        let mut db = MagicDb::new();
5524        db.load(parse_assert!("0\tsearch\t__NEVER_MATCH__\tnope\n"));
5525
5526        let mut ascii = BufReader::from_slice(b"hello world");
5527        let m = db.best_magic(&mut ascii, None).unwrap();
5528        assert_eq!(m.stream_kind(), Some(StreamKind::Text(TextEncoding::Ascii)));
5529
5530        let mut utf8 = BufReader::from_slice("héllo wörld".as_bytes());
5531        let m = db.best_magic(&mut utf8, None).unwrap();
5532        assert_eq!(m.stream_kind(), Some(StreamKind::Text(TextEncoding::Utf8)));
5533
5534        let mut binary = BufReader::from_slice(&[0x00u8, 0x01, 0x02, 0xff, 0xfe, 0x00, 0x00, 0x00]);
5535        let m = db.best_magic(&mut binary, None).unwrap();
5536        assert_eq!(m.stream_kind(), Some(StreamKind::Binary));
5537    }
5538
5539    // Regression: a buffer that's valid ASCII/UTF-8 byte-for-byte (every byte
5540    // < 0x80) can still contain control bytes (e.g. embedded NUL, as in a
5541    // fuzzed PE header) that libmagic's text_chars whitelist rejects
5542    // (encoding.c: looks_ascii/file_looks_utf8), making it treat the buffer
5543    // as binary rather than text. Confirmed against real `file
5544    // --mime-encoding`.
5545    #[test]
5546    fn test_stream_kind_rejects_ascii_with_forbidden_control_bytes() {
5547        assert_eq!(
5548            guess_stream_kind(b"MZ\x00\x00\x00\x00this is otherwise all printable ASCII"),
5549            StreamKind::Binary
5550        );
5551    }
5552
5553    #[test]
5554    fn test_load_bulk() {
5555        let mut db = MagicDb::new();
5556
5557        let rules = vec![
5558            parse_assert!("0 search test"),
5559            parse_assert!("0 search/24/s test"),
5560            parse_assert!("0 search/s/24 test"),
5561        ];
5562
5563        db.load_bulk(rules.into_iter());
5564        db.verify().unwrap();
5565    }
5566
5567    #[test]
5568    fn test_load_bulk_failure() {
5569        let mut db = MagicDb::new();
5570
5571        let rules = vec![parse_assert!(
5572            r#"
55730 search/s/24 test
5574>0 use test
5575"#
5576        )];
5577
5578        db.load_bulk(rules.into_iter());
5579        assert!(matches!(db.verify(), Err(Error::Verify(_, _, _))));
5580    }
5581
5582    // try_csv runs before any rule; pass a never-matching rule so the
5583    // harness only exercises the hardcoded CSV detector.
5584    fn csv_magic(content: &[u8]) -> Magic<'static> {
5585        best_magic(
5586            "0\tstring\t__NEVER_MATCH__\tnope\n",
5587            content,
5588            StreamKind::Text(TextEncoding::Utf8),
5589        )
5590        .unwrap()
5591    }
5592
5593    #[test]
5594    fn test_csv_two_rows_two_cols() {
5595        let m = csv_magic(b"a,b\n1,2\n");
5596        assert_eq!(m.mime_type(), "text/csv");
5597    }
5598
5599    #[test]
5600    fn test_csv_short_consistent_rows() {
5601        let m = csv_magic(b"a,b,c\n1,2,3\n4,5,6\n7,8,9\n10,11,12\n");
5602        assert_eq!(m.mime_type(), "text/csv");
5603    }
5604
5605    #[test]
5606    fn test_csv_many_rows_still_detected() {
5607        let body: &[u8] = b"a,b,c\n1,2,3\n4,5,6\n7,8,9\n10,11,12\n13,14,15\n16,17,18\n19,20,21\n22,23,24\n25,26,27\n28,29,30\n31,32,33\n";
5608        let m = csv_magic(body);
5609        assert_eq!(m.mime_type(), "text/csv");
5610    }
5611
5612    #[test]
5613    fn test_csv_single_field_rejected() {
5614        let m = csv_magic(b"hello\nworld\nfoo\n");
5615        assert_ne!(m.mime_type(), "text/csv");
5616    }
5617
5618    #[test]
5619    fn test_csv_ragged_columns_rejected() {
5620        let m = csv_magic(b"a,b,c\n1,2\n3,4,5\n");
5621        assert_ne!(m.mime_type(), "text/csv");
5622    }
5623
5624    // Regression: nested scalar tests under a top-level `string` match used to
5625    // be dropped on text inputs because the binary/text gate ran inside every
5626    // recursion. The shape mirrors magdir/rtf:11–18 — the message lives on
5627    // the level-2 ubyte child, so dropping the children silently strips the
5628    // whole rule. Upstream libmagic's softmagic.c::match() applies the gate
5629    // only at the outer match loop.
5630    #[test]
5631    fn test_string_parent_with_scalar_children_on_text_stream() {
5632        assert_magic_match_text!(
5633            r"
56340	string		{\\rtf
5635>5	ubyte		!0xAB
5636>>5	ubyte		!0x5C		Rich Text Format data
5637!:mime	text/rtf
5638!:ext	rtf
5639            ",
5640            b"{\\rtf1\\ansi\\ansicpg1252\nHello world}",
5641            "Rich Text Format data"
5642        );
5643    }
5644
5645    // The level-1 `>5 ubyte !0xAB` qualifier exists upstream specifically to
5646    // skip DROID fmt-355-signature-id-522.rtf. With \xAB at offset 5 the rule
5647    // must NOT emit a message even now that children run on text streams.
5648    #[test]
5649    fn test_rtf_droid_skip_still_rejects() {
5650        assert_magic_not_match_text!(
5651            r"
56520	string		{\\rtf
5653>5	ubyte		!0xAB
5654>>5	ubyte		!0x5C		Rich Text Format data
5655            ",
5656            b"{\\rtf\xab......\n"
5657        );
5658    }
5659
5660    // Regression: a `!=` test against out-of-bounds data is considered true.
5661    #[test]
5662    fn test_neq_test_on_unreadable_offset_matches() {
5663        let mut content = vec![0u8; 64];
5664        content[0] = b'M';
5665        content[1] = b'Z';
5666        // offset 0x3c holds a 4-byte LE offset pointing far past EOF
5667        content[0x3c..0x40].copy_from_slice(&0xFFFF_FFF0u32.to_le_bytes());
5668
5669        let m = best_magic(
5670            "0\tstring\tMZ\n>(0x3c.l)\tstring\t!PE\\0\\0\tMS-DOS executable\n",
5671            &content,
5672            StreamKind::Binary,
5673        )
5674        .unwrap();
5675        assert_eq!(m.message(), "MS-DOS executable");
5676    }
5677
5678    #[test]
5679    fn test_eq_test_on_unreadable_offset_does_not_match() {
5680        let mut content = vec![0u8; 64];
5681        content[0] = b'M';
5682        content[1] = b'Z';
5683        content[0x3c..0x40].copy_from_slice(&0xFFFF_FFF0u32.to_le_bytes());
5684
5685        let m = best_magic(
5686            "0\tstring\tMZ\n>(0x3c.l)\tstring\tPE\\0\\0\tPE executable\n",
5687            &content,
5688            StreamKind::Binary,
5689        )
5690        .unwrap();
5691        assert_eq!(m.message(), "data");
5692    }
5693}