首先,微软什么都没有。NET Framework提供了此功能。如果你想要它,简单明了,你需要直接或使用第三方库进行某种互操作。
如果您使用的是Windows Server 2003或更高版本,Kernel32.dll会向FindFirstFile和FindNextFile公开对应项,以提供您要查找的确切功能。FindFirstStreamW和FindNextStreamW允许您查找和枚举特定文件中的所有备用数据流,检索每个数据流的信息,包括其名称和长度。从托管代码中使用这些函数的代码与我在12月份的专栏中展示的代码非常相似,如图1所示。
使用FindFirstStreamW和FindNextStreamW
[SecurityPermission(SecurityAction.LinkDemand, UnmanagedCode = true)]
public sealed class SafeFindHandle : SafeHandleZeroOrMinusOneIsInvalid {
private SafeFindHandle() : base(true) { }
protected override bool ReleaseHandle() {
return FindClose(this.handle);
}
[DllImport("kernel32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
private static extern bool FindClose(IntPtr handle);
}
public class FileStreamSearcher {
private const int ERROR_HANDLE_EOF = 38;
private enum StreamInfoLevels { FindStreamInfoStandard = 0 }
[DllImport("kernel32.dll", ExactSpelling = true, CharSet = CharSet.Auto, SetLastError = true)]
private static extern SafeFindHandle FindFirstStreamW(string lpFileName, StreamInfoLevels InfoLevel, [In, Out, MarshalAs(UnmanagedType.LPStruct)] WIN32_FIND_STREAM_DATA lpFindStreamData, uint dwFlags);
[DllImport("kernel32.dll", ExactSpelling = true, CharSet = CharSet.Auto, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool FindNextStreamW(SafeFindHandle hndFindFile, [In, Out, MarshalAs(UnmanagedType.LPStruct)] WIN32_FIND_STREAM_DATA lpFindStreamData);
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private class WIN32_FIND_STREAM_DATA {
public long StreamSize;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 296)]
public string cStreamName;
}
public static IEnumerable<string> GetStreams(FileInfo file) {
if (file == null) throw new ArgumentNullException("file");
WIN32_FIND_STREAM_DATA findStreamData = new WIN32_FIND_STREAM_DATA();
SafeFindHandle handle = FindFirstStreamW(file.FullName, StreamInfoLevels.FindStreamInfoStandard, findStreamData, 0);
if (handle.IsInvalid) throw new Win32Exception();
try {
do {
yield return findStreamData.cStreamName;
} while (FindNextStreamW(handle, findStreamData));
int lastError = Marshal.GetLastWin32Error();
if (lastError != ERROR_HANDLE_EOF) throw new Win32Exception(lastError);
} finally {
handle.Dispose();
}
}
}
您只需调用FindFirstStreamW,将目标文件的完整路径传递给它。FindFirstStreamW的第二个参数决定了您希望返回数据的详细程度;目前,只有一个级别(FindStreamInfoStandard),其数值为0。该函数的第三个参数是指向WIN32_FIND_STREAM_DATA结构的指针(从技术上讲,第三个变量指向的内容由详细说明信息级别的第二个参数的值决定,但由于目前只有一个级别,因此实际上这是一个WIN32_FILEND_STREAM.DATA)。我已经将该结构的托管对应项声明为类,并在互操作签名中将其标记为封送为指向结构的指针。最后一个参数保留供将来使用,应为0。
如果从FindFirstStreamW返回有效句柄,则WIN32_FIND_STREAM_DATA实例包含有关找到的流的信息,其cStreamName值可以作为第一个可用流名称返回给调用者。FindNextStreamW接受FindFirstStreamW返回的句柄,并在提供的WIN32_FIND_STREAM_DATA中填充有关下一个可用流的信息(如果存在)。如果另一个流可用,则FindNextStreamW返回true,否则返回false。
因此,我不断调用FindNextStreamW并生成结果流名称,直到FindNextStreamW返回false。当这种情况发生时,我会仔细检查最后一个错误值,以确保迭代停止是因为FindNextStreamW流用尽,而不是出于某种意外原因。
图2:
BOOL BackupRead(HANDLE hFile, LPBYTE lpBuffer, DWORD nNumberOfBytesToRead, LPDWORD lpNumberOfBytesRead, BOOL bAbort, BOOL bProcessSecurity, LPVOID* lpContext);
BOOL BackupSeek(HANDLE hFile, DWORD dwLowBytesToSeek, DWORD dwHighBytesToSeek, LPDWORD lpdwLowByteSeeked, LPDWORD lpdwHighByteSeeked, LPVOID* lpContext);
使用BackupRead和BackupSeek
public enum StreamType {
Data = 1,
ExternalData = 2,
SecurityData = 3,
AlternateData = 4,
Link = 5,
PropertyData = 6,
ObjectID = 7,
ReparseData = 8,
SparseDock = 9
}
public struct StreamInfo {
public StreamInfo(string name, StreamType type, long size) {
Name = name;
Type = type;
Size = size;
}
readonly string Name;
public readonly StreamType Type;
public readonly long Size;
}
public class FileStreamSearcher {
[DllImport("kernel32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool BackupRead(SafeFileHandle hFile, IntPtr lpBuffer, uint nNumberOfBytesToRead, out uint lpNumberOfBytesRead, [MarshalAs(UnmanagedType.Bool)] bool bAbort, [MarshalAs(UnmanagedType.Bool)] bool bProcessSecurity, ref IntPtr lpContext);[DllImport("kernel32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool BackupSeek(SafeFileHandle hFile, uint dwLowBytesToSeek, uint dwHighBytesToSeek, out uint lpdwLowByteSeeked, out uint lpdwHighByteSeeked, ref IntPtr lpContext); public static IEnumerable<StreamInfo> GetStreams(FileInfo file) {
const int bufferSize = 4096;
using (FileStream fs = file.OpenRead()) {
IntPtr context = IntPtr.Zero;
IntPtr buffer = Marshal.AllocHGlobal(bufferSize);
try {
while (true) {
uint numRead;
if (!BackupRead(fs.SafeFileHandle, buffer, (uint)Marshal.SizeOf(typeof(Win32StreamID)), out numRead, false, true, ref context)) throw new Win32Exception();
if (numRead > 0) {
Win32StreamID streamID = (Win32StreamID)Marshal.PtrToStructure(buffer, typeof(Win32StreamID));
string name = null;
if (streamID.dwStreamNameSize > 0) {
if (!BackupRead(fs.SafeFileHandle, buffer, (uint)Math.Min(bufferSize, streamID.dwStreamNameSize), out numRead, false, true, ref context)) throw new Win32Exception(); name = Marshal.PtrToStringUni(buffer, (int)numRead / 2);
}
yield return new StreamInfo(name, streamID.dwStreamId, streamID.Size);
if (streamID.Size > 0) {
uint lo, hi; BackupSeek(fs.SafeFileHandle, uint.MaxValue, int.MaxValue, out lo, out hi, ref context);
}
} else break;
}
} finally {
Marshal.FreeHGlobal(buffer);
uint numRead;
if (!BackupRead(fs.SafeFileHandle, IntPtr.Zero, 0, out numRead, true, false, ref context)) throw new Win32Exception();
}
}
}
}
BackupRead背后的想法是,它可以用于将文件中的数据读取到缓冲区中,然后可以将其写入备份存储介质。但是,BackupRead对于查找组成目标文件的每个备用数据流的信息也非常方便。它将文件中的所有数据作为一系列离散的字节流进行处理(每个备用数据流都是这些字节流之一),每个流前面都有一个WIN32_Stream_ID结构。因此,为了枚举所有流,您只需从每个流的开头读取所有这些WIN32_STREAM_ID结构(这就是BackupSeek非常方便的地方,因为它可以用于从一个流跳到另一个流,而无需读取文件中的所有数据)。
首先,您需要为非托管WIN32_STREAM_ID结构创建一个托管对应项:
typedef struct _WIN32_STREAM_ID {
DWORD dwStreamId; DWORD dwStreamAttributes;
LARGE_INTEGER Size;
DWORD dwStreamNameSize;
WCHAR cStreamName[ANYSIZE_ARRAY];
} WIN32_STREAM_ID;
在大多数情况下,这就像您通过P/Invoke封送的任何其他结构一样。然而,也有一些并发症。首先,WIN32_STREAM_ID是一个可变大小的结构。它的最后一个成员cStreamName是一个长度为ANYSIZE_array的数组。虽然ANYSIZE_ARRAY被定义为1,但cStreamName只是结构中前四个字段之后其余数据的地址,这意味着如果结构被分配为大于sizeof(WIN32_STREAM_ID)字节,那么额外的空间实际上将是cStreamName数组的一部分。前面的字段dwStreamNameSize精确指定了数组的长度。
虽然这对Win32开发来说是件好事,但它对编组器造成了严重破坏,编组器需要将此数据从非托管内存复制到托管内存,作为BackupRead互操作调用的一部分。考虑到WIN32_STREAM_ID结构的大小是可变的,编组器如何知道它实际上有多大?它没有。
[StructLayout(LayoutKind.Sequential)]
public struct Win32StreamID {
public int dwStreamId;
public int dwStreamAttributes;
public long Size;
public int dwStreamNameSize;
}
Int32的大小为4个字节,Int64的大小为8个字节。因此,您会期望此结构体为20个字节。但是,如果你运行以下代码,你会发现这两个值都是24,而不是20:
int size1 = Marshal.SizeOf(typeof(Win32StreamID));
int size2 = sizeof(Win32StreamID); // in an unsafe context
问题是编译器希望确保这些结构中的值始终在正确的边界上对齐。四字节值应该位于可被4整除的地址处,8字节值应该在可被8整除的边界处,以此类推。现在想象一下,如果你创建一个Win32StreamID结构数组,会发生什么。数组第一个实例中的所有字段都将正确对齐。例如,由于Size字段遵循两个32位整数,因此它将从数组的开头开始为8个字节,非常适合8字节的值。但是,如果结构的大小为20个字节,则数组中的第二个实例将不会正确对齐其所有成员。整数值都可以,但长值将是从数组开始的28个字节,这个值不能被8整除。为了解决这个问题,编译器将结构填充到24的大小,这样所有字段都将始终正确对齐(假设数组本身是)。
如果编译器正在做正确的事情,你可能会想知道我为什么担心这个。如果您查看图2中的代码,您将看到原因。为了解决我描述的第一个封送处理问题,我确实将cStreamName排除在Win32StreamID结构之外。我使用BackupRead读取足够的字节来填充我的Win32StreamID结构,然后检查该结构的dwStreamNameSize字段。现在我知道这个名称有多长了,我可以再次使用BackupRead从文件中读取字符串的值。这一切都很好,但如果元帅。SizeOf为我的Win32StreamID结构返回24,而不是20,我将尝试读取太多数据。
[StructLayout(LayoutKind.Sequential, Size = 20)]
[StructLayout(LayoutKind.Sequential, Pack = 4)]
public struct Win32StreamID {
public StreamType dwStreamId;
public int dwStreamAttributes;
public long Size;
public int dwStreamNameSize; // WCHAR cStreamName[1];
}
static void Main(string[] args) {
foreach (string path in args) {
Console.WriteLine(path + ":");
foreach (StreamInfo stream in FileStreamSearcher.GetStreams(new FileInfo(path))) {
Console.WriteLine("\t{0}\t{1}\t{2}", stream.Name != null ? stream.Name : "(unnamed)", stream.Type, stream.Size);
}
}
}
您会注意到,此版本的FileStreamSearcher返回的信息比使用FindFirstStreamW和FindNextStreamW的版本多。BackupRead不仅可以提供主流和备用数据流上的数据,还可以对包含安全信息、重新分析数据等的流进行操作。如果你只想看到备用数据流,你可以根据StreamInfo的Type属性进行筛选,该属性将是StreamType。备用数据流的备用数据。
> echo ".NET Matters" > C:\test.txt
> echo "MSDN Magazine" > C:\test.txt:magStream
> StreamEnumerator.exe C:\test.txt
test.txt:
(unnamed) SecurityData 164
(unnamed) Data 17
:magStream:$DATA AlternateData 18
> type C:\test.txt
".NET Matters"
> more < C:\test.txt:magStream
"MSDN Magazine"
因此,现在您可以检索存储在文件中的所有备用数据流的名称。太棒了但是,如果你想真正操纵其中一个流中的数据呢?不幸的是,如果您试图将备用数据流的路径传递给FileStream构造函数之一,将抛出NotSupportedException:“不支持给定路径的格式。”
图3
).我为CreateFile函数使用了P/Invoke来打开和检索指定路径的SafeFileHandle,而无需对路径执行任何托管权限检查,因此它可以包含备用数据流标识符。然后,使用此SafeFileHandle创建新的托管FileStream,提供所需的访问权限。有了这个,就可以使用系统轻松操纵备用数据流的内容。IO命名空间的功能。以下示例读取并打印出在前面的示例中创建的C:\test.txt:magStream的内容:
string path = @"C:\test.txt:magStream";
using (StreamReader reader = new StreamReader(CreateFileStream(path, FileAccess.Read, FileMode.Open, FileShare.Read))) {
Console.WriteLine(reader.ReadToEnd());
}
使用P/Invoke创建文件
private static FileStream CreateFileStream(string path, FileAccess access, FileMode mode, FileShare share) {
if (mode == FileMode.Append) mode = FileMode.OpenOrCreate; SafeFileHandle handle = CreateFile(path, access, share, IntPtr.Zero, mode, 0, IntPtr.Zero);
if (handle.IsInvalid) throw new IOException("Could not open file stream.", new Win32Exception());
return new FileStream(handle, access);
}
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern SafeFileHandle CreateFile(string lpFileName, FileAccess dwDesiredAccess, FileShare dwShareMode, IntPtr lpSecurityAttributes, FileMode dwCreationDisposition, int dwFlagsAndAttributes, IntPtr hTemplateFile);
Stephen Toub
在……里面
MSDN Magazine from January 2006
.