0%

Windows文件系统概述(一)基本概念

最近接触Windows文件备份相关的业务,从Win32 API入手对Windows文件系统的基本概念做一个总结。学习Windows文件系统最好方式还是阅读官方文档,本文只是对相关概念进行粗浅的介绍,权当抛砖引玉。
本文完整代码见:https://github.com/XUranus/FileSystemUtil

Win32 API Doc

官方文档。文档会详细描述每一个API的入参、出参、返回的字段含义和取值范围,部分还会给出Example程序。

Windows程序开发一般使用MSVC,和GCC有不少区别。为了更有效率的学习Windows文件系统,在介绍文件系统之前,本文先给Linux转Windows的开发者介绍一下Windows的编码和文档阅读方式,磨刀不误砍柴工。

UTF-8/UTF-16

涉及字符串的Win32 API一般提供两类接口:

  • Ansi字符串接口(一般以A结尾)
  • UTF-16宽字符串接口(一般以W结尾)

例如GetFileAttributesAGetFileAttributesW

typedef _Null_terminated_ CONST WCHAR *LPCWSTR, *PCWSTR;
typedef _Null_terminated_ CONST CHAR *LPCSTR, *PCSTR;
...
DWORD GetFileAttributesA(
[in] LPCSTR lpFileName
);

DWORD GetFileAttributesW(
[in] LPCWSTR lpFileName
);

LPCSTR的类型是CHAR*、即char*,表示一个ANSI字符串的指针。而LPCWSTR的类型是WCHAR*、即wchar_t*,表示一个UTF-16字符串指针。此外还提供不显式声明字符串类型的接口,例如GetFileAttributes,他的宏定义如下:

#ifdef UNICODE
#define GetFileAttributes GetFileAttributesW
#else
#define GetFileAttributes GetFileAttributesA
#endif // !UNICODE

可见这类接口会根据是否定义UNICODE宏来决定使用哪种API。对于字符串可能包含Unicode字符的程序,尽量使用宽字符API,否则可能会调用失败!

ANSI版本的Window API中, 路径长度被限制在MAX_PATH、即260个字符. 要拓展这个限制到23767宽字符(wide character), 需要调用这个函数的Unicode版本, 并且在路径的首部加上\\?\,例如C:\Users\XUranus\Desktop应转为\\?\C:\Users\XUranus\Desktop再作为参数传入。本文将默认使用Unicode版本的API,并默认路径已经带上了\\?\前缀。有关文件路径详见Naming Files, Paths, and Namespaces

C++程序一般使用UTF-8编码的std::string来表示字符串,Linux接口一般使用UTF-8编码的字符串,而Windows是一个UTF-16的操作系统。如果程序需要跨平台,一般在公共业务部分用std::string,因为UTF-8表示相同的含有Unicode特殊字符的字符串可能占用更少的空间,在网络IO和数据落盘时的开销也更小,而在涉及Windows接口的部分则最好使用UTF-16编码的std::wstring。此时就不得不考虑std::stringstd::wstring的转码问题。

C++11标准库提供了std::wstringstd::wstring无损互转换工具std::codecvt_utf8_utf16,用法如下:

#define _SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING /* deprecated since C++17, supress warning */
#include <locale>
#include <codecvt>

std::wstring Utf8ToUtf16(const std::string& str)
{
using ConvertTypeX = std::codecvt_utf8_utf16<wchar_t>;
std::wstring_convert<ConvertTypeX> converterX;
std::wstring wstr = converterX.from_bytes(str);
return wstr;
}

std::string Utf16ToUtf8(const std::wstring& wstr)
{
using ConvertTypeX = std::codecvt_utf8_utf16<wchar_t>;
std::wstring_convert<ConvertTypeX> converterX;
return converterX.to_bytes(wstr);
}

对于UTF-8和UTF-16编码的概念和转换方法,本文就不再赘述,读者可以查询相关资料自行拓展。了解了编码相关的知识,就能知道Windows的两种API该如何使用,接下来说说Win32 API文档的阅读方式。

