From: Joel Rosdahl Date: Fri, 7 Aug 2026 08:44:53 +0000 (+0200) Subject: fix: Use Unicode-aware case comparison for Windows paths X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=cecb4fda4767614db446ca61b8fba3ce9a458550;p=thirdparty%2Fccache.git fix: Use Unicode-aware case comparison for Windows paths --- diff --git a/src/ccache/ccache.cpp b/src/ccache/ccache.cpp index 379082678..8db1441c3 100644 --- a/src/ccache/ccache.cpp +++ b/src/ccache/ccache.cpp @@ -429,12 +429,8 @@ remember_include_file(Context& ctx, if (!ctx.ignore_header_paths.empty()) { // Canonicalize path for comparison; Clang uses ./header.h. - auto canonical_path_str = + const std::string& canonical_path_str = path_str.str().starts_with("./") ? path_str.str().substr(2) : path_str; -#ifdef _WIN32 - // Handle case-insensitive paths by converting to lowercase. - canonical_path_str = util::to_lowercase(canonical_path_str); -#endif for (const auto& ignore_header_path : ctx.ignore_header_paths) { if (file_path_matches_dir_prefix_or_file(ignore_header_path, canonical_path_str)) { @@ -3200,11 +3196,7 @@ do_cache_compilation(Context& ctx) bool is_ccache_executable(const fs::path& path) { - std::string name = path.filename().string(); -#ifdef _WIN32 - name = util::to_lowercase(name); -#endif - return name.starts_with("ccache"); + return util::path_component_starts_with_case_aware(path.filename(), "ccache"); } bool @@ -3217,7 +3209,8 @@ file_path_matches_dir_prefix_or_file(const fs::path& dir_prefix_or_file, auto end = std::mismatch(dir_prefix_or_file.begin(), dir_prefix_or_file.end(), file_path.begin(), - file_path.end()) + file_path.end(), + util::path_components_equal_case_aware) .first; return end == dir_prefix_or_file.end() || end->empty(); } diff --git a/src/ccache/context.cpp b/src/ccache/context.cpp index 99ff4ec4e..adcf9f456 100644 --- a/src/ccache/context.cpp +++ b/src/ccache/context.cpp @@ -59,12 +59,6 @@ Context::initialize(util::Args&& compiler_and_args, util::logging::init(config.debug(), config.log_file()); ignore_header_paths = util::split_path_list(config.ignore_headers_in_manifest()); -#ifdef _WIN32 - // Handle case-insensitive paths by converting to lowercase. - for (auto& path : ignore_header_paths) { - path = util::to_lowercase(path.string()); - } -#endif set_ignore_options(util::split_into_strings(config.ignore_options(), " ")); // Set default umask for all files created by ccache from now on (if diff --git a/src/ccache/util/path.cpp b/src/ccache/util/path.cpp index 64151c26a..f7feae555 100644 --- a/src/ccache/util/path.cpp +++ b/src/ccache/util/path.cpp @@ -18,12 +18,14 @@ #include "path.hpp" +#include #include #include #include -#include +#include #include +#include #ifdef _WIN32 const char k_dev_null_path[] = "nul:"; @@ -39,12 +41,46 @@ fs::path lexically_relative_case_aware(const fs::path& path, const fs::path& base) { #ifdef _WIN32 - // Note: Case-folding might in theory lead to an incorrect path on Windows - // since not all filesystems are case-insensitive, but this is only done to - // produce a candidate path that will be verified by the caller later. - fs::path p = util::to_lowercase(path.string()); - fs::path b = util::to_lowercase(base.string()); - return p.lexically_relative(b); + // Note: Case-insensitive comparison might in theory lead to an incorrect path + // on Windows since not all filesystems are case-insensitive, but this is only + // done to produce a candidate path that will be verified by the caller later. + if (!util::path_components_equal_case_aware(path.root_name(), + base.root_name()) + || path.is_absolute() != base.is_absolute() + || (!path.has_root_directory() && base.has_root_directory())) { + return {}; + } + + auto [path_it, base_it] = + std::mismatch(path.begin(), + path.end(), + base.begin(), + base.end(), + util::path_components_equal_case_aware); + if (path_it == path.end() && base_it == base.end()) { + return "."; + } + + int num_parents = 0; + for (; base_it != base.end(); ++base_it) { + if (*base_it == "..") { + --num_parents; + } else if (*base_it != "." && !base_it->empty()) { + ++num_parents; + } + } + if (num_parents < 0) { + return {}; + } + + fs::path result; + for (int i = 0; i < num_parents; ++i) { + result /= ".."; + } + for (; path_it != path.end(); ++path_it) { + result /= *path_it; + } + return result; #else return path.lexically_relative(base); #endif @@ -141,6 +177,58 @@ make_relative_path(const fs::path& dir1, return path; } +bool +path_components_equal_case_aware(const fs::path& component1, + const fs::path& component2) +{ +#ifdef _WIN32 + const auto& string1 = component1.native(); + const auto& string2 = component2.native(); + if (string1.empty() || string2.empty()) { + return string1.empty() && string2.empty(); + } + if (!std::in_range(string1.size()) + || !std::in_range(string2.size())) { + return false; + } + return CompareStringOrdinal(string1.data(), + static_cast(string1.size()), + string2.data(), + static_cast(string2.size()), + TRUE) + == CSTR_EQUAL; +#else + return component1.native() == component2.native(); +#endif +} + +bool +path_component_starts_with_case_aware(const fs::path& component, + const fs::path& prefix) +{ + const auto& string = component.native(); + const auto& prefix_string = prefix.native(); + if (prefix_string.size() > string.size()) { + return false; + } +#ifdef _WIN32 + if (prefix_string.empty()) { + return true; + } + if (!std::in_range(prefix_string.size())) { + return false; + } + return CompareStringOrdinal(string.data(), + static_cast(prefix_string.size()), + prefix_string.data(), + static_cast(prefix_string.size()), + TRUE) + == CSTR_EQUAL; +#else + return string.starts_with(prefix_string); +#endif +} + bool path_starts_with(const fs::path& path, const fs::path& prefix) { @@ -148,8 +236,8 @@ path_starts_with(const fs::path& path, const fs::path& prefix) // Note: Not all paths on Windows are case insensitive, but for our purposes // (checking whether a path is below the base directory) users will expect // them to be. - fs::path p1 = util::to_lowercase(util::lexically_normal(path).string()); - fs::path p2 = util::to_lowercase(util::lexically_normal(prefix).string()); + fs::path p1 = util::lexically_normal(path); + fs::path p2 = util::lexically_normal(prefix); #else const fs::path& p1 = path; const fs::path& p2 = prefix; @@ -164,7 +252,12 @@ path_starts_with(const fs::path& path, const fs::path& prefix) } } - return std::mismatch(p1.begin(), p1.end(), p2.begin(), p2_end).second + return std::mismatch(p1.begin(), + p1.end(), + p2.begin(), + p2_end, + util::path_components_equal_case_aware) + .second == p2_end; } diff --git a/src/ccache/util/path.hpp b/src/ccache/util/path.hpp index 3373aab11..e25795e1d 100644 --- a/src/ccache/util/path.hpp +++ b/src/ccache/util/path.hpp @@ -19,9 +19,6 @@ #pragma once #include -#ifdef _WIN32 -# include -#endif #include #include @@ -74,6 +71,17 @@ make_path(const T&... args) return (std::filesystem::path{} / ... / args).lexically_normal(); } +// Return whether `component1` and `component2` are equal. Comparison is case +// insensitive on Windows and case sensitive on other platforms. +bool path_components_equal_case_aware(const std::filesystem::path& component1, + const std::filesystem::path& component2); + +// Return whether `component` starts with `prefix`. Comparison is case +// insensitive on Windows and case sensitive on other platforms. +bool +path_component_starts_with_case_aware(const std::filesystem::path& component, + const std::filesystem::path& prefix); + // Return whether `path` starts with `prefix` considering path specifics on // Windows. bool path_starts_with(const std::filesystem::path& path, @@ -108,7 +116,7 @@ is_dev_null_path(const std::filesystem::path& path) { return path == "/dev/null" #ifdef _WIN32 - || util::to_lowercase(path.string()) == "nul" + || path_components_equal_case_aware(path, "nul") #endif ; } diff --git a/unittest/test_ccache.cpp b/unittest/test_ccache.cpp index 20c2e644b..854202b19 100644 --- a/unittest/test_ccache.cpp +++ b/unittest/test_ccache.cpp @@ -297,6 +297,9 @@ TEST_CASE("file_path_matches_dir_prefix_or_file") #ifdef _WIN32 CHECK(file_path_matches_dir_prefix_or_file("\\aa", "\\aa\\bb")); CHECK(file_path_matches_dir_prefix_or_file("\\aa\\", "\\aa\\bb")); + CHECK(file_path_matches_dir_prefix_or_file( + fs::path(L"C:\\\u00c5ngstr\u00f6m"), + fs::path(L"c:\\\u00e5NGSTR\u00d6M\\header.h"))); #else CHECK(!file_path_matches_dir_prefix_or_file("\\aa", "\\aa\\bb")); CHECK(!file_path_matches_dir_prefix_or_file("\\aa\\", "\\aa\\bb")); diff --git a/unittest/test_util_path.cpp b/unittest/test_util_path.cpp index a84cfcdd1..1e02eb0d3 100644 --- a/unittest/test_util_path.cpp +++ b/unittest/test_util_path.cpp @@ -70,6 +70,35 @@ TEST_CASE("util::is_dev_null_path") #endif } +TEST_CASE("util::path_components_equal_case_aware") +{ + CHECK(util::path_components_equal_case_aware("", "")); + CHECK(util::path_components_equal_case_aware("foo", "foo")); + CHECK(!util::path_components_equal_case_aware("foo", "bar")); +#ifdef _WIN32 + CHECK(util::path_components_equal_case_aware("FOO", "foo")); + CHECK(util::path_components_equal_case_aware(fs::path(L"\u00c5"), + fs::path(L"\u00e5"))); +#else + CHECK(!util::path_components_equal_case_aware("FOO", "foo")); +#endif +} + +TEST_CASE("util::path_component_starts_with_case_aware") +{ + CHECK(util::path_component_starts_with_case_aware("foo", "")); + CHECK(util::path_component_starts_with_case_aware("foobar", "foo")); + CHECK(!util::path_component_starts_with_case_aware("foo", "foobar")); + CHECK(!util::path_component_starts_with_case_aware("foobar", "bar")); +#ifdef _WIN32 + CHECK(util::path_component_starts_with_case_aware("FOOBAR", "foo")); + CHECK(util::path_component_starts_with_case_aware( + fs::path(L"\u00c5ngstr\u00f6m"), fs::path(L"\u00e5NG"))); +#else + CHECK(!util::path_component_starts_with_case_aware("FOOBAR", "foo")); +#endif +} + TEST_CASE("util::lexically_normal") { CHECK(util::lexically_normal("") == ""); @@ -166,6 +195,15 @@ TEST_CASE("util::make_relative_path") CHECK(make_relative_path( lower_drive_cwd, lower_drive_cwd, upper_drive_cwd + "/nonexistent") == "nonexistent"); + + const fs::path unicode_dir = fs::path(L"\u00c5ngstr\u00f6m"); + const fs::path unicode_dir_case_variant = fs::path(L"\u00e5NGSTR\u00d6M"); + REQUIRE(fs::create_directory(unicode_dir)); + CHECK(make_relative_path(fs::path(actual_cwd) / unicode_dir, + fs::path(actual_cwd) / unicode_dir, + fs::path(actual_cwd) / unicode_dir_case_variant + / "nonexistent") + == "nonexistent"); } #endif @@ -199,6 +237,8 @@ TEST_CASE("util::path_starts_with") CHECK(util::path_starts_with("c:/foo/bar", "C:\\FOO")); CHECK(util::path_starts_with("c:/foo/bar/", "C:\\FOO")); CHECK(util::path_starts_with("c:/foo/bar", "C:\\FOO\\")); + CHECK(util::path_starts_with(fs::path(L"C:\\\u00c5ngstr\u00f6m\\bar"), + fs::path(L"c:/\u00e5NGSTR\u00d6M"))); CHECK(!util::path_starts_with("C:\\foo\\bar", "/foo/baz")); CHECK(!util::path_starts_with("C:\\foo\\bar", "C:/foo/baz")); CHECK(!util::path_starts_with("C:\\beh\\foo", "/foo"));