diff --git a/CMakeLists.txt b/CMakeLists.txt index d5d7338..ad32dd0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 $ ${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 $ ${WIBO_TEST_BIN_DIR}/test_synchapi.exe) set_tests_properties(wibo.test_synchapi PROPERTIES diff --git a/dll/crt.cpp b/dll/crt.cpp index 49bc36d..c49eae7 100644 --- a/dll/crt.cpp +++ b/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, diff --git a/dll/kernel32/fileapi.cpp b/dll/kernel32/fileapi.cpp index 63ff996..47cc5f4 100644 --- a/dll/kernel32/fileapi.cpp +++ b/dll/kernel32/fileapi.cpp @@ -11,28 +11,29 @@ #include "timeutil.h" #include +#include #include #include #include #include #include -#include +#include #include #include #include +#include #include #include #include #include +#include +#include namespace { using random_shorts_engine = std::independent_bits_engine; -constexpr uintptr_t kPseudoFindHandleValue = 1; -const HANDLE kPseudoFindHandle = reinterpret_cast(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(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 entries; + size_t nextIndex = 0; +}; + +std::mutex g_findHandleMutex; +std::unordered_map> g_findHandles; + +HANDLE registerFindHandle(std::unique_ptr 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(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(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 detachFindHandle(HANDLE handle) { + std::lock_guard lk(g_findHandleMutex); + auto *raw = reinterpret_cast(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(std::tolower(static_cast(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(ts.tv_sec) + static_cast(kSecondsBetween1601And1970); + if (seconds < 0) { + seconds = 0; + } + uint64_t ticks = static_cast(seconds) * kWindowsTicksPerSecond; + ticks += static_cast(ts.tv_nsec > 0 ? ts.tv_nsec / 100 : 0); + out.dwLowDateTime = static_cast(ticks & 0xFFFFFFFFULL); + out.dwHighDateTime = static_cast(ticks >> 32); +} + +template 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(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(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(fileSize >> 32); - sizeLow = static_cast(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 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(st.st_size); + out.nFileSizeHigh = static_cast(fileSize >> 32); + out.nFileSizeLow = static_cast(fileSize & 0xFFFFFFFFULL); + toFileTime(changeTimespec(st), out.ftCreationTime); + toFileTime(accessTimespec(st), out.ftLastAccessTime); + toFileTime(modifyTimespec(st), out.ftLastWriteTime); +} + +template 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(MAX_PATH - 1, wstrlen(wideName.data())); - wstrncpy(data.cFileName, wideName.data(), copyLen); - data.cFileName[copyLen] = 0; - auto wideAlt = stringToWideString("8P3FMTFN.BAD"); - copyLen = std::min(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 &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 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(); + state->singleResult = true; + return registerFindHandle(std::move(state)); + } + + std::vector 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(); + state->entries = std::move(matches); + state->nextIndex = 1; + return registerFindHandle(std::move(state)); } std::optional 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 = 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 = 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(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(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(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(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(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; } diff --git a/src/errors.h b/src/errors.h index b9f5088..10d2c24 100644 --- a/src/errors.h +++ b/src/errors.h @@ -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 diff --git a/test/test_findfile.c b/test/test_findfile.c new file mode 100644 index 0000000..93c6f01 --- /dev/null +++ b/test/test_findfile.c @@ -0,0 +1,315 @@ +#include "test_assert.h" + +#include +#include +#include +#include +#include + +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; +}