using System.Text; using DiscUtils; using DiscUtils.ExFat; using DiscUtils.Fat; using DiscUtils.Ntfs; using XFESystem.Core; namespace XFESystem.Storage; public sealed class MountedVolume : IDisposable { const int MaxFileBytes=4*1024*1024; readonly IPhysicalDisk disk; readonly DiskPartition partition; readonly string kind; readonly VolumeDirtyState? dirtyState; DiscFileSystem? filesystem; public string Path {get;} public string FileSystem=>kind; public string Label {get;} public bool Writable {get;private set;} public string ReadOnlyReason {get;private set;}=""; public MountedVolume(IPhysicalDisk disk,DiskPartition partition,string path) { this.disk=disk;this.partition=partition;Path=SystemPaths.Normalize(path); byte[] boot=new byte[512];disk.Read(partition.Start,boot); kind=boot.AsSpan(3,8).SequenceEqual("NTFS "u8)?"NTFS":boot.AsSpan(3,8).SequenceEqual("EXFAT "u8)?"exFAT":boot.AsSpan(3,8).SequenceEqual("-FVE-FS-"u8)?throw new IOException("BitLocker volume is locked."):"FAT"; filesystem=Open(false);Label=filesystem.VolumeLabel??""; Writable=disk.CanWrite&&partition.DataPartition; if(!Writable)ReadOnlyReason="Protected partition or read-only device"; if(Writable) { try { dirtyState=VolumeDirtyState.Create(new PartitionStream(disk,partition,true),kind,filesystem as NtfsFileSystem); if(dirtyState.IsDirty){Writable=false;ReadOnlyReason="Filesystem requires recovery in its original operating system";} if(filesystem is NtfsFileSystem ntfs) { if((ntfs.VolumeInfo.Flags&VolumeInformationFlags.Dirty)!=0) {Writable=false;ReadOnlyReason="NTFS requires recovery in Windows before writing";} else if(Hibernated(ntfs)) {Writable=false;ReadOnlyReason="Windows is hibernated; fully shut down Windows before writing";} } } catch(IOException error){Writable=false;ReadOnlyReason=error.Message;} } } DiscFileSystem Open(bool write) { Stream stream=new PartitionStream(disk,partition,write); // The exFAT library otherwise inherits host Path separators. Our guest // uses a Linux-ABI BCL while the workspace API always uses backslashes. DiscFileSystem fs=kind switch {"NTFS"=>new NtfsFileSystem(stream),"exFAT"=>new ExFatFileSystem(stream,['\\','/']),_=>new FatFileSystem(stream)}; if(fs is NtfsFileSystem ntfs){ntfs.NtfsOptions.HideHiddenFiles=false;ntfs.NtfsOptions.HideSystemFiles=false;ntfs.NtfsOptions.HideMetafiles=true;} return fs; } static bool Hibernated(NtfsFileSystem fs) { if(!fs.FileExists("hiberfil.sys"))return false; using var file=fs.OpenFile("hiberfil.sys",FileMode.Open,FileAccess.Read);Span header=stackalloc byte[4]; if(file.Read(header)!=4)return true; return System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian(header)!=0; } DiscFileSystem Fs=>filesystem??=Open(false); string Relative(string path) { path=SystemPaths.Normalize(path); if(path.Equals(Path,StringComparison.OrdinalIgnoreCase))return ""; if(!path.StartsWith(Path+"\\",StringComparison.OrdinalIgnoreCase))throw new IOException("Path leaves its mounted volume."); string relative=path[(Path.Length+1)..];string current=""; foreach(string part in relative.Split('\\')) { if(part.StartsWith('$'))throw new UnauthorizedAccessException("Filesystem metadata is protected."); current=current.Length==0?part:current+"\\"+part; if(Fs.Exists(current)&&(Fs.GetAttributes(current)&FileAttributes.ReparsePoint)!=0)throw new IOException("Reparse points are not followed by this system."); } return relative; } public bool Exists(string path){string relative=Relative(path);return relative.Length==0||Fs.Exists(relative);} public WorkspaceEntry[] List(string path) { string relative=Relative(path); return Fs.GetFileSystemEntries(relative).Take(32769).Select(name=> { string clean=name.TrimStart('\\');string full=Path+"\\"+clean; var attributes=Fs.GetAttributes(clean);bool directory=(attributes&FileAttributes.Directory)!=0; return new WorkspaceEntry(full,directory,directory?0:Fs.GetFileLength(clean)); }).OrderBy(e=>!e.IsDirectory).ThenBy(e=>e.Name,StringComparer.OrdinalIgnoreCase).ToArray() is var entries&&entries.Length<=32768?entries:throw new IOException("Directory exceeds the supported entry count."); } public byte[] ReadBytes(string path) { string relative=Relative(path);long length=Fs.GetFileLength(relative); if(length<0||length>MaxFileBytes)throw new IOException("The current file read limit is 4 MiB."); using var file=Fs.OpenFile(relative,FileMode.Open,FileAccess.Read);byte[] bytes=new byte[(int)length];file.ReadExactly(bytes);return bytes; } public string ReadText(string path) {using var reader=new StreamReader(new MemoryStream(ReadBytes(path),false),new UTF8Encoding(false,true),true);return reader.ReadToEnd();} void Mutate(Action action) { if(!Writable||dirtyState==null)throw new UnauthorizedAccessException("Read-only volume: "+ReadOnlyReason); filesystem?.Dispose();filesystem=null; try { dirtyState.Set(true); using(var fs=Open(true))action(fs); disk.Flush();dirtyState.Set(false); } catch {Writable=false;ReadOnlyReason="A filesystem operation failed; the dirty flag was retained for recovery";throw;} } public void WriteBytes(string path,ReadOnlySpan content) { string relative=Relative(path);if(relative.Length==0||content.Length>MaxFileBytes)throw new IOException("Invalid file path or size."); int separator=relative.LastIndexOf('\\');string parent=separator<0?"":relative[..separator]; if(!Fs.DirectoryExists(parent))throw new DirectoryNotFoundException(parent); byte[] bytes=content.ToArray();Mutate(fs=>{using var file=fs.OpenFile(relative,FileMode.Create,FileAccess.Write);file.Write(bytes);file.Flush();}); } public void WriteText(string path,string text)=>WriteBytes(path,Encoding.UTF8.GetBytes(text)); public void CreateDirectory(string path) {string relative=Relative(path);if(relative.Length==0||Fs.DirectoryExists(relative))return;Mutate(fs=>fs.CreateDirectory(relative));} public void Delete(string path) { string relative=Relative(path);if(relative.Length==0)throw new IOException("A volume root cannot be deleted."); bool directory=Fs.DirectoryExists(relative);if(!directory&&!Fs.FileExists(relative))throw new FileNotFoundException(path); Mutate(fs=>{if(directory)fs.DeleteDirectory(relative,true);else fs.DeleteFile(relative);}); } public void Move(string source,string destination) { string from=Relative(source),to=Relative(destination); if(from.Length==0||to.Length==0)throw new IOException("A volume root cannot be moved."); bool directory=Fs.DirectoryExists(from);if(!directory&&!Fs.FileExists(from))throw new FileNotFoundException(source); if(Fs.Exists(to))throw new IOException("Destination already exists."); Mutate(fs=>{if(directory)fs.MoveDirectory(from,to);else fs.MoveFile(from,to);}); } public void Dispose(){filesystem?.Dispose();filesystem=null;} }