我如何从.NET创建/删除/读/写/ NTFS备用数据流?
如果没有本机.NET支持,我会使用哪个Win32 API?另外,我如何使用它们,因为我不认为这是记录在案的?
这是C#的一个版本
using System.Runtime.InteropServices; class Program { static void Main(string[] args) { var mainStream = NativeMethods.CreateFileW( "testfile", NativeConstants.GENERIC_WRITE, NativeConstants.FILE_SHARE_WRITE, IntPtr.Zero, NativeConstants.OPEN_ALWAYS, 0, IntPtr.Zero); var stream = NativeMethods.CreateFileW( "testfile:stream", NativeConstants.GENERIC_WRITE, NativeConstants.FILE_SHARE_WRITE, IntPtr.Zero, NativeConstants.OPEN_ALWAYS, 0, IntPtr.Zero); } } public partial class NativeMethods { /// Return Type: HANDLE->void* ///lpFileName: LPCWSTR->WCHAR* ///dwDesiredAccess: DWORD->unsigned int ///dwShareMode: DWORD->unsigned int ///lpSecurityAttributes: LPSECURITY_ATTRIBUTES->_SECURITY_ATTRIBUTES* ///dwCreationDisposition: DWORD->unsigned int ///dwFlagsAndAttributes: DWORD->unsigned int ///hTemplateFile: HANDLE->void* [DllImportAttribute("kernel32.dll", EntryPoint = "CreateFileW")] public static extern System.IntPtr CreateFileW( [InAttribute()] [MarshalAsAttribute(UnmanagedType.LPWStr)] string lpFileName, uint dwDesiredAccess, uint dwShareMode, [InAttribute()] System.IntPtr lpSecurityAttributes, uint dwCreationDisposition, uint dwFlagsAndAttributes, [InAttribute()] System.IntPtr hTemplateFile ); } public partial class NativeConstants { /// GENERIC_WRITE -> (0x40000000L) public const int GENERIC_WRITE = 1073741824; /// FILE_SHARE_DELETE -> 0x00000004 public const int FILE_SHARE_DELETE = 4; /// FILE_SHARE_WRITE -> 0x00000002 public const int FILE_SHARE_WRITE = 2; /// FILE_SHARE_READ -> 0x00000001 public const int FILE_SHARE_READ = 1; /// OPEN_ALWAYS -> 4 public const int OPEN_ALWAYS = 4; }
这个nuget包CodeFluent运行时客户端(在其他实用程序中)有一个NtfsAlternateStream类,它支持创建/读取/更新/删除/枚举操作.
它们没有本机.NET支持.您必须使用P/Invoke来调用本机Win32方法.
要创建它们,请使用类似路径调用CreateFilefilename.txt:streamname
.如果使用返回SafeFileHandle的interop调用,则可以使用它来构造可以读取和写入的FileStream.
要列出文件中存在的流,请使用FindFirstStreamW和FindNextStreamW(仅存在于Server 2003及更高版本 - 而不是XP).
我不相信你可以删除一个流,除非复制文件的其余部分并从其中一个流中删除.将长度设置为0也可以,但我没有尝试过.
您还可以在目录上拥有备用数据流.您可以像访问文件一样访问它们 - C:\some\directory:streamname
.
Streams可以独立于默认流设置压缩,加密和稀疏.
首先,Microsoft®.NETFramework中没有任何内容提供此功能.如果你想要它,简单而简单,你需要直接或使用第三方库进行某种互操作.
如果您使用的是Windows Server™2003或更高版本,Kernel32.dll会向FindFirstFile和FindNextFile公开提供您正在寻找的确切功能的对应项.FindFirstStreamW和FindNextStreamW允许您查找和枚举特定文件中的所有备用数据流,检索有关每个文件的信息,包括其名称和长度.从托管代码中使用这些函数的代码与我在12月专栏中显示的代码非常相似,如图1所示.
图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 IEnumerableGetStreams(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_FIND_STREAM_DATA).我已经将结构的托管对应方式声明为类,并且在interop签名中我将其标记为封送为结构的指针.最后一个参数保留供将来使用,应该为0.如果从FindFirstStreamW返回有效句柄,则WIN32_FIND_STREAM_DATA实例包含有关所找到的流的信息,并且其cStreamName值可以作为可用的第一个流名称返回给调用者.FindNextStreamW接受从FindFirstStreamW返回的句柄,并填充提供的WIN32_FIND_STREAM_DATA,其中包含有关下一个可用流的信息(如果存在).如果另一个流可用,则FindNextStreamW返回true,否则返回false.因此,我不断调用FindNextStreamW并生成结果流名称,直到FindNextStreamW返回false.当发生这种情况时,我会仔细检查最后一个错误值,以确保迭代停止,因为FindNextStreamW用完了流,而不是出于某些意外原因.不幸的是,如果您使用的是Windows®XP或Windows 2000 Server,则无法使用这些功能,但有几种选择.第一个解决方案涉及当前从Kernel32.dll,NTQueryInformationFile导出的未记录的函数.但是,由于某种原因,未记录的功能没有记录,并且可以在将来的任何时间更改或甚至删除它们.最好不要使用它们.如果您确实想使用此功能,请搜索Web,您将找到大量参考资料和示例源代码.但这样做需要您自担风险.另一个解决方案,也是我在图2中演示的解决方案,依赖于从Kernel32.dll导出的两个函数,这些都是记录在案的.顾名思义,BackupRead和BackupSeek是Win32®API的一部分,用于备份支持:
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);
图2 使用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 IEnumerableGetStreams(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结构实际有多大,因为它的大小可变?它没有.第二个问题与包装和对齐有关.暂时忽略cStreamName,请考虑以下对托管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从文件中读取字符串的值.这一切都很好,但是如果Marshal.SizeOf为我的Win32StreamID结构而不是20返回24,那么我将尝试读取太多数据.为了避免这种情况,我需要确保Win32StreamID的大小实际上是20而不是24.这可以使用装饰结构的StructLayoutAttribute上的字段以两种不同的方式完成.第一个是使用Size字段,它指示运行时确切的结构应该是多大:
[StructLayout(LayoutKind.Sequential, Size = 20)]
第二个选项是使用Pack字段.Pack指示在指定LayoutKind.Sequential值时应使用的打包大小,并控制结构中字段的对齐方式.托管结构的默认打包大小是8.如果我将其更改为4,我得到了我正在寻找的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属性进行过滤,该属性将为Transternate Data Streams的StreamType.AlternateData.要测试此代码,可以在命令提示符下使用echo命令创建具有备用数据流的文件:
> 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:"不支持给定路径的格式." 要解决这个问题,您可以通过直接访问从kernel32.dll公开的CreateFile函数来绕过FileStream的路径规范化检查(参见图3).我已经使用P/Invoke为CreateFile函数打开并检索指定路径的SafeFileHandle,而不对路径执行任何托管权限检查,因此它可以包含备用数据流标识符.然后,此SafeFileHandle用于创建新的托管FileStream,从而提供所需的访问权限.有了这些,就可以使用System.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()); }
图3 为CreateFile使用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于2006年1月在MSDN杂志上发表.