Delphi-PRAXiS

Delphi-PRAXiS (https://www.delphipraxis.net/forum.php)
-   Algorithmen, Datenstrukturen und Klassendesign (https://www.delphipraxis.net/78-algorithmen-datenstrukturen-und-klassendesign/)
-   -   Small library for Alternative Data Streams (ADS) (https://www.delphipraxis.net/215690-small-library-alternative-data-streams-ads.html)

Kas Ob. 21. Aug 2024 12:04


Small library for Alternative Data Streams (ADS)
 
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

dummzeuch 21. Aug 2024 12:21

AW: Small library for Alternative Data Streams (ADS)
 
Thanks. Interesting.

Could you please edit your post and replace the code tags with Delphi tags, so we get syntax highlighting?

Code:
procedure bla;
begin
end;
vs.
Delphi-Quellcode:
procedure bla;
begin
end;

Kas Ob. 21. Aug 2024 12:26

AW: Small library for Alternative Data Streams (ADS)
 
Zitat:

Zitat von dummzeuch (Beitrag 1540050)
could you please edit your post and replace the code tags with Delphi tags, so we get syntax highlighting?

Thank you, i never saw that button :oops:

DeddyH 21. Aug 2024 13:46

AW: Small library for Alternative Data Streams (ADS)
 
Passing a TStringList as Var-Parameter does not make sense in this context. The underlying instance has to be created and destroyed from outside, so you can even pass it as Const-Parameter.

himitsu 21. Aug 2024 13:48

AW: Small library for Alternative Data Streams (ADS)
 
Die BackupAPI, und dafür die nötigen Rechte, ist garnicht nötig. (Administrator oder ein Nutzer mit Backup&Restore-Privilegien aka SE_BACKUP_NAME)
Seit Vista gibt es auch eine neue API dafür. MSDN-Library durchsuchenFindFirstStreamW.

https://learn.microsoft.com/de-de/wi...ndfirststreamw
https://learn.microsoft.com/en-us/ar...g-ntfs-streams
https://www.codeproject.com/Articles...e-Data-Streams

https://learn.microsoft.com/en-us/wi...o/file-streams



Mußt du dir aber selbt im Delphi implementieren, da die API-Imports im Delphi viel zu alt sind. (Winapi.Windows.pas)

Aber mit etwas Glück, kannst du diesen Teil vielleicht aus dem WinMD nutzen,
auch wenn zu viel Anderes einfach nur Schrott ist. (WinMD selbst ist OK, aber nicht das was Embarcadero daraus gemacht hat)
https://getitnow.embarcadero.com/win...pi-from-winmd/
https://www.delphipraxis.net/214473-...vor-winmd.html



PS:
Delphi-Quellcode:
Length(StreamArr) - 1
=
Delphi-Quellcode:
High(StreamArr)

Kas Ob. 21. Aug 2024 13:58

AW: Small library for Alternative Data Streams (ADS)
 
Zitat:

Zitat von DeddyH (Beitrag 1540053)
Passing a TStringList as Var-Parameter does not make sense in this context. The underlying instance has to be created and destroyed from outside, so you can even pass it as Const-Parameter.

Right and makes sense, only i prefer this usage (const vs var) with classes to distinguish between functions/procedures that read-only against what might change the content, so yes it is just cosmetic, like with this case and that example, but will makes difference with multithreads usage, easier to notice and remember and that is it, where and when need thread synchronization.

Also: we only can dream of compiler and more advanced Delphi/Pascal language feature to detect and prevent internal changes coming form class internal implementation, that makes const and var more powerful, producing safer memory and logical handling.


Alle Zeitangaben in WEZ +1. Es ist jetzt 02:58 Uhr.

Powered by vBulletin® Copyright ©2000 - 2025, Jelsoft Enterprises Ltd.
LinkBacks Enabled by vBSEO © 2011, Crawlability, Inc.
Delphi-PRAXiS (c) 2002 - 2023 by Daniel R. Wolf, 2024 by Thomas Breitkreuz