From 259d64e3725a9837912549be19779abef075658a Mon Sep 17 00:00:00 2001 From: vasilito Date: Mon, 3 Aug 2026 19:06:40 +0300 Subject: [PATCH] fnmatch: implement the GNU FNM_FILE_NAME and FNM_LEADING_DIR extensions GCC 16's libiberty/fnmatch.c uses both and failed to build against relibc: fnmatch.c:84: error: 'FNM_FILE_NAME' undeclared fnmatch.c:213: error: 'FNM_LEADING_DIR' undeclared FNM_FILE_NAME is the GNU synonym for FNM_PATHNAME, so it is an alias. FNM_LEADING_DIR is real behaviour, not just a constant: the pattern may match a leading directory of the input rather than all of it, so 'foo' matches 'foo/bar'. Implemented by trying the exact match first and then each prefix ending immediately before a '/'. Declaring the flag without honouring it would be a stub, which local/AGENTS.md forbids and which would silently mismatch for any caller that passes it. Value 16 is the next free bit in relibc's own numbering (NOESCAPE 1, PATHNAME 2, PERIOD 4, CASEFOLD 8); relibc does not use glibc's bit assignments, so the value is chosen for this header, not copied. cargo check passes for x86_64-unknown-redox. --- src/header/fnmatch/mod.rs | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/header/fnmatch/mod.rs b/src/header/fnmatch/mod.rs index 4d267d00a7..429b1d15a9 100644 --- a/src/header/fnmatch/mod.rs +++ b/src/header/fnmatch/mod.rs @@ -28,6 +28,11 @@ pub const FNM_PERIOD: c_int = 4; pub const FNM_CASEFOLD: c_int = 8; /// Equivalent to `FNM_CASEFOLD`. pub const FNM_IGNORECASE: c_int = FNM_CASEFOLD; +/// Equivalent to `FNM_PATHNAME`. GNU extension. +pub const FNM_FILE_NAME: c_int = FNM_PATHNAME; +/// The pattern may match a leading directory of the input string, i.e. +/// `foo` matches `foo/bar`. GNU extension. +pub const FNM_LEADING_DIR: c_int = 16; // TODO: FNM_EXTMATCH (Non-POSIX) unsafe fn tokenize(mut pattern: *const u8, flags: c_int) -> Tree { @@ -160,10 +165,21 @@ pub unsafe extern "C" fn fnmatch( let tokens = unsafe { tokenize(pattern.cast::(), flags) }; - if PosixRegex::new(Cow::Owned(tokens)) - .case_insensitive(flags & FNM_CASEFOLD == FNM_CASEFOLD) - .matches_exact(input) - .is_some() + let case_insensitive = flags & FNM_CASEFOLD == FNM_CASEFOLD; + let regex = PosixRegex::new(Cow::Owned(tokens)).case_insensitive(case_insensitive); + + // FNM_LEADING_DIR: the pattern may match a leading directory of the input + // rather than the whole of it, so `foo` matches `foo/bar`. Try the exact + // match first, then each prefix that ends immediately before a '/'. + let matched = regex.clone().matches_exact(input).is_some() + || (flags & FNM_LEADING_DIR == FNM_LEADING_DIR + && input + .iter() + .enumerate() + .filter(|&(_, b)| *b == b'/') + .any(|(i, _)| regex.clone().matches_exact(&input[..i]).is_some())); + + if matched { 0 } else {