[libc-commits] [libc] [libc][realpath] Implement symbolic path resolution (PR #204467)
Jackson Stogel via libc-commits
libc-commits at lists.llvm.org
Mon Jun 29 15:45:59 PDT 2026
https://github.com/jtstogel updated https://github.com/llvm/llvm-project/pull/204467
>From 5a867eb371bfc52da10103ff436191d9a96314d4 Mon Sep 17 00:00:00 2001
From: jtstogel <jtstogel at google.com>
Date: Tue, 16 Jun 2026 19:43:30 -0700
Subject: [PATCH 01/10] [libc][realpath] Implement symbolic path resolution
This is an incremental implementation of realpath. It performs no syscalls. A real implementation would perform getcwd/statx/readlinkat to actually resolve symlinks / valid that paths exist / etc.
For testing purposes, I've added the entrypoint when `LLVM_LIBC_ENABLE_EXPERIMENTAL_ENTRYPOINTS` is enabled.
---
libc/config/linux/x86_64/entrypoints.txt | 4 +
libc/include/stdlib.yaml | 7 +
libc/src/stdlib/CMakeLists.txt | 15 +-
libc/src/stdlib/linux/CMakeLists.txt | 18 ++
libc/src/stdlib/linux/realpath.cpp | 217 +++++++++++++++++++++++
libc/src/stdlib/realpath.h | 25 +++
libc/test/src/stdlib/CMakeLists.txt | 15 ++
libc/test/src/stdlib/realpath_test.cpp | 109 ++++++++++++
8 files changed, 406 insertions(+), 4 deletions(-)
create mode 100644 libc/src/stdlib/linux/realpath.cpp
create mode 100644 libc/src/stdlib/realpath.h
create mode 100644 libc/test/src/stdlib/realpath_test.cpp
diff --git a/libc/config/linux/x86_64/entrypoints.txt b/libc/config/linux/x86_64/entrypoints.txt
index ce88a6749d9dc..78e88bf1a352f 100644
--- a/libc/config/linux/x86_64/entrypoints.txt
+++ b/libc/config/linux/x86_64/entrypoints.txt
@@ -1512,6 +1512,10 @@ if(LLVM_LIBC_FULL_BUILD)
endif()
if(LLVM_LIBC_ENABLE_EXPERIMENTAL_ENTRYPOINTS)
+ list(APPEND TARGET_LIBC_ENTRYPOINTS
+ libc.src.stdlib.realpath
+ )
+
if(LLVM_LIBC_FULL_BUILD)
list(APPEND TARGET_LIBC_ENTRYPOINTS
# regex.h entrypoints
diff --git a/libc/include/stdlib.yaml b/libc/include/stdlib.yaml
index bf77d5d850ea0..a9b7f2f73ad57 100644
--- a/libc/include/stdlib.yaml
+++ b/libc/include/stdlib.yaml
@@ -216,6 +216,13 @@ functions:
return_type: int
arguments:
- type: void
+ - name: realpath
+ standards:
+ - posix
+ return_type: char *
+ arguments:
+ - type: const char *__restrict
+ - type: char *__restrict
- name: srand
standards:
- stdc
diff --git a/libc/src/stdlib/CMakeLists.txt b/libc/src/stdlib/CMakeLists.txt
index 4593288881c7d..2bb515bbf6c24 100644
--- a/libc/src/stdlib/CMakeLists.txt
+++ b/libc/src/stdlib/CMakeLists.txt
@@ -585,6 +585,17 @@ if(NOT LIBC_TARGET_OS_IS_BAREMETAL AND NOT LIBC_TARGET_OS_IS_GPU)
endif()
endif()
+if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/${LIBC_TARGET_OS})
+ add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/${LIBC_TARGET_OS})
+endif()
+
+add_entrypoint_object(
+ realpath
+ ALIAS
+ DEPENDS
+ .${LIBC_TARGET_OS}.realpath
+)
+
if(NOT LLVM_LIBC_FULL_BUILD)
return()
endif()
@@ -666,10 +677,6 @@ add_entrypoint_object(
libc.src.__support.str_to_integer
)
-if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/${LIBC_TARGET_OS})
- add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/${LIBC_TARGET_OS})
-endif()
-
add_entrypoint_object(
setenv
ALIAS
diff --git a/libc/src/stdlib/linux/CMakeLists.txt b/libc/src/stdlib/linux/CMakeLists.txt
index aedcd3f11bf38..2ea3cbeb06cb8 100644
--- a/libc/src/stdlib/linux/CMakeLists.txt
+++ b/libc/src/stdlib/linux/CMakeLists.txt
@@ -9,6 +9,24 @@ add_header_library(
libc.src.signal.linux.signal_utils
)
+add_entrypoint_object(
+ realpath
+ SRCS
+ realpath.cpp
+ HDRS
+ ../realpath.h
+ DEPENDS
+ libc.hdr.errno_macros
+ libc.hdr.limits_macros
+ libc.src.__support.alloc_checker
+ libc.src.__support.common
+ libc.src.__support.CPP.string_view
+ libc.src.__support.error_or
+ libc.src.__support.libc_errno
+ libc.src.__support.macros.config
+ libc.src.string.memory_utils.inline_memcpy
+)
+
add_entrypoint_object(
setenv
SRCS
diff --git a/libc/src/stdlib/linux/realpath.cpp b/libc/src/stdlib/linux/realpath.cpp
new file mode 100644
index 0000000000000..e635b72dacca6
--- /dev/null
+++ b/libc/src/stdlib/linux/realpath.cpp
@@ -0,0 +1,217 @@
+//===----------------------------------------------------------------------===//
+//
+// 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 POSIX realpath.
+///
+//===----------------------------------------------------------------------===//
+
+#include "src/stdlib/realpath.h"
+#include "hdr/errno_macros.h"
+#include "hdr/limits_macros.h"
+#include "src/__support/CPP/string_view.h"
+#include "src/__support/alloc-checker.h"
+#include "src/__support/common.h"
+#include "src/__support/error_or.h"
+#include "src/__support/libc_errno.h"
+#include "src/__support/macros/config.h"
+#include "src/string/memory_utils/inline_memcpy.h"
+
+namespace LIBC_NAMESPACE_DECL {
+namespace {
+
+// Separator character for POSIX paths.
+constexpr char PATH_SEP = '/';
+
+// Dummy struct to represent success in `ErrorOr` when no value is needed.
+struct Ok {};
+
+// Whether a path is absolute.
+bool is_absolute(cpp::string_view path) { return path.starts_with(PATH_SEP); }
+
+// Container for a fully resolved, canonical path.
+//
+// The contained path is always in its canonical form. It is:
+// - Absolute
+// - Symlink-free
+// - Without a trailing separator
+// - Devoid of path traversals like "." or ".."
+class ResolvedPath {
+public:
+ ResolvedPath() { set_to_root(); }
+
+ void set_to_root() {
+ buf_[0] = PATH_SEP;
+ size_ = 1;
+ }
+
+ bool is_root() const { return size_ == 1; }
+
+ ErrorOr<Ok> set_to_cwd() { return Error(ENOSYS); }
+
+ void set_to_parent() {
+ size_t sep_index = view().find_last_of(PATH_SEP);
+
+ // Ensure we maintain the root separator.
+ size_ = sep_index == 0 ? 1 : sep_index;
+ }
+
+ // Adds a single component to the end of this path.
+ ErrorOr<Ok> push_component(cpp::string_view component) {
+ if (component.size() > NAME_MAX)
+ return Error(ENAMETOOLONG);
+
+ if (!is_root()) {
+ if (ErrorOr<Ok> res = push_raw(PATH_SEP); !res)
+ return res;
+ }
+
+ return push_raw(component);
+ }
+
+ cpp::string_view view() const { return cpp::string_view(buf_, size_); }
+
+private:
+ ErrorOr<Ok> push_raw(cpp::string_view value) {
+ if (value.size() > sizeof(buf_) - size_)
+ return Error(ENAMETOOLONG);
+
+ inline_memcpy(buf_ + size_, value.data(), value.size());
+ size_ += value.size();
+ return Ok{};
+ }
+
+ ErrorOr<Ok> push_raw(char value) {
+ return push_raw(cpp::string_view(&value, 1));
+ }
+
+ // Current size of the path stored in `buf_`.
+ size_t size_;
+
+ // `PATH_MAX` includes a null-terminator in its count,
+ // so use `PATH_MAX - 1` here as `ResolvedPath` is not null-terminated.
+ char buf_[PATH_MAX - 1];
+};
+
+// A view over path components yet to be processed by realpath.
+//
+// When `realpath("./a/../b")` is called, the input path can be viewed as
+// a stack of components, where components closest to the root are at the top.
+// For example:
+//
+// ```
+// PendingPath p("./a/..");
+// assert(p.advance_component() == ".");
+// assert(p.advance_component() == "a");
+// assert(p.advance_component() == "..");
+// assert(p.empty());
+// ```
+class PendingPath {
+public:
+ explicit PendingPath(cpp::string_view path) : view_(path) {}
+
+ // Whether all path components have been consumed.
+ bool empty() const { return view_.empty(); }
+
+ // Takes the next path component,
+ // starting with the component closest to the root.
+ cpp::string_view advance_component() {
+ const cpp::string_view path = view_;
+
+ const size_t component_start = path.find_first_not_of(PATH_SEP);
+ if (component_start == cpp::string_view::npos) {
+ view_ = "";
+ return "";
+ }
+
+ const size_t component_end =
+ path.find_first_of(PATH_SEP, /* From = */ component_start);
+ if (component_end == cpp::string_view::npos) {
+ view_ = "";
+ return path.substr(component_start);
+ }
+
+ view_ = view_.substr(component_end);
+ return path.substr(component_start, component_end - component_start);
+ }
+
+private:
+ cpp::string_view view_;
+};
+
+ErrorOr<char *> copy_or_allocate_cstr(char *dst, cpp::string_view src) {
+ if (dst == nullptr) {
+ AllocChecker ac;
+ dst = new (ac) char[src.size() + 1];
+ if (!ac)
+ return Error(ENOMEM);
+ }
+ inline_memcpy(dst, src.data(), src.size());
+ dst[src.size()] = '\0';
+ return dst;
+}
+
+ErrorOr<Ok> resolve_path(PendingPath &pending_path,
+ ResolvedPath &resolved_path) {
+ while (!pending_path.empty()) {
+ cpp::string_view component = pending_path.advance_component();
+ if (component.empty() || component == ".")
+ continue;
+
+ if (component == "..") {
+ resolved_path.set_to_parent();
+ continue;
+ }
+
+ if (ErrorOr<Ok> res = resolved_path.push_component(component); !res)
+ return res;
+ }
+
+ return Ok{};
+}
+
+ErrorOr<char *> realpath_impl(const char *__restrict path_cstr,
+ char *__restrict resolved_path_buf) {
+ if (path_cstr == nullptr)
+ return Error(EINVAL);
+
+ cpp::string_view path(path_cstr);
+ if (path.size() == 0)
+ return Error(ENOENT);
+
+ if (path.size() >= PATH_MAX)
+ return Error(ENAMETOOLONG);
+
+ PendingPath pending_path(path);
+
+ ResolvedPath resolved_path;
+ if (!is_absolute(path)) {
+ if (ErrorOr<Ok> res = resolved_path.set_to_cwd(); !res)
+ return Error(res.error());
+ }
+
+ if (ErrorOr<Ok> res = resolve_path(pending_path, resolved_path); !res)
+ return Error(res.error());
+
+ return copy_or_allocate_cstr(resolved_path_buf, resolved_path.view());
+}
+
+} // namespace
+
+LLVM_LIBC_FUNCTION(char *, realpath,
+ (const char *__restrict path,
+ char *__restrict resolved_path)) {
+ ErrorOr<char *> res = realpath_impl(path, resolved_path);
+ if (!res) {
+ libc_errno = res.error();
+ return nullptr;
+ }
+ return *res;
+}
+
+} // namespace LIBC_NAMESPACE_DECL
diff --git a/libc/src/stdlib/realpath.h b/libc/src/stdlib/realpath.h
new file mode 100644
index 0000000000000..ddc8700ffc197
--- /dev/null
+++ b/libc/src/stdlib/realpath.h
@@ -0,0 +1,25 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+/// Declaration of the POSIX realpath function.
+///
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_LIBC_SRC_STDLIB_REALPATH_H
+#define LLVM_LIBC_SRC_STDLIB_REALPATH_H
+
+#include "src/__support/macros/config.h"
+
+namespace LIBC_NAMESPACE_DECL {
+
+char *realpath(const char *__restrict path, char *__restrict resolved_path);
+
+} // namespace LIBC_NAMESPACE_DECL
+
+#endif // LLVM_LIBC_SRC_STDLIB_REALPATH_H
diff --git a/libc/test/src/stdlib/CMakeLists.txt b/libc/test/src/stdlib/CMakeLists.txt
index 95e5a3ccc2bb5..6b48d86d6ae50 100644
--- a/libc/test/src/stdlib/CMakeLists.txt
+++ b/libc/test/src/stdlib/CMakeLists.txt
@@ -376,6 +376,21 @@ add_libc_test(
libc.src.stdlib.srand
)
+add_libc_test(
+ realpath_test
+ SUITE
+ libc-stdlib-tests
+ SRCS
+ realpath_test.cpp
+ DEPENDS
+ libc.hdr.errno_macros
+ libc.hdr.limits_macros
+ libc.src.__support.CPP.string
+ libc.src.__support.macros.config
+ libc.src.stdlib.realpath
+ libc.test.UnitTest.ErrnoCheckingTest
+)
+
add_libc_test(
memalignment_test
SUITE
diff --git a/libc/test/src/stdlib/realpath_test.cpp b/libc/test/src/stdlib/realpath_test.cpp
new file mode 100644
index 0000000000000..604121e321dd0
--- /dev/null
+++ b/libc/test/src/stdlib/realpath_test.cpp
@@ -0,0 +1,109 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+/// Unit tests for realpath.
+///
+//===----------------------------------------------------------------------===//
+
+#include "hdr/errno_macros.h"
+#include "hdr/limits_macros.h"
+#include "src/__support/CPP/string.h"
+#include "src/__support/macros/config.h"
+#include "src/stdlib/realpath.h"
+#include "test/UnitTest/ErrnoCheckingTest.h"
+
+namespace LIBC_NAMESPACE_DECL {
+
+class LlvmLibcRealpathTest : public testing::ErrnoCheckingTest {
+public:
+ char *realpath_buffered(const char *path) { return realpath(path, buf_); }
+
+ char *realpath_buffered(const cpp::string &path) {
+ return realpath_buffered(path.c_str());
+ }
+
+ char buf_[PATH_MAX];
+};
+
+TEST_F(LlvmLibcRealpathTest, ErrorsWithInvalidArgIfNullPath) {
+ ASSERT_EQ(realpath_buffered(nullptr), nullptr);
+ ASSERT_ERRNO_EQ(EINVAL);
+}
+
+TEST_F(LlvmLibcRealpathTest, ErrorsWithNoEntryIfEmptyPath) {
+ ASSERT_EQ(realpath_buffered(""), nullptr);
+ ASSERT_ERRNO_EQ(ENOENT);
+}
+
+TEST_F(LlvmLibcRealpathTest, OkIfPathIsExactlyMaxSize) {
+ // PATH_MAX counts null terminator, so construct a path of size PATH_MAX-1.
+ cpp::string s(PATH_MAX - 1, 'a');
+ for (size_t i = 0; i < s.size(); i += NAME_MAX + 1)
+ s[i] = '/';
+
+ ASSERT_STREQ(realpath_buffered(s), s.c_str());
+}
+
+TEST_F(LlvmLibcRealpathTest, ErrorsWithNameTooLongIfPathExceedsMaxSize) {
+ // PATH_MAX counts null terminator, so construct a path of size PATH_MAX.
+ cpp::string s(PATH_MAX, 'a');
+ for (size_t i = 0; i < s.size(); i += NAME_MAX + 1)
+ s[i] = '/';
+
+ ASSERT_EQ(realpath_buffered(s), nullptr);
+ ASSERT_ERRNO_EQ(ENAMETOOLONG);
+}
+
+TEST_F(LlvmLibcRealpathTest, OkWhenComponentLengthIsExactlyNameMax) {
+ cpp::string s(NAME_MAX + 1, 'a'); // +1 for leading "/"
+ s[0] = '/';
+
+ ASSERT_STREQ(realpath_buffered(s), s.c_str());
+}
+
+TEST_F(LlvmLibcRealpathTest, ErrorsIfComponentExceedsNameMax) {
+ constexpr size_t COMPONENT_SIZE = NAME_MAX + 1;
+ cpp::string s(COMPONENT_SIZE + 1, 'a'); // +1 for leading "/"
+ s[0] = '/';
+
+ ASSERT_EQ(realpath_buffered(s), nullptr);
+ ASSERT_ERRNO_EQ(ENAMETOOLONG);
+}
+
+TEST_F(LlvmLibcRealpathTest, RootResolvesToRoot) {
+ ASSERT_STREQ(realpath_buffered("/"), "/");
+}
+
+TEST_F(LlvmLibcRealpathTest, RootDotDotTraversalStaysAtRoot) {
+ ASSERT_STREQ(realpath_buffered("/.."), "/");
+}
+
+TEST_F(LlvmLibcRealpathTest, SimpleAbsolutePath) {
+ ASSERT_STREQ(realpath_buffered("/a/b/c"), "/a/b/c");
+}
+
+TEST_F(LlvmLibcRealpathTest, DotDotTraversesParent) {
+ ASSERT_STREQ(realpath_buffered("/a/b/.."), "/a");
+}
+
+TEST_F(LlvmLibcRealpathTest, DotTraversalIsNop) {
+ ASSERT_STREQ(realpath_buffered("/a/b/./"), "/a/b");
+}
+
+TEST_F(LlvmLibcRealpathTest, ConsecutiveSeparatorsIgnored) {
+ ASSERT_STREQ(realpath_buffered("////a///..///a//"), "/a");
+}
+
+TEST_F(LlvmLibcRealpathTest, AllocatesResultWhenBufferIsNull) {
+ char *result = LIBC_NAMESPACE::realpath("/a", nullptr);
+ ASSERT_STREQ(result, "/a");
+ ::free(result);
+}
+
+} // namespace LIBC_NAMESPACE_DECL
>From 7267857d21175c444af8dadc6470505c5c1835e4 Mon Sep 17 00:00:00 2001
From: jtstogel <jtstogel at gmail.com>
Date: Wed, 17 Jun 2026 19:50:25 -0700
Subject: [PATCH 02/10] Comment on AllocChecker usage for resolved_path
---
libc/src/stdlib/linux/realpath.cpp | 2 ++
1 file changed, 2 insertions(+)
diff --git a/libc/src/stdlib/linux/realpath.cpp b/libc/src/stdlib/linux/realpath.cpp
index e635b72dacca6..7b5cde08f5cee 100644
--- a/libc/src/stdlib/linux/realpath.cpp
+++ b/libc/src/stdlib/linux/realpath.cpp
@@ -147,6 +147,8 @@ class PendingPath {
ErrorOr<char *> copy_or_allocate_cstr(char *dst, cpp::string_view src) {
if (dst == nullptr) {
AllocChecker ac;
+ // `dst` is safe to return and let the caller `free()`, since AllocChecker
+ // delegates to malloc, and this value is trivially destructible.
dst = new (ac) char[src.size() + 1];
if (!ac)
return Error(ENOMEM);
>From e85d0364dadb9aa58608fad4bba4248e862e22fe Mon Sep 17 00:00:00 2001
From: jtstogel <jtstogel at gmail.com>
Date: Wed, 17 Jun 2026 19:53:54 -0700
Subject: [PATCH 03/10] Include size_t
---
libc/src/stdlib/linux/CMakeLists.txt | 1 +
libc/src/stdlib/linux/realpath.cpp | 1 +
libc/test/src/stdlib/CMakeLists.txt | 1 +
libc/test/src/stdlib/realpath_test.cpp | 1 +
4 files changed, 4 insertions(+)
diff --git a/libc/src/stdlib/linux/CMakeLists.txt b/libc/src/stdlib/linux/CMakeLists.txt
index 2ea3cbeb06cb8..71c890a4ecf43 100644
--- a/libc/src/stdlib/linux/CMakeLists.txt
+++ b/libc/src/stdlib/linux/CMakeLists.txt
@@ -19,6 +19,7 @@ add_entrypoint_object(
libc.hdr.errno_macros
libc.hdr.limits_macros
libc.src.__support.alloc_checker
+ libc.hdr.types.size_t
libc.src.__support.common
libc.src.__support.CPP.string_view
libc.src.__support.error_or
diff --git a/libc/src/stdlib/linux/realpath.cpp b/libc/src/stdlib/linux/realpath.cpp
index 7b5cde08f5cee..b5fb2d7f14902 100644
--- a/libc/src/stdlib/linux/realpath.cpp
+++ b/libc/src/stdlib/linux/realpath.cpp
@@ -14,6 +14,7 @@
#include "src/stdlib/realpath.h"
#include "hdr/errno_macros.h"
#include "hdr/limits_macros.h"
+#include "hdr/types/size_t.h"
#include "src/__support/CPP/string_view.h"
#include "src/__support/alloc-checker.h"
#include "src/__support/common.h"
diff --git a/libc/test/src/stdlib/CMakeLists.txt b/libc/test/src/stdlib/CMakeLists.txt
index 6b48d86d6ae50..224e9c4ac3bad 100644
--- a/libc/test/src/stdlib/CMakeLists.txt
+++ b/libc/test/src/stdlib/CMakeLists.txt
@@ -385,6 +385,7 @@ add_libc_test(
DEPENDS
libc.hdr.errno_macros
libc.hdr.limits_macros
+ libc.hdr.types.size_t
libc.src.__support.CPP.string
libc.src.__support.macros.config
libc.src.stdlib.realpath
diff --git a/libc/test/src/stdlib/realpath_test.cpp b/libc/test/src/stdlib/realpath_test.cpp
index 604121e321dd0..62d7f14c53c8d 100644
--- a/libc/test/src/stdlib/realpath_test.cpp
+++ b/libc/test/src/stdlib/realpath_test.cpp
@@ -13,6 +13,7 @@
#include "hdr/errno_macros.h"
#include "hdr/limits_macros.h"
+#include "hdr/types/size_t.h"
#include "src/__support/CPP/string.h"
#include "src/__support/macros/config.h"
#include "src/stdlib/realpath.h"
>From 2157635c0b8fc5475c439d344bca9e9f08bb46f9 Mon Sep 17 00:00:00 2001
From: jtstogel <jtstogel at gmail.com>
Date: Wed, 17 Jun 2026 19:55:33 -0700
Subject: [PATCH 04/10] Don't check against NAME_MAX, we can let the kernel do
so in statx
---
libc/src/stdlib/linux/realpath.cpp | 3 ---
libc/test/src/stdlib/realpath_test.cpp | 35 +++++++++++---------------
2 files changed, 14 insertions(+), 24 deletions(-)
diff --git a/libc/src/stdlib/linux/realpath.cpp b/libc/src/stdlib/linux/realpath.cpp
index b5fb2d7f14902..f23b705d0cdbd 100644
--- a/libc/src/stdlib/linux/realpath.cpp
+++ b/libc/src/stdlib/linux/realpath.cpp
@@ -64,9 +64,6 @@ class ResolvedPath {
// Adds a single component to the end of this path.
ErrorOr<Ok> push_component(cpp::string_view component) {
- if (component.size() > NAME_MAX)
- return Error(ENAMETOOLONG);
-
if (!is_root()) {
if (ErrorOr<Ok> res = push_raw(PATH_SEP); !res)
return res;
diff --git a/libc/test/src/stdlib/realpath_test.cpp b/libc/test/src/stdlib/realpath_test.cpp
index 62d7f14c53c8d..515ef37be410d 100644
--- a/libc/test/src/stdlib/realpath_test.cpp
+++ b/libc/test/src/stdlib/realpath_test.cpp
@@ -42,36 +42,29 @@ TEST_F(LlvmLibcRealpathTest, ErrorsWithNoEntryIfEmptyPath) {
ASSERT_ERRNO_EQ(ENOENT);
}
-TEST_F(LlvmLibcRealpathTest, OkIfPathIsExactlyMaxSize) {
+TEST_F(LlvmLibcRealpathTest, OkIfPathArgIsExactlyMaxSize) {
// PATH_MAX counts null terminator, so construct a path of size PATH_MAX-1.
- cpp::string s(PATH_MAX - 1, 'a');
- for (size_t i = 0; i < s.size(); i += NAME_MAX + 1)
- s[i] = '/';
+ cpp::string s(PATH_MAX - 1, '/');
+ for (size_t i = 1; i < s.size(); i += 2)
+ s[i] = '.';
- ASSERT_STREQ(realpath_buffered(s), s.c_str());
+ ASSERT_STREQ(realpath_buffered(s), "/");
}
-TEST_F(LlvmLibcRealpathTest, ErrorsWithNameTooLongIfPathExceedsMaxSize) {
- // PATH_MAX counts null terminator, so construct a path of size PATH_MAX.
- cpp::string s(PATH_MAX, 'a');
- for (size_t i = 0; i < s.size(); i += NAME_MAX + 1)
+TEST_F(LlvmLibcRealpathTest, OkIfResolvedPathIsExactlyMaxSize) {
+ // PATH_MAX counts null terminator, so construct a path of size PATH_MAX-1.
+ cpp::string s(PATH_MAX - 1, 'a');
+ for (size_t i = 0; i < s.size() - 1; i += 8)
s[i] = '/';
- ASSERT_EQ(realpath_buffered(s), nullptr);
- ASSERT_ERRNO_EQ(ENAMETOOLONG);
-}
-
-TEST_F(LlvmLibcRealpathTest, OkWhenComponentLengthIsExactlyNameMax) {
- cpp::string s(NAME_MAX + 1, 'a'); // +1 for leading "/"
- s[0] = '/';
-
ASSERT_STREQ(realpath_buffered(s), s.c_str());
}
-TEST_F(LlvmLibcRealpathTest, ErrorsIfComponentExceedsNameMax) {
- constexpr size_t COMPONENT_SIZE = NAME_MAX + 1;
- cpp::string s(COMPONENT_SIZE + 1, 'a'); // +1 for leading "/"
- s[0] = '/';
+TEST_F(LlvmLibcRealpathTest, ErrorsWithNameTooLongIfPathArgExceedsMaxSize) {
+ // PATH_MAX counts null terminator, so construct a path of size PATH_MAX.
+ cpp::string s(PATH_MAX, '/');
+ for (size_t i = 1; i < s.size(); i += 2)
+ s[i] = '.';
ASSERT_EQ(realpath_buffered(s), nullptr);
ASSERT_ERRNO_EQ(ENAMETOOLONG);
>From e3ed1329db69c8f3bd9e1d1b33f50a8df1bf0aff Mon Sep 17 00:00:00 2001
From: jtstogel <jtstogel at gmail.com>
Date: Thu, 18 Jun 2026 12:22:12 -0700
Subject: [PATCH 05/10] Simplify is_root.
---
libc/src/stdlib/linux/realpath.cpp | 11 +++++------
1 file changed, 5 insertions(+), 6 deletions(-)
diff --git a/libc/src/stdlib/linux/realpath.cpp b/libc/src/stdlib/linux/realpath.cpp
index f23b705d0cdbd..3213e3c095156 100644
--- a/libc/src/stdlib/linux/realpath.cpp
+++ b/libc/src/stdlib/linux/realpath.cpp
@@ -29,6 +29,9 @@ namespace {
// Separator character for POSIX paths.
constexpr char PATH_SEP = '/';
+// Separator for POSIX paths, as a `cpp::string_view`.
+constexpr cpp::string_view PATH_SEP_STRING = "/";
+
// Dummy struct to represent success in `ErrorOr` when no value is needed.
struct Ok {};
@@ -51,7 +54,7 @@ class ResolvedPath {
size_ = 1;
}
- bool is_root() const { return size_ == 1; }
+ bool is_root() const { return view() == PATH_SEP_STRING; }
ErrorOr<Ok> set_to_cwd() { return Error(ENOSYS); }
@@ -65,7 +68,7 @@ class ResolvedPath {
// Adds a single component to the end of this path.
ErrorOr<Ok> push_component(cpp::string_view component) {
if (!is_root()) {
- if (ErrorOr<Ok> res = push_raw(PATH_SEP); !res)
+ if (ErrorOr<Ok> res = push_raw(PATH_SEP_STRING); !res)
return res;
}
@@ -84,10 +87,6 @@ class ResolvedPath {
return Ok{};
}
- ErrorOr<Ok> push_raw(char value) {
- return push_raw(cpp::string_view(&value, 1));
- }
-
// Current size of the path stored in `buf_`.
size_t size_;
>From 0d31ab5d2b61065a8bec866e0b54c1c0134960ae Mon Sep 17 00:00:00 2001
From: jtstogel <jtstogel at gmail.com>
Date: Tue, 23 Jun 2026 14:23:04 -0700
Subject: [PATCH 06/10] Use cpp::string instead of manual buffer management
---
libc/src/__support/CPP/string.h | 44 ++++++++++++---
libc/src/stdlib/linux/CMakeLists.txt | 2 +-
libc/src/stdlib/linux/realpath.cpp | 61 +++++++++------------
libc/test/src/__support/CPP/CMakeLists.txt | 1 +
libc/test/src/__support/CPP/string_test.cpp | 20 +++++++
5 files changed, 84 insertions(+), 44 deletions(-)
diff --git a/libc/src/__support/CPP/string.h b/libc/src/__support/CPP/string.h
index 04f0e9538d03e..541dbe6552110 100644
--- a/libc/src/__support/CPP/string.h
+++ b/libc/src/__support/CPP/string.h
@@ -23,6 +23,20 @@
namespace LIBC_NAMESPACE_DECL {
namespace cpp {
+namespace {
+
+char *realloc_or_die(char *ptr, size_t size) {
+ void *new_ptr = ::realloc(ptr, size);
+ if (new_ptr == nullptr) {
+ // Out of memory: this is not handled in current implementation,
+ // We trap the program and exits.
+ __builtin_trap();
+ }
+
+ return reinterpret_cast<char *>(new_ptr);
+}
+
+} // namespace
// This class mimics std::string but does not intend to be a full fledged
// implementation. Most notably it does not provide support for character traits
@@ -133,15 +147,9 @@ class string {
if (new_capacity < SIZE_MAX / 11)
new_capacity = new_capacity * 11 / 8;
- if (void *Ptr = ::realloc(buffer_ == get_empty_string() ? nullptr : buffer_,
- new_capacity)) {
- buffer_ = static_cast<char *>(Ptr);
- capacity_ = new_capacity;
- return;
- }
- // Out of memory: this is not handled in current implementation,
- // We trap the program and exits.
- __builtin_trap();
+ buffer_ = realloc_or_die(buffer_ == get_empty_string() ? nullptr : buffer_,
+ new_capacity);
+ capacity_ = new_capacity;
}
LIBC_INLINE void resize(size_t size) {
@@ -153,6 +161,24 @@ class string {
set_size_and_add_null_character(size);
}
+ // Releases the backing C-string.
+ //
+ // The returned pointer must be free'd by the caller.
+ // This is a non-standard extension to std::string.
+ LIBC_INLINE char *release_c_str() {
+ if (buffer_ == get_empty_string()) {
+ // Ensure the buffer is heap allocated,
+ // so that it may later be passed to `free`.
+ char *res = realloc_or_die(nullptr, 1);
+ res[0] = '\0';
+ return res;
+ }
+
+ char *res = buffer_;
+ reset_no_deallocate();
+ return res;
+ }
+
LIBC_INLINE string &operator+=(const string &rhs) {
const size_t new_size = size_ + rhs.size();
reserve(new_size);
diff --git a/libc/src/stdlib/linux/CMakeLists.txt b/libc/src/stdlib/linux/CMakeLists.txt
index 71c890a4ecf43..0800145301638 100644
--- a/libc/src/stdlib/linux/CMakeLists.txt
+++ b/libc/src/stdlib/linux/CMakeLists.txt
@@ -18,9 +18,9 @@ add_entrypoint_object(
DEPENDS
libc.hdr.errno_macros
libc.hdr.limits_macros
- libc.src.__support.alloc_checker
libc.hdr.types.size_t
libc.src.__support.common
+ libc.src.__support.CPP.string
libc.src.__support.CPP.string_view
libc.src.__support.error_or
libc.src.__support.libc_errno
diff --git a/libc/src/stdlib/linux/realpath.cpp b/libc/src/stdlib/linux/realpath.cpp
index 3213e3c095156..ef26ad74b52d5 100644
--- a/libc/src/stdlib/linux/realpath.cpp
+++ b/libc/src/stdlib/linux/realpath.cpp
@@ -15,8 +15,8 @@
#include "hdr/errno_macros.h"
#include "hdr/limits_macros.h"
#include "hdr/types/size_t.h"
+#include "src/__support/CPP/string.h"
#include "src/__support/CPP/string_view.h"
-#include "src/__support/alloc-checker.h"
#include "src/__support/common.h"
#include "src/__support/error_or.h"
#include "src/__support/libc_errno.h"
@@ -49,20 +49,19 @@ class ResolvedPath {
public:
ResolvedPath() { set_to_root(); }
- void set_to_root() {
- buf_[0] = PATH_SEP;
- size_ = 1;
- }
+ void set_to_root() { path_ = PATH_SEP_STRING; }
- bool is_root() const { return view() == PATH_SEP_STRING; }
+ bool is_root() const { return path_ == PATH_SEP_STRING; }
ErrorOr<Ok> set_to_cwd() { return Error(ENOSYS); }
+ // Removes the trailing path component.
void set_to_parent() {
- size_t sep_index = view().find_last_of(PATH_SEP);
+ size_t sep_index = cpp::string_view(path_).find_last_of(PATH_SEP);
- // Ensure we maintain the root separator.
- size_ = sep_index == 0 ? 1 : sep_index;
+ // Never move past the root separator. For example,
+ // ensures that set_to_parent on "/hello" only resizes to "/".
+ path_.resize(sep_index >= 1 ? sep_index : 1);
}
// Adds a single component to the end of this path.
@@ -75,24 +74,28 @@ class ResolvedPath {
return push_raw(component);
}
- cpp::string_view view() const { return cpp::string_view(buf_, size_); }
+ // Releases ownership of the underlying C-string and resets this path.
+ //
+ // Must be free'd by the caller.
+ char *release() { return path_.release_c_str(); }
+
+ // Copies the content of this path to `dst`.
+ void copy_to(char *dst) {
+ inline_memcpy(dst, path_.c_str(), path_.size() + 1);
+ }
private:
ErrorOr<Ok> push_raw(cpp::string_view value) {
- if (value.size() > sizeof(buf_) - size_)
+ // -1 because PATH_MAX includes a null-terminator.
+ size_t remaining_bytes = (PATH_MAX - 1) - path_.size();
+ if (value.size() > remaining_bytes)
return Error(ENAMETOOLONG);
- inline_memcpy(buf_ + size_, value.data(), value.size());
- size_ += value.size();
+ path_ += value;
return Ok{};
}
- // Current size of the path stored in `buf_`.
- size_t size_;
-
- // `PATH_MAX` includes a null-terminator in its count,
- // so use `PATH_MAX - 1` here as `ResolvedPath` is not null-terminated.
- char buf_[PATH_MAX - 1];
+ cpp::string path_;
};
// A view over path components yet to be processed by realpath.
@@ -141,20 +144,6 @@ class PendingPath {
cpp::string_view view_;
};
-ErrorOr<char *> copy_or_allocate_cstr(char *dst, cpp::string_view src) {
- if (dst == nullptr) {
- AllocChecker ac;
- // `dst` is safe to return and let the caller `free()`, since AllocChecker
- // delegates to malloc, and this value is trivially destructible.
- dst = new (ac) char[src.size() + 1];
- if (!ac)
- return Error(ENOMEM);
- }
- inline_memcpy(dst, src.data(), src.size());
- dst[src.size()] = '\0';
- return dst;
-}
-
ErrorOr<Ok> resolve_path(PendingPath &pending_path,
ResolvedPath &resolved_path) {
while (!pending_path.empty()) {
@@ -197,7 +186,11 @@ ErrorOr<char *> realpath_impl(const char *__restrict path_cstr,
if (ErrorOr<Ok> res = resolve_path(pending_path, resolved_path); !res)
return Error(res.error());
- return copy_or_allocate_cstr(resolved_path_buf, resolved_path.view());
+ if (resolved_path_buf != nullptr) {
+ resolved_path.copy_to(resolved_path_buf);
+ return resolved_path_buf;
+ }
+ return resolved_path.release();
}
} // namespace
diff --git a/libc/test/src/__support/CPP/CMakeLists.txt b/libc/test/src/__support/CPP/CMakeLists.txt
index 1d47455dcd31f..dec097e776efd 100644
--- a/libc/test/src/__support/CPP/CMakeLists.txt
+++ b/libc/test/src/__support/CPP/CMakeLists.txt
@@ -167,6 +167,7 @@ add_libc_test(
SRCS
string_test.cpp
DEPENDS
+ libc.hdr.func.free
libc.src.__support.CPP.string
libc.src.__support.CPP.string_view
)
diff --git a/libc/test/src/__support/CPP/string_test.cpp b/libc/test/src/__support/CPP/string_test.cpp
index e606ab127adc2..5e44a2b11688a 100644
--- a/libc/test/src/__support/CPP/string_test.cpp
+++ b/libc/test/src/__support/CPP/string_test.cpp
@@ -6,6 +6,7 @@
//
//===----------------------------------------------------------------------===//
+#include "hdr/func/free.h"
#include "src/__support/CPP/string.h"
#include "test/UnitTest/Test.h"
@@ -186,6 +187,25 @@ TEST(LlvmLibcStringTest, ConcatWithCString) {
ASSERT_STREQ(("a" + string("b")).c_str(), "ab");
}
+TEST(LlvmLibcStringTest, ReleaseCStrResetsString) {
+ string s("abc");
+
+ char *cstr = s.release_c_str();
+ ASSERT_STREQ(cstr, "abc");
+ ASSERT_STREQ(s.c_str(), "");
+ free(cstr);
+}
+
+TEST(LlvmLibcStringTest, ReleaseCStrOnDefaultString) {
+ string s;
+
+ char *cstr = s.release_c_str();
+ ASSERT_STREQ(cstr, "");
+ ASSERT_STREQ(s.c_str(), "");
+ ASSERT_NE(cstr, s.data()); // Different pointers.
+ free(cstr);
+}
+
TEST(LlvmLibcStringTest, Comparison) {
// Here we simply check that comparison of string and string_view have the
// same semantic.
>From 6b6ce1bf1677069b93c5ecde2a04ae1e65ccc98d Mon Sep 17 00:00:00 2001
From: jtstogel <jtstogel at gmail.com>
Date: Fri, 26 Jun 2026 11:39:47 -0700
Subject: [PATCH 07/10] Move generic path functions to OSUtil/path.h
---
libc/src/__support/CPP/string.h | 5 +++
libc/src/__support/OSUtil/CMakeLists.txt | 29 +++++++++-----
.../src/__support/OSUtil/linux/CMakeLists.txt | 9 +++++
libc/src/__support/OSUtil/linux/path.h | 39 +++++++++++++++++++
libc/src/__support/OSUtil/path.h | 23 +++++++++++
libc/src/stdlib/linux/CMakeLists.txt | 1 +
libc/src/stdlib/linux/realpath.cpp | 29 ++++++--------
libc/test/src/__support/CPP/string_test.cpp | 9 ++++-
8 files changed, 115 insertions(+), 29 deletions(-)
create mode 100644 libc/src/__support/OSUtil/linux/path.h
create mode 100644 libc/src/__support/OSUtil/path.h
diff --git a/libc/src/__support/CPP/string.h b/libc/src/__support/CPP/string.h
index 541dbe6552110..823a5f94cb7e7 100644
--- a/libc/src/__support/CPP/string.h
+++ b/libc/src/__support/CPP/string.h
@@ -90,6 +90,11 @@ class string {
return (*this) += other;
}
+ LIBC_INLINE string &operator=(char other) {
+ resize(0);
+ return (*this) += other;
+ }
+
LIBC_INLINE string &operator=(string &&other) {
buffer_ = other.buffer_;
size_ = other.size_;
diff --git a/libc/src/__support/OSUtil/CMakeLists.txt b/libc/src/__support/OSUtil/CMakeLists.txt
index 94d1042ccbb4a..da5158a850bef 100644
--- a/libc/src/__support/OSUtil/CMakeLists.txt
+++ b/libc/src/__support/OSUtil/CMakeLists.txt
@@ -1,17 +1,26 @@
if(NOT EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/${LIBC_TARGET_OS})
return()
endif()
-
add_subdirectory(${LIBC_TARGET_OS})
+
set(target_os_util libc.src.__support.OSUtil.${LIBC_TARGET_OS}.${LIBC_TARGET_OS}_util)
-if(NOT TARGET ${target_os_util})
- return()
+if(TARGET ${target_os_util})
+ add_object_library(
+ osutil
+ ALIAS
+ ${target_os_util}
+ DEPENDS
+ ${target_os_util}
+ )
endif()
-add_object_library(
- osutil
- ALIAS
- ${target_os_util}
- DEPENDS
- ${target_os_util}
-)
+set(target_path libc.src.__support.OSUtil.${LIBC_TARGET_OS}.path)
+if(TARGET ${target_path})
+ add_header_library(
+ path
+ HDRS
+ path.h
+ DEPENDS
+ ${target_path}
+ )
+endif()
diff --git a/libc/src/__support/OSUtil/linux/CMakeLists.txt b/libc/src/__support/OSUtil/linux/CMakeLists.txt
index 525e62c7ca23b..3dc8c31212e30 100644
--- a/libc/src/__support/OSUtil/linux/CMakeLists.txt
+++ b/libc/src/__support/OSUtil/linux/CMakeLists.txt
@@ -43,6 +43,15 @@ add_header_library(
libc.src.__support.threads.callonce
)
+add_header_library(
+ path
+ HDRS
+ path.h
+ DEPENDS
+ libc.src.__support.common
+ libc.src.__support.CPP.string_view
+)
+
add_header_library(
sysinfo
HDRS
diff --git a/libc/src/__support/OSUtil/linux/path.h b/libc/src/__support/OSUtil/linux/path.h
new file mode 100644
index 0000000000000..955751c308793
--- /dev/null
+++ b/libc/src/__support/OSUtil/linux/path.h
@@ -0,0 +1,39 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+/// Internal linux path helpers.
+///
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_LIBC_SRC___SUPPORT_OSUTIL_LINUX_PATH_H
+#define LLVM_LIBC_SRC___SUPPORT_OSUTIL_LINUX_PATH_H
+
+#include "src/__support/CPP/string_view.h"
+#include "src/__support/macros/config.h"
+
+namespace LIBC_NAMESPACE_DECL {
+namespace path {
+
+// Separator character for linux paths.
+constexpr char SEPARATOR = '/';
+
+// Whether `path` is absolute.
+LIBC_INLINE constexpr bool is_absolute(cpp::string_view path) {
+ return path.starts_with(SEPARATOR);
+}
+
+// Whether `path` is absolute.
+LIBC_INLINE constexpr bool is_root(cpp::string_view path) {
+ return path.size() == 1 && path[0] == SEPARATOR;
+}
+
+} // namespace path
+} // namespace LIBC_NAMESPACE_DECL
+
+#endif // LLVM_LIBC_SRC___SUPPORT_OSUTIL_LINUX_PATH_H
diff --git a/libc/src/__support/OSUtil/path.h b/libc/src/__support/OSUtil/path.h
new file mode 100644
index 0000000000000..ba7cf6d0cbc81
--- /dev/null
+++ b/libc/src/__support/OSUtil/path.h
@@ -0,0 +1,23 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+/// Internal path helpers.
+///
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_LIBC_SRC___SUPPORT_OSUTIL_PATH_H
+#define LLVM_LIBC_SRC___SUPPORT_OSUTIL_PATH_H
+
+#if defined(__linux__)
+#include "src/__support/OSUtil/linux/path.h"
+#else
+#error Path unavailable on this platfrom
+#endif
+
+#endif // LLVM_LIBC_SRC___SUPPORT_OSUTIL_PATH_H
diff --git a/libc/src/stdlib/linux/CMakeLists.txt b/libc/src/stdlib/linux/CMakeLists.txt
index 0800145301638..b287fc3a1a8f6 100644
--- a/libc/src/stdlib/linux/CMakeLists.txt
+++ b/libc/src/stdlib/linux/CMakeLists.txt
@@ -25,6 +25,7 @@ add_entrypoint_object(
libc.src.__support.error_or
libc.src.__support.libc_errno
libc.src.__support.macros.config
+ libc.src.__support.OSUtil.path
libc.src.string.memory_utils.inline_memcpy
)
diff --git a/libc/src/stdlib/linux/realpath.cpp b/libc/src/stdlib/linux/realpath.cpp
index ef26ad74b52d5..d6c1a148eb35f 100644
--- a/libc/src/stdlib/linux/realpath.cpp
+++ b/libc/src/stdlib/linux/realpath.cpp
@@ -17,6 +17,7 @@
#include "hdr/types/size_t.h"
#include "src/__support/CPP/string.h"
#include "src/__support/CPP/string_view.h"
+#include "src/__support/OSUtil/path.h"
#include "src/__support/common.h"
#include "src/__support/error_or.h"
#include "src/__support/libc_errno.h"
@@ -26,18 +27,9 @@
namespace LIBC_NAMESPACE_DECL {
namespace {
-// Separator character for POSIX paths.
-constexpr char PATH_SEP = '/';
-
-// Separator for POSIX paths, as a `cpp::string_view`.
-constexpr cpp::string_view PATH_SEP_STRING = "/";
-
// Dummy struct to represent success in `ErrorOr` when no value is needed.
struct Ok {};
-// Whether a path is absolute.
-bool is_absolute(cpp::string_view path) { return path.starts_with(PATH_SEP); }
-
// Container for a fully resolved, canonical path.
//
// The contained path is always in its canonical form. It is:
@@ -49,15 +41,13 @@ class ResolvedPath {
public:
ResolvedPath() { set_to_root(); }
- void set_to_root() { path_ = PATH_SEP_STRING; }
-
- bool is_root() const { return path_ == PATH_SEP_STRING; }
+ void set_to_root() { path_ = path::SEPARATOR; }
ErrorOr<Ok> set_to_cwd() { return Error(ENOSYS); }
// Removes the trailing path component.
void set_to_parent() {
- size_t sep_index = cpp::string_view(path_).find_last_of(PATH_SEP);
+ size_t sep_index = cpp::string_view(path_).find_last_of(path::SEPARATOR);
// Never move past the root separator. For example,
// ensures that set_to_parent on "/hello" only resizes to "/".
@@ -66,8 +56,8 @@ class ResolvedPath {
// Adds a single component to the end of this path.
ErrorOr<Ok> push_component(cpp::string_view component) {
- if (!is_root()) {
- if (ErrorOr<Ok> res = push_raw(PATH_SEP_STRING); !res)
+ if (!path::is_root(path_)) {
+ if (ErrorOr<Ok> res = push_raw(path::SEPARATOR); !res)
return res;
}
@@ -95,6 +85,9 @@ class ResolvedPath {
return Ok{};
}
+ // Adds a single component to the end of this path.
+ ErrorOr<Ok> push_raw(char c) { return push_raw(cpp::string_view(&c, 1)); }
+
cpp::string path_;
};
@@ -123,14 +116,14 @@ class PendingPath {
cpp::string_view advance_component() {
const cpp::string_view path = view_;
- const size_t component_start = path.find_first_not_of(PATH_SEP);
+ const size_t component_start = path.find_first_not_of(path::SEPARATOR);
if (component_start == cpp::string_view::npos) {
view_ = "";
return "";
}
const size_t component_end =
- path.find_first_of(PATH_SEP, /* From = */ component_start);
+ path.find_first_of(path::SEPARATOR, /* From = */ component_start);
if (component_end == cpp::string_view::npos) {
view_ = "";
return path.substr(component_start);
@@ -178,7 +171,7 @@ ErrorOr<char *> realpath_impl(const char *__restrict path_cstr,
PendingPath pending_path(path);
ResolvedPath resolved_path;
- if (!is_absolute(path)) {
+ if (!path::is_absolute(path)) {
if (ErrorOr<Ok> res = resolved_path.set_to_cwd(); !res)
return Error(res.error());
}
diff --git a/libc/test/src/__support/CPP/string_test.cpp b/libc/test/src/__support/CPP/string_test.cpp
index 5e44a2b11688a..cc7c47c55e1b6 100644
--- a/libc/test/src/__support/CPP/string_test.cpp
+++ b/libc/test/src/__support/CPP/string_test.cpp
@@ -78,7 +78,7 @@ TEST(LlvmLibcStringTest, InitializeRepeatedChar) {
ASSERT_EQ(string_view(s), string_view("1111"));
}
-TEST(LlvmLibcStringTest, InitializeZeorChar) {
+TEST(LlvmLibcStringTest, InitializeZeroChar) {
const string s(0, '1');
ASSERT_TRUE(s.empty());
}
@@ -136,6 +136,13 @@ TEST(LlvmLibcStringTest, StringViewAssign) {
ASSERT_EQ(s.back(), 'b');
}
+TEST(LlvmLibcStringTest, CharacterAssign) {
+ string s("abc");
+ s = 'a';
+ ASSERT_EQ(s.size(), size_t(1));
+ ASSERT_STREQ(s.c_str(), "a");
+}
+
TEST(LlvmLibcStringTest, Concat) {
const char *const str = "abc";
string a(str);
>From 40bdbe7b04818f6f62285d4c50d562c3a82730d9 Mon Sep 17 00:00:00 2001
From: jtstogel <jtstogel at gmail.com>
Date: Fri, 26 Jun 2026 17:02:15 -0700
Subject: [PATCH 08/10] Better error message
---
libc/src/__support/OSUtil/path.h | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/libc/src/__support/OSUtil/path.h b/libc/src/__support/OSUtil/path.h
index ba7cf6d0cbc81..7d3d3d6c19b96 100644
--- a/libc/src/__support/OSUtil/path.h
+++ b/libc/src/__support/OSUtil/path.h
@@ -17,7 +17,7 @@
#if defined(__linux__)
#include "src/__support/OSUtil/linux/path.h"
#else
-#error Path unavailable on this platfrom
+#error path.h unavailable on this platfrom
#endif
#endif // LLVM_LIBC_SRC___SUPPORT_OSUTIL_PATH_H
>From 167a9e6bc6796c52676abd25328e01d4c52e0737 Mon Sep 17 00:00:00 2001
From: jtstogel <jtstogel at gmail.com>
Date: Fri, 26 Jun 2026 17:03:43 -0700
Subject: [PATCH 09/10] Fix comment
---
libc/src/stdlib/linux/realpath.cpp | 1 -
1 file changed, 1 deletion(-)
diff --git a/libc/src/stdlib/linux/realpath.cpp b/libc/src/stdlib/linux/realpath.cpp
index d6c1a148eb35f..e4e8db3263ad4 100644
--- a/libc/src/stdlib/linux/realpath.cpp
+++ b/libc/src/stdlib/linux/realpath.cpp
@@ -85,7 +85,6 @@ class ResolvedPath {
return Ok{};
}
- // Adds a single component to the end of this path.
ErrorOr<Ok> push_raw(char c) { return push_raw(cpp::string_view(&c, 1)); }
cpp::string path_;
>From e63859f1b0f8ca730dc314bfe030251db3ffc219 Mon Sep 17 00:00:00 2001
From: jtstogel <jtstogel at gmail.com>
Date: Mon, 29 Jun 2026 15:31:29 -0700
Subject: [PATCH 10/10] Remove Ok monotype for error handling.
---
libc/src/stdlib/linux/CMakeLists.txt | 1 +
libc/src/stdlib/linux/realpath.cpp | 38 ++++++++++++++--------------
2 files changed, 20 insertions(+), 19 deletions(-)
diff --git a/libc/src/stdlib/linux/CMakeLists.txt b/libc/src/stdlib/linux/CMakeLists.txt
index b287fc3a1a8f6..a796e54da2fce 100644
--- a/libc/src/stdlib/linux/CMakeLists.txt
+++ b/libc/src/stdlib/linux/CMakeLists.txt
@@ -20,6 +20,7 @@ add_entrypoint_object(
libc.hdr.limits_macros
libc.hdr.types.size_t
libc.src.__support.common
+ libc.src.__support.CPP.optional
libc.src.__support.CPP.string
libc.src.__support.CPP.string_view
libc.src.__support.error_or
diff --git a/libc/src/stdlib/linux/realpath.cpp b/libc/src/stdlib/linux/realpath.cpp
index e4e8db3263ad4..2410816532d4e 100644
--- a/libc/src/stdlib/linux/realpath.cpp
+++ b/libc/src/stdlib/linux/realpath.cpp
@@ -15,6 +15,7 @@
#include "hdr/errno_macros.h"
#include "hdr/limits_macros.h"
#include "hdr/types/size_t.h"
+#include "src/__support/CPP/optional.h"
#include "src/__support/CPP/string.h"
#include "src/__support/CPP/string_view.h"
#include "src/__support/OSUtil/path.h"
@@ -27,9 +28,6 @@
namespace LIBC_NAMESPACE_DECL {
namespace {
-// Dummy struct to represent success in `ErrorOr` when no value is needed.
-struct Ok {};
-
// Container for a fully resolved, canonical path.
//
// The contained path is always in its canonical form. It is:
@@ -43,7 +41,7 @@ class ResolvedPath {
void set_to_root() { path_ = path::SEPARATOR; }
- ErrorOr<Ok> set_to_cwd() { return Error(ENOSYS); }
+ cpp::optional<Error> set_to_cwd() { return Error(ENOSYS); }
// Removes the trailing path component.
void set_to_parent() {
@@ -55,10 +53,10 @@ class ResolvedPath {
}
// Adds a single component to the end of this path.
- ErrorOr<Ok> push_component(cpp::string_view component) {
+ cpp::optional<Error> push_component(cpp::string_view component) {
if (!path::is_root(path_)) {
- if (ErrorOr<Ok> res = push_raw(path::SEPARATOR); !res)
- return res;
+ if (cpp::optional<Error> err = push_raw(path::SEPARATOR); err)
+ return err;
}
return push_raw(component);
@@ -75,17 +73,19 @@ class ResolvedPath {
}
private:
- ErrorOr<Ok> push_raw(cpp::string_view value) {
+ cpp::optional<Error> push_raw(cpp::string_view value) {
// -1 because PATH_MAX includes a null-terminator.
size_t remaining_bytes = (PATH_MAX - 1) - path_.size();
if (value.size() > remaining_bytes)
return Error(ENAMETOOLONG);
path_ += value;
- return Ok{};
+ return cpp::nullopt;
}
- ErrorOr<Ok> push_raw(char c) { return push_raw(cpp::string_view(&c, 1)); }
+ cpp::optional<Error> push_raw(char c) {
+ return push_raw(cpp::string_view(&c, 1));
+ }
cpp::string path_;
};
@@ -136,8 +136,8 @@ class PendingPath {
cpp::string_view view_;
};
-ErrorOr<Ok> resolve_path(PendingPath &pending_path,
- ResolvedPath &resolved_path) {
+cpp::optional<Error> resolve_path(PendingPath &pending_path,
+ ResolvedPath &resolved_path) {
while (!pending_path.empty()) {
cpp::string_view component = pending_path.advance_component();
if (component.empty() || component == ".")
@@ -148,11 +148,11 @@ ErrorOr<Ok> resolve_path(PendingPath &pending_path,
continue;
}
- if (ErrorOr<Ok> res = resolved_path.push_component(component); !res)
- return res;
+ if (cpp::optional<Error> err = resolved_path.push_component(component); err)
+ return err;
}
- return Ok{};
+ return cpp::nullopt;
}
ErrorOr<char *> realpath_impl(const char *__restrict path_cstr,
@@ -171,12 +171,12 @@ ErrorOr<char *> realpath_impl(const char *__restrict path_cstr,
ResolvedPath resolved_path;
if (!path::is_absolute(path)) {
- if (ErrorOr<Ok> res = resolved_path.set_to_cwd(); !res)
- return Error(res.error());
+ if (cpp::optional<Error> err = resolved_path.set_to_cwd(); err)
+ return *err;
}
- if (ErrorOr<Ok> res = resolve_path(pending_path, resolved_path); !res)
- return Error(res.error());
+ if (cpp::optional<Error> err = resolve_path(pending_path, resolved_path); err)
+ return *err;
if (resolved_path_buf != nullptr) {
resolved_path.copy_to(resolved_path_buf);
More information about the libc-commits
mailing list