diff --git a/src/header/glob/cbindgen.toml b/src/header/glob/cbindgen.toml new file mode 100644 index 0000000000..61c5b4e48f --- /dev/null +++ b/src/header/glob/cbindgen.toml @@ -0,0 +1,9 @@ +sys_includes = ["stddef.h"] +include_guard = "_RELIBC_GLOB_H" +language = "C" +style = "type" +no_includes = true +cpp_compat = true + +[enum] +prefix_with_name = true diff --git a/src/header/glob/mod.rs b/src/header/glob/mod.rs new file mode 100644 index 0000000000..974471ebe1 --- /dev/null +++ b/src/header/glob/mod.rs @@ -0,0 +1,383 @@ +//! glob implementation, following https://pubs.opengroup.org/onlinepubs/9699919799/functions/glob.html + +use core::ptr; + +use alloc::{boxed::Box, vec::Vec}; + +use crate::{ + c_str::{CStr, CString}, + header::{ + dirent::{closedir, opendir, readdir, DIR}, + errno::*, + fnmatch::{fnmatch, FNM_NOESCAPE, FNM_PERIOD}, + sys_stat::{stat, S_IFDIR, S_IFLNK, S_IFMT}, + }, + platform::{self, types::*}, +}; + +// Cause glob() to return on error +pub const GLOB_ERR: c_int = 0x0001; +// Each pathname that is a directory that matches pattern has a slash appended +pub const GLOB_MARK: c_int = 0x0002; +// Do not sort returned pathnames +pub const GLOB_NOSORT: c_int = 0x0004; +// Add gl_offs amount of null pointers to the beginning of `gl_pathv` +pub const GLOB_DOOFFS: c_int = 0x0008; +// If pattern does not match, return a list containing only pattern +pub const GLOB_NOCHECK: c_int = 0x0010; +// Append generated pathnames to those previously obtained +pub const GLOB_APPEND: c_int = 0x0020; +// Disable backslash escaping +pub const GLOB_NOESCAPE: c_int = 0x0040; +// Allow wildcards to match '.' (GNU extension) +pub const GLOB_PERIOD: c_int = 0x0080; + +// Attempt to allocate memory failed +pub const GLOB_NOSPACE: c_int = 1; +// Scan was stopped because GLOB_ERR was set or `errfunc` returned non-zero +pub const GLOB_ABORTED: c_int = 2; +// Pattern does not match any existing pathname, and GLOB_NOCHECK was not set +pub const GLOB_NOMATCH: c_int = 3; + +#[repr(C)] +#[derive(Debug)] +pub struct glob_t { + pub gl_pathc: size_t, // Count of paths matched by pattern (POSIX required field) + pub gl_offs: size_t, // Slots to reserve at the beginning of gl_pathv (POSIX required field) + pub gl_pathv: *mut *mut c_char, // Pointer to list of matched pathnames (POSIX required field) + + // Opaque pointer to allocation data + __opaque: *mut c_void, // Vec<*mut c_char> +} + +#[no_mangle] +pub unsafe extern "C" fn glob( + pattern: *const c_char, + flags: c_int, + errfunc: Option c_int>, + pglob: *mut glob_t, +) -> c_int { + if flags & GLOB_APPEND != GLOB_APPEND { + (*pglob).gl_pathc = 0; + (*pglob).gl_pathv = ptr::null_mut(); + (*pglob).__opaque = ptr::null_mut(); + } + + let glob_expr = CStr::from_ptr(pattern); + + if glob_expr.to_bytes() == b"" { + return GLOB_NOMATCH; + } + + let base_path = + CStr::from_bytes_with_nul_unchecked(if glob_expr.to_bytes().get(0) == Some(&b'/') { + b"/\0" + } else { + b"\0" + }); + + let errfunc = match errfunc { + Some(f) => f, + None => default_errfunc, + }; + + // Do the globbing + let mut results = match inner_glob(&base_path, &glob_expr, flags, errfunc) { + Ok(res) => res, + Err(e) => return e, + }; + + // Handle GLOB_NOCHECK and no matches + if results.is_empty() { + if flags & GLOB_NOCHECK == GLOB_NOCHECK { + results.push(glob_expr.to_owned_cstring()); + } else { + return GLOB_NOMATCH; + } + } + + // Handle GLOB_NOSORT + if flags & GLOB_NOSORT != GLOB_NOSORT { + results.sort(); + } + + // Set gl_pathc + if flags & GLOB_APPEND == GLOB_APPEND { + (*pglob).gl_pathc += results.len(); + } else { + (*pglob).gl_pathc = results.len(); + } + + let mut pathv: Box>; + if flags & GLOB_APPEND == GLOB_APPEND { + pathv = Box::from_raw((*pglob).__opaque as *mut _); + pathv.pop(); // Remove NULL from end + } else { + pathv = Box::new(Vec::new()); + if flags & GLOB_DOOFFS == GLOB_DOOFFS { + for _ in 0..(*pglob).gl_offs { + pathv.push(ptr::null_mut()); + } + } + } + + pathv.extend(results.into_iter().map(|s| s.into_raw())); + + pathv.push(ptr::null_mut()); + + pathv.shrink_to_fit(); + + (*pglob).gl_pathv = pathv.as_ptr() as *mut *mut c_char; + (*pglob).__opaque = Box::into_raw(pathv) as *mut _; + + 0 +} + +#[no_mangle] +pub unsafe extern "C" fn globfree(pglob: *mut glob_t) { + // Retake ownership + if !(*pglob).__opaque.is_null() { + let pathv: Box> = Box::from_raw((*pglob).__opaque as *mut _); + for (idx, path) in pathv.into_iter().enumerate() { + if idx < (*pglob).gl_offs { + continue; + } + if !path.is_null() { + CString::from_raw(path); + } + } + (*pglob).gl_pathv = ptr::null_mut(); + } +} + +type GlobErrorFunc = unsafe extern "C" fn(epath: *const c_char, eerrno: c_int) -> c_int; + +struct DirEntry { + name: CString, + is_dir: bool, +} + +unsafe extern "C" fn default_errfunc(epath: *const c_char, eerrno: c_int) -> c_int { + 0 +} + +fn list_dir( + path: &CStr, + errfunc: GlobErrorFunc, + abort_on_error: bool, +) -> Result, c_int> { + const DT_DIR: c_uchar = 4; // From dirent.h + const DT_LNK: c_uchar = 10; // From dirent.h + + let old_errno = platform::ERRNO.get(); + let mut results: Vec = Vec::new(); + let open_path = if path.to_bytes().len() == 0 { + unsafe { &CStr::from_bytes_with_nul_unchecked(b".\0") } + } else { + path + }; + let mut dir = unsafe { opendir(open_path.as_ptr()) }; + + if dir.is_null() { + let new_errno = platform::ERRNO.get(); + platform::ERRNO.set(old_errno); + + if unsafe { errfunc(path.as_ptr(), new_errno) } != 0 || abort_on_error { + return Err(GLOB_ABORTED); + } + + return Ok(results); + } + + platform::ERRNO.set(0); + + loop { + let entry = unsafe { readdir(&mut *dir) }; + if entry.is_null() { + break; + } + + let name = unsafe { CStr::from_ptr((*entry).d_name.as_ptr()).to_owned_cstring() }; + + if name.as_bytes() == b"." || name.as_bytes() == b".." { + continue; + } + + let is_dir: bool = unsafe { + if (*entry).d_type == DT_DIR { + true + } else if (*entry).d_type == DT_LNK { + // Resolve symbolic link + let mut full_path = path.to_owned_cstring().into_string().unwrap(); + if !full_path.ends_with('/') { + full_path.push('/'); + } + full_path.push_str(name.to_str().unwrap()); + full_path.push('\0'); + + let mut link_info = stat::default(); + if stat( + full_path.as_ptr() as *const c_char, + &mut link_info as *mut _, + ) != 0 + { + let errno = platform::ERRNO.get(); + platform::ERRNO.set(old_errno); + if errfunc(full_path.as_ptr() as *const c_char, errno) != 0 || abort_on_error { + return Err(GLOB_ABORTED); + } + } + link_info.st_mode & S_IFMT == S_IFDIR + } else { + false + } + }; + + results.push(DirEntry { name, is_dir }); + } + + // Check if entry == NULL because of an error + let errno = platform::ERRNO.get(); + + unsafe { closedir(Box::from_raw(dir)) }; + + // Restore the old errno + platform::ERRNO.set(old_errno); + + if errno != 0 { + if unsafe { errfunc(path.as_ptr(), errno) } != 0 || abort_on_error { + return Err(GLOB_ABORTED); + } + } + + Ok(results) +} + +fn inner_glob( + current_dir: &CStr, + glob_expr: &CStr, + flags: c_int, + errfunc: GlobErrorFunc, +) -> Result, c_int> { + let mut pattern: Vec = Vec::new(); + + // Remove any '/' chars at the start of the expression + let glob_expr = { + let mut expr = glob_expr.to_bytes_with_nul(); + while expr.get(0) == Some(&b'/') { + expr = &expr[1..]; + } + unsafe { CStr::from_bytes_with_nul_unchecked(expr) } + }; + + // Get the next section of the glob expression (up to non-escaped '/') + let mut glob_iter = glob_expr.to_bytes(); + let mut in_bracket = false; + let mut escaped = false; + let mut glob_consumed = 0; + + for ch in glob_iter { + // Don't consume nul + if ch == &b'\0' { + break; + } + + glob_consumed += 1; + + if ch == &b'/' && !escaped { + break; + } + + if ch == &b'[' && !escaped { + in_bracket = true; + } else if ch == &b']' { + // '\' is a normal character in brackets so doesn't escape + in_bracket = false; + } + + escaped = + ch == &b'\\' && !in_bracket && !escaped && (flags & GLOB_NOESCAPE != GLOB_NOESCAPE); + + pattern.push(*ch); + } + + // Needs to be C-string + pattern.push(b'\0'); + + let new_glob_expr = unsafe { + CStr::from_bytes_with_nul_unchecked(&glob_expr.to_bytes_with_nul()[glob_consumed..]) + }; + + // Handle special path sections + if pattern == b".\0" || pattern == b"..\0" { + let mut new_dir: Vec = Vec::new(); + new_dir.extend_from_slice(current_dir.to_bytes()); + new_dir.extend_from_slice(&pattern); + return inner_glob( + unsafe { &CStr::from_bytes_with_nul_unchecked(&new_dir) }, + &new_glob_expr, + flags, + errfunc, + ); + } + + let mut fnmatch_flags = 0; + if flags & GLOB_NOESCAPE == GLOB_NOESCAPE { + fnmatch_flags |= FNM_NOESCAPE; + } + if flags & GLOB_PERIOD == GLOB_PERIOD { + fnmatch_flags |= FNM_PERIOD; + } + + let mut matches: Vec = Vec::new(); + + for entry in list_dir(current_dir, errfunc, flags & GLOB_ERR == GLOB_ERR)? { + // If we still have pattern to match ignore non-directories + if new_glob_expr.to_bytes().len() != 0 && !entry.is_dir { + continue; + } + + let mut path = current_dir.to_bytes().to_vec(); + + if path != b"" && !path.ends_with(b"/") { + path.push(b'/'); + } + path.extend_from_slice(entry.name.as_bytes()); + + if flags & GLOB_MARK == GLOB_MARK && new_glob_expr.to_bytes() == b"" && entry.is_dir { + path.push(b'/'); + } + + // This shouldn't ever panic, we know the vec has no nul bytes + let path = CString::new(path).unwrap(); + + if unsafe { + fnmatch( + pattern.as_ptr() as *const c_char, + entry.name.as_ptr(), + fnmatch_flags, + ) + } == 0 + { + if entry.is_dir && new_glob_expr.to_bytes() != b"" { + let new_matches = inner_glob(&CStr::borrow(&path), &new_glob_expr, flags, errfunc)?; + matches.extend(new_matches); + } else { + matches.push(path); + } + } + } + + // It is an error if we don't find a directory when we expect one + if matches.len() == 0 && new_glob_expr.to_bytes().len() != 0 { + let mut path = current_dir.to_bytes().to_vec(); + path.extend_from_slice(&pattern); + if unsafe { errfunc(path.as_ptr() as *const c_char, ENOENT) } != 0 + || flags & GLOB_ERR == GLOB_ERR + { + return Err(GLOB_ABORTED); + } + } + + return Ok(matches); +} diff --git a/src/header/mod.rs b/src/header/mod.rs index b5ca0fdafc..a5f9db6498 100644 --- a/src/header/mod.rs +++ b/src/header/mod.rs @@ -25,7 +25,7 @@ pub mod float; pub mod fnmatch; // TODO: ftw.h pub mod getopt; -// TODO: glob.h +pub mod glob; pub mod grp; // TODO: iconv.h pub mod inttypes; diff --git a/tests/Makefile b/tests/Makefile index 759d95debf..eade24365b 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -22,6 +22,7 @@ EXPECT_NAMES=\ features \ fnmatch \ futimens \ + glob \ iso646 \ libgen \ locale \ diff --git a/tests/expected/glob.stderr b/tests/expected/glob.stderr new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/expected/glob.stdout b/tests/expected/glob.stdout new file mode 100644 index 0000000000..ef1351361b --- /dev/null +++ b/tests/expected/glob.stdout @@ -0,0 +1,70 @@ +Pattern = eample_dir/* +(NOMATCH) + +Pattern = eample_dir/* +glob_err("eample_dir", "No such file or directory") -> 0 +(ABORTED) + +Pattern = ./example_dir/* +(Matched 8) +0 - ./example_dir/1-never-gonna-give-you-up +1 - ./example_dir/2-never-gonna-let-you-down +2 - ./example_dir/3-never-gonna-run-around +3 - ./example_dir/4-and-desert-you +4 - ./example_dir/5-never-gonna-make-you-cry +5 - ./example_dir/6-never-gonna-say-goodbye +6 - ./example_dir/7-never-gonna-tell-a-lie +7 - ./example_dir/8-and-hurt-you + +Pattern = example_dir/*never* +(Matched 6) +0 - example_dir/1-never-gonna-give-you-up +1 - example_dir/2-never-gonna-let-you-down +2 - example_dir/3-never-gonna-run-around +3 - example_dir/5-never-gonna-make-you-cry +4 - example_dir/6-never-gonna-say-goodbye +5 - example_dir/7-never-gonna-tell-a-lie + +Pattern = example_dir/?-and* +(Matched 8) +0 - example_dir/1-never-gonna-give-you-up +1 - example_dir/2-never-gonna-let-you-down +2 - example_dir/3-never-gonna-run-around +3 - example_dir/5-never-gonna-make-you-cry +4 - example_dir/6-never-gonna-say-goodbye +5 - example_dir/7-never-gonna-tell-a-lie +6 - example_dir/4-and-desert-you +7 - example_dir/8-and-hurt-you + +Pattern = example_dir/*never* +(Matched 6) (with 4 gl_offs) +0 - NULL +1 - NULL +2 - NULL +3 - NULL +4 - example_dir/1-never-gonna-give-you-up +5 - example_dir/2-never-gonna-let-you-down +6 - example_dir/3-never-gonna-run-around +7 - example_dir/5-never-gonna-make-you-cry +8 - example_dir/6-never-gonna-say-goodbye +9 - example_dir/7-never-gonna-tell-a-lie + +Pattern = example_dir/?-and* +(Matched 8) (with 4 gl_offs) +0 - +1 - NULL +2 - NULL +3 - NULL +4 - example_dir/1-never-gonna-give-you-up +5 - example_dir/2-never-gonna-let-you-down +6 - example_dir/3-never-gonna-run-around +7 - example_dir/5-never-gonna-make-you-cry +8 - example_dir/6-never-gonna-say-goodbye +9 - example_dir/7-never-gonna-tell-a-lie +10 - example_dir/4-and-desert-you +11 - example_dir/8-and-hurt-you + +Pattern = example_dir +(Matched 1) +0 - example_dir/ + diff --git a/tests/glob.c b/tests/glob.c new file mode 100644 index 0000000000..b7e545dd6e --- /dev/null +++ b/tests/glob.c @@ -0,0 +1,85 @@ +#include +#include +#include +#include + + +typedef int (*err_func_t)(const char *epath, int eerrno); + + +static char *gl_offs_test_string = ""; + + +int glob_err(const char *epath, int eerrno) { + printf("glob_err(\"%s\", \"%s\") -> %d\n", epath, strerror(eerrno), 0); + return 0; +} + +void glob_test(const char *pattern, int flags, err_func_t errfunc, glob_t *pglob) { + printf("Pattern = %s\n", pattern); + + int retval = glob(pattern, flags, errfunc, pglob); + + if (retval != 0) { + if (retval == GLOB_ABORTED) { + puts("(ABORTED)\n"); + } + else if (retval == GLOB_NOMATCH) { + puts("(NOMATCH)\n"); + } + else if (retval == GLOB_NOSPACE) { + puts("(NOSPACE)\n"); + } + else { + printf(" (Unknown retval %d!)\n", retval); + } + return; + } + + size_t gl_offs = flags & GLOB_DOOFFS ? pglob->gl_offs : 0; + + printf("(Matched %lu)", pglob->gl_pathc); + + if (flags & GLOB_DOOFFS) { + printf(" (with %lu gl_offs)\n", gl_offs); + } + else { + puts(""); + } + + for(unsigned int i = 0; i < (gl_offs + pglob->gl_pathc); i++) { + if (!pglob->gl_pathv[i]) { + printf("%d - NULL\n", i); + } + else { + printf("%d - %s\n", i, pglob->gl_pathv[i]); + } + } + printf("%s", "\n"); +} + +int main(void) { + glob_t pglob = {0}; + + glob_test("eample_dir/*", 0, NULL, &pglob); + globfree(&pglob); + + glob_test("eample_dir/*", GLOB_ERR, glob_err, &pglob); + globfree(&pglob); + + glob_test("./example_dir/*", 0, glob_err, &pglob); + globfree(&pglob); + + glob_test("example_dir/*never*", 0, glob_err, &pglob); + glob_test("example_dir/?-and*", GLOB_APPEND, glob_err, &pglob); + globfree(&pglob); + + pglob.gl_offs = 4; + glob_test("example_dir/*never*", GLOB_DOOFFS, glob_err, &pglob); + pglob.gl_pathv[0] = gl_offs_test_string; + glob_test("example_dir/?-and*", GLOB_DOOFFS | GLOB_APPEND, glob_err, &pglob); + globfree(&pglob); + + glob_test("example_dir", GLOB_MARK, glob_err, &pglob); + globfree(&pglob); +}