CNA-BUG-047: StorageContainer::GetFileNames/GetDirectoryNames(searchPattern): GlobMatch treats ? as one byte and compares case-sensitively on every host
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
modules/storage/src/StorageContainer.cpp— GlobMatch in the anonymous namespace; GetFileNames and GetDirectoryNames pattern overloadsmodules/storage/include/Microsoft/Xna/Framework/Storage/StorageContainer.hpp— searchPattern documented only as a glob patternmodules/c-api/src/CnaCApiStorage.cpp— the C name routes forward search_pattern to the same overloadsmodules/storage/tests/Microsoft/Xna/Framework/Storage/StorageDeviceTests.cpp— no pattern casemodules/c-api/tests/pure_c/StorageSmoke.c— only the zero-length all-names view is used
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.
Related pages
The same subject is explained at several altitudes. These are the neighbouring pages at each one.
- User guide
- Storage guide: wildcard patterns
- Known issues
- Bug index