Win32 API Document

官方文档会根据头文件列出其中包含的API,例如FindFirstFileWSyntax中描述如下:

HANDLE FindFirstFileW(
[in] LPCWSTR lpFileName,
[out] LPWIN32_FIND_DATAW lpFindFileData
);

表明了第一个参数lpFileName是个LPCWSTR类型的入参,而lpFindFileData是个LPWIN32_FIND_DATAW类型的出参,返回值是HANDLE类型,表示的含义可以自Return Value中查到。LPWIN32_FIND_DATAW则是该接口获取的信息,其结构体构成也可在页面Parameters中找到详细信息的链接

Remarks区域,列举了该API调用的注意点,例如会提示你用FindClose在调用成功后关闭API返回的句柄。

在Linux中一般用int fd描述一个打开文件的描述符(File Descriptor),当fd < 0往往意味调用失败。Windows中用HANDLE hFile,实际类型是void*来表示一个打开文件的句柄(File Handle),如果hFileINVALID_HANDLE_VALUE则表示调用失败。

失败处理

几乎所有的操作都有可能失败,或者由于参数不合法,或者由于没有权限、没有资源。所以阅读Windows API的时候需要特别留意失败场景的处理方式。有时候一个步骤失败但之前已经分配了资源,这个时候Remark部分可能会提示你手动释放之前步骤准备的资源。再Linux下失败的操作往往可以通过errno宏拿到错误码,在Windows下这个错误码用GetLastError()获取,它能返回上一个DWORD类型的值标记操作失败的原因,具体参考:System Error Codes

文件属性

Windows文件拥有和Linux文件相似的属性信息,在Linux中一般用stat来获取文件的struct stat结构,里面包含文件的基本属性信息。Windows下获取文件属性的API有很多,但是大部分只能获取很小一部分属性。能获取比较完整的属性的API是GetFileInformationByHandle(),它将一个文件/目录句柄作为参数,获取文件信息存储在BY_HANDLE_FILE_INFORMATION结构中。

首先定义一个结构体StatResult来描述一个文件属性信息对象,接下来将基于它逐步封装文件属性查询接口:

class StatResult {
public:
StatResult(const std::wstring& wPath, const BY_HANDLE_FILE_INFORMATION& handleFileInformation);

private:
BY_HANDLE_FILE_INFORMATION m_handleFileInformation{};
std::wstring m_wPath; /* raw input path */
};

StatResult::StatResult(const std::wstring& wPath, const BY_HANDLE_FILE_INFORMATION& handleFileInformation)
: m_wPath(wPath)
{
memcpy_s(&m_handleFileInformation, sizeof(BY_HANDLE_FILE_INFORMATION),
&handleFileInformation, sizeof(BY_HANDLE_FILE_INFORMATION));
}

要获取文件/目录对应的BY_HANDLE_FILE_INFORMATION结构体,就需要先拿到文件/目录的句柄。用CreateFileW()打开一个文件并获取HANDLE类型的句柄。HANDLE类型的句柄用于指向Windows中打开的资源,它类似于Linux下的int类型的文件描述符(File Descriptor)。Linux下常用int fd = open(fname, mode)打开一个文件,且用完资源后需要用close(fd)释放,同理,在Windows下也需要对打开的句柄用CloseHandle(handle)进行关闭。Linux下fd < 0常用于描述失效的句柄,而Windows下失效的句柄等于内置常量INVALID_HANDLE_VALUE。无论在Linux下还是Windows下,句柄都不只局限于文件,对句柄的操作一定要检查句柄是否合法,用完句柄后也一定要及时释放。

CreateFileW相关参数说明详见:CreateFileW function (fileapi.h)

GetFileInformationByHandle实现一个Windows下的stat函数,他将BY_HANDLE_FILE_INFORMATION信息封装在StatResult结构体中:

