Skip to main content

c/
path_extra.rs

1// Copyright (c) 2026 Meng Zhuo
2// SPDX-License-Identifier: BSD-3-Clause
3
4// realpath, glob/globfree and nftw/ftw.
5
6const PATH_MAX_V: usize = 4096;
7
8// ============================================================
9// realpath
10// ============================================================
11
12static mut REALPATH_BUF: [c_char; PATH_MAX_V] = [0; PATH_MAX_V];
13
14unsafe fn rp_is_dir(path: *const c_char) -> bool {
15    let mut st: Stat = core::mem::zeroed();
16    if crate::lstat(path, &mut st as *mut Stat as *mut c_void) != 0 { return false; }
17    st.st_mode & 0o170000 == 0o040000
18}
19
20#[no_mangle]
21pub unsafe extern "C" fn realpath(
22    filename: *const c_char,
23    resolved: *mut c_char,
24) -> *mut c_char {
25    if filename.is_null() { ERRNO!() = EINVAL; return core::ptr::null_mut(); }
26    let flen = crate::strlen(filename);
27    if flen == 0 { ERRNO!() = 2; return core::ptr::null_mut(); }
28    if flen >= PATH_MAX_V { ERRNO!() = 36; return core::ptr::null_mut(); }
29
30    let mut out = [0u8; PATH_MAX_V + 1];
31    let mut olen;
32    let mut pend = [0u8; 2 * PATH_MAX_V + 1];
33    core::ptr::copy_nonoverlapping(filename as *const u8, pend.as_mut_ptr(), flen);
34    pend[flen] = 0;
35    let mut pi = 0usize;
36    let absolute = pend[0] == b'/';
37    if absolute {
38        pi = 1;
39        out[0] = b'/';
40        olen = 1;
41    } else {
42        let r = crate::getcwd(out.as_mut_ptr() as *mut c_char, PATH_MAX_V);
43        if r.is_null() { return core::ptr::null_mut(); }
44        olen = crate::strlen(out.as_ptr() as *const c_char);
45        if olen == 0 { out[0] = b'/'; olen = 1; }
46    }
47    let mut symlinks = 0;
48
49    loop {
50        while pend[pi] == b'/' { pi += 1; }
51        if pend[pi] == 0 { break; }
52        let start = pi;
53        while pend[pi] != 0 && pend[pi] != b'/' { pi += 1; }
54        let comp_len = pi - start;
55        let mut q = pi;
56        while pend[q] == b'/' { q += 1; }
57        let is_last = pend[q] == 0;
58        let followed_by_slash = pend[pi] == b'/';
59        let needs_dir = !is_last || followed_by_slash;
60
61        if comp_len == 1 && pend[start] == b'.' {
62            if needs_dir && is_last { /* "." is a dir */ }
63            continue;
64        }
65        if comp_len == 2 && pend[start] == b'.' && pend[start + 1] == b'.' {
66            while olen > 0 && out[olen - 1] != b'/' { olen -= 1; }
67            if olen > 0 { olen -= 1; }
68            continue;
69        }
70
71        if olen == 0 {
72            out[0] = b'/';
73            olen = 1;
74        }
75        if olen > 0 && out[olen - 1] != b'/' {
76            if olen + 1 >= PATH_MAX_V { ERRNO!() = 36; return core::ptr::null_mut(); }
77            out[olen] = b'/';
78            olen += 1;
79        }
80        if olen + comp_len >= PATH_MAX_V { ERRNO!() = 36; return core::ptr::null_mut(); }
81        core::ptr::copy_nonoverlapping(pend.as_ptr().add(start), out.as_mut_ptr().add(olen), comp_len);
82        olen += comp_len;
83        out[olen] = 0;
84
85        let mut st: Stat = core::mem::zeroed();
86        if crate::lstat(out.as_ptr() as *const c_char, &mut st as *mut Stat as *mut c_void) != 0 {
87            if ERRNO!() == 2 { ERRNO!() = 2; }
88            return core::ptr::null_mut();
89        }
90        let mode = st.st_mode;
91        if mode & 0o170000 == 0o120000 {
92            symlinks += 1;
93            if symlinks > 40 { ERRNO!() = 40; return core::ptr::null_mut(); }
94            let mut link = [0u8; PATH_MAX_V + 1];
95            let n = crate::readlink(out.as_ptr() as *const c_char, link.as_mut_ptr() as *mut c_char, PATH_MAX_V);
96            if n < 0 { return core::ptr::null_mut(); }
97            let n = n as usize;
98            if n == 0 { ERRNO!() = 2; return core::ptr::null_mut(); }
99            link[n] = 0;
100            // Remove the component we just appended.
101            while olen > 0 && out[olen - 1] != b'/' { olen -= 1; }
102            if olen > 0 { olen -= 1; }
103            let mut next = [0u8; 2 * PATH_MAX_V + 1];
104            let link_abs = link[0] == b'/';
105            let mut nl = n;
106            core::ptr::copy_nonoverlapping(link.as_ptr(), next.as_mut_ptr(), n);
107            let rest = &pend[pi..];
108            let rest_len = crate::strlen(rest.as_ptr() as *const c_char);
109            if rest_len > 0 {
110                if nl > 0 && next[nl - 1] != b'/' {
111                    next[nl] = b'/';
112                    nl += 1;
113                }
114                if nl + rest_len >= next.len() { ERRNO!() = 36; return core::ptr::null_mut(); }
115                core::ptr::copy_nonoverlapping(rest.as_ptr(), next.as_mut_ptr().add(nl), rest_len + 1);
116                nl += rest_len;
117            }
118            if link_abs { olen = 0; }
119            pend = next;
120            pi = 0;
121            let _ = nl;
122            continue;
123        }
124        if mode & 0o170000 != 0o040000 && needs_dir {
125            ERRNO!() = 20; // ENOTDIR
126            return core::ptr::null_mut();
127        }
128    }
129
130    if olen == 0 {
131        out[0] = b'/';
132        olen = 1;
133    }
134    // Collapse a trailing slash except for the root.
135    while olen > 1 && out[olen - 1] == b'/' { olen -= 1; }
136    out[olen] = 0;
137    let dst = if resolved.is_null() { REALPATH_BUF.as_mut_ptr() } else { resolved };
138    core::ptr::copy_nonoverlapping(out.as_ptr(), dst as *mut u8, olen + 1);
139    dst
140}
141
142// ============================================================
143// glob
144// ============================================================
145
146#[repr(C)]
147pub struct GlobT {
148    pub gl_pathc: usize,
149    pub gl_pathv: *mut *mut c_char,
150    pub gl_offs: usize,
151    __dummy1: c_int,
152    __dummy2: [*mut c_void; 5],
153}
154
155const GLOB_ERR_V: c_int = 0x01;
156const GLOB_MARK_V: c_int = 0x02;
157const GLOB_NOSORT_V: c_int = 0x04;
158const GLOB_DOOFFS_V: c_int = 0x08;
159const GLOB_NOCHECK_V: c_int = 0x10;
160const GLOB_APPEND_V: c_int = 0x20;
161const GLOB_NOESCAPE_V: c_int = 0x40;
162const GLOB_PERIOD_V: c_int = 0x80;
163const GLOB_TILDE_V: c_int = 0x1000;
164const GLOB_TILDE_CHECK_V: c_int = 0x4000;
165
166const GLOB_NOSPACE_V: c_int = 1;
167const GLOB_ABORTED_V: c_int = 2;
168const GLOB_NOMATCH_V: c_int = 3;
169
170struct GlobList {
171    items: *mut *mut c_char,
172    n: usize,
173    cap: usize,
174}
175
176unsafe fn glob_list_push(list: *mut GlobList, s: *const c_char) {
177    if (*list).n == (*list).cap {
178        let ncap = if (*list).cap == 0 { 32 } else { (*list).cap * 2 };
179        let na = crate::realloc(
180            (*list).items as *mut c_void,
181            ncap * core::mem::size_of::<*mut c_char>(),
182        ) as *mut *mut c_char;
183        if na.is_null() { return; }
184        (*list).items = na;
185        (*list).cap = ncap;
186    }
187    let l = crate::strlen(s);
188    let copy = crate::malloc(l + 1) as *mut c_char;
189    if copy.is_null() { return; }
190    core::ptr::copy_nonoverlapping(s as *const u8, copy as *mut u8, l + 1);
191    *(*list).items.add((*list).n) = copy;
192    (*list).n += 1;
193}
194
195unsafe fn glob_list_push_n(list: *mut GlobList, s: *const u8, len: usize) {
196    if (*list).n == (*list).cap {
197        let ncap = if (*list).cap == 0 { 32 } else { (*list).cap * 2 };
198        let na = crate::realloc(
199            (*list).items as *mut c_void,
200            ncap * core::mem::size_of::<*mut c_char>(),
201        ) as *mut *mut c_char;
202        if na.is_null() { return; }
203        (*list).items = na;
204        (*list).cap = ncap;
205    }
206    let copy = crate::malloc(len + 1) as *mut c_char;
207    if copy.is_null() { return; }
208    core::ptr::copy_nonoverlapping(s, copy as *mut u8, len);
209    *copy.add(len) = 0;
210    *(*list).items.add((*list).n) = copy;
211    (*list).n += 1;
212}
213
214unsafe fn list_dir_names(path: *const c_char) -> (*mut *mut c_char, usize) {
215    let fd = crate::open(path, crate::O_DIRECTORY, 0);
216    if fd < 0 { return (core::ptr::null_mut(), 0); }
217    let mut cap = 32usize;
218    let mut count = 0usize;
219    let mut arr = crate::malloc(cap * core::mem::size_of::<*mut c_char>()) as *mut *mut c_char;
220    if arr.is_null() { crate::close(fd); return (core::ptr::null_mut(), 0); }
221    #[repr(align(8))]
222    struct Buf([u8; 8192]);
223    let mut buf = Buf([0u8; 8192]);
224    let name_off = core::mem::offset_of!(linux_dirent64, d_name);
225    loop {
226        let n = <Arch as Syscalls>::syscall3(
227            SYS_GETDENTS64,
228            fd as i64,
229            buf.0.as_mut_ptr() as i64,
230            buf.0.len() as i64,
231        ) as isize;
232        if n <= 0 { break; }
233        let mut off = 0usize;
234        while off + name_off < n as usize {
235            let ent = buf.0.as_ptr().add(off) as *const linux_dirent64;
236            let reclen = (*ent).d_reclen as usize;
237            if reclen == 0 { break; }
238            let name = buf.0.as_ptr().add(off + name_off) as *const c_char;
239            let nl = crate::strlen(name);
240            if count + 2 >= cap {
241                cap *= 2;
242                let na = crate::realloc(arr as *mut c_void, cap * core::mem::size_of::<*mut c_char>()) as *mut *mut c_char;
243                if na.is_null() { break; }
244                arr = na;
245            }
246            let copy = crate::malloc(nl + 1) as *mut c_char;
247            if copy.is_null() { break; }
248            core::ptr::copy_nonoverlapping(name as *const u8, copy as *mut u8, nl + 1);
249            *arr.add(count) = copy;
250            count += 1;
251            off += reclen;
252        }
253    }
254    crate::close(fd);
255    (arr, count)
256}
257
258unsafe fn free_names(arr: *mut *mut c_char, n: usize) {
259    if arr.is_null() { return; }
260    for i in 0..n {
261        if !(*arr.add(i)).is_null() { crate::free(*arr.add(i) as *mut c_void); }
262    }
263    crate::free(arr as *mut c_void);
264}
265
266fn glob_has_magic(comp: &[u8], noescape: bool) -> bool {
267    let mut i = 0;
268    while i < comp.len() {
269        match comp[i] {
270            b'\\' if !noescape => i += 1,
271            b'*' | b'?' | b'[' => return true,
272            _ => {}
273        }
274        i += 1;
275    }
276    false
277}
278
279unsafe fn glob_unescape(dst: &mut [u8], comp: &[u8], noescape: bool) -> usize {
280    let mut o = 0usize;
281    let mut i = 0usize;
282    while i < comp.len() {
283        if comp[i] == b'\\' && !noescape && i + 1 < comp.len() {
284            i += 1;
285        }
286        if o < dst.len() { dst[o] = comp[i]; o += 1; }
287        i += 1;
288    }
289    o
290}
291
292#[derive(Copy, Clone)]
293struct Comp {
294    start: usize,
295    end: usize,
296}
297
298unsafe fn glob_split(pat: &[u8], noescape: bool, comps: &mut [Comp; 128]) -> (usize, bool) {
299    let mut n = 0usize;
300    let mut i = 0usize;
301    let absolute = !pat.is_empty() && pat[0] == b'/';
302    let mut trailing = false;
303    while i < pat.len() {
304        while i < pat.len() && pat[i] == b'/' { i += 1; }
305        if i >= pat.len() { if i > 0 { trailing = pat[i - 1] == b'/'; } break; }
306        let start = i;
307        while i < pat.len() {
308            if pat[i] == b'\\' && !noescape && i + 1 < pat.len() { i += 2; continue; }
309            if pat[i] == b'/' { break; }
310            i += 1;
311        }
312        if n < comps.len() {
313            comps[n] = Comp { start, end: i };
314            n += 1;
315        }
316    }
317    let _ = trailing;
318    (n, absolute)
319}
320
321unsafe fn glob_walk(
322    base: &mut [u8; PATH_MAX_V + 1],
323    blen: &mut usize,
324    pat: &[u8],
325    comps: &[Comp],
326    ci: usize,
327    trailing: bool,
328    flags: c_int,
329    errfunc: Option<unsafe extern "C" fn(*const c_char, c_int) -> c_int>,
330    list: *mut GlobList,
331) -> c_int {
332    if ci >= comps.len() {
333        glob_list_push_n(list, base.as_ptr(), *blen);
334        return 0;
335    }
336    let comp = &pat[comps[ci].start..comps[ci].end];
337    let last = ci + 1 == comps.len();
338    let noescape = flags & GLOB_NOESCAPE_V != 0;
339
340    if !glob_has_magic(comp, noescape) {
341        let mut name = [0u8; PATH_MAX_V + 1];
342        let nlen = glob_unescape(&mut name, comp, noescape);
343        let mut newlen = *blen;
344        if newlen > 0 && base[newlen - 1] != b'/' {
345            if newlen + 1 >= base.len() { return GLOB_NOSPACE_V; }
346            base[newlen] = b'/';
347            newlen += 1;
348        }
349        if newlen + nlen >= base.len() { return GLOB_NOSPACE_V; }
350        core::ptr::copy_nonoverlapping(name.as_ptr(), base.as_mut_ptr().add(newlen), nlen);
351        newlen += nlen;
352        base[newlen] = 0;
353        let mut st: Stat = core::mem::zeroed();
354        let exists = crate::lstat(base.as_ptr() as *const c_char, &mut st as *mut Stat as *mut c_void) == 0;
355        let is_dir = exists && st.st_mode & 0o170000 == 0o040000;
356        if last {
357            if exists && (!trailing || is_dir) {
358                if flags & GLOB_ONLYDIR_V != 0 && !is_dir {
359                    *blen = newlen;
360                    return 0;
361                }
362                let mut end = newlen;
363                if (flags & GLOB_MARK_V != 0) && is_dir {
364                    if end + 1 < base.len() {
365                        base[end] = b'/';
366                        end += 1;
367                    }
368                }
369                glob_list_push_n(list, base.as_ptr(), end);
370            }
371        } else {
372            if !is_dir {
373                if flags & GLOB_ERR_V != 0 {
374                    *blen = newlen;
375                    return GLOB_ABORTED_V;
376                }
377            } else {
378                let old = *blen;
379                *blen = newlen;
380                let r = glob_walk(base, blen, pat, comps, ci + 1, trailing, flags, errfunc, list);
381                *blen = old;
382                if r != 0 { return r; }
383            }
384        }
385        *blen = newlen;
386        return 0;
387    }
388
389    // Wildcard component: enumerate the base directory.
390    let mut dirc = [0 as c_char; PATH_MAX_V + 1];
391    let dp: *const c_char;
392    if *blen == 0 {
393        dirc[0] = b'.' as c_char;
394        dirc[1] = 0;
395        dp = dirc.as_ptr();
396    } else {
397        core::ptr::copy_nonoverlapping(base.as_ptr(), dirc.as_mut_ptr() as *mut u8, *blen);
398        dirc[*blen] = 0;
399        dp = dirc.as_ptr();
400    }
401    let (names, count) = list_dir_names(dp);
402    if names.is_null() {
403        if flags & GLOB_ERR_V != 0 {
404            if let Some(f) = errfunc { let _ = f(dp, ERRNO!()); }
405            return GLOB_ABORTED_V;
406        }
407        return 0;
408    }
409    let mut patc = [0 as c_char; PATH_MAX_V + 1];
410    core::ptr::copy_nonoverlapping(comp.as_ptr(), patc.as_mut_ptr() as *mut u8, comp.len());
411    patc[comp.len()] = 0;
412    let mut fnm_flags = 0;
413    if noescape { fnm_flags |= FNM_NOESCAPE; }
414    if flags & GLOB_PERIOD_V == 0 { fnm_flags |= FNM_PERIOD; }
415    let old = *blen;
416    for i in 0..count {
417        let name = *names.add(i);
418        if flags & GLOB_PERIOD_V == 0 {
419            // FNM_PERIOD already covers this, but skip "." / ".." here.
420            if *name == b'.' as c_char {
421                let second = *name.add(1);
422                if second == 0 || (second == b'.' as c_char && *name.add(2) == 0) { continue; }
423            }
424        }
425        if crate::fnmatch(patc.as_ptr(), name, fnm_flags) != 0 { continue; }
426        let nlen = crate::strlen(name);
427        let mut newlen = old;
428        if newlen > 0 && base[newlen - 1] != b'/' {
429            if newlen + 1 >= base.len() { free_names(names, count); return GLOB_NOSPACE_V; }
430            base[newlen] = b'/';
431            newlen += 1;
432        }
433        if newlen + nlen >= base.len() { free_names(names, count); return GLOB_NOSPACE_V; }
434        core::ptr::copy_nonoverlapping(name as *const u8, base.as_mut_ptr().add(newlen), nlen);
435        newlen += nlen;
436        base[newlen] = 0;
437        let mut st: Stat = core::mem::zeroed();
438        let exists = crate::lstat(base.as_ptr() as *const c_char, &mut st as *mut Stat as *mut c_void) == 0;
439        let is_dir = exists && st.st_mode & 0o170000 == 0o040000;
440        if last {
441            if flags & GLOB_ONLYDIR_V != 0 && !is_dir { continue; }
442            let mut end = newlen;
443            if (flags & GLOB_MARK_V != 0) && is_dir && end + 1 < base.len() {
444                base[end] = b'/';
445                end += 1;
446            }
447            glob_list_push_n(list, base.as_ptr(), end);
448        } else if is_dir {
449            *blen = newlen;
450            let r = glob_walk(base, blen, pat, comps, ci + 1, trailing, flags, errfunc, list);
451            *blen = old;
452            if r != 0 { free_names(names, count); return r; }
453        }
454    }
455    free_names(names, count);
456    *blen = old;
457    0
458}
459
460const GLOB_ONLYDIR_V: c_int = 0x200; // same value as musl/glibc
461
462unsafe fn glob_expand_tilde(pat: *const c_char, buf: *mut u8, cap: usize) -> Option<usize> {
463    let s = pat as *const u8;
464    if *s != b'~' { return None; }
465    let mut i = 1usize;
466    while *s.add(i) != 0 && *s.add(i) != b'/' { i += 1; }
467    let mut name = [0 as c_char; 256];
468    let nlen = i - 1;
469    if nlen >= name.len() { return None; }
470    core::ptr::copy_nonoverlapping(s.add(1), name.as_mut_ptr() as *mut u8, nlen);
471    name[nlen] = 0;
472    let pw = if nlen == 0 {
473        crate::getpwuid(crate::getuid())
474    } else {
475        crate::getpwnam(name.as_ptr())
476    };
477    if pw.is_null() { return None; }
478    let home = (*pw).pw_dir;
479    let hl = crate::strlen(home);
480    let rest = crate::strlen(s.add(i) as *const c_char);
481    if hl + rest + 1 > cap { return None; }
482    core::ptr::copy_nonoverlapping(home as *const u8, buf, hl);
483    core::ptr::copy_nonoverlapping(s.add(i), buf.add(hl), rest + 1);
484    Some(hl + rest)
485}
486
487#[no_mangle]
488pub unsafe extern "C" fn glob(
489    pattern: *const c_char,
490    flags: c_int,
491    errfunc: Option<unsafe extern "C" fn(*const c_char, c_int) -> c_int>,
492    pglob: *mut GlobT,
493) -> c_int {
494    if pattern.is_null() || pglob.is_null() { ERRNO!() = EINVAL; return GLOB_ABORTED_V; }
495
496    let mut expanded = [0u8; PATH_MAX_V + 1];
497    let mut pat_ptr = pattern as *const u8;
498    let mut pat_len = crate::strlen(pattern);
499    if flags & (GLOB_TILDE_V | GLOB_TILDE_CHECK_V) != 0 {
500        if let Some(n) = glob_expand_tilde(pattern, expanded.as_mut_ptr(), PATH_MAX_V) {
501            pat_ptr = expanded.as_ptr();
502            pat_len = n;
503        } else if flags & GLOB_TILDE_CHECK_V != 0 {
504            return GLOB_NOMATCH_V;
505        }
506    }
507    let pat = &core::slice::from_raw_parts(pat_ptr, pat_len)[..];
508
509    let mut comps = [Comp { start: 0, end: 0 }; 128];
510    let (ncomp, absolute) = glob_split(pat, flags & GLOB_NOESCAPE_V != 0, &mut comps);
511
512    let mut list = GlobList { items: core::ptr::null_mut(), n: 0, cap: 0 };
513    let mut base = [0u8; PATH_MAX_V + 1];
514    let mut blen = 0usize;
515    if absolute {
516        base[0] = b'/';
517        blen = 1;
518    }
519    let trailing = !pat.is_empty() && pat[pat.len() - 1] == b'/';
520    let r = if ncomp == 0 {
521        if absolute {
522            glob_list_push_n(&mut list, b"/".as_ptr(), 1);
523            0
524        } else {
525            GLOB_NOMATCH_V
526        }
527    } else {
528        glob_walk(&mut base, &mut blen, pat, &comps[..ncomp], 0, trailing, flags, errfunc, &mut list)
529    };
530    if r == GLOB_ABORTED_V {
531        for i in 0..list.n { crate::free(*list.items.add(i) as *mut c_void); }
532        crate::free(list.items as *mut c_void);
533        return r;
534    }
535    let mut matches = list.n;
536    if matches == 0 && flags & GLOB_NOCHECK_V != 0 {
537        glob_list_push(&mut list, pattern);
538        matches = list.n;
539    }
540    if matches == 0 && flags & GLOB_APPEND_V == 0 {
541        crate::free(list.items as *mut c_void);
542        return GLOB_NOMATCH_V;
543    }
544    if flags & GLOB_NOSORT_V == 0 && matches > 1 {
545        crate::qsort(
546            list.items as *mut c_void,
547            matches,
548            core::mem::size_of::<*mut c_char>(),
549            glob_cmp,
550        );
551    }
552
553    let old_n = if flags & GLOB_APPEND_V != 0 { (*pglob).gl_pathc } else { 0 };
554    let offs = if flags & GLOB_DOOFFS_V != 0 { (*pglob).gl_offs } else { 0 };
555    if old_n != 0 && !(*pglob).gl_pathv.is_null() && flags & GLOB_APPEND_V == 0 {
556        // Nothing to free; append is off.
557    }
558    let total = offs + old_n + matches + 1;
559    let pathv = crate::calloc(total, core::mem::size_of::<*mut c_char>()) as *mut *mut c_char;
560    if pathv.is_null() {
561        for i in 0..list.n { crate::free(*list.items.add(i) as *mut c_void); }
562        crate::free(list.items as *mut c_void);
563        return GLOB_NOSPACE_V;
564    }
565    for i in 0..offs { *pathv.add(i) = core::ptr::null_mut(); }
566    if flags & GLOB_APPEND_V != 0 && old_n != 0 && !(*pglob).gl_pathv.is_null() {
567        for i in 0..old_n {
568            *pathv.add(offs + i) = *(*pglob).gl_pathv.add(offs + i);
569        }
570        crate::free((*pglob).gl_pathv as *mut c_void);
571    }
572    for i in 0..list.n {
573        *pathv.add(offs + old_n + i) = *list.items.add(i);
574    }
575    *pathv.add(offs + old_n + matches) = core::ptr::null_mut();
576    crate::free(list.items as *mut c_void);
577
578    (*pglob).gl_pathc = old_n + matches;
579    (*pglob).gl_pathv = pathv;
580    (*pglob).gl_offs = offs;
581    (*pglob).__dummy1 = 0;
582    0
583}
584
585unsafe extern "C" fn glob_cmp(a: *const c_void, b: *const c_void) -> c_int {
586    let sa = *(a as *const *const c_char);
587    let sb = *(b as *const *const c_char);
588    crate::strcmp(sa as *const u8, sb as *const u8)
589}
590
591#[no_mangle]
592pub unsafe extern "C" fn globfree(pglob: *mut GlobT) {
593    if pglob.is_null() || (*pglob).gl_pathv.is_null() { return; }
594    let offs = (*pglob).gl_offs;
595    for i in 0..(*pglob).gl_pathc {
596        let p = *(*pglob).gl_pathv.add(offs + i);
597        if !p.is_null() { crate::free(p as *mut c_void); }
598    }
599    crate::free((*pglob).gl_pathv as *mut c_void);
600    (*pglob).gl_pathv = core::ptr::null_mut();
601    (*pglob).gl_pathc = 0;
602}
603
604// ============================================================
605// nftw / ftw
606// ============================================================
607
608#[repr(C)]
609pub struct Ftw {
610    pub base: c_int,
611    pub level: c_int,
612}
613
614const FTW_F_V: c_int = 1;
615const FTW_D_V: c_int = 2;
616const FTW_DNR_V: c_int = 3;
617const FTW_NS_V: c_int = 4;
618const FTW_SL_V: c_int = 5;
619const FTW_DP_V: c_int = 6;
620const FTW_SLN_V: c_int = 7;
621
622const FTW_PHYS_V: c_int = 1;
623const FTW_MOUNT_V: c_int = 2;
624const FTW_DEPTH_V: c_int = 8;
625
626struct History {
627    chain: *mut History,
628    dev: u64,
629    ino: u64,
630    level: c_int,
631    base: c_int,
632}
633
634#[derive(Copy, Clone)]
635enum WalkCb {
636    N(Option<unsafe extern "C" fn(*const c_char, *const Stat, c_int, *mut Ftw) -> c_int>),
637    F(Option<unsafe extern "C" fn(*const c_char, *const Stat, c_int) -> c_int>),
638}
639
640unsafe fn walk_call(cb: WalkCb, path: *const c_char, st: *const Stat, type_: c_int, ftw: *mut Ftw) -> c_int {
641    match cb {
642        WalkCb::N(Some(f)) => f(path, st, type_, ftw),
643        WalkCb::F(Some(f)) => f(path, st, type_),
644        _ => 0,
645    }
646}
647
648unsafe fn nftw_do(
649    buf: *mut u8,
650    cap: usize,
651    len: usize,
652    cb: WalkCb,
653    fd_limit: c_int,
654    flags: c_int,
655    h: *mut History,
656    _base: c_int,
657    level: c_int,
658) -> c_int {
659    let path = buf as *const c_char;
660    let mut st: Stat = core::mem::zeroed();
661    let phys = flags & FTW_PHYS_V != 0;
662    let mut type_: c_int;
663    let mut stat_ok = true;
664    let rc = if phys { crate::lstat(path, &mut st as *mut Stat as *mut c_void) } else { crate::stat(path, &mut st) };
665    if rc != 0 {
666        if !phys && ERRNO!() == 2 && crate::lstat(path, &mut st as *mut Stat as *mut c_void) == 0 {
667            type_ = FTW_SLN_V;
668        } else if ERRNO!() != 13 {
669            return -1;
670        } else {
671            type_ = FTW_NS_V;
672            st = core::mem::zeroed();
673            stat_ok = false;
674        }
675    } else {
676        let mode = st.st_mode;
677        if mode & 0o170000 == 0o040000 {
678            type_ = if flags & FTW_DEPTH_V != 0 { FTW_DP_V } else { FTW_D_V };
679        } else if mode & 0o170000 == 0o120000 {
680            type_ = if phys { FTW_SL_V } else { FTW_SLN_V };
681        } else {
682            type_ = FTW_F_V;
683        }
684    }
685    if flags & FTW_MOUNT_V != 0 && !h.is_null() && stat_ok && type_ != FTW_NS_V && st.st_dev != (*h).dev {
686        return 0;
687    }
688    let parent_base = if h.is_null() {
689        let mut k = len;
690        while k > 0 && *buf.add(k - 1) == b'/' { k -= 1; }
691        while k > 0 && *buf.add(k - 1) != b'/' { k -= 1; }
692        k as c_int
693    } else {
694        (*h).base
695    };
696    let mut new = History {
697        chain: h,
698        dev: st.st_dev,
699        ino: st.st_ino,
700        level: level + 1,
701        base: parent_base,
702    };
703    let mut ftw = Ftw { base: parent_base, level: level };
704
705    let mut dfd: c_int = -1;
706    let mut open_err: c_int = 0;
707    if type_ == FTW_D_V || type_ == FTW_DP_V {
708        dfd = crate::open(path, 0, 0);
709        open_err = ERRNO!();
710        if dfd < 0 && open_err == 13 { type_ = FTW_DNR_V; }
711        if fd_limit == 0 && dfd >= 0 { crate::close(dfd); dfd = -1; }
712    }
713
714    if flags & FTW_DEPTH_V == 0 {
715        let r = walk_call(cb, path, &st, type_, &mut ftw);
716        if r != 0 { return r; }
717    }
718
719    let mut p = h;
720    while !p.is_null() {
721        if (*p).dev == st.st_dev && (*p).ino == st.st_ino { return 0; }
722        p = (*p).chain;
723    }
724
725    if (type_ == FTW_D_V || type_ == FTW_DP_V) && fd_limit > 0 {
726        if dfd < 0 {
727            ERRNO!() = open_err;
728            return -1;
729        }
730        // Enumerate with getdents64 so nested walks are independent.
731        #[repr(align(8))]
732        struct Buf([u8; 8192]);
733        let mut dbuf = Buf([0u8; 8192]);
734        let name_off = core::mem::offset_of!(linux_dirent64, d_name);
735        loop {
736            let n = <Arch as Syscalls>::syscall3(
737                SYS_GETDENTS64,
738                dfd as i64,
739                dbuf.0.as_mut_ptr() as i64,
740                dbuf.0.len() as i64,
741            ) as isize;
742            if n <= 0 { break; }
743            let mut off = 0usize;
744            while off + name_off < n as usize {
745                let ent = dbuf.0.as_ptr().add(off) as *const linux_dirent64;
746                let reclen = (*ent).d_reclen as usize;
747                if reclen == 0 { break; }
748                let name = dbuf.0.as_ptr().add(off + name_off) as *const c_char;
749                off += reclen;
750                if *name == b'.' as c_char {
751                    let s = *name.add(1);
752                    if s == 0 || (s == b'.' as c_char && *name.add(2) == 0) { continue; }
753                }
754                let nl = crate::strlen(name);
755                if len + 1 + nl >= cap {
756                    ERRNO!() = 36;
757                    crate::close(dfd);
758                    return -1;
759                }
760                *buf.add(len) = b'/';
761                core::ptr::copy_nonoverlapping(name as *const u8, buf.add(len + 1), nl + 1);
762                let r = nftw_do(buf, cap, len + 1 + nl, cb, fd_limit - 1, flags, &mut new, parent_base, level + 1);
763                if r != 0 { crate::close(dfd); return r; }
764            }
765        }
766        crate::close(dfd);
767    } else if dfd >= 0 {
768        crate::close(dfd);
769    }
770    *buf.add(len) = 0;
771
772    if flags & FTW_DEPTH_V != 0 {
773        let mut f2 = Ftw { base: parent_base, level: level };
774        let r = walk_call(cb, path, &st, type_, &mut f2);
775        if r != 0 { return r; }
776    }
777    let _ = &mut new;
778    0
779}
780
781#[no_mangle]
782pub unsafe extern "C" fn nftw(
783    path: *const c_char,
784    f: Option<unsafe extern "C" fn(*const c_char, *const Stat, c_int, *mut Ftw) -> c_int>,
785    fd_limit: c_int,
786    flags: c_int,
787) -> c_int {
788    if path.is_null() || f.is_none() { ERRNO!() = EINVAL; return -1; }
789    if fd_limit <= 0 { return 0; }
790    let l = crate::strlen(path);
791    if l >= PATH_MAX_V { ERRNO!() = 36; return -1; }
792    let mut buf = [0u8; PATH_MAX_V + 1];
793    core::ptr::copy_nonoverlapping(path as *const u8, buf.as_mut_ptr(), l + 1);
794    nftw_do(buf.as_mut_ptr(), buf.len(), l, WalkCb::N(f), fd_limit, flags, core::ptr::null_mut(), 0, 0)
795}
796
797#[no_mangle]
798pub unsafe extern "C" fn ftw(
799    path: *const c_char,
800    f: Option<unsafe extern "C" fn(*const c_char, *const Stat, c_int) -> c_int>,
801    fd_limit: c_int,
802) -> c_int {
803    if path.is_null() || f.is_none() { ERRNO!() = EINVAL; return -1; }
804    if fd_limit <= 0 { return 0; }
805    let l = crate::strlen(path);
806    if l >= PATH_MAX_V { ERRNO!() = 36; return -1; }
807    let mut buf = [0u8; PATH_MAX_V + 1];
808    core::ptr::copy_nonoverlapping(path as *const u8, buf.as_mut_ptr(), l + 1);
809    nftw_do(buf.as_mut_ptr(), buf.len(), l, WalkCb::F(f), fd_limit, FTW_PHYS_V, core::ptr::null_mut(), 0, 0)
810}