XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFESystem

【Rust】XFE系统,基于Rust和汇编

公开
关注 0 Fork 0 Star 0
UTF-8
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<byte> 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<DiscFileSystem> 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<byte> 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;}
}