std::optional<StatResult> StatW(const std::wstring& wPath)
{
BY_HANDLE_FILE_INFORMATION handleFileInformation{};
HANDLE hFile = ::CreateFileW(
wPath.c_str(),
GENERIC_READ,
FILE_SHARE_READ,
nullptr,
OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
0);
if (hFile == INVALID_HANDLE_VALUE) {
return std::nullopt;
}
if (::GetFileInformationByHandle(hFile, &handleFileInformation) == 0) {
::CloseHandle(hFile);
return std::nullopt;
}
::CloseHandle(hFile);
return std::make_optional<StatResult>(wPath, handleFileInformation);
}

BY_HANDLE_FILE_INFORMATION的成员定义如下:
typedef struct _BY_HANDLE_FILE_INFORMATION {
DWORD dwFileAttributes;
FILETIME ftCreationTime;
FILETIME ftLastAccessTime;
FILETIME ftLastWriteTime;
DWORD dwVolumeSerialNumber;
DWORD nFileSizeHigh;
DWORD nFileSizeLow;
DWORD nNumberOfLinks;
DWORD nFileIndexHigh;
DWORD nFileIndexLow;
} BY_HANDLE_FILE_INFORMATION, *PBY_HANDLE_FILE_INFORMATION, *LPBY_HANDLE_FILE_INFORMATION;

拥有了BY_HANDLE_FILE_INFORMATION就可以获取文件的基本信息:

  • dwFileAttributes:它类似于Linux stat 获得的mode_t类型,用符号位标记文件/目录的attributes,例如:目录、稀疏文件、归档文件、系统文件、隐藏文件、等
  • ftCreationTime:类似于Linux stat信息中的time_t st_ctime字段,记录创建时间对应的Windows时间戳
  • ftLastAccessTime:类似于Linux stat信息中的time_t st_atime字段,记录上次访问时间对应的Windows时间戳
  • ftLastWriteTime:类似于Linux stat信息中的time_t st_mtime字段,记录上次修改时间对应的Windows时间戳
  • dwVolumeSerialNumber:类似于Linux stat信息中的st_rdev字段,记录创建时间
  • nFileSizeHighnFileSizeLow:类似于Linux stat信息中的st_size字段,用高低字节记录文件的大小(单位bytes)
  • nNumberOfLinks:类似于Linux stat信息中的nlink字段,记录硬链接数量
  • nFileIndexHighnFileIndexLow:用高低字节记录文件/目录的index值,index是Windows文件系统中对标Linux文件系统中inode的概念。区别是Linux中inode全局唯一,而Windows中index只在卷中唯一

Linux文件系统还有组ID和用户ID的概念,他们在Windows下默认为0

其中sizeindex被拆分成了高低字节,time字段也是拆分为高低字节的结构体,它的定义如下:

typedef struct _FILETIME {
DWORD dwLowDateTime;
DWORD dwHighDateTime;
} FILETIME, *PFILETIME, *LPFILETIME;

这类用双字(两个DWORD描述的)数据转uint64_t可以构造LARGE_INTEGER结构体,并通过QuadPart属性来整合两个字段的最终值
inline uint64_t CombineDWORD(DWORD low, DWORD high) {
LARGE_INTEGER li;
li.LowPart = low;
li.HighPart = high;
return li.QuadPart;
}

UNIX的时间戳从1970年1月1日开始,Windows的时间戳转UNIX时间戳需要减去0x019DB1DED53E8000换算
inline uint64_t ConvertWin32Time(DWORD low, DWORD high)
{
const uint64_t UNIX_TIME_START = 0x019DB1DED53E8000; /* January 1, 1970 (start of Unix epoch) in "ticks" */
const uint64_t TICKS_PER_SECOND = 10000000; /* a tick is 100ns */
LARGE_INTEGER li;
li.LowPart = low;
li.HighPart = high;
#ifdef KEEP_WIN32_NATIVE_TIMESTAMP_VALUE
return li.QuadPart;
#else
/* Convert ticks since 1/1/1970 into seconds */
return (li.QuadPart - UNIX_TIME_START) / TICKS_PER_SECOND;
#endif
}

