// ************************************************************************** // * This file is part of the FreeFileSync project. It is distributed under * // * GNU General Public License: http://www.gnu.org/licenses/gpl.html * // * Copyright (C) 2008-2011 ZenJu (zhnmju123 AT gmx.de) * // ************************************************************************** #include "find_file_plus.h" //#include //these two don't play nice with each other #include "load_dll.h" #include using namespace dll; using namespace findplus; namespace { //-------------------------------------------------------------------------------------------------------------- typedef NTSTATUS (NTAPI* NtOpenFileFunc)(PHANDLE fileHandle, ACCESS_MASK desiredAccess, POBJECT_ATTRIBUTES objectAttributes, PIO_STATUS_BLOCK ioStatusBlock, ULONG shareAccess, ULONG openOptions); typedef NTSTATUS (NTAPI* NtCloseFunc)(HANDLE handle); typedef NTSTATUS (NTAPI* NtQueryDirectoryFileFunc)(HANDLE fileHandle, HANDLE event, PIO_APC_ROUTINE apcRoutine, PVOID apcContext, PIO_STATUS_BLOCK ioStatusBlock, PVOID fileInformation, ULONG length, FILE_INFORMATION_CLASS fileInformationClass, BOOLEAN ReturnSingleEntry, PUNICODE_STRING fileMask, BOOLEAN restartScan); typedef ULONG (NTAPI* RtlNtStatusToDosErrorFunc)(NTSTATUS /*__in status*/); typedef struct _RTL_RELATIVE_NAME_U { UNICODE_STRING RelativeName; HANDLE ContainingDirectory; PVOID /*PRTLP_CURDIR_REF*/ CurDirRef; } RTL_RELATIVE_NAME_U, *PRTL_RELATIVE_NAME_U; typedef BOOLEAN (NTAPI* RtlDosPathNameToNtPathName_UFunc)(PCWSTR, //__in dosFileName, PUNICODE_STRING, //__out ntFileName, PCWSTR*, //__out_optFilePart, PRTL_RELATIVE_NAME_U); //__out_opt relativeName typedef BOOLEAN (NTAPI* RtlDosPathNameToRelativeNtPathName_UFunc)(PCWSTR, //__in dosFileName, PUNICODE_STRING, //__out ntFileName, PWSTR*, //__out_optFilePart, PRTL_RELATIVE_NAME_U); //__out_opt relativeName typedef VOID (NTAPI* RtlFreeUnicodeStringFunc)(PUNICODE_STRING); //__inout unicodeString //-------------------------------------------------------------------------------------------------------------- //it seems we cannot use any of the ntoskrnl.lib files in WinDDK as they produce access violations //fortunately dynamic binding works fine: NtOpenFileFunc ntOpenFile; NtCloseFunc ntClose; NtQueryDirectoryFileFunc ntQueryDirectoryFile; RtlNtStatusToDosErrorFunc rtlNtStatusToDosError; RtlFreeUnicodeStringFunc rtlFreeUnicodeString; RtlDosPathNameToNtPathName_UFunc rtlDosPathNameToNtPathName_U; template inline void initDllFun(FunType& fun, const char* functionName) //throw FileError { if (!fun) { fun = getSystemDllFun(L"ntdll.dll", functionName); if (!fun) throw FileError(182); //== ERROR_INVALID_ORDINAL, verified at compile time in "load_dll.cpp"; we better not use rtlNtStatusToDosError(STATUS_ORDINAL_NOT_FOUND) here ;) } } struct FileError { FileError(ULONG errorCode) : win32Error(errorCode) {} ULONG win32Error; }; } class findplus::FileSearcher { public: FileSearcher(const wchar_t* dirname); //throw FileError ~FileSearcher(); void readdir(FileInformation& output); //throw FileError private: UNICODE_STRING ntPathName; //it seems hDir implicitly keeps a reference to this, at least ::FindFirstFile() does no cleanup before ::FindClose()! HANDLE hDir; ULONG nextEntryOffset; //!= 0 if entry is waiting in buffer //::FindNextFileW() uses 0x1000 = 4096 = sizeof(FILE_BOTH_DIR_INFORMATION) + sizeof(TCHAR) * 2000 //=> let's use the same, even if our header is 16 byte larger; maybe there is some packet size advantage for networks? Note that larger buffers seem to degrade performance. static const ULONG BUFFER_SIZE = 4096; LONGLONG buffer[BUFFER_SIZE / sizeof(LONGLONG)]; //buffer needs to be aligned at LONGLONG boundary static_assert(BUFFER_SIZE % sizeof(LONGLONG) == 0, "ups, our buffer is trimmed!"); }; FileSearcher::FileSearcher(const wchar_t* dirname) : hDir(NULL), nextEntryOffset(0) { UNICODE_STRING cleanDummy = {}; ntPathName = cleanDummy; Loki::ScopeGuard guardConstructor = Loki::MakeGuard([&]() { this->~FileSearcher(); }); //NT/ZwXxx Routines //http://msdn.microsoft.com/en-us/library/ff567122(v=VS.85).aspx //Run-Time Library (RTL) Routines //http://msdn.microsoft.com/en-us/library/ff563638(v=VS.85).aspx //init static dll functions initDllFun(ntOpenFile, "NtOpenFile"); //throw FileError initDllFun(ntClose, "NtClose"); initDllFun(ntQueryDirectoryFile, "NtQueryDirectoryFile"); initDllFun(rtlNtStatusToDosError, "RtlNtStatusToDosError"); initDllFun(rtlFreeUnicodeString, "RtlFreeUnicodeString"); try { initDllFun(rtlDosPathNameToNtPathName_U, "RtlDosPathNameToRelativeNtPathName_U"); //use the newer version if available } catch (const FileError&) { initDllFun(rtlDosPathNameToNtPathName_U, "RtlDosPathNameToNtPathName_U"); //fallback for XP } //-------------------------------------------------------------------------------------------------------------- //convert dosFileName, e.g. C:\Users or \\?\C:\Users to ntFileName \??\C:\Users //in contrast to ::FindFirstFile() we don't evaluate the relativeName, however tests indicate ntFileName is *always* filled with an absolute name, even if dosFileName is relative //NOTE: RtlDosPathNameToNtPathName_U may be used on all XP/Win7/Win8 for compatibility // RtlDosPathNameToNtPathName_U: used by Windows XP available with OS version 3.51 (Windows NT) and higher // RtlDosPathNameToRelativeNtPathName_U: used by Win7/Win8 available with OS version 5.2 (Windows Server 2003) and higher if (!rtlDosPathNameToNtPathName_U(dirname, //__in dosFileName, &ntPathName, //__out ntFileName, NULL, //__out_optFilePart, NULL)) //__out_opt relativeName - empty if dosFileName is absolute throw FileError(rtlNtStatusToDosError(STATUS_OBJECT_PATH_NOT_FOUND)); //translates to ERROR_PATH_NOT_FOUND, same behavior like ::FindFirstFileEx() OBJECT_ATTRIBUTES objAttr = {}; InitializeObjectAttributes(&objAttr, //[out] POBJECT_ATTRIBUTES initializedAttributes, &ntPathName, //[in] PUNICODE_STRING objectName, OBJ_CASE_INSENSITIVE, //[in] ULONG attributes, NULL, //[in] HANDLE rootDirectory, NULL); //[in, optional] PSECURITY_DESCRIPTOR securityDescriptor { IO_STATUS_BLOCK status = {}; NTSTATUS rv = ntOpenFile(&hDir, //__out PHANDLE FileHandle, FILE_LIST_DIRECTORY | SYNCHRONIZE, //__in ACCESS_MASK desiredAccess, - 100001 used by ::FindFirstFile() on all XP/Win7/Win8 &objAttr, //__in POBJECT_ATTRIBUTES objectAttributes, &status, //__out PIO_STATUS_BLOCK ioStatusBlock, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, //__in ULONG shareAccess, - 7 on Win7/Win8, 3 on XP FILE_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT | FILE_OPEN_FOR_BACKUP_INTENT); //__in ULONG openOptions - 4021 used on all XP/Win7/Win8 if (!NT_SUCCESS(rv)) throw FileError(rtlNtStatusToDosError(rv)); } guardConstructor.Dismiss(); } FileSearcher::~FileSearcher() { //cleanup in reverse order if (hDir) ntClose(hDir); if (ntPathName.Buffer) rtlFreeUnicodeString(&ntPathName); //cleanup identical to ::FindFirstFile() //note that most if this function seems inlined by the linker, so that its assembly looks equivalent to "RtlFreeHeap(GetProcessHeap(), 0, ntPathName.Buffer)" } void FileSearcher::readdir(FileInformation& output) { //although FILE_ID_FULL_DIR_INFORMATION should suffice for our purposes, there are problems on Windows XP for certain directories, e.g. "\\Vboxsvr\build" //making NtQueryDirectoryFile() return with STATUS_INVALID_PARAMETER while other directories, e.g. "C:\" work fine for some reason //FILE_ID_BOTH_DIR_INFORMATION on the other hand works on XP/Win7/Win8 //performance: there is no noticable difference between FILE_ID_BOTH_DIR_INFORMATION and FILE_ID_FULL_DIR_INFORMATION /* corresponding first access in ::FindFirstFileW() NTSTATUS rv = ntQueryDirectoryFile(hDir, //__in HANDLE fileHandle, NULL, //__in_opt HANDLE event, NULL, //__in_opt PIO_APC_ROUTINE apcRoutine, NULL, //__in_opt PVOID apcContext, &status, //__out PIO_STATUS_BLOCK ioStatusBlock, &buffer, //__out_bcount(Length) PVOID fileInformation, BUFFER_SIZE, //__in ULONG length, ::FindFirstFileW() on all XP/Win7/Win8 uses sizeof(FILE_BOTH_DIR_INFORMATION) + sizeof(TCHAR) * MAX_PATH == 0x268 FileIdBothDirectoryInformation, //__in FILE_INFORMATION_CLASS fileInformationClass - all XP/Win7/Win8 use "FileBothDirectoryInformation" true, //__in BOOLEAN returnSingleEntry, NULL, //__in_opt PUNICODE_STRING mask, false); //__in BOOLEAN restartScan */ //analog to ::FindNextFileW() with performance optimized access (in contrast to first access in ::FindFirstFileW()) if (nextEntryOffset == 0) { IO_STATUS_BLOCK status = {}; NTSTATUS rv = ntQueryDirectoryFile(hDir, //__in HANDLE fileHandle, NULL, //__in_opt HANDLE event, NULL, //__in_opt PIO_APC_ROUTINE apcRoutine, NULL, //__in_opt PVOID apcContext, &status, //__out PIO_STATUS_BLOCK ioStatusBlock, &buffer, //__out_bcount(Length) PVOID fileInformation, BUFFER_SIZE, //__in ULONG length, ::FindNextFileW() on all XP/Win7/Win8 uses sizeof(FILE_BOTH_DIR_INFORMATION) + sizeof(TCHAR) * 2000 == 0x1000 FileIdBothDirectoryInformation, //__in FILE_INFORMATION_CLASS fileInformationClass - all XP/Win7/Win8 use "FileBothDirectoryInformation" false, //__in BOOLEAN returnSingleEntry, NULL, //__in_opt PUNICODE_STRING mask, false); //__in BOOLEAN restartScan if (!NT_SUCCESS(rv)) throw FileError(rtlNtStatusToDosError(rv)); //throws STATUS_NO_MORE_FILES when finished if (status.Information == 0) //except for the first call to call ::NtQueryDirectoryFile(): throw FileError(rtlNtStatusToDosError(STATUS_BUFFER_OVERFLOW)); //if buffer size is too small, return value is STATUS_SUCCESS and Information == 0 -> we don't expect this! } const FILE_ID_BOTH_DIR_INFORMATION& dirInfo = *reinterpret_cast(reinterpret_cast(buffer) + nextEntryOffset); if (dirInfo.NextEntryOffset == 0) nextEntryOffset = 0; //our offset is relative to the beginning of the buffer else nextEntryOffset += dirInfo.NextEntryOffset; auto toFileTime = [](const LARGE_INTEGER & rawTime) -> FILETIME { FILETIME tmp = { rawTime.LowPart, rawTime.HighPart }; return tmp; }; output.creationTime = toFileTime(dirInfo.CreationTime); output.lastWriteTime = toFileTime(dirInfo.LastWriteTime); output.fileSize.QuadPart = dirInfo.EndOfFile.QuadPart; output.fileId.QuadPart = dirInfo.FileId.QuadPart; output.fileAttributes = dirInfo.FileAttributes; output.shortNameLength = dirInfo.FileNameLength / sizeof(TCHAR); //FileNameLength is in bytes! if (dirInfo.FileNameLength + sizeof(TCHAR) > sizeof(output.shortName)) //this may actually happen if ::NtQueryDirectoryFile() decides to return a throw FileError(rtlNtStatusToDosError(STATUS_BUFFER_OVERFLOW)); //short name of length MAX_PATH + 1, 0-termination is not required! ::memcpy(output.shortName, dirInfo.FileName, dirInfo.FileNameLength); output.shortName[output.shortNameLength] = 0; //NOTE: FILE_ID_BOTH_DIR_INFORMATION::FileName in general is NOT 0-terminated! It is on XP/Win7, but NOT on Win8! static_assert(sizeof(output.creationTime) == sizeof(dirInfo.CreationTime), "dang!"); static_assert(sizeof(output.lastWriteTime) == sizeof(dirInfo.LastWriteTime), "dang!"); static_assert(sizeof(output.fileSize) == sizeof(dirInfo.EndOfFile), "dang!"); static_assert(sizeof(output.fileId) == sizeof(dirInfo.FileId), "dang!"); static_assert(sizeof(output.fileAttributes) == sizeof(dirInfo.FileAttributes), "dang!"); } FindHandle findplus::openDir(const wchar_t* dirname) { try { return new FileSearcher(dirname); //throw FileError } catch (const FileError& err) { setWin32Error(err.win32Error); return NULL; } } bool findplus::readDir(FindHandle hnd, FileInformation& output) { try { hnd->readdir(output); //throw FileError return true; } catch (const FileError& err) { setWin32Error(err.win32Error); return false; } } void findplus::closeDir(FindHandle hnd) { if (hnd) //play a little "nice" delete static_cast(hnd); }