Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 24 additions & 4 deletions src/platform/path.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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());
}
}

Expand Down
29 changes: 29 additions & 0 deletions tests/unit/platform_path_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<wchar_t>(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);
Expand Down
Loading