Skip to content

fix(platform): resolve_case_insensitive must not abort on an unconvertible neighbour - #167

Open
russimicro wants to merge 1 commit into
FiveTechSoft:mainfrom
russimicro:pr/resolve-case-insensitive-unconvertible-neighbour
Open

fix(platform): resolve_case_insensitive must not abort on an unconvertible neighbour#167
russimicro wants to merge 1 commit into
FiveTechSoft:mainfrom
russimicro:pr/resolve-case-insensitive-unconvertible-neighbour

Conversation

@russimicro

Copy link
Copy Markdown
Collaborator

OpenADS — resolve_case_insensitive must not abort on an unconvertible neighbour file

Repo FiveTechSoft/OpenADS
Reported / fixed by Russoft / Zerus ERP (Harbour 3.2 + rddads + OpenADS x64)
Date 2026-08-09
File src/platform/path.cppresolve_case_insensitive()
Affects any table open on Windows, ADS_CDX and ADS_ADT alike
Severity High — crashes the whole process while opening a table that has nothing wrong with it

1. Symptom

Opening a perfectly healthy .dbf/.cdx (or .adt/.adi) crashes the process with an
unhandled C++ exception, reported downstream as ADSADT/7017 "indice corrupto" — the table
itself is not corrupt.

Reproduced with a real table: prueba3.dbf/.cdx (6000 records, 2 tags), created and grown
with the genuine Advantage Data Architect (SAP), opened through Harbour rddads:

USE ( cTbl ) VIA "ADS" ALIAS T3 SHARED READONLY NEW

Crashes with Unrecoverable error 6005: Exception error / Exception Code:E06D7363 before
control returns to the application.

2. Root cause

resolve_case_insensitive() scans every entry of the parent directory with
fs::directory_iterator and converts each name to a narrow std::string via
fs::path::string() — for both the exact-case pass and the case-folded pass:

for (const auto& entry : fs::directory_iterator(parent, ec)) {
    if (ec) break;
    if (entry.path().filename().string() == leaf) {   // <-- throws
        return entry.path().string();
    }
}

On Windows, path::string() converts the native wide (UTF-16) name to the narrow/ACP
encoding, and throws std::system_error when some character has no representation in
that codepage. That is exactly what happens with the NTFS PUA remap (U+F000-U+F0FF),
which the filesystem uses for a filename that picked up an illegal character — in our repro,
a stray zero-byte file left behind by a botched robocopy/redirect that dropped a literal
: into a filename (F + U+F03A + temp_robocopy.log, NTFS's remap of the illegal :).

That file has nothing to do with the table being opened — it just happens to live in the
same directory. Because nothing catches the exception, it unwinds straight out of
AdsOpenTable, through Connection::open_table -> Connection::resolve_table_file ->
resolve_case_insensitive, and kills the process. Confirmed with a Debug build + cdb.exe
(stack below), and confirmed in both directions by moving the stray file in and out of the
directory: present -> crash every time; absent -> the same table opens clean, 6000 records,
OrdCount()=2, 0 duplicate tag names.

KERNELBASE!RaiseException
VCRUNTIME140D!_CxxThrowException
openace64!std::_Throw_system_error_from_std_win_error
openace64!std::_Check_convert_result
openace64!std::_Convert_wide_to_narrow<...>
openace64!std::filesystem::_Convert_wide_to<...>
openace64!std::filesystem::path::string<char,...>
openace64!std::filesystem::path::string
openace64!openads::platform::resolve_case_insensitive+0x317
openace64!openads::session::Connection::resolve_table_file+0x4cd
openace64!openads::session::Connection::open_table+0x7a
openace64!AdsOpenTable+0x15c9

This is why the symptom looked like table corruption: any directory that happens to contain
one file with a bad name (a stray temp/log file, a half-written backup, anything an external
tool dropped) makes ADS refuse to open every table in it, regardless of which one the
application asked for.

3. Fix

Guard every path::string() call inside the two directory scans and skip the entry instead
of letting the exception escape:

std::string safe_path_string(const fs::path& p) {
    try {
        return p.string();
    } catch (const std::exception&) {
        return std::string();
    }
}

applied to both the exact-case pass and the case-folded pass; an entry whose name cannot be
converted is skipped (continue), it simply never matches. Net: +28 / -4 in one file.

4. Verification

4.1 Real data, through Harbour rddads, Debug build

DLL stray file present result
unpatched yes crash (Unrecoverable error 6005, E06D7363)
unpatched no opens clean
patched yes opens clean
patched no opens clean

4.2 Regression test

tests/unit/platform_path_test.cpp — new case, Windows-only (#if defined(_WIN32)):
creates a well-formed neighbour and one with a PUA-remapped name (0xF03A) in the same
temp directory, then checks that resolving the well-formed one still succeeds.

4.3 Full suite

Built and run against a clean checkout of main (no unrelated changes in the tree):

100% tests passed, 0 tests failed out of 5
openads_unit_tests: all cases passed (including the new one)

5. Scope

This only touches resolve_case_insensitive()'s two directory scans. Connection's own
path-building calls (cand.string(), rel.string(), full.string() in
session/connection.cpp) operate on names the caller/data-root already supplied, not on
arbitrary directory entries, so they are outside this fix's scope — no report of a crash
from that side.

…tible neighbour

resolve_case_insensitive() scans every entry of the parent directory with
fs::directory_iterator and converts each name from wide (UTF-16) to narrow
via fs::path::string(). On Windows that conversion THROWS std::system_error
when a name has no representation in the narrow codepage -- notably the
NTFS PUA remap (U+F000-U+F0FF) applied to a filename that picked up an
illegal character (e.g. ":" from a botched robocopy/redirect). Nothing
caught it, so opening ANY table crashed the whole process as soon as one
such neighbour sat in the same directory -- unrelated to the table being
opened, and easy to misread as index corruption (reported downstream as
ADSADT/7017 "indice corrupto").

Reproduced end-to-end through Harbour rddads against a real .dbf/.cdx: with
the stray file present the process aborts inside AdsOpenTable ->
Connection::open_table -> Connection::resolve_table_file ->
resolve_case_insensitive, confirmed via a Debug build + cdb.exe stack; with
it absent (or with this fix), the same table opens clean.

Fix: guard every path::string() call in the two directory scans behind a
try/catch, skip the unconvertible entry instead of propagating the
exception. New test creates a neighbour with a PUA-remapped name and checks
that resolving an unrelated, well-formed file in the same directory still
succeeds.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant