diff --git a/src/platform/path.cpp b/src/platform/path.cpp index 66cd082c..f66871c8 100644 --- a/src/platform/path.cpp +++ b/src/platform/path.cpp @@ -30,6 +30,22 @@ std::string to_lower(std::string s) { return s; } +// fs::path::string() converts the native wide (UTF-16, on Windows) name to +// the narrow/ACP encoding, and THROWS std::system_error when some character +// has no representation in that codepage — notably the NTFS PUA remap +// (U+F000-U+F0FF) used for filenames that slipped in an illegal character +// (":" from a botched "robocopy"/redirect, seen in production as a stray +// zero-byte file next to real tables). One such neighbour in the directory +// must not abort resolving every OTHER file in it, so every conversion in +// this scan is guarded and unconvertible entries are skipped, not fatal. +std::string safe_path_string(const fs::path& p) { + try { + return p.string(); + } catch (const std::exception&) { + return std::string(); + } +} + } // namespace std::string resolve_case_insensitive(const std::string& path) { @@ -50,16 +66,20 @@ std::string resolve_case_insensitive(const std::string& path) { // the input verbatim when the case is already correct. for (const auto& entry : fs::directory_iterator(parent, ec)) { if (ec) break; - if (entry.path().filename().string() == leaf) { - return entry.path().string(); + const std::string name = safe_path_string(entry.path().filename()); + if (name.empty()) continue; // unconvertible neighbour: skip it + if (name == leaf) { + return safe_path_string(entry.path()); } } // Second pass: case-insensitive match returns the on-disk casing. for (const auto& entry : fs::directory_iterator(parent, ec)) { if (ec) break; - if (to_lower(entry.path().filename().string()) == leaf_low) { - return entry.path().string(); + const std::string name = safe_path_string(entry.path().filename()); + if (name.empty()) continue; // unconvertible neighbour: skip it + if (to_lower(name) == leaf_low) { + return safe_path_string(entry.path()); } } diff --git a/tests/unit/platform_path_test.cpp b/tests/unit/platform_path_test.cpp index f95575b8..1ccc8ca3 100644 --- a/tests/unit/platform_path_test.cpp +++ b/tests/unit/platform_path_test.cpp @@ -35,6 +35,35 @@ TEST_CASE("Case-insensitive resolve matches by case-folded leaf") { fs::remove_all(dir); } +#if defined(_WIN32) +// RCB 2026-08-09 -- production crash (reported as ADSADT/7017 "indice +// corrupto" on an unrelated table): resolve_case_insensitive scans every +// entry of the parent directory with fs::directory_iterator and converts +// each name via path::string(), which THROWS std::system_error on Windows +// when a name has no narrow-codepage representation -- notably the NTFS +// PUA remap (U+F000-U+F0FF) left behind by a botched robocopy/redirect +// that dropped an illegal character (":") into a filename. One such +// neighbour must not abort resolving every OTHER file in the directory. +TEST_CASE("Case-insensitive resolve skips a neighbour with an unconvertible name") { + const auto dir = fs::temp_directory_path() / "openads_path_t4"; + fs::create_directories(dir); + const auto file = dir / "Clientes.dbf"; + { std::ofstream(file) << "x"; } + + std::wstring trap_name = L"trap_"; + trap_name.push_back(static_cast(0xF03A)); // PUA remap of ':' + trap_name += L".tmp"; + const auto trap = dir / trap_name; + { std::ofstream trap_stream(trap); trap_stream << "x"; } + + auto resolved = resolve_case_insensitive((dir / "clientes.dbf").string()); + CHECK(resolved == file.string()); + + fs::remove(trap); + fs::remove_all(dir); +} +#endif + TEST_CASE("Case-insensitive resolve returns input on miss") { const auto dir = fs::temp_directory_path() / "openads_path_t3"; fs::create_directories(dir);