Einzelnen Beitrag anzeigen

Kas Ob.

Registriert seit: 3. Sep 2023
347 Beiträge
 
#1

Small library for Alternative Data Streams (ADS)

  Alt 21. Aug 2024, 12:04
Hi,

Having my fun with ADS i wrote this, and hope you find it useful
Delphi-Quellcode:
unit lcNTFSFileStreams;

interface

uses
  Windows, Classes;

const
  // known StreamID values
  BACKUP_INVALID = 0;
  BACKUP_DATA = 1; // the file data/content itself
  BACKUP_EA_DATA = 2;
  BACKUP_SECURITY_DATA = 3; // security descriptor of the file
  BACKUP_ALTERNATE_DATA = 4; // attached/alternative data stream, must be have a name
  BACKUP_LINK = 5;
  BACKUP_PROPERTY_DATA = 6;
  BACKUP_OBJECT_ID = 7;
  BACKUP_REPARSE_DATA = 8;
  BACKUP_SPARSE_BLOCK = 9;
  BACKUP_TXFS_DATA = 10;
  BACKUP_GHOSTED_FILE_EXTENTS = 11;

  // known StreamAttributes bits
  STREAM_NORMAL_ATTRIBUTE = 0;
  STREAM_MODIFIED_WHEN_READ = 1;
  STREAM_CONTAINS_SECURITY = 2;
  STREAM_CONTAINS_PROPERTIES = 4;
  STREAM_SPARSE_ATTRIBUTE = 8;
  STREAM_CONTAINS_GHOSTED_FILE_EXTENTS = 16;

type
  TFileStreamRec = record
    StreamID: Cardinal;
    StreamAttribute: Cardinal;
    Size: Int64;
    StreamName: string;
  end;

  TFileStreamArr = array of TFileStreamRec;

// will enumerate and append to the list any streams with names ( streams with ID = ALTERNATE_DATA )
// return True on success enumeration in full
// return False on failure, but will not remove what already being added before failing
function FileStreamsEnum(const FileName: string; var StreamNameList: TStringList): Boolean;


// same as EnumFileStreams for the result, but with all stream details, and can return all streams not just named
function FileStreamsEnumEx(const FileName: string; var StreamsArr: TFileStreamArr; OnlyNamed: Boolean = True): Boolean;


// delete named stream(s) (Id = BACKUP_ALTERNATE_DATA)
// when StreamName is empty '' , all named streams will be deleted
function FileStreamsDelete(const FileName: string; const StreamName: string = ''): Boolean;

// not implemented yet, better to be with streamId with the name to suport all, another version is needed to get TBytes in memory
//function FileStreamsExtract(const SourceFile,DestFile,StreamName: string):Integer;

implementation

type
  PWIN32_STREAM_ID = ^TWIN32_STREAM_ID;

  TWIN32_STREAM_ID = packed record // Delphi XE8 doesn't have this record packed, i prefer packed
    dwStreamId: Cardinal;
    dwStreamAttributes: Cardinal;
    Size: TLargeInteger;
    dwStreamNameSize: Cardinal;
    cStreamName: array[0..1] of WChar; // we compensate with extra 1 char (2 bytes) for 4 bytes, just for alignment
   end;

function FileStreamsEnumEx(const FileName: string; var StreamsArr: TFileStreamArr; OnlyNamed: Boolean = True): Boolean;
var
  Handle: THandle;
  StreamRec: TWIN32_STREAM_ID;
  StreamContext: Pointer;
  BytesRead: Cardinal;
  dwLowByteSeeked, dwHighByteSeeked: Cardinal;
  SeekResult: Boolean;
  StreamName: string;

  procedure AddStreamToList;
  var
    CurrIndex: Integer;
  begin
    CurrIndex := Length(StreamsArr);
    SetLength(StreamsArr, CurrIndex + 1);
    StreamsArr[CurrIndex].StreamID := StreamRec.dwStreamId;
    StreamsArr[CurrIndex].StreamAttribute := StreamRec.dwStreamAttributes;
    StreamsArr[CurrIndex].Size := StreamRec.Size;
  end;

