Virtual-File-System/VfsBase.pas

490 lines
14 KiB
ObjectPascal

unit VfsBase;
(*
Description: Implements in-memory virtual file system data storage.
Author: Alexander Shostak (aka Berserker aka EtherniDee aka BerSoft)
TODO: Use optimized hash-table storage for VfsItems instead of ansi-to-wide string keys in regular binary tree.
*)
(***) interface (***)
uses
SysUtils, Math, Windows,
Utils, WinNative, Alg, Concur, TypeWrappers, Lists, DataLib,
StrLib,
VfsUtils;
type
(* Import *)
TDict = DataLib.TDict;
TObjDict = DataLib.TObjDict;
TString = TypeWrappers.TString;
TList = Lists.TList;
const
OVERWRITE_EXISTING = true;
DONT_OVERWRITE_EXISTING = false;
AUTO_PRIORITY = MAXLONGINT div 2;
INITIAL_OVERWRITING_PRIORITY = AUTO_PRIORITY + 1;
INITIAL_ADDING_PRIORITY = AUTO_PRIORITY - 1;
type
(*
Specifies the order, in which files from different mapped directories will be listed in virtual directory.
Virtual directory sorting is performed by priorities firstly and lexicographically secondly.
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);
(* Single redirected VFS entry: file or directory *)
TVfsItem = class
private
function GetName: WideString; inline;
procedure SetName (const NewName: WideString); inline;
public
(* Name in lower case, used for wildcard mask matching *)
SearchName: WideString;
(* Absolute path to real file/folder location without trailing slash for non-drives *)
RealPath: WideString;
(* The priority used in virtual directories sorting for listing *)
Priority: integer;
(* List of directory child items or nil *)
{On} Children: {U} TList {OF TVfsItem};
(* Up to 32 special non-Windows attribute flags *)
Attrs: integer;
(* Full file info *)
Info: TNativeFileInfo;
function IsDir (): boolean;
destructor Destroy; override;
(* Name in original case. Automatically sets/converts SearchName, Info.FileName, Info.Base.FileNameLength *)
property Name: WideString read GetName write SetName;
end; // .class TVfsItem
(* Allows to disable VFS temporarily for current thread only *)
TThreadVfsDisabler = record
PrevDisableVfsForThisThread: boolean;
procedure DisableVfsForThread;
procedure RestoreVfsForThread;
end;
var
(* Global VFS access synchronizer *)
VfsCritSection: Concur.TCritSection;
function GetThreadVfsDisabler: TThreadVfsDisabler;
(* Runs VFS. Higher level API must install hooks in VfsCritSection protected area *)
function RunVfs (DirListingOrder: TDirListingSortType): boolean;
(* Stops VFS and clears all mappings *)
function ResetVfs: boolean;
(* Returns real path for VFS item by its absolute virtual path or empty string. Optionally returns file info structure *)
function GetVfsItemRealPath (const AbsVirtPath: WideString; {n} FileInfo: PNativeFileInfo = nil): WideString;
(* Returns virtual directory info. Adds virtual entries to specified directory listing container *)
function GetVfsDirInfo (const AbsVirtPath, Mask: WideString; {OUT} var DirInfo: TNativeFileInfo; DirListing: TDirListing): boolean;
(* Maps real directory contents to virtual path. Target must exist for success *)
function MapDir (const VirtPath, RealPath: WideString; OverwriteExisting: boolean; Flags: integer = 0): boolean;
(***) implementation (***)
var
(*
Global map of case-insensitive normalized path to file/directory => corresponding TVfsItem.
Access is controlled via critical section and global/thread switchers.
Represents the whole cached virtual file system contents.
*)
{O} VfsItems: {O} TDict {OF TVfsItem};
(* Global VFS state indicator. If false, all VFS search operations must fail *)
VfsIsRunning: boolean = false;
(* Automatical VFS items priority management *)
OverwritingPriority: integer = INITIAL_OVERWRITING_PRIORITY;
AddingPriority: integer = INITIAL_ADDING_PRIORITY;
// All threadvar variables are automatically zeroed during finalization, thus zero must be the safest default value
threadvar
DisableVfsForThisThread: boolean;
function TVfsItem.IsDir: boolean;
begin
result := (Self.Info.Base.FileAttributes and Windows.FILE_ATTRIBUTE_DIRECTORY) <> 0;
end;
function TVfsItem.GetName: WideString;
begin
result := Self.Info.FileName;
end;
procedure TVfsItem.SetName (const NewName: WideString);
begin
Self.Info.SetFileName(NewName);
Self.SearchName := StrLib.WideLowerCase(NewName);
end;
destructor TVfsItem.Destroy;
begin
SysUtils.FreeAndNil(Self.Children);
end;
procedure TThreadVfsDisabler.DisableVfsForThread;
begin
Self.PrevDisableVfsForThisThread := DisableVfsForThisThread;
DisableVfsForThisThread := true;
end;
procedure TThreadVfsDisabler.RestoreVfsForThread;
begin
DisableVfsForThisThread := Self.PrevDisableVfsForThisThread;
end;
function GetThreadVfsDisabler: TThreadVfsDisabler;
begin
end;
function EnterVfs: boolean;
begin
result := not DisableVfsForThisThread;
if result then begin
VfsCritSection.Enter;
result := VfsIsRunning;
if not result then begin
VfsCritSection.Leave;
end;
end;
end;
procedure LeaveVfs;
begin
VfsCritSection.Leave;
end;
function CompareVfsItemsByPriorityDescAndNameAsc (Item1, Item2: integer): integer;
begin
result := TVfsItem(Item2).Priority - TVfsItem(Item1).Priority;
if result = 0 then begin
result := StrLib.CompareBinStringsW(TVfsItem(Item1).SearchName, TVfsItem(Item2).SearchName);
end;
end;
function CompareVfsItemsByPriorityAscAndNameAsc (Item1, Item2: integer): integer;
begin
result := TVfsItem(Item1).Priority - TVfsItem(Item2).Priority;
if result = 0 then begin
result := StrLib.CompareBinStringsW(TVfsItem(Item1).SearchName, TVfsItem(Item2).SearchName);
end;
end;
procedure SortVfsListing ({U} List: DataLib.TList {OF TVfsItem}; SortType: TDirListingSortType);
begin
if SortType = SORT_FIFO then begin
List.CustomSort(CompareVfsItemsByPriorityDescAndNameAsc);
end else begin
List.CustomSort(CompareVfsItemsByPriorityAscAndNameAsc);
end;
end;
procedure SortVfsDirListings (SortType: TDirListingSortType);
var
{Un} Children: DataLib.TList {OF TVfsItem};
begin
Children := nil;
// * * * * * //
with DataLib.IterateDict(VfsItems) do begin
while IterNext() do begin
Children := TVfsItem(IterValue).Children;
if (Children <> nil) and (Children.Count > 1) then begin
SortVfsListing(Children, SortType);
end;
end;
end;
end; // .procedure SortVfsDirListings
function FindVfsItemByNormalizedPath (const Path: WideString; {U} var {OUT} Res: TVfsItem): boolean;
var
{Un} VfsItem: TVfsItem;
begin
VfsItem := VfsItems[WideStrToCaselessKey(Path)];
result := VfsItem <> nil;
if result then begin
Res := VfsItem;
end;
end;
function FindVfsItemByPath (const Path: WideString; {U} var {OUT} Res: TVfsItem): boolean;
begin
result := FindVfsItemByNormalizedPath(NormalizePath(Path), Res);
end;
(* All children list of VFS items MUST be empty *)
procedure BuildVfsItemsTree;
var
{Un} DirVfsItem: TVfsItem;
AbsDirPath: WideString;
begin
DirVfsItem := nil;
// * * * * * //
with DataLib.IterateDict(VfsItems) do begin
while IterNext() do begin
AbsDirPath := StrLib.ExtractDirPathW(CaselessKeyToWideStr(IterKey));
if FindVfsItemByNormalizedPath(AbsDirPath, DirVfsItem) then begin
DirVfsItem.Children.Add(IterValue);
end;
end;
end;
end; // .procedure BuildVfsItemsTree
function RunVfs (DirListingOrder: TDirListingSortType): boolean;
begin
result := not DisableVfsForThisThread;
if result then begin
with VfsCritSection do begin
Enter;
if not VfsIsRunning then begin
BuildVfsItemsTree();
SortVfsDirListings(DirListingOrder);
VfsIsRunning := true;
end;
Leave;
end;
end;
end; // .function RunVfs
function ResetVfs: boolean;
begin
result := not DisableVfsForThisThread;
if result then begin
with VfsCritSection do begin
Enter;
VfsIsRunning := false;
VfsItems.Clear();
Leave;
end;
end;
end;
(* Returns real path for vfs item by its absolute virtual path or empty string. Optionally returns file info structure *)
function GetVfsItemRealPath (const AbsVirtPath: WideString; {n} FileInfo: PNativeFileInfo = nil): WideString;
var
{n} VfsItem: TVfsItem;
begin
VfsItem := nil;
result := '';
// * * * * * //
if EnterVfs then begin
if FindVfsItemByNormalizedPath(AbsVirtPath, VfsItem) then begin
result := VfsItem.RealPath;
if FileInfo <> nil then begin
FileInfo^ := VfsItem.Info;
end;
end;
LeaveVfs;
end; // .if
end; // .function GetVfsItemRealPath
function GetVfsDirInfo (const AbsVirtPath, Mask: WideString; {OUT} var DirInfo: TNativeFileInfo; DirListing: TDirListing): boolean;
var
{n} VfsItem: TVfsItem;
NormalizedMask: WideString;
i: integer;
begin
{!} Assert(DirListing <> nil);
VfsItem := nil;
// * * * * * //
result := EnterVfs;
if result then begin
result := FindVfsItemByNormalizedPath(AbsVirtPath, VfsItem) and VfsItem.IsDir;
if result then begin
DirInfo := VfsItem.Info;
if VfsItem.Children <> nil then begin
NormalizedMask := StrLib.WideLowerCase(Mask);
for i := 0 to VfsItem.Children.Count - 1 do begin
if StrLib.MatchW(TVfsItem(VfsItem.Children[i]).SearchName, NormalizedMask) then begin
DirListing.AddItem(@TVfsItem(VfsItem.Children[i]).Info);
end;
end;
end; // .if
end; // .if
LeaveVfs;
end; // .if
end; // .function GetVfsDirInfo
procedure CopyFileInfoWithoutNames (var Src, Dest: WinNative.FILE_ID_BOTH_DIR_INFORMATION);
begin
Dest.FileIndex := 0;
Dest.CreationTime := Src.CreationTime;
Dest.LastAccessTime := Src.LastAccessTime;
Dest.LastWriteTime := Src.LastWriteTime;
Dest.ChangeTime := Src.ChangeTime;
Dest.EndOfFile := Src.EndOfFile;
Dest.AllocationSize := Src.AllocationSize;
Dest.FileAttributes := Src.FileAttributes;
Dest.EaSize := Src.EaSize;
end;
(* Redirects single file/directory path (not including directory contents). Target must exist for success *)
function RedirectFile (const AbsVirtPath, AbsRealPath: WideString; {n} FileInfoPtr: WinNative.PFILE_ID_BOTH_DIR_INFORMATION; OverwriteExisting: boolean; Priority: integer): {Un} TVfsItem;
const
WIDE_NULL_CHAR_LEN = Length(#0);
var
{Un} VfsItem: TVfsItem;
PackedVirtPath: string;
IsNewItem: boolean;
FileInfo: TNativeFileInfo;
Success: boolean;
begin
VfsItem := nil;
result := nil;
// * * * * * //
PackedVirtPath := WideStrToCaselessKey(AbsVirtPath);
VfsItem := VfsItems[PackedVirtPath];
IsNewItem := VfsItem = nil;
Success := true;
if IsNewItem or OverwriteExisting then begin
if FileInfoPtr = nil then begin
Success := GetFileInfo(AbsRealPath, FileInfo);
end;
if Success then begin
if IsNewItem then begin
VfsItem := TVfsItem.Create();
VfsItems[PackedVirtPath] := VfsItem;
VfsItem.Name := StrLib.ExtractFileNameW(AbsVirtPath);
VfsItem.SearchName := StrLib.WideLowerCase(VfsItem.Name);
VfsItem.Info.Base.ShortNameLength := 0;
VfsItem.Info.Base.ShortName[0] := #0;
end;
if FileInfoPtr <> nil then begin
CopyFileInfoWithoutNames(FileInfoPtr^, VfsItem.Info.Base);
end else begin
CopyFileInfoWithoutNames(FileInfo.Base, VfsItem.Info.Base);
end;
VfsItem.RealPath := AbsRealPath;
VfsItem.Priority := Priority;
VfsItem.Attrs := 0;
end; // .if
end; // .if
if Success then begin
result := VfsItem;
end;
end; // .function RedirectFile
function _MapDir (const AbsVirtPath, AbsRealPath: WideString; {n} FileInfoPtr: WinNative.PFILE_ID_BOTH_DIR_INFORMATION; OverwriteExisting: boolean; Priority: integer): {Un} TVfsItem;
var
{O} Subdirs: {O} TList {OF TFileInfo};
{U} SubdirInfo: TFileInfo;
{Un} DirVfsItem: TVfsItem;
Success: boolean;
FileInfo: TNativeFileInfo;
VirtPathPrefix: WideString;
RealPathPrefix: WideString;
i: integer;
begin
DirVfsItem := nil;
Subdirs := DataLib.NewList(Utils.OWNS_ITEMS);
SubdirInfo := nil;
result := nil;
// * * * * * //
if Priority = AUTO_PRIORITY then begin
if OverwriteExisting then begin
Priority := OverwritingPriority;
Inc(OverwritingPriority);
end else begin
Priority := AddingPriority;
Dec(AddingPriority);
end;
end;
DirVfsItem := RedirectFile(AbsVirtPath, AbsRealPath, FileInfoPtr, OverwriteExisting, Priority);
Success := DirVfsItem <> nil;
if Success then begin
VirtPathPrefix := AbsVirtPath + '\';
RealPathPrefix := AbsRealPath + '\';
if DirVfsItem.Children = nil then begin
DirVfsItem.Children := DataLib.NewList(not Utils.OWNS_ITEMS);
end;
with SysScanDir(AbsRealPath, '*') do begin
while IterNext(FileInfo.FileName, @FileInfo.Base) do begin
if Utils.HasFlag(FileInfo.Base.FileAttributes, Windows.FILE_ATTRIBUTE_DIRECTORY) then begin
if (FileInfo.FileName <> '.') and (FileInfo.FileName <> '..') then begin
Subdirs.Add(TFileInfo.Create(@FileInfo));
end;
end else begin
RedirectFile(VirtPathPrefix + FileInfo.FileName, RealPathPrefix + FileInfo.FileName, @FileInfo, OverwriteExisting, Priority);
end;
end;
end;
for i := 0 to Subdirs.Count - 1 do begin
SubdirInfo := TFileInfo(Subdirs[i]);
_MapDir(VirtPathPrefix + SubdirInfo.Data.FileName, RealPathPrefix + SubdirInfo.Data.FileName, @SubdirInfo.Data, OverwriteExisting, Priority);
end;
end; // .if
if Success then begin
result := DirVfsItem;
end;
// * * * * * //
SysUtils.FreeAndNil(Subdirs);
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;
end;
begin
VfsCritSection.Init;
VfsItems := DataLib.NewDict(Utils.OWNS_ITEMS, DataLib.CASE_SENSITIVE);
end.