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
253 changes: 219 additions & 34 deletions dll/kernel32/fileapi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,12 @@ std::unique_ptr<FindSearchHandle> detachFindHandle(HANDLE handle) {
return owned;
}

bool containsWildcard(std::string_view value) { return value.find_first_of("*?") != std::string_view::npos; }
// The full FsRtlIsUnicodeCharacterWild set: '*', '?' and the DOS tokens
// DOS_STAR '<', DOS_QM '>', DOS_DOT '"'. Windows treats all five as
// wildcards when deciding between enumeration and a direct lookup (they
// are invalid in on-disk Windows names, so a pattern containing them can
// only be a wildcard expression).
bool containsWildcard(std::string_view value) { return value.find_first_of("*?<>\"") != std::string_view::npos; }

bool containsWildcardOutsideExtendedPrefix(std::string_view value) {
if (value.rfind(R"(\\?\)", 0) == 0) {
Expand All @@ -196,43 +201,221 @@ inline char toLowerAscii(char ch) { return static_cast<char>(std::tolower(static

inline bool equalsIgnoreCase(char a, char b) { return toLowerAscii(a) == toLowerAscii(b); }

bool wildcardMatchInsensitive(std::string_view pattern, std::string_view text) {
size_t p = 0;
size_t t = 0;
size_t star = std::string_view::npos;
size_t match = 0;

while (t < text.size()) {
if (p < pattern.size()) {
char pc = pattern[p];
if (pc == '?') {
++p;
++t;
continue;
bool endsWithInsensitive(std::string_view text, std::string_view suffix, bool ignoreCase) {
if (suffix.size() > text.size()) {
return false;
}
std::string_view tail = text.substr(text.size() - suffix.size());
for (size_t i = 0; i < suffix.size(); ++i) {
if (ignoreCase ? !equalsIgnoreCase(tail[i], suffix[i]) : tail[i] != suffix[i]) {
return false;
}
}
return true;
}

// ---------------------------------------------------------------------------
// Win32 filename wildcard matching with DOS-compatible semantics.
//
// Windows does not glob filenames with a plain '*'/'?' matcher. The
// FindFirstFile path first rewrites the user's '*', '?' and '.' into the
// special DOS wildcard tokens
// '<' = DOS_STAR, '>' = DOS_QM, '"' = DOS_DOT
// and then matches with the kernel's FsRtlIsNameInExpression NFA. That is
// why e.g. "*.*" matches names with no extension, "*." matches *only*
// extensionless names, a trailing '?' can match zero characters, and a
// trailing '.' is absorbed.
//
// translateWin32Expression() + matchWin32Expression() below are a port of
// .NET's System.IO.Enumeration.FileSystemName (MIT licensed:
// TranslateWin32Expression + MatchPattern), which itself mirrors the native
// FsRtlIsNameInExpression implementation, with two deliberate deviations
// that make the matcher FsRtl-exact rather than .NET-exact:
// * no '\\' escape character (.NET extension for FileSystemWatcher
// filters; FsRtl treats '\\' as a literal),
// * empty-vs-empty matches (FsRtl returns TRUE iff both strings are
// empty; .NET returns false whenever either is empty).
// Validated against the ReactOS kmtest FsRtlExpression table (149 usable
// cases, values recorded from real Windows kernels) and exhaustively
// differential-tested against an independent transliteration of ReactOS's
// FsRtlIsNameInExpressionPrivate (0 divergences over the full pattern space
// up to length 4 × names up to length 5 over {a,b,.,*,?,<,>,\"}).
// ---------------------------------------------------------------------------

// Rewrite a Win32 search pattern into the DOS-token expression matchWin32
// Expression() expects. Mirrors Windows' own FindFirstFile rewrite.
std::string translateWin32Expression(std::string_view expression) {
if (expression.empty() || expression == "*" || expression == "*.*") {
return "*";
}
std::string out;
out.reserve(expression.size());
const size_t length = expression.size();
bool modified = false;
for (size_t i = 0; i < length; ++i) {
char c = expression[i];
switch (c) {
case '.':
modified = true;
if (i >= 1 && i == length - 1 && expression[i - 1] == '*') {
out.back() = '<'; // DOS_STAR — pattern ends in "*."
} else if (i + 1 < length && (expression[i + 1] == '?' || expression[i + 1] == '*')) {
out.push_back('"'); // DOS_DOT
} else {
out.push_back('.');
}
if (pc == '*') {
star = p++;
match = t;
continue;
break;
case '?':
modified = true;
out.push_back('>'); // DOS_QM
break;
default:
out.push_back(c);
break;
}
}
return modified ? out : std::string(expression);
}

// Match a (translated) DOS-token expression against a literal name. Tokens:
// '*' zero-or-more chars, '?' one char, '<' DOS_STAR, '>' DOS_QM, '"' DOS_DOT;
// every other character (including '\\') matches literally.
bool matchWin32Expression(std::string_view expression, std::string_view name, bool ignoreCase = true) {
if (expression.empty() || name.empty()) {
// FsRtl semantics: two empty strings match, otherwise no match.
return expression.empty() && name.empty();
}
if (expression[0] == '*') {
if (expression.size() == 1) {
return true;
}
std::string_view expressionEnd = expression.substr(1);
if (expressionEnd.find_first_of("\"<>*?") == std::string_view::npos) {
// "*<literal>" degenerates to an "ends with" test.
if (name.size() < expressionEnd.size()) {
return false;
}
if (equalsIgnoreCase(pc, text[t])) {
++p;
++t;
continue;
return endsWithInsensitive(name, expressionEnd, ignoreCase);
}
}

const size_t maxState = expression.size() * 2;
std::vector<size_t> currentMatches(16, 0);
std::vector<size_t> priorMatches(16, 0);
priorMatches[0] = 0;
size_t matchCount = 1;

size_t nameOffset = 0;
char nameChar = '\0';
bool nameFinished = false;

while (!nameFinished) {
if (nameOffset < name.size()) {
nameChar = name[nameOffset++];
} else {
if (priorMatches[matchCount - 1] == maxState) {
break;
}
nameFinished = true;
}
if (star != std::string_view::npos) {
p = star + 1;
t = ++match;
continue;

size_t priorMatch = 0;
size_t currentMatch = 0;
size_t priorMatchCount = 0;

while (priorMatch < matchCount) {
size_t expressionOffset = (priorMatches[priorMatch++] + 1) / 2;

while (expressionOffset < expression.size()) {
if (currentMatch >= currentMatches.size() - 2) {
currentMatches.resize(currentMatches.size() * 2, 0);
priorMatches.resize(priorMatches.size() * 2, 0);
}

size_t currentState = expressionOffset * 2;
char expressionChar = expression[expressionOffset];

bool zeroOrMore = false; // add currentState and currentState+1, then advance
bool zero = false; // add currentState+1, then advance
bool finished = false; // stop scanning this expression branch

if (expressionChar == '*') {
zeroOrMore = true;
} else if (expressionChar == '<') { // DOS_STAR
bool notLastPeriod =
!nameFinished && nameChar == '.' && name.find('.', nameOffset) != std::string_view::npos;
if (nameFinished || nameChar != '.' || notLastPeriod) {
zeroOrMore = true;
} else {
zero = true;
}
} else {
currentState += 2;
if (expressionChar == '>') { // DOS_QM
if (!nameFinished && nameChar != '.') {
currentMatches[currentMatch++] = currentState;
}
// else: advance to next expression character
if (nameFinished || nameChar == '.') {
// fallthrough to advance
} else {
finished = true;
}
} else if (expressionChar == '"') { // DOS_DOT
if (!nameFinished) {
if (nameChar == '.') {
currentMatches[currentMatch++] = currentState;
}
finished = true;
}
// nameFinished: advance to next expression character
} else {
// '?' or a literal character (no escapes — FsRtl semantics)
if (!nameFinished &&
(expressionChar == '?' ||
(ignoreCase ? equalsIgnoreCase(expressionChar, nameChar) : expressionChar == nameChar))) {
currentMatches[currentMatch++] = currentState;
}
finished = true;
}
}

if (finished) {
break;
}
if (zeroOrMore) {
currentMatches[currentMatch++] = currentState;
}
if (zeroOrMore || zero) {
currentMatches[currentMatch++] = currentState + 1;
}
if (++expressionOffset == expression.size()) {
currentMatches[currentMatch++] = maxState;
}
}

// Keep currentMatches duplicate-free relative to priorMatches.
if ((priorMatch < matchCount) && (priorMatchCount < currentMatch)) {
while (priorMatchCount < currentMatch) {
size_t previousLength = priorMatches.size();
while ((priorMatch < previousLength) &&
(priorMatches[priorMatch] < currentMatches[priorMatchCount])) {
priorMatch++;
}
priorMatchCount++;
}
}
}

if (currentMatch == 0) {
return false;
}
return false;
}

while (p < pattern.size() && pattern[p] == '*') {
++p;
std::swap(priorMatches, currentMatches);
matchCount = currentMatch;
}
return p == pattern.size();

return priorMatches[matchCount - 1] == maxState;
}

void toFileTime(const struct timespec &ts, FILETIME &out) {
Expand Down Expand Up @@ -370,18 +553,20 @@ bool collectDirectoryMatches(const std::filesystem::path &directory, const std::
outEntries.push_back(std::move(entry));
};

if (wildcardMatchInsensitive(pattern, ".")) {
const std::string expr = translateWin32Expression(pattern);

if (matchWin32Expression(expr, ".")) {
addEntry(".", directory, true);
}
if (wildcardMatchInsensitive(pattern, "..")) {
if (matchWin32Expression(expr, "..")) {
addEntry("..", parentOrSelf(directory), true);
}

std::error_code iterEc;
std::filesystem::directory_iterator end;
for (std::filesystem::directory_iterator it(directory, iterEc); !iterEc && it != end; ++it) {
std::string name = it->path().filename().string();
if (!wildcardMatchInsensitive(pattern, name)) {
if (!matchWin32Expression(expr, name)) {
continue;
}
std::error_code statusEc;
Expand Down
Loading