c#-NTFS备用数据流-.NET

如何从.NET创建/删除/读取/写入/ NTFS备用数据流?

如果没有本机.NET支持,我将使用哪个Win32 API? 另外,由于我认为没有记载,我将如何使用它们?

user72491 asked 2020-02-22T17:53:52Z
5个解决方案
32 votes

这是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;
}
JaredPar answered 2020-02-22T17:54:26Z
15 votes

此nuget包CodeFluent Runtime Client(除其他实用程序外)具有NtfsAlternateStream类,该类支持创建/读取/更新/删除/枚举操作。

Simon Mourier answered 2020-02-22T17:54:46Z
14 votes

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

要创建它们,请使用诸如C:\some\directory:streamname之类的路径调用CreateFile。如果您使用返回SafeFileHandle的互操作调用,则可以使用它来构造一个FileStream,然后可以对其进行读写。

要列出文件中存在的流,请使用FindFirstStreamW和FindNextStreamW(它们仅在Server 2003和更高版本上存在,而不在XP上存在)。

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

您还可以在目录上具有备用数据流。 您可以使用与文件相同的方式来访问它们-C:\some\directory:streamname

流可以具有独立于默认流的压缩,加密和稀疏性设置。

Zack Elan answered 2020-02-22T17:55:29Z
10 votes

首先,Microsoft®.NET 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导出的未记录功能。但是,未记录的功能由于某种原因而未被记录,因此将来可以随时更改甚至删除它们。最好不要使用它们。如果您确实想使用此功能,请在网上搜索,然后找到大量参考资料和示例源代码。但这需要您自担风险。另一种解决方案(我已在图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变得非常方便的地方,因为它可以用于从流跳转到流而无需 以读取文件中的所有数据)。首先,您首先需要为非托管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整除的边界处,依此类推。现在想象一下,如果要创建Win32StreamID结构的数组会发生什么。数组第一个实例中的所有字段都将正确对齐。例如,由于Size字段紧跟两个32位整数,因此从数组开始起需要8个字节,非常适合8字节的值。但是,如果该结构的大小为20字节,则数组中的第二个实例将无法使其所有成员正确对齐。整数值都是可以的,但长值应该是从数组开始处起28个字节,该值不能被8整除。要解决此问题,编译器将结构填充为24,以便所有字段将始终正确对齐(假设数组本身是正确的)。如果编译器做正确的事情,您可能想知道为什么我对此感到担心。您将看到为什么要看一下图2中的代码。为了解决我描述的第一个编组问题,实际上我确实将cStreamName放在Win32StreamID结构之外。我使用BackupRead读取足够的字节来填充Win32StreamID结构,然后检查该结构的dwStreamNameSize字段。现在,我知道了名称的长度,可以再次使用BackupRead从文件中读取字符串的值。一切都很好,但是如果Marshal.SizeOf为Win32StreamID结构返回24而不是20,那么我将尝试读取太多数据。为避免这种情况,我需要确保Win32StreamID的大小实际上是20,而不是24。这可以使用装饰结构的StructLayoutAttribute上的字段以两种不同的方式来实现。第一种是使用“大小”字段,该字段向运行时明确指示结构应有多大:

[StructLayout(LayoutKind.Sequential, Size = 20)]

第二个选项是使用“打包”字段。 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)。我已使用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使用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杂志》上发表。

Václav Dajbych answered 2020-02-22T17:57:10Z
4 votes

不在.NET中:

[HTTP://support.Microsoft.com/恐怖/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);
   }
Otávio Décio answered 2020-02-22T17:54:06Z
translate from https://stackoverflow.com:/questions/604960/ntfs-alternate-data-streams-net