NTFS Alternate Data Streams – .NET

Comment créer / supprimer / lire / écrire / NTFS des stream de données alternatifs à partir de .NET?

S’il n’y a pas de support .NET natif, quelles API Win32 utiliserais-je? En outre, comment les utiliserais-je, car je ne pense pas que cela soit documenté?

    Pas dans .NET:

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

    #include  #include  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); } 

    Voici une version pour C #

     using System.Runtime.InteropServices; class Program { static void Main(ssortingng[] 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 ///lpSecurityAtsortingbutes: LPSECURITY_ATTRIBUTES->_SECURITY_ATTRIBUTES* ///dwCreationDisposition: DWORD->unsigned int ///dwFlagsAndAtsortingbutes: DWORD->unsigned int ///hTemplateFile: HANDLE->void* [DllImportAtsortingbute("kernel32.dll", EntryPoint = "CreateFileW")] public static extern System.IntPtr CreateFileW( [InAtsortingbute()] [MarshalAsAtsortingbute(UnmanagedType.LPWStr)] ssortingng lpFileName, uint dwDesiredAccess, uint dwShareMode, [InAtsortingbute()] System.IntPtr lpSecurityAtsortingbutes, uint dwCreationDisposition, uint dwFlagsAndAtsortingbutes, [InAtsortingbute()] 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; } 

    Ce paquet nuget CodeFluent Runtime Client possède (parmi d’autres utilitaires) une classe NtfsAlternateStream qui prend en charge les opérations create / read / update / delete / enumeration.

    Il n’y a pas de support .NET natif pour eux. Vous devez utiliser P / Invoke pour appeler les méthodes Win32 natives.

    Pour les créer, appelez CreateFile avec un chemin tel que filename.txt:streamname . Si vous utilisez l’appel interop qui renvoie un SafeFileHandle, vous pouvez l’utiliser pour construire un FileStream que vous pouvez ensuite lire et écrire.

    Pour répertorier les stream existant dans un fichier, utilisez FindFirstStreamW et FindNextStreamW (qui existent uniquement sur Server 2003 et versions ultérieures, pas sur XP).

    Je ne pense pas que vous pouvez supprimer un stream, sauf en copiant le rest du fichier et en laissant l’un des stream. Mettre la longueur à 0 peut aussi fonctionner, mais je ne l’ai pas essayé.

    Vous pouvez également avoir d’autres stream de données dans un répertoire. Vous y accédez de la même manière qu’avec les fichiers – C:\some\directory:streamname .

    La compression, le chiffrement et la parcimonie peuvent être définis sur les stream indépendamment du stream par défaut.

    R Premièrement, rien dans Microsoft® .NET Framework ne fournit cette fonctionnalité. Si vous le souhaitez, simple et simple, vous devrez faire une sorte d’interopérabilité, soit directement, soit en utilisant une bibliothèque tierce.

    Si vous utilisez Windows Server ™ 2003 ou une version ultérieure, Kernel32.dll expose les équivalents à FindFirstFile et à FindNextFile, qui fournissent la fonctionnalité exacte que vous recherchez. FindFirstStreamW et FindNextStreamW vous permettent de rechercher et d’énumérer tous les stream de données alternatifs dans un fichier particulier, en récupérant des informations sur chacun d’eux, y compris son nom et sa longueur. Le code d’utilisation de ces fonctions à partir du code managé est très similaire à celui que j’ai montré dans ma colonne de décembre et est illustré à la figure 1.

    Figure 1 Utilisation de FindFirstStreamW et 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(ssortingng 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 ssortingng cStreamName; } public static IEnumerable 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(); } } } 

    Vous appelez simplement FindFirstStreamW en lui transmettant le chemin d’access complet au fichier cible. Le second paramètre de FindFirstStreamW détermine le niveau de détail souhaité dans les données renvoyées. actuellement, il n’y a qu’un seul niveau (FindStreamInfoStandard), qui a une valeur numérique de 0. Le troisième paramètre de la fonction est un pointeur sur une structure WIN32_FIND_STREAM_DATA (techniquement, ce que le troisième paramètre pointe est dicté par la valeur du second paramètre). détaillant le niveau d’information, mais comme il n’existe actuellement qu’un seul niveau, il s’agit pour l’essentiel d’un WIN32_FIND_STREAM_DATA). J’ai déclaré la contrepartie gérée de cette structure en tant que classe et, dans la signature d’interopérabilité, je l’ai marquée comme étant un pointeur vers une structure. Le dernier paramètre est réservé pour une utilisation future et doit être 0. Si un descripteur valide est renvoyé par FindFirstStreamW, l’instance WIN32_FIND_STREAM_DATA contient des informations sur le stream trouvé et sa valeur cStreamName peut être renvoyée à l’appelant comme premier nom de stream disponible. FindNextStreamW accepte le handle renvoyé par FindFirstStreamW et remplit le WIN32_FIND_STREAM_DATA fourni avec des informations sur le prochain stream disponible, s’il existe. FindNextStreamW renvoie true si un autre stream est disponible ou false s’il ne l’est pas. Par conséquent, j’appelle continuellement FindNextStreamW et génère le nom du stream obtenu jusqu’à ce que FindNextStreamW renvoie false. Lorsque cela se produit, je vérifie la dernière valeur d’erreur pour m’assurer que l’itération s’est arrêtée car FindNextStreamW était à court de stream, et pas pour une raison inattendue. Malheureusement, si vous utilisez Windows® XP ou Windows 2000 Server, ces fonctions ne sont pas disponibles, mais il existe plusieurs alternatives. La première solution implique une fonction non documentée actuellement exscope à partir de Kernel32.dll, NTQueryInformationFile. Cependant, les fonctions non documentées ne sont pas documentées pour une raison et elles peuvent être modifiées ou même supprimées à tout moment dans le futur. Il est préférable de ne pas les utiliser. Si vous souhaitez utiliser cette fonction, recherchez sur le Web et vous trouverez de nombreuses références et des exemples de code source. Mais faites-le à vos risques et périls. Une autre solution, que j’ai illustrée à la figure 2 , repose sur deux fonctions exscopes depuis Kernel32.dll, et celles-ci sont documentées. Comme leurs noms l’indiquent, BackupRead et BackupSeek font partie de l’API Win32® pour la prise en charge de la sauvegarde:

     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); 

    Figure 2 Utilisation de BackupRead et 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(ssortingng name, StreamType type, long size) { Name = name; Type = type; Size = size; } readonly ssortingng 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 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)); ssortingng 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.PtrToSsortingngUni(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(); } } } } 

    L’idée derrière BackupRead est qu’il peut être utilisé pour lire des données à partir d’un fichier dans un tampon, qui peut ensuite être écrit sur le support de stockage de sauvegarde. Cependant, BackupRead est également très utile pour trouver des informations sur chacun des stream de données alternatifs constituant le fichier cible. Il traite toutes les données du fichier sous la forme d’une série de stream d’octets discrets (chaque stream de données alternatif est l’un de ces stream d’octets) et chacun des stream est précédé d’une structure WIN32_STREAM_ID. Ainsi, pour énumérer tous les stream, vous devez simplement lire toutes ces structures WIN32_STREAM_ID depuis le début de chaque stream (c’est là que BackupSeek devient très pratique, car il peut être utilisé pour passer d’un stream à un autre sans lire toutes les données du fichier). Pour commencer, vous devez d’abord créer un équivalent géré pour la structure WIN32_STREAM_ID non gérée:

     typedef struct _WIN32_STREAM_ID { DWORD dwStreamId; DWORD dwStreamAtsortingbutes; LARGE_INTEGER Size; DWORD dwStreamNameSize; WCHAR cStreamName[ANYSIZE_ARRAY]; } WIN32_STREAM_ID; 

    Pour la plupart, cela ressemble à n’importe quelle autre structure que vous alliez faire passer par P / Invoke. Cependant, il y a quelques complications. WIN32_STREAM_ID est avant tout une structure de taille variable. Son dernier membre, cStreamName, est un tableau de longueur ANYSIZE_ARRAY. Alors que ANYSIZE_ARRAY est défini sur 1, cStreamName n’est que l’adresse du rest des données de la structure après les quatre champs précédents, ce qui signifie que si la structure est plus grande que sizeof (WIN32_STREAM_ID) octets, cet espace supplémentaire sera en effet faire partie du tableau cStreamName. Le champ précédent, dwStreamNameSize, spécifie exactement combien de temps le tableau est. Bien que ce soit génial pour le développement Win32, cela fait des ravages sur un marshaler qui doit copier ces données de la mémoire non gérée vers la mémoire gérée dans le cadre de l’appel interopérant à BackupRead. Comment le marshaler sait-il la taille réelle de la structure WIN32_STREAM_ID, étant donné sa taille variable? Ce n’est pas le cas. Le second problème concerne l’emballage et l’alignement. Si vous ignorez cStreamName pendant un moment, envisagez la possibilité suivante pour votre homologue WIN32_STREAM_ID géré:

     [StructLayout(LayoutKind.Sequential)] public struct Win32StreamID { public int dwStreamId; public int dwStreamAtsortingbutes; public long Size; public int dwStreamNameSize; } 

    Une Int32 a une taille de 4 octets et une Int64 est de 8 octets. Ainsi, vous pouvez vous attendre à ce que cette structure soit de 20 octets. Toutefois, si vous exécutez le code suivant, vous constaterez que les deux valeurs sont 24 et non 20:

     int size1 = Marshal.SizeOf(typeof(Win32StreamID)); int size2 = sizeof(Win32StreamID); // in an unsafe context 

    Le problème est que le compilateur veut s’assurer que les valeurs dans ces structures sont toujours alignées sur la limite appropriée. Les valeurs à quatre octets doivent être divisées par 4, les valeurs à 8 octets doivent être divisibles par 8, etc. Imaginez maintenant ce qui se passerait si vous deviez créer un tableau de structures Win32StreamID. Tous les champs de la première instance du tableau seraient correctement alignés. Par exemple, comme le champ Size suit deux entiers de 32 bits, il s’agirait de 8 octets à partir du début du tableau, ce qui est parfait pour une valeur de 8 octets. Cependant, si la taille de la structure était de 20 octets, tous les membres de la deuxième instance du groupe ne seraient pas correctement alignés. Les valeurs entières seraient toutes correctes, mais la valeur longue serait de 28 octets à partir du début du tableau, une valeur qui n’est pas divisible par 8. Pour résoudre ce problème, le compilateur place la structure à une taille de 24, les champs seront toujours correctement alignés (en supposant que le tableau lui-même est). Si le compilateur fait ce qu’il faut, vous vous demandez peut-être pourquoi cela m’inquiète. Vous verrez pourquoi si vous regardez le code de la figure 2. Afin de contourner le premier problème de marshaling que j’ai décrit, je laisse en fait le cStreamName hors de la structure Win32StreamID. J’utilise BackupRead pour lire suffisamment d’octets pour remplir ma structure Win32StreamID, puis j’examine le champ dwStreamNameSize de la structure. Maintenant que je sais combien de temps le nom est, je peux utiliser à nouveau BackupRead pour lire la valeur de la chaîne à partir du fichier. C’est très bien, mais si Marshal.SizeOf retourne 24 pour ma structure Win32StreamID au lieu de 20, je vais essayer de lire trop de données. Pour éviter cela, je dois m’assurer que la taille de Win32StreamID est en fait 20 et non 24. Cela peut être fait de deux manières différentes en utilisant des champs sur StructLayoutAtsortingbute qui ornent la structure. La première consiste à utiliser le champ Taille, qui dicte à l’environnement d’exécution la taille exacte de la structure:

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

    La deuxième option consiste à utiliser le champ Pack. Le pack indique la taille d’emballage à utiliser lorsque la valeur LayoutKind.Sequential est spécifiée et contrôle l’alignement des champs au sein de la structure. La taille par défaut pour une structure gérée est de 8. Si je change cela en 4, j’obtiens la structure de 20 octets que je recherche (et comme je n’utilise pas cela dans un tableau, je ne perds pas mon efficacité) ou stabilité pouvant résulter d’un tel changement d’emballage:

     [StructLayout(LayoutKind.Sequential, Pack = 4)] public struct Win32StreamID { public StreamType dwStreamId; public int dwStreamAtsortingbutes; public long Size; public int dwStreamNameSize; // WCHAR cStreamName[1]; } 

    Avec ce code en place, je peux maintenant énumérer tous les stream dans un fichier, comme indiqué ici:

     static void Main(ssortingng[] args) { foreach (ssortingng 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); } } } 

    Vous remarquerez que cette version de FileStreamSearcher renvoie plus d’informations que la version utilisant FindFirstStreamW et FindNextStreamW. BackupRead peut fournir des données sur plus que le stream principal et les stream de données alternatifs, fonctionnant également sur des stream contenant des informations de sécurité, des données d’parsing, etc. Si vous souhaitez uniquement afficher les autres stream de données, vous pouvez filtrer en fonction de la propriété Type de StreamInfo, qui sera StreamType.AlternateData pour les autres stream de données. Pour tester ce code, vous pouvez créer un fichier contenant des stream de données alternatifs à l’aide de la commande echo à l’invite de commandes:

     > 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" 

    Ainsi, vous pouvez maintenant récupérer les noms de tous les stream de données alternatifs stockés dans un fichier. Génial. Mais que faire si vous voulez réellement manipuler les données dans l'un de ces stream? Malheureusement, si vous tentez de transmettre un chemin d'access à un autre stream de données à l'un des constructeurs FileStream, une exception NotSupportedException sera lancée: "Le format du chemin donné n'est pas pris en charge." Pour contourner ce problème, vous pouvez contourner les vérifications de la canonisation du chemin d'access de FileStream en accédant directement à la fonction CreateFile exposée à partir de kernel32.dll (voir la figure 3 ). J'ai utilisé un appel P / Invoke pour la fonction CreateFile pour ouvrir et récupérer un SafeFileHandle pour le chemin spécifié, sans effectuer aucune des vérifications d'permissions gérées sur le chemin, afin qu'il puisse inclure des identifiants Alternate Data Stream. Ce SafeFileHandle est ensuite utilisé pour créer un nouveau FileStream géré, fournissant l'access requirejs. Grâce à cela, il est facile de manipuler le contenu d'un stream de données alternatif en utilisant les fonctionnalités de l'espace de noms System.IO. L'exemple suivant lit et imprime le contenu du fichier C: \ test.txt: magStream créé dans l'exemple précédent:

     ssortingng path = @"C:\test.txt:magStream"; using (StreamReader reader = new StreamReader(CreateFileStream(path, FileAccess.Read, FileMode.Open, FileShare.Read))) { Console.WriteLine(reader.ReadToEnd()); } 

    Figure 3 Utilisation de P / Invoke pour CreateFile

     private static FileStream CreateFileStream(ssortingng 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(ssortingng lpFileName, FileAccess dwDesiredAccess, FileShare dwShareMode, IntPtr lpSecurityAtsortingbutes, FileMode dwCreationDisposition, int dwFlagsAndAtsortingbutes, IntPtr hTemplateFile); 

    Stephen Toub dans MSDN Magazine de janvier 2006 .