CNA-BUG-047: StorageContainer::GetFileNames/GetDirectoryNames(searchPattern): GlobMatch treats ? as one byte and compares case-sensitively on every host

CNA snapshot 009d40f5  ·  Known Issues › Current bugs  ·  source links pinned to 009d40f5

✓

Evidence basis: source-verified at the pinned commit; tests exist (not executed for this page). Claims on this page were checked by reading the CNA source at commit 009d40f5; unless a sentence says otherwise, nothing here was built or executed. Nothing on this page was executed unless the Evidence section says so.

The hand-written matcher behind the pattern overloads matches ? against one UTF-8 byte rather than one character and compares bytes case-sensitively even on Windows, where XNA's Directory.GetFiles matching is not; only star patterns over ASCII names are exercised by any test, and only through the C ABI smoke test.

Identifier
CNA-BUG-047
Category
Bug
Subsystem
Storage
Status
Open
Verified against
CNA 009d40f5 (009d40f5dd085c4e674d3479675fac84b12b3e0a)
Severity
Low (a triage suggestion, not a project priority)
Evidence basis
Source-verified: read at TARGET, not executed
Tests touching this area
Yes: see Current tests
Affected contract
Microsoft::Xna::Framework::Storage::StorageContainer::GetFileNames(const std::string& searchPattern) and GetDirectoryNames(const std::string& searchPattern); the C name-count and name-copy routes that take a search_pattern

Expected behaviour

XNA documents the pattern overloads with "Both single-character ("?") and multicharacter ("*") wildcards are supported" (xna4-spec StorageContainer) and implements them with Directory.GetFiles/GetDirectories (decompiled StorageContainer), which match UTF-16 characters and follow the file system's case rules, case-insensitive on Windows. CNA's header describes the argument only as a "Glob pattern". A game that lists save slots with "slot?.sav" expects the same result on every host.

Actual behaviour at TARGET

GlobMatch is a 22-line backtracking matcher in an anonymous namespace of StorageContainer.cpp; its own comment says '?' = any single char. It compares bytes of the UTF-8 file name: ? consumes exactly one byte, so a non-ASCII character (two to four bytes) needs as many ?; every other byte, the dot included, matches only itself, case-sensitively on every host. "?.sav" therefore misses é.sav, and "slot?.sav" misses SLOT1.SAV on Windows, where the file system treats the two spellings as one name and XNA would return it. No C++ storage test calls either pattern overload. The only test that does is the C ABI smoke test (StorageSmoke.c, registered as CApi_StorageSmoke only when CNA_BUILD_C_API is ON; it is OFF by default and no workflow at TARGET enables it), which checks the star patterns lev*, z*, *.dat and *.png against ASCII names. The ? wildcard, non-ASCII names and case are not tested anywhere.

Source locations

Evidence

Checked by reading StorageContainer.cpp, the tests and the C adapter at 009d40f5; not executed. The byte and case behaviour follows mechanically from the matcher's single comparison (pattern[pi] == '?' || pattern[pi] == str[si]). The XNA side is from xna4-spec's StorageContainer documentation and the decompiled source. .NET Framework's pattern matching has further legacy rules (short-name and three-letter-extension matches) that CNA does not reproduce; they were not examined and are not part of this entry. The storage guide already documents the byte semantics for game authors; nothing tests them.

Independently observed as a separate finding (merged): The only storage test source, StorageDeviceTests.cpp, has 14 tests covering refusals, containment and directory operations and never calls the pattern overloads, so the hand-written wildcard matcher is unexercised.

Independent re-verification: Checked by reading StorageContainer.cpp, the tests and the C adapter at 009d40f5; not executed. The byte and case behaviour follows mechanically from the matcher's single comparison (pattern[pi] == '?' || pattern[pi] == str[si]). The XNA side is from xna4-spec's StorageContainer documentation and the decompiled StorageContainer (Directory.GetFiles/GetDirectories). The earlier text said that nothing tests the overloads and that the C smoke test passes only a zero-length pattern; that is wrong: StorageSmoke.c's directory and file cases pass non-empty star patterns, and CNA's own coverage_mappings.json records 'glob and empty-pattern listings'. StorageDeviceTests.cpp (14 tests) still never calls the pattern overloads. .NET Framework's pattern matching has further legacy rules (short-name and three-letter-extension matches, and *.* matching names without a dot) that CNA does not reproduce; they were not examined and are not part of this entry.

Focused reproduction

// Illustrative; not compiled or run for this entry.
// The container holds "\xC3\xA9.sav" (e-acute .sav in UTF-8) and "SLOT1.SAV".
auto a = container->GetFileNames("?.sav");      // empty: '?' consumed only the byte C3
auto b = container->GetFileNames("slot?.sav");  // empty on every host, Windows included

Current tests

None. StorageDeviceTests.cpp exercises path containment and deletion only; PathContainmentTests.cpp covers paths, not patterns; CApi_StorageSmoke lists all names.

Regression test

A table-driven test through GetFileNames on a temporary container: empty and dot-less names, literal dots, leading, trailing and repeated stars, ? against a multi-byte UTF-8 name, and upper/lower case, with the expected result per host decided and written down first (XNA-on-Windows case folding, or CNA-defined byte semantics) and then enforced.

Blast radius

Save-slot listings that use ? with non-ASCII names, or mixed-case patterns and names on case-insensitive file systems (Windows, default macOS). The pattern-less overloads and lower-case ASCII patterns over names the game wrote itself are unaffected.

Workaround

Call the pattern-less GetFileNames()/GetDirectoryNames() and filter the names in game code with the rule the game needs.

The same subject is explained at several altitudes. These are the neighbouring pages at each one.

Known issues
Bug index