基于以上概念给StatResult添加相关方法及实现:

uint64_t StatResult::AccessTime() const
{
return ConvertWin32Time(m_handleFileInformation.ftLastAccessTime.dwLowDateTime,
m_handleFileInformation.ftLastAccessTime.dwHighDateTime);
}

uint64_t StatResult::CreationTime() const
{
return ConvertWin32Time(m_handleFileInformation.ftCreationTime.dwLowDateTime,
m_handleFileInformation.ftCreationTime.dwHighDateTime);
}

uint64_t StatResult::ModifyTime() const
{
return ConvertWin32Time(m_handleFileInformation.ftLastWriteTime.dwLowDateTime,
m_handleFileInformation.ftLastWriteTime.dwHighDateTime);
}

uint64_t StatResult::UniqueID() const
{
return CombineDWORD(m_handleFileInformation.nFileIndexLow,
m_handleFileInformation.nFileIndexHigh);
}

uint64_t StatResult::Size() const
{
return CombineDWORD(m_handleFileInformation.nFileSizeLow,
m_handleFileInformation.nFileSizeHigh);
}

uint64_t StatResult::DeviceID() const
{
return static_cast<uint64_t>(m_handleFileInformation.dwVolumeSerialNumber);
}

uint64_t StatResult::LinksCount() const
{
return static_cast<uint64_t>(m_handleFileInformation.nNumberOfLinks);
}

DWORD描述的dwFileAttributes可以与一系列系统常量相与来判断文件是否是某种类型:

