53

如何使用 .NET 读取和修改“NTFS 备用数据流”?

似乎没有对它的本机 .NET 支持。我会使用哪些 Win32 API?另外,我将如何使用它们,因为我不认为这是记录在案的?

4

5 回答 5

33

这是 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;
}
于 2009-03-03T03:29:26.857 回答
16

没有对它们的本机 .NET 支持。您必须使用 P/Invoke 来调用本机 Win32 方法。

要创建它们,请使用类似filename.txt:streamname. 如果您使用返回 SafeFileHandle 的互操作调用,则可以使用它来构造一个 FileStream,然后您可以对其进行读写。

要列出文件中存在的流,请使用FindFirstStreamWFindNextStreamW(仅存在于 Server 2003 及更高版本 - 不是 XP)。

我不相信您可以删除流,除非复制文件的其余部分并保留其中一个流。将长度设置为 0 也可能有效,但我没有尝试过。

您还可以在目录上拥有备用数据流。您可以像访问文件一样访问它们 - C:\some\directory:streamname.

流可以独立于默认流设置压缩、加密和稀疏性。

于 2009-03-03T05:04:34.617 回答
15

这个 nuget 包CodeFluent 运行时客户端(在其他实用程序中)有一个支持创建/读取/更新/删除/枚举操作的NtfsAlternateStream 类。

于 2013-01-23T08:59:11.080 回答
14

答 首先,Microsoft® .NE​​T Framework 中没有任何东西提供此功能。如果你想要它,简单明了,你需要直接或使用第三方库进行某种互操作。

如果您使用的是 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 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_FIND_STREAM_DATA)。我已经将该结构的托管副本声明为一个类,并在互操作签名中将其标记为编组为指向结构的指针。最后一个参数保留供将来使用,应为 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 上搜索,您会发现大量参考资料和示例源代码。但这样做需要您自担风险。另一种解决方案,也是我在 并且它们可以在将来的任何时间更改甚至删除。最好不要使用它们。如果您确实想使用此功能,请在 Web 上搜索,您会发现大量参考资料和示例源代码。但这样做需要您自担风险。另一种解决方案,也是我在 并且它们可以在将来的任何时间更改甚至删除。最好不要使用它们。如果您确实想使用此功能,请在 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 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 变得非常方便的地方,因为它可以用于从流跳转到流而无需读取文件中的所有数据)。开始,

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 字节,则数组中的第二个实例不会使其所有成员都正确对齐。整数值都可以,但 long 值将是数组开头的 28 个字节,这个值不能被 8 整除。要解决这个问题,编译器将结构填充为 24 的大小,以便所有字段始终正确对齐(假设数组本身是)。如果编译器做对了,你可能想知道我为什么要关心这个。如果您查看图 2 中的代码,您就会明白为什么。为了解决我描述的第一个封送处理问题,我确实将 cStreamName 排除在 Win32StreamID 结构之外。我使用 BackupRead 读取足够的字节来填充我的 Win32StreamID 结构,然后检查结构的 dwStreamNameSize 字段。现在我知道名称有多长,我可以再次使用 BackupRead 从文件中读取字符串的值。这一切都很好,但如果 Marshal.SizeOf 为我的 Win32StreamID 结构返回 24 而不是 20,我将尝试读取太多数据。为了避免这种情况,我需要确保 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 属性进行过滤,该属性将为备用数据流的 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)。我为 CreateFile 函数使用了 P/Invoke 来打开和检索指定路径的 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 杂志上发表

于 2011-11-02T22:57:52.400 回答
4

不在 .NET 中:

http://support.microsoft.com/kb/105763

#include <windows.h>
   #include <stdio.h>

   void main( )
   {
      HANDLE hFile, hStream;
      DWORD dwRet;

      hFile = CreateFile( "testfile",
                       GENERIC_WRITE,
                    FILE_SHARE_WRITE,
                                NULL,
                         OPEN_ALWAYS,
                                   0,
                                NULL );
      if( hFile == INVALID_HANDLE_VALUE )
         printf( "Cannot open testfile\n" );
      else
          WriteFile( hFile, "This is testfile", 16, &dwRet, NULL );

      hStream = CreateFile( "testfile:stream",
                                GENERIC_WRITE,
                             FILE_SHARE_WRITE,
                                         NULL,
                                  OPEN_ALWAYS,
                                            0,
                                         NULL );
      if( hStream == INVALID_HANDLE_VALUE )
         printf( "Cannot open testfile:stream\n" );
      else
         WriteFile(hStream, "This is testfile:stream", 23, &dwRet, NULL);
   }
于 2009-03-03T03:14:49.920 回答