diff --git a/Tests/Fs/Mods/Apache/503.html b/Tests/Fs/Mods/Apache/503.html
new file mode 100644
index 0000000..177f983
--- /dev/null
+++ b/Tests/Fs/Mods/Apache/503.html
@@ -0,0 +1 @@
+Please, call us later.
\ No newline at end of file
diff --git a/Tests/VfsBaseTest.pas b/Tests/VfsBaseTest.pas
index 9ce9ba7..2eb3188 100644
--- a/Tests/VfsBaseTest.pas
+++ b/Tests/VfsBaseTest.pas
@@ -46,8 +46,14 @@ begin
VfsBase.MapDir(RootDir, RootDir + '\Mods\B', DONT_OVERWRITE_EXISTING);
VfsBase.MapDir(RootDir, RootDir + '\Mods\A', DONT_OVERWRITE_EXISTING);
VfsBase.RunVfs(SORT_FIFO);
- VfsBase.GetVfsDirInfo(RootDir, '*', DirInfo, DirListing);
+ VfsBase.PauseVfs;
+ VfsBase.GetVfsDirInfo(RootDir, '*', DirInfo, DirListing);
+ DirListing.Rewind;
+ Check(DirListing.GetDebugDump() = '', 'Virtual directory listing must be empty when VFS is paused. Got: ' + DirListing.GetDebugDump());
+
+ VfsBase.RunVfs(SORT_FIFO);
+ VfsBase.GetVfsDirInfo(RootDir, '*', DirInfo, DirListing);
DirListing.Rewind;
Check(DirListing.GetDebugDump() = 'vcredist.bmp'#13#10'eula.1028.txt', 'Invalid virtual directoring listing. Got: ' + DirListing.GetDebugDump());
diff --git a/Tests/VfsControl.pas b/Tests/VfsControl.pas
new file mode 100644
index 0000000..dd81884
--- /dev/null
+++ b/Tests/VfsControl.pas
@@ -0,0 +1,71 @@
+unit VfsControl;
+(*
+ Facade unit for high-level VFS API.
+*)
+
+
+(***) interface (***)
+
+uses
+ Windows, SysUtils,
+ Utils,
+ VfsBase, VfsUtils, VfsHooks;
+
+
+(* Runs all VFS subsystems, unless VFS is already running *)
+function RunVfs (DirListingOrder: VfsBase.TDirListingSortType): boolean;
+
+
+(***) implementation (***)
+
+
+function GetCurrentDirW: WideString;
+var
+ Buf: array [0..32767 - 1] of WideChar;
+ ResLen: integer;
+
+begin
+ result := '';
+ ResLen := Windows.GetCurrentDirectoryW(sizeof(Buf), @Buf);
+
+ if ResLen > 0 then begin
+ SetLength(result, ResLen);
+ Utils.CopyMem(ResLen * sizeof(WideChar), @Buf, PWideChar(result));
+ end;
+end;
+
+function SetCurrentDirW (const DirPath: WideString): boolean;
+var
+ AbsPath: WideString;
+
+begin
+ AbsPath := VfsUtils.NormalizePath(DirPath);
+ result := Windows.SetCurrentDirectoryW(PWideChar(AbsPath));
+end;
+
+function RunVfs (DirListingOrder: VfsBase.TDirListingSortType): boolean;
+var
+ CurrDir: WideString;
+
+begin
+ with VfsBase.VfsCritSection do begin
+ Enter;
+
+ result := VfsBase.RunVfs(DirListingOrder);
+
+ if result then begin
+ VfsHooks.InstallHooks;
+
+ // Try to ensure, that current directory handle is tracked by VfsOpenFiles
+ CurrDir := GetCurrentDirW;
+
+ if CurrDir <> '' then begin
+ SetCurrentDirW(CurrDir);
+ end;
+ end;
+
+ Leave;
+ end; // .with
+end; // function RunVfs
+
+end.
\ No newline at end of file
diff --git a/Tests/VfsIntegratedTest.pas b/Tests/VfsIntegratedTest.pas
new file mode 100644
index 0000000..6622082
--- /dev/null
+++ b/Tests/VfsIntegratedTest.pas
@@ -0,0 +1,97 @@
+unit VfsIntegratedTest;
+
+(***) interface (***)
+
+uses
+ SysUtils, TestFramework, Windows,
+ Utils, WinUtils, ConsoleApi,
+ VfsUtils, VfsBase, VfsDebug,
+ VfsControl;
+
+type
+ TestIntegrated = class (TTestCase)
+ private
+ Inited: boolean;
+
+ function GetRootDir: string;
+
+ protected
+ procedure SetUp; override;
+ procedure TearDown; override;
+
+ published
+ procedure TestGetFileAttributes;
+ end;
+
+(***) implementation (***)
+
+
+procedure LogSomething (Operation, Message: pchar); stdcall;
+begin
+ WriteLn('>> ', string(Operation), ': ', string(Message), #13#10);
+end;
+
+function TestIntegrated.GetRootDir: string;
+begin
+ result := SysUtils.ExtractFileDir(WinUtils.GetExePath) + '\Tests\Fs';
+end;
+
+procedure TestIntegrated.SetUp;
+var
+ RootDir: string;
+
+begin
+ if not Inited then begin
+ Inited := true;
+ RootDir := Self.GetRootDir;
+ VfsBase.ResetVfs();
+ VfsBase.MapDir(RootDir, RootDir + '\Mods\FullyVirtual', DONT_OVERWRITE_EXISTING);
+ VfsBase.MapDir(RootDir, RootDir + '\Mods\B', DONT_OVERWRITE_EXISTING);
+ VfsBase.MapDir(RootDir, RootDir + '\Mods\A', DONT_OVERWRITE_EXISTING);
+ VfsBase.MapDir(RootDir, RootDir + '\Mods\Apache', DONT_OVERWRITE_EXISTING);
+ VfsDebug.SetLoggingProc(LogSomething);
+ VfsControl.RunVfs(VfsBase.SORT_FIFO);
+ end;
+end;
+
+procedure TestIntegrated.TearDown;
+begin
+ VfsBase.PauseVfs();
+ VfsDebug.SetLoggingProc(nil);
+end;
+
+procedure TestIntegrated.TestGetFileAttributes;
+var
+ RootDir: string;
+
+ function HasValidAttrs (const Path: string; const RequiredAttrs: integer = 0; const ForbiddenAttrs: integer = 0): boolean;
+ var
+ Attrs: integer;
+
+ begin
+ Attrs := Int(Windows.GetFileAttributes(pchar(Path)));
+ result := Attrs <> -1;
+
+ if result then begin
+ if RequiredAttrs <> 0 then begin
+ result := (Attrs and RequiredAttrs) = RequiredAttrs;
+ end;
+
+ if result and (ForbiddenAttrs <> 0) then begin
+ result := (Attrs and ForbiddenAttrs) = 0;
+ end;
+ end;
+ end; // .function HasValidAttrs
+
+begin
+ RootDir := Self.GetRootDir;
+ Check(not HasValidAttrs(RootDir + '\non-existing.non'), '{1}');
+ Check(HasValidAttrs(RootDir + '\Hobbots\mms.cfg', 0, Windows.FILE_ATTRIBUTE_DIRECTORY), '{2}');
+ Check(HasValidAttrs(RootDir + '\503.html', 0, Windows.FILE_ATTRIBUTE_DIRECTORY), '{3}');
+ Check(HasValidAttrs(RootDir + '\Hobbots\', Windows.FILE_ATTRIBUTE_DIRECTORY), '{4}');
+ Check(HasValidAttrs(RootDir + '\Mods', Windows.FILE_ATTRIBUTE_DIRECTORY), '{5}');
+end;
+
+begin
+ RegisterTest(TestIntegrated.Suite);
+end.
\ No newline at end of file
diff --git a/Tests/VfsTestHelper.pas b/Tests/VfsTestHelper.pas
new file mode 100644
index 0000000..624dada
--- /dev/null
+++ b/Tests/VfsTestHelper.pas
@@ -0,0 +1,46 @@
+unit VfsTestHelper;
+(*
+
+*)
+
+
+(***) interface (***)
+
+uses
+ SysUtils, Windows,
+ Utils;
+
+(* Initializes debug console *)
+procedure InitConsole;
+
+
+(***) implementation (***)
+
+
+procedure InitConsole;
+var
+ Rect: TSmallRect;
+ BufSize: TCoord;
+ hIn: THandle;
+ hOut: THandle;
+
+begin
+ AllocConsole;
+ SetConsoleCP(GetACP);
+ SetConsoleOutputCP(GetACP);
+ hIn := GetStdHandle(STD_INPUT_HANDLE);
+ hOut := GetStdHandle(STD_OUTPUT_HANDLE);
+ pinteger(@System.Input)^ := hIn;
+ pinteger(@System.Output)^ := hOut;
+ BufSize.x := 120;
+ BufSize.y := 1000;
+ SetConsoleScreenBufferSize(hOut, BufSize);
+ Rect.Left := 0;
+ Rect.Top := 0;
+ Rect.Right := 120 - 1;
+ Rect.Bottom := 50 - 1;
+ SetConsoleWindowInfo(hOut, true, Rect);
+ SetConsoleTextAttribute(hOut, (0 shl 4) or $0F);
+end; // .procedure InitConsole;
+
+end.
\ No newline at end of file
diff --git a/VfsBase.pas b/VfsBase.pas
index 94637e4..c8a2335 100644
--- a/VfsBase.pas
+++ b/VfsBase.pas
@@ -36,7 +36,7 @@ type
SORT_FIFO - Items of the first mapped directory will be listed before the second mapped directory items.
SORT_LIFO - Items of The last mapped directory will be listed before all other mapped directory items.
*)
- TDirListingSortType = (SORT_FIFO, SORT_LIFO);
+ TDirListingSortType = (SORT_FIFO = 0, SORT_LIFO = 1);
(* Single redirected VFS entry: file or directory *)
TVfsItem = class
@@ -79,6 +79,8 @@ type
procedure RestoreVfsForThread;
end;
+ TSingleArgExternalFunc = function (Arg: pointer = nil): integer; stdcall;
+
var
(* Global VFS access synchronizer *)
VfsCritSection: Concur.TCritSection;
@@ -86,9 +88,13 @@ var
function GetThreadVfsDisabler: TThreadVfsDisabler;
-(* Runs VFS. Higher level API must install hooks in VfsCritSection protected area *)
+(* Runs VFS. Higher level API must install hooks in VfsCritSection protected area.
+ Listing order is ignored if VFS is resumed from pause *)
function RunVfs (DirListingOrder: TDirListingSortType): boolean;
+(* Temporarily pauses VFS, but does not reset existing mappings *)
+function PauseVfs: boolean;
+
(* Stops VFS and clears all mappings *)
function ResetVfs: boolean;
@@ -101,6 +107,9 @@ function GetVfsDirInfo (const AbsVirtPath, Mask: WideString; {OUT} var DirInfo:
(* Maps real directory contents to virtual path. Target must exist for success *)
function MapDir (const VirtPath, RealPath: WideString; OverwriteExisting: boolean; Flags: integer = 0): boolean;
+(* Calls specified function with a single argument and returns its result. VFS is disabled for current thread during function exection *)
+function CallWithoutVfs (Func: TSingleArgExternalFunc; Arg: pointer = nil): integer; stdcall;
+
(***) implementation (***)
@@ -115,6 +124,9 @@ var
(* Global VFS state indicator. If false, all VFS search operations must fail *)
VfsIsRunning: boolean = false;
+
+ (* If true, VFS file/directory hierarchy is built and no mapping is allowed untill full reset *)
+ VfsTreeIsBuilt: boolean = false;
(* Automatical VFS items priority management *)
OverwritingPriority: integer = INITIAL_OVERWRITING_PRIORITY;
@@ -271,15 +283,32 @@ begin
Enter;
if not VfsIsRunning then begin
- BuildVfsItemsTree();
- SortVfsDirListings(DirListingOrder);
+ if not VfsTreeIsBuilt then begin
+ BuildVfsItemsTree();
+ SortVfsDirListings(DirListingOrder);
+ VfsTreeIsBuilt := true;
+ end;
+
VfsIsRunning := true;
end;
+ Leave;
+ end; // .with
+ end; // .if
+end; // .function RunVfs
+
+function PauseVfs: boolean;
+begin
+ result := not DisableVfsForThisThread;
+
+ if result then begin
+ with VfsCritSection do begin
+ Enter;
+ VfsIsRunning := false;
Leave;
end;
end;
-end; // .function RunVfs
+end;
function ResetVfs: boolean;
begin
@@ -288,8 +317,9 @@ begin
if result then begin
with VfsCritSection do begin
Enter;
- VfsIsRunning := false;
VfsItems.Clear();
+ VfsIsRunning := false;
+ VfsTreeIsBuilt := false;
Leave;
end;
end;
@@ -481,9 +511,36 @@ end; // .function _MapDir
function MapDir (const VirtPath, RealPath: WideString; OverwriteExisting: boolean; Flags: integer = 0): boolean;
begin
- result := _MapDir(NormalizePath(VirtPath), NormalizePath(RealPath), nil, OverwriteExisting, AUTO_PRIORITY) <> nil;
+ with VfsCritSection do begin
+ Enter;
+
+ result := not VfsIsRunning and not VfsTreeIsBuilt;
+
+ if result then begin
+ result := _MapDir(NormalizePath(VirtPath), NormalizePath(RealPath), nil, OverwriteExisting, AUTO_PRIORITY) <> nil;
+ end;
+
+ Leave;
+ end;
end;
+function CallWithoutVfs (Func: TSingleArgExternalFunc; Arg: pointer = nil): integer; stdcall;
+begin
+ with GetThreadVfsDisabler do begin
+ try
+ DisableVfsForThread;
+ result := Func(Arg);
+ except
+ on E: Exception do begin
+ RestoreVfsForThread;
+ raise E;
+ end;
+ end; // .try
+
+ RestoreVfsForThread;
+ end; // .with
+end; // .function CallWithoutVfs
+
begin
VfsCritSection.Init;
VfsItems := DataLib.NewDict(Utils.OWNS_ITEMS, DataLib.CASE_SENSITIVE);
diff --git a/VfsHooks.pas b/VfsHooks.pas
new file mode 100644
index 0000000..4914629
--- /dev/null
+++ b/VfsHooks.pas
@@ -0,0 +1,556 @@
+unit VfsHooks;
+(*
+ Description: WinNT code hooks package.
+*)
+
+
+(***) interface (***)
+
+uses
+ Windows, SysUtils, Math,
+ Utils, WinNative, Concur,
+ StrLib, Alg,
+ VfsBase, VfsUtils, VfsPatching,
+ VfsDebug, VfsApiDigger, VfsOpenFiles;
+
+
+(* Installs VFS hooks, if not already installed, in a thread-safe manner *)
+procedure InstallHooks;
+
+
+(***) implementation (***)
+
+
+var
+ HooksCritSection: Concur.TCritSection;
+ HooksInstalled: boolean = false;
+
+ NativeNtQueryAttributesFile: WinNative.TNtQueryAttributesFile;
+ NativeNtQueryFullAttributesFile: WinNative.TNtQueryFullAttributesFile;
+ NativeNtOpenFile: WinNative.TNtOpenFile;
+ NativeNtCreateFile: WinNative.TNtCreateFile;
+ NativeNtClose: WinNative.TNtClose;
+ NativeNtQueryDirectoryFile: WinNative.TNtQueryDirectoryFile;
+
+
+(* There is no 100% portable and reliable way to get file path by handle, unless file creation/opening
+ was tracked. Thus we rely heavily on VfsOpenFiles.
+ In Windows access to files in curren directory under relative paths is performed via [hDir, RelPath] pair,
+ thus it's strongly recommended to ensure, that current directory handle is tracked by VfsOpenedFiles.
+ It can be perfomed via SetCurrentDir(GetCurrentDir) after VFS was run *)
+function GetFilePathByHandle (hFile: THandle): WideString;
+begin
+ result := VfsOpenFiles.GetOpenedFilePath(hFile);
+end;
+
+(* Returns single absolute path, not dependant on RootDirectory member. '\??\' prefix is always removed, \\.\ and \\?\ paths remain not touched. *)
+function GetFileObjectPath (ObjectAttributes: POBJECT_ATTRIBUTES): WideString;
+var
+ FilePath: WideString;
+ DirPath: WideString;
+
+begin
+ FilePath := ObjectAttributes.ObjectName.ToWideStr();
+ result := '';
+
+ if FilePath <> '' then begin
+ if FilePath[1] = '\' then begin
+ FilePath := VfsUtils.StripNtAbsPathPrefix(FilePath);
+ end;
+
+ if ObjectAttributes.RootDirectory <> 0 then begin
+ DirPath := GetFilePathByHandle(ObjectAttributes.RootDirectory);
+
+ if DirPath <> '' then begin
+ if DirPath[Length(DirPath)] <> '\' then begin
+ result := DirPath + '\' + FilePath;
+ end else begin
+ result := DirPath + FilePath;
+ end;
+ end;
+ end else begin
+ result := FilePath;
+ end;
+ end; // .if
+end; // .function GetFileObjectPath
+
+function Hook_NtQueryAttributesFile (OrigFunc: WinNative.TNtQueryAttributesFile; ObjectAttributes: POBJECT_ATTRIBUTES; FileInformation: PFILE_BASIC_INFORMATION): NTSTATUS; stdcall;
+var
+ ExpandedPath: WideString;
+ RedirectedPath: WideString;
+ ReplacedObjAttrs: WinNative.TObjectAttributes;
+ FileInfo: TNativeFileInfo;
+ HadTrailingDelim: boolean;
+
+begin
+ if VfsDebug.LoggingEnabled then begin
+ WriteLog('NtQueryAttributesFile', Format('Dir: %d. Path: "%s"', [ObjectAttributes.RootDirectory, ObjectAttributes.ObjectName.ToWideStr()]));
+ end;
+
+ ReplacedObjAttrs := ObjectAttributes^;
+ ReplacedObjAttrs.Length := sizeof(ReplacedObjAttrs);
+ ExpandedPath := GetFileObjectPath(ObjectAttributes);
+ RedirectedPath := '';
+
+ if ExpandedPath <> '' then begin
+ RedirectedPath := VfsBase.GetVfsItemRealPath(StrLib.ExcludeTrailingDelimW(ExpandedPath, @HadTrailingDelim), @FileInfo);
+ end;
+
+ // Return cached VFS file info
+ if RedirectedPath <> '' then begin
+ if not HadTrailingDelim or Utils.HasFlag(FILE_ATTRIBUTE_DIRECTORY, FileInfo.Base.FileAttributes) then begin
+ FileInformation.CreationTime := FileInfo.Base.CreationTime;
+ FileInformation.LastAccessTime := FileInfo.Base.LastAccessTime;
+ FileInformation.LastWriteTime := FileInfo.Base.LastWriteTime;
+ FileInformation.ChangeTime := FileInfo.Base.ChangeTime;
+ FileInformation.FileAttributes := FileInfo.Base.FileAttributes;
+ result := WinNative.STATUS_SUCCESS;
+ end else begin
+ result := WinNative.STATUS_NO_SUCH_FILE;
+ end;
+ end
+ // Query file with real path
+ else begin
+ RedirectedPath := ExpandedPath;
+
+ if RedirectedPath <> '' then begin
+ if RedirectedPath[1] <> '\' then begin
+ RedirectedPath := '\??\' + RedirectedPath;
+ end;
+
+ ReplacedObjAttrs.RootDirectory := 0;
+ ReplacedObjAttrs.Attributes := ReplacedObjAttrs.Attributes or WinNative.OBJ_CASE_INSENSITIVE;
+ ReplacedObjAttrs.ObjectName.AssignExistingStr(RedirectedPath);
+ end;
+
+ result := OrigFunc(@ReplacedObjAttrs, FileInformation);
+ end; // .else
+
+ if VfsDebug.LoggingEnabled then begin
+ WriteLog('NtQueryAttributesFile', Format('Result: %x. Attrs: 0x%x. Path: "%s" => "%s"', [result, FileInformation.FileAttributes, string(ExpandedPath), string(RedirectedPath)]));
+ end;
+end; // .function Hook_NtQueryAttributesFile
+
+function Hook_NtQueryFullAttributesFile (OrigFunc: WinNative.TNtQueryFullAttributesFile; ObjectAttributes: POBJECT_ATTRIBUTES; FileInformation: PFILE_NETWORK_OPEN_INFORMATION): NTSTATUS; stdcall;
+var
+ ExpandedPath: WideString;
+ RedirectedPath: WideString;
+ ReplacedObjAttrs: WinNative.TObjectAttributes;
+ HadTrailingDelim: boolean;
+
+begin
+ if VfsDebug.LoggingEnabled then begin
+ WriteLog('NtQueryFullAttributesFile', Format('Dir: %d. Path: "%s"', [ObjectAttributes.RootDirectory, ObjectAttributes.ObjectName.ToWideStr()]));
+ end;
+
+ ReplacedObjAttrs := ObjectAttributes^;
+ ReplacedObjAttrs.Length := sizeof(ReplacedObjAttrs);
+ ExpandedPath := GetFileObjectPath(ObjectAttributes);
+ RedirectedPath := '';
+
+ if ExpandedPath <> '' then begin
+ RedirectedPath := VfsBase.GetVfsItemRealPath(StrLib.ExcludeTrailingDelimW(ExpandedPath, @HadTrailingDelim));
+ end;
+
+ if RedirectedPath = '' then begin
+ RedirectedPath := ExpandedPath;
+ end else if HadTrailingDelim then begin
+ RedirectedPath := RedirectedPath + '\';
+ end;
+
+ if (RedirectedPath <> '') and (RedirectedPath[1] <> '\') then begin
+ RedirectedPath := '\??\' + RedirectedPath;
+ end;
+
+ ReplacedObjAttrs.RootDirectory := 0;
+ ReplacedObjAttrs.Attributes := ReplacedObjAttrs.Attributes or WinNative.OBJ_CASE_INSENSITIVE;
+ ReplacedObjAttrs.ObjectName.AssignExistingStr(RedirectedPath);
+
+ result := OrigFunc(@ReplacedObjAttrs, FileInformation);
+
+ if VfsDebug.LoggingEnabled then begin
+ WriteLog('NtQueryFullAttributesFile', Format('Result: %x. Attrs: 0x%x. Path: "%s" => "%s"', [result, FileInformation.FileAttributes, string(ExpandedPath), string(RedirectedPath)]));
+ end;
+end; // .Hook_NtQueryFullAttributesFile
+
+function Hook_NtOpenFile (OrigFunc: WinNative.TNtOpenFile; FileHandle: PHANDLE; DesiredAccess: ACCESS_MASK; ObjectAttributes: POBJECT_ATTRIBUTES;
+ IoStatusBlock: PIO_STATUS_BLOCK; ShareAccess: ULONG; OpenOptions: ULONG): NTSTATUS; stdcall;
+begin
+ if VfsDebug.LoggingEnabled then begin
+ WriteLog('NtOpenFile', ObjectAttributes.ObjectName.ToWideStr());
+ end;
+
+ result := WinNative.NtCreateFile(FileHandle, DesiredAccess, ObjectAttributes, IoStatusBlock, nil, 0, ShareAccess, WinNative.FILE_OPEN, OpenOptions, nil, 0);
+end;
+
+function Hook_NtCreateFile (OrigFunc: WinNative.TNtCreateFile; FileHandle: PHANDLE; DesiredAccess: ACCESS_MASK; ObjectAttributes: POBJECT_ATTRIBUTES; IoStatusBlock: PIO_STATUS_BLOCK;
+ AllocationSize: PLARGE_INTEGER; FileAttributes: ULONG; ShareAccess: ULONG; CreateDisposition: ULONG; CreateOptions: ULONG; EaBuffer: PVOID; EaLength: ULONG): NTSTATUS; stdcall;
+var
+ ExpandedPath: WideString;
+ RedirectedPath: WideString;
+ ReplacedObjAttrs: WinNative.TObjectAttributes;
+ HadTrailingDelim: boolean;
+
+ FileInfo: Windows.TWin32FindDataW;
+
+begin
+ if VfsDebug.LoggingEnabled then begin
+ WriteLog('NtCreateFile', ObjectAttributes.ObjectName.ToWideStr());
+ end;
+
+ ReplacedObjAttrs := ObjectAttributes^;
+ ReplacedObjAttrs.Length := sizeof(ReplacedObjAttrs);
+ ExpandedPath := GetFileObjectPath(ObjectAttributes);
+ RedirectedPath := '';
+
+ if (ExpandedPath <> '') and ((DesiredAccess and WinNative.DELETE) = 0) and (CreateDisposition = WinNative.FILE_OPEN) then begin
+ RedirectedPath := VfsBase.GetVfsItemRealPath(StrLib.ExcludeTrailingDelimW(ExpandedPath, @HadTrailingDelim), @FileInfo);
+ end;
+
+ if RedirectedPath = '' then begin
+ RedirectedPath := ExpandedPath;
+ end else if HadTrailingDelim then begin
+ RedirectedPath := RedirectedPath + '\';
+ end;
+
+ if (RedirectedPath <> '') and (RedirectedPath[1] <> '\') then begin
+ RedirectedPath := '\??\' + RedirectedPath;
+ end;
+
+ ReplacedObjAttrs.RootDirectory := 0;
+ ReplacedObjAttrs.Attributes := ReplacedObjAttrs.Attributes or WinNative.OBJ_CASE_INSENSITIVE;
+ ReplacedObjAttrs.ObjectName.AssignExistingStr(RedirectedPath);
+
+ result := OrigFunc(FileHandle, DesiredAccess, @ReplacedObjAttrs, IoStatusBlock, AllocationSize, FileAttributes, ShareAccess, CreateDisposition, CreateOptions, EaBuffer, EaLength);
+
+ if (result = WinNative.STATUS_SUCCESS) and Utils.HasFlag(WinNative.FILE_SYNCHRONOUS_IO_NONALERT, CreateOptions) and Utils.HasFlag(WinNative.SYNCHRONIZE, DesiredAccess) then begin
+ VfsOpenFiles.SetOpenedFileInfo(FileHandle^, TOpenedFile.Create(FileHandle^, ExpandedPath));
+ end;
+
+ if VfsDebug.LoggingEnabled then begin
+ if ExpandedPath <> StripNtAbsPathPrefix(RedirectedPath) then begin
+ WriteLog('NtCreateFile', Format('Access: 0x%x. Handle: %x. Status: %x. Redirected "%s" => "%s"', [DesiredAccess, FileHandle^, result, StrLib.WideToAnsiSubstitute(ExpandedPath), StrLib.WideToAnsiSubstitute(StripNtAbsPathPrefix(RedirectedPath))]));
+ end else begin
+ WriteLog('NtCreateFile', Format('Access: 0x%x. Handle: %x. Status: %x. Path: "%s"', [DesiredAccess, FileHandle^, result, StrLib.WideToAnsiSubstitute(ExpandedPath)]));
+ end;
+ end;
+end; // .function Hook_NtCreateFile
+
+function Hook_NtClose (OrigFunc: WinNative.TNtClose; hData: HANDLE): NTSTATUS; stdcall;
+begin
+ if VfsDebug.LoggingEnabled then begin
+ WriteLog('NtClose', Format('Handle: %x', [integer(hData)]));
+ end;
+
+ with OpenFilesCritSection do begin
+ Enter;
+ result := OrigFunc(hData);
+
+ if WinNative.NT_SUCCESS(result) then begin
+ VfsOpenFiles.DeleteOpenedFileInfo(hData);
+ end;
+
+ Leave;
+ end;
+
+ if VfsDebug.LoggingEnabled then begin
+ WriteLog('NtClose', Format('Status: %x', [integer(result)]));
+ end;
+end; // .function Hook_NtClose
+
+function IsSupportedFileInformationClass (FileInformationClass: integer): boolean;
+begin
+ result := (FileInformationClass <= High(byte)) and (FILE_INFORMATION_CLASS(byte(FileInformationClass)) in [FileBothDirectoryInformation, FileDirectoryInformation, FileFullDirectoryInformation, FileIdBothDirectoryInformation, FileIdFullDirectoryInformation, FileNamesInformation]);
+end;
+
+type
+ TFileInfoConvertResult = (TOO_SMALL_BUF, COPIED_ALL, TRUNCATED_NAME);
+ TTruncatedNamesStrategy = (DONT_TRUNCATE_NAMES, TRUNCATE_NAMES);
+
+function ConvertFileInfoStruct (SrcInfo: PNativeFileInfo; TargetFormat: FILE_INFORMATION_CLASS; {n} Buf: pointer; BufSize: integer; TruncatedNamesStrategy: TTruncatedNamesStrategy;
+ {OUT} var BytesWritten: integer): TFileInfoConvertResult;
+var
+{n} FileNameBuf: pointer;
+ FileNameBufSize: integer;
+ StructBaseSize: integer;
+ StructFullSize: integer;
+
+begin
+ {!} Assert(SrcInfo <> nil);
+ {!} Assert(IsSupportedFileInformationClass(ord(TargetFormat)), Format('Unsupported file information class: %d', [ord(TargetFormat)]));
+ FileNameBuf := nil;
+ // * * * * * //
+ BytesWritten := 0;
+ StructBaseSize := WinNative.GetFileInformationClassSize(TargetFormat);
+ StructFullSize := StructBaseSize + Int(SrcInfo.Base.FileNameLength);
+
+ if (Buf = nil) or (BufSize < StructBaseSize) then begin
+ result := TOO_SMALL_BUF;
+ exit;
+ end;
+
+ result := COPIED_ALL;
+
+ if BufSize < StructFullSize then begin
+ result := TRUNCATED_NAME;
+
+ if TruncatedNamesStrategy = DONT_TRUNCATE_NAMES then begin
+ exit;
+ end;
+ end;
+
+ case TargetFormat of
+ FileNamesInformation: PFILE_NAMES_INFORMATION(Buf).FileNameLength := SrcInfo.Base.FileNameLength;
+
+ FileBothDirectoryInformation, FileDirectoryInformation, FileFullDirectoryInformation, FileIdBothDirectoryInformation, FileIdFullDirectoryInformation: begin
+ Utils.CopyMem(StructBaseSize, @SrcInfo.Base, Buf);
+ end;
+ else
+ {!} Assert(IsSupportedFileInformationClass(ord(TargetFormat)), Format('Unexpected unsupported file information class: %d', [ord(TargetFormat)]));
+ end;
+
+ FileNameBufSize := Min(BufSize - StructBaseSize, SrcInfo.Base.FileNameLength) and not $00000001;
+ FileNameBuf := Utils.PtrOfs(Buf, StructBaseSize);
+
+ Utils.CopyMem(FileNameBufSize, PWideChar(SrcInfo.FileName), FileNameBuf);
+
+ BytesWritten := StructBaseSize + FileNameBufSize;
+end; // .function ConvertFileInfoStruct
+
+function Hook_NtQueryDirectoryFile (OrigFunc: WinNative.TNtQueryDirectoryFile; FileHandle: HANDLE; Event: HANDLE; ApcRoutine: pointer; ApcContext: PVOID; Io: PIO_STATUS_BLOCK; Buffer: PVOID;
+ BufLength: ULONG; InfoClass: integer (* FILE_INFORMATION_CLASS *); SingleEntry: BOOLEAN; {n} Mask: PUNICODE_STRING; RestartScan: BOOLEAN): NTSTATUS; stdcall;
+const
+ ENTRIES_ALIGNMENT = 8;
+
+type
+ PPrevEntry = ^TPrevEntry;
+ TPrevEntry = packed record
+ NextEntryOffset: ULONG;
+ FileIndex: ULONG;
+ end;
+
+var
+{Un} OpenedFile: TOpenedFile;
+{Un} FileInfo: TFileInfo;
+{n} BufCurret: pointer;
+{n} PrevEntry: PPrevEntry;
+ BufSize: integer;
+ BufSizeLeft: integer;
+ BytesWritten: integer;
+ IsFirstEntry: boolean;
+ Proceed: boolean;
+ TruncatedNamesStrategy: TTruncatedNamesStrategy;
+ StructConvertResult: TFileInfoConvertResult;
+ EmptyMask: UNICODE_STRING;
+ EntryName: WideString;
+
+begin
+ OpenedFile := nil;
+ FileInfo := nil;
+ BufCurret := nil;
+ PrevEntry := nil;
+ BufSize := 0;
+ // * * * * * //
+ with OpenFilesCritSection do begin
+ if Mask = nil then begin
+ EmptyMask.Reset;
+ Mask := @EmptyMask;
+ end;
+
+ if VfsDebug.LoggingEnabled then begin
+ WriteLog('NtQueryDirectoryFile', Format('Handle: %x. InfoClass: %s. Mask: %s', [integer(FileHandle), WinNative.FileInformationClassToStr(InfoClass), AnsiString(Mask.ToWideStr())]));
+ end;
+
+ Enter;
+
+ // FIXME REWRITE ME
+ //OpenedFile := OpenedFiles[pointer(FileHandle)];
+
+ if (OpenedFile = nil) or (Event <> 0) or (ApcRoutine <> nil) or (ApcContext <> nil) then begin
+ WriteLog('NtQueryDirectoryFile', Format('Calling native NtQueryDirectoryFile. OpenedFile: %x. %d %d %d', [integer(OpenedFile), integer(Event), integer(ApcRoutine), integer(ApcContext)]));
+ result := OrigFunc(FileHandle, Event, ApcRoutine, ApcContext, Io, Buffer, BufLength, InfoClass, SingleEntry, Mask, RestartScan);
+ end else begin
+ int(Io.Information) := 0;
+ result := STATUS_SUCCESS;
+
+ if RestartScan then begin
+ SysUtils.FreeAndNil(OpenedFile.DirListing);
+ end;
+
+ OpenedFile.FillDirListing(Mask.ToWideStr());
+
+ Proceed := (Buffer <> nil) and (BufLength > 0);
+
+ // Validate buffer
+ if not Proceed then begin
+ result := STATUS_INVALID_BUFFER_SIZE;
+ end else begin
+ BufSize := Utils.IfThen(int(BufLength) > 0, int(BufLength), High(int));
+ end;
+
+ // Validate information class
+ if Proceed then begin
+ Proceed := IsSupportedFileInformationClass(InfoClass);
+
+ if not Proceed then begin
+ result := STATUS_INVALID_INFO_CLASS;
+ end;
+ end;
+
+ // Signal of scanning end, if necessary
+ if Proceed then begin
+ Proceed := not OpenedFile.DirListing.IsEnd;
+
+ if not Proceed then begin
+ result := STATUS_NO_MORE_FILES;
+ end;
+ end;
+
+ // Scan directory
+ if Proceed then begin
+ BufCurret := Buffer;
+ BytesWritten := 1;
+
+ while (BytesWritten > 0) and OpenedFile.DirListing.GetNextItem(FileInfo) do begin
+ // Align next record to 8-bytes boundary from Buffer start
+ BufCurret := pointer(int(Buffer) + Alg.IntRoundToBoundary(int(Io.Information), ENTRIES_ALIGNMENT));
+ BufSizeLeft := BufSize - (int(BufCurret) - int(Buffer));
+
+ IsFirstEntry := OpenedFile.DirListing.FileInd = 1;
+
+ if IsFirstEntry then begin
+ TruncatedNamesStrategy := TRUNCATE_NAMES;
+ end else begin
+ TruncatedNamesStrategy := DONT_TRUNCATE_NAMES;
+ end;
+
+ StructConvertResult := ConvertFileInfoStruct(@FileInfo.Data, FILE_INFORMATION_CLASS(byte(InfoClass)), BufCurret, BufSizeLeft, TruncatedNamesStrategy, BytesWritten);
+
+ if VfsDebug.LoggingEnabled then begin
+ EntryName := Copy(FileInfo.Data.FileName, 1, Min(BytesWritten - WinNative.GetFileInformationClassSize(InfoClass), FileInfo.Data.Base.FileNameLength) div 2);
+ WriteLog('NtQueryDirectoryFile', 'Written entry: ' + EntryName);
+ end;
+
+ //VarDump(['Converted struct to buf offset:', int(BufCurret) - int(Buffer), 'Written:', BytesWritten, 'Result:', ord(StructConvertResult)]);
+
+ with PFILE_ID_BOTH_DIR_INFORMATION(BufCurret)^ do begin
+ NextEntryOffset := 0;
+ FileIndex := 0;
+ end;
+
+ if StructConvertResult = TOO_SMALL_BUF then begin
+ OpenedFile.DirListing.SeekRel(-1);
+
+ if IsFirstEntry then begin
+ result := STATUS_BUFFER_TOO_SMALL;
+ end;
+ end else if StructConvertResult = TRUNCATED_NAME then begin
+ if IsFirstEntry then begin
+ result := STATUS_BUFFER_OVERFLOW;
+ Inc(int(Io.Information), BytesWritten);
+ end else begin
+ OpenedFile.DirListing.SeekRel(-1);
+ end;
+ end else if StructConvertResult = COPIED_ALL then begin
+ if PrevEntry <> nil then begin
+ int(Io.Information) := int(BufCurret) - int(Buffer) + BytesWritten;
+ end else begin
+ int(Io.Information) := BytesWritten;
+ end;
+ end; // .else
+
+ if (BytesWritten > 0) and (PrevEntry <> nil) then begin
+ PrevEntry.NextEntryOffset := cardinal(int(BufCurret) - int(PrevEntry));
+ end;
+
+ PrevEntry := BufCurret;
+
+ //Msg(Format('Written: %d. Total: %d', [BytesWritten, int(Io.Information)]));
+
+ if SingleEntry then begin
+ BytesWritten := 0;
+ end;
+ end; // .while
+ end; // .if
+
+ Io.Status.Status := result;
+ end; // .else
+
+ Leave;
+ end; // .with
+
+ if VfsDebug.LoggingEnabled then begin
+ WriteLog('NtQueryDirectoryFile', Format('Status: %x. Written: %d bytes', [integer(result), integer(Io.Information)]));
+ end;
+end; // .function Hook_NtQueryDirectoryFile
+
+procedure InstallHooks;
+var
+ hDll: Windows.THandle;
+ NtdllHandle: integer;
+
+begin
+ with HooksCritSection do begin
+ Enter;
+
+ if not HooksInstalled then begin
+ HooksInstalled := true;
+
+ // Ensure, that library with VFS hooks installed is never unloaded
+ if System.IsLibrary then begin
+ WinNative.GetModuleHandleExW(WinNative.GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS or WinNative.GET_MODULE_HANDLE_EX_FLAG_PIN, @InstallHooks, hDll);
+ end;
+
+ NtdllHandle:= Windows.GetModuleHandle('ntdll.dll');
+ {!} Assert(NtdllHandle <> 0, 'Failed to load ntdll.dll library');
+
+ WriteLog('InstallHook', 'Installing NtQueryAttributesFile hook');
+ NativeNtQueryAttributesFile := VfsPatching.SpliceWinApi
+ (
+ VfsApiDigger.GetRealProcAddress(NtdllHandle, 'NtQueryAttributesFile'),
+ @Hook_NtQueryAttributesFile
+ );
+
+ // WriteLog('InstallHook', 'Installing NtQueryFullAttributesFile hook');
+ // NativeNtQueryFullAttributesFile := VfsPatching.SpliceWinApi
+ // (
+ // VfsApiDigger.GetRealProcAddress(NtdllHandle, 'NtQueryFullAttributesFile'),
+ // @Hook_NtQueryFullAttributesFile
+ // );
+
+ // WriteLog('InstallHook', 'Installing NtOpenFile hook');
+ // NativeNtOpenFile := VfsPatching.SpliceWinApi
+ // (
+ // VfsApiDigger.GetRealProcAddress(NtdllHandle, 'NtOpenFile'),
+ // @Hook_NtOpenFile
+ // );
+
+ // WriteLog('InstallHook', 'Installing NtCreateFile hook');
+ // NativeNtCreateFile := VfsPatching.SpliceWinApi
+ // (
+ // VfsApiDigger.GetRealProcAddress(NtdllHandle, 'NtCreateFile'),
+ // @Hook_NtCreateFile
+ // );
+
+ // WriteLog('InstallHook', 'Installing NtClose hook');
+ // NativeNtClose := VfsPatching.SpliceWinApi
+ // (
+ // VfsApiDigger.GetRealProcAddress(NtdllHandle, 'NtClose'),
+ // @Hook_NtClose
+ // );
+
+ // WriteLog('InstallHook', 'Installing NtQueryDirectoryFile hook');
+ // NativeNtQueryDirectoryFile := VfsPatching.SpliceWinApi
+ // (
+ // VfsApiDigger.GetRealProcAddress(NtdllHandle, 'NtQueryDirectoryFile'),
+ // @Hook_NtQueryDirectoryFile
+ // );
+ end; // .if
+
+ Leave;
+ end; // .with
+end; // .procedure InstallHooks
+
+begin
+ HooksCritSection.Init;
+end.
\ No newline at end of file
diff --git a/VfsPatching.pas b/VfsPatching.pas
new file mode 100644
index 0000000..1385180
--- /dev/null
+++ b/VfsPatching.pas
@@ -0,0 +1,129 @@
+unit VfsPatching;
+(*
+ Description: Code patching facilities, based on PatchForge library.
+ All hooks are thread-safe.
+*)
+
+
+(***) interface (***)
+
+uses
+ Windows, SysUtils, Utils, PatchForge;
+
+
+(* Replaces original STDCALL function with the new one with the same prototype and one extra argument.
+ The argument is callable pointer, used to execute original function. The pointer is passed as THE FIRST
+ argument before other arguments. *)
+function SpliceWinApi (OrigFunc, HandlerFunc: pointer): pointer;
+
+
+(***) implementation (***)
+
+
+type
+ (* Import *)
+ TPatchMaker = PatchForge.TPatchMaker;
+ TPatchHelper = PatchForge.TPatchHelper;
+
+
+(* Writes arbitrary data to any write-protected section *)
+function WriteAtCode (NumBytes: integer; {n} Src, {n} Dst: pointer): boolean;
+var
+ OldPageProtect: integer;
+
+begin
+ {!} Assert(Utils.IsValidBuf(Src, NumBytes));
+ {!} Assert(Utils.IsValidBuf(Dst, NumBytes));
+ result := NumBytes = 0;
+
+ if not result then begin
+ try
+ result := Windows.VirtualProtect(Dst, NumBytes, Windows.PAGE_EXECUTE_READWRITE, @OldPageProtect);
+
+ if result then begin
+ Utils.CopyMem(NumBytes, Src, Dst);
+ Windows.VirtualProtect(Dst, NumBytes, OldPageProtect, @OldPageProtect);
+ end;
+ except
+ result := false;
+ end;
+ end; // .if
+end; // .function WriteAtCode
+
+(* Writes patch to any write-protected section *)
+function WritePatchAtCode (PatchMaker: TPatchMaker; {n} Dst: pointer): boolean;
+var
+ Buf: Utils.TArrayOfByte;
+
+begin
+ {!} Assert(PatchMaker <> nil);
+ {!} Assert((Dst <> nil) or (PatchMaker.Size = 0));
+ // * * * * * //
+ result := true;
+
+ if PatchMaker.Size > 0 then begin
+ SetLength(Buf, PatchMaker.Size);
+ PatchMaker.ApplyPatch(pointer(Buf), Dst);
+ result := WriteAtCode(Length(Buf), pointer(Buf), Dst);
+ end;
+end; // .function WritePatchAtCode
+
+function SpliceWinApi (OrigFunc, HandlerFunc: pointer): pointer;
+const
+ CODE_ADDR_ALIGNMENT = 8;
+
+var
+{O} p: PatchForge.TPatchHelper;
+{OI} SpliceBridge: pbyte; // Memory is never freed
+ OrigFuncBridgeLabel: string;
+ OrigCodeBridgeStartPos: integer;
+ OverwrittenCodeSize: integer;
+
+begin
+ {!} Assert(OrigFunc <> nil);
+ {!} Assert(HandlerFunc <> nil);
+ p := TPatchHelper.Wrap(TPatchMaker.Create);
+ SpliceBridge := nil;
+ result := nil;
+ // * * * * * //
+
+ // === BEGIN generating SpliceBridge ===
+ // Add pointer to original function bridge as the first argument
+ p.WriteTribyte(PatchForge.INSTR_PUSH_PTR_ESP);
+ p.WriteInt(PatchForge.INSTR_MOV_ESP_PLUS_4_CONST32);
+ p.ExecActionOnApply(PatchForge.TAddLabelRealAddrAction.Create(p.NewAutoLabel(OrigFuncBridgeLabel)));
+ p.WriteInt(0);
+
+ // Jump to new handler
+ p.Jump(PatchForge.JMP, HandlerFunc);
+
+ // Ensure original code bridge is aligned
+ p.Nop(p.Pos mod CODE_ADDR_ALIGNMENT);
+
+ // Set result to offset from splice bridge start to original function bridge
+ result := pointer(p.Pos);
+
+ // Write original function bridge
+ p.PutLabel(OrigFuncBridgeLabel);
+ OrigCodeBridgeStartPos := p.Pos;
+ p.WriteCode(OrigFunc, PatchForge.TMinCodeSizeDetector.Create(sizeof(PatchForge.TJumpCall32Rec)));
+ OverwrittenCodeSize := p.Pos - OrigCodeBridgeStartPos;
+ p.Jump(PatchForge.JMP, Utils.PtrOfs(OrigFunc, OverwrittenCodeSize));
+ // === END generating SpliceBridge ===
+
+ // Persist splice bridge
+ GetMem(SpliceBridge, p.Size);
+ WritePatchAtCode(p.PatchMaker, SpliceBridge);
+
+ // Turn result from offset to absolute address
+ result := Ptr(integer(SpliceBridge) + integer(result));
+
+ // Create and apply hook at target function start
+ p.Clear();
+ p.Jump(PatchForge.JMP, SpliceBridge);
+ WritePatchAtCode(p.PatchMaker, OrigFunc);
+ // * * * * * //
+ p.Release;
+end;
+
+end.
\ No newline at end of file
diff --git a/VfsTest.dpr b/VfsTest.dpr
index 702b9eb..f5feb2c 100644
--- a/VfsTest.dpr
+++ b/VfsTest.dpr
@@ -4,10 +4,13 @@ uses
TestFramework, GuiTestRunner,
VfsUtils, VfsBase, VfsDebug,
VfsApiDigger, VfsExport, VfsOpenFiles,
+ VfsHooks, VfsControl,
+ VfsTestHelper,
VfsDebugTest, VfsUtilsTest, VfsBaseTest,
- VfsApiDiggerTest, VfsOpenFilesTest;
+ VfsApiDiggerTest, VfsOpenFilesTest, VfsIntegratedTest;
begin
+ VfsTestHelper.InitConsole;
TGUITestRunner.RunRegisteredTests;
end.
diff --git a/_TODO_.txt b/_TODO_.txt
new file mode 100644
index 0000000..ebb4d42
--- /dev/null
+++ b/_TODO_.txt
@@ -0,0 +1,15 @@
+UTF-8 Logging
+
+SetCurrentDirectoryW(GetCurrentDirectoryW)
+System.IsMultiThread for DLL and exported API
+
+(* Trying to turn off DEP *)
+SetProcessDEPPolicyAddr := Windows.GetProcAddress(Kernel32Handle, 'SetProcessDEPPolicy');
+
+if SetProcessDEPPolicyAddr <> nil then begin
+ if PatchApi.Call(PatchApi.STDCALL_, SetProcessDEPPolicyAddr, [0]) <> 0 then begin
+ Log.Write('VFS', 'SetProcessDEPPolicy', 'DEP was turned off');
+ end else begin
+ Log.Write('VFS', 'SetProcessDEPPolicy', 'Failed to turn DEP off');
+ end;
+end;
\ No newline at end of file