bool StatResult::IsDirectory() const { return (m_handleFileInformation.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0; }
bool StatResult::IsArchive() const { return (m_handleFileInformation.dwFileAttributes & FILE_ATTRIBUTE_ARCHIVE) != 0; }
bool StatResult::IsCompressed() const { return (m_handleFileInformation.dwFileAttributes & FILE_ATTRIBUTE_COMPRESSED) != 0; }
bool StatResult::IsEncrypted() const { return (m_handleFileInformation.dwFileAttributes & FILE_ATTRIBUTE_ENCRYPTED) != 0; }
bool StatResult::IsSparseFile() const { return (m_handleFileInformation.dwFileAttributes & FILE_ATTRIBUTE_SPARSE_FILE) != 0; }
bool StatResult::IsHidden() const { return (m_handleFileInformation.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN) != 0; }
bool StatResult::IsOffline() const { return (m_handleFileInformation.dwFileAttributes & FILE_ATTRIBUTE_OFFLINE) != 0; }
bool StatResult::IsReadOnly() const { return (m_handleFileInformation.dwFileAttributes & FILE_ATTRIBUTE_READONLY) != 0; }
bool StatResult::IsSystem() const { return (m_handleFileInformation.dwFileAttributes & FILE_ATTRIBUTE_SYSTEM) != 0; }
bool StatResult::IsTemporary() const { return (m_handleFileInformation.dwFileAttributes & FILE_ATTRIBUTE_TEMPORARY) != 0; }
bool StatResult::IsNormal() const { return (m_handleFileInformation.dwFileAttributes & FILE_ATTRIBUTE_NORMAL) != 0; }
bool StatResult::IsReparsePoint() const { return (m_handleFileInformation.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0; }

相关FILE_ATTRIBUTE_XXX常量详见:File Attribute Constants

上述我们尝试实现Windows上对于Linux下stat的替换方案,用Windows API重新封装了一个StatW。实际上Windows也提供了stat方法:stat functions,提供了Linux下stat一样的参数列表,返回结构struct stat也有一样的成员结构。只不过该方法只是用于给Linux程序迁移到Windows提供遍历,它并不能获取完整的Windows文件信息。

卷与驱动器

Windows文件系统有驱动器的概念,其中卷(Volume)和Linux中卷的概念一致,卷可以是物理卷、也可以是逻辑卷,卷可能是本地卷、也可能是网络卷(SMB),可能是可读写的,也可能是只读的。

每个卷对应唯一的卷名(VolumeName),每个卷又属于唯一的设备(Device),每个卷可能被挂载在若干数量的路径上。他我们先定义一个Win32VolumeDetail类,一步步了解卷的相关属性的获取:

class Win32VolumesDetail {
public:
Win32VolumesDetail(const std::wstring& wVolumeName);
std::wstring VolumeNameW() const;
std::optional<std::wstring> GetVolumeDeviceNameW();
std::optional<std::vector<std::wstring>> GetVolumePathListW();
private:
std::wstring m_wVolumeName;
};

首先通过API FindFirstVolumeW()FindNextVolumeW()可以遍历所有的卷。VolumeName最大长度不会超过路径的最大长度,可以用Win32 API预制宏MAX_PATH定义。FindFirstVolume成功执行会返回第一个卷的句柄,通过它和FindNextVolumeW()可以遍历并列出所有的卷名(VolumeName)。

FindNextVolumeW function (fileapi.h)

constexpr auto VOLUME_BUFFER_MAX_LEN = MAX_PATH;
constexpr auto VOLUME_PATH_MAX_LEN = MAX_PATH;

std::optional<std::vector<Win32VolumesDetail>> GetWin32VolumeList()
{
std::vector<std::wstring> wVolumes;
std::vector<Win32VolumesDetail> volumeDetails;
WCHAR wVolumeNameBuffer[VOLUME_BUFFER_MAX_LEN] = L"";
HANDLE handle = ::FindFirstVolumeW(wVolumeNameBuffer, VOLUME_BUFFER_MAX_LEN);
if (handle == INVALID_HANDLE_VALUE) {
::FindVolumeClose(handle);
return std::nullopt;
}
wVolumes.push_back(std::wstring(wVolumeNameBuffer));
while (::FindNextVolumeW(handle, wVolumeNameBuffer, VOLUME_BUFFER_MAX_LEN)) {
wVolumes.push_back(std::wstring(wVolumeNameBuffer));
}
::FindVolumeClose(handle);
handle = INVALID_HANDLE_VALUE;
for (const std::wstring& wVolumeName : wVolumes) {
Win32VolumesDetail volumeDetail(wVolumeName);
volumeDetails.push_back(volumeDetail);
}
return volumeDetails;
}

/* member methods implementation for Win32VolumeDetail */
Win32VolumesDetail::Win32VolumesDetail(const std::wstring& wVolumeName) : m_wVolumeName(wVolumeName) {}

std::wstring Win32VolumesDetail::VolumeNameW() const { return m_wVolumeName; }

该方法只能查询本地卷,不能查询网络卷。

拿到的卷名形如\\?\Volume{a501f5cc-311e-423c-bc58-94a6c1b6b509}\,由\\?\Volume和一串GUID构成。有了卷名就可以查询设备和挂载点了。API QueryDosDeviceW用于根据卷名查询对应设备名,设备名同样最大不超过MAX_PATH,代码如下:

std::optional<std::wstring> Win32VolumesDetail::GetVolumeDeviceNameW()
{
if (m_wVolumeName.size() < 4 ||
m_wVolumeName[0] != L'\\' ||
m_wVolumeName[1] != L'\\' ||
m_wVolumeName[2] != L'?' ||
m_wVolumeName[3] != L'\\' ||
m_wVolumeName.back() != L'\\') { /* illegal volume name */
return std::nullopt;
}
std::wstring wVolumeParam = m_wVolumeName;
wVolumeParam.pop_back(); /* QueryDosDeviceW does not allow a trailing backslash */
wVolumeParam = wVolumeParam.substr(4);
WCHAR deviceNameBuf[DEVICE_BUFFER_MAX_LEN] = L"";
DWORD charCount = ::QueryDosDeviceW(wVolumeParam.c_str(), deviceNameBuf, ARRAYSIZE(deviceNameBuf));
if (charCount == 0) {
return std::nullopt;
}
return std::make_optional<std::wstring>(deviceNameBuf);
}

查询到的设备名形如:\Device\HarddiskVolume3,由设备类型和序号组成。

一个卷可能由0个、1个或者多个挂载点,可以用GetVolumePathNamesForVolumeNameW获得:

std::optional<std::vector<std::wstring>> Win32VolumesDetail::GetVolumePathListW()
{
/* https://learn.microsoft.com/en-us/windows/win32/fileio/displaying-volume-paths */
if (m_wVolumeName.size() < 4 ||
m_wVolumeName[0] != L'\\' ||
m_wVolumeName[1] != L'\\' ||
m_wVolumeName[2] != L'?' ||
m_wVolumeName[3] != L'\\' ||
m_wVolumeName.back() != L'\\') { /* illegal volume name */
return std::nullopt;
}
std::vector<std::wstring> wPathList;
PWCHAR devicePathNames = nullptr;
DWORD charCount = MAX_PATH + 1;
bool success = false;
while (true) {
devicePathNames = (PWCHAR) new BYTE[charCount * sizeof(WCHAR)];
if (!devicePathNames) { /* failed to malloc on heap */
return std::nullopt;
}
success = ::GetVolumePathNamesForVolumeNameW(
m_wVolumeName.c_str(),
devicePathNames,
charCount,
&charCount
);
if (success || ::GetLastError() != ERROR_MORE_DATA) {
break;
}
delete[] devicePathNames;
devicePathNames = nullptr;
}
if (success) {
for (PWCHAR nameIdx = devicePathNames;
nameIdx[0] != L'\0';
nameIdx += ::wcslen(nameIdx) + 1) {
wPathList.push_back(std::wstring(nameIdx));
}
}
if (devicePathNames != nullptr) {
delete[] devicePathNames;
devicePathNames = nullptr;
}
return std::make_optional<std::vector<std::wstring>>(wPathList);
}

查询到的挂载点是Windows文件路径,例如:C:\D:\E:\dir\mount\(挂载点可以不是根目录)

驱动器(Driver)是区别于卷的Windows特有的概念,驱动器有26个字母盘符可供选择。其中AB盘是早期用于标记软盘,如今驱动器一般从C开始分配。不同于Posix路径以/作为文件系统的根目录,Windows每个驱动器都有自己的根目录例如C:\D:\。一般每个磁盘系统会分配一个驱动器,如果驱动器盘符耗尽,可以把磁盘挂载在某个驱动器的某个空白的NTFS卷的目录下例如E:\dir\mount。Windows文件系统路径一般以反斜杠(Backslash)作为分隔符(Path Separator),而Posix路径一般以左斜杠(Slash)作为分隔符。Win32 API一般也接受非标准的路径例如:C:\User/XUranus/Desktop,但一些C++标准库对Windows路径有着严格要求,必须写作C:\User\XUranus\Desktop,否则会调用失败。

驱动器分配可以在“磁盘管理”中右击某个卷选择“更改驱动器号和路径”,可以为卷删除、或者添加一个或多个盘符或挂载路径。对于装载到某个“空白的NTFS卷目录”,实际上是给那个目录创建了一个MountPoint类型的指向点(REPARSE POINT),我们将在下一章详解REPARSE POINT。与Linux文件系统的挂载不同,这种装载类似于将这个空白目录修改成了软连接,会导致文件系统成环!

Windows提供API GetLogicalDriveStrings用于获得所有已分配的驱动器卷标

std::vector<std::wstring> GetWin32DriverListW()
{
std::vector<std::wstring> wdrivers;
DWORD dwLen = ::GetLogicalDriveStrings(0, nullptr); /* the length of volumes str */
if (dwLen <= 0) {
return wdrivers;
}
wchar_t* pszDriver = new wchar_t[dwLen];
::GetLogicalDriveStringsW(dwLen, pszDriver);
wchar_t* pDriver = pszDriver;
while (*pDriver != '\0') {
std::wstring wDriver = std::wstring(pDriver);
wdrivers.push_back(wDriver);
pDriver += wDriver.length() + 1;
}
delete[] pszDriver;
pszDriver = nullptr;
pDriver = nullptr;
return wdrivers;
}

返回结果形如:{"C:\","D:\","E:\"}

目录遍历

Linux下用Posix接口遍历目录的流程是:

  1. opendir()打开一个目录,获取一个struct dirent*类型的句柄
  2. readdir()传入之前的struct dirent*句柄并返回下一个句柄

循环该过程直到struct dirent*nullptr

Windows下遍历目录也有类似的过程,和之前封装StatResult模拟stat一样,这里来封装一个OpenDirEntry模拟struct dirent

class OpenDirEntry
{
public:
OpenDirEntry(
const std::string& dirPath,
const WIN32_FIND_DATAW& findFileData,
const HANDLE& fileHandle);

bool IsDirectory() const;
std::wstring NameW() const;
bool Next();

/* disable copy/assign construct */
OpenDirEntry(const OpenDirEntry&) = delete;
OpenDirEntry operator = (const OpenDirEntry&) = delete;
~OpenDirEntry();

private:
std::wstring m_dirPath;
HANDLE m_fileHandle = nullptr;
WIN32_FIND_DATAW m_findFileData;
};

OpenDirEntry描述目录遍历过程中的一个入口,提供IsDirectory判断入口是否是目录,NameW()返回入口的名称,Next()返回是否遍历结束。WIN32_FIND_DATAW是用于存放遍历过程入口信息的结构,HANDLE指向当前遍历入口的句柄:
OpenDirEntry::OpenDirEntry(
const std::string& dirPath,
const WIN32_FIND_DATAW& findFileData,
const HANDLE& fileHandle)
: m_dirPath(Utf8ToUtf16(dirPath)), m_findFileData(findFileData), m_fileHandle(fileHandle) {}

Windows文件遍历用到的API是FindFirstFileW()FindNextFileW()FindFirstFileW()接受一个路径的模式串,如果能找到一个复合的遍历入口则返回对应的入口句柄。FindNextFileW()基于当前句柄返回下一个入口对应的句柄,如果找不到则返回INVALID_HANDLE_VALUE。遍历过程中当前入口的信息存放在WIN32_FIND_DATAW m_findFileData中:

Windows支持模式串作为遍历参数,例如C:\Users\XUranus\Desktop\*.*用于描述C:\Users\XUranus\Desktop下所有的目录和文件。详见FindFirstFileA function (fileapi.h)

std::optional<OpenDirEntry> OpenDir(const std::string& wPath)
{
std::wstring wpathPattern = wPath;
if (!wpathPattern.empty() && wpathPattern.back() != L'\\') {
wpathPattern.push_back(L'\\');
}
wpathPattern += L"*.*";
WIN32_FIND_DATAW findFileData{};
HANDLE fileHandle = ::FindFirstFileW(wpathPattern.c_str(), &findFileData);
if (fileHandle == INVALID_HANDLE_VALUE) {
return std::nullopt;
}
return std::make_optional<OpenDirEntry>(path, findFileData, fileHandle);
}

bool OpenDirEntry::Next()
{
if (m_fileHandle == nullptr || m_fileHandle == INVALID_HANDLE_VALUE) {
return false;
}
if (!::FindNextFileW(m_fileHandle, &m_findFileData)) {
m_fileHandle = nullptr;
return false;
}
return true;
}

遍历完成必须用FindClose()手动关闭打开目录的句柄,可以用RAII实现:

OpenDirEntry::~OpenDirEntry()
{
if (m_fileHandle != nullptr && m_fileHandle != INVALID_HANDLE_VALUE) {
::FindClose(m_fileHandle);
m_fileHandle = nullptr;
}
}

WIN32_FIND_DATAW定义如下:

typedef struct _WIN32_FIND_DATAW {
DWORD dwFileAttributes;
FILETIME ftCreationTime;
FILETIME ftLastAccessTime;
FILETIME ftLastWriteTime;
DWORD nFileSizeHigh;
DWORD nFileSizeLow;
DWORD dwReserved0;
DWORD dwReserved1;
_Field_z_ WCHAR cFileName[ MAX_PATH ];
_Field_z_ WCHAR cAlternateFileName[ 14 ];
#ifdef _MAC
DWORD dwFileType;
DWORD dwCreatorType;
WORD wFinderFlags;
#endif
} WIN32_FIND_DATAW, *PWIN32_FIND_DATAW, *LPWIN32_FIND_DATAW;

可以看到它拥有和BY_HANDLE_FILE_INFORMATION类似的成员,通过它可以实现判断当前入口的属性、名称、大小等信息:
bool OpenDirEntry::IsDirectory() const
{
return (m_findFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0;
}

std::string OpenDirEntry::Name() const
{
return Utf16ToUtf8(std::wstring(m_findFileData.cFileName));
}

文件IO

先回顾一下Linux下读取文件IO的流程:

  1. int fd = open(fname, mode)获取文件的描述符
  2. read(fd, buf, sizeof(buf))将数据读取buffer
  3. write(fd, buf, len)将数据从buffer写入文件
  4. IO完成后用close(fd)释放资源

Windows下遵循类似的流程:

  1. HANDLE hFile = CreateFileW()获取打开文件句柄
  2. ReadFile将文件部分读入buffer
  3. WriteFile将buffer写入文件
  4. IO完成后用CloseHandle(hFile)释放资源

基于上述逻辑实现一个拷贝文件的逻辑:

bool Win32CopyFileW(const std::wstring& wSrcPath, const std::wstring& wDstPath)
{
const int DEFAULT_BUFF_SIZE = 1024;
char buff[DEFAULT_BUFF_SIZE] = "\0";
/* open file for read */
HANDLE hInFile = ::CreateFileW(
wSrcPath.c_str(),
GENERIC_READ,
FILE_SHARE_READ,
nullptr,
OPEN_EXISTING,
0,
nullptr);
if (hInFile == INVALID_HANDLE_VALUE) {
return false;
}
/* open file for write */
HANDLE hOutFile = ::CreateFileW(
wDstPath.c_str(),
GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
nullptr,
CREATE_NEW,
0,
nullptr);
if (hOutFile == INVALID_HANDLE_VALUE) {
::CloseHandle(hInFile);
return false;
}
LARGE_INTEGER sizeEx;
::GetFileSizeEx(hInFile, &sizeEx);
uint64_t bytesCopied = 0;
uint64_t size = sizeEx.QuadPart;
/* copy file */
while (size != bytesCopied) {
uint64_t bytesLeft = size - bytesCopied;
nbytes = bytesLeft < sizeof(buff) ? bytesLeft : sizeof(buff);
if (!::ReadFile(hInFile, buff, nbytes, nullptr, nullptr)) {
/* read failed */
::CloseHandle(hInFile);
::CloseHandle(hOutFile);
return false;
}
DWORD nWritten = 0;
if (!::WriteFile(hOutFile, buff, nbytes, &nWritten, nullptr)) {
/* write failed */
::CloseHandle(hInFile);
::CloseHandle(hOutFile);
return false;
}
}
/* copy success */
::CloseHandle(hInFile);
::CloseHandle(hOutFile);
return true;
}

打开文件后默认文件的读写指针从0开始,读写n后字节后当前读写指针偏移量会自动后移相应长度。Linux下如果想从指定偏移量读写文件可以用lseek来改变文件读写指针的位置,在Windows下这个函数是SetFilePointer

相关参考资料:

ReadFile function (fileapi.h)
WriteFile function (fileapi.h)
SetFilePointer function (fileapi.h)

Disqus评论区没有正常加载,请使用科学上网