Skip to main content

relibc/header/glob/
mod.rs

1//! `glob.h` implementation.
2//!
3//! See <https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/glob.h.html>.
4
5use core::ptr;
6
7use alloc::{boxed::Box, vec::Vec};
8
9use crate::{
10    c_str::{CStr, CString},
11    header::{
12        dirent::{closedir, opendir, readdir},
13        errno::*,
14        fnmatch::{FNM_NOESCAPE, FNM_PERIOD, fnmatch},
15        sys_stat::{S_IFDIR, S_IFMT, stat},
16    },
17    platform::{
18        self,
19        types::{c_char, c_int, c_uchar, c_void, size_t},
20    },
21};
22
23// Cause glob() to return on error
24pub const GLOB_ERR: c_int = 0x0001;
25// Each pathname that is a directory that matches pattern has a slash appended
26pub const GLOB_MARK: c_int = 0x0002;
27// Do not sort returned pathnames
28pub const GLOB_NOSORT: c_int = 0x0004;
29// Add gl_offs amount of null pointers to the beginning of `gl_pathv`
30pub const GLOB_DOOFFS: c_int = 0x0008;
31// If pattern does not match, return a list containing only pattern
32pub const GLOB_NOCHECK: c_int = 0x0010;
33// Append generated pathnames to those previously obtained
34pub const GLOB_APPEND: c_int = 0x0020;
35// Disable backslash escaping
36pub const GLOB_NOESCAPE: c_int = 0x0040;
37// Allow wildcards to match '.' (GNU extension)
38pub const GLOB_PERIOD: c_int = 0x0080;
39
40// Attempt to allocate memory failed
41pub const GLOB_NOSPACE: c_int = 1;
42// Scan was stopped because GLOB_ERR was set or `errfunc` returned non-zero
43pub const GLOB_ABORTED: c_int = 2;
44// Pattern does not match any existing pathname, and GLOB_NOCHECK was not set
45pub const GLOB_NOMATCH: c_int = 3;
46
47/// See <https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/glob.h.html>.
48#[derive(Debug)]
49#[repr(C)]
50pub struct glob_t {
51    pub gl_pathc: size_t, // Count of paths matched by pattern (POSIX required field)
52    pub gl_offs: size_t,  // Slots to reserve at the beginning of gl_pathv (POSIX required field)
53    pub gl_pathv: *mut *mut c_char, // Pointer to list of matched pathnames (POSIX required field)
54
55    // Opaque pointer to allocation data
56    __opaque: *mut c_void, // Vec<*mut c_char>
57}
58
59/// See <https://pubs.opengroup.org/onlinepubs/9799919799/functions/glob.html>.
60#[linkage = "weak"] // GNU prefers its own glob e.g. in Make
61#[unsafe(no_mangle)]
62pub unsafe extern "C" fn glob(
63    pattern: *const c_char,
64    flags: c_int,
65    errfunc: Option<unsafe extern "C" fn(epath: *const c_char, eerrno: c_int) -> c_int>,
66    pglob: *mut glob_t,
67) -> c_int {
68    if flags & GLOB_APPEND != GLOB_APPEND {
69        unsafe {
70            (*pglob).gl_pathc = 0;
71            (*pglob).gl_pathv = ptr::null_mut();
72            (*pglob).__opaque = ptr::null_mut();
73        }
74    }
75
76    let glob_expr = unsafe { CStr::from_ptr(pattern) };
77
78    if glob_expr.to_bytes() == b"" {
79        return GLOB_NOMATCH;
80    }
81
82    let base_path = unsafe {
83        CStr::from_bytes_with_nul_unchecked(if glob_expr.to_bytes().first() == Some(&b'/') {
84            b"/\0"
85        } else {
86            b"\0"
87        })
88    };
89
90    let mut pathv: Box<Vec<*mut c_char>>;
91    if flags & GLOB_APPEND == GLOB_APPEND {
92        pathv = unsafe { Box::from_raw((*pglob).__opaque.cast()) };
93        pathv.pop(); // Remove NULL from end
94    } else {
95        pathv = Box::new(Vec::new());
96        if flags & GLOB_DOOFFS == GLOB_DOOFFS {
97            let gl_offs = unsafe { (*pglob).gl_offs };
98            pathv.reserve(gl_offs);
99            for _ in 0..gl_offs {
100                pathv.push(ptr::null_mut());
101            }
102        }
103    }
104
105    let errfunc = match errfunc {
106        Some(f) => f,
107        None => default_errfunc,
108    };
109
110    // Do the globbing
111    let return_value = match inner_glob(&base_path, &glob_expr, flags, errfunc) {
112        Ok(mut results) => {
113            // Handle GLOB_NOCHECK and no matches
114            if results.is_empty() && flags & GLOB_NOCHECK != GLOB_NOCHECK {
115                GLOB_NOMATCH
116            } else {
117                if results.is_empty() {
118                    results.push(glob_expr.to_owned_cstring());
119                }
120
121                // Handle GLOB_NOSORT
122                if flags & GLOB_NOSORT != GLOB_NOSORT {
123                    results.sort();
124                }
125
126                unsafe {
127                    (*pglob).gl_pathc += results.len();
128                }
129
130                pathv.reserve_exact(results.len());
131                pathv.extend(results.into_iter().map(|s| s.into_raw()));
132
133                0
134            }
135        }
136        Err(e) => e,
137    };
138
139    // add terminating NULL
140    pathv.reserve_exact(1);
141    pathv.push(ptr::null_mut());
142    unsafe {
143        (*pglob).gl_pathv = pathv.as_ptr().cast_mut();
144        (*pglob).__opaque = Box::into_raw(pathv).cast();
145    }
146
147    return_value
148}
149
150/// See <https://pubs.opengroup.org/onlinepubs/9799919799/functions/glob.html>.
151#[linkage = "weak"] // GNU prefers its own glob e.g. in Make
152#[unsafe(no_mangle)]
153pub unsafe extern "C" fn globfree(pglob: *mut glob_t) {
154    // Retake ownership
155    if unsafe { !(*pglob).__opaque.is_null() } {
156        let pathv: Box<Vec<*mut c_char>> = unsafe { Box::from_raw((*pglob).__opaque.cast()) };
157        for (idx, path) in pathv.into_iter().enumerate() {
158            if unsafe { idx < (*pglob).gl_offs } {
159                continue;
160            }
161            if !path.is_null() {
162                unsafe {
163                    drop(CString::from_raw(path));
164                }
165            }
166        }
167        unsafe {
168            (*pglob).gl_pathv = ptr::null_mut();
169        }
170    }
171}
172
173type GlobErrorFunc = unsafe extern "C" fn(epath: *const c_char, eerrno: c_int) -> c_int;
174
175struct DirEntry {
176    name: CString,
177    is_dir: bool,
178}
179
180unsafe extern "C" fn default_errfunc(epath: *const c_char, eerrno: c_int) -> c_int {
181    0
182}
183
184fn list_dir(
185    path: &CStr,
186    errfunc: GlobErrorFunc,
187    abort_on_error: bool,
188) -> Result<Vec<DirEntry>, c_int> {
189    const DT_DIR: c_uchar = 4; // From dirent.h
190    const DT_LNK: c_uchar = 10; // From dirent.h
191
192    let old_errno = platform::ERRNO.get();
193    let mut results: Vec<DirEntry> = Vec::new();
194    let open_path = if path.to_bytes().is_empty() {
195        unsafe { &CStr::from_bytes_with_nul_unchecked(b".\0") }
196    } else {
197        path
198    };
199    let dir = unsafe { opendir(open_path.as_ptr()) };
200
201    if dir.is_null() {
202        let new_errno = platform::ERRNO.get();
203        platform::ERRNO.set(old_errno);
204
205        if unsafe { errfunc(path.as_ptr(), new_errno) } != 0 || abort_on_error {
206            return Err(GLOB_ABORTED);
207        }
208
209        return Ok(results);
210    }
211
212    platform::ERRNO.set(0);
213
214    loop {
215        let entry = unsafe { readdir(&mut *dir) };
216        if entry.is_null() {
217            break;
218        }
219
220        let name = unsafe { CStr::from_ptr((*entry).d_name.as_ptr()).to_owned_cstring() };
221
222        if name.as_bytes() == b"." || name.as_bytes() == b".." {
223            continue;
224        }
225
226        let is_dir: bool = unsafe {
227            if (*entry).d_type == DT_DIR {
228                true
229            } else if (*entry).d_type == DT_LNK {
230                // Resolve symbolic link
231                let mut full_path = path.to_bytes().to_vec();
232                if !full_path.ends_with(b"/") {
233                    full_path.push(b'/');
234                }
235                full_path.extend_from_slice(name.as_bytes());
236                let full_path = CString::new(full_path).unwrap();
237
238                let mut link_info = stat::default();
239                if stat(full_path.as_ptr(), ptr::from_mut(&mut link_info)) != 0 {
240                    let errno = platform::ERRNO.get();
241                    platform::ERRNO.set(old_errno);
242                    if errfunc(full_path.as_ptr(), errno) != 0 || abort_on_error {
243                        return Err(GLOB_ABORTED);
244                    }
245                }
246                link_info.st_mode & S_IFMT == S_IFDIR
247            } else {
248                false
249            }
250        };
251
252        results.push(DirEntry { name, is_dir });
253    }
254
255    // Check if entry == NULL because of an error
256    let errno = platform::ERRNO.get();
257
258    unsafe { closedir(Box::from_raw(dir)) };
259
260    // Restore the old errno
261    platform::ERRNO.set(old_errno);
262
263    if errno != 0 && (unsafe { errfunc(path.as_ptr(), errno) } != 0 || abort_on_error) {
264        return Err(GLOB_ABORTED);
265    }
266
267    Ok(results)
268}
269
270fn inner_glob(
271    current_dir: &CStr,
272    glob_expr: &CStr,
273    flags: c_int,
274    errfunc: GlobErrorFunc,
275) -> Result<Vec<CString>, c_int> {
276    let mut pattern: Vec<u8> = Vec::new();
277
278    // Remove any '/' chars at the start of the expression
279    let glob_expr = {
280        let mut expr = glob_expr.to_bytes_with_nul();
281        while expr.first() == Some(&b'/') {
282            expr = &expr[1..];
283        }
284        unsafe { CStr::from_bytes_with_nul_unchecked(expr) }
285    };
286
287    // Get the next section of the glob expression (up to non-escaped '/')
288    let glob_iter = glob_expr.to_bytes();
289    let mut in_bracket = false;
290    let mut escaped = false;
291    let mut glob_consumed = 0;
292
293    for ch in glob_iter {
294        // Don't consume nul
295        if ch == &b'\0' {
296            break;
297        }
298
299        glob_consumed += 1;
300
301        if ch == &b'/' && !escaped {
302            break;
303        }
304
305        if ch == &b'[' && !escaped {
306            in_bracket = true;
307        } else if ch == &b']' {
308            // '\' is a normal character in brackets so doesn't escape
309            in_bracket = false;
310        }
311
312        escaped =
313            ch == &b'\\' && !in_bracket && !escaped && (flags & GLOB_NOESCAPE != GLOB_NOESCAPE);
314
315        pattern.push(*ch);
316    }
317
318    // Needs to be C-string
319    pattern.push(b'\0');
320
321    let new_glob_expr = unsafe {
322        CStr::from_bytes_with_nul_unchecked(&glob_expr.to_bytes_with_nul()[glob_consumed..])
323    };
324
325    // Handle special path sections
326    if pattern == b".\0" || pattern == b"..\0" {
327        let mut new_dir: Vec<u8> = Vec::new();
328        new_dir.extend_from_slice(current_dir.to_bytes());
329        new_dir.extend_from_slice(&pattern);
330        let new_dir_c = unsafe { CStr::from_bytes_with_nul_unchecked(&new_dir) };
331        return inner_glob(&new_dir_c, &new_glob_expr, flags, errfunc);
332    }
333
334    let mut fnmatch_flags = 0;
335    if flags & GLOB_NOESCAPE == GLOB_NOESCAPE {
336        fnmatch_flags |= FNM_NOESCAPE;
337    }
338    if flags & GLOB_PERIOD == GLOB_PERIOD {
339        fnmatch_flags |= FNM_PERIOD;
340    }
341
342    let mut matches: Vec<CString> = Vec::new();
343
344    for entry in list_dir(current_dir, errfunc, flags & GLOB_ERR == GLOB_ERR)? {
345        // If we still have pattern to match ignore non-directories
346        if !new_glob_expr.to_bytes().is_empty() && !entry.is_dir {
347            continue;
348        }
349
350        let mut path = current_dir.to_bytes().to_vec();
351
352        if path != b"" && !path.ends_with(b"/") {
353            path.push(b'/');
354        }
355        path.extend_from_slice(entry.name.as_bytes());
356
357        if flags & GLOB_MARK == GLOB_MARK && new_glob_expr.to_bytes() == b"" && entry.is_dir {
358            path.push(b'/');
359        }
360
361        // This shouldn't ever panic, we know the vec has no nul bytes
362        let path = CString::new(path).unwrap();
363
364        if unsafe {
365            fnmatch(
366                pattern.as_ptr().cast::<c_char>(),
367                entry.name.as_ptr(),
368                fnmatch_flags,
369            )
370        } == 0
371        {
372            if entry.is_dir && new_glob_expr.to_bytes() != b"" {
373                let new_matches = inner_glob(&CStr::borrow(&path), &new_glob_expr, flags, errfunc)?;
374                matches.extend(new_matches);
375            } else {
376                matches.push(path);
377            }
378        }
379    }
380
381    // It is an error if we don't find a directory when we expect one
382    if matches.is_empty() && !new_glob_expr.to_bytes().is_empty() {
383        let mut path = current_dir.to_bytes().to_vec();
384        path.extend_from_slice(&pattern);
385        if unsafe { errfunc(path.as_ptr().cast::<c_char>(), ENOENT) } != 0
386            || flags & GLOB_ERR == GLOB_ERR
387        {
388            return Err(GLOB_ABORTED);
389        }
390    }
391
392    Ok(matches)
393}