From 2e45187b77d30366aa48264c7e7efb9d090cb5cb Mon Sep 17 00:00:00 2001 From: Mitch Capper Date: Sun, 14 Sep 2025 02:47:18 -0700 Subject: [PATCH 1/6] Initial implementation hooked into Vfs for now --- Library/DiscUtils.Btrfs/VfsBtrfsFileSystem.cs | 6 ++ Library/DiscUtils.Core/DiscFileSystem.cs | 5 ++ Library/DiscUtils.Core/IFileSystem.cs | 3 + Library/DiscUtils.Core/Vfs/IAbstractRecord.cs | 50 +++++++++++++++ Library/DiscUtils.Core/Vfs/VfsFileSystem.cs | 62 ++++++++++++++++++- Library/DiscUtils.Ext/ExtFileSystem.cs | 2 + Library/DiscUtils.Ext/VfsExtFileSystem.cs | 7 +++ Library/DiscUtils.Iso9660/VfsCDReader.cs | 6 ++ .../VfsSquashFileSystemReader.cs | 8 +++ 9 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 Library/DiscUtils.Core/Vfs/IAbstractRecord.cs diff --git a/Library/DiscUtils.Btrfs/VfsBtrfsFileSystem.cs b/Library/DiscUtils.Btrfs/VfsBtrfsFileSystem.cs index 89b5656c1..b83a97ad2 100644 --- a/Library/DiscUtils.Btrfs/VfsBtrfsFileSystem.cs +++ b/Library/DiscUtils.Btrfs/VfsBtrfsFileSystem.cs @@ -131,7 +131,13 @@ public IEnumerable GetSubvolumes() yield return new Subvolume { Id = volume.Key.Offset, Name = volume.Name }; } } + protected override Directory ConvertDirEntryToDirectory(DirEntry dirEntry) { + if (dirEntry.IsDirectory) + throw new Exception("Invalid Directory Request record is not a directory"); + + return dirEntry.CachedDirectory ??= new Directory(dirEntry, Context); + } protected override File ConvertDirEntryToFile(DirEntry dirEntry) { if (dirEntry.IsDirectory) diff --git a/Library/DiscUtils.Core/DiscFileSystem.cs b/Library/DiscUtils.Core/DiscFileSystem.cs index eeaacd26f..cfd0bfd09 100644 --- a/Library/DiscUtils.Core/DiscFileSystem.cs +++ b/Library/DiscUtils.Core/DiscFileSystem.cs @@ -25,6 +25,7 @@ using System.IO; using System.Linq; using DiscUtils.Streams; +using DiscUtils.Vfs; namespace DiscUtils; @@ -520,6 +521,10 @@ protected virtual void Dispose(bool disposing) } } + public virtual IAbstractRecord GetAbstractRecord(string path) => throw new NotImplementedException(); + public virtual string GetSymlinkTarget(IAbstractRecord dirEntry) => throw new NotImplementedException(); + + public event EventHandler Disposed; #endregion diff --git a/Library/DiscUtils.Core/IFileSystem.cs b/Library/DiscUtils.Core/IFileSystem.cs index 887c5e19e..704ca356b 100644 --- a/Library/DiscUtils.Core/IFileSystem.cs +++ b/Library/DiscUtils.Core/IFileSystem.cs @@ -370,4 +370,7 @@ public interface IFileSystem /// UsedSpace and Size properties /// bool SupportsUsedAvailableSpace { get; } + + Vfs.IAbstractRecord GetAbstractRecord(string path); + string GetSymlinkTarget(Vfs.IAbstractRecord dirEntry); } diff --git a/Library/DiscUtils.Core/Vfs/IAbstractRecord.cs b/Library/DiscUtils.Core/Vfs/IAbstractRecord.cs new file mode 100644 index 000000000..ae1524084 --- /dev/null +++ b/Library/DiscUtils.Core/Vfs/IAbstractRecord.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace DiscUtils.Vfs; + +internal class FullFile(VfsDirEntry DirEntry, IVfsFile File) : IAbstractRecord { + public DateTime CreationTimeUtc => File.CreationTimeUtc; + public FileAttributes FileAttributes => File.FileAttributes; + public string FileName => DirEntry.FileName; + public bool IsDirectory => DirEntry.IsDirectory; + public bool IsSymlink => DirEntry.IsSymlink; + public DateTime LastAccessTimeUtc => File.LastAccessTimeUtc; + public DateTime LastWriteTimeUtc => File.LastWriteTimeUtc; + public long FileId => DirEntry.UniqueCacheId; + public long FileSize => File.FileLength; + public Streams.IBuffer FileContent => File.FileContent; + + public IAbstractDirectory GetAsAbstractDirectory() { + if (! IsDirectory) + throw new InvalidOperationException("Not a directory"); + if (this is IAbstractDirectory dir) + return dir; + throw new InvalidOperationException("We are not an instance of a directory but should be"); + } + public VfsDirEntry GetAsDirEntry() => DirEntry; + public IVfsFile GetAsFile() => File; + + +} +public interface IAbstractRecord { + DateTime CreationTimeUtc { get; } + FileAttributes FileAttributes { get; } + string FileName { get; } + bool IsDirectory { get; } + bool IsSymlink { get; } + DateTime LastAccessTimeUtc { get; } + DateTime LastWriteTimeUtc { get; } + long FileId { get; } + long FileSize { get; } + Streams.IBuffer FileContent { get; } + VfsDirEntry GetAsDirEntry(); + IVfsFile GetAsFile(); + IAbstractDirectory GetAsAbstractDirectory(); +} + +public interface IAbstractDirectory : IAbstractRecord { + IEnumerable AllEntries { get; } +} diff --git a/Library/DiscUtils.Core/Vfs/VfsFileSystem.cs b/Library/DiscUtils.Core/Vfs/VfsFileSystem.cs index 131c147c4..e94d49b30 100644 --- a/Library/DiscUtils.Core/Vfs/VfsFileSystem.cs +++ b/Library/DiscUtils.Core/Vfs/VfsFileSystem.cs @@ -655,6 +655,50 @@ public override long GetFileLength(string path) return file.FileLength; } + virtual protected IAbstractRecord GetAbstractRecord(TDirEntry dirEntry){ + if (dirEntry.IsDirectory) + return new FullDirectory(this, dirEntry, ConvertDirEntryToDirectory(dirEntry)); + else + return new FullFile(dirEntry, GetFile(dirEntry)); + } + internal class FullDirectory(VfsFileSystem fs, VfsDirEntry DirEntry, TDirectory Directory) : FullFile(DirEntry, Directory), IAbstractDirectory { + public IEnumerable AllEntries => Directory.AllEntries.Values.Select(fs.GetAbstractRecord); + + } + private class FakeRootDirEntry(TDirectory rootDir, String rootPath) : VfsDirEntry { + public override DateTime CreationTimeUtc => rootDir.CreationTimeUtc; + public override FileAttributes FileAttributes => rootDir.FileAttributes; + public override string FileName => rootPath; + public override bool HasVfsFileAttributes => true; + public override bool HasVfsTimeInfo => true; + public override bool IsDirectory => true; + public override bool IsSymlink => false; + public override DateTime LastAccessTimeUtc => rootDir.LastAccessTimeUtc; + public override DateTime LastWriteTimeUtc => rootDir.LastWriteTimeUtc; + public override long UniqueCacheId => -1; + } + + //override string GetSymlinkTarget( + public override IAbstractRecord GetAbstractRecord(string path) { + if (IsRoot(path)) + { + if (RootDirectory.Self == null){ + return new FullDirectory(this, new FakeRootDirEntry(RootDirectory, path), RootDirectory); + } + return GetAbstractRecord(RootDirectory.Self); + } + + if (path == null) + { + return default; + } + + var dirEntry = GetDirectoryEntry(path) + ?? throw new FileNotFoundException("No such file or directory", path); + return GetAbstractRecord(dirEntry); + } + + protected TFile GetFile(TDirEntry dirEntry) { var cacheKey = dirEntry.UniqueCacheId; @@ -756,6 +800,9 @@ protected TFile GetFile(string path) return GetFile(dirEntry); } + + protected virtual TDirectory ConvertDirEntryToDirectory(TDirEntry dirEntry) => throw new NotImplementedException(); + /// /// Converts a directory entry to an object representing a file. /// @@ -928,7 +975,20 @@ private IEnumerable DoSearch(string path, Func filter, boo } } } - + public override string GetSymlinkTarget(IAbstractRecord dirEntry) { + if (! dirEntry.IsSymlink) + throw new ArgumentException(dirEntry.FileName + " is not a symlink"); + var file = dirEntry.GetAsFile(); + if (file is not IVfsSymlink) { + var dirEnt = dirEntry.GetAsDirEntry() as TDirEntry; + if (dirEnt == null) + throw new ArgumentException("Not a valid record for this filesystem"); + file = GetFile(dirEnt); + } + if (file is not IVfsSymlink symlink) + throw new AccessViolationException($"Unknown Error: the directory entry says it is a symlink but unable to get as symlink"); + return symlink.TargetPath; + } protected virtual (TDirEntry TargetEntry, string TargetPath) ResolveSymlink(TDirEntry entry, string path) { var currentEntry = entry; diff --git a/Library/DiscUtils.Ext/ExtFileSystem.cs b/Library/DiscUtils.Ext/ExtFileSystem.cs index cc956498c..ee7096a46 100644 --- a/Library/DiscUtils.Ext/ExtFileSystem.cs +++ b/Library/DiscUtils.Ext/ExtFileSystem.cs @@ -108,4 +108,6 @@ internal static bool Detect(Stream stream) return superblock.Magic == SuperBlock.Ext2Magic; } + public override IAbstractRecord GetAbstractRecord(string path) => GetRealFileSystem().GetAbstractRecord(path); + public override string GetSymlinkTarget(IAbstractRecord dirEntry) => GetRealFileSystem().GetSymlinkTarget(dirEntry); } diff --git a/Library/DiscUtils.Ext/VfsExtFileSystem.cs b/Library/DiscUtils.Ext/VfsExtFileSystem.cs index c0746ba15..c69f531cf 100644 --- a/Library/DiscUtils.Ext/VfsExtFileSystem.cs +++ b/Library/DiscUtils.Ext/VfsExtFileSystem.cs @@ -154,7 +154,14 @@ public IEnumerable> PathToClusters(string path) var file = GetFile(path); return file.EnumerateAllocationClusters(); } + protected override Directory ConvertDirEntryToDirectory(DirEntry dirEntry) { + var inode = GetInode(dirEntry.Record.Inode); + if (dirEntry.Record.FileType != DirectoryRecord.FileTypeDirectory) + throw new Exception("Invalid Directory Request record is not a directory"); + + return new Directory(Context, dirEntry.Record.Inode, inode); + } protected override File ConvertDirEntryToFile(DirEntry dirEntry) { var inode = GetInode(dirEntry.Record.Inode); diff --git a/Library/DiscUtils.Iso9660/VfsCDReader.cs b/Library/DiscUtils.Iso9660/VfsCDReader.cs index 4713a0512..dfe880ab9 100644 --- a/Library/DiscUtils.Iso9660/VfsCDReader.cs +++ b/Library/DiscUtils.Iso9660/VfsCDReader.cs @@ -539,7 +539,13 @@ public Stream OpenBootImage() throw new InvalidOperationException("No valid boot image"); } + protected override ReaderDirectory ConvertDirEntryToDirectory(ReaderDirEntry dirEntry) { + if (dirEntry.IsDirectory) + throw new Exception("Invalid Directory Request record is not a directory"); + + return new ReaderDirectory(Context, dirEntry); + } protected override File ConvertDirEntryToFile(ReaderDirEntry dirEntry) { if (dirEntry.IsDirectory) diff --git a/Library/DiscUtils.SquashFs/VfsSquashFileSystemReader.cs b/Library/DiscUtils.SquashFs/VfsSquashFileSystemReader.cs index bd58ceec3..6febadddd 100644 --- a/Library/DiscUtils.SquashFs/VfsSquashFileSystemReader.cs +++ b/Library/DiscUtils.SquashFs/VfsSquashFileSystemReader.cs @@ -179,7 +179,15 @@ internal static UnixFileType FileTypeFromInodeType(InodeType inodeType) _ => throw new NotSupportedException($"Unrecognized inode type: {inodeType}"), }; } + protected override Directory ConvertDirEntryToDirectory(DirectoryEntry dirEntry) { + if (!dirEntry.IsDirectory) + throw new Exception("Invalid Directory Request record is not a directory"); + var inodeRef = dirEntry.InodeReference; + _context.InodeReader.SetPosition(inodeRef); + var inode = Inode.Read(_context.InodeReader); + return new Directory(_context, inode, inodeRef); + } protected override File ConvertDirEntryToFile(DirectoryEntry dirEntry) { var inodeRef = dirEntry.InodeReference; From 2416b8c42c190d19ad86faf5ca7f43e3cb1e652a Mon Sep 17 00:00:00 2001 From: Mitch Capper Date: Mon, 15 Sep 2025 16:48:34 -0700 Subject: [PATCH 2/6] WinFS and NTFS abstract examples --- Library/DiscUtils.Core/NativeFileSystem.cs | 4 ++ Library/DiscUtils.Core/ReparsePoint.cs | 46 ++++++++++++++++- Library/DiscUtils.Core/Vfs/IAbstractRecord.cs | 26 +--------- .../DiscUtils.Core/Vfs/VfsAbstractRecord.cs | 33 ++++++++++++ Library/DiscUtils.Core/Vfs/VfsFileSystem.cs | 4 +- Library/DiscUtils.Ntfs/Directory.cs | 14 +++++- Library/DiscUtils.Ntfs/NtfsAbstractRecord.cs | 50 +++++++++++++++++++ Library/DiscUtils.Ntfs/NtfsFileSystem.cs | 18 +++++++ .../VirtualFileSystem.cs | 3 ++ Library/DiscUtils.Wim/WimFileSystem.cs | 44 +++++++++++++++- 10 files changed, 210 insertions(+), 32 deletions(-) create mode 100644 Library/DiscUtils.Core/Vfs/VfsAbstractRecord.cs create mode 100644 Library/DiscUtils.Ntfs/NtfsAbstractRecord.cs diff --git a/Library/DiscUtils.Core/NativeFileSystem.cs b/Library/DiscUtils.Core/NativeFileSystem.cs index 2607de1b5..ae427ce33 100644 --- a/Library/DiscUtils.Core/NativeFileSystem.cs +++ b/Library/DiscUtils.Core/NativeFileSystem.cs @@ -29,6 +29,7 @@ using System.Linq; using DiscUtils.Internal; using DiscUtils.Streams; +using DiscUtils.Vfs; namespace DiscUtils; @@ -782,4 +783,7 @@ private string CleanItems(string dirtyItems) { return dirtyItems.Substring(BasePath.Length - 1); } + + public override IAbstractRecord GetAbstractRecord(string path) => throw new NotImplementedException(); + public override string GetSymlinkTarget(IAbstractRecord dirEntry) => throw new NotImplementedException(); } diff --git a/Library/DiscUtils.Core/ReparsePoint.cs b/Library/DiscUtils.Core/ReparsePoint.cs index 111ad3bdd..a8425db91 100644 --- a/Library/DiscUtils.Core/ReparsePoint.cs +++ b/Library/DiscUtils.Core/ReparsePoint.cs @@ -20,6 +20,10 @@ // DEALINGS IN THE SOFTWARE. // +using System; +using System.IO; +using System.Text; + namespace DiscUtils; /// @@ -47,4 +51,44 @@ public ReparsePoint(int tag, byte[] content) /// Gets or sets the defined reparse point tag. /// public int Tag { get; set; } -} \ No newline at end of file + // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/c8e77b37-3909-4fe6-a4ea-2b9d423b1ee4 + private const int IO_REPARSE_TAG_MOUNT_POINT = unchecked((int)0xA0000003); + private const int IO_REPARSE_TAG_SYMLINK = unchecked((int)0xA000000C); + // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/b41f1cbf-10df-4a47-98d4-1c52a833d913 + private enum SymlinkFlags : int { + FullpathName = 0, + SYMLINK_FLAG_RELATIVE = 1 + } + internal string ParseSymlink(String originalPath) { + + var reparsePoint = this; + + using var stream = new MemoryStream(reparsePoint.Content); + using var reader = new BinaryReader(stream); + if (reparsePoint.Tag != IO_REPARSE_TAG_SYMLINK && reparsePoint.Tag != IO_REPARSE_TAG_MOUNT_POINT) + throw new IOException($"Reparse point on {originalPath} is not a symlink or mount point (tag: 0x{reparsePoint.Tag:X8})"); + + var substNameOffset = reader.ReadUInt16(); + var substNameLength = reader.ReadUInt16(); + var printNameOffset = reader.ReadUInt16(); + var printNameLength = reader.ReadUInt16(); + SymlinkFlags? flags = null; + if (reparsePoint.Tag == IO_REPARSE_TAG_SYMLINK) + flags = (SymlinkFlags)reader.ReadUInt32(); + + + string target; + // Prefer PrintName if available + if (printNameLength > 0) { + stream.Seek(printNameOffset, SeekOrigin.Current); + var pathBytes = reader.ReadBytes(printNameLength); + target = Encoding.Unicode.GetString(pathBytes); + } else { + stream.Seek(substNameOffset, SeekOrigin.Current); + var pathBytes = reader.ReadBytes(substNameLength); + target = Encoding.Unicode.GetString(pathBytes); + // alternatives I have done additional cleaning but for here we may want raw values: https://github.com/mitchcapper/gnulib/blob/b5c3b1b1f1fe6225363cddd72310e1fe95312466/lib/readlink.c#L115-#L182 but the use case here may be a bit different + } + return target; + } +} diff --git a/Library/DiscUtils.Core/Vfs/IAbstractRecord.cs b/Library/DiscUtils.Core/Vfs/IAbstractRecord.cs index ae1524084..a0c9e2205 100644 --- a/Library/DiscUtils.Core/Vfs/IAbstractRecord.cs +++ b/Library/DiscUtils.Core/Vfs/IAbstractRecord.cs @@ -1,34 +1,10 @@ using System; using System.Collections.Generic; using System.IO; -using System.Text; namespace DiscUtils.Vfs; -internal class FullFile(VfsDirEntry DirEntry, IVfsFile File) : IAbstractRecord { - public DateTime CreationTimeUtc => File.CreationTimeUtc; - public FileAttributes FileAttributes => File.FileAttributes; - public string FileName => DirEntry.FileName; - public bool IsDirectory => DirEntry.IsDirectory; - public bool IsSymlink => DirEntry.IsSymlink; - public DateTime LastAccessTimeUtc => File.LastAccessTimeUtc; - public DateTime LastWriteTimeUtc => File.LastWriteTimeUtc; - public long FileId => DirEntry.UniqueCacheId; - public long FileSize => File.FileLength; - public Streams.IBuffer FileContent => File.FileContent; - public IAbstractDirectory GetAsAbstractDirectory() { - if (! IsDirectory) - throw new InvalidOperationException("Not a directory"); - if (this is IAbstractDirectory dir) - return dir; - throw new InvalidOperationException("We are not an instance of a directory but should be"); - } - public VfsDirEntry GetAsDirEntry() => DirEntry; - public IVfsFile GetAsFile() => File; - - -} public interface IAbstractRecord { DateTime CreationTimeUtc { get; } FileAttributes FileAttributes { get; } @@ -39,7 +15,7 @@ public interface IAbstractRecord { DateTime LastWriteTimeUtc { get; } long FileId { get; } long FileSize { get; } - Streams.IBuffer FileContent { get; } + Streams.SparseStream FileContent { get; } VfsDirEntry GetAsDirEntry(); IVfsFile GetAsFile(); IAbstractDirectory GetAsAbstractDirectory(); diff --git a/Library/DiscUtils.Core/Vfs/VfsAbstractRecord.cs b/Library/DiscUtils.Core/Vfs/VfsAbstractRecord.cs new file mode 100644 index 000000000..8255c4071 --- /dev/null +++ b/Library/DiscUtils.Core/Vfs/VfsAbstractRecord.cs @@ -0,0 +1,33 @@ +using DiscUtils.Streams; +using DiscUtils.Vfs; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace DiscUtils.Vfs; + +public class VfsAbstractRecord(VfsDirEntry DirEntry, IVfsFile File) : IAbstractRecord { + public DateTime CreationTimeUtc => File.CreationTimeUtc; + public FileAttributes FileAttributes => File.FileAttributes; + public string FileName => DirEntry.FileName; + public bool IsDirectory => DirEntry.IsDirectory; + public bool IsSymlink => DirEntry.IsSymlink; + public DateTime LastAccessTimeUtc => File.LastAccessTimeUtc; + public DateTime LastWriteTimeUtc => File.LastWriteTimeUtc; + public long FileId => DirEntry.UniqueCacheId; + public long FileSize => File.FileLength; + public SparseStream FileContent => new BufferStream(File.FileContent, FileAccess.Read); + + public IAbstractDirectory GetAsAbstractDirectory() { + if (! IsDirectory) + throw new InvalidOperationException("Not a directory"); + if (this is IAbstractDirectory dir) + return dir; + throw new InvalidOperationException("We are not an instance of a directory but should be"); + } + public VfsDirEntry GetAsDirEntry() => DirEntry; + public IVfsFile GetAsFile() => File; +} diff --git a/Library/DiscUtils.Core/Vfs/VfsFileSystem.cs b/Library/DiscUtils.Core/Vfs/VfsFileSystem.cs index e94d49b30..334bde8b8 100644 --- a/Library/DiscUtils.Core/Vfs/VfsFileSystem.cs +++ b/Library/DiscUtils.Core/Vfs/VfsFileSystem.cs @@ -659,9 +659,9 @@ virtual protected IAbstractRecord GetAbstractRecord(TDirEntry dirEntry){ if (dirEntry.IsDirectory) return new FullDirectory(this, dirEntry, ConvertDirEntryToDirectory(dirEntry)); else - return new FullFile(dirEntry, GetFile(dirEntry)); + return new VfsAbstractRecord(dirEntry, GetFile(dirEntry)); } - internal class FullDirectory(VfsFileSystem fs, VfsDirEntry DirEntry, TDirectory Directory) : FullFile(DirEntry, Directory), IAbstractDirectory { + internal class FullDirectory(VfsFileSystem fs, VfsDirEntry DirEntry, TDirectory Directory) : VfsAbstractRecord(DirEntry, Directory), IAbstractDirectory { public IEnumerable AllEntries => Directory.AllEntries.Values.Select(fs.GetAbstractRecord); } diff --git a/Library/DiscUtils.Ntfs/Directory.cs b/Library/DiscUtils.Ntfs/Directory.cs index ecb996d6f..7022bcba8 100644 --- a/Library/DiscUtils.Ntfs/Directory.cs +++ b/Library/DiscUtils.Ntfs/Directory.cs @@ -26,7 +26,7 @@ using System.Linq; using System.Text; using DiscUtils.Internal; - +using DiscUtils.Vfs; using DirectoryIndexEntry = System.Collections.Generic.KeyValuePair; @@ -254,4 +254,14 @@ public int CompareTo(byte[] buffer) return _upperCase.Compare(_query, 0, _query.Length, buffer, 0x42, fnLen * 2); } } -} \ No newline at end of file + internal class NtfsAbstractDirectory : NtfsAbstractRecord, IAbstractDirectory { + public NtfsAbstractDirectory(NtfsFileSystem fileSystem, FileNameRecord Record, FileRecordReference File, Directory Directory, String DirectoryPath) : base(fileSystem, Record, File,DirectoryPath) { + this.Directory = Directory; + } + + public Directory Directory { get; } + + IEnumerable IAbstractDirectory.AllEntries => Directory.Index.Entries.Where(FileSystem.FilterEntry).Select(entry => new NtfsAbstractRecord(this.FileSystem, entry.Key, entry.Value,Utilities.CombinePaths(this.FileName,entry.Key.FileName))); + + } +} diff --git a/Library/DiscUtils.Ntfs/NtfsAbstractRecord.cs b/Library/DiscUtils.Ntfs/NtfsAbstractRecord.cs new file mode 100644 index 000000000..259cde6bc --- /dev/null +++ b/Library/DiscUtils.Ntfs/NtfsAbstractRecord.cs @@ -0,0 +1,50 @@ +using DiscUtils.Streams; +using DiscUtils.Vfs; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace DiscUtils.Ntfs; + + +internal class NtfsAbstractRecord(NtfsFileSystem fileSystem, FileNameRecord Record, FileRecordReference FileIndex, String FilePath) : IAbstractRecord, IVfsFile { + + public DateTime CreationTimeUtc => Record.CreationTime; + public FileAttributes FileAttributes => Record.FileAttributes; + public string FileName => FilePath; + public bool IsDirectory => Record.Flags.HasFlag(NtfsFileAttributes.Directory); + public bool IsSymlink => Record.Flags.HasFlag(NtfsFileAttributes.ReparsePoint); + public DateTime LastAccessTimeUtc => Record.LastAccessTime; + public DateTime LastWriteTimeUtc => Record.ModificationTime; + public long FileId => (long)FileIndex.Value; + public long FileSize => (long)Record.RealSize; + public File AsFile() => FileSystem.GetFile(FileIndex); + public SparseStream FileContent => AsFile().OpenStream(AttributeType.Data, default, FileAccess.Read); // AttributeType.Data attributeName=null + + protected NtfsFileSystem FileSystem { get; } = fileSystem; + protected FileNameRecord Record { get; } = Record; + protected FileRecordReference FileIndex { get; } = FileIndex; + DateTime IVfsFile.CreationTimeUtc { get; set; } + FileAttributes IVfsFile.FileAttributes { get; set; } + IBuffer IVfsFile.FileContent { get; } + long IVfsFile.FileLength { get; } + DateTime IVfsFile.LastAccessTimeUtc { get; set; } + DateTime IVfsFile.LastWriteTimeUtc { get; set; } + + public IAbstractDirectory GetAsAbstractDirectory() { + + if (!IsDirectory) + throw new InvalidOperationException("Not a directory"); + var file = AsFile(); + if (file is Directory d) + return new Directory.NtfsAbstractDirectory(FileSystem, Record, FileIndex, d,FilePath); + throw new InvalidOperationException("fileSystem.GetFile() should have returned us as a Directory but we were not"); + } + + public VfsDirEntry GetAsDirEntry() => throw new NotImplementedException(); + public IVfsFile GetAsFile() => this; + IEnumerable IVfsFile.EnumerateAllocationExtents() => this.GetAsFile().EnumerateAllocationExtents(); +} diff --git a/Library/DiscUtils.Ntfs/NtfsFileSystem.cs b/Library/DiscUtils.Ntfs/NtfsFileSystem.cs index 7b172879b..d509e017f 100644 --- a/Library/DiscUtils.Ntfs/NtfsFileSystem.cs +++ b/Library/DiscUtils.Ntfs/NtfsFileSystem.cs @@ -35,6 +35,8 @@ System.Collections.Generic.KeyValuePair; using System.Collections.Concurrent; using DiscUtils.Ntfs.Internals; +using System.Text; +using DiscUtils.Vfs; namespace DiscUtils.Ntfs; @@ -2663,6 +2665,22 @@ public override long UsedSpace public override uint VolumeId => (uint)_context.BiosParameterBlock.VolumeSerialNumber; + public override IAbstractRecord GetAbstractRecord(string path) { + var dirEntryPath = ParsePath(path, out _, out _); + var entry = GetDirectoryEntry(dirEntryPath); + return new NtfsAbstractRecord(this, entry.Value.Details, entry.Value.Reference, dirEntryPath); + } + + public override string GetSymlinkTarget(IAbstractRecord dirEntry) { + if (!dirEntry.IsSymlink) + throw new ArgumentException($"dirEntry is not a symlink"); + + var reparsePoint = GetReparsePoint(dirEntry.FileName); + if (reparsePoint == null) + throw new IOException($"Unable to read reparse point for {dirEntry.FileName}"); + + return reparsePoint.ParseSymlink(dirEntry.FileName); + } /// /// A plugin system for handling reparse points. Handlers for specific tags can register here /// with a delegate that handles such reparse points when they are opened. diff --git a/Library/DiscUtils.VirtualFileSystem/VirtualFileSystem.cs b/Library/DiscUtils.VirtualFileSystem/VirtualFileSystem.cs index 1063e87e7..d920bb380 100644 --- a/Library/DiscUtils.VirtualFileSystem/VirtualFileSystem.cs +++ b/Library/DiscUtils.VirtualFileSystem/VirtualFileSystem.cs @@ -6,6 +6,7 @@ using DiscUtils.Streams; using System.Collections.Generic; using System.Text.RegularExpressions; +using DiscUtils.Vfs; namespace DiscUtils.VirtualFileSystem; public partial class VirtualFileSystem : DiscFileSystem, IWindowsFileSystem, IUnixFileSystem, IFileSystemBuilder @@ -834,4 +835,6 @@ void IFileSystemBuilder.AddFile(string name, Stream source, int ownerId, int gro void IFileSystemBuilder.AddFile(string name, string sourcePath, int ownerId, int groupId, UnixFilePermissions fileMode, DateTime modificationTime) => AddFile(name, sourcePath, ownerId, groupId, fileMode, UnixFileType.Regular, modificationTime, modificationTime, modificationTime); + public override string GetSymlinkTarget(IAbstractRecord record) => throw new NotImplementedException(); + public override IAbstractRecord GetAbstractRecord(string path) => throw new NotImplementedException(); } diff --git a/Library/DiscUtils.Wim/WimFileSystem.cs b/Library/DiscUtils.Wim/WimFileSystem.cs index 97f846d6b..c4452803d 100644 --- a/Library/DiscUtils.Wim/WimFileSystem.cs +++ b/Library/DiscUtils.Wim/WimFileSystem.cs @@ -30,6 +30,7 @@ using System.Xml.XPath; using System.Linq; using LTRData.Extensions.Split; +using DiscUtils.Vfs; namespace DiscUtils.Wim; @@ -109,8 +110,10 @@ public void SetSecurity(string path, RawSecurityDescriptor securityDescriptor) public ReparsePoint GetReparsePoint(string path) { var dirEntry = GetEntry(path); - - var hdr = _file.LocateResource(dirEntry.Hash) + var hash = dirEntry.Hash; + if (Utilities.IsAllZeros(hash)) + hash = GetFileHash(path); + var hdr = _file.LocateResource(hash) ?? throw new IOException("No reparse point"); using var s = _file.OpenResourceStream(hdr); @@ -744,4 +747,41 @@ private IEnumerable DoSearch(string path, Func filter, boo } } } + internal class WimAbstractRecord (WimFileSystem FileSystem, DirectoryEntry Record, String Path) : IAbstractRecord{ + public DateTime CreationTimeUtc => DateTime.FromFileTimeUtc(Record.CreationTime); // Entry has creationTime + public FileAttributes FileAttributes => Record.Attributes; + public string FileName => Path; + public bool IsDirectory => Record.Attributes.HasFlag(FileAttributes.Directory); + public bool IsSymlink => Record.Attributes.HasFlag(FileAttributes.ReparsePoint); + public DateTime LastAccessTimeUtc => DateTime.FromFileTimeUtc(Record.LastAccessTime); + public DateTime LastWriteTimeUtc => DateTime.FromFileTimeUtc(Record.LastWriteTime); + public long FileId => BitConverter.ToInt64(Record.Hash, 0) ^ BitConverter.ToInt64(Record.Hash, 8) ^ BitConverter.ToInt32(Record.Hash, 16); + public long FileSize => FileSystem._file.LocateResource(Record.GetStreamHash(default))?.OriginalSize ?? 0; + public SparseStream FileContent => FileSystem.OpenFile(FileName, FileMode.Open, FileAccess.Read); + protected WimFileSystem FileSystem { get; } = FileSystem; + protected DirectoryEntry Record{ get; } = Record; + + public IAbstractDirectory GetAsAbstractDirectory() => (IAbstractDirectory) this; + public VfsDirEntry GetAsDirEntry() => throw new NotImplementedException(); + public IVfsFile GetAsFile() => throw new NotImplementedException(); + } + internal class WimAbstractDirectory(WimFileSystem FileSystem, DirectoryEntry Record, String Path) : WimAbstractRecord(FileSystem, Record, Path), IAbstractDirectory { + public IEnumerable AllEntries => FileSystem.GetDirectory(this.Record.SubdirOffset).Select(record => FileSystem.GetEntryAsAbstractRecord(record,Utilities.CombinePaths(Path,record.FileName))); + + + } + private IAbstractRecord GetEntryAsAbstractRecord(DirectoryEntry record, String Path) => (record.Attributes.HasFlag(FileAttributes.Directory) && record.Attributes.HasFlag(FileAttributes.ReparsePoint) == false) ? + new WimAbstractDirectory(this,record,Path) : + new WimAbstractRecord(this,record,Path); + public override IAbstractRecord GetAbstractRecord(string path)=> GetEntryAsAbstractRecord(GetEntry(path),String.IsNullOrWhiteSpace(path) ? "/" : ""); + public override string GetSymlinkTarget(IAbstractRecord dirEntry) { + if (!dirEntry.IsSymlink) + throw new ArgumentException($"dirEntry is not a symlink"); + + var reparsePoint = GetReparsePoint(dirEntry.FileName); + if (reparsePoint == null) + throw new IOException($"Unable to read reparse point for {dirEntry.FileName}"); + + return reparsePoint.ParseSymlink(dirEntry.FileName); + } } From f658e86c3cbd1d2bfba251c853668d6b3ae5c3d9 Mon Sep 17 00:00:00 2001 From: Mitch Capper Date: Thu, 18 Sep 2025 01:53:26 -0700 Subject: [PATCH 3/6] Finished most others minus nativeFS didn't add native calls to get inode/fileid --- Library/DiscUtils.Btrfs/BtrfsFileSystem.cs | 3 +- Library/DiscUtils.Core/DiscFileSystem.cs | 5 +- Library/DiscUtils.Core/NativeFileSystem.cs | 84 ++++++++++++++++++- Library/DiscUtils.Core/Vfs/VfsDirEntry.cs | 1 + Library/DiscUtils.Core/Vfs/VfsFileSystem.cs | 4 +- Library/DiscUtils.Fat/FatFileSystem.cs | 50 ++++++++++- .../DiscUtils.HfsPlus/HfsPlusFileSystem.cs | 3 +- .../HfsPlusFileSystemImpl.cs | 10 ++- Library/DiscUtils.Iso9660/CDReader.cs | 3 +- Library/DiscUtils.Nfs/NfsFileSystem.cs | 4 + .../SquashFileSystemReader.cs | 3 +- Library/DiscUtils.Swap/SwapFileSystem.cs | 4 + Library/DiscUtils.Udf/UdfReader.cs | 6 +- Library/DiscUtils.Xfs/VfsXfsFileSystem.cs | 4 + Library/DiscUtils.Xfs/XfsFileSystem.cs | 2 + 15 files changed, 168 insertions(+), 18 deletions(-) diff --git a/Library/DiscUtils.Btrfs/BtrfsFileSystem.cs b/Library/DiscUtils.Btrfs/BtrfsFileSystem.cs index 06ed70dca..ec2304120 100644 --- a/Library/DiscUtils.Btrfs/BtrfsFileSystem.cs +++ b/Library/DiscUtils.Btrfs/BtrfsFileSystem.cs @@ -42,7 +42,8 @@ public BtrfsFileSystem(Stream stream) : base(new VfsBtrfsFileSystem(stream)) { } - + public override IAbstractRecord GetAbstractRecord(string path) => GetRealFileSystem().GetAbstractRecord(path); + public override string GetSymlinkTarget(IAbstractRecord dirEntry) => GetRealFileSystem().GetSymlinkTarget(dirEntry); /// /// Initializes a new instance of the BtrfsFileSystem class. /// diff --git a/Library/DiscUtils.Core/DiscFileSystem.cs b/Library/DiscUtils.Core/DiscFileSystem.cs index cfd0bfd09..3a4649b52 100644 --- a/Library/DiscUtils.Core/DiscFileSystem.cs +++ b/Library/DiscUtils.Core/DiscFileSystem.cs @@ -521,9 +521,8 @@ protected virtual void Dispose(bool disposing) } } - public virtual IAbstractRecord GetAbstractRecord(string path) => throw new NotImplementedException(); - public virtual string GetSymlinkTarget(IAbstractRecord dirEntry) => throw new NotImplementedException(); - + public abstract IAbstractRecord GetAbstractRecord(string path); + public abstract string GetSymlinkTarget(IAbstractRecord dirEntry); public event EventHandler Disposed; diff --git a/Library/DiscUtils.Core/NativeFileSystem.cs b/Library/DiscUtils.Core/NativeFileSystem.cs index ae427ce33..04760eef3 100644 --- a/Library/DiscUtils.Core/NativeFileSystem.cs +++ b/Library/DiscUtils.Core/NativeFileSystem.cs @@ -783,7 +783,85 @@ private string CleanItems(string dirtyItems) { return dirtyItems.Substring(BasePath.Length - 1); } - - public override IAbstractRecord GetAbstractRecord(string path) => throw new NotImplementedException(); - public override string GetSymlinkTarget(IAbstractRecord dirEntry) => throw new NotImplementedException(); + internal class NativeAbstractDirectory : NativeAbstractRecord, IAbstractDirectory { + private DirectoryInfo di; + + public NativeAbstractDirectory(NativeFileSystem fs, DirectoryInfo di) : base(fs,di,true){ + this.di = di; + } + + public NativeAbstractDirectory(NativeFileSystem fs, String path) : this(fs,new DirectoryInfo(path)) { + + } + + public IEnumerable AllEntries { + get{ + foreach(var d in di.GetDirectories()) + yield return new NativeAbstractDirectory(fs,d); + foreach(var d in di.GetFiles()) + yield return new NativeAbstractRecord(fs,d,false); + } + } + + + } + internal class NativeAbstractRecord : IAbstractRecord { + public NativeAbstractRecord(NativeFileSystem fs, String path){ + info = new FileInfo(Path.Combine(fs.BasePath,path)); + IsDirectory = false; + FileName = fs.CleanItems(info.FullName); + this.fs = fs; + } + internal NativeAbstractRecord(NativeFileSystem fs, System.IO.FileSystemInfo info, bool isDirectory){ + this.info = info; + IsDirectory = true; + FileName = fs.CleanItems(info.FullName); + this.fs = fs; + } + protected System.IO.FileSystemInfo info; + + public DateTime CreationTimeUtc => info.CreationTimeUtc; + public FileAttributes FileAttributes => info.Attributes; + public string FileName {get; } + + protected NativeFileSystem fs; + + public virtual bool IsDirectory {get; } + public bool IsSymlink => info.LinkTarget != null; + public DateTime LastAccessTimeUtc => info.LastAccessTimeUtc; + public DateTime LastWriteTimeUtc => info.LastWriteTimeUtc; + public long FileId => throw new NotImplementedException(); // need native call for it or inode + public long FileSize => (info is FileInfo fi) ? fi.Length : 0; + public SparseStream FileContent => (info is FileInfo fi) ? fs.OpenFile(FileName, FileMode.Open, FileAccess.Read) : null; + + public VfsDirEntry GetAsDirEntry() => throw new NotImplementedException(); + public IVfsFile GetAsFile() => throw new NotImplementedException(); + public IAbstractDirectory GetAsAbstractDirectory() => this as IAbstractDirectory; + } + public override IAbstractRecord GetAbstractRecord(string path) { + var truePath = Path.Combine(BasePath,path); + if (Directory.Exists(truePath)){ + var di = new DirectoryInfo(truePath); + return new NativeAbstractRecord(this,di,true); + } + else if (File.Exists(truePath)){ + return new NativeAbstractRecord(this,path); + } + else + return null; + } + public override string GetSymlinkTarget(IAbstractRecord dirEntry) { + if (! dirEntry.IsSymlink) + throw new ArgumentException("Not a symlink", nameof(dirEntry)); + string target; + var ourPath = Path.Combine(BasePath, dirEntry.FileName); + if (dirEntry.IsDirectory) + target = Directory.ResolveLinkTarget(ourPath, false).FullName; + else { + target = File.ResolveLinkTarget(ourPath, false).FullName; + } + if (target.StartsWith(BasePath, StringComparison.CurrentCultureIgnoreCase) == false) + return null; + return CleanItems(target); + } } diff --git a/Library/DiscUtils.Core/Vfs/VfsDirEntry.cs b/Library/DiscUtils.Core/Vfs/VfsDirEntry.cs index d5d4fe67e..7cb5c969b 100644 --- a/Library/DiscUtils.Core/Vfs/VfsDirEntry.cs +++ b/Library/DiscUtils.Core/Vfs/VfsDirEntry.cs @@ -37,6 +37,7 @@ namespace DiscUtils.Vfs; /// public abstract class VfsDirEntry { + public static bool NO_SYMLINK_RESOLUTION; /// /// Gets the creation time of the file or directory. /// diff --git a/Library/DiscUtils.Core/Vfs/VfsFileSystem.cs b/Library/DiscUtils.Core/Vfs/VfsFileSystem.cs index 334bde8b8..7c31849d9 100644 --- a/Library/DiscUtils.Core/Vfs/VfsFileSystem.cs +++ b/Library/DiscUtils.Core/Vfs/VfsFileSystem.cs @@ -801,7 +801,7 @@ protected TFile GetFile(string path) } - protected virtual TDirectory ConvertDirEntryToDirectory(TDirEntry dirEntry) => throw new NotImplementedException(); + protected abstract TDirectory ConvertDirEntryToDirectory(TDirEntry dirEntry);// => throw new NotImplementedException(); /// /// Converts a directory entry to an object representing a file. @@ -997,7 +997,7 @@ protected virtual (TDirEntry TargetEntry, string TargetPath) ResolveSymlink(TDir var resolvesLeft = 20; while (currentEntry.IsSymlink && resolvesLeft > 0) { - if (GetFile(currentEntry) is not IVfsSymlink symlink) + if (VfsDirEntry.NO_SYMLINK_RESOLUTION || GetFile(currentEntry) is not IVfsSymlink symlink) { Trace.WriteLine($"Unable to resolve symlink '{path}'"); return default; diff --git a/Library/DiscUtils.Fat/FatFileSystem.cs b/Library/DiscUtils.Fat/FatFileSystem.cs index 5812e81d0..e88045ae4 100644 --- a/Library/DiscUtils.Fat/FatFileSystem.cs +++ b/Library/DiscUtils.Fat/FatFileSystem.cs @@ -29,6 +29,7 @@ using DiscUtils.Internal; using DiscUtils.Streams; using DiscUtils.Streams.Compatibility; +using DiscUtils.Vfs; using LTRData.Extensions.Split; namespace DiscUtils.Fat; @@ -2109,6 +2110,53 @@ public static FatFileSystem FormatPartition( stream.Position = pos; return new FatFileSystem(stream); } + internal class FatAbstractDirectory : FatAbstractRecord, IAbstractDirectory { + public FatAbstractDirectory(FatFileSystem fs, DirectoryEntry entry) : base(fs,entry) { + this.IsDirectory = true; + } + + public IEnumerable AllEntries { + get{ + var dir = fs.GetDirectory(FileName); + foreach (var di in dir.GetDirectories()) + yield return new FatAbstractDirectory(fs,di); + foreach (var fi in dir.GetFiles()) + yield return new FatAbstractRecord(fs,fi); + } + } + } + internal class FatAbstractRecord : IAbstractRecord { + protected FatFileSystem fs; + protected DirectoryEntry entry; + + public FatAbstractRecord(FatFileSystem fs, DirectoryEntry entry) { + this.fs = fs; + this.entry = entry; + } + public DateTime CreationTimeUtc => entry.CreationTime.ToUniversalTime(); + public FileAttributes FileAttributes => IsDirectory ? FileAttributes.Directory : (FileAttributes)entry.Attributes; + public string FileName => entry.Name.FullName; + public bool IsDirectory { get;protected set; } + public bool IsSymlink { get; } = false; + public DateTime LastAccessTimeUtc => entry.LastAccessTime.ToUniversalTime(); + public DateTime LastWriteTimeUtc => entry.LastWriteTime.ToUniversalTime(); + public long FileId => entry.FirstCluster; + public long FileSize => entry.FileSize; + public SparseStream FileContent => fs.OpenFile(FileName,FileMode.Open,FileAccess.Read); + + public IAbstractDirectory GetAsAbstractDirectory() => this as IAbstractDirectory; + public VfsDirEntry GetAsDirEntry() => throw new NotImplementedException(); + public IVfsFile GetAsFile() => throw new NotImplementedException(); + } + public override IAbstractRecord GetAbstractRecord(string path) { + var dirEntry = GetDirectoryEntry(path) + ?? throw new FileNotFoundException("No such file", path); + if (dirEntry.Attributes.HasFlag(FatAttributes.Directory)) + return new FatAbstractDirectory(this,dirEntry); + return new FatAbstractRecord(this,dirEntry); + } + public override string GetSymlinkTarget(IAbstractRecord dirEntry) => null; -#endregion + #endregion } + diff --git a/Library/DiscUtils.HfsPlus/HfsPlusFileSystem.cs b/Library/DiscUtils.HfsPlus/HfsPlusFileSystem.cs index cf47cf2c0..139f84596 100644 --- a/Library/DiscUtils.HfsPlus/HfsPlusFileSystem.cs +++ b/Library/DiscUtils.HfsPlus/HfsPlusFileSystem.cs @@ -49,7 +49,8 @@ public UnixFileSystemInfo GetUnixFileInfo(string path) { return GetRealFileSystem().GetUnixFileInfo(path); } - + public override IAbstractRecord GetAbstractRecord(string path) => GetRealFileSystem().GetAbstractRecord(path); + public override string GetSymlinkTarget(IAbstractRecord dirEntry) => GetRealFileSystem().GetSymlinkTarget(dirEntry); internal static bool Detect(Stream stream) { if (stream.Length < 1536) diff --git a/Library/DiscUtils.HfsPlus/HfsPlusFileSystemImpl.cs b/Library/DiscUtils.HfsPlus/HfsPlusFileSystemImpl.cs index 170dc5367..922b2d70c 100644 --- a/Library/DiscUtils.HfsPlus/HfsPlusFileSystemImpl.cs +++ b/Library/DiscUtils.HfsPlus/HfsPlusFileSystemImpl.cs @@ -119,10 +119,12 @@ public IEnumerable PathToExtents(string path) return fileBuffer.EnumerateAllocationExtents(); } - /// - /// Size of the Filesystem in bytes - /// - public override long Size => throw new NotSupportedException("Filesystem size is not (yet) supported"); + protected override Directory ConvertDirEntryToDirectory(DirEntry dirEntry) => ConvertDirEntryToFile(dirEntry) as Directory; + + /// + /// Size of the Filesystem in bytes + /// + public override long Size => throw new NotSupportedException("Filesystem size is not (yet) supported"); /// /// Used space of the Filesystem in bytes diff --git a/Library/DiscUtils.Iso9660/CDReader.cs b/Library/DiscUtils.Iso9660/CDReader.cs index fab67b8a5..2bfd6294c 100644 --- a/Library/DiscUtils.Iso9660/CDReader.cs +++ b/Library/DiscUtils.Iso9660/CDReader.cs @@ -168,7 +168,8 @@ public UnixFileSystemInfo GetUnixFileInfo(string path) { return GetRealFileSystem().GetUnixFileInfo(path); } - + public override IAbstractRecord GetAbstractRecord(string path) => GetRealFileSystem().GetAbstractRecord(path); + public override string GetSymlinkTarget(IAbstractRecord dirEntry) => GetRealFileSystem().GetSymlinkTarget(dirEntry); /// /// Detects if a stream contains a valid ISO file system. /// diff --git a/Library/DiscUtils.Nfs/NfsFileSystem.cs b/Library/DiscUtils.Nfs/NfsFileSystem.cs index aae82b0e5..d3eb19006 100644 --- a/Library/DiscUtils.Nfs/NfsFileSystem.cs +++ b/Library/DiscUtils.Nfs/NfsFileSystem.cs @@ -26,6 +26,7 @@ using System.IO; using DiscUtils.Internal; using DiscUtils.Streams; +using DiscUtils.Vfs; using LTRData.Extensions.Buffers; namespace DiscUtils.Nfs; @@ -829,4 +830,7 @@ private Nfs3FileHandle GetDirectory(Nfs3FileHandle parent, string[] dirs) return handle; } + + public override IAbstractRecord GetAbstractRecord(string path) => throw new NotImplementedException(); + public override string GetSymlinkTarget(IAbstractRecord dirEntry) => throw new NotImplementedException(); } diff --git a/Library/DiscUtils.SquashFs/SquashFileSystemReader.cs b/Library/DiscUtils.SquashFs/SquashFileSystemReader.cs index d8ef29858..912930e54 100644 --- a/Library/DiscUtils.SquashFs/SquashFileSystemReader.cs +++ b/Library/DiscUtils.SquashFs/SquashFileSystemReader.cs @@ -59,7 +59,8 @@ public UnixFileSystemInfo GetUnixFileInfo(string path) { return GetRealFileSystem().GetUnixFileInfo(path); } - + public override IAbstractRecord GetAbstractRecord(string path) => GetRealFileSystem().GetAbstractRecord(path); + public override string GetSymlinkTarget(IAbstractRecord dirEntry) => GetRealFileSystem().GetSymlinkTarget(dirEntry); /// /// Detects if the stream contains a SquashFs file system. /// diff --git a/Library/DiscUtils.Swap/SwapFileSystem.cs b/Library/DiscUtils.Swap/SwapFileSystem.cs index 14d630dee..9175af4e3 100644 --- a/Library/DiscUtils.Swap/SwapFileSystem.cs +++ b/Library/DiscUtils.Swap/SwapFileSystem.cs @@ -21,6 +21,7 @@ // using System; +using System.Collections.Generic; using System.IO; using DiscUtils.Streams; using DiscUtils.Vfs; @@ -108,4 +109,7 @@ protected override IVfsFile ConvertDirEntryToFile(VfsDirEntry dirEntry) { throw new NotImplementedException(); } + + + protected override IVfsDirectory ConvertDirEntryToDirectory(VfsDirEntry dirEntry) => throw new NotImplementedException(); } diff --git a/Library/DiscUtils.Udf/UdfReader.cs b/Library/DiscUtils.Udf/UdfReader.cs index c9a5bd7bd..eadda4f21 100644 --- a/Library/DiscUtils.Udf/UdfReader.cs +++ b/Library/DiscUtils.Udf/UdfReader.cs @@ -49,6 +49,8 @@ public UdfReader(Stream data) public UdfReader(Stream data, int sectorSize) : base(new VfsUdfReader(data, sectorSize)) {} + public override IAbstractRecord GetAbstractRecord(string path) => GetRealFileSystem().GetAbstractRecord(path); + public override string GetSymlinkTarget(IAbstractRecord dirEntry) => GetRealFileSystem().GetSymlinkTarget(dirEntry); /// /// Detects if a stream contains a valid UDF file system. /// @@ -309,5 +311,7 @@ private bool ProbeSectorSize(int size) return dt.TagIdentifier == TagIdentifier.AnchorVolumeDescriptorPointer && dt.TagLocation == 256; } - } + + protected override Directory ConvertDirEntryToDirectory(FileIdentifier dirEntry) => (Directory)File.FromDescriptor(Context, dirEntry.FileLocation); + } } diff --git a/Library/DiscUtils.Xfs/VfsXfsFileSystem.cs b/Library/DiscUtils.Xfs/VfsXfsFileSystem.cs index c43183c9e..69406a70d 100644 --- a/Library/DiscUtils.Xfs/VfsXfsFileSystem.cs +++ b/Library/DiscUtils.Xfs/VfsXfsFileSystem.cs @@ -203,4 +203,8 @@ private static long XFS_AGF_DADDR(SuperBlock sb) { return 1 << (sb.SectorSizeLog2 - BBSHIFT); } + + protected override Directory ConvertDirEntryToDirectory(DirEntry dirEntry) { + return dirEntry.CachedDirectory ??= new Directory(Context, dirEntry.Inode); + } } diff --git a/Library/DiscUtils.Xfs/XfsFileSystem.cs b/Library/DiscUtils.Xfs/XfsFileSystem.cs index c74b3f96a..60bd00027 100644 --- a/Library/DiscUtils.Xfs/XfsFileSystem.cs +++ b/Library/DiscUtils.Xfs/XfsFileSystem.cs @@ -67,6 +67,8 @@ public IEnumerable PathToExtents(string path) { return GetRealFileSystem().PathToExtents(path); } + public override IAbstractRecord GetAbstractRecord(string path) => GetRealFileSystem().GetAbstractRecord(path); + public override string GetSymlinkTarget(IAbstractRecord dirEntry) => GetRealFileSystem().GetSymlinkTarget(dirEntry); internal static bool Detect(Stream stream) { From 0ff313e543d42c4b7799a4b58ec3ee20ad22d231 Mon Sep 17 00:00:00 2001 From: Mitch Capper Date: Tue, 23 Sep 2025 03:42:48 -0700 Subject: [PATCH 4/6] Fixed Fat abstract directory support. --- Library/DiscUtils.Core/Vfs/IAbstractRecord.cs | 3 ++ Library/DiscUtils.Fat/FatFileSystem.cs | 32 ++++++++++++------- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/Library/DiscUtils.Core/Vfs/IAbstractRecord.cs b/Library/DiscUtils.Core/Vfs/IAbstractRecord.cs index a0c9e2205..702c5eba2 100644 --- a/Library/DiscUtils.Core/Vfs/IAbstractRecord.cs +++ b/Library/DiscUtils.Core/Vfs/IAbstractRecord.cs @@ -8,6 +8,9 @@ namespace DiscUtils.Vfs; public interface IAbstractRecord { DateTime CreationTimeUtc { get; } FileAttributes FileAttributes { get; } + /// + /// the SubPath relative to the IAbstractDirectory that returned it + /// string FileName { get; } bool IsDirectory { get; } bool IsSymlink { get; } diff --git a/Library/DiscUtils.Fat/FatFileSystem.cs b/Library/DiscUtils.Fat/FatFileSystem.cs index e88045ae4..2d2a98cc4 100644 --- a/Library/DiscUtils.Fat/FatFileSystem.cs +++ b/Library/DiscUtils.Fat/FatFileSystem.cs @@ -2111,17 +2111,18 @@ public static FatFileSystem FormatPartition( return new FatFileSystem(stream); } internal class FatAbstractDirectory : FatAbstractRecord, IAbstractDirectory { - public FatAbstractDirectory(FatFileSystem fs, DirectoryEntry entry) : base(fs,entry) { + public FatAbstractDirectory(FatFileSystem fs, DirectoryEntry entry, String FullPath) : base(fs,entry,FullPath) { this.IsDirectory = true; } + public override string FileName => entry.Name.IsEndMarker() ? "" : entry.Name.FullName; //IsEndMarker is only true for us on the fake root dir, otherwise we would return nulls here. public IEnumerable AllEntries { get{ - var dir = fs.GetDirectory(FileName); + var dir = fs.GetDirectory(FullPath); foreach (var di in dir.GetDirectories()) - yield return new FatAbstractDirectory(fs,di); + yield return new FatAbstractDirectory(fs,di,Path.Combine(FullPath,di.Name.FullName)); foreach (var fi in dir.GetFiles()) - yield return new FatAbstractRecord(fs,fi); + yield return new FatAbstractRecord(fs,fi, Path.Combine(FullPath,fi.Name.FullName)); } } } @@ -2129,31 +2130,38 @@ internal class FatAbstractRecord : IAbstractRecord { protected FatFileSystem fs; protected DirectoryEntry entry; - public FatAbstractRecord(FatFileSystem fs, DirectoryEntry entry) { + public FatAbstractRecord(FatFileSystem fs, DirectoryEntry entry, String FullPath) { this.fs = fs; this.entry = entry; + this.FullPath = FullPath; } public DateTime CreationTimeUtc => entry.CreationTime.ToUniversalTime(); public FileAttributes FileAttributes => IsDirectory ? FileAttributes.Directory : (FileAttributes)entry.Attributes; - public string FileName => entry.Name.FullName; - public bool IsDirectory { get;protected set; } + public virtual string FileName => entry.Name.FullName; + protected string FullPath; + public bool IsDirectory { get; protected set; } public bool IsSymlink { get; } = false; public DateTime LastAccessTimeUtc => entry.LastAccessTime.ToUniversalTime(); public DateTime LastWriteTimeUtc => entry.LastWriteTime.ToUniversalTime(); public long FileId => entry.FirstCluster; public long FileSize => entry.FileSize; - public SparseStream FileContent => fs.OpenFile(FileName,FileMode.Open,FileAccess.Read); + public SparseStream FileContent => fs.OpenFile(FullPath,FileMode.Open,FileAccess.Read); public IAbstractDirectory GetAsAbstractDirectory() => this as IAbstractDirectory; public VfsDirEntry GetAsDirEntry() => throw new NotImplementedException(); public IVfsFile GetAsFile() => throw new NotImplementedException(); } public override IAbstractRecord GetAbstractRecord(string path) { - var dirEntry = GetDirectoryEntry(path) - ?? throw new FileNotFoundException("No such file", path); + var dirEntry = GetDirectoryEntry(path); + if (dirEntry == null && IsRootPath(path)){ + var dir = GetDirectory(path); + dirEntry = dir.SelfEntry;//directory will create a fake one for us for root + } + if (dirEntry == null) + throw new FileNotFoundException("No such file", path); if (dirEntry.Attributes.HasFlag(FatAttributes.Directory)) - return new FatAbstractDirectory(this,dirEntry); - return new FatAbstractRecord(this,dirEntry); + return new FatAbstractDirectory(this,dirEntry,path); + return new FatAbstractRecord(this,dirEntry,path); } public override string GetSymlinkTarget(IAbstractRecord dirEntry) => null; From 668c6e1c50d21097b3266ce53a8b3f213a3b0850 Mon Sep 17 00:00:00 2001 From: Mitch Capper Date: Thu, 23 Oct 2025 23:41:36 -0700 Subject: [PATCH 5/6] NTFSish system improvements to symlink/reparse points IAbstractRecord Filename standardization for NTFSish as well --- Library/DiscUtils.Core/NativeFileSystem.cs | 8 ++----- Library/DiscUtils.Core/ReparsePoint.cs | 13 +++++++----- Library/DiscUtils.Core/Vfs/IAbstractRecord.cs | 2 +- Library/DiscUtils.Ntfs/NtfsAbstractRecord.cs | 8 ++++--- Library/DiscUtils.Ntfs/NtfsFileSystem.cs | 11 ++++++---- Library/DiscUtils.Wim/WimFileSystem.cs | 21 +++++++++++-------- 6 files changed, 35 insertions(+), 28 deletions(-) diff --git a/Library/DiscUtils.Core/NativeFileSystem.cs b/Library/DiscUtils.Core/NativeFileSystem.cs index 04760eef3..b176429cc 100644 --- a/Library/DiscUtils.Core/NativeFileSystem.cs +++ b/Library/DiscUtils.Core/NativeFileSystem.cs @@ -806,15 +806,11 @@ public IEnumerable AllEntries { } internal class NativeAbstractRecord : IAbstractRecord { - public NativeAbstractRecord(NativeFileSystem fs, String path){ - info = new FileInfo(Path.Combine(fs.BasePath,path)); - IsDirectory = false; - FileName = fs.CleanItems(info.FullName); - this.fs = fs; + public NativeAbstractRecord(NativeFileSystem fs, String path) : this (fs, new FileInfo(Path.Combine(fs.BasePath, path)), false) { } internal NativeAbstractRecord(NativeFileSystem fs, System.IO.FileSystemInfo info, bool isDirectory){ this.info = info; - IsDirectory = true; + IsDirectory = isDirectory; FileName = fs.CleanItems(info.FullName); this.fs = fs; } diff --git a/Library/DiscUtils.Core/ReparsePoint.cs b/Library/DiscUtils.Core/ReparsePoint.cs index a8425db91..e243763e0 100644 --- a/Library/DiscUtils.Core/ReparsePoint.cs +++ b/Library/DiscUtils.Core/ReparsePoint.cs @@ -36,7 +36,7 @@ public sealed class ReparsePoint /// /// The defined reparse point tag. /// The reparse point's content. - public ReparsePoint(int tag, byte[] content) + public ReparsePoint(uint tag, byte[] content) { Tag = tag; Content = content; @@ -50,22 +50,25 @@ public ReparsePoint(int tag, byte[] content) /// /// Gets or sets the defined reparse point tag. /// - public int Tag { get; set; } + public uint Tag { get; set; } // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/c8e77b37-3909-4fe6-a4ea-2b9d423b1ee4 - private const int IO_REPARSE_TAG_MOUNT_POINT = unchecked((int)0xA0000003); - private const int IO_REPARSE_TAG_SYMLINK = unchecked((int)0xA000000C); + private const uint IO_REPARSE_TAG_MOUNT_POINT = 0xA0000003; + private const uint IO_REPARSE_TAG_SYMLINK = 0xA000000C; // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/b41f1cbf-10df-4a47-98d4-1c52a833d913 private enum SymlinkFlags : int { FullpathName = 0, SYMLINK_FLAG_RELATIVE = 1 } + public static bool IsValidSymlinkTag(uint tag) { + return tag == IO_REPARSE_TAG_SYMLINK || tag == IO_REPARSE_TAG_MOUNT_POINT; + } internal string ParseSymlink(String originalPath) { var reparsePoint = this; using var stream = new MemoryStream(reparsePoint.Content); using var reader = new BinaryReader(stream); - if (reparsePoint.Tag != IO_REPARSE_TAG_SYMLINK && reparsePoint.Tag != IO_REPARSE_TAG_MOUNT_POINT) + if (! IsValidSymlinkTag(reparsePoint.Tag) ) throw new IOException($"Reparse point on {originalPath} is not a symlink or mount point (tag: 0x{reparsePoint.Tag:X8})"); var substNameOffset = reader.ReadUInt16(); diff --git a/Library/DiscUtils.Core/Vfs/IAbstractRecord.cs b/Library/DiscUtils.Core/Vfs/IAbstractRecord.cs index 702c5eba2..da9978e62 100644 --- a/Library/DiscUtils.Core/Vfs/IAbstractRecord.cs +++ b/Library/DiscUtils.Core/Vfs/IAbstractRecord.cs @@ -9,7 +9,7 @@ public interface IAbstractRecord { DateTime CreationTimeUtc { get; } FileAttributes FileAttributes { get; } /// - /// the SubPath relative to the IAbstractDirectory that returned it + /// the SubPath relative to the parent IAbstractDirectory /// string FileName { get; } bool IsDirectory { get; } diff --git a/Library/DiscUtils.Ntfs/NtfsAbstractRecord.cs b/Library/DiscUtils.Ntfs/NtfsAbstractRecord.cs index 259cde6bc..de1faca10 100644 --- a/Library/DiscUtils.Ntfs/NtfsAbstractRecord.cs +++ b/Library/DiscUtils.Ntfs/NtfsAbstractRecord.cs @@ -14,9 +14,10 @@ internal class NtfsAbstractRecord(NtfsFileSystem fileSystem, FileNameRecord Reco public DateTime CreationTimeUtc => Record.CreationTime; public FileAttributes FileAttributes => Record.FileAttributes; - public string FileName => FilePath; + public string FileName => Record.FileName; public bool IsDirectory => Record.Flags.HasFlag(NtfsFileAttributes.Directory); - public bool IsSymlink => Record.Flags.HasFlag(NtfsFileAttributes.ReparsePoint); + public bool IsSymlink => Record.Flags.HasFlag(NtfsFileAttributes.ReparsePoint) && ReparsePoint.IsValidSymlinkTag(Record.EASizeOrReparsePointTag); + public DateTime LastAccessTimeUtc => Record.LastAccessTime; public DateTime LastWriteTimeUtc => Record.ModificationTime; public long FileId => (long)FileIndex.Value; @@ -26,6 +27,7 @@ internal class NtfsAbstractRecord(NtfsFileSystem fileSystem, FileNameRecord Reco protected NtfsFileSystem FileSystem { get; } = fileSystem; protected FileNameRecord Record { get; } = Record; + public String FullPath => FilePath; protected FileRecordReference FileIndex { get; } = FileIndex; DateTime IVfsFile.CreationTimeUtc { get; set; } FileAttributes IVfsFile.FileAttributes { get; set; } @@ -35,7 +37,7 @@ internal class NtfsAbstractRecord(NtfsFileSystem fileSystem, FileNameRecord Reco DateTime IVfsFile.LastWriteTimeUtc { get; set; } public IAbstractDirectory GetAsAbstractDirectory() { - + if (!IsDirectory) throw new InvalidOperationException("Not a directory"); var file = AsFile(); diff --git a/Library/DiscUtils.Ntfs/NtfsFileSystem.cs b/Library/DiscUtils.Ntfs/NtfsFileSystem.cs index d509e017f..fbdf19cf9 100644 --- a/Library/DiscUtils.Ntfs/NtfsFileSystem.cs +++ b/Library/DiscUtils.Ntfs/NtfsFileSystem.cs @@ -1426,7 +1426,7 @@ public ReparsePoint GetReparsePoint(string path) using var contentStream = stream.Value.Open(FileAccess.Read); var rp = contentStream.ReadStruct((int)contentStream.Length); - return new ReparsePoint((int)rp.Tag, rp.Content); + return new ReparsePoint(rp.Tag, rp.Content); } } @@ -2674,12 +2674,15 @@ public override IAbstractRecord GetAbstractRecord(string path) { public override string GetSymlinkTarget(IAbstractRecord dirEntry) { if (!dirEntry.IsSymlink) throw new ArgumentException($"dirEntry is not a symlink"); + if (dirEntry is not NtfsAbstractRecord ntfsDirEntry) + throw new ArgumentException($"dirEntry is not an NtfsAbstractRecord"); + - var reparsePoint = GetReparsePoint(dirEntry.FileName); + var reparsePoint = GetReparsePoint(ntfsDirEntry.FullPath); if (reparsePoint == null) - throw new IOException($"Unable to read reparse point for {dirEntry.FileName}"); + throw new IOException($"Unable to read reparse point for {ntfsDirEntry.FullPath}"); - return reparsePoint.ParseSymlink(dirEntry.FileName); + return reparsePoint.ParseSymlink(ntfsDirEntry.FullPath); } /// /// A plugin system for handling reparse points. Handlers for specific tags can register here diff --git a/Library/DiscUtils.Wim/WimFileSystem.cs b/Library/DiscUtils.Wim/WimFileSystem.cs index c4452803d..e62f13b23 100644 --- a/Library/DiscUtils.Wim/WimFileSystem.cs +++ b/Library/DiscUtils.Wim/WimFileSystem.cs @@ -118,7 +118,7 @@ public ReparsePoint GetReparsePoint(string path) using var s = _file.OpenResourceStream(hdr); var buffer = s.ReadExactly((int)s.Length); - return new ReparsePoint((int)dirEntry.ReparseTag, buffer); + return new ReparsePoint(dirEntry.ReparseTag, buffer); } /// @@ -750,14 +750,15 @@ private IEnumerable DoSearch(string path, Func filter, boo internal class WimAbstractRecord (WimFileSystem FileSystem, DirectoryEntry Record, String Path) : IAbstractRecord{ public DateTime CreationTimeUtc => DateTime.FromFileTimeUtc(Record.CreationTime); // Entry has creationTime public FileAttributes FileAttributes => Record.Attributes; - public string FileName => Path; + public string FileName => Record.FileName; + public string FullPath => Path; public bool IsDirectory => Record.Attributes.HasFlag(FileAttributes.Directory); - public bool IsSymlink => Record.Attributes.HasFlag(FileAttributes.ReparsePoint); + public bool IsSymlink => Record.Attributes.HasFlag(FileAttributes.ReparsePoint) && ReparsePoint.IsValidSymlinkTag(Record.ReparseTag); public DateTime LastAccessTimeUtc => DateTime.FromFileTimeUtc(Record.LastAccessTime); public DateTime LastWriteTimeUtc => DateTime.FromFileTimeUtc(Record.LastWriteTime); public long FileId => BitConverter.ToInt64(Record.Hash, 0) ^ BitConverter.ToInt64(Record.Hash, 8) ^ BitConverter.ToInt32(Record.Hash, 16); public long FileSize => FileSystem._file.LocateResource(Record.GetStreamHash(default))?.OriginalSize ?? 0; - public SparseStream FileContent => FileSystem.OpenFile(FileName, FileMode.Open, FileAccess.Read); + public SparseStream FileContent => FileSystem.OpenFile(Path, FileMode.Open, FileAccess.Read); protected WimFileSystem FileSystem { get; } = FileSystem; protected DirectoryEntry Record{ get; } = Record; @@ -771,17 +772,19 @@ internal class WimAbstractDirectory(WimFileSystem FileSystem, DirectoryEntry Rec } private IAbstractRecord GetEntryAsAbstractRecord(DirectoryEntry record, String Path) => (record.Attributes.HasFlag(FileAttributes.Directory) && record.Attributes.HasFlag(FileAttributes.ReparsePoint) == false) ? - new WimAbstractDirectory(this,record,Path) : - new WimAbstractRecord(this,record,Path); + new WimAbstractDirectory(this,record, Path) : + new WimAbstractRecord(this,record, Path); public override IAbstractRecord GetAbstractRecord(string path)=> GetEntryAsAbstractRecord(GetEntry(path),String.IsNullOrWhiteSpace(path) ? "/" : ""); public override string GetSymlinkTarget(IAbstractRecord dirEntry) { if (!dirEntry.IsSymlink) throw new ArgumentException($"dirEntry is not a symlink"); + if (dirEntry is not WimAbstractRecord dirEntryWim) + throw new ArgumentException($"dirEntry is not a WimAbstractRecord"); - var reparsePoint = GetReparsePoint(dirEntry.FileName); + var reparsePoint = GetReparsePoint(dirEntryWim.FullPath); if (reparsePoint == null) - throw new IOException($"Unable to read reparse point for {dirEntry.FileName}"); + throw new IOException($"Unable to read reparse point for {dirEntryWim.FullPath}"); - return reparsePoint.ParseSymlink(dirEntry.FileName); + return reparsePoint.ParseSymlink(dirEntryWim.FullPath); } } From d1d76b7f2c4f67a66aa3966324d75f992b435ca9 Mon Sep 17 00:00:00 2001 From: Mitch Capper Date: Tue, 28 Oct 2025 15:46:56 -0700 Subject: [PATCH 6/6] Proper conversion for abstract entry to directory --- Library/DiscUtils.Iso9660/VfsCDReader.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Library/DiscUtils.Iso9660/VfsCDReader.cs b/Library/DiscUtils.Iso9660/VfsCDReader.cs index dfe880ab9..a03491f35 100644 --- a/Library/DiscUtils.Iso9660/VfsCDReader.cs +++ b/Library/DiscUtils.Iso9660/VfsCDReader.cs @@ -540,7 +540,7 @@ public Stream OpenBootImage() throw new InvalidOperationException("No valid boot image"); } protected override ReaderDirectory ConvertDirEntryToDirectory(ReaderDirEntry dirEntry) { - if (dirEntry.IsDirectory) + if (! dirEntry.IsDirectory) throw new Exception("Invalid Directory Request record is not a directory"); return new ReaderDirectory(Context, dirEntry);