[libc-commits] [libc] [libc] Implement ftw and nftw functions (PR #188611)
Jeff Bailey via libc-commits
libc-commits at lists.llvm.org
Thu Mar 26 08:25:39 PDT 2026
https://github.com/kaladron updated https://github.com/llvm/llvm-project/pull/188611
>From e5058709d14d377be62eeca3b950202cb91c2343 Mon Sep 17 00:00:00 2001
From: Jeff Bailey <jbailey at raspberryginger.com>
Date: Mon, 23 Dec 2024 15:27:28 +0000
Subject: [PATCH] [libc] Implement ftw and nftw functions
Add POSIX-compliant ftw() and nftw() functions for file tree walking.
Key implementation details:
- Shared implementation in ftw_impl.cpp using add_object_library
- Uses LLVM libc's own lstat/stat to avoid struct stat ABI mismatch
between LLVM libc (120 bytes) and glibc (144 bytes)
- CallbackWrapper struct with union avoids virtual functions (required
due to -fno-rtti build constraints)
- ScopedDir RAII wrapper for automatic directory cleanup
New types added:
- struct_FTW: Contains base offset and directory level
- __ftw_func_t: Callback type for ftw()
- __nftw_func_t: Callback type for nftw()
Header (ftw.h.def) defines POSIX constants as enums with explicit values:
- Type flags: FTW_F, FTW_D, FTW_DNR, FTW_NS, FTW_SL, FTW_DP, FTW_SLN
- Walk flags: FTW_PHYS, FTW_MOUNT, FTW_CHDIR, FTW_DEPTH, FTW_ACTIONRETVAL
- Action return values (GNU extension): FTW_CONTINUE, FTW_STOP,
FTW_SKIP_SUBTREE, FTW_SKIP_SIBLINGS
Tested on Linux x86_64 with 7 unit tests covering basic traversal,
nonexistent paths, FTW_DEPTH, FTW_PHYS flags, and callback return values.
Co-authored-by: Raman Tennti <rtenneti at google.com>
---
libc/config/linux/x86_64/entrypoints.txt | 4 +
libc/docs/CMakeLists.txt | 1 +
libc/docs/headers/index.rst | 1 +
libc/hdr/CMakeLists.txt | 9 +
libc/hdr/ftw_macros.h | 22 +
libc/include/CMakeLists.txt | 12 +
libc/include/ftw.h.def | 18 +
libc/include/ftw.yaml | 28 ++
libc/include/llvm-libc-macros/CMakeLists.txt | 6 +
libc/include/llvm-libc-macros/ftw-macros.h | 37 ++
libc/include/llvm-libc-types/CMakeLists.txt | 3 +
libc/include/llvm-libc-types/__ftw_func_t.h | 16 +
libc/include/llvm-libc-types/__nftw_func_t.h | 18 +
libc/include/llvm-libc-types/struct_FTW.h | 17 +
libc/src/CMakeLists.txt | 1 +
libc/src/__support/File/CMakeLists.txt | 9 +
libc/src/__support/File/scoped_dir.h | 68 ++++
libc/src/ftw/CMakeLists.txt | 61 +++
libc/src/ftw/ftw.cpp | 32 ++
libc/src/ftw/ftw.h | 21 +
libc/src/ftw/ftw_impl.cpp | 253 ++++++++++++
libc/src/ftw/ftw_impl.h | 66 +++
libc/src/ftw/nftw.cpp | 36 ++
libc/src/ftw/nftw.h | 21 +
libc/src/sys/stat/lstat.h | 4 +
libc/src/sys/stat/stat.h | 4 +
libc/test/src/CMakeLists.txt | 1 +
libc/test/src/ftw/CMakeLists.txt | 28 ++
libc/test/src/ftw/ftw_test.cpp | 398 +++++++++++++++++++
libc/test/src/ftw/testdata/CMakeLists.txt | 4 +
libc/utils/docgen/ftw.yaml | 39 ++
31 files changed, 1238 insertions(+)
create mode 100644 libc/hdr/ftw_macros.h
create mode 100644 libc/include/ftw.h.def
create mode 100644 libc/include/ftw.yaml
create mode 100644 libc/include/llvm-libc-macros/ftw-macros.h
create mode 100644 libc/include/llvm-libc-types/__ftw_func_t.h
create mode 100644 libc/include/llvm-libc-types/__nftw_func_t.h
create mode 100644 libc/include/llvm-libc-types/struct_FTW.h
create mode 100644 libc/src/__support/File/scoped_dir.h
create mode 100644 libc/src/ftw/CMakeLists.txt
create mode 100644 libc/src/ftw/ftw.cpp
create mode 100644 libc/src/ftw/ftw.h
create mode 100644 libc/src/ftw/ftw_impl.cpp
create mode 100644 libc/src/ftw/ftw_impl.h
create mode 100644 libc/src/ftw/nftw.cpp
create mode 100644 libc/src/ftw/nftw.h
create mode 100644 libc/test/src/ftw/CMakeLists.txt
create mode 100644 libc/test/src/ftw/ftw_test.cpp
create mode 100644 libc/test/src/ftw/testdata/CMakeLists.txt
create mode 100644 libc/utils/docgen/ftw.yaml
diff --git a/libc/config/linux/x86_64/entrypoints.txt b/libc/config/linux/x86_64/entrypoints.txt
index af54217b190fc..9a7bc0e97e5f8 100644
--- a/libc/config/linux/x86_64/entrypoints.txt
+++ b/libc/config/linux/x86_64/entrypoints.txt
@@ -33,6 +33,10 @@ set(TARGET_LIBC_ENTRYPOINTS
libc.src.fcntl.open
libc.src.fcntl.openat
+ # ftw.h entrypoints
+ libc.src.ftw.ftw
+ libc.src.ftw.nftw
+
# poll.h entrypoints
libc.src.poll.poll
diff --git a/libc/docs/CMakeLists.txt b/libc/docs/CMakeLists.txt
index 259902f62791f..aac7ab728b7d5 100644
--- a/libc/docs/CMakeLists.txt
+++ b/libc/docs/CMakeLists.txt
@@ -49,6 +49,7 @@ if (SPHINX_FOUND)
errno
fenv
float
+ ftw
glob
inttypes
locale
diff --git a/libc/docs/headers/index.rst b/libc/docs/headers/index.rst
index f4ad77b490761..5b87f076c31a1 100644
--- a/libc/docs/headers/index.rst
+++ b/libc/docs/headers/index.rst
@@ -15,6 +15,7 @@ Implementation Status
errno
fenv
float
+ ftw
glob
inttypes
locale
diff --git a/libc/hdr/CMakeLists.txt b/libc/hdr/CMakeLists.txt
index 0a496206f4909..4714044472a17 100644
--- a/libc/hdr/CMakeLists.txt
+++ b/libc/hdr/CMakeLists.txt
@@ -57,6 +57,15 @@ add_proxy_header_library(
)
add_header_library(fcntl_overlay HDRS fcntl_overlay.h)
+add_proxy_header_library(
+ ftw_macros
+ HDRS
+ ftw_macros.h
+ FULL_BUILD_DEPENDS
+ libc.include.ftw
+ libc.include.llvm-libc-macros.ftw_macros
+)
+
add_proxy_header_library(
fcntl_macros
HDRS
diff --git a/libc/hdr/ftw_macros.h b/libc/hdr/ftw_macros.h
new file mode 100644
index 0000000000000..edfde3fdb35c7
--- /dev/null
+++ b/libc/hdr/ftw_macros.h
@@ -0,0 +1,22 @@
+//===-- Definition of macros from ftw.h -----------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_LIBC_HDR_FTW_MACROS_H
+#define LLVM_LIBC_HDR_FTW_MACROS_H
+
+#ifdef LIBC_FULL_BUILD
+
+#include "include/llvm-libc-macros/ftw-macros.h"
+
+#else // Overlay mode
+
+#include <ftw.h>
+
+#endif // LLVM_LIBC_FULL_BUILD
+
+#endif // LLVM_LIBC_HDR_FTW_MACROS_H
diff --git a/libc/include/CMakeLists.txt b/libc/include/CMakeLists.txt
index 3f4bbf85160b7..d6b0b09ac25a9 100644
--- a/libc/include/CMakeLists.txt
+++ b/libc/include/CMakeLists.txt
@@ -64,6 +64,18 @@ add_header_macro(
.llvm_libc_common_h
)
+add_header_macro(
+ ftw
+ ../libc/include/ftw.yaml
+ ftw.h
+ DEPENDS
+ .llvm_libc_common_h
+ .llvm-libc-macros.ftw_macros
+ .llvm-libc-types.struct_FTW
+ .llvm-libc-types.__ftw_func_t
+ .llvm-libc-types.__nftw_func_t
+)
+
add_header_macro(
dlfcn
../libc/include/dlfcn.yaml
diff --git a/libc/include/ftw.h.def b/libc/include/ftw.h.def
new file mode 100644
index 0000000000000..990f64e98caee
--- /dev/null
+++ b/libc/include/ftw.h.def
@@ -0,0 +1,18 @@
+//===-- C standard library header ftw.h -----------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_LIBC_FTW_H
+#define LLVM_LIBC_FTW_H
+
+#include "__llvm-libc-common.h"
+
+#include "llvm-libc-macros/ftw-macros.h"
+
+%%public_api()
+
+#endif // LLVM_LIBC_FTW_H
diff --git a/libc/include/ftw.yaml b/libc/include/ftw.yaml
new file mode 100644
index 0000000000000..eb18a8c1c8160
--- /dev/null
+++ b/libc/include/ftw.yaml
@@ -0,0 +1,28 @@
+header: ftw.h
+header_template: ftw.h.def
+macros: []
+types:
+ - type_name: struct_FTW
+ - type_name: __ftw_func_t
+ - type_name: __nftw_func_t
+enums: []
+objects: []
+functions:
+ - name: ftw
+ standards:
+ - posix
+ return_type: int
+ arguments:
+ - type: const char *
+ - type: __ftw_func_t
+ - type: int
+ - name: nftw
+ standards:
+ - posix
+ - gnu
+ return_type: int
+ arguments:
+ - type: const char *
+ - type: __nftw_func_t
+ - type: int
+ - type: int
diff --git a/libc/include/llvm-libc-macros/CMakeLists.txt b/libc/include/llvm-libc-macros/CMakeLists.txt
index 1f34257c57e01..fd850d4bbee1d 100644
--- a/libc/include/llvm-libc-macros/CMakeLists.txt
+++ b/libc/include/llvm-libc-macros/CMakeLists.txt
@@ -66,6 +66,12 @@ add_macro_header(
null-macro.h
)
+add_macro_header(
+ ftw_macros
+ HDR
+ ftw-macros.h
+)
+
add_macro_header(
fcntl_macros
HDR
diff --git a/libc/include/llvm-libc-macros/ftw-macros.h b/libc/include/llvm-libc-macros/ftw-macros.h
new file mode 100644
index 0000000000000..4a9f44356591b
--- /dev/null
+++ b/libc/include/llvm-libc-macros/ftw-macros.h
@@ -0,0 +1,37 @@
+//===-- Definition of macros from ftw.h -----------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_LIBC_MACROS_FTW_MACROS_H
+#define LLVM_LIBC_MACROS_FTW_MACROS_H
+
+// Values for the typeflag argument to the callback function.
+#define FTW_F 0 // Non-directory file.
+#define FTW_D 1 // Directory.
+#define FTW_DNR 2 // Directory without read permission.
+#define FTW_NS 3 // Unknown type; stat() failed.
+#define FTW_SL 4 // Symbolic link.
+#define FTW_DP 5 // Directory with subdirectories visited.
+#define FTW_SLN 6 // Symbolic link naming non-existing file.
+
+// Flags for the flags argument to nftw().
+#define FTW_PHYS 1 // Physical walk, does not follow symbolic links.
+#define FTW_MOUNT 2 // The walk does not cross a mount point.
+#define FTW_CHDIR 4 // Change to each directory before processing.
+#define FTW_DEPTH 8 // All subdirectories visited before the directory.
+
+#ifdef _GNU_SOURCE
+#define FTW_ACTIONRETVAL 16 // Use FTW_* action return values (GNU extension).
+
+// Return values from callback functions (when FTW_ACTIONRETVAL is set).
+#define FTW_CONTINUE 0 // Continue with next sibling.
+#define FTW_STOP 1 // Return from ftw/nftw with FTW_STOP.
+#define FTW_SKIP_SUBTREE 2 // Don't walk through subtree (FTW_D only).
+#define FTW_SKIP_SIBLINGS 3 // Skip siblings, continue with parent.
+#endif // _GNU_SOURCE
+
+#endif // LLVM_LIBC_MACROS_FTW_MACROS_H
diff --git a/libc/include/llvm-libc-types/CMakeLists.txt b/libc/include/llvm-libc-types/CMakeLists.txt
index 55693eeaaf0da..d16e0ab9115fd 100644
--- a/libc/include/llvm-libc-types/CMakeLists.txt
+++ b/libc/include/llvm-libc-types/CMakeLists.txt
@@ -9,6 +9,7 @@ add_header(
libc.include.llvm-libc-macros.annex_k_macros
)
add_header(ssize_t HDR ssize_t.h)
+add_header(struct_FTW HDR struct_FTW.h)
add_header(__atfork_callback_t HDR __atfork_callback_t.h)
add_header(__search_compare_t HDR __search_compare_t.h)
add_header(__call_once_func_t HDR __call_once_func_t.h)
@@ -137,6 +138,8 @@ add_header(
.dev_t .ino_t .mode_t .nlink_t .uid_t .gid_t .off_t .struct_timespec
.blksize_t .blkcnt_t
)
+add_header(__ftw_func_t HDR __ftw_func_t.h DEPENDS .struct_stat)
+add_header(__nftw_func_t HDR __nftw_func_t.h DEPENDS .struct_stat .struct_FTW)
add_header(struct_tm HDR struct_tm.h)
add_header(struct_utsname HDR struct_utsname.h)
add_header(thrd_start_t HDR thrd_start_t.h)
diff --git a/libc/include/llvm-libc-types/__ftw_func_t.h b/libc/include/llvm-libc-types/__ftw_func_t.h
new file mode 100644
index 0000000000000..f93b2f82c8ac8
--- /dev/null
+++ b/libc/include/llvm-libc-types/__ftw_func_t.h
@@ -0,0 +1,16 @@
+//===-- Definition of type __ftw_func_t -----------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_LIBC_TYPES___FTW_FUNC_T_H
+#define LLVM_LIBC_TYPES___FTW_FUNC_T_H
+
+struct stat;
+
+typedef int (*__ftw_func_t)(const char *, const struct stat *, int);
+
+#endif // LLVM_LIBC_TYPES___FTW_FUNC_T_H
diff --git a/libc/include/llvm-libc-types/__nftw_func_t.h b/libc/include/llvm-libc-types/__nftw_func_t.h
new file mode 100644
index 0000000000000..c7b74da8571a7
--- /dev/null
+++ b/libc/include/llvm-libc-types/__nftw_func_t.h
@@ -0,0 +1,18 @@
+//===-- Definition of type __nftw_func_t ----------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_LIBC_TYPES___NFTW_FUNC_T_H
+#define LLVM_LIBC_TYPES___NFTW_FUNC_T_H
+
+struct stat;
+struct FTW;
+
+typedef int (*__nftw_func_t)(const char *, const struct stat *, int,
+ struct FTW *);
+
+#endif // LLVM_LIBC_TYPES___NFTW_FUNC_T_H
diff --git a/libc/include/llvm-libc-types/struct_FTW.h b/libc/include/llvm-libc-types/struct_FTW.h
new file mode 100644
index 0000000000000..46825c8f6b781
--- /dev/null
+++ b/libc/include/llvm-libc-types/struct_FTW.h
@@ -0,0 +1,17 @@
+//===-- Definition of struct FTW ------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_LIBC_TYPES_STRUCT_FTW_H
+#define LLVM_LIBC_TYPES_STRUCT_FTW_H
+
+struct FTW {
+ int base; // Offset of the filename in the pathname.
+ int level; // Depth of the file in the tree.
+};
+
+#endif // LLVM_LIBC_TYPES_STRUCT_FTW_H
diff --git a/libc/src/CMakeLists.txt b/libc/src/CMakeLists.txt
index 8a0acccaed708..694d9baa7a92a 100644
--- a/libc/src/CMakeLists.txt
+++ b/libc/src/CMakeLists.txt
@@ -22,6 +22,7 @@ add_subdirectory(wctype)
if(${LIBC_TARGET_OS} STREQUAL "linux")
add_subdirectory(dirent)
add_subdirectory(fcntl)
+ add_subdirectory(ftw)
add_subdirectory(poll)
add_subdirectory(pthread)
add_subdirectory(sched)
diff --git a/libc/src/__support/File/CMakeLists.txt b/libc/src/__support/File/CMakeLists.txt
index f5388ed8e5f34..99c32e15a8daa 100644
--- a/libc/src/__support/File/CMakeLists.txt
+++ b/libc/src/__support/File/CMakeLists.txt
@@ -35,6 +35,15 @@ add_object_library(
libc.src.__support.threads.mutex
)
+add_header_library(
+ scoped_dir
+ HDRS
+ scoped_dir.h
+ DEPENDS
+ .dir
+ libc.src.__support.macros.config
+)
+
if(NOT EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/${LIBC_TARGET_OS})
return()
endif()
diff --git a/libc/src/__support/File/scoped_dir.h b/libc/src/__support/File/scoped_dir.h
new file mode 100644
index 0000000000000..7cf38c9cde6d3
--- /dev/null
+++ b/libc/src/__support/File/scoped_dir.h
@@ -0,0 +1,68 @@
+//===-- A scoped Dir wrapper for RAII directory handling -------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_LIBC_SRC___SUPPORT_FILE_SCOPED_DIR_H
+#define LLVM_LIBC_SRC___SUPPORT_FILE_SCOPED_DIR_H
+
+#include "src/__support/File/dir.h"
+#include "src/__support/macros/config.h"
+
+namespace LIBC_NAMESPACE_DECL {
+
+// RAII wrapper for Dir that automatically closes the directory on destruction.
+// Usage:
+// auto result = Dir::open(path);
+// if (!result) { handle error }
+// ScopedDir dir(result.value());
+// // dir automatically closes when it goes out of scope
+class ScopedDir {
+ Dir *dir = nullptr;
+
+public:
+ LIBC_INLINE ScopedDir() = default;
+ LIBC_INLINE explicit ScopedDir(Dir *d) : dir(d) {}
+
+ LIBC_INLINE ~ScopedDir() {
+ if (dir)
+ dir->close();
+ }
+
+ // Move-only type
+ LIBC_INLINE ScopedDir(ScopedDir &&other) : dir(other.dir) {
+ other.dir = nullptr;
+ }
+ LIBC_INLINE ScopedDir &operator=(ScopedDir &&other) {
+ if (this != &other) {
+ if (dir)
+ dir->close();
+ dir = other.dir;
+ other.dir = nullptr;
+ }
+ return *this;
+ }
+
+ // Non-copyable
+ ScopedDir(const ScopedDir &) = delete;
+ ScopedDir &operator=(const ScopedDir &) = delete;
+
+ LIBC_INLINE Dir *operator->() { return dir; }
+ LIBC_INLINE Dir &operator*() { return *dir; }
+ LIBC_INLINE explicit operator bool() const { return dir != nullptr; }
+ LIBC_INLINE Dir *get() { return dir; }
+
+ // Release ownership without closing
+ LIBC_INLINE Dir *release() {
+ Dir *tmp = dir;
+ dir = nullptr;
+ return tmp;
+ }
+};
+
+} // namespace LIBC_NAMESPACE_DECL
+
+#endif // LLVM_LIBC_SRC___SUPPORT_FILE_SCOPED_DIR_H
diff --git a/libc/src/ftw/CMakeLists.txt b/libc/src/ftw/CMakeLists.txt
new file mode 100644
index 0000000000000..ae831aea8c2d9
--- /dev/null
+++ b/libc/src/ftw/CMakeLists.txt
@@ -0,0 +1,61 @@
+add_object_library(
+ ftw_impl
+ SRCS
+ ftw_impl.cpp
+ HDRS
+ ftw_impl.h
+ DEPENDS
+ libc.hdr.fcntl_macros
+ libc.hdr.ftw_macros
+ libc.hdr.sys_stat_macros
+ libc.include.dirent
+ libc.include.ftw
+ libc.include.sys_stat
+ libc.include.llvm-libc-types.struct_dirent
+ libc.include.llvm-libc-types.struct_FTW
+ libc.include.llvm-libc-types.struct_stat
+ libc.src.__support.CPP.expected
+ libc.src.__support.CPP.string
+ libc.src.__support.CPP.string_view
+ libc.src.__support.File.dir
+ libc.src.__support.File.scoped_dir
+ libc.src.__support.libc_errno
+ libc.src.fcntl.open
+ libc.src.sys.stat.lstat
+ libc.src.sys.stat.stat
+ libc.src.unistd.chdir
+ libc.src.unistd.close
+ libc.src.unistd.fchdir
+)
+
+add_entrypoint_object(
+ ftw
+ SRCS
+ ftw.cpp
+ HDRS
+ ftw.h
+ DEPENDS
+ libc.hdr.ftw_macros
+ libc.include.ftw
+ libc.include.llvm-libc-types.__ftw_func_t
+ libc.src.__support.CPP.string
+ libc.src.__support.common
+ libc.src.__support.libc_errno
+ .ftw_impl
+)
+
+add_entrypoint_object(
+ nftw
+ SRCS
+ nftw.cpp
+ HDRS
+ nftw.h
+ DEPENDS
+ libc.hdr.ftw_macros
+ libc.include.ftw
+ libc.include.llvm-libc-types.__nftw_func_t
+ libc.src.__support.CPP.string
+ libc.src.__support.common
+ libc.src.__support.libc_errno
+ .ftw_impl
+)
diff --git a/libc/src/ftw/ftw.cpp b/libc/src/ftw/ftw.cpp
new file mode 100644
index 0000000000000..19b4c7854399a
--- /dev/null
+++ b/libc/src/ftw/ftw.cpp
@@ -0,0 +1,32 @@
+//===-- Implementation of ftw function ------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "src/ftw/ftw.h"
+#include "hdr/ftw_macros.h"
+#include "src/ftw/ftw_impl.h"
+
+#include "src/__support/common.h"
+#include "src/__support/libc_errno.h"
+
+namespace LIBC_NAMESPACE_DECL {
+
+LLVM_LIBC_FUNCTION(int, ftw,
+ (const char *DirPath, __ftw_func_t Fn, int FdLimit)) {
+ ftw_impl::CallbackWrapper Wrapper;
+ Wrapper.IsNftw = false;
+ Wrapper.FtwFnVal = Fn;
+ auto Result =
+ ftw_impl::doMergedFtw(DirPath, Wrapper, FdLimit, 0, 0, 0, nullptr);
+ if (!Result) {
+ libc_errno = Result.error();
+ return -1;
+ }
+ return Result.value();
+}
+
+} // namespace LIBC_NAMESPACE_DECL
diff --git a/libc/src/ftw/ftw.h b/libc/src/ftw/ftw.h
new file mode 100644
index 0000000000000..fc11c1dcf99b6
--- /dev/null
+++ b/libc/src/ftw/ftw.h
@@ -0,0 +1,21 @@
+//===-- Implementation header of ftw ----------------------------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_LIBC_SRC_FTW_FTW_H
+#define LLVM_LIBC_SRC_FTW_FTW_H
+
+#include "include/llvm-libc-types/__ftw_func_t.h"
+#include "src/__support/macros/config.h"
+
+namespace LIBC_NAMESPACE_DECL {
+
+int ftw(const char *DirPath, __ftw_func_t Fn, int FdLimit);
+
+} // namespace LIBC_NAMESPACE_DECL
+
+#endif // LLVM_LIBC_SRC_FTW_FTW_H
diff --git a/libc/src/ftw/ftw_impl.cpp b/libc/src/ftw/ftw_impl.cpp
new file mode 100644
index 0000000000000..073eeea417f2c
--- /dev/null
+++ b/libc/src/ftw/ftw_impl.cpp
@@ -0,0 +1,253 @@
+//===-- Implementation of shared ftw/nftw logic ---------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "src/ftw/ftw_impl.h"
+
+#include "src/__support/CPP/string.h"
+#include "src/__support/CPP/string_view.h"
+#include "src/__support/File/scoped_dir.h"
+#include "src/__support/libc_errno.h"
+#include "src/fcntl/open.h"
+#include "src/sys/stat/lstat.h"
+#include "src/sys/stat/stat.h"
+#include "src/unistd/chdir.h"
+#include "src/unistd/close.h"
+#include "src/unistd/fchdir.h"
+
+#include "hdr/fcntl_macros.h"
+#include "hdr/ftw_macros.h"
+#include "hdr/sys_stat_macros.h"
+#ifdef LIBC_FULL_BUILD
+#include "include/llvm-libc-types/struct_FTW.h"
+#include "include/llvm-libc-types/struct_dirent.h"
+#include "include/llvm-libc-types/struct_stat.h"
+#else
+#include <dirent.h>
+#include <ftw.h>
+#include <sys/stat.h>
+#endif
+
+namespace LIBC_NAMESPACE_DECL {
+namespace ftw_impl {
+
+class StartDirSaver {
+ int StartFd;
+
+public:
+ StartDirSaver(int Fd) : StartFd(Fd) {}
+ ~StartDirSaver() {
+ if (StartFd >= 0) {
+ fchdir(StartFd);
+ close(StartFd);
+ }
+ }
+};
+
+class LevelDirSaver {
+ bool Active;
+
+public:
+ LevelDirSaver(bool DoChdir) : Active(DoChdir) {}
+ ~LevelDirSaver() {
+ if (Active)
+ chdir("..");
+ }
+};
+
+cpp::expected<int, int> doMergedFtw(const cpp::string &DirPath,
+ const CallbackWrapper &Fn, int FdLimit,
+ int Flags, int Level,
+ unsigned long StartDevice,
+ AncestorDir *Ancestors) {
+ int StartFd = -1;
+ // Save starting directory for FTW_CHDIR restoration.
+ if (Level == 0 && (Flags & FTW_CHDIR)) {
+ StartFd = open(".", O_RDONLY);
+ if (StartFd < 0)
+ return cpp::unexpected<int>(libc_errno);
+ }
+ StartDirSaver RootSaver(StartFd);
+
+ // Set up FTW buffer and calculate filename offset base.
+ struct FTW FtwBuf;
+ FtwBuf.level = Level;
+ cpp::string_view PathView(DirPath);
+ size_t SlashFound = PathView.find_last_of('/');
+ FtwBuf.base = (SlashFound != cpp::string_view::npos)
+ ? static_cast<int>(SlashFound + 1)
+ : 0;
+
+ const char *OsPath = (Level == 0 || !(Flags & FTW_CHDIR))
+ ? DirPath.c_str()
+ : (DirPath.c_str() + FtwBuf.base);
+
+ int TypeFlag = FTW_F;
+ struct stat StatBuf;
+ // Stat the path, respecting FTW_PHYS with lstat vs stat.
+ // We use LIBC_NAMESPACE:: so that we call the internal (l)stat in overlay
+ // mode.
+ if (Flags & FTW_PHYS) {
+ if (LIBC_NAMESPACE::lstat(OsPath, &StatBuf) < 0) {
+ if (libc_errno == EACCES)
+ TypeFlag = FTW_NS;
+ else
+ return cpp::unexpected<int>(libc_errno);
+ }
+ } else {
+ if (LIBC_NAMESPACE::stat(OsPath, &StatBuf) < 0) {
+ if (libc_errno == EACCES) {
+ TypeFlag = FTW_NS;
+ } else if (LIBC_NAMESPACE::lstat(OsPath, &StatBuf) == 0) {
+ // Dangling symlink found.
+ TypeFlag = FTW_SLN;
+ } else if (libc_errno == EACCES) {
+ TypeFlag = FTW_NS;
+ } else {
+ return cpp::unexpected<int>(libc_errno);
+ }
+ }
+ }
+
+ // Track starting device for FTW_MOUNT traversal limits.
+ if (Level == 0)
+ StartDevice = StatBuf.st_dev;
+
+ // Skip traversal into mounted filesystems if FTW_MOUNT is set.
+ if ((Flags & FTW_MOUNT) && Level > 0 && StatBuf.st_dev != StartDevice)
+ return 0;
+
+ // Map stat mode to final FTW_* type flags.
+ if (TypeFlag != FTW_SLN && TypeFlag != FTW_NS) {
+ if (S_ISDIR(StatBuf.st_mode))
+ TypeFlag = (Flags & FTW_DEPTH) ? FTW_DP : FTW_D;
+ else if (S_ISLNK(StatBuf.st_mode))
+ TypeFlag = (Flags & FTW_PHYS) ? FTW_SL : FTW_SLN;
+ else
+ TypeFlag = FTW_F;
+ }
+
+ // Legacy ftw() must map FTW_SLN to FTW_SL.
+ if (!Fn.IsNftw && TypeFlag == FTW_SLN)
+ TypeFlag = FTW_SL;
+
+ // Cycle detection for directories to prevent infinite recursion.
+ if (TypeFlag == FTW_D || TypeFlag == FTW_DP || TypeFlag == FTW_DNR) {
+ for (AncestorDir *A = Ancestors; A != nullptr; A = A->Parent) {
+ if (A->Dev == StatBuf.st_dev && A->Ino == StatBuf.st_ino)
+ return 0;
+ }
+ }
+
+ bool SkipSubtree = false;
+ Dir *OpenDir = nullptr;
+ // Attempt directory open; propagate fd count exhaustion errors.
+ if (TypeFlag == FTW_D || TypeFlag == FTW_DP) {
+ if (FdLimit <= 0)
+ return cpp::unexpected<int>(EMFILE);
+ auto DirResult = Dir::open(OsPath);
+ if (!DirResult) {
+ if (DirResult.error() == EACCES) {
+ TypeFlag = FTW_DNR;
+ } else {
+ return cpp::unexpected<int>(DirResult.error());
+ }
+ } else {
+ OpenDir = DirResult.value();
+ }
+ }
+
+ if (TypeFlag != FTW_D && TypeFlag != FTW_DP)
+ return Fn.call(DirPath.c_str(), &StatBuf, TypeFlag, &FtwBuf);
+
+ // Pre-order traversal: call callback BEFORE descending.
+ if (!(Flags & FTW_DEPTH)) {
+ int Ret = Fn.call(DirPath.c_str(), &StatBuf, TypeFlag, &FtwBuf);
+ if (Ret != 0) {
+ if (Flags & FTW_ACTIONRETVAL) {
+ // Honor action return values if requested.
+ if (Ret == FTW_SKIP_SUBTREE) {
+ SkipSubtree = true;
+ } else if (Ret == FTW_SKIP_SIBLINGS) {
+ if (OpenDir)
+ OpenDir->close(); // ScopedDir not yet created
+ return Ret;
+ } else {
+ if (OpenDir)
+ OpenDir->close();
+ return Ret;
+ }
+ } else {
+ if (OpenDir)
+ OpenDir->close();
+ return Ret;
+ }
+ }
+ }
+
+ // Descend into children.
+ if (OpenDir && !SkipSubtree) {
+ ScopedDir DirGuard(OpenDir);
+ if (Flags & FTW_CHDIR) {
+ if (chdir(OsPath) < 0)
+ return cpp::unexpected<int>(libc_errno);
+ }
+ LevelDirSaver LevelSaver(Flags & FTW_CHDIR);
+ AncestorDir CurrentAncestor = {StatBuf.st_dev, StatBuf.st_ino, Ancestors};
+
+ while (true) {
+ auto Entry = DirGuard->read();
+ if (!Entry)
+ return cpp::unexpected(Entry.error());
+
+ struct ::dirent *DirentPtr = Entry.value();
+ if (DirentPtr == nullptr)
+ break;
+
+ // Skip dot and dot-dot directories.
+ if (DirentPtr->d_name[0] == '.') {
+ if (DirentPtr->d_name[1] == '\0' ||
+ (DirentPtr->d_name[1] == '.' && DirentPtr->d_name[2] == '\0'))
+ continue;
+ }
+
+ cpp::string EntryPath = DirPath;
+ if (!EntryPath.empty() && EntryPath[EntryPath.size() - 1] != '/')
+ EntryPath += "/";
+ EntryPath += DirentPtr->d_name;
+
+ auto Res = doMergedFtw(EntryPath, Fn, FdLimit - 1, Flags, Level + 1,
+ StartDevice, &CurrentAncestor);
+ if (!Res)
+ return Res;
+ if (Flags & FTW_ACTIONRETVAL) {
+ if (Res.value() == FTW_SKIP_SIBLINGS)
+ break;
+ if (Res.value() != 0 && Res.value() != FTW_SKIP_SUBTREE)
+ return Res.value();
+ } else if (Res.value() != 0) {
+ return Res.value();
+ }
+ }
+ } else if (OpenDir) {
+ OpenDir->close();
+ }
+
+ // Post-order traversal: call callback AFTER descending.
+ if ((Flags & FTW_DEPTH) && !SkipSubtree) {
+ int Ret = Fn.call(DirPath.c_str(), &StatBuf, TypeFlag, &FtwBuf);
+ if (Flags & FTW_ACTIONRETVAL) {
+ if (Ret == FTW_SKIP_SIBLINGS || Ret == FTW_SKIP_SUBTREE)
+ return Ret;
+ }
+ return Ret;
+ }
+ return 0;
+}
+
+} // namespace ftw_impl
+} // namespace LIBC_NAMESPACE_DECL
diff --git a/libc/src/ftw/ftw_impl.h b/libc/src/ftw/ftw_impl.h
new file mode 100644
index 0000000000000..35a8945793b38
--- /dev/null
+++ b/libc/src/ftw/ftw_impl.h
@@ -0,0 +1,66 @@
+//===-- Internal implementation for ftw/nftw --------------------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_LIBC_SRC_FTW_FTW_IMPL_H
+#define LLVM_LIBC_SRC_FTW_FTW_IMPL_H
+
+#include "src/__support/CPP/expected.h"
+#include "src/__support/CPP/string.h"
+#ifdef LIBC_FULL_BUILD
+#include "include/llvm-libc-types/struct_FTW.h"
+#include "include/llvm-libc-types/struct_stat.h"
+#else
+#include <ftw.h>
+#include <sys/stat.h>
+#endif
+
+namespace LIBC_NAMESPACE_DECL {
+namespace ftw_impl {
+
+struct AncestorDir {
+ dev_t Dev;
+ ino_t Ino;
+ AncestorDir *Parent;
+};
+
+using NftwFn = int (*)(const char *FilePath, const struct stat *StatBuf,
+ int TFlag, struct FTW *FtwBuf);
+
+using FtwFn = int (*)(const char *FilePath, const struct stat *StatBuf,
+ int TFlag);
+
+// Unified callback wrapper - uses a union to avoid virtual functions
+struct CallbackWrapper {
+ bool IsNftw;
+ union {
+ NftwFn NftwFnVal;
+ FtwFn FtwFnVal;
+ };
+
+ LIBC_INLINE int call(const char *Path, const struct stat *Sb, int Type,
+ struct FTW *Ftwbuf) const {
+ if (IsNftw)
+ return NftwFnVal(Path, Sb, Type, Ftwbuf);
+ else
+ return FtwFnVal(Path, Sb, Type);
+ }
+};
+
+// Main implementation function - defined in ftw_impl.cpp
+// Returns the callback return value on success (which might be non-zero),
+// or an unexpected errno on failure.
+cpp::expected<int, int> doMergedFtw(const cpp::string &DirPath,
+ const CallbackWrapper &Fn, int FdLimit,
+ int Flags, int Level,
+ unsigned long StartDevice,
+ AncestorDir *Ancestors);
+
+} // namespace ftw_impl
+} // namespace LIBC_NAMESPACE_DECL
+
+#endif // LLVM_LIBC_SRC_FTW_FTW_IMPL_H
diff --git a/libc/src/ftw/nftw.cpp b/libc/src/ftw/nftw.cpp
new file mode 100644
index 0000000000000..b4492b873e527
--- /dev/null
+++ b/libc/src/ftw/nftw.cpp
@@ -0,0 +1,36 @@
+//===-- Implementation of nftw function -----------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "src/ftw/nftw.h"
+#include "src/ftw/ftw_impl.h"
+
+#include "src/__support/common.h"
+#include "src/__support/libc_errno.h"
+
+#include "hdr/ftw_macros.h"
+
+namespace LIBC_NAMESPACE_DECL {
+
+LLVM_LIBC_FUNCTION(int, nftw,
+ (const char *DirPath, __nftw_func_t Fn, int FdLimit,
+ int Flags)) {
+ ftw_impl::CallbackWrapper Wrapper;
+ Wrapper.IsNftw = true;
+ Wrapper.NftwFnVal = Fn;
+ auto Result =
+ ftw_impl::doMergedFtw(DirPath, Wrapper, FdLimit, Flags, 0, 0, nullptr);
+ if (!Result) {
+ libc_errno = Result.error();
+ return -1;
+ }
+ if (Result.value() == -1 && libc_errno == 0)
+ libc_errno = EACCES;
+ return Result.value();
+}
+
+} // namespace LIBC_NAMESPACE_DECL
diff --git a/libc/src/ftw/nftw.h b/libc/src/ftw/nftw.h
new file mode 100644
index 0000000000000..f85f5a6c0f336
--- /dev/null
+++ b/libc/src/ftw/nftw.h
@@ -0,0 +1,21 @@
+//===-- Implementation header of nftw ---------------------------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_LIBC_SRC_FTW_NFTW_H
+#define LLVM_LIBC_SRC_FTW_NFTW_H
+
+#include "include/llvm-libc-types/__nftw_func_t.h"
+#include "src/__support/macros/config.h"
+
+namespace LIBC_NAMESPACE_DECL {
+
+int nftw(const char *DirPath, __nftw_func_t Fn, int FdLimit, int Flags);
+
+} // namespace LIBC_NAMESPACE_DECL
+
+#endif // LLVM_LIBC_SRC_FTW_NFTW_H
diff --git a/libc/src/sys/stat/lstat.h b/libc/src/sys/stat/lstat.h
index a94fc69969ab9..232e3b38409a7 100644
--- a/libc/src/sys/stat/lstat.h
+++ b/libc/src/sys/stat/lstat.h
@@ -10,7 +10,11 @@
#define LLVM_LIBC_SRC_SYS_STAT_LSTAT_H
#include "src/__support/macros/config.h"
+#ifdef LIBC_FULL_BUILD
+#include "include/llvm-libc-types/struct_stat.h"
+#else
#include <sys/stat.h>
+#endif
namespace LIBC_NAMESPACE_DECL {
diff --git a/libc/src/sys/stat/stat.h b/libc/src/sys/stat/stat.h
index 8ec3e9b1aa9e0..b0aac385e7c74 100644
--- a/libc/src/sys/stat/stat.h
+++ b/libc/src/sys/stat/stat.h
@@ -10,7 +10,11 @@
#define LLVM_LIBC_SRC_SYS_STAT_STAT_H
#include "src/__support/macros/config.h"
+#ifdef LIBC_FULL_BUILD
+#include "include/llvm-libc-types/struct_stat.h"
+#else
#include <sys/stat.h>
+#endif
namespace LIBC_NAMESPACE_DECL {
diff --git a/libc/test/src/CMakeLists.txt b/libc/test/src/CMakeLists.txt
index 5e61c739c066a..ff2c69d1f0c7f 100644
--- a/libc/test/src/CMakeLists.txt
+++ b/libc/test/src/CMakeLists.txt
@@ -81,6 +81,7 @@ add_subdirectory(inttypes)
if(${LIBC_TARGET_OS} STREQUAL "linux")
add_subdirectory(fcntl)
+ add_subdirectory(ftw)
add_subdirectory(poll)
add_subdirectory(sched)
add_subdirectory(sys)
diff --git a/libc/test/src/ftw/CMakeLists.txt b/libc/test/src/ftw/CMakeLists.txt
new file mode 100644
index 0000000000000..a9f15114bf754
--- /dev/null
+++ b/libc/test/src/ftw/CMakeLists.txt
@@ -0,0 +1,28 @@
+add_subdirectory(testdata)
+add_custom_target(libc_ftw_unittests)
+
+add_libc_unittest(
+ ftw_test
+ SUITE
+ libc_ftw_unittests
+ SRCS
+ ftw_test.cpp
+ DEPENDS
+ libc.hdr.ftw_macros
+ libc.include.ftw
+ libc.src.__support.CPP.string_view
+ libc.src.dirent.closedir
+ libc.src.dirent.opendir
+ libc.src.dirent.readdir
+ libc.src.errno.errno
+ libc.src.fcntl.open
+ libc.src.ftw.ftw
+ libc.src.ftw.nftw
+ libc.src.sys.stat.chmod
+ libc.src.sys.stat.mkdir
+ libc.src.unistd.close
+ libc.src.unistd.getcwd
+ libc.src.unistd.rmdir
+ libc.src.unistd.symlink
+ libc.src.unistd.unlink
+)
diff --git a/libc/test/src/ftw/ftw_test.cpp b/libc/test/src/ftw/ftw_test.cpp
new file mode 100644
index 0000000000000..1a35e281222e4
--- /dev/null
+++ b/libc/test/src/ftw/ftw_test.cpp
@@ -0,0 +1,398 @@
+//===-- Unittests for ftw and nftw ----------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "hdr/ftw_macros.h"
+#include "src/__support/CPP/string_view.h"
+#include "src/dirent/closedir.h"
+#include "src/dirent/opendir.h"
+#include "src/dirent/readdir.h"
+#include "src/fcntl/open.h"
+#ifdef LIBC_FULL_BUILD
+#include "include/llvm-libc-types/struct_FTW.h"
+#else
+#include <ftw.h>
+#endif
+#include "src/ftw/ftw.h"
+#include "src/ftw/nftw.h"
+#include "src/sys/stat/chmod.h"
+#include "src/sys/stat/mkdir.h"
+#include "src/unistd/close.h"
+#include "src/unistd/getcwd.h"
+#include "src/unistd/rmdir.h"
+#include "src/unistd/symlink.h"
+#include "src/unistd/unlink.h"
+#include "test/UnitTest/ErrnoCheckingTest.h"
+#include "test/UnitTest/ErrnoSetterMatcher.h"
+#include "test/UnitTest/Test.h"
+
+namespace LIBC_NAMESPACE {
+
+using LlvmLibcFtwTest = testing::ErrnoCheckingTest;
+using LlvmLibcNftwTest = testing::ErrnoCheckingTest;
+using cpp::string_view;
+
+// Test data structure to track visited files
+struct VisitedFiles {
+ static constexpr int MAX_FILES = 32;
+ char Paths[MAX_FILES][256];
+ int Types[MAX_FILES];
+ int Levels[MAX_FILES];
+ int Count;
+
+ void reset() { Count = 0; }
+
+ void add(const char *Path, int Type, int Level) {
+ if (Count < MAX_FILES) {
+ // Copy path manually.
+ int I = 0;
+ while (Path[I] && I < 255) {
+ Paths[Count][I] = Path[I];
+ I++;
+ }
+ Paths[Count][I] = '\0';
+ Types[Count] = Type;
+ Levels[Count] = Level;
+ Count++;
+ }
+ }
+
+ bool contains(const char *Substring) const {
+ string_view Sub(Substring);
+ for (int I = 0; I < Count; I++) {
+ string_view Path(Paths[I]);
+ if (Path.find_first_of(Sub[0]) != string_view::npos) {
+ // Simple Substring check
+ for (size_t J = 0; J <= Path.size() - Sub.size(); J++) {
+ bool Match = true;
+ for (size_t K = 0; K < Sub.size() && Match; K++) {
+ if (Paths[I][J + K] != Substring[K])
+ Match = false;
+ }
+ if (Match)
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ int getTypeFor(const char *Substring) const {
+ string_view Sub(Substring);
+ for (int I = 0; I < Count; I++) {
+ string_view Path(Paths[I]);
+ for (size_t J = 0; J + Sub.size() <= Path.size(); J++) {
+ bool Match = true;
+ for (size_t K = 0; K < Sub.size() && Match; K++) {
+ if (Paths[I][J + K] != Substring[K])
+ Match = false;
+ }
+ if (Match)
+ return Types[I];
+ }
+ }
+ return -1;
+ }
+};
+
+static VisitedFiles gVisited;
+
+// Callback for nftw that records visited files
+static int recordVisit(const char *Fpath, const struct stat *Sb, int Typeflag,
+ struct FTW *Ftwbuf) {
+ (void)Sb; // unused
+ gVisited.add(Fpath, Typeflag, Ftwbuf->level);
+ return 0; // continue traversal
+}
+
+// Callback for ftw that records visited files
+static int recordVisitFtw(const char *Fpath, const struct stat *Sb,
+ int Typeflag) {
+ (void)Sb; // unused
+ gVisited.add(Fpath, Typeflag, 0);
+ return 0; // continue traversal
+}
+
+// Simplest callback that does nothing
+static int simpleCallback(const char *Fpath, const struct stat *Sb,
+ int Typeflag) {
+ (void)Fpath;
+ (void)Sb;
+ (void)Typeflag;
+ return 0;
+}
+
+// Use static test directory that exists
+TEST_F(LlvmLibcFtwTest, BasicTraversalWithTestData) {
+ // First make sure testdata directory exists
+ ::DIR *Dir = LIBC_NAMESPACE::opendir(libc_make_test_file_path("testdata"));
+ if (Dir == nullptr) {
+ // Skip test if testdata doesn't exist
+ return;
+ }
+ LIBC_NAMESPACE::closedir(Dir);
+
+ int Result = LIBC_NAMESPACE::ftw(libc_make_test_file_path("testdata"),
+ simpleCallback, 10);
+ ASSERT_EQ(Result, 0);
+}
+
+TEST_F(LlvmLibcFtwTest, NonexistentPath) {
+ gVisited.reset();
+ int result = LIBC_NAMESPACE::ftw("/nonexistent/path", recordVisitFtw, 10);
+ EXPECT_EQ(result, -1);
+ ASSERT_ERRNO_EQ(ENOENT);
+}
+
+TEST_F(LlvmLibcNftwTest, BasicTraversalWithTestData) {
+ gVisited.reset();
+ int result = LIBC_NAMESPACE::nftw(libc_make_test_file_path("testdata"),
+ recordVisit, 10, 0);
+ ASSERT_EQ(result, 0);
+
+ // Should have visited some files
+ EXPECT_GE(gVisited.Count, 1);
+}
+
+TEST_F(LlvmLibcNftwTest, NonexistentPath) {
+ gVisited.reset();
+ int result = LIBC_NAMESPACE::nftw("/nonexistent/path/that/does/not/exist",
+ recordVisit, 10, 0);
+ EXPECT_EQ(result, -1);
+ ASSERT_ERRNO_EQ(ENOENT);
+}
+
+TEST_F(LlvmLibcNftwTest, DepthFirstFlag) {
+ gVisited.reset();
+ int result = LIBC_NAMESPACE::nftw(libc_make_test_file_path("testdata"),
+ recordVisit, 10, FTW_DEPTH);
+ ASSERT_EQ(result, 0);
+
+ // Verify post-order traversal: contents before directory
+ int NestedIndex = -1;
+ int SubdirIndex = -1;
+ for (int i = 0; i < gVisited.Count; i++) {
+ string_view Path(gVisited.Paths[i]);
+ if (Path.ends_with("nested.txt"))
+ NestedIndex = i;
+ else if (Path.ends_with("subdir") && gVisited.Types[i] == FTW_DP)
+ SubdirIndex = i;
+ }
+ ASSERT_NE(NestedIndex, -1);
+ ASSERT_NE(SubdirIndex, -1);
+ EXPECT_LT(NestedIndex, SubdirIndex);
+}
+
+TEST_F(LlvmLibcNftwTest, PhysicalFlag) {
+ gVisited.reset();
+ int result = LIBC_NAMESPACE::nftw(libc_make_test_file_path("testdata"),
+ recordVisit, 10, FTW_PHYS);
+ ASSERT_EQ(result, 0);
+
+ // Should have visited files
+ EXPECT_GE(gVisited.Count, 1);
+}
+
+TEST_F(LlvmLibcNftwTest, CallbackCanStopTraversal) {
+ gVisited.reset();
+ // Use a callback that returns non-zero
+ auto stopImmediately = [](const char *, const struct stat *, int,
+ struct FTW *) -> int { return 42; };
+ int result = LIBC_NAMESPACE::nftw(libc_make_test_file_path("testdata"),
+ stopImmediately, 10, 0);
+ // nftw should return the callback's return value
+ EXPECT_EQ(result, 42);
+}
+
+TEST_F(LlvmLibcNftwTest, ChdirFlag) {
+ char original_cwd[1024];
+ ASSERT_TRUE(LIBC_NAMESPACE::getcwd(original_cwd, sizeof(original_cwd)) !=
+ nullptr);
+
+ auto checkCwd = [](const char *, const struct stat *, int,
+ struct FTW *) -> int {
+ char cwd[1024];
+ if (LIBC_NAMESPACE::getcwd(cwd, sizeof(cwd)) == nullptr)
+ return -1;
+ return 0;
+ };
+
+ int result = LIBC_NAMESPACE::nftw(libc_make_test_file_path("testdata"),
+ checkCwd, 10, FTW_CHDIR);
+ ASSERT_EQ(result, 0);
+
+ char final_cwd[1024];
+ ASSERT_TRUE(LIBC_NAMESPACE::getcwd(final_cwd, sizeof(final_cwd)) != nullptr);
+ // Verify that the original CWD was restored
+ ASSERT_STREQ(original_cwd, final_cwd);
+}
+
+TEST_F(LlvmLibcFtwTest, DanglingSymlinkMapping) {
+ // Create a dangling symlink: link -> nonexistent
+ auto linkName = libc_make_test_file_path("testdata/dangling_link");
+ LIBC_NAMESPACE::unlink(linkName);
+ ASSERT_EQ(LIBC_NAMESPACE::symlink("nonexistent_target", linkName), 0);
+
+ auto checkFtw = [](const char *Fpath, const struct stat *,
+ int Typeflag) -> int {
+ if (string_view(Fpath).ends_with("dangling_link")) {
+ // For legacy ftw, FTW_SLN must be mapped to FTW_SL
+ if (Typeflag == FTW_SL)
+ return 0;
+ return -1;
+ }
+ return 0;
+ };
+
+ libc_errno = 0;
+ int result = LIBC_NAMESPACE::ftw(linkName, checkFtw, 10);
+ EXPECT_EQ(result, 0);
+
+ auto checkNftw = [](const char *Fpath, const struct stat *, int Typeflag,
+ struct FTW *) -> int {
+ if (string_view(Fpath).ends_with("dangling_link")) {
+ // For nftw, FTW_SLN should be reported as is
+ if (Typeflag == FTW_SLN)
+ return 0;
+ return -1;
+ }
+ return 0;
+ };
+
+ libc_errno = 0;
+ result = LIBC_NAMESPACE::nftw(linkName, checkNftw, 10, 0);
+ EXPECT_EQ(result, 0);
+
+ LIBC_NAMESPACE::unlink(linkName);
+ libc_errno = 0;
+}
+
+TEST_F(LlvmLibcNftwTest, UnreadableDirectory) {
+ // Create an unreadable directory
+ const char *dirName = "unreadable_dir";
+ LIBC_NAMESPACE::rmdir(dirName);
+ ASSERT_EQ(LIBC_NAMESPACE::mkdir(dirName, 0333), 0); // No read permission
+
+ gVisited.reset();
+ int result = LIBC_NAMESPACE::nftw(dirName, recordVisit, 10, 0);
+ EXPECT_EQ(result, 0);
+
+ // Should have visited the directory itself as FTW_DNR
+ bool found = false;
+ for (int i = 0; i < gVisited.Count; i++) {
+ if (string_view(gVisited.Paths[i]) == dirName) {
+ EXPECT_EQ(gVisited.Types[i], FTW_DNR);
+ found = true;
+ }
+ }
+ EXPECT_TRUE(found);
+
+ LIBC_NAMESPACE::rmdir(dirName);
+ libc_errno = 0;
+}
+
+TEST_F(LlvmLibcNftwTest, NoSearchPermission) {
+ // Create a parent directory and a child, then remove search permission from
+ // parent
+ const char *parentName = "no_search_parent";
+ const char *childName = "no_search_parent/child";
+
+ LIBC_NAMESPACE::unlink(childName);
+ LIBC_NAMESPACE::rmdir(parentName);
+
+ ASSERT_EQ(LIBC_NAMESPACE::mkdir(parentName, 0777), 0);
+ int fd = LIBC_NAMESPACE::open(childName, O_CREAT | O_WRONLY, 0666);
+ ASSERT_GE(fd, 0);
+ LIBC_NAMESPACE::close(fd);
+
+ // Remove search (execute) permission from parent
+ ASSERT_EQ(LIBC_NAMESPACE::chmod(parentName, 0666), 0);
+
+ gVisited.reset();
+ // We specify FTW_PHYS to avoid stat() trying to resolve and potentially
+ // failing with EACCES before nftw handles it
+ int result = LIBC_NAMESPACE::nftw(childName, recordVisit, 10, FTW_PHYS);
+ EXPECT_EQ(result, 0);
+
+ // Should have visited the child as FTW_NS
+ bool found = false;
+ for (int i = 0; i < gVisited.Count; i++) {
+ if (string_view(gVisited.Paths[i]) == childName) {
+ EXPECT_EQ(gVisited.Types[i], FTW_NS);
+ found = true;
+ }
+ }
+ EXPECT_TRUE(found);
+
+ // Restore permission to allow cleanup
+ LIBC_NAMESPACE::chmod(parentName, 0777);
+ LIBC_NAMESPACE::unlink(childName);
+ LIBC_NAMESPACE::rmdir(parentName);
+ libc_errno = 0;
+}
+
+TEST_F(LlvmLibcNftwTest, ExcessiveDepthRespectsFdLimit) {
+ // Creating a path with depth > 2
+ auto path = libc_make_test_file_path("testdata");
+ // If we specify fdLimit = 1, it should fail with EMFILE when trying to
+ // iterate subdir, or even when visiting files in testdata because of how
+ // recursion works. level 0 (testdata): fdLimit=1. Continue. level 1
+ // (file1.txt): doMergedFtw(..., fdLimit=0). FAILS!
+ int result = LIBC_NAMESPACE::nftw(path, recordVisit, 1, 0);
+ EXPECT_EQ(result, -1);
+ ASSERT_ERRNO_EQ(EMFILE);
+}
+TEST_F(LlvmLibcNftwTest, ActionRetValSkipSubtree) {
+ gVisited.reset();
+ auto callback = [](const char *Fpath, const struct stat *, int Typeflag,
+ struct FTW *Ftwbuf) -> int {
+ gVisited.add(Fpath, Typeflag, Ftwbuf->level);
+ if (string_view(Fpath).ends_with("subdir"))
+ return FTW_SKIP_SUBTREE;
+ return FTW_CONTINUE;
+ };
+
+ int result = LIBC_NAMESPACE::nftw(libc_make_test_file_path("testdata"),
+ callback, 10, FTW_ACTIONRETVAL);
+ ASSERT_EQ(result, 0);
+
+ bool FoundSubdir = false;
+ bool FoundNested = false;
+ for (int i = 0; i < gVisited.Count; i++) {
+ string_view Path(gVisited.Paths[i]);
+ if (Path.ends_with("subdir"))
+ FoundSubdir = true;
+ if (Path.ends_with("nested.txt"))
+ FoundNested = true;
+ }
+ EXPECT_TRUE(FoundSubdir);
+ EXPECT_FALSE(FoundNested);
+}
+
+TEST_F(LlvmLibcNftwTest, ActionRetValSkipSiblings) {
+ gVisited.reset();
+ auto callback = [](const char *Fpath, const struct stat *, int Typeflag,
+ struct FTW *Ftwbuf) -> int {
+ gVisited.add(Fpath, Typeflag, Ftwbuf->level);
+ if (Ftwbuf->level == 1)
+ return FTW_SKIP_SIBLINGS;
+ return FTW_CONTINUE;
+ };
+
+ int result = LIBC_NAMESPACE::nftw(libc_make_test_file_path("testdata"),
+ callback, 10, FTW_ACTIONRETVAL);
+ ASSERT_EQ(result, 0);
+
+ int Level1Count = 0;
+ for (int i = 0; i < gVisited.Count; i++) {
+ if (gVisited.Levels[i] == 1)
+ Level1Count++;
+ }
+ EXPECT_EQ(Level1Count, 1);
+}
+
+} // namespace LIBC_NAMESPACE
diff --git a/libc/test/src/ftw/testdata/CMakeLists.txt b/libc/test/src/ftw/testdata/CMakeLists.txt
new file mode 100644
index 0000000000000..57e0f4a0ba4d8
--- /dev/null
+++ b/libc/test/src/ftw/testdata/CMakeLists.txt
@@ -0,0 +1,4 @@
+file(TOUCH ${CMAKE_CURRENT_BINARY_DIR}/file1.txt)
+file(TOUCH ${CMAKE_CURRENT_BINARY_DIR}/file2.txt)
+file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/subdir)
+file(TOUCH ${CMAKE_CURRENT_BINARY_DIR}/subdir/nested.txt)
diff --git a/libc/utils/docgen/ftw.yaml b/libc/utils/docgen/ftw.yaml
new file mode 100644
index 0000000000000..45c16fd7eb2b5
--- /dev/null
+++ b/libc/utils/docgen/ftw.yaml
@@ -0,0 +1,39 @@
+macros:
+ FTW_F:
+ in-latest-posix: ''
+ FTW_D:
+ in-latest-posix: ''
+ FTW_DNR:
+ in-latest-posix: ''
+ FTW_NS:
+ in-latest-posix: ''
+ FTW_SL:
+ in-latest-posix: ''
+ FTW_DP:
+ in-latest-posix: ''
+ FTW_SLN:
+ in-latest-posix: ''
+ FTW_PHYS:
+ in-latest-posix: ''
+ FTW_MOUNT:
+ in-latest-posix: ''
+ FTW_CHDIR:
+ in-latest-posix: ''
+ FTW_DEPTH:
+ in-latest-posix: ''
+ FTW_ACTIONRETVAL:
+ in-latest-posix: ''
+ FTW_CONTINUE:
+ in-latest-posix: ''
+ FTW_STOP:
+ in-latest-posix: ''
+ FTW_SKIP_SUBTREE:
+ in-latest-posix: ''
+ FTW_SKIP_SIBLINGS:
+ in-latest-posix: ''
+
+functions:
+ ftw:
+ in-latest-posix: ''
+ nftw:
+ in-latest-posix: ''
More information about the libc-commits
mailing list