From 3cf7a732e39ee53a56a64a6b4a5db033e9296303 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Mon, 27 Jul 2026 23:45:30 -0400 Subject: [PATCH 1/2] Expose and test system::qualified_path(). --- .../system/unicode/utf8_everywhere/paths.hpp | 6 +++ src/unicode/utf8_everywhere/paths.cpp | 19 ++++++++-- test/unicode/utf8_everywhere/environment.cpp | 2 - test/unicode/utf8_everywhere/paths.cpp | 38 +++++++++++++++++++ 4 files changed, 59 insertions(+), 6 deletions(-) diff --git a/include/bitcoin/system/unicode/utf8_everywhere/paths.hpp b/include/bitcoin/system/unicode/utf8_everywhere/paths.hpp index b273e20689..54fc355217 100644 --- a/include/bitcoin/system/unicode/utf8_everywhere/paths.hpp +++ b/include/bitcoin/system/unicode/utf8_everywhere/paths.hpp @@ -37,6 +37,12 @@ BC_API std::filesystem::path to_path(const std::string& value) NOEXCEPT; BC_API std::filesystem::path default_config_path( const std::filesystem::path& subdirectory) NOEXCEPT; +/// Fully qualify the path and return it as another. +/// Not thread safe. If another thread calls SetCurrentDirectory during this +/// call, the result may be corrupted as static storage is used for directory. +BC_API std::filesystem::path qualified_path( + const std::filesystem::path& path) NOEXCEPT; + /// Extend the path and return it as another. /// std::filesystem::path does not extend long paths, so we provide this. /// Not thread safe. If another thread calls SetCurrentDirectory during this diff --git a/src/unicode/utf8_everywhere/paths.cpp b/src/unicode/utf8_everywhere/paths.cpp index cdcc138e9b..5b23184629 100644 --- a/src/unicode/utf8_everywhere/paths.cpp +++ b/src/unicode/utf8_everywhere/paths.cpp @@ -23,6 +23,7 @@ #include #endif #include +#include #include #include @@ -82,7 +83,7 @@ std::filesystem::path default_config_path( #if defined(HAVE_MSC) -// Helper for to_fully_qualified_path. +// Helper for qualified_path. inline auto replace_all(std::wstring text, wchar_t from, wchar_t to) NOEXCEPT { for (auto position = text.find(from); position != std::string::npos; @@ -95,8 +96,7 @@ inline auto replace_all(std::wstring text, wchar_t from, wchar_t to) NOEXCEPT }; // docs.microsoft.com/windows/win32/api/fileapi/nf-fileapi-getfullpathnamew -static std::wstring to_fully_qualified_path( - const std::filesystem::path& path) NOEXCEPT +std::filesystem::path qualified_path(const std::filesystem::path& path) NOEXCEPT { // Separator normalization required by use of length extender. BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT) @@ -121,7 +121,9 @@ static std::wstring to_fully_qualified_path( // The returned size does not include the null terminator, and cannot // exceed the original, but does become smaller, so resize accordingly. + BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT) return { directory.begin(), std::next(directory.begin(), size) }; + BC_POP_WARNING() } std::filesystem::path extended_path(const std::filesystem::path& path) NOEXCEPT @@ -129,12 +131,21 @@ std::filesystem::path extended_path(const std::filesystem::path& path) NOEXCEPT // The length extension prefix works only with a fully-qualified path. // However this includes "considered relative" paths (with ".." segments). // That is of no consequence here because those will also be converted. - const auto full = to_fully_qualified_path(path); + BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT) + const auto full = qualified_path(path).wstring(); return { (full.length() > MAX_PATH) ? L"\\\\?\\" + full : full }; + BC_POP_WARNING() } #else +std::filesystem::path qualified_path(const std::filesystem::path& path) NOEXCEPT +{ + std::error_code ec{}; + const auto full = std::filesystem::absolute(path, ec); + return ec ? path : full; +} + std::filesystem::path extended_path(const std::filesystem::path& path) NOEXCEPT { return path; diff --git a/test/unicode/utf8_everywhere/environment.cpp b/test/unicode/utf8_everywhere/environment.cpp index 5af6ba3025..600bc152bf 100644 --- a/test/unicode/utf8_everywhere/environment.cpp +++ b/test/unicode/utf8_everywhere/environment.cpp @@ -356,8 +356,6 @@ BOOST_AUTO_TEST_CASE(utf8_environment__allocate_environment_args__null_terminati free_environment(narrow_args); } -// extended_path - #endif // HAVE_MSC BOOST_AUTO_TEST_SUITE_END() diff --git a/test/unicode/utf8_everywhere/paths.cpp b/test/unicode/utf8_everywhere/paths.cpp index 65cbf5f9e7..1136be3006 100644 --- a/test/unicode/utf8_everywhere/paths.cpp +++ b/test/unicode/utf8_everywhere/paths.cpp @@ -122,6 +122,44 @@ BOOST_AUTO_TEST_CASE(paths__to_path__always__expected) BOOST_REQUIRE(result.u8string() == u8"\\\\?\\C:\\very\\long\\path\\exceeding\\MAX_PATH\\Unicode 文件名.txt"); } +// The functions are not platform-specific, though the vectors are. Only win32 +// qualifies a path with a drive and extends one that exceeds the maximum. +constexpr size_t long_path_size = 260; + +#if defined(HAVE_MSC) +static const auto test_absolute = std::filesystem::path{ L"C:\\path\\file.ext" }; +static const auto test_long = L"C:\\" + std::wstring(long_path_size, L'x'); +static const auto test_extended = std::filesystem::path{ L"\\\\?\\" + test_long }; +#else +static const auto test_absolute = std::filesystem::path{ "/path/file.ext" }; +static const auto test_long = "/" + std::string(long_path_size, 'x'); +static const auto test_extended = std::filesystem::path{ test_long }; +#endif + +// qualified_path + +BOOST_AUTO_TEST_CASE(paths__qualified_path__absolute__unchanged) +{ + BOOST_REQUIRE_EQUAL(qualified_path(test_absolute), test_absolute); +} + +BOOST_AUTO_TEST_CASE(paths__qualified_path__long_absolute__unextended) +{ + BOOST_REQUIRE_EQUAL(qualified_path({ test_long }), std::filesystem::path{ test_long }); +} + +// extended_path + +BOOST_AUTO_TEST_CASE(paths__extended_path__absolute__unchanged) +{ + BOOST_REQUIRE_EQUAL(extended_path(test_absolute), test_absolute); +} + +BOOST_AUTO_TEST_CASE(paths__extended_path__long_absolute__expected) +{ + BOOST_REQUIRE_EQUAL(extended_path({ test_long }), test_extended); +} + BOOST_AUTO_TEST_SUITE_END() BC_POP_WARNING() From e78e0b576e600e7fc03bd2af2b67cbdb4de06ff6 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Tue, 28 Jul 2026 02:44:59 -0400 Subject: [PATCH 2/2] Add module_path(). --- .../system/unicode/utf8_everywhere/paths.hpp | 3 ++ src/unicode/utf8_everywhere/paths.cpp | 35 ++++++++++++++++++- test/unicode/utf8_everywhere/paths.cpp | 19 ++++++++++ 3 files changed, 56 insertions(+), 1 deletion(-) diff --git a/include/bitcoin/system/unicode/utf8_everywhere/paths.hpp b/include/bitcoin/system/unicode/utf8_everywhere/paths.hpp index 54fc355217..8f8e18905c 100644 --- a/include/bitcoin/system/unicode/utf8_everywhere/paths.hpp +++ b/include/bitcoin/system/unicode/utf8_everywhere/paths.hpp @@ -37,6 +37,9 @@ BC_API std::filesystem::path to_path(const std::string& value) NOEXCEPT; BC_API std::filesystem::path default_config_path( const std::filesystem::path& subdirectory) NOEXCEPT; +/// Get the module path (not implemented for non-Windows). +BC_API std::filesystem::path module_path() NOEXCEPT; + /// Fully qualify the path and return it as another. /// Not thread safe. If another thread calls SetCurrentDirectory during this /// call, the result may be corrupted as static storage is used for directory. diff --git a/src/unicode/utf8_everywhere/paths.cpp b/src/unicode/utf8_everywhere/paths.cpp index 5b23184629..61602065fd 100644 --- a/src/unicode/utf8_everywhere/paths.cpp +++ b/src/unicode/utf8_everywhere/paths.cpp @@ -16,7 +16,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -#include +#include #ifdef HAVE_MSC #include @@ -137,6 +137,32 @@ std::filesystem::path extended_path(const std::filesystem::path& path) NOEXCEPT BC_POP_WARNING() } +std::filesystem::path module_path() NOEXCEPT +{ + BC_PUSH_WARNING(NO_CASTS_FOR_ARITHMETIC_CONVERSION) + constexpr auto max_path = static_cast(MAX_PATH); + BC_POP_WARNING() + + for (auto size = max_path; !is_zero(size); size *= 2u) + { + std::vector buffer(size); + const auto length = ::GetModuleFileNameW(NULL, buffer.data(), size); + if (is_zero(length)) + break; + + if (length < size) + { + BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT) + return { buffer.begin(), std::next(buffer.begin(), length) }; + BC_POP_WARNING() + } + } + + BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT) + return {}; + BC_POP_WARNING() +} + #else std::filesystem::path qualified_path(const std::filesystem::path& path) NOEXCEPT @@ -151,6 +177,13 @@ std::filesystem::path extended_path(const std::filesystem::path& path) NOEXCEPT return path; } +std::filesystem::path module_path() NOEXCEPT +{ + BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT) + return {}; + BC_POP_WARNING() +} + #endif // HAVE_MSC } // namespace system diff --git a/test/unicode/utf8_everywhere/paths.cpp b/test/unicode/utf8_everywhere/paths.cpp index 1136be3006..d0aedfba5a 100644 --- a/test/unicode/utf8_everywhere/paths.cpp +++ b/test/unicode/utf8_everywhere/paths.cpp @@ -136,6 +136,25 @@ static const auto test_long = "/" + std::string(long_path_size, 'x'); static const auto test_extended = std::filesystem::path{ test_long }; #endif +// module_path + +BOOST_AUTO_TEST_CASE(paths__module_path__always__expected) +{ + const auto module = module_path(); + +#if defined(HAVE_MSC) + // The module is the test executable, already qualified and existing. + BOOST_REQUIRE(!module.empty()); + BOOST_REQUIRE(module.is_absolute()); + BOOST_REQUIRE(module.has_filename()); + BOOST_REQUIRE(std::filesystem::exists(module)); + BOOST_REQUIRE_EQUAL(qualified_path(module), module); +#else + // Not implemented for non-Windows. + BOOST_REQUIRE(module.empty()); +#endif +} + // qualified_path BOOST_AUTO_TEST_CASE(paths__qualified_path__absolute__unchanged)