Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Library/DiscUtils.Btrfs/BtrfsFileSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ public BtrfsFileSystem(Stream stream)
: base(new VfsBtrfsFileSystem(stream))
{
}

public override IAbstractRecord GetAbstractRecord(string path) => GetRealFileSystem<VfsBtrfsFileSystem>().GetAbstractRecord(path);
public override string GetSymlinkTarget(IAbstractRecord dirEntry) => GetRealFileSystem<VfsBtrfsFileSystem>().GetSymlinkTarget(dirEntry);
/// <summary>
/// Initializes a new instance of the BtrfsFileSystem class.
/// </summary>
Expand Down
6 changes: 6 additions & 0 deletions Library/DiscUtils.Btrfs/VfsBtrfsFileSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,13 @@ public IEnumerable<Subvolume> 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)
Expand Down
4 changes: 4 additions & 0 deletions Library/DiscUtils.Core/DiscFileSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
using System.IO;
using System.Linq;
using DiscUtils.Streams;
using DiscUtils.Vfs;

namespace DiscUtils;

Expand Down Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions Library/DiscUtils.Core/IFileSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -370,4 +370,7 @@ public interface IFileSystem
/// UsedSpace and Size properties
/// </summary>
bool SupportsUsedAvailableSpace { get; }

Vfs.IAbstractRecord GetAbstractRecord(string path);
string GetSymlinkTarget(Vfs.IAbstractRecord dirEntry);
}
78 changes: 78 additions & 0 deletions Library/DiscUtils.Core/NativeFileSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
using System.Linq;
using DiscUtils.Internal;
using DiscUtils.Streams;
using DiscUtils.Vfs;

namespace DiscUtils;

Expand Down Expand Up @@ -782,4 +783,81 @@
{
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<IAbstractRecord> 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;

Check failure on line 826 in Library/DiscUtils.Core/NativeFileSystem.cs

View workflow job for this annotation

GitHub Actions / build

'FileSystemInfo' does not contain a definition for 'LinkTarget' and no accessible extension method 'LinkTarget' accepting a first argument of type 'FileSystemInfo' could be found (are you missing a using directive or an assembly reference?)

Check failure on line 826 in Library/DiscUtils.Core/NativeFileSystem.cs

View workflow job for this annotation

GitHub Actions / build

'FileSystemInfo' does not contain a definition for 'LinkTarget' and no accessible extension method 'LinkTarget' accepting a first argument of type 'FileSystemInfo' could be found (are you missing a using directive or an assembly reference?)

Check failure on line 826 in Library/DiscUtils.Core/NativeFileSystem.cs

View workflow job for this annotation

GitHub Actions / build

'FileSystemInfo' does not contain a definition for 'LinkTarget' and no accessible extension method 'LinkTarget' accepting a first argument of type 'FileSystemInfo' could be found (are you missing a using directive or an assembly reference?)
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;

Check failure on line 855 in Library/DiscUtils.Core/NativeFileSystem.cs

View workflow job for this annotation

GitHub Actions / build

'Directory' does not contain a definition for 'ResolveLinkTarget'

Check failure on line 855 in Library/DiscUtils.Core/NativeFileSystem.cs

View workflow job for this annotation

GitHub Actions / build

'Directory' does not contain a definition for 'ResolveLinkTarget'

Check failure on line 855 in Library/DiscUtils.Core/NativeFileSystem.cs

View workflow job for this annotation

GitHub Actions / build

'Directory' does not contain a definition for 'ResolveLinkTarget'
else {
target = File.ResolveLinkTarget(ourPath, false).FullName;

Check failure on line 857 in Library/DiscUtils.Core/NativeFileSystem.cs

View workflow job for this annotation

GitHub Actions / build

'File' does not contain a definition for 'ResolveLinkTarget'

Check failure on line 857 in Library/DiscUtils.Core/NativeFileSystem.cs

View workflow job for this annotation

GitHub Actions / build

'File' does not contain a definition for 'ResolveLinkTarget'

Check failure on line 857 in Library/DiscUtils.Core/NativeFileSystem.cs

View workflow job for this annotation

GitHub Actions / build

'File' does not contain a definition for 'ResolveLinkTarget'
}
if (target.StartsWith(BasePath, StringComparison.CurrentCultureIgnoreCase) == false)
return null;
return CleanItems(target);
}
}
53 changes: 50 additions & 3 deletions Library/DiscUtils.Core/ReparsePoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
// DEALINGS IN THE SOFTWARE.
//

