[libc-commits] [libc] [libc] Implement basename and dirname in libgen.h (PR #204554)

Jeff Bailey via libc-commits libc-commits at lists.llvm.org
Fri Jun 19 08:34:01 PDT 2026


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

>From 93ff8169c72e369bd8a9d152a7fec0869fd97afe Mon Sep 17 00:00:00 2001
From: Jeff Bailey <jbailey at raspberryginger.com>
Date: Thu, 18 Jun 2026 11:52:41 +0100
Subject: [PATCH 1/5] [libc] Implement basename and dirname in libgen.h

Added the POSIX standard functions basename and dirname under a new
libgen.h header. The implementations modify the input path in-place
using cpp::string_view to determine boundaries safely.

Added find_last_not_of to cpp::string_view to support trailing slash
removal.

Implemented:
* libc/include/libgen.yaml, libgen.h.def: Public API definitions.
* libc/src/libgen/basename.cpp, dirname.cpp: Generic implementations.
* libc/test/src/libgen/: Unit and hermetic tests.

Registered the new entrypoints for all active Linux targets (x86_64,
aarch64, arm, riscv) and added docgen configuration.

Assisted-by: Automated tooling, human reviewed.
---
 libc/config/linux/aarch64/entrypoints.txt |  4 ++
 libc/config/linux/arm/entrypoints.txt     |  4 ++
 libc/config/linux/riscv/entrypoints.txt   |  4 ++
 libc/config/linux/x86_64/entrypoints.txt  |  4 ++
 libc/docs/CMakeLists.txt                  |  1 +
 libc/docs/headers/index.rst               |  1 +
 libc/include/CMakeLists.txt               |  8 +++
 libc/include/libgen.h.def                 | 21 +++++++
 libc/include/libgen.yaml                  | 21 +++++++
 libc/src/CMakeLists.txt                   |  1 +
 libc/src/__support/CPP/string_view.h      |  9 +++
 libc/src/libgen/CMakeLists.txt            | 23 +++++++
 libc/src/libgen/basename.cpp              | 46 ++++++++++++++
 libc/src/libgen/basename.h                | 30 +++++++++
 libc/src/libgen/dirname.cpp               | 54 +++++++++++++++++
 libc/src/libgen/dirname.h                 | 30 +++++++++
 libc/test/src/CMakeLists.txt              |  1 +
 libc/test/src/libgen/CMakeLists.txt       | 21 +++++++
 libc/test/src/libgen/basename_test.cpp    | 62 +++++++++++++++++++
 libc/test/src/libgen/dirname_test.cpp     | 74 +++++++++++++++++++++++
 libc/utils/docgen/libgen.yaml             |  5 ++
 21 files changed, 424 insertions(+)
 create mode 100644 libc/include/libgen.h.def
 create mode 100644 libc/include/libgen.yaml
 create mode 100644 libc/src/libgen/CMakeLists.txt
 create mode 100644 libc/src/libgen/basename.cpp
 create mode 100644 libc/src/libgen/basename.h
 create mode 100644 libc/src/libgen/dirname.cpp
 create mode 100644 libc/src/libgen/dirname.h
 create mode 100644 libc/test/src/libgen/CMakeLists.txt
 create mode 100644 libc/test/src/libgen/basename_test.cpp
 create mode 100644 libc/test/src/libgen/dirname_test.cpp
 create mode 100644 libc/utils/docgen/libgen.yaml

diff --git a/libc/config/linux/aarch64/entrypoints.txt b/libc/config/linux/aarch64/entrypoints.txt
index 5cddf3dc89799..3072c3d22aa5f 100644
--- a/libc/config/linux/aarch64/entrypoints.txt
+++ b/libc/config/linux/aarch64/entrypoints.txt
@@ -109,6 +109,10 @@ set(TARGET_LIBC_ENTRYPOINTS
     libc.src.inttypes.strtoimax
     libc.src.inttypes.strtoumax
 
+    # libgen.h entrypoints
+    libc.src.libgen.basename
+    libc.src.libgen.dirname
+
     # stdbit.h entrypoints
     libc.src.stdbit.stdc_bit_ceil_uc
     libc.src.stdbit.stdc_bit_ceil_ui
diff --git a/libc/config/linux/arm/entrypoints.txt b/libc/config/linux/arm/entrypoints.txt
index c4ac53c4925a3..805738a3a5756 100644
--- a/libc/config/linux/arm/entrypoints.txt
+++ b/libc/config/linux/arm/entrypoints.txt
@@ -73,6 +73,10 @@ set(TARGET_LIBC_ENTRYPOINTS
     libc.src.inttypes.strtoimax
     libc.src.inttypes.strtoumax
 
+    # libgen.h entrypoints
+    libc.src.libgen.basename
+    libc.src.libgen.dirname
+
     # stdbit.h entrypoints
     libc.src.stdbit.stdc_bit_ceil_uc
     libc.src.stdbit.stdc_bit_ceil_ui
diff --git a/libc/config/linux/riscv/entrypoints.txt b/libc/config/linux/riscv/entrypoints.txt
index a57efbb8e464d..bcdcf2320f7bd 100644
--- a/libc/config/linux/riscv/entrypoints.txt
+++ b/libc/config/linux/riscv/entrypoints.txt
@@ -109,6 +109,10 @@ set(TARGET_LIBC_ENTRYPOINTS
     libc.src.inttypes.strtoimax
     libc.src.inttypes.strtoumax
 
+    # libgen.h entrypoints
+    libc.src.libgen.basename
+    libc.src.libgen.dirname
+
     # stdbit.h entrypoints
     libc.src.stdbit.stdc_bit_ceil_uc
     libc.src.stdbit.stdc_bit_ceil_ui
diff --git a/libc/config/linux/x86_64/entrypoints.txt b/libc/config/linux/x86_64/entrypoints.txt
index ce88a6749d9dc..f19da5902bba1 100644
--- a/libc/config/linux/x86_64/entrypoints.txt
+++ b/libc/config/linux/x86_64/entrypoints.txt
@@ -125,6 +125,10 @@ set(TARGET_LIBC_ENTRYPOINTS
     libc.src.inttypes.wcstoimax
     libc.src.inttypes.wcstoumax
 
+    # libgen.h entrypoints
+    libc.src.libgen.basename
+    libc.src.libgen.dirname
+
     # stdbit.h entrypoints
     libc.src.stdbit.stdc_bit_ceil_uc
     libc.src.stdbit.stdc_bit_ceil_ui
diff --git a/libc/docs/CMakeLists.txt b/libc/docs/CMakeLists.txt
index cf54edeae66de..ded99393f9390 100644
--- a/libc/docs/CMakeLists.txt
+++ b/libc/docs/CMakeLists.txt
@@ -54,6 +54,7 @@ if (SPHINX_FOUND)
       float
       glob
       inttypes
+      libgen
       locale
       nl_types
       net/if
diff --git a/libc/docs/headers/index.rst b/libc/docs/headers/index.rst
index e818e1549c0d1..893ddf839cccd 100644
--- a/libc/docs/headers/index.rst
+++ b/libc/docs/headers/index.rst
@@ -20,6 +20,7 @@ Implementation Status
    float
    glob
    inttypes
+   libgen
    locale
    math/index.rst
    net/if
diff --git a/libc/include/CMakeLists.txt b/libc/include/CMakeLists.txt
index 549dbd9e4c3f8..e8168687109b0 100644
--- a/libc/include/CMakeLists.txt
+++ b/libc/include/CMakeLists.txt
@@ -130,6 +130,14 @@ add_header_macro(
     .llvm-libc-macros.float_macros
 )
 
+add_header_macro(
+  libgen
+  ../libc/include/libgen.yaml
+  libgen.h
+  DEPENDS
+    .llvm_libc_common_h
+)
+
 add_header_macro(
   limits
   ../libc/include/limits.yaml
diff --git a/libc/include/libgen.h.def b/libc/include/libgen.h.def
new file mode 100644
index 0000000000000..dd0dee8ed55fe
--- /dev/null
+++ b/libc/include/libgen.h.def
@@ -0,0 +1,21 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+///
+/// \file
+/// C standard library header libgen.h.
+///
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_LIBC_LIBGEN_H
+#define LLVM_LIBC_LIBGEN_H
+
+#include "__llvm-libc-common.h"
+
+%%public_api()
+
+#endif // LLVM_LIBC_LIBGEN_H
diff --git a/libc/include/libgen.yaml b/libc/include/libgen.yaml
new file mode 100644
index 0000000000000..57dd92a627644
--- /dev/null
+++ b/libc/include/libgen.yaml
@@ -0,0 +1,21 @@
+header: libgen.h
+header_template: libgen.h.def
+standards:
+  - posix
+macros: []
+types: []
+enums: []
+objects: []
+functions:
+  - name: basename
+    standards:
+      - posix
+    return_type: char *
+    arguments:
+      - type: char *
+  - name: dirname
+    standards:
+      - posix
+    return_type: char *
+    arguments:
+      - type: char *
diff --git a/libc/src/CMakeLists.txt b/libc/src/CMakeLists.txt
index 9db314f54723b..56085c9632f59 100644
--- a/libc/src/CMakeLists.txt
+++ b/libc/src/CMakeLists.txt
@@ -7,6 +7,7 @@ add_subdirectory(dlfcn)
 add_subdirectory(errno)
 add_subdirectory(fenv)
 add_subdirectory(inttypes)
+add_subdirectory(libgen)
 add_subdirectory(link)
 add_subdirectory(math)
 add_subdirectory(netinet)
diff --git a/libc/src/__support/CPP/string_view.h b/libc/src/__support/CPP/string_view.h
index 6991fd46a4ace..7b98b7e5fb6c8 100644
--- a/libc/src/__support/CPP/string_view.h
+++ b/libc/src/__support/CPP/string_view.h
@@ -205,6 +205,15 @@ class string_view {
     return npos;
   }
 
+  LIBC_INLINE constexpr size_t find_last_not_of(const char c,
+                                                size_t end = npos) const {
+    end = end >= size() ? size() : end + 1;
+    for (; end > 0; --end)
+      if ((*this)[end - 1] != c)
+        return end - 1;
+    return npos;
+  }
+
   // Finds the first character not equal to c in this view, starting at
   // position From.
   LIBC_INLINE constexpr size_t find_first_not_of(const char c,
diff --git a/libc/src/libgen/CMakeLists.txt b/libc/src/libgen/CMakeLists.txt
new file mode 100644
index 0000000000000..9c315a4e7b41d
--- /dev/null
+++ b/libc/src/libgen/CMakeLists.txt
@@ -0,0 +1,23 @@
+add_entrypoint_object(
+  basename
+  SRCS
+    basename.cpp
+  HDRS
+    basename.h
+  DEPENDS
+    libc.src.__support.CPP.string_view
+    libc.src.__support.common
+    libc.src.__support.macros.config
+)
+
+add_entrypoint_object(
+  dirname
+  SRCS
+    dirname.cpp
+  HDRS
+    dirname.h
+  DEPENDS
+    libc.src.__support.CPP.string_view
+    libc.src.__support.common
+    libc.src.__support.macros.config
+)
diff --git a/libc/src/libgen/basename.cpp b/libc/src/libgen/basename.cpp
new file mode 100644
index 0000000000000..9c4b4b961ceb1
--- /dev/null
+++ b/libc/src/libgen/basename.cpp
@@ -0,0 +1,46 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+///
+/// \file
+/// Implementation of basename.
+///
+//===----------------------------------------------------------------------===//
+
+#include "src/libgen/basename.h"
+#include "src/__support/CPP/string_view.h"
+#include "src/__support/common.h"
+#include "src/__support/macros/config.h"
+
+namespace LIBC_NAMESPACE_DECL {
+
+LLVM_LIBC_FUNCTION(char *, basename, (char *path)) {
+  if (path == nullptr || path[0] == '\0') {
+    static char dot[] = ".";
+    return dot;
+  }
+
+  cpp::string_view sv(path);
+  size_t last_non_slash = sv.find_last_not_of('/');
+
+  if (last_non_slash == cpp::string_view::npos) {
+    static char slash[] = "/";
+    return slash;
+  }
+
+  size_t last_slash = sv.substr(0, last_non_slash).find_last_of('/');
+
+  size_t start = (last_slash == cpp::string_view::npos) ? 0 : last_slash + 1;
+  size_t end = last_non_slash + 1;
+
+  if (end < sv.size())
+    path[end] = '\0';
+
+  return path + start;
+}
+
+} // namespace LIBC_NAMESPACE_DECL
diff --git a/libc/src/libgen/basename.h b/libc/src/libgen/basename.h
new file mode 100644
index 0000000000000..15239888aa97a
--- /dev/null
+++ b/libc/src/libgen/basename.h
@@ -0,0 +1,30 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+///
+/// \file
+/// Header for basename.
+///
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_LIBC_SRC_LIBGEN_BASENAME_H
+#define LLVM_LIBC_SRC_LIBGEN_BASENAME_H
+
+#include "src/__support/macros/config.h"
+
+namespace LIBC_NAMESPACE_DECL {
+
+/// Return the last component of a pathname.
+///
+/// \param path Pointer to the null-terminated pathname string.
+/// \return Pointer to the last component of path, or "." if path is null or
+/// empty, or "/" if path is all slashes.
+char *basename(char *path);
+
+} // namespace LIBC_NAMESPACE_DECL
+
+#endif // LLVM_LIBC_SRC_LIBGEN_BASENAME_H
diff --git a/libc/src/libgen/dirname.cpp b/libc/src/libgen/dirname.cpp
new file mode 100644
index 0000000000000..4cc74a038a2d6
--- /dev/null
+++ b/libc/src/libgen/dirname.cpp
@@ -0,0 +1,54 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+///
+/// \file
+/// Implementation of dirname.
+///
+//===----------------------------------------------------------------------===//
+
+#include "src/libgen/dirname.h"
+#include "src/__support/CPP/string_view.h"
+#include "src/__support/common.h"
+#include "src/__support/macros/config.h"
+
+namespace LIBC_NAMESPACE_DECL {
+
+LLVM_LIBC_FUNCTION(char *, dirname, (char *path)) {
+  if (path == nullptr || path[0] == '\0') {
+    static char dot[] = ".";
+    return dot;
+  }
+
+  cpp::string_view sv(path);
+  size_t last_non_slash = sv.find_last_not_of('/');
+
+  if (last_non_slash == cpp::string_view::npos) {
+    static char slash[] = "/";
+    return slash;
+  }
+
+  size_t last_slash = sv.substr(0, last_non_slash).find_last_of('/');
+
+  if (last_slash == cpp::string_view::npos) {
+    static char dot[] = ".";
+    return dot;
+  }
+
+  cpp::string_view dir_sv = sv.substr(0, last_slash);
+  size_t dir_last_non_slash = dir_sv.find_last_not_of('/');
+
+  if (dir_last_non_slash == cpp::string_view::npos) {
+    path[1] = '\0';
+    return path;
+  }
+
+  path[dir_last_non_slash + 1] = '\0';
+  return path;
+}
+
+} // namespace LIBC_NAMESPACE_DECL
diff --git a/libc/src/libgen/dirname.h b/libc/src/libgen/dirname.h
new file mode 100644
index 0000000000000..4909b2eb222ad
--- /dev/null
+++ b/libc/src/libgen/dirname.h
@@ -0,0 +1,30 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+///
+/// \file
+/// Header for dirname.
+///
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_LIBC_SRC_LIBGEN_DIRNAME_H
+#define LLVM_LIBC_SRC_LIBGEN_DIRNAME_H
+
+#include "src/__support/macros/config.h"
+
+namespace LIBC_NAMESPACE_DECL {
+
+/// Return the directory component of a pathname.
+///
+/// \param path Pointer to the null-terminated pathname string.
+/// \return Pointer to the directory component of path, or "." if path is null
+/// or empty, or "/" if path is all slashes.
+char *dirname(char *path);
+
+} // namespace LIBC_NAMESPACE_DECL
+
+#endif // LLVM_LIBC_SRC_LIBGEN_DIRNAME_H
diff --git a/libc/test/src/CMakeLists.txt b/libc/test/src/CMakeLists.txt
index dd232b29a7a7b..45815c9bba8ca 100644
--- a/libc/test/src/CMakeLists.txt
+++ b/libc/test/src/CMakeLists.txt
@@ -63,6 +63,7 @@ add_subdirectory(complex)
 add_subdirectory(ctype)
 add_subdirectory(errno)
 add_subdirectory(fenv)
+add_subdirectory(libgen)
 add_subdirectory(link)
 add_subdirectory(math)
 add_subdirectory(netinet)
diff --git a/libc/test/src/libgen/CMakeLists.txt b/libc/test/src/libgen/CMakeLists.txt
new file mode 100644
index 0000000000000..1d2cab3ad61d8
--- /dev/null
+++ b/libc/test/src/libgen/CMakeLists.txt
@@ -0,0 +1,21 @@
+add_custom_target(libc-libgen-tests)
+
+add_libc_test(
+  basename_test
+  SUITE
+    libc-libgen-tests
+  SRCS
+    basename_test.cpp
+  DEPENDS
+    libc.src.libgen.basename
+)
+
+add_libc_test(
+  dirname_test
+  SUITE
+    libc-libgen-tests
+  SRCS
+    dirname_test.cpp
+  DEPENDS
+    libc.src.libgen.dirname
+)
diff --git a/libc/test/src/libgen/basename_test.cpp b/libc/test/src/libgen/basename_test.cpp
new file mode 100644
index 0000000000000..2e8feef715137
--- /dev/null
+++ b/libc/test/src/libgen/basename_test.cpp
@@ -0,0 +1,62 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+///
+/// \file
+/// Unittests for basename.
+///
+//===----------------------------------------------------------------------===//
+
+#include "src/libgen/basename.h"
+#include "test/UnitTest/Test.h"
+
+TEST(LlvmLibcBasenameTest, NullPointer) {
+  ASSERT_STREQ(LIBC_NAMESPACE::basename(nullptr), ".");
+}
+
+TEST(LlvmLibcBasenameTest, EmptyString) {
+  char path[] = "";
+  ASSERT_STREQ(LIBC_NAMESPACE::basename(path), ".");
+}
+
+TEST(LlvmLibcBasenameTest, RegularPath) {
+  char path[] = "/usr/lib";
+  ASSERT_STREQ(LIBC_NAMESPACE::basename(path), "lib");
+}
+
+TEST(LlvmLibcBasenameTest, TrailingSlash) {
+  char path[] = "/usr/";
+  ASSERT_STREQ(LIBC_NAMESPACE::basename(path), "usr");
+  ASSERT_STREQ(path, "/usr");
+}
+
+TEST(LlvmLibcBasenameTest, SingleSlash) {
+  char path[] = "/";
+  ASSERT_STREQ(LIBC_NAMESPACE::basename(path), "/");
+}
+
+TEST(LlvmLibcBasenameTest, MultipleSlashes) {
+  char path[] = "///";
+  ASSERT_STREQ(LIBC_NAMESPACE::basename(path), "/");
+}
+
+TEST(LlvmLibcBasenameTest, SimpleName) {
+  char path[] = "a";
+  ASSERT_STREQ(LIBC_NAMESPACE::basename(path), "a");
+}
+
+TEST(LlvmLibcBasenameTest, SimpleNameTrailingSlash) {
+  char path[] = "a/";
+  ASSERT_STREQ(LIBC_NAMESPACE::basename(path), "a");
+  ASSERT_STREQ(path, "a");
+}
+
+TEST(LlvmLibcBasenameTest, ComplexPath) {
+  char path[] = "///a///";
+  ASSERT_STREQ(LIBC_NAMESPACE::basename(path), "a");
+  ASSERT_STREQ(path, "///a");
+}
diff --git a/libc/test/src/libgen/dirname_test.cpp b/libc/test/src/libgen/dirname_test.cpp
new file mode 100644
index 0000000000000..afd718fb6e559
--- /dev/null
+++ b/libc/test/src/libgen/dirname_test.cpp
@@ -0,0 +1,74 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+///
+/// \file
+/// Unittests for dirname.
+///
+//===----------------------------------------------------------------------===//
+
+#include "src/libgen/dirname.h"
+#include "test/UnitTest/Test.h"
+
+TEST(LlvmLibcDirnameTest, NullPointer) {
+  ASSERT_STREQ(LIBC_NAMESPACE::dirname(nullptr), ".");
+}
+
+TEST(LlvmLibcDirnameTest, EmptyString) {
+  char path[] = "";
+  ASSERT_STREQ(LIBC_NAMESPACE::dirname(path), ".");
+}
+
+TEST(LlvmLibcDirnameTest, RegularPath) {
+  char path[] = "/usr/lib";
+  ASSERT_STREQ(LIBC_NAMESPACE::dirname(path), "/usr");
+  ASSERT_STREQ(path, "/usr");
+}
+
+TEST(LlvmLibcDirnameTest, TrailingSlash) {
+  char path[] = "/usr/";
+  ASSERT_STREQ(LIBC_NAMESPACE::dirname(path), "/");
+  ASSERT_STREQ(path, "/");
+}
+
+TEST(LlvmLibcDirnameTest, SingleSlash) {
+  char path[] = "/";
+  ASSERT_STREQ(LIBC_NAMESPACE::dirname(path), "/");
+}
+
+TEST(LlvmLibcDirnameTest, MultipleSlashes) {
+  char path[] = "///";
+  ASSERT_STREQ(LIBC_NAMESPACE::dirname(path), "/");
+}
+
+TEST(LlvmLibcDirnameTest, SimpleName) {
+  char path[] = "a";
+  ASSERT_STREQ(LIBC_NAMESPACE::dirname(path), ".");
+}
+
+TEST(LlvmLibcDirnameTest, SimpleNameTrailingSlash) {
+  char path[] = "a/";
+  ASSERT_STREQ(LIBC_NAMESPACE::dirname(path), ".");
+}
+
+TEST(LlvmLibcDirnameTest, ComplexPath) {
+  char path[] = "///a///b///";
+  ASSERT_STREQ(LIBC_NAMESPACE::dirname(path), "///a");
+  ASSERT_STREQ(path, "///a");
+}
+
+TEST(LlvmLibcDirnameTest, SlashA) {
+  char path[] = "/a";
+  ASSERT_STREQ(LIBC_NAMESPACE::dirname(path), "/");
+  ASSERT_STREQ(path, "/");
+}
+
+TEST(LlvmLibcDirnameTest, MultipleSlashesA) {
+  char path[] = "///a";
+  ASSERT_STREQ(LIBC_NAMESPACE::dirname(path), "/");
+  ASSERT_STREQ(path, "/");
+}
diff --git a/libc/utils/docgen/libgen.yaml b/libc/utils/docgen/libgen.yaml
new file mode 100644
index 0000000000000..07aad5f1be55c
--- /dev/null
+++ b/libc/utils/docgen/libgen.yaml
@@ -0,0 +1,5 @@
+functions:
+  basename:
+    in-latest-posix: ''
+  dirname:
+    in-latest-posix: ''

>From 05258d39ddfa1c8fb056d5fe59eb403c0bd118f0 Mon Sep 17 00:00:00 2001
From: Jeff Bailey <jbailey at raspberryginger.com>
Date: Thu, 18 Jun 2026 15:28:45 +0100
Subject: [PATCH 2/5] [libc] Remove redundant libgen.h.def template

The libgen.h header does not require custom macros or template content.
Removing the template and relying on the default hdrgen template
simplifies the source tree.

Assisted-by: Automated tooling, human reviewed.
---
 libc/include/libgen.h.def | 21 ---------------------
 libc/include/libgen.yaml  |  1 -
 2 files changed, 22 deletions(-)
 delete mode 100644 libc/include/libgen.h.def

diff --git a/libc/include/libgen.h.def b/libc/include/libgen.h.def
deleted file mode 100644
index dd0dee8ed55fe..0000000000000
--- a/libc/include/libgen.h.def
+++ /dev/null
@@ -1,21 +0,0 @@
-//===----------------------------------------------------------------------===//
-//
-// 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
-//
-//===----------------------------------------------------------------------===//
-///
-/// \file
-/// C standard library header libgen.h.
-///
-//===----------------------------------------------------------------------===//
-
-#ifndef LLVM_LIBC_LIBGEN_H
-#define LLVM_LIBC_LIBGEN_H
-
-#include "__llvm-libc-common.h"
-
-%%public_api()
-
-#endif // LLVM_LIBC_LIBGEN_H
diff --git a/libc/include/libgen.yaml b/libc/include/libgen.yaml
index 57dd92a627644..c79ab79259be6 100644
--- a/libc/include/libgen.yaml
+++ b/libc/include/libgen.yaml
@@ -1,5 +1,4 @@
 header: libgen.h
-header_template: libgen.h.def
 standards:
   - posix
 macros: []

>From 31dc156aba91fa1f0a8e9f51652f6a2e9d953b61 Mon Sep 17 00:00:00 2001
From: Jeff Bailey <jbailey at raspberryginger.com>
Date: Thu, 18 Jun 2026 17:29:12 +0100
Subject: [PATCH 3/5] [libc] Return const_cast'ed string literals for fallback
 paths

Change basename and dirname to return const_cast<char *>(".") and
const_cast<char *>("/") for fallback paths instead of static arrays.
This prevents callers from mutating the shared static buffers.

Add unit tests checking that modifying the returned pointers results in
a crash. Subprocess tests are disabled for hermetic tests to avoid
linker issues.

Assisted-by: Automated tooling, human reviewed.
---
 libc/src/libgen/basename.cpp           | 12 ++++--------
 libc/src/libgen/dirname.cpp            | 18 ++++++------------
 libc/test/UnitTest/PlatformDefs.h      | 10 +++++++++-
 libc/test/src/libgen/basename_test.cpp |  7 +++++++
 libc/test/src/libgen/dirname_test.cpp  |  7 +++++++
 5 files changed, 33 insertions(+), 21 deletions(-)

diff --git a/libc/src/libgen/basename.cpp b/libc/src/libgen/basename.cpp
index 9c4b4b961ceb1..acf53f2d446e9 100644
--- a/libc/src/libgen/basename.cpp
+++ b/libc/src/libgen/basename.cpp
@@ -19,18 +19,14 @@
 namespace LIBC_NAMESPACE_DECL {
 
 LLVM_LIBC_FUNCTION(char *, basename, (char *path)) {
-  if (path == nullptr || path[0] == '\0') {
-    static char dot[] = ".";
-    return dot;
-  }
+  if (path == nullptr || path[0] == '\0')
+    return const_cast<char *>(".");
 
   cpp::string_view sv(path);
   size_t last_non_slash = sv.find_last_not_of('/');
 
-  if (last_non_slash == cpp::string_view::npos) {
-    static char slash[] = "/";
-    return slash;
-  }
+  if (last_non_slash == cpp::string_view::npos)
+    return const_cast<char *>("/");
 
   size_t last_slash = sv.substr(0, last_non_slash).find_last_of('/');
 
diff --git a/libc/src/libgen/dirname.cpp b/libc/src/libgen/dirname.cpp
index 4cc74a038a2d6..9dd958b63ce9f 100644
--- a/libc/src/libgen/dirname.cpp
+++ b/libc/src/libgen/dirname.cpp
@@ -19,25 +19,19 @@
 namespace LIBC_NAMESPACE_DECL {
 
 LLVM_LIBC_FUNCTION(char *, dirname, (char *path)) {
-  if (path == nullptr || path[0] == '\0') {
-    static char dot[] = ".";
-    return dot;
-  }
+  if (path == nullptr || path[0] == '\0')
+    return const_cast<char *>(".");
 
   cpp::string_view sv(path);
   size_t last_non_slash = sv.find_last_not_of('/');
 
-  if (last_non_slash == cpp::string_view::npos) {
-    static char slash[] = "/";
-    return slash;
-  }
+  if (last_non_slash == cpp::string_view::npos)
+    return const_cast<char *>("/");
 
   size_t last_slash = sv.substr(0, last_non_slash).find_last_of('/');
 
-  if (last_slash == cpp::string_view::npos) {
-    static char dot[] = ".";
-    return dot;
-  }
+  if (last_slash == cpp::string_view::npos)
+    return const_cast<char *>(".");
 
   cpp::string_view dir_sv = sv.substr(0, last_slash);
   size_t dir_last_non_slash = dir_sv.find_last_not_of('/');
diff --git a/libc/test/UnitTest/PlatformDefs.h b/libc/test/UnitTest/PlatformDefs.h
index f9911b1557698..72ee3d330aae7 100644
--- a/libc/test/UnitTest/PlatformDefs.h
+++ b/libc/test/UnitTest/PlatformDefs.h
@@ -9,7 +9,15 @@
 #ifndef LLVM_LIBC_TEST_UNITTEST_PLATFORMDEFS_H
 #define LLVM_LIBC_TEST_UNITTEST_PLATFORMDEFS_H
 
-#if !defined(_WIN32)
+#define LIBC_TEST_UNIT 1
+#define LIBC_TEST_HERMETIC 2
+
+#define CONCAT_HELPER(a, b) a ## b
+#define CONCAT(a, b) CONCAT_HELPER(a, b)
+
+#define CHECK_TEST_TYPE(type) CONCAT(LIBC_TEST_, type)
+
+#if !defined(_WIN32) && (CHECK_TEST_TYPE(LIBC_TEST) != LIBC_TEST_HERMETIC)
 #define ENABLE_SUBPROCESS_TESTS
 #endif
 
diff --git a/libc/test/src/libgen/basename_test.cpp b/libc/test/src/libgen/basename_test.cpp
index 2e8feef715137..b402a51296df0 100644
--- a/libc/test/src/libgen/basename_test.cpp
+++ b/libc/test/src/libgen/basename_test.cpp
@@ -60,3 +60,10 @@ TEST(LlvmLibcBasenameTest, ComplexPath) {
   ASSERT_STREQ(LIBC_NAMESPACE::basename(path), "a");
   ASSERT_STREQ(path, "///a");
 }
+
+#ifdef ENABLE_SUBPROCESS_TESTS
+TEST(LlvmLibcBasenameTest, ModifyReturnValue) {
+  char *r = LIBC_NAMESPACE::basename(nullptr);
+  ASSERT_DEATH([r]() { r[0] = 'a'; }, WITH_SIGNAL(-1));
+}
+#endif
diff --git a/libc/test/src/libgen/dirname_test.cpp b/libc/test/src/libgen/dirname_test.cpp
index afd718fb6e559..3db49aa58c26d 100644
--- a/libc/test/src/libgen/dirname_test.cpp
+++ b/libc/test/src/libgen/dirname_test.cpp
@@ -72,3 +72,10 @@ TEST(LlvmLibcDirnameTest, MultipleSlashesA) {
   ASSERT_STREQ(LIBC_NAMESPACE::dirname(path), "/");
   ASSERT_STREQ(path, "/");
 }
+
+#ifdef ENABLE_SUBPROCESS_TESTS
+TEST(LlvmLibcDirnameTest, ModifyReturnValue) {
+  char *r = LIBC_NAMESPACE::dirname(nullptr);
+  ASSERT_DEATH([r]() { r[0] = 'a'; }, WITH_SIGNAL(-1));
+}
+#endif

>From ab490f9d70e4baa5926c24b47a9790e464b90617 Mon Sep 17 00:00:00 2001
From: Jeff Bailey <jbailey at raspberryginger.com>
Date: Thu, 18 Jun 2026 17:41:12 +0100
Subject: [PATCH 4/5] [libc] Split death tests into separate targets

Move basename and dirname death tests to separate files and define them
as UNIT_TEST_ONLY targets in CMake. This avoids linking issues in
hermetic mode by not compiling death tests for hermetic runs.
Revert changes to PlatformDefs.h.

Assisted-by: Automated tooling, human reviewed.
---
 libc/test/UnitTest/PlatformDefs.h            | 10 +--------
 libc/test/src/libgen/CMakeLists.txt          | 22 ++++++++++++++++++++
 libc/test/src/libgen/basename_death_test.cpp | 17 +++++++++++++++
 libc/test/src/libgen/basename_test.cpp       |  7 -------
 libc/test/src/libgen/dirname_death_test.cpp  | 17 +++++++++++++++
 libc/test/src/libgen/dirname_test.cpp        |  7 -------
 6 files changed, 57 insertions(+), 23 deletions(-)
 create mode 100644 libc/test/src/libgen/basename_death_test.cpp
 create mode 100644 libc/test/src/libgen/dirname_death_test.cpp

diff --git a/libc/test/UnitTest/PlatformDefs.h b/libc/test/UnitTest/PlatformDefs.h
index 72ee3d330aae7..f9911b1557698 100644
--- a/libc/test/UnitTest/PlatformDefs.h
+++ b/libc/test/UnitTest/PlatformDefs.h
@@ -9,15 +9,7 @@
 #ifndef LLVM_LIBC_TEST_UNITTEST_PLATFORMDEFS_H
 #define LLVM_LIBC_TEST_UNITTEST_PLATFORMDEFS_H
 
-#define LIBC_TEST_UNIT 1
-#define LIBC_TEST_HERMETIC 2
-
-#define CONCAT_HELPER(a, b) a ## b
-#define CONCAT(a, b) CONCAT_HELPER(a, b)
-
-#define CHECK_TEST_TYPE(type) CONCAT(LIBC_TEST_, type)
-
-#if !defined(_WIN32) && (CHECK_TEST_TYPE(LIBC_TEST) != LIBC_TEST_HERMETIC)
+#if !defined(_WIN32)
 #define ENABLE_SUBPROCESS_TESTS
 #endif
 
diff --git a/libc/test/src/libgen/CMakeLists.txt b/libc/test/src/libgen/CMakeLists.txt
index 1d2cab3ad61d8..d3ee13a9f1cab 100644
--- a/libc/test/src/libgen/CMakeLists.txt
+++ b/libc/test/src/libgen/CMakeLists.txt
@@ -19,3 +19,25 @@ add_libc_test(
   DEPENDS
     libc.src.libgen.dirname
 )
+
+add_libc_test(
+  basename_death_test
+  UNIT_TEST_ONLY
+  SUITE
+    libc-libgen-tests
+  SRCS
+    basename_death_test.cpp
+  DEPENDS
+    libc.src.libgen.basename
+)
+
+add_libc_test(
+  dirname_death_test
+  UNIT_TEST_ONLY
+  SUITE
+    libc-libgen-tests
+  SRCS
+    dirname_death_test.cpp
+  DEPENDS
+    libc.src.libgen.dirname
+)
diff --git a/libc/test/src/libgen/basename_death_test.cpp b/libc/test/src/libgen/basename_death_test.cpp
new file mode 100644
index 0000000000000..cacf25dfa5f61
--- /dev/null
+++ b/libc/test/src/libgen/basename_death_test.cpp
@@ -0,0 +1,17 @@
+//===-- Death tests for basename ------------------------------------------===//
+//
+// 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/libgen/basename.h"
+#include "test/UnitTest/Test.h"
+
+#ifdef ENABLE_SUBPROCESS_TESTS
+TEST(LlvmLibcBasenameTest, ModifyReturnValue) {
+  char *r = LIBC_NAMESPACE::basename(nullptr);
+  ASSERT_DEATH([r]() { r[0] = 'a'; }, WITH_SIGNAL(-1));
+}
+#endif
diff --git a/libc/test/src/libgen/basename_test.cpp b/libc/test/src/libgen/basename_test.cpp
index b402a51296df0..2e8feef715137 100644
--- a/libc/test/src/libgen/basename_test.cpp
+++ b/libc/test/src/libgen/basename_test.cpp
@@ -60,10 +60,3 @@ TEST(LlvmLibcBasenameTest, ComplexPath) {
   ASSERT_STREQ(LIBC_NAMESPACE::basename(path), "a");
   ASSERT_STREQ(path, "///a");
 }
-
-#ifdef ENABLE_SUBPROCESS_TESTS
-TEST(LlvmLibcBasenameTest, ModifyReturnValue) {
-  char *r = LIBC_NAMESPACE::basename(nullptr);
-  ASSERT_DEATH([r]() { r[0] = 'a'; }, WITH_SIGNAL(-1));
-}
-#endif
diff --git a/libc/test/src/libgen/dirname_death_test.cpp b/libc/test/src/libgen/dirname_death_test.cpp
new file mode 100644
index 0000000000000..6501ee771eaec
--- /dev/null
+++ b/libc/test/src/libgen/dirname_death_test.cpp
@@ -0,0 +1,17 @@
+//===-- Death tests for dirname -------------------------------------------===//
+//
+// 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/libgen/dirname.h"
+#include "test/UnitTest/Test.h"
+
+#ifdef ENABLE_SUBPROCESS_TESTS
+TEST(LlvmLibcDirnameTest, ModifyReturnValue) {
+  char *r = LIBC_NAMESPACE::dirname(nullptr);
+  ASSERT_DEATH([r]() { r[0] = 'a'; }, WITH_SIGNAL(-1));
+}
+#endif
diff --git a/libc/test/src/libgen/dirname_test.cpp b/libc/test/src/libgen/dirname_test.cpp
index 3db49aa58c26d..afd718fb6e559 100644
--- a/libc/test/src/libgen/dirname_test.cpp
+++ b/libc/test/src/libgen/dirname_test.cpp
@@ -72,10 +72,3 @@ TEST(LlvmLibcDirnameTest, MultipleSlashesA) {
   ASSERT_STREQ(LIBC_NAMESPACE::dirname(path), "/");
   ASSERT_STREQ(path, "/");
 }
-
-#ifdef ENABLE_SUBPROCESS_TESTS
-TEST(LlvmLibcDirnameTest, ModifyReturnValue) {
-  char *r = LIBC_NAMESPACE::dirname(nullptr);
-  ASSERT_DEATH([r]() { r[0] = 'a'; }, WITH_SIGNAL(-1));
-}
-#endif

>From 3ec2540bac669616d536c5e47d9fb97ca8422f1c Mon Sep 17 00:00:00 2001
From: Jeff Bailey <jbailey at raspberryginger.com>
Date: Fri, 19 Jun 2026 16:33:44 +0100
Subject: [PATCH 5/5] [libc][NFC] Update header style for libgen death tests

Update the file headers in basename_death_test.cpp and
dirname_death_test.cpp to use the standard three-section LLVM format
with Doxygen \file blocks.

Assisted-by: Automated tooling, human reviewed.
---
 libc/test/src/libgen/basename_death_test.cpp | 7 ++++++-
 libc/test/src/libgen/dirname_death_test.cpp  | 7 ++++++-
 2 files changed, 12 insertions(+), 2 deletions(-)

diff --git a/libc/test/src/libgen/basename_death_test.cpp b/libc/test/src/libgen/basename_death_test.cpp
index cacf25dfa5f61..24100dc48aa1b 100644
--- a/libc/test/src/libgen/basename_death_test.cpp
+++ b/libc/test/src/libgen/basename_death_test.cpp
@@ -1,10 +1,15 @@
-//===-- Death tests for basename ------------------------------------------===//
+//===----------------------------------------------------------------------===//
 //
 // 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
 //
 //===----------------------------------------------------------------------===//
+///
+/// \file
+/// Death tests for basename.
+///
+//===----------------------------------------------------------------------===//
 
 #include "src/libgen/basename.h"
 #include "test/UnitTest/Test.h"
diff --git a/libc/test/src/libgen/dirname_death_test.cpp b/libc/test/src/libgen/dirname_death_test.cpp
index 6501ee771eaec..e135a4ce60cd6 100644
--- a/libc/test/src/libgen/dirname_death_test.cpp
+++ b/libc/test/src/libgen/dirname_death_test.cpp
@@ -1,10 +1,15 @@
-//===-- Death tests for dirname -------------------------------------------===//
+//===----------------------------------------------------------------------===//
 //
 // 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
 //
 //===----------------------------------------------------------------------===//
+///
+/// \file
+/// Death tests for dirname.
+///
+//===----------------------------------------------------------------------===//
 
 #include "src/libgen/dirname.h"
 #include "test/UnitTest/Test.h"



More information about the libc-commits mailing list