Skip to main content

relibc/header/fnmatch/
mod.rs

1//! `fnmatch.h` implementation.
2//!
3//! See <https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/fnmatch.h.html>.
4
5use alloc::{borrow::Cow, vec::Vec};
6use core::slice;
7
8use crate::platform::types::{c_char, c_int};
9use posix_regex::{
10    PosixRegex,
11    compile::{Collation, Range, Token},
12    tree::{Tree, TreeBuilder},
13};
14
15/// cbindgen:ignore
16const ONCE: Range = Range(1, Some(1));
17
18/// The input string does not match the specified pattern.
19pub const FNM_NOMATCH: c_int = 1;
20
21/// Disable backslash escaping.
22pub const FNM_NOESCAPE: c_int = 1;
23/// Backslash in the input string only matches backlash in pattern.
24pub const FNM_PATHNAME: c_int = 2;
25/// Leading period in input string only matches period in pattern.
26pub const FNM_PERIOD: c_int = 4;
27/// Compare input string and pattern in a case-insensitive manner.
28pub const FNM_CASEFOLD: c_int = 8;
29/// Equivalent to `FNM_CASEFOLD`.
30pub const FNM_IGNORECASE: c_int = FNM_CASEFOLD;
31// TODO: FNM_EXTMATCH (Non-POSIX)
32
33unsafe fn tokenize(mut pattern: *const u8, flags: c_int) -> Tree {
34    fn any(leading: bool, flags: c_int) -> Token {
35        let mut list = Vec::new();
36        if flags & FNM_PATHNAME == FNM_PATHNAME {
37            list.push(Collation::Char(b'/'))
38        }
39        if leading && flags & FNM_PERIOD == FNM_PERIOD {
40            list.push(Collation::Char(b'.'))
41        }
42        Token::OneOf { invert: true, list }
43    }
44    fn can_push(leading: bool, flags: c_int, c: u8) -> bool {
45        (c != b'/' || flags & FNM_PATHNAME != FNM_PATHNAME)
46            && (c != b'.' || !leading || flags & FNM_PERIOD != FNM_PERIOD)
47    }
48    fn is_leading(flags: c_int, c: u8) -> bool {
49        c == b'/' && flags & FNM_PATHNAME == FNM_PATHNAME
50    }
51
52    let mut leading = true;
53    let mut need_collapsing = false;
54
55    let mut builder = TreeBuilder::default();
56    builder.start_internal(Token::Root, Range(1, Some(1)));
57    builder.start_internal(Token::Alternative, Range(1, Some(1)));
58
59    while unsafe { *pattern != 0 } {
60        let was_leading = leading;
61        leading = false;
62
63        let c = unsafe { *pattern };
64        pattern = unsafe { pattern.offset(1) };
65
66        match (c == b'*', need_collapsing) {
67            (true, true) => continue,
68            (true, false) => need_collapsing = true,
69            (false, _) => need_collapsing = false,
70        }
71
72        let (token, range) = match c {
73            b'\\' if flags & FNM_NOESCAPE == FNM_NOESCAPE => {
74                let c = unsafe { *pattern };
75                if c == 0 {
76                    // Trailing backslash. Maybe error here?
77                    break;
78                }
79                pattern = unsafe { pattern.offset(1) };
80                leading = is_leading(flags, c);
81                (Token::Char(c), ONCE)
82            }
83            b'?' => (any(was_leading, flags), ONCE),
84            b'*' => (any(was_leading, flags), Range(0, None)),
85            b'[' => {
86                let mut list: Vec<Collation> = Vec::new();
87                let invert = if unsafe { *pattern == b'!' } {
88                    pattern = unsafe { pattern.offset(1) };
89                    true
90                } else {
91                    false
92                };
93
94                loop {
95                    let mut c = unsafe { *pattern };
96                    if c == 0 {
97                        break;
98                    }
99                    pattern = unsafe { pattern.offset(1) };
100                    match c {
101                        b']' => break,
102                        b'\\' => {
103                            c = unsafe { *pattern };
104                            pattern = unsafe { pattern.offset(1) };
105                            if c == 0 {
106                                // Trailing backslash. Maybe error?
107                                break;
108                            }
109                        }
110                        _ => (),
111                    }
112                    if unsafe { *pattern == b'-' && *pattern.offset(1) != 0 } {
113                        let end = unsafe { *pattern.offset(1) };
114                        pattern = unsafe { pattern.offset(2) };
115                        for c in c..=end {
116                            if can_push(was_leading, flags, c) {
117                                list.push(Collation::Char(c));
118                            }
119                        }
120                    } else if can_push(was_leading, flags, c) {
121                        list.push(Collation::Char(c));
122                    }
123                }
124                // Otherwise, there was no closing ]. Maybe error?
125
126                (Token::OneOf { invert, list }, ONCE)
127            }
128            c => {
129                leading = is_leading(flags, c);
130                (Token::Char(c), ONCE)
131            }
132        };
133        builder.leaf(token, range);
134    }
135    builder.leaf(Token::End, ONCE);
136    builder.finish_internal();
137    builder.finish_internal();
138    builder.finish()
139}
140
141/// See <https://pubs.opengroup.org/onlinepubs/9799919799/functions/fnmatch.html>.
142///
143/// Checks the `input` string to see if it matches `pattern`.
144///
145/// # Safety
146///
147/// The `input` and `pattern` parameters must be non-null and properly aligned.
148#[unsafe(no_mangle)]
149#[linkage = "weak"] // often redefined in GNU programs
150pub unsafe extern "C" fn fnmatch(
151    pattern: *const c_char,
152    input: *const c_char,
153    flags: c_int,
154) -> c_int {
155    let mut len = 0;
156    while unsafe { *input.offset(len) != 0 } {
157        len += 1;
158    }
159    let input = unsafe { slice::from_raw_parts(input.cast::<u8>(), len as usize) };
160
161    let tokens = unsafe { tokenize(pattern.cast::<u8>(), flags) };
162
163    if PosixRegex::new(Cow::Owned(tokens))
164        .case_insensitive(flags & FNM_CASEFOLD == FNM_CASEFOLD)
165        .matches_exact(input)
166        .is_some()
167    {
168        0
169    } else {
170        FNM_NOMATCH
171    }
172}