using System;
using System.IO;
using System.Text;

namespace DiscUtils;

/// <summary>
Expand All @@ -32,7 +36,7 @@ public sealed class ReparsePoint
/// </summary>
/// <param name="tag">The defined reparse point tag.</param>
/// <param name="content">The reparse point's content.</param>
public ReparsePoint(int tag, byte[] content)
public ReparsePoint(uint tag, byte[] content)
{
Tag = tag;
Content = content;
Expand All @@ -46,5 +50,48 @@ public ReparsePoint(int tag, byte[] content)
/// <summary>
/// Gets or sets the defined reparse point tag.
/// </summary>
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 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;
}
}
29 changes: 29 additions & 0 deletions Library/DiscUtils.Core/Vfs/IAbstractRecord.cs
Original file line number Diff line number Diff line change
@@ -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; }
/// <summary>
/// the SubPath relative to the parent IAbstractDirectory
/// </summary>
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<IAbstractRecord> AllEntries { get; }
}
33 changes: 33 additions & 0 deletions Library/DiscUtils.Core/Vfs/VfsAbstractRecord.cs
Original file line number Diff line number Diff line change
@@ -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;
}
1 change: 1 addition & 0 deletions Library/DiscUtils.Core/Vfs/VfsDirEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ namespace DiscUtils.Vfs;
/// </remarks>
public abstract class VfsDirEntry
{
public static bool NO_SYMLINK_RESOLUTION;
/// <summary>
/// Gets the creation time of the file or directory.
/// </summary>
Expand Down
64 changes: 62 additions & 2 deletions Library/DiscUtils.Core/Vfs/VfsFileSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<TDirEntry, TFile, TDirectory, TContext> fs, VfsDirEntry DirEntry, TDirectory Directory) : VfsAbstractRecord(DirEntry, Directory), IAbstractDirectory {
public IEnumerable<IAbstractRecord> 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;
Expand Down Expand Up @@ -756,6 +800,9 @@ protected TFile GetFile(string path)
return GetFile(dirEntry);
}


protected abstract TDirectory ConvertDirEntryToDirectory(TDirEntry dirEntry);// => throw new NotImplementedException();

/// <summary>
/// Converts a directory entry to an object representing a file.
/// </summary>
Expand Down Expand Up @@ -928,7 +975,20 @@ private IEnumerable<string> DoSearch(string path, Func<string, bool> 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<TDirEntry, TFile>) {
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<TDirEntry, TFile> 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;
Expand All @@ -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<TDirEntry, TFile> symlink)
if (VfsDirEntry.NO_SYMLINK_RESOLUTION || GetFile(currentEntry) is not IVfsSymlink<TDirEntry, TFile> symlink)
{
Trace.WriteLine($"Unable to resolve symlink '{path}'");
return default;
Expand Down
2 changes: 2 additions & 0 deletions Library/DiscUtils.Ext/ExtFileSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,4 +108,6 @@ internal static bool Detect(Stream stream)

return superblock.Magic == SuperBlock.Ext2Magic;
}
public override IAbstractRecord GetAbstractRecord(string path) => GetRealFileSystem<VfsExtFileSystem>().GetAbstractRecord(path);
public override string GetSymlinkTarget(IAbstractRecord dirEntry) => GetRealFileSystem<VfsExtFileSystem>().GetSymlinkTarget(dirEntry);
}
7 changes: 7 additions & 0 deletions Library/DiscUtils.Ext/VfsExtFileSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,14 @@ public IEnumerable<Range<long, long>> 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);
Expand Down
Loading
Loading