begin
  Result := False;
  if FileName = 'then
    Exit;

  Handle := CreateFile(Pchar(FileName), GENERIC_READ, FILE_SHARE_READ, nil, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, 0);

  if Handle = INVALID_HANDLE_VALUE then
    Exit;

  StreamContext := nil; // must be intialized to nil
  while BackupRead(Handle, @StreamRec, SizeOf(StreamRec) - SizeOf(StreamRec.cStreamName), BytesRead, False, True, StreamContext) do
  begin
    if (BytesRead = 0) or (BytesRead < SizeOf(StreamRec) - SizeOf(StreamRec.cStreamName)) then // reached the end
      Break;

    // invalid or unknown StreamId, safer to not handle
    if (StreamRec.dwStreamId = BACKUP_INVALID) or (StreamRec.dwStreamId > BACKUP_GHOSTED_FILE_EXTENTS) then
      Break;

    // per specification defined in [MS-BKUP]-210625
    // The value of this field (dwStreamId) MUST be 0 for all dwStreamId values other than
    // ALTERNATE_DATA. For StreamID ALTERNATE_DATA, the value of this field MUST be in the range
    // 0–65536, and it MUST be an integral multiple of two.
    if (StreamRec.dwStreamNameSize > 0) and (StreamRec.dwStreamId <> BACKUP_ALTERNATE_DATA) then // corrupted header or we lost position
      Break;

    if not OnlyNamed then
      AddStreamToList;

    if (StreamRec.dwStreamNameSize > 0) and (StreamRec.dwStreamNameSize < $10000) and (StreamRec.dwStreamNameSize and 1 = 0) then
    begin
      SetLength(StreamName, StreamRec.dwStreamNameSize div SizeOf(Char));
      if (not BackupRead(Handle, @StreamName[1], StreamRec.dwStreamNameSize, BytesRead, //
        False, True, StreamContext)) or (BytesRead <> StreamRec.dwStreamNameSize) then
        Break;
      if OnlyNamed then
        AddStreamToList;
      StreamsArr[Length(StreamsArr) - 1].StreamName := StreamName;
    end;

    if StreamRec.Size > 0 then
    begin
      // BackUpSeek will not seek across stream headers, so we can use one of the following two method
      // 1) we do BackupSeek with maximum range without result check, letting BackupRead fail later
      //BackupSeek(Handle, Cardinal(-1), Cardinal(-1), dwLowByteSeeked, dwHighByteSeeked, StreamContext);

      // 2) or we do it with exact size and checking for seek result then break now
      SeekResult := BackupSeek(Handle, Cardinal(StreamRec.Size), Cardinal(StreamRec.Size shr 32), //
        dwLowByteSeeked, dwHighByteSeeked, StreamContext);
      if (not SeekResult) or (dwLowByteSeeked + dwHighByteSeeked shl 32 = 0) then
        Break;
      // the difference is : which one we prefer to fail as end of streams for this file, BackupRead or BackupSeek
    end;
  end;

  Result := True;

  if StreamContext <> nil then // cleanup and release stream context
    BackupRead(Handle, nil, 0, BytesRead, True, True, StreamContext); // bAbort = True
  CloseHandle(Handle);
end;

function FileStreamsEnum(const FileName: string; var StreamNameList: TStringList): Boolean;
var
  Streams: TFileStreamArr;
  i: Integer;
begin
  Result := FileStreamsEnumEx(FileName, Streams, True);
  if Length(Streams) > 0 then
    for i := 0 to Length(Streams) - 1 do
      StreamNameList.Add(Streams[i].StreamName);
end;

function FileStreamsDelete(const FileName: string; const StreamName: string = ''): Boolean;
var
  Streams: TFileStreamArr;
  i: Integer;
begin
  Result := FileStreamsEnumEx(FileName, Streams, True);

  i := 0;
  while i < Length(Streams) do
  begin
    if (StreamName = '') or (StreamName = Streams[i].StreamName) then
      Result := DeleteFile(PChar(FileName + string(Streams[i].StreamName)));// just concatenate, DeleteFile support Names Streams
    if StreamName = Streams[i].StreamName then
      Break;
    Inc(i);
  end;
end;

end.
example to use
Delphi-Quellcode:
uses
  lcNTFSFileStreams;

procedure TForm10.Button1Click(Sender: TObject);
const
  FILE_NAME = 'D:\Projects Delphi\NTFSStreams\1.zip';
var
  StreamArr: TFileStreamArr;

  procedure DumpList;
  var
    i: Integer;
  begin
    if Length(StreamArr) > 0 then
    begin
      for i := 0 to Length(StreamArr) - 1 do
        Memo1.Lines.Append(#9'ID: ' + IntToStr(StreamArr[i].StreamID) + ' Size: ' + IntToStr(StreamArr[i].Size) + ' '#9 + StreamArr[i].StreamName);
    end
    else
      Memo1.Lines.Add(#9'file has no alternative data streams');
  end;

begin
  Memo1.Lines.Add('list all');
  FileStreamsEnumEx(FILE_NAME, StreamArr, False);
  DumpList;

  Memo1.Lines.Add('deleting one');
  if FileStreamsDelete(FILE_NAME, ':URL:$DATA') then
    Memo1.Lines.Add('Stream deleted')
  else
    Memo1.Lines.Add('Stream wasn''t deleted');

  SetLength(StreamArr, 0); // clear
  FileStreamsEnumEx(FILE_NAME, StreamArr);
  DumpList;

  Memo1.Lines.Add('deleting all');
  FileStreamsEnumEx(FILE_NAME, StreamArr);
  FileStreamsDelete(FILE_NAME, '');

  Memo1.Lines.Add('list named');
  SetLength(StreamArr, 0); // clear
  FileStreamsEnumEx(FILE_NAME, StreamArr);
  DumpList;

  Memo1.Lines.Add('list all');
  FileStreamsEnumEx(FILE_NAME, StreamArr, False);
  DumpList;
end;
Memo result
Code:
list all
   ID: 3 Size: 92     
   ID: 1 Size: 1681665     
   ID: 4 Size: 935     :Delphi_File:$DATA
   ID: 4 Size: 19     :URL:$DATA
   ID: 4 Size: 167     :Zone.Identifier:$DATA
deleting one
Stream deleted
   ID: 4 Size: 935     :Delphi_File:$DATA
   ID: 4 Size: 167     :Zone.Identifier:$DATA
deleting all
list named
   file has no alternative data streams
list all
   ID: 3 Size: 92     
   ID: 1 Size: 1681665
Kas

Geändert von Kas Ob. (21. Aug 2024 um 12:25 Uhr)
  Mit Zitat antworten Zitat