Windows Delphi / NTFS Files Attributes

Author Jean-Pierre LESUEUR (DarkCoderSc)
Platform Windows
Language Delphi
Technique NTFS Files Attributes

Description:

This code let you handle Alternate Data Streams using two different techniques.

  • FindFirstStreamW / FindNextStreamW : Available since Windows Vista and easier to use.
  • BackupRead : Available since Windows XP and more tricky to use.

You can:

  • Enumerate ADS Files attached to a target file.
  • Backup ADS File(s) attached to a target file.
  • Copy any file to target file ADS.
  • Delete ADS File(s) attached to a target file.

If you want to learn more about how to use this tiny library you can check this example project on Github.

Code

unit UntDataStreamObject;

interface

uses WinAPI.Windows, System.Classes, System.SysUtils, Generics.Collections,
      RegularExpressions;

type
  TEnumDataStream = class;
  TADSBackupStatus = (absTotal, absPartial, absError);

  TDataStream = class
  private
    FOwner      : TEnumDataStream;
    FStreamName : String;
    FStreamSize : Int64;

    {@M}
    function GetStreamPath() : String;
  public
    {@C}
    constructor Create(AOwner : TEnumDataStream; AStreamName : String; AStreamSize : Int64);

    {@M}
    function CopyFileToADS(AFileName : String) : Boolean;
    function BackupFromADS(ADestPath : String) : Boolean;
    function DeleteFromADS() : Boolean;

    {@G/S}
    property StreamName : String read FStreamName;
    property StreamSize : Int64  read FStreamSize;
    property StreamPath : String read GetStreamPath;
  end;

  TEnumDataStream = class
  private
    FTargetFile            : String;
    FItems                 : TObjectList<TDataStream>;
    FForceBackUpReadMethod : Boolean;

    {@M}
    function Enumerate_FindFirstStream() : Int64;
    function Enumerate_BackupRead() : Int64;
    function ExtractADSName(ARawName : String) : String;
    function CopyFromTo(AFrom, ATo : String) : Boolean;
    function GetDataStreamFromName(AStreamName : String) : TDataStream;
  public
    {@C}
    constructor Create(ATargetFile : String; AEnumerateNow : Boolean = True; AForceBackUpReadMethod : Boolean = False);
    destructor Destroy(); override;

    {@M}
    function Refresh() : Int64;

    function CopyFileToADS(AFilePath : String) : Boolean;
    function BackupFromADS(ADataStream : TDataStream; ADestPath : String) : Boolean; overload;
    function DeleteFromADS(ADataStream : TDataStream) : Boolean; overload;
    function BackupAllFromADS(ADestPath : String) : TADSBackupStatus;
    function BackupFromADS(AStreamName, ADestPath : String) : Boolean; overload;
    function DeleteFromADS(AStreamName : String) : Boolean; overload;

    {@G}
    property TargetFile : String                   read FTargetFile;
    property Items      : TObjectList<TDataStream> read FItems;
  end;

implementation

{+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++


   TEnumDataStream


+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++}

{
  FindFirstStream / FindNextStream API Definition
}
type
  _STREAM_INFO_LEVELS = (FindStreamInfoStandard, FindStreamInfoMaxInfoLevel);
  TStreamInfoLevels = _STREAM_INFO_LEVELS;

  _WIN32_FIND_STREAM_DATA = record
    StreamSize : LARGE_INTEGER;
    cStreamName : array[0..(MAX_PATH + 36)] of WideChar;
  end;
  TWin32FindStreamData = _WIN32_FIND_STREAM_DATA;

var hKernel32         : THandle;
    _FindFirstStreamW : function(lpFileName : LPCWSTR; InfoLevel : TStreamInfoLevels; lpFindStreamData : LPVOID; dwFlags : DWORD) : THandle; stdcall;
    _FindNextStreamW  : function(hFindStream : THandle; lpFindStreamData : LPVOID) : BOOL; stdcall;


{-------------------------------------------------------------------------------
  Return the ADS name from it raw name (:<name>:$DATA)
-------------------------------------------------------------------------------}
function TEnumDataStream.ExtractADSName(ARawName : String) : String;
var AMatch : TMatch;
    AName  : String;
begin
  result := ARawName;
  ///

  AName := '';
  AMatch := TRegEx.Match(ARawName, ':(.*):');
  if (AMatch.Groups.Count < 2) then
    Exit();

  result := AMatch.Groups.Item[1].Value;
end;

{-------------------------------------------------------------------------------
  Scan for ADS using method N�1 (FindFirstStream / FindNextStream). Work since
  Microsoft Windows Vista.
-------------------------------------------------------------------------------}
function TEnumDataStream.Enumerate_FindFirstStream() : Int64;
var hStream     : THandle;
    AData       : TWin32FindStreamData;

    procedure ProcessDataStream();
    var ADataStream : TDataStream;
    begin
      if (String(AData.cStreamName).CompareTo('::$DATA') = 0) then
        Exit();
      ///

      ADataStream := TDataStream.Create(self, ExtractADSName(String(AData.cStreamName)), Int64(AData.StreamSize));

      FItems.Add(ADataStream);
    end;

begin
  result := 0;
  ///

  self.FItems.Clear();

  if NOT FileExists(FTargetFile) then
    Exit(-1);

  if (NOT Assigned(@_FindFirstStreamW)) or (NOT Assigned(@_FindNextStreamW)) then
    Exit(-2);

  FillChar(AData, SizeOf(TWin32FindStreamData), #0);

  // https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-findfirststreamw
  hStream := _FindFirstStreamW(PWideChar(FTargetFile), FindStreamInfoStandard, @AData, 0);
  if (hStream = INVALID_HANDLE_VALUE) then begin
    case GetLastError() of
      ERROR_HANDLE_EOF : begin
        Exit(-3); // No ADS Found
      end;

      ERROR_INVALID_PARAMETER : begin
        Exit(-4); // Not compatible
      end;

      else begin
        Exit(-5);
      end;
    end;
  end;

  ProcessDataStream();

  // https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-findnextstreamw
  while True do begin
    FillChar(AData, SizeOf(TWin32FindStreamData), #0);

    if NOT _FindNextStreamW(hStream, @AData) then
      break;

    ProcessDataStream();
  end;

  ///
  result := self.FItems.Count;
end;

{-------------------------------------------------------------------------------
  Scan for ADS using method N�2 (BackupRead()). Works since
  Microsoft Windows XP.
-------------------------------------------------------------------------------}
function TEnumDataStream.Enumerate_BackupRead() : Int64;
var hFile           : THandle;
    AStreamId       : TWIN32StreamID;
    ABytesRead      : Cardinal;
    pContext        : Pointer;
    ALowByteSeeked  : Cardinal;
    AHighByteSeeked : Cardinal;
    AName           : String;
    ABytesToRead    : Cardinal;
    ASeekTo         : LARGE_INTEGER;
    AClose          : Boolean;
begin
  result := 0;
  AClose := False;
  ///
  hFile := CreateFile(
                        PWideChar(self.TargetFile),
                        GENERIC_READ,
                        FILE_SHARE_READ,
                        nil,
                        OPEN_EXISTING,
                        FILE_FLAG_BACKUP_SEMANTICS,
                        0
  );
  if (hFile = INVALID_HANDLE_VALUE) then
    Exit(-1);
  try
    pContext := nil;
    try
      while True do begin
        FillChar(AStreamId, SizeOf(TWIN32StreamID), #0);
        ///

        {
          Read Stream
        }
        ABytesToRead := SizeOf(TWIN32StreamID) - 4; // We don't count "cStreamName"

        if NOT BackupRead(hFile, @AStreamId, ABytesToRead, ABytesRead, False, False, pContext) then
          break;

        AClose := True;

        if (ABytesRead = 0) then
          break;

        ASeekTo.QuadPart := (AStreamId.Size + AStreamId.dwStreamNameSize);

        case AStreamId.dwStreamId of
          {
            Deadling with ADS Only
          }
          BACKUP_ALTERNATE_DATA : begin
            if (AStreamId.dwStreamNameSize > 0) then begin
              {
                Read ADS Name
              }
              ABytesToRead := AStreamId.dwStreamNameSize;
              SetLength(AName, (ABytesToRead div SizeOf(WideChar)));
              if BackupRead(hFile, PByte(AName), ABytesToRead, ABytesRead, False, False, pContext) then begin
                Dec(ASeekTo.QuadPart, ABytesRead); // Already done

                FItems.Add(TDataStream.Create(self, ExtractADSName(AName), AStreamId.Size));
              end;
            end;
          end;
        end;

        {
          Goto Next Stream.
        }
        if NOT BackupSeek(hFile, ASeekTo.LowPart, ASeekTo.HighPart, ALowByteSeeked, AHighByteSeeked, pContext) then
          break;

        (*
          //////////////////////////////////////////////////////////////////////
          // BackupSeek() Alternative (Manual method)
          //////////////////////////////////////////////////////////////////////

          var ABuffer : array[0..2096-1] of byte;
          // ...
          while True do begin
            if (ASeekTo.QuadPart < SizeOf(ABuffer)) then
              ABytesToRead := ASeekTo.QuadPart
            else
              ABytesToRead := SizeOf(ABuffer);

            if ABytesToRead = 0 then
              break;

            if NOT BackupRead(hFile, PByte(@ABuffer), ABytesToRead, ABytesRead, False, False, pContext) then
              break;
            ///

            Dec(ASeekTo.QuadPart, ABytesRead);

            if (ASeekTo.QuadPart <= 0) then
              break;
          end;
          // ...

          //////////////////////////////////////////////////////////////////////
        *)
      end;
    finally
      if AClose then
        BackupRead(hFile, nil, 0, ABytesRead, True, False, pContext);
    end;
  finally
    CloseHandle(hFile);
  end;
end;

{-------------------------------------------------------------------------------
  Refresh embedded data stream objects using Windows API. Returns number of
  data stream objects or an error identifier.
-------------------------------------------------------------------------------}
function TEnumDataStream.Refresh() : Int64;
var AVersion : TOSVersion;
begin
  result := 0;
  ///

  if (AVersion.Major >= 6) then begin
    {
      Vista and above
    }
    if self.FForceBackUpReadMethod then
      result := self.Enumerate_BackupRead()
    else
      result := self.Enumerate_FindFirstStream();
  end else if (AVersion.Major = 5) and (AVersion.Minor >= 1) then begin
    {
      Windows XP / Server 2003 & R2
    }
    result := self.Enumerate_BackupRead();
  end else begin
    // Unsupported (???)
  end;
end;

{-------------------------------------------------------------------------------
  Refresh ADS Files and retrieve one ADS file by it name.
-------------------------------------------------------------------------------}
function TEnumDataStream.GetDataStreamFromName(AStreamName : String) : TDataStream;
var I       : Integer;
    AStream : TDataStream;
begin
  result := nil;
  ///

  if (self.Refresh() > 0) then begin
    for I := 0 to self.Items.count -1 do begin
      AStream := self.Items.Items[i];
      if NOT Assigned(AStream) then
        continue;
      ///

      if (String.Compare(AStream.StreamName, AStreamName, True) = 0) then
        result := AStream;
    end;
  end;
end;

{-------------------------------------------------------------------------------
  ADS Classic Actions
    - Copy file to current ADS Location.
    - Copy ADS item to destination path.
    - Delete ADS Item.
-------------------------------------------------------------------------------}

function TEnumDataStream.CopyFromTo(AFrom, ATo : String) : Boolean;
var hFromFile     : THandle;
    hToFile       : THandle;

    ABuffer       : array[0..4096-1] of byte;
    ABytesRead    : Cardinal;
    ABytesWritten : Cardinal;
begin
  result := False;
  ///

  hFromFile := INVALID_HANDLE_VALUE;
  hToFile   := INVALID_HANDLE_VALUE;

  try
    hFromFile := CreateFile(PWideChar(AFrom), GENERIC_READ, FILE_SHARE_READ, nil, OPEN_EXISTING, 0, 0);
    if (hFromFile = INVALID_HANDLE_VALUE) then
      Exit();

    hToFile := CreateFile(
                            PWideChar(ATo),
                            GENERIC_WRITE,
                            FILE_SHARE_WRITE,
                            nil,
                            CREATE_ALWAYS,
                            FILE_ATTRIBUTE_NORMAL,
                            0
    );

    if (hToFile = INVALID_HANDLE_VALUE) then
      Exit();
    ///

    while True do begin
      {
        Read
      }
      if NOT ReadFile(hFromFile, ABuffer, SizeOf(ABuffer), ABytesRead, nil) then
        Exit();

      if ABytesRead = 0 then
        break; // Success

      {
        Write
      }
      if NOT WriteFile(hToFile, ABuffer, ABytesRead, ABytesWritten, nil) then
        Exit();

      if (ABytesWritten <> ABytesRead) then
        Exit();
    end;

    ///
    result := True;
  finally
    if hFromFile <> INVALID_HANDLE_VALUE then
      CloseHandle(hFromFile);

    if hToFile <> INVALID_HANDLE_VALUE then
      CloseHandle(hToFile);

    ///
    self.Refresh();
  end;
end;

function TEnumDataStream.CopyFileToADS(AFilePath : String) : Boolean;
begin
  result := CopyFromTo(AFilePath, Format('%s:%s', [self.FTargetFile, ExtractFileName(AFilePath)]));
end;

function TEnumDataStream.BackupFromADS(ADataStream : TDataStream; ADestPath : String) : Boolean;
begin
  result := False;

  if NOT Assigned(ADataStream) then
    Exit();

  result := CopyFromTo(ADataStream.StreamPath, Format('%s%s', [IncludeTrailingPathDelimiter(ADestPath), ADataStream.StreamName]));
end;

function TEnumDataStream.DeleteFromADS(ADataStream : TDataStream) : Boolean;
begin
  result := DeleteFile(ADataStream.StreamPath);
end;

function TEnumDataStream.BackupAllFromADS(ADestPath : String) : TADSBackupStatus;
var I       : integer;
    AStream : TDataStream;
begin
  result := absError;
  ///

  if (self.Refresh() > 0) then begin
    for I := 0 to self.Items.count -1 do begin
      AStream := self.Items.Items[i];
      if NOT Assigned(AStream) then
        continue;
      ///

      if AStream.BackupFromADS(ADestPath) and (result <> absPartial) then
        result := absTotal
      else
        result := absPartial;
    end;
  end;
end;

function TEnumDataStream.BackupFromADS(AStreamName, ADestPath : String) : Boolean;
var AStream : TDataStream;
begin
  result := False;
  ///

  AStream := self.GetDataStreamFromName(AStreamName);
  if Assigned(AStream) then
    result := self.BackupFromADS(AStream, ADestPath);
end;

function TEnumDataStream.DeleteFromADS(AStreamName : String) : Boolean;
var AStream : TDataStream;
begin
  result := False;
  ///

  AStream := self.GetDataStreamFromName(AStreamName);
  if Assigned(AStream) then
    result := self.DeleteFromADS(AStream);
end;

{-------------------------------------------------------------------------------
  ___constructor
-------------------------------------------------------------------------------}
constructor TEnumDataStream.Create(ATargetFile : String; AEnumerateNow : Boolean = True; AForceBackUpReadMethod : Boolean = False);
begin
  self.FTargetFile := ATargetFile;
  self.FForceBackUpReadMethod := AForceBackupReadMethod;

  FItems := TObjectList<TDataStream>.Create();
  FItems.OwnsObjects := True;

  if AEnumerateNow then
    self.Refresh();
end;

{-------------------------------------------------------------------------------
  ___destructor
-------------------------------------------------------------------------------}
destructor TEnumDataStream.Destroy();
begin
  if Assigned(FItems) then
    FreeAndNil(FItems);

  ///
  inherited Destroy();
end;

{+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++


   TDataStream


+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++}

constructor TDataStream.Create(AOwner : TEnumDataStream; AStreamName : String; AStreamSize : Int64);
begin
  self.FOwner      := AOwner;
  self.FStreamName := AStreamName;
  self.FStreamSize := AStreamSize;
end;

{-------------------------------------------------------------------------------
  Generate Stream Path Accordingly
-------------------------------------------------------------------------------}
function TDataStream.GetStreamPath() : String;
begin
  result := '';

  if NOT Assigned(FOwner) then
    Exit();

  result := Format('%s:%s', [FOwner.TargetFile, self.FStreamName]);
end;

{-------------------------------------------------------------------------------
  ADS Classic Actions (Redirected to Owner Object)
-------------------------------------------------------------------------------}

function TDataStream.CopyFileToADS(AFileName : String) : Boolean;
begin
  if Assigned(FOwner) then
    result := FOwner.CopyFileToADS(AFileName);
end;

function TDataStream.BackupFromADS(ADestPath : String) : Boolean;
begin
  if Assigned(FOwner) then
    result := FOwner.BackupFromADS(self, ADestPath);
end;

function TDataStream.DeleteFromADS() : Boolean;
begin
  if Assigned(FOwner) then
    result := FOwner.DeleteFromADS(self);
end;

// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

initialization
  _FindFirstStreamW := nil;
  _FindNextStreamW  := nil;

  hKernel32 := LoadLibrary('KERNEL32.DLL');
  if (hKernel32 > 0) then begin
    @_FindFirstStreamW := GetProcAddress(hKernel32, 'FindFirstStreamW');
    @_FindNextStreamW := GetProcAddress(hKernel32, 'FindNextStreamW');
  end;

finalization
  _FindFirstStreamW := nil;
  _FindNextStreamW  := nil;

end.

Created

December 1, 2020

Last Revised

April 22, 2024