mirror of
https://github.com/decompals/wibo.git
synced 2025-10-15 14:45:12 +00:00
Rewrite FindFirstFile/FindNextFile (again), add comprehensive tests
This commit is contained in:
parent
f5aa320800
commit
f366e77956
@ -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
|
||||
|
26
dll/crt.cpp
26
dll/crt.cpp
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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
315
test/test_findfile.c
Normal 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;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user