[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 07:30:38 PDT 2026


https://github.com/kaladron updated https://github.com/llvm/llvm-project/pull/188611

>From 830f6dff27d6ae1d0cc780893354e43cd5074b84 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 1/2] [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         |  69 ++++
 libc/src/ftw/CMakeLists.txt                  |  61 +++
 libc/src/ftw/ftw.cpp                         |  32 ++
 libc/src/ftw/ftw.h                           |  21 +
 libc/src/ftw/ftw_impl.cpp                    | 244 ++++++++++++
 libc/src/ftw/ftw_impl.h                      |  60 +++
 libc/src/ftw/nftw.cpp                        |  36 ++
 libc/src/ftw/nftw.h                          |  21 +
 libc/src/sys/stat/lstat.h                    |   2 +-
 libc/src/sys/stat/stat.h                     |   2 +-
 libc/test/src/CMakeLists.txt                 |   1 +
 libc/test/src/ftw/CMakeLists.txt             |  28 ++
 libc/test/src/ftw/ftw_test.cpp               | 382 +++++++++++++++++++
 libc/test/src/ftw/testdata/CMakeLists.txt    |   4 +
 30 files changed, 1163 insertions(+), 2 deletions(-)
 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

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..fb0ba524cab66
--- /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..5d66aef1c8cd4
--- /dev/null
+++ b/libc/src/__support/File/scoped_dir.h
@@ -0,0 +1,69 @@
+//===-- 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..7ec88922b3d07
--- /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 "hdr/ftw_macros.h"
+#include "src/ftw/ftw.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..553bf3611a627
--- /dev/null
+++ b/libc/src/ftw/ftw_impl.cpp
@@ -0,0 +1,244 @@
+//===-- 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"
+#include "include/llvm-libc-types/struct_FTW.h"
+#include "include/llvm-libc-types/struct_dirent.h"
+#include "include/llvm-libc-types/struct_stat.h"
+
+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.
+  if (Flags & FTW_PHYS) {
+    if (lstat(OsPath, &StatBuf) < 0) {
+      if (libc_errno == EACCES)
+        TypeFlag = FTW_NS;
+      else
+        return cpp::unexpected<int>(libc_errno);
+    }
+  } else {
+    if (stat(OsPath, &StatBuf) < 0) {
+      if (libc_errno == EACCES) {
+        TypeFlag = FTW_NS;
+      } else if (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..6203b46302fd0
--- /dev/null
+++ b/libc/src/ftw/ftw_impl.h
@@ -0,0 +1,60 @@
+//===-- 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"
+#include "include/llvm-libc-types/struct_FTW.h"
+#include "include/llvm-libc-types/struct_stat.h"
+
+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..f32b916b46f2c
--- /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 "src/__support/macros/config.h"
+#include "include/llvm-libc-types/__nftw_func_t.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..319af3418f3ec 100644
--- a/libc/src/sys/stat/lstat.h
+++ b/libc/src/sys/stat/lstat.h
@@ -10,7 +10,7 @@
 #define LLVM_LIBC_SRC_SYS_STAT_LSTAT_H
 
 #include "src/__support/macros/config.h"
-#include <sys/stat.h>
+#include "include/llvm-libc-types/struct_stat.h"
 
 namespace LIBC_NAMESPACE_DECL {
 
diff --git a/libc/src/sys/stat/stat.h b/libc/src/sys/stat/stat.h
index 8ec3e9b1aa9e0..d4cb698020a3e 100644
--- a/libc/src/sys/stat/stat.h
+++ b/libc/src/sys/stat/stat.h
@@ -10,7 +10,7 @@
 #define LLVM_LIBC_SRC_SYS_STAT_STAT_H
 
 #include "src/__support/macros/config.h"
-#include <sys/stat.h>
+#include "include/llvm-libc-types/struct_stat.h"
 
 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..86f277f3982b0
--- /dev/null
+++ b/libc/test/src/ftw/ftw_test.cpp
@@ -0,0 +1,382 @@
+//===-- 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 "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"
+#include "hdr/ftw_macros.h"
+#include "include/llvm-libc-types/struct_FTW.h"
+#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 = 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)

>From bc3c2e346e71965d7a473c919b634a25c204c65c Mon Sep 17 00:00:00 2001
From: Jeff Bailey <jbailey at raspberryginger.com>
Date: Thu, 26 Mar 2026 14:16:58 +0000
Subject: [PATCH 2/2] [libc] Conditionalize include of struct_stat.h and
 struct_FTW.h based on LIBC_FULL_BUILD

---
 libc/src/ftw/ftw_impl.cpp      | 13 ++++++++++---
 libc/src/ftw/ftw_impl.h        |  5 +++++
 libc/src/sys/stat/lstat.h      |  4 ++++
 libc/src/sys/stat/stat.h       |  4 ++++
 libc/test/src/ftw/ftw_test.cpp |  6 +++++-
 5 files changed, 28 insertions(+), 4 deletions(-)

diff --git a/libc/src/ftw/ftw_impl.cpp b/libc/src/ftw/ftw_impl.cpp
index 553bf3611a627..10c841db8bf64 100644
--- a/libc/src/ftw/ftw_impl.cpp
+++ b/libc/src/ftw/ftw_impl.cpp
@@ -22,9 +22,15 @@
 #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 {
@@ -82,18 +88,19 @@ doMergedFtw(const cpp::string &DirPath, const CallbackWrapper &Fn, int FdLimit,
   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 (lstat(OsPath, &StatBuf) < 0) {
+    if (LIBC_NAMESPACE::lstat(OsPath, &StatBuf) < 0) {
       if (libc_errno == EACCES)
         TypeFlag = FTW_NS;
       else
         return cpp::unexpected<int>(libc_errno);
     }
   } else {
-    if (stat(OsPath, &StatBuf) < 0) {
+    if (LIBC_NAMESPACE::stat(OsPath, &StatBuf) < 0) {
       if (libc_errno == EACCES) {
         TypeFlag = FTW_NS;
-      } else if (lstat(OsPath, &StatBuf) == 0) {
+      } else if (LIBC_NAMESPACE::lstat(OsPath, &StatBuf) == 0) {
         // Dangling symlink found.
         TypeFlag = FTW_SLN;
       } else if (libc_errno == EACCES) {
diff --git a/libc/src/ftw/ftw_impl.h b/libc/src/ftw/ftw_impl.h
index 6203b46302fd0..da0f44c0b95ec 100644
--- a/libc/src/ftw/ftw_impl.h
+++ b/libc/src/ftw/ftw_impl.h
@@ -11,8 +11,13 @@
 
 #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 {
diff --git a/libc/src/sys/stat/lstat.h b/libc/src/sys/stat/lstat.h
index 319af3418f3ec..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 d4cb698020a3e..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/ftw/ftw_test.cpp b/libc/test/src/ftw/ftw_test.cpp
index 86f277f3982b0..e62f4d8bb278a 100644
--- a/libc/test/src/ftw/ftw_test.cpp
+++ b/libc/test/src/ftw/ftw_test.cpp
@@ -12,7 +12,11 @@
 #include "src/dirent/readdir.h"
 #include "src/fcntl/open.h"
 #include "hdr/ftw_macros.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"
@@ -132,7 +136,7 @@ TEST_F(LlvmLibcFtwTest, BasicTraversalWithTestData) {
   }
   LIBC_NAMESPACE::closedir(Dir);
 
-  int Result = ftw(libc_make_test_file_path("testdata"), simpleCallback, 10);
+  int Result = LIBC_NAMESPACE::ftw(libc_make_test_file_path("testdata"), simpleCallback, 10);
   ASSERT_EQ(Result, 0);
 }
 



More information about the libc-commits mailing list