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.