Rewrite FindFirstFile/FindNextFile (again), add comprehensive tests

This commit is contained in:
Luke Street 2025-10-06 17:56:42 -06:00
parent f5aa320800
commit f366e77956
5 changed files with 790 additions and 175 deletions

View File

@ -221,6 +221,17 @@ if(BUILD_TESTING)
${CMAKE_CURRENT_SOURCE_DIR}/test/test_handleapi.c
${CMAKE_CURRENT_SOURCE_DIR}/test/test_assert.h)
add_custom_command(
OUTPUT ${WIBO_TEST_BIN_DIR}/test_findfile.exe
COMMAND ${WIBO_MINGW_CC} -Wall -Wextra -O2
-I${CMAKE_CURRENT_SOURCE_DIR}/test
-o test_findfile.exe
${CMAKE_CURRENT_SOURCE_DIR}/test/test_findfile.c
WORKING_DIRECTORY ${WIBO_TEST_BIN_DIR}
DEPENDS
${CMAKE_CURRENT_SOURCE_DIR}/test/test_findfile.c
${CMAKE_CURRENT_SOURCE_DIR}/test/test_assert.h)
add_custom_command(
OUTPUT ${WIBO_TEST_BIN_DIR}/test_synchapi.exe
COMMAND ${WIBO_MINGW_CC} -Wall -Wextra -O2
@ -376,6 +387,7 @@ if(BUILD_TESTING)
${WIBO_TEST_BIN_DIR}/test_resources.exe
${WIBO_TEST_BIN_DIR}/test_threading.exe
${WIBO_TEST_BIN_DIR}/test_handleapi.exe
${WIBO_TEST_BIN_DIR}/test_findfile.exe
${WIBO_TEST_BIN_DIR}/test_synchapi.exe
${WIBO_TEST_BIN_DIR}/test_processes.exe
${WIBO_TEST_BIN_DIR}/test_heap.exe
@ -430,6 +442,12 @@ if(BUILD_TESTING)
WORKING_DIRECTORY ${WIBO_TEST_BIN_DIR}
DEPENDS wibo.build_fixtures)
add_test(NAME wibo.test_findfile
COMMAND $<TARGET_FILE:wibo> ${WIBO_TEST_BIN_DIR}/test_findfile.exe)
set_tests_properties(wibo.test_findfile PROPERTIES
WORKING_DIRECTORY ${WIBO_TEST_BIN_DIR}
DEPENDS wibo.build_fixtures)
add_test(NAME wibo.test_synchapi
COMMAND $<TARGET_FILE:wibo> ${WIBO_TEST_BIN_DIR}/test_synchapi.exe)
set_tests_properties(wibo.test_synchapi PROPERTIES

View File

@ -193,6 +193,18 @@ char *WIN_ENTRY strcpy(char *dest, const char *src) {
return ::strcpy(dest, src);
}
char *WIN_ENTRY strncpy(char *dest, const char *src, size_t count) {
HOST_CONTEXT_GUARD();
VERBOSE_LOG("strncpy(%p, %p, %zu)\n", dest, src, count);
return ::strncpy(dest, src, count);
}
const char *WIN_ENTRY strrchr(const char *str, int ch) {
HOST_CONTEXT_GUARD();
VERBOSE_LOG("strrchr(%p, %i)\n", str, ch);
return ::strrchr(str, ch);
}
void *WIN_ENTRY malloc(size_t size) {
HOST_CONTEXT_GUARD();
VERBOSE_LOG("malloc(%zu)\n", size);
@ -348,6 +360,13 @@ int WIN_ENTRY __stdio_common_vsprintf(unsigned long long options, char *buffer,
return result;
}
int WIN_ENTRY qsort(void *base, size_t num, size_t size, int (*compar)(const void *, const void *)) {
HOST_CONTEXT_GUARD();
DEBUG_LOG("qsort(%p, %zu, %zu, %p)\n", base, num, size, compar);
::qsort(base, num, size, compar);
return 0;
}
} // namespace crt
static void *resolveByName(const char *name) {
@ -393,6 +412,10 @@ static void *resolveByName(const char *name) {
return (void *)crt::strncmp;
if (strcmp(name, "strcpy") == 0)
return (void *)crt::strcpy;
if (strcmp(name, "strncpy") == 0)
return (void *)crt::strncpy;
if (strcmp(name, "strrchr") == 0)
return (void *)crt::strrchr;
if (strcmp(name, "malloc") == 0)
return (void *)crt::malloc;
if (strcmp(name, "calloc") == 0)
@ -435,6 +458,8 @@ static void *resolveByName(const char *name) {
return (void *)crt::_register_onexit_function;
if (strcmp(name, "_execute_onexit_table") == 0)
return (void *)crt::_execute_onexit_table;
if (strcmp(name, "qsort") == 0)
return (void *)crt::qsort;
return nullptr;
}
@ -448,6 +473,7 @@ wibo::ModuleStub lib_crt = {
"api-ms-win-crt-environment-l1-1-0",
"api-ms-win-crt-math-l1-1-0",
"api-ms-win-crt-private-l1-1-0",
"api-ms-win-crt-utility-l1-1-0",
nullptr,
},
resolveByName,

View File

@ -11,28 +11,29 @@
#include "timeutil.h"
#include <algorithm>
#include <cctype>
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <fcntl.h>
#include <filesystem>
#include <fnmatch.h>
#include <mutex>
#include <optional>
#include <random>
#include <string>
#include <string_view>
#include <sys/stat.h>
#include <sys/time.h>
#include <system_error>
#include <unistd.h>
#include <unordered_map>
#include <vector>
namespace {
using random_shorts_engine =
std::independent_bits_engine<std::default_random_engine, sizeof(unsigned short) * 8, unsigned short>;
constexpr uintptr_t kPseudoFindHandleValue = 1;
const HANDLE kPseudoFindHandle = reinterpret_cast<HANDLE>(kPseudoFindHandleValue);
constexpr uint64_t kWindowsTicksPerSecond = 10000000ULL;
constexpr uint64_t kSecondsBetween1601And1970 = 11644473600ULL;
const FILETIME kDefaultFindFileTime = {
@ -89,12 +90,6 @@ struct timespec changeTimespec(const struct stat &st) {
#endif
}
struct FindFirstFileHandle {
std::filesystem::directory_iterator it;
std::filesystem::directory_iterator end;
std::string pattern;
};
struct FullPathInfo {
std::string path;
size_t filePartOffset = std::string::npos;
@ -135,99 +130,380 @@ bool computeFullPath(const std::string &input, FullPathInfo &outInfo) {
return true;
}
inline bool isPseudoHandle(HANDLE handle) { return reinterpret_cast<uintptr_t>(handle) == kPseudoFindHandleValue; }
struct FindSearchEntry {
std::string name;
std::filesystem::path fullPath;
bool isDirectory = false;
};
inline void setCommonFindDataFields(WIN32_FIND_DATAA &data) {
data.ftCreationTime = kDefaultFindFileTime;
data.ftLastAccessTime = kDefaultFindFileTime;
data.ftLastWriteTime = kDefaultFindFileTime;
data.dwFileAttributes = 0;
data.nFileSizeHigh = 0;
data.nFileSizeLow = 0;
data.dwReserved0 = 0;
data.dwReserved1 = 0;
data.cFileName[0] = '\0';
data.cAlternateFileName[0] = '\0';
struct FindSearchHandle {
bool singleResult = false;
std::vector<FindSearchEntry> entries;
size_t nextIndex = 0;
};
std::mutex g_findHandleMutex;
std::unordered_map<FindSearchHandle *, std::unique_ptr<FindSearchHandle>> g_findHandles;
HANDLE registerFindHandle(std::unique_ptr<FindSearchHandle> handle) {
if (!handle) {
return INVALID_HANDLE_VALUE;
}
FindSearchHandle *raw = handle.get();
std::lock_guard lk(g_findHandleMutex);
g_findHandles.emplace(raw, std::move(handle));
return reinterpret_cast<HANDLE>(raw);
}
inline void setCommonFindDataFields(WIN32_FIND_DATAW &data) {
data.ftCreationTime = kDefaultFindFileTime;
data.ftLastAccessTime = kDefaultFindFileTime;
data.ftLastWriteTime = kDefaultFindFileTime;
data.dwFileAttributes = 0;
data.nFileSizeHigh = 0;
data.nFileSizeLow = 0;
data.dwReserved0 = 0;
data.dwReserved1 = 0;
data.cFileName[0] = 0;
data.cAlternateFileName[0] = 0;
FindSearchHandle *lookupFindHandleLocked(HANDLE handle) {
if (handle == nullptr) {
return nullptr;
}
auto *raw = reinterpret_cast<FindSearchHandle *>(handle);
auto it = g_findHandles.find(raw);
if (it == g_findHandles.end()) {
return nullptr;
}
return it->second.get();
}
DWORD computeAttributesAndSize(const std::filesystem::path &path, DWORD &sizeHigh, DWORD &sizeLow) {
std::error_code ec;
auto status = std::filesystem::status(path, ec);
uint64_t fileSize = 0;
std::unique_ptr<FindSearchHandle> detachFindHandle(HANDLE handle) {
std::lock_guard lk(g_findHandleMutex);
auto *raw = reinterpret_cast<FindSearchHandle *>(handle);
auto it = g_findHandles.find(raw);
if (it == g_findHandles.end()) {
return nullptr;
}
auto owned = std::move(it->second);
g_findHandles.erase(it);
return owned;
}
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) {
value.remove_prefix(4);
}
return containsWildcard(value);
}
inline char toLowerAscii(char ch) { return static_cast<char>(std::tolower(static_cast<unsigned char>(ch))); }
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;
}
if (pc == '*') {
star = p++;
match = t;
continue;
}
if (equalsIgnoreCase(pc, text[t])) {
++p;
++t;
continue;
}
}
if (star != std::string_view::npos) {
p = star + 1;
t = ++match;
continue;
}
return false;
}
while (p < pattern.size() && pattern[p] == '*') {
++p;
}
return p == pattern.size();
}
void toFileTime(const struct timespec &ts, FILETIME &out) {
int64_t seconds = static_cast<int64_t>(ts.tv_sec) + static_cast<int64_t>(kSecondsBetween1601And1970);
if (seconds < 0) {
seconds = 0;
}
uint64_t ticks = static_cast<uint64_t>(seconds) * kWindowsTicksPerSecond;
ticks += static_cast<uint64_t>(ts.tv_nsec > 0 ? ts.tv_nsec / 100 : 0);
out.dwLowDateTime = static_cast<DWORD>(ticks & 0xFFFFFFFFULL);
out.dwHighDateTime = static_cast<DWORD>(ticks >> 32);
}
template <typename FindData> void resetFindDataStruct(FindData &data) { std::memset(&data, 0, sizeof(FindData)); }
void assignFileName(WIN32_FIND_DATAA &data, const std::string &name) {
size_t count = std::min(name.size(), static_cast<size_t>(MAX_PATH - 1));
std::memcpy(data.cFileName, name.data(), count);
data.cFileName[count] = '\0';
}
void assignFileName(WIN32_FIND_DATAW &data, const std::string &name) {
auto wide = stringToWideString(name.c_str(), name.size());
size_t length = std::min<size_t>(wstrlen(wide.data()), MAX_PATH - 1);
wstrncpy(data.cFileName, wide.data(), length);
data.cFileName[length] = 0;
}
void clearAlternateName(WIN32_FIND_DATAA &data) { data.cAlternateFileName[0] = '\0'; }
void clearAlternateName(WIN32_FIND_DATAW &data) { data.cAlternateFileName[0] = 0; }
DWORD buildFileAttributes(const struct stat &st, bool isDirectory) {
DWORD attributes = 0;
if (status.type() == std::filesystem::file_type::directory) {
mode_t mode = st.st_mode;
if (S_ISDIR(mode) || isDirectory) {
attributes |= FILE_ATTRIBUTE_DIRECTORY;
}
if (status.type() == std::filesystem::file_type::regular) {
attributes |= FILE_ATTRIBUTE_NORMAL;
fileSize = std::filesystem::file_size(path, ec);
if (S_ISREG(mode) && !isDirectory) {
attributes |= FILE_ATTRIBUTE_ARCHIVE;
}
if ((mode & S_IWUSR) == 0) {
attributes |= FILE_ATTRIBUTE_READONLY;
}
if (attributes == 0) {
attributes = FILE_ATTRIBUTE_NORMAL;
}
sizeHigh = static_cast<DWORD>(fileSize >> 32);
sizeLow = static_cast<DWORD>(fileSize);
return attributes;
}
void setFindFileDataFromPath(const std::filesystem::path &path, WIN32_FIND_DATAA &data) {
setCommonFindDataFields(data);
data.dwFileAttributes = computeAttributesAndSize(path, data.nFileSizeHigh, data.nFileSizeLow);
std::string fileName = path.filename().string();
if (fileName.size() >= MAX_PATH) {
fileName.resize(MAX_PATH - 1);
template <typename FindData> void populateFromStat(const FindSearchEntry &entry, const struct stat &st, FindData &out) {
out.dwFileAttributes = buildFileAttributes(st, entry.isDirectory);
uint64_t fileSize = (entry.isDirectory || !S_ISREG(st.st_mode)) ? 0ULL : static_cast<uint64_t>(st.st_size);
out.nFileSizeHigh = static_cast<DWORD>(fileSize >> 32);
out.nFileSizeLow = static_cast<DWORD>(fileSize & 0xFFFFFFFFULL);
toFileTime(changeTimespec(st), out.ftCreationTime);
toFileTime(accessTimespec(st), out.ftLastAccessTime);
toFileTime(modifyTimespec(st), out.ftLastWriteTime);
}
template <typename FindData> void populateFindData(const FindSearchEntry &entry, FindData &out) {
resetFindDataStruct(out);
std::string nativePath = entry.fullPath.empty() ? std::string() : entry.fullPath.u8string();
struct stat st{};
if (!nativePath.empty() && stat(nativePath.c_str(), &st) == 0) {
populateFromStat(entry, st, out);
} else {
out.dwFileAttributes = entry.isDirectory ? FILE_ATTRIBUTE_DIRECTORY : FILE_ATTRIBUTE_NORMAL;
out.ftCreationTime = kDefaultFindFileTime;
out.ftLastAccessTime = kDefaultFindFileTime;
out.ftLastWriteTime = kDefaultFindFileTime;
out.nFileSizeHigh = 0;
out.nFileSizeLow = 0;
}
std::strncpy(data.cFileName, fileName.c_str(), MAX_PATH);
data.cFileName[MAX_PATH - 1] = '\0';
std::strncpy(data.cAlternateFileName, "8P3FMTFN.BAD", sizeof(data.cAlternateFileName));
data.cAlternateFileName[sizeof(data.cAlternateFileName) - 1] = '\0';
assignFileName(out, entry.name);
clearAlternateName(out);
}
void setFindFileDataFromPath(const std::filesystem::path &path, WIN32_FIND_DATAW &data) {
setCommonFindDataFields(data);
data.dwFileAttributes = computeAttributesAndSize(path, data.nFileSizeHigh, data.nFileSizeLow);
std::string fileName = path.filename().string();
auto wideName = stringToWideString(fileName.c_str());
size_t copyLen = std::min<size_t>(MAX_PATH - 1, wstrlen(wideName.data()));
wstrncpy(data.cFileName, wideName.data(), copyLen);
data.cFileName[copyLen] = 0;
auto wideAlt = stringToWideString("8P3FMTFN.BAD");
copyLen = std::min<size_t>(sizeof(data.cAlternateFileName) / sizeof(data.cAlternateFileName[0]) - 1,
wstrlen(wideAlt.data()));
wstrncpy(data.cAlternateFileName, wideAlt.data(), copyLen);
data.cAlternateFileName[copyLen] = 0;
std::filesystem::path parentOrSelf(const std::filesystem::path &path) {
auto parent = path.parent_path();
if (parent.empty()) {
return path;
}
return parent;
}
bool nextMatch(FindFirstFileHandle &handle, std::filesystem::path &outPath) {
for (; handle.it != handle.end; ++handle.it) {
const auto current = *handle.it;
if (fnmatch(handle.pattern.c_str(), current.path().filename().c_str(), 0) == 0) {
outPath = current.path();
++handle.it;
return true;
std::filesystem::path resolvedPath(const std::filesystem::path &path) {
std::error_code ec;
auto canonical = std::filesystem::weakly_canonical(path, ec);
if (!ec) {
return canonical;
}
auto absolute = std::filesystem::absolute(path, ec);
if (!ec) {
return absolute;
}
return path;
}
std::string determineDisplayName(const std::filesystem::path &path, const std::string &filePart) {
std::string name = path.filename().string();
if (name.empty() || name == "." || name == "..") {
std::error_code ec;
auto absolute = std::filesystem::absolute(path, ec);
if (!ec) {
auto absoluteName = absolute.filename().string();
if (!absoluteName.empty()) {
name = absoluteName;
}
}
}
return false;
if (name.empty()) {
name = filePart;
}
return name;
}
bool initializeEnumeration(const std::filesystem::path &parent, const std::string &pattern, FindFirstFileHandle &handle,
std::filesystem::path &firstMatch) {
if (pattern.empty()) {
bool collectDirectoryMatches(const std::filesystem::path &directory, const std::string &pattern,
std::vector<FindSearchEntry> &outEntries) {
auto addEntry = [&](const std::string &name, const std::filesystem::path &path, bool isDirectory) {
FindSearchEntry entry;
entry.name = name;
entry.fullPath = resolvedPath(path);
entry.isDirectory = isDirectory;
outEntries.push_back(std::move(entry));
};
if (wildcardMatchInsensitive(pattern, ".")) {
addEntry(".", directory, true);
}
if (wildcardMatchInsensitive(pattern, "..")) {
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)) {
continue;
}
std::error_code statusEc;
bool isDir = it->is_directory(statusEc);
if (statusEc) {
isDir = false;
}
FindSearchEntry entry;
entry.name = name;
entry.fullPath = resolvedPath(it->path());
entry.isDirectory = isDir;
outEntries.push_back(std::move(entry));
}
if (iterEc) {
wibo::lastError = wibo::winErrorFromErrno(iterEc.value());
return false;
}
handle = FindFirstFileHandle{std::filesystem::directory_iterator(parent), std::filesystem::directory_iterator(),
pattern};
return nextMatch(handle, firstMatch);
return true;
}
template <typename FindData> HANDLE findFirstFileCommon(const std::string &rawInput, FindData *lpFindFileData) {
if (!lpFindFileData) {
wibo::lastError = ERROR_INVALID_PARAMETER;
return INVALID_HANDLE_VALUE;
}
if (rawInput.empty()) {
wibo::lastError = ERROR_PATH_NOT_FOUND;
return INVALID_HANDLE_VALUE;
}
std::string input = rawInput;
std::replace(input.begin(), input.end(), '/', '\\');
if (input.empty()) {
wibo::lastError = ERROR_PATH_NOT_FOUND;
return INVALID_HANDLE_VALUE;
}
if (!input.empty() && input.back() == '\\') {
wibo::lastError = ERROR_FILE_NOT_FOUND;
return INVALID_HANDLE_VALUE;
}
std::string directoryPart;
std::string filePart;
size_t lastSlash = input.find_last_of('\\');
if (lastSlash == std::string::npos) {
directoryPart = ".";
filePart = input;
} else {
directoryPart = input.substr(0, lastSlash);
filePart = input.substr(lastSlash + 1);
if (directoryPart.empty()) {
directoryPart = "\\";
} else if (lastSlash == 2 && input.size() >= 3 && input[1] == ':') {
directoryPart = input.substr(0, lastSlash + 1);
}
}
if (filePart.empty()) {
wibo::lastError = ERROR_FILE_NOT_FOUND;
return INVALID_HANDLE_VALUE;
}
if (containsWildcardOutsideExtendedPrefix(directoryPart)) {
wibo::lastError = ERROR_INVALID_NAME;
return INVALID_HANDLE_VALUE;
}
if (directoryPart.empty()) {
directoryPart = ".";
}
std::filesystem::path hostDirectory = resolvedPath(files::pathFromWindows(directoryPart.c_str()));
std::error_code dirStatusEc;
auto dirStatus = std::filesystem::status(hostDirectory, dirStatusEc);
if (dirStatusEc) {
wibo::lastError = wibo::winErrorFromErrno(dirStatusEc.value());
return INVALID_HANDLE_VALUE;
}
if (dirStatus.type() == std::filesystem::file_type::not_found) {
wibo::lastError = ERROR_PATH_NOT_FOUND;
return INVALID_HANDLE_VALUE;
}
if (dirStatus.type() != std::filesystem::file_type::directory) {
wibo::lastError = ERROR_PATH_NOT_FOUND;
return INVALID_HANDLE_VALUE;
}
bool hasWildcards = containsWildcard(filePart);
if (!hasWildcards) {
std::filesystem::path targetPath = resolvedPath(files::pathFromWindows(input.c_str()));
std::error_code targetEc;
auto targetStatus = std::filesystem::status(targetPath, targetEc);
if (targetEc) {
wibo::lastError = wibo::winErrorFromErrno(targetEc.value());
return INVALID_HANDLE_VALUE;
}
if (targetStatus.type() == std::filesystem::file_type::not_found) {
wibo::lastError = ERROR_FILE_NOT_FOUND;
return INVALID_HANDLE_VALUE;
}
FindSearchEntry entry;
entry.fullPath = targetPath;
entry.isDirectory = targetStatus.type() == std::filesystem::file_type::directory;
entry.name = determineDisplayName(targetPath, filePart);
populateFindData(entry, *lpFindFileData);
wibo::lastError = ERROR_SUCCESS;
auto state = std::make_unique<FindSearchHandle>();
state->singleResult = true;
return registerFindHandle(std::move(state));
}
std::vector<FindSearchEntry> matches;
if (!collectDirectoryMatches(hostDirectory, filePart, matches)) {
return INVALID_HANDLE_VALUE;
}
if (matches.empty()) {
wibo::lastError = ERROR_FILE_NOT_FOUND;
return INVALID_HANDLE_VALUE;
}
populateFindData(matches[0], *lpFindFileData);
wibo::lastError = ERROR_SUCCESS;
auto state = std::make_unique<FindSearchHandle>();
state->entries = std::move(matches);
state->nextIndex = 1;
return registerFindHandle(std::move(state));
}
std::optional<DWORD> stdHandleForConsoleDevice(const std::string &name, DWORD desiredAccess) {
@ -576,7 +852,7 @@ BOOL WIN_FUNC ReadFile(HANDLE hFile, LPVOID lpBuffer, DWORD nNumberOfBytesToRead
signalOverlappedEvent(lpOverlapped);
}
DEBUG_LOG("-> %u bytes read, error %d\n", io.bytesTransferred, wibo::lastError);
DEBUG_LOG("-> %u bytes read, error %d\n", io.bytesTransferred, io.unixError == 0 ? 0 : wibo::lastError);
return io.unixError == 0;
}
@ -1454,123 +1730,104 @@ DWORD WIN_FUNC GetTempPathA(DWORD nBufferLength, LPSTR lpBuffer) {
HANDLE WIN_FUNC FindFirstFileA(LPCSTR lpFileName, LPWIN32_FIND_DATAA lpFindFileData) {
HOST_CONTEXT_GUARD();
DEBUG_LOG("FindFirstFileA(%s, %p)", lpFileName ? lpFileName : "(null)", lpFindFileData);
if (!lpFileName || !lpFindFileData) {
if (!lpFindFileData) {
wibo::lastError = ERROR_INVALID_PARAMETER;
DEBUG_LOG(" -> ERROR_INVALID_PARAMETER\n");
return INVALID_HANDLE_VALUE;
}
std::filesystem::path hostPath = files::pathFromWindows(lpFileName);
DEBUG_LOG(" -> %s\n", hostPath.c_str());
std::error_code ec;
auto status = std::filesystem::status(hostPath, ec);
setCommonFindDataFields(*lpFindFileData);
if (status.type() == std::filesystem::file_type::regular) {
setFindFileDataFromPath(hostPath, *lpFindFileData);
return kPseudoFindHandle;
}
std::filesystem::path parent = hostPath.parent_path();
if (parent.empty()) {
parent = ".";
}
if (!std::filesystem::exists(parent)) {
if (!lpFileName) {
wibo::lastError = ERROR_PATH_NOT_FOUND;
DEBUG_LOG(" -> ERROR_PATH_NOT_FOUND\n");
return INVALID_HANDLE_VALUE;
}
std::filesystem::path match;
auto *handle = new FindFirstFileHandle();
if (!initializeEnumeration(parent, hostPath.filename().string(), *handle, match)) {
delete handle;
wibo::lastError = ERROR_FILE_NOT_FOUND;
return INVALID_HANDLE_VALUE;
}
setFindFileDataFromPath(match, *lpFindFileData);
return reinterpret_cast<HANDLE>(handle);
HANDLE handle = findFirstFileCommon(std::string(lpFileName), lpFindFileData);
DEBUG_LOG(" -> %p\n", handle);
return handle;
}
HANDLE WIN_FUNC FindFirstFileW(LPCWSTR lpFileName, LPWIN32_FIND_DATAW lpFindFileData) {
HOST_CONTEXT_GUARD();
DEBUG_LOG("FindFirstFileW(%p, %p)", lpFileName, lpFindFileData);
if (!lpFileName || !lpFindFileData) {
if (!lpFindFileData) {
wibo::lastError = ERROR_INVALID_PARAMETER;
DEBUG_LOG(" -> ERROR_INVALID_PARAMETER\n");
return INVALID_HANDLE_VALUE;
}
if (!lpFileName) {
wibo::lastError = ERROR_PATH_NOT_FOUND;
DEBUG_LOG(" -> ERROR_PATH_NOT_FOUND\n");
return INVALID_HANDLE_VALUE;
}
std::string narrowName = wideStringToString(lpFileName);
std::filesystem::path hostPath = files::pathFromWindows(narrowName.c_str());
DEBUG_LOG(", %s -> %s\n", narrowName.c_str(), hostPath.c_str());
std::error_code ec;
auto status = std::filesystem::status(hostPath, ec);
setCommonFindDataFields(*lpFindFileData);
if (status.type() == std::filesystem::file_type::regular) {
setFindFileDataFromPath(hostPath, *lpFindFileData);
return kPseudoFindHandle;
}
std::filesystem::path parent = hostPath.parent_path();
if (parent.empty()) {
parent = ".";
}
if (!std::filesystem::exists(parent)) {
wibo::lastError = ERROR_PATH_NOT_FOUND;
return INVALID_HANDLE_VALUE;
}
std::filesystem::path match;
auto *handle = new FindFirstFileHandle();
if (!initializeEnumeration(parent, hostPath.filename().string(), *handle, match)) {
delete handle;
wibo::lastError = ERROR_FILE_NOT_FOUND;
return INVALID_HANDLE_VALUE;
}
setFindFileDataFromPath(match, *lpFindFileData);
return reinterpret_cast<HANDLE>(handle);
HANDLE handle = findFirstFileCommon(narrowName, lpFindFileData);
DEBUG_LOG(" -> %p\n", handle);
return handle;
}
HANDLE WIN_FUNC FindFirstFileExA(LPCSTR lpFileName, FINDEX_INFO_LEVELS fInfoLevelId, LPVOID lpFindFileData,
FINDEX_SEARCH_OPS fSearchOp, LPVOID lpSearchFilter, DWORD dwAdditionalFlags) {
HOST_CONTEXT_GUARD();
DEBUG_LOG("FindFirstFileExA(%s, %d, %p, %d, %p, 0x%x) -> ", lpFileName ? lpFileName : "(null)", fInfoLevelId,
DEBUG_LOG("FindFirstFileExA(%s, %d, %p, %d, %p, 0x%x)", lpFileName ? lpFileName : "(null)", fInfoLevelId,
lpFindFileData, fSearchOp, lpSearchFilter, dwAdditionalFlags);
(void)fInfoLevelId;
(void)fSearchOp;
(void)lpSearchFilter;
(void)dwAdditionalFlags;
return FindFirstFileA(lpFileName, static_cast<LPWIN32_FIND_DATAA>(lpFindFileData));
if (!lpFindFileData) {
DEBUG_LOG(" -> ERROR_INVALID_PARAMETER\n");
wibo::lastError = ERROR_INVALID_PARAMETER;
return INVALID_HANDLE_VALUE;
}
if (!lpFileName) {
DEBUG_LOG(" -> ERROR_PATH_NOT_FOUND\n");
wibo::lastError = ERROR_PATH_NOT_FOUND;
return INVALID_HANDLE_VALUE;
}
if (fInfoLevelId != FindExInfoStandard) {
DEBUG_LOG(" -> ERROR_INVALID_PARAMETER\n");
wibo::lastError = ERROR_INVALID_PARAMETER;
return INVALID_HANDLE_VALUE;
}
if (fSearchOp != FindExSearchNameMatch) {
DEBUG_LOG(" -> ERROR_INVALID_PARAMETER\n");
wibo::lastError = ERROR_INVALID_PARAMETER;
return INVALID_HANDLE_VALUE;
}
if (lpSearchFilter) {
DEBUG_LOG(" -> ERROR_INVALID_PARAMETER\n");
wibo::lastError = ERROR_INVALID_PARAMETER;
return INVALID_HANDLE_VALUE;
}
if (dwAdditionalFlags != 0) {
DEBUG_LOG(" -> ERROR_INVALID_PARAMETER\n");
wibo::lastError = ERROR_INVALID_PARAMETER;
return INVALID_HANDLE_VALUE;
}
auto *findData = static_cast<LPWIN32_FIND_DATAA>(lpFindFileData);
return findFirstFileCommon(std::string(lpFileName), findData);
}
BOOL WIN_FUNC FindNextFileA(HANDLE hFindFile, LPWIN32_FIND_DATAA lpFindFileData) {
HOST_CONTEXT_GUARD();
DEBUG_LOG("FindNextFileA(%p, %p)\n", hFindFile, lpFindFileData);
if (!lpFindFileData) {
DEBUG_LOG(" -> ERROR_INVALID_PARAMETER\n");
wibo::lastError = ERROR_INVALID_PARAMETER;
return FALSE;
}
if (isPseudoHandle(hFindFile)) {
wibo::lastError = ERROR_NO_MORE_FILES;
return FALSE;
}
auto *handle = reinterpret_cast<FindFirstFileHandle *>(hFindFile);
if (!handle) {
std::lock_guard lk(g_findHandleMutex);
auto *state = lookupFindHandleLocked(hFindFile);
if (!state) {
DEBUG_LOG(" -> ERROR_INVALID_HANDLE\n");
wibo::lastError = ERROR_INVALID_HANDLE;
return FALSE;
}
std::filesystem::path match;
if (!nextMatch(*handle, match)) {
if (state->singleResult || state->nextIndex >= state->entries.size()) {
DEBUG_LOG(" -> ERROR_NO_MORE_FILES\n");
wibo::lastError = ERROR_NO_MORE_FILES;
return FALSE;
}
setFindFileDataFromPath(match, *lpFindFileData);
populateFindData(state->entries[state->nextIndex++], *lpFindFileData);
return TRUE;
}
@ -1581,40 +1838,38 @@ BOOL WIN_FUNC FindNextFileW(HANDLE hFindFile, LPWIN32_FIND_DATAW lpFindFileData)
wibo::lastError = ERROR_INVALID_PARAMETER;
return FALSE;
}
if (isPseudoHandle(hFindFile)) {
wibo::lastError = ERROR_NO_MORE_FILES;
return FALSE;
}
auto *handle = reinterpret_cast<FindFirstFileHandle *>(hFindFile);
if (!handle) {
std::lock_guard lk(g_findHandleMutex);
auto *state = lookupFindHandleLocked(hFindFile);
if (!state) {
DEBUG_LOG(" -> ERROR_INVALID_HANDLE\n");
wibo::lastError = ERROR_INVALID_HANDLE;
return FALSE;
}
std::filesystem::path match;
if (!nextMatch(*handle, match)) {
if (state->singleResult || state->nextIndex >= state->entries.size()) {
DEBUG_LOG(" -> ERROR_NO_MORE_FILES\n");
wibo::lastError = ERROR_NO_MORE_FILES;
return FALSE;
}
setFindFileDataFromPath(match, *lpFindFileData);
populateFindData(state->entries[state->nextIndex++], *lpFindFileData);
return TRUE;
}
BOOL WIN_FUNC FindClose(HANDLE hFindFile) {
HOST_CONTEXT_GUARD();
DEBUG_LOG("FindClose(%p)\n", hFindFile);
if (isPseudoHandle(hFindFile) || hFindFile == nullptr) {
return TRUE;
}
auto *handle = reinterpret_cast<FindFirstFileHandle *>(hFindFile);
if (!handle) {
if (hFindFile == nullptr) {
DEBUG_LOG(" -> ERROR_INVALID_HANDLE\n");
wibo::lastError = ERROR_INVALID_HANDLE;
return FALSE;
}
auto owned = detachFindHandle(hFindFile);
if (!owned) {
DEBUG_LOG(" -> ERROR_INVALID_HANDLE\n");
wibo::lastError = ERROR_INVALID_HANDLE;
return FALSE;
}
delete handle;
return TRUE;
}

View File

@ -22,6 +22,7 @@
#define ERROR_CALL_NOT_IMPLEMENTED 120
#define ERROR_BUFFER_OVERFLOW 111
#define ERROR_INSUFFICIENT_BUFFER 122
#define ERROR_INVALID_NAME 123
#define ERROR_IO_INCOMPLETE 996
#define ERROR_IO_PENDING 997
#define ERROR_OPERATION_ABORTED 995

315
test/test_findfile.c Normal file
View File

@ -0,0 +1,315 @@
#include "test_assert.h"
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <windows.h>
static char g_original_dir[MAX_PATH];
static char g_fixture_dir[MAX_PATH];
static const char *leaf_name(const char *path) {
const char *back = strrchr(path, '\\');
const char *forward = strrchr(path, '/');
const char *candidate = back;
if (!candidate || (forward && forward > candidate)) {
candidate = forward;
}
if (candidate && candidate[1] != '\0') {
return candidate + 1;
}
return path;
}
static void join_path(char *buffer, size_t buffer_size, const char *a, const char *b) {
int written = snprintf(buffer, buffer_size, "%s\\%s", a, b);
TEST_CHECK_MSG(written > 0 && (size_t)written < buffer_size, "join_path overflow");
}
static void create_file_with_content(const char *path, const char *content) {
HANDLE handle = CreateFileA(path, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
TEST_CHECK_MSG(handle != INVALID_HANDLE_VALUE, "CreateFileA(%s) failed", path);
DWORD to_write = (DWORD)strlen(content);
DWORD written = 0;
BOOL ok = WriteFile(handle, content, to_write, &written, NULL);
TEST_CHECK(ok);
TEST_CHECK_EQ(to_write, written);
TEST_CHECK(CloseHandle(handle));
}
static void setup_fixture(void) {
DWORD len = GetCurrentDirectoryA(sizeof(g_original_dir), g_original_dir);
TEST_CHECK(len > 0 && len < sizeof(g_original_dir));
char temp_path[MAX_PATH];
DWORD tmp_len = GetTempPathA(sizeof(temp_path), temp_path);
TEST_CHECK(tmp_len > 0 && tmp_len < sizeof(temp_path));
char temp_name[MAX_PATH];
UINT unique = GetTempFileNameA(temp_path, "wbo", 0, temp_name);
TEST_CHECK(unique != 0);
TEST_CHECK(DeleteFileA(temp_name));
TEST_CHECK(CreateDirectoryA(temp_name, NULL));
strncpy(g_fixture_dir, temp_name, sizeof(g_fixture_dir));
g_fixture_dir[sizeof(g_fixture_dir) - 1] = '\0';
TEST_CHECK(SetCurrentDirectoryA(g_fixture_dir));
TEST_CHECK(CreateDirectoryA("dir", NULL));
TEST_CHECK(CreateDirectoryA("dir\\child", NULL));
TEST_CHECK(CreateDirectoryA("dir_extra", NULL));
create_file_with_content("dir\\file.txt", "file.txt\n");
create_file_with_content("dir\\file.bin", "file.bin\n");
create_file_with_content("dir\\data01.txt", "data01\n");
create_file_with_content("dir\\data02.txt", "data02\n");
create_file_with_content("dir\\data10.txt", "data10\n");
create_file_with_content("dir\\child\\nested.txt", "nested\n");
create_file_with_content("dir_extra\\other.txt", "other\n");
}
static void cleanup_fixture(void) {
TEST_CHECK(SetCurrentDirectoryA(g_original_dir));
char path[MAX_PATH];
join_path(path, sizeof(path), g_fixture_dir, "dir\\child\\nested.txt");
DeleteFileA(path);
join_path(path, sizeof(path), g_fixture_dir, "dir\\child");
RemoveDirectoryA(path);
join_path(path, sizeof(path), g_fixture_dir, "dir\\file.txt");
DeleteFileA(path);
join_path(path, sizeof(path), g_fixture_dir, "dir\\file.bin");
DeleteFileA(path);
join_path(path, sizeof(path), g_fixture_dir, "dir\\data01.txt");
DeleteFileA(path);
join_path(path, sizeof(path), g_fixture_dir, "dir\\data02.txt");
DeleteFileA(path);
join_path(path, sizeof(path), g_fixture_dir, "dir\\data10.txt");
DeleteFileA(path);
join_path(path, sizeof(path), g_fixture_dir, "dir");
RemoveDirectoryA(path);
join_path(path, sizeof(path), g_fixture_dir, "dir_extra\\other.txt");
DeleteFileA(path);
join_path(path, sizeof(path), g_fixture_dir, "dir_extra");
RemoveDirectoryA(path);
RemoveDirectoryA(g_fixture_dir);
}
static HANDLE find_first_checked(const char *pattern, WIN32_FIND_DATAA *out_data) {
SetLastError(0xDEADBEEF);
HANDLE handle = FindFirstFileA(pattern, out_data);
TEST_CHECK_MSG(handle != INVALID_HANDLE_VALUE, "FindFirstFileA failed for %s (err=%lu)", pattern, GetLastError());
return handle;
}
static void test_empty_pattern(void) {
WIN32_FIND_DATAA data;
SetLastError(0xDEADBEEF);
HANDLE handle = FindFirstFileA("", &data);
TEST_CHECK(handle == INVALID_HANDLE_VALUE);
TEST_CHECK_EQ(ERROR_PATH_NOT_FOUND, GetLastError());
}
static void test_null_pattern(void) {
WIN32_FIND_DATAA data;
SetLastError(0xDEADBEEF);
HANDLE handle = FindFirstFileA(NULL, &data);
TEST_CHECK(handle == INVALID_HANDLE_VALUE);
TEST_CHECK_EQ(ERROR_PATH_NOT_FOUND, GetLastError());
}
static void test_dot_pattern(void) {
WIN32_FIND_DATAA data;
HANDLE handle = find_first_checked(".", &data);
TEST_CHECK(handle != INVALID_HANDLE_VALUE);
TEST_CHECK(data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY);
TEST_CHECK_STR_EQ(leaf_name(g_fixture_dir), data.cFileName);
SetLastError(0xDEADBEEF);
TEST_CHECK(!FindNextFileA(handle, &data));
TEST_CHECK_EQ(ERROR_NO_MORE_FILES, GetLastError());
TEST_CHECK(FindClose(handle));
}
static void test_trailing_slash(void) {
WIN32_FIND_DATAA data;
SetLastError(0xDEADBEEF);
HANDLE handle = FindFirstFileA("dir\\", &data);
TEST_CHECK(handle == INVALID_HANDLE_VALUE);
TEST_CHECK_EQ(ERROR_FILE_NOT_FOUND, GetLastError());
SetLastError(0xDEADBEEF);
handle = FindFirstFileA("dir/", &data);
TEST_CHECK(handle == INVALID_HANDLE_VALUE);
TEST_CHECK_EQ(ERROR_FILE_NOT_FOUND, GetLastError());
}
static void test_trailing_dot(void) {
WIN32_FIND_DATAA data;
HANDLE handle = find_first_checked("dir\\.", &data);
TEST_CHECK(handle != INVALID_HANDLE_VALUE);
TEST_CHECK(data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY);
TEST_CHECK_STR_EQ("dir", data.cFileName);
TEST_CHECK(FindClose(handle));
handle = find_first_checked("dir/.", &data);
TEST_CHECK(handle != INVALID_HANDLE_VALUE);
TEST_CHECK(data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY);
TEST_CHECK_STR_EQ("dir", data.cFileName);
TEST_CHECK(FindClose(handle));
}
static void test_direct_file_paths(void) {
WIN32_FIND_DATAA data;
HANDLE handle = find_first_checked("dir\\file.txt", &data);
TEST_CHECK(handle != INVALID_HANDLE_VALUE);
TEST_CHECK((data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0);
TEST_CHECK_STR_EQ("file.txt", data.cFileName);
SetLastError(0xDEADBEEF);
TEST_CHECK(!FindNextFileA(handle, &data));
TEST_CHECK_EQ(ERROR_NO_MORE_FILES, GetLastError());
TEST_CHECK(FindClose(handle));
handle = find_first_checked("dir/file.txt", &data);
TEST_CHECK(handle != INVALID_HANDLE_VALUE);
TEST_CHECK((data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0);
TEST_CHECK_STR_EQ("file.txt", data.cFileName);
SetLastError(0xDEADBEEF);
TEST_CHECK(!FindNextFileA(handle, &data));
TEST_CHECK_EQ(ERROR_NO_MORE_FILES, GetLastError());
TEST_CHECK(FindClose(handle));
}
static int compare_strings(const void *a, const void *b) {
const char *sa = (const char *)a;
const char *sb = (const char *)b;
return strcmp(sa, sb);
}
static void collect_matches(const char *pattern, char matches[][MAX_PATH], size_t *out_count) {
*out_count = 0;
WIN32_FIND_DATAA data;
SetLastError(0xDEADBEEF);
HANDLE handle = FindFirstFileA(pattern, &data);
if (handle == INVALID_HANDLE_VALUE) {
*out_count = 0;
return;
}
do {
strncpy(matches[*out_count], data.cFileName, MAX_PATH);
matches[*out_count][MAX_PATH - 1] = '\0';
(*out_count)++;
TEST_CHECK(*out_count < 64);
} while (FindNextFileA(handle, &data));
TEST_CHECK_EQ(ERROR_NO_MORE_FILES, GetLastError());
TEST_CHECK(FindClose(handle));
}
static void test_wildcard_star(void) {
char matches[64][MAX_PATH];
size_t count = 0;
collect_matches("dir\\*.txt", matches, &count);
TEST_CHECK(count >= 3);
qsort(matches, count, sizeof(matches[0]), compare_strings);
bool saw_data01 = false;
bool saw_data02 = false;
bool saw_file = false;
for (size_t i = 0; i < count; ++i) {
saw_data01 = saw_data01 || strcmp(matches[i], "data01.txt") == 0;
saw_data02 = saw_data02 || strcmp(matches[i], "data02.txt") == 0;
saw_file = saw_file || strcmp(matches[i], "file.txt") == 0;
}
TEST_CHECK(saw_data01);
TEST_CHECK(saw_data02);
TEST_CHECK(saw_file);
}
static void test_wildcard_question(void) {
char matches[64][MAX_PATH];
size_t count = 0;
collect_matches("dir\\data0?.txt", matches, &count);
TEST_CHECK_EQ(2, count);
qsort(matches, count, sizeof(matches[0]), compare_strings);
TEST_CHECK_STR_EQ("data01.txt", matches[0]);
TEST_CHECK_STR_EQ("data02.txt", matches[1]);
count = 0;
collect_matches("dir\\.\\data1?.txt", matches, &count);
TEST_CHECK_EQ(1, count);
TEST_CHECK_STR_EQ("data10.txt", matches[0]);
count = 0;
collect_matches("dir\\child\\..\\data??.txt", matches, &count);
TEST_CHECK_EQ(3, count);
qsort(matches, count, sizeof(matches[0]), compare_strings);
TEST_CHECK_STR_EQ("data01.txt", matches[0]);
TEST_CHECK_STR_EQ("data02.txt", matches[1]);
TEST_CHECK_STR_EQ("data10.txt", matches[2]);
}
static void test_wildcard_in_directory_segment(void) {
WIN32_FIND_DATAA data;
SetLastError(0xDEADBEEF);
HANDLE handle = FindFirstFileA("dir*\\file.txt", &data);
TEST_CHECK(handle == INVALID_HANDLE_VALUE);
TEST_CHECK_EQ(ERROR_INVALID_NAME, GetLastError());
SetLastError(0xDEADBEEF);
handle = FindFirstFileA("dir*\\child\\nested.txt", &data);
TEST_CHECK(handle == INVALID_HANDLE_VALUE);
TEST_CHECK_EQ(ERROR_INVALID_NAME, GetLastError());
}
static void test_directory_iteration_includes_special_entries(void) {
char matches[64][MAX_PATH];
size_t count = 0;
collect_matches("dir\\*", matches, &count);
TEST_CHECK(count >= 5);
bool saw_dot = false;
bool saw_dotdot = false;
bool saw_child = false;
for (size_t i = 0; i < count; ++i) {
saw_dot = saw_dot || strcmp(matches[i], ".") == 0;
saw_dotdot = saw_dotdot || strcmp(matches[i], "..") == 0;
saw_child = saw_child || strcmp(matches[i], "child") == 0;
}
TEST_CHECK(saw_dot);
TEST_CHECK(saw_dotdot);
TEST_CHECK(saw_child);
}
static void test_findclose_invalid_handle(void) {
SetLastError(0xDEADBEEF);
TEST_CHECK(!FindClose(NULL));
TEST_CHECK_EQ(ERROR_INVALID_HANDLE, GetLastError());
}
int main(void) {
setup_fixture();
test_empty_pattern();
test_null_pattern();
test_dot_pattern();
test_trailing_slash();
test_trailing_dot();
test_direct_file_paths();
test_wildcard_star();
test_wildcard_question();
test_wildcard_in_directory_segment();
test_directory_iteration_includes_special_entries();
test_findclose_invalid_handle();
cleanup_fixture();
return EXIT_SUCCESS;
}