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.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..3a4649b52 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,9 @@ protected virtual void Dispose(bool disposing) } } + public abstract IAbstractRecord GetAbstractRecord(string path); + public abstract string GetSymlinkTarget(IAbstractRecord dirEntry); + 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/NativeFileSystem.cs b/Library/DiscUtils.Core/NativeFileSystem.cs index 2607de1b5..b176429cc 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,81 @@ private string CleanItems(string dirtyItems) { return dirtyItems.Substring(BasePath.Length - 1); } + 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) : this (fs, new FileInfo(Path.Combine(fs.BasePath, path)), false) { + } + internal NativeAbstractRecord(NativeFileSystem fs, System.IO.FileSystemInfo info, bool isDirectory){ + this.info = info; + IsDirectory = isDirectory; + 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/ReparsePoint.cs b/Library/DiscUtils.Core/ReparsePoint.cs index 111ad3bdd..e243763e0 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; /// @@ -32,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; @@ -46,5 +50,48 @@ 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 + public uint Tag { get; set; } + // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/c8e77b37-3909-4fe6-a4ea-2b9d423b1ee4 + 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 (! 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(); + 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 new file mode 100644 index 000000000..da9978e62 --- /dev/null +++ b/Library/DiscUtils.Core/Vfs/IAbstractRecord.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace DiscUtils.Vfs; + + +public interface IAbstractRecord { + DateTime CreationTimeUtc { get; } + FileAttributes FileAttributes { get; } + /// + /// the SubPath relative to the parent IAbstractDirectory + /// + string FileName { get; } + bool IsDirectory { get; } + bool IsSymlink { get; } + DateTime LastAccessTimeUtc { get; } + DateTime LastWriteTimeUtc { get; } + long FileId { get; } + long FileSize { get; } + Streams.SparseStream FileContent { get; } + VfsDirEntry GetAsDirEntry(); + IVfsFile GetAsFile(); + IAbstractDirectory GetAsAbstractDirectory(); +} + +public interface IAbstractDirectory : IAbstractRecord { + IEnumerable AllEntries { get; } +} 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/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 131c147c4..7c31849d9 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 VfsAbstractRecord(dirEntry, GetFile(dirEntry)); + } + internal class FullDirectory(VfsFileSystem fs, VfsDirEntry DirEntry, TDirectory Directory) : VfsAbstractRecord(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 abstract 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; @@ -937,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.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.Fat/FatFileSystem.cs b/Library/DiscUtils.Fat/FatFileSystem.cs index 5812e81d0..2d2a98cc4 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,61 @@ public static FatFileSystem FormatPartition( stream.Position = pos; return new FatFileSystem(stream); } + internal class FatAbstractDirectory : FatAbstractRecord, IAbstractDirectory { + 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(FullPath); + foreach (var di in dir.GetDirectories()) + yield return new FatAbstractDirectory(fs,di,Path.Combine(FullPath,di.Name.FullName)); + foreach (var fi in dir.GetFiles()) + yield return new FatAbstractRecord(fs,fi, Path.Combine(FullPath,fi.Name.FullName)); + } + } + } + internal class FatAbstractRecord : IAbstractRecord { + protected FatFileSystem fs; + protected 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 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(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); + 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,path); + return new FatAbstractRecord(this,dirEntry,path); + } + 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.Iso9660/VfsCDReader.cs b/Library/DiscUtils.Iso9660/VfsCDReader.cs index 4713a0512..a03491f35 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.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.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..de1faca10 --- /dev/null +++ b/Library/DiscUtils.Ntfs/NtfsAbstractRecord.cs @@ -0,0 +1,52 @@ +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 => Record.FileName; + public bool IsDirectory => Record.Flags.HasFlag(NtfsFileAttributes.Directory); + 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; + 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; + public String FullPath => FilePath; + 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..fbdf19cf9 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; @@ -1424,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); } } @@ -2663,6 +2665,25 @@ 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"); + if (dirEntry is not NtfsAbstractRecord ntfsDirEntry) + throw new ArgumentException($"dirEntry is not an NtfsAbstractRecord"); + + + var reparsePoint = GetReparsePoint(ntfsDirEntry.FullPath); + if (reparsePoint == null) + throw new IOException($"Unable to read reparse point for {ntfsDirEntry.FullPath}"); + + return reparsePoint.ParseSymlink(ntfsDirEntry.FullPath); + } /// /// 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.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.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; 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.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..e62f13b23 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,13 +110,15 @@ 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); var buffer = s.ReadExactly((int)s.Length); - return new ReparsePoint((int)dirEntry.ReparseTag, buffer); + return new ReparsePoint(dirEntry.ReparseTag, buffer); } /// @@ -744,4 +747,44 @@ 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 => Record.FileName; + public string FullPath => Path; + public bool IsDirectory => Record.Attributes.HasFlag(FileAttributes.Directory); + 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(Path, 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"); + if (dirEntry is not WimAbstractRecord dirEntryWim) + throw new ArgumentException($"dirEntry is not a WimAbstractRecord"); + + var reparsePoint = GetReparsePoint(dirEntryWim.FullPath); + if (reparsePoint == null) + throw new IOException($"Unable to read reparse point for {dirEntryWim.FullPath}"); + + return reparsePoint.ParseSymlink(dirEntryWim.FullPath); + } } 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) {