Files
romrepomgr/RomRepoMgr.Core/Filesystem/Winfsp.cs

687 lines
24 KiB
C#

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Security.AccessControl;
using Fsp;
using Fsp.Interop;
using RomRepoMgr.Database.Models;
using FileInfo = Fsp.Interop.FileInfo;
namespace RomRepoMgr.Core.Filesystem;
[SupportedOSPlatform("windows")]
public class Winfsp(Vfs vfs) : FileSystemBase
{
FileSystemHost _host;
public static bool IsAvailable
{
get
{
try
{
Version winfspVersion = FileSystemHost.Version();
if(winfspVersion == null) return false;
return winfspVersion.Major == 1 && winfspVersion.Minor >= 7;
}
catch(Exception)
{
return false;
}
}
}
internal bool Mount(string mountPoint)
{
_host = new FileSystemHost(this)
{
SectorSize = 512,
CasePreservedNames = true,
CaseSensitiveSearch = true,
FileSystemName = "romrepomgrfs",
MaxComponentLength = 255,
UnicodeOnDisk = true,
SectorsPerAllocationUnit = 1
};
if(Directory.Exists(mountPoint)) Directory.Delete(mountPoint);
int ret = _host.Mount(mountPoint);
if(ret == STATUS_SUCCESS) return true;
_host = null;
return false;
}
internal void Umount() => _host?.Unmount();
public override int SetVolumeLabel(string volumeLabel, out VolumeInfo volumeInfo)
{
volumeInfo = default(VolumeInfo);
return STATUS_MEDIA_WRITE_PROTECTED;
}
public override int Create(string fileName, uint createOptions, uint grantedAccess, uint fileAttributes,
byte[] securityDescriptor, ulong allocationSize, out object fileNode,
out object fileDesc, out FileInfo fileInfo, out string normalizedName)
{
fileNode = default(object);
fileDesc = default(object);
fileInfo = default(FileInfo);
normalizedName = default(string);
return STATUS_MEDIA_WRITE_PROTECTED;
}
public override int Overwrite(object fileNode, object fileDesc, uint fileAttributes, bool replaceFileAttributes,
ulong allocationSize, out FileInfo fileInfo)
{
fileInfo = default(FileInfo);
return STATUS_MEDIA_WRITE_PROTECTED;
}
public override int Write(object fileNode, object fileDesc, IntPtr buffer, ulong offset, uint length,
bool writeToEndOfFile, bool constrainedIo, out uint bytesTransferred,
out FileInfo fileInfo)
{
bytesTransferred = default(uint);
fileInfo = default(FileInfo);
return STATUS_MEDIA_WRITE_PROTECTED;
}
public override int SetBasicInfo(object fileNode, object fileDesc, uint fileAttributes, ulong creationTime,
ulong lastAccessTime, ulong lastWriteTime, ulong changeTime, out FileInfo fileInfo)
{
fileInfo = default(FileInfo);
return STATUS_MEDIA_WRITE_PROTECTED;
}
public override int SetFileSize(object fileNode, object fileDesc, ulong newSize, bool setAllocationSize,
out FileInfo fileInfo)
{
fileInfo = default(FileInfo);
return STATUS_MEDIA_WRITE_PROTECTED;
}
public override int CanDelete(object fileNode, object fileDesc, string fileName) => STATUS_MEDIA_WRITE_PROTECTED;
public override int Rename(object fileNode, object fileDesc, string fileName, string newFileName,
bool replaceIfExists) => STATUS_MEDIA_WRITE_PROTECTED;
public override int GetVolumeInfo(out VolumeInfo volumeInfo)
{
volumeInfo = new VolumeInfo();
vfs.GetInfo(out _, out ulong totalSize);
volumeInfo.FreeSize = 0;
volumeInfo.TotalSize = totalSize;
return STATUS_SUCCESS;
}
public override int Open(string fileName, uint createOptions, uint grantedAccess, out object fileNode,
out object fileDesc, out FileInfo fileInfo, out string normalizedName)
{
fileNode = default(object);
fileDesc = default(object);
fileInfo = default(FileInfo);
normalizedName = default(string);
string[] pieces = vfs.SplitPath(fileName);
// Root directory
if(pieces.Length == 0)
{
fileInfo = new FileInfo
{
CreationTime = (ulong)DateTime.UtcNow.ToFileTimeUtc(),
FileAttributes = (uint)(FileAttributes.Directory | FileAttributes.Compressed),
LastWriteTime = (ulong)DateTime.UtcNow.ToFileTimeUtc()
};
normalizedName = "";
fileNode = new FileNode
{
FileName = normalizedName,
IsDirectory = true,
Info = fileInfo,
Path = fileName
};
return STATUS_SUCCESS;
}
long romSetId = vfs.GetRomSetId(pieces[0]);
if(romSetId <= 0) return STATUS_OBJECT_NAME_NOT_FOUND;
RomSet romSet = vfs.GetRomSet(romSetId);
if(romSet == null) return STATUS_OBJECT_NAME_NOT_FOUND;
// ROM Set
if(pieces.Length == 1)
{
fileInfo = new FileInfo
{
CreationTime = (ulong)romSet.CreatedOn.ToUniversalTime().ToFileTimeUtc(),
FileAttributes = (uint)(FileAttributes.Directory | FileAttributes.Compressed),
LastWriteTime = (ulong)romSet.UpdatedOn.ToUniversalTime().ToFileTimeUtc()
};
normalizedName = Path.GetFileName(fileName);
fileNode = new FileNode
{
FileName = normalizedName,
IsDirectory = true,
Info = fileInfo,
Path = fileName,
RomSetId = romSet.Id,
ParentInfo = new FileInfo
{
CreationTime = (ulong)DateTime.UtcNow.ToFileTimeUtc(),
FileAttributes = (uint)(FileAttributes.Directory | FileAttributes.Compressed),
LastWriteTime = (ulong)DateTime.UtcNow.ToFileTimeUtc()
}
};
return STATUS_SUCCESS;
}
CachedMachine machine = vfs.GetMachine(romSetId, pieces[1]);
if(machine == null) return STATUS_OBJECT_NAME_NOT_FOUND;
// Machine
if(pieces.Length == 2)
{
fileInfo = new FileInfo
{
CreationTime = (ulong)machine.CreationDate.ToUniversalTime().ToFileTimeUtc(),
FileAttributes = (uint)(FileAttributes.Directory | FileAttributes.Compressed),
LastWriteTime = (ulong)machine.ModificationDate.ToUniversalTime().ToFileTimeUtc()
};
normalizedName = Path.GetFileName(fileName);
fileNode = new FileNode
{
FileName = normalizedName,
IsDirectory = true,
Info = fileInfo,
Path = fileName,
MachineId = machine.Id,
ParentInfo = new FileInfo
{
CreationTime = (ulong)romSet.CreatedOn.ToUniversalTime().ToFileTimeUtc(),
FileAttributes = (uint)(FileAttributes.Directory | FileAttributes.Compressed),
LastWriteTime = (ulong)romSet.UpdatedOn.ToUniversalTime().ToFileTimeUtc()
}
};
return STATUS_SUCCESS;
}
long handle = 0;
CachedFile file = vfs.GetFile(machine.Id, pieces[2]);
if(file != null)
{
if(pieces.Length > 3) return STATUS_INVALID_DEVICE_REQUEST;
if(file.Sha384 == null) return STATUS_OBJECT_NAME_NOT_FOUND;
handle = vfs.Open(file.Sha384, (long)file.Size);
if(handle <= 0) return STATUS_OBJECT_NAME_NOT_FOUND;
normalizedName = Path.GetFileName(fileName);
// TODO: Real allocation size
fileInfo = new FileInfo
{
ChangeTime = (ulong)file.UpdatedOn.ToFileTimeUtc(),
AllocationSize = (file.Size + 511) / 512,
FileSize = file.Size,
CreationTime = (ulong)file.CreatedOn.ToFileTimeUtc(),
FileAttributes = (uint)(FileAttributes.Normal | FileAttributes.Compressed | FileAttributes.ReadOnly),
IndexNumber = file.Id,
LastAccessTime = (ulong)DateTime.UtcNow.ToFileTimeUtc(),
LastWriteTime = (ulong)(file.FileLastModification?.ToFileTimeUtc() ?? file.UpdatedOn.ToFileTimeUtc())
};
fileNode = new FileNode
{
FileName = normalizedName,
Info = fileInfo,
Path = fileName,
Handle = handle
};
return STATUS_SUCCESS;
}
CachedDisk disk = vfs.GetDisk(machine.Id, pieces[2]);
if(disk != null)
{
if(pieces.Length > 3) return STATUS_INVALID_DEVICE_REQUEST;
if(disk.Sha1 == null && disk.Md5 == null) return STATUS_OBJECT_NAME_NOT_FOUND;
handle = vfs.OpenDisk(disk.Sha1, disk.Md5);
if(handle <= 0) return STATUS_OBJECT_NAME_NOT_FOUND;
normalizedName = Path.GetFileName(fileName);
// TODO: Real allocation size
fileInfo = new FileInfo
{
ChangeTime = (ulong)disk.UpdatedOn.ToFileTimeUtc(),
AllocationSize = (disk.Size + 511) / 512,
FileSize = disk.Size,
CreationTime = (ulong)disk.CreatedOn.ToFileTimeUtc(),
FileAttributes = (uint)(FileAttributes.Normal | FileAttributes.Compressed | FileAttributes.ReadOnly),
IndexNumber = disk.Id,
LastAccessTime = (ulong)DateTime.UtcNow.ToFileTimeUtc(),
LastWriteTime = (ulong)disk.UpdatedOn.ToFileTimeUtc()
};
fileNode = new FileNode
{
FileName = normalizedName,
Info = fileInfo,
Path = fileName,
Handle = handle
};
return STATUS_SUCCESS;
}
CachedMedia media = vfs.GetMedia(machine.Id, pieces[2]);
if(media == null) return STATUS_OBJECT_NAME_NOT_FOUND;
if(pieces.Length > 3) return STATUS_INVALID_DEVICE_REQUEST;
if(media.Sha256 == null && media.Sha1 == null && media.Md5 == null) return STATUS_OBJECT_NAME_NOT_FOUND;
handle = vfs.OpenMedia(media.Sha256, media.Sha1, media.Md5);
if(handle <= 0) return STATUS_OBJECT_NAME_NOT_FOUND;
normalizedName = Path.GetFileName(fileName);
// TODO: Real allocation size
fileInfo = new FileInfo
{
ChangeTime = (ulong)media.UpdatedOn.ToFileTimeUtc(),
AllocationSize = (media.Size + 511) / 512,
FileSize = media.Size,
CreationTime = (ulong)media.CreatedOn.ToFileTimeUtc(),
FileAttributes = (uint)(FileAttributes.Normal | FileAttributes.Compressed | FileAttributes.ReadOnly),
IndexNumber = media.Id,
LastAccessTime = (ulong)DateTime.UtcNow.ToFileTimeUtc(),
LastWriteTime = (ulong)media.UpdatedOn.ToFileTimeUtc()
};
fileNode = new FileNode
{
FileName = normalizedName,
Info = fileInfo,
Path = fileName,
Handle = handle
};
return STATUS_SUCCESS;
}
public override void Close(object fileNode, object fileDesc)
{
if(fileNode is not FileNode node) return;
if(node.Handle <= 0) return;
vfs.Close(node.Handle);
}
public override int Read(object fileNode, object fileDesc, IntPtr buffer, ulong offset, uint length,
out uint bytesTransferred)
{
bytesTransferred = 0;
if(fileNode is not FileNode { Handle: > 0 } node) return STATUS_INVALID_HANDLE;
byte[] buf = new byte[length];
int ret = vfs.Read(node.Handle, buf, (long)offset);
if(ret < 0) return STATUS_INVALID_HANDLE;
Marshal.Copy(buf, 0, buffer, ret);
bytesTransferred = (uint)ret;
return STATUS_SUCCESS;
}
public override int GetFileInfo(object fileNode, object fileDesc, out FileInfo fileInfo)
{
fileInfo = default(FileInfo);
if(fileNode is not FileNode node) return STATUS_INVALID_HANDLE;
fileInfo = node.Info;
return STATUS_SUCCESS;
}
public override bool ReadDirectoryEntry(object fileNode, object fileDesc, string pattern, string marker,
ref object context, out string fileName, out FileInfo fileInfo)
{
fileName = default(string);
fileInfo = default(FileInfo);
if(fileNode is not FileNode { IsDirectory: true } node) return false;
if(context is not IEnumerator<FileEntry> enumerator)
{
if(node.MachineId > 0)
{
ConcurrentDictionary<string, CachedFile> cachedMachineFiles = vfs.GetFilesFromMachine(node.MachineId);
ConcurrentDictionary<string, CachedDisk> cachedMachineDisks = vfs.GetDisksFromMachine(node.MachineId);
ConcurrentDictionary<string, CachedMedia> cachedMachineMedias =
vfs.GetMediasFromMachine(node.MachineId);
node.Children =
[
new FileEntry
{
FileName = ".",
Info = node.Info
},
new FileEntry
{
FileName = "..",
Info = node.ParentInfo
}
];
node.Children.AddRange(cachedMachineFiles.Select(file => new FileEntry
{
FileName = file.Key,
Info = new FileInfo
{
ChangeTime = (ulong)file.Value.UpdatedOn.ToFileTimeUtc(),
AllocationSize = (file.Value.Size + 511) / 512,
FileSize = file.Value.Size,
CreationTime = (ulong)file.Value.CreatedOn.ToFileTimeUtc(),
FileAttributes =
(uint)(FileAttributes.Normal | FileAttributes.Compressed | FileAttributes.ReadOnly),
IndexNumber = file.Value.Id,
LastAccessTime = (ulong)DateTime.UtcNow.ToFileTimeUtc(),
LastWriteTime = (ulong)(file.Value.FileLastModification?.ToFileTimeUtc() ??
file.Value.UpdatedOn.ToFileTimeUtc())
}
}));
node.Children.AddRange(cachedMachineDisks.Select(disk => new FileEntry
{
FileName = disk.Key + ".chd",
Info = new FileInfo
{
ChangeTime = (ulong)disk.Value.UpdatedOn.ToFileTimeUtc(),
AllocationSize = (disk.Value.Size + 511) / 512,
FileSize = disk.Value.Size,
CreationTime = (ulong)disk.Value.CreatedOn.ToFileTimeUtc(),
FileAttributes =
(uint)(FileAttributes.Normal | FileAttributes.Compressed | FileAttributes.ReadOnly),
IndexNumber = disk.Value.Id,
LastAccessTime = (ulong)DateTime.UtcNow.ToFileTimeUtc(),
LastWriteTime = (ulong)disk.Value.UpdatedOn.ToFileTimeUtc()
}
}));
node.Children.AddRange(cachedMachineMedias.Select(media => new FileEntry
{
FileName = media.Key + ".aif",
Info = new FileInfo
{
ChangeTime = (ulong)media.Value.UpdatedOn.ToFileTimeUtc(),
AllocationSize = (media.Value.Size + 511) / 512,
FileSize = media.Value.Size,
CreationTime = (ulong)media.Value.CreatedOn.ToFileTimeUtc(),
FileAttributes =
(uint)(FileAttributes.Normal | FileAttributes.Compressed | FileAttributes.ReadOnly),
IndexNumber = media.Value.Id,
LastAccessTime = (ulong)DateTime.UtcNow.ToFileTimeUtc(),
LastWriteTime = (ulong)media.Value.UpdatedOn.ToFileTimeUtc()
}
}));
}
else if(node.RomSetId > 0)
{
ConcurrentDictionary<string, CachedMachine> machines = vfs.GetMachinesFromRomSet(node.RomSetId);
node.Children =
[
new FileEntry
{
FileName = ".",
Info = node.Info
},
new FileEntry
{
FileName = "..",
Info = node.ParentInfo
}
];
node.Children.AddRange(machines.Select(machine => new FileEntry
{
FileName = machine.Key,
Info = new FileInfo
{
CreationTime = (ulong)machine.Value.CreationDate.ToUniversalTime().ToFileTimeUtc(),
FileAttributes = (uint)(FileAttributes.Directory | FileAttributes.Compressed),
LastWriteTime = (ulong)machine.Value.ModificationDate.ToUniversalTime().ToFileTimeUtc()
}
}));
}
else
{
node.Children = [];
node.Children.AddRange(vfs.GetRootEntries()
.Select(e => new FileEntry
{
FileName = e,
IsRomSet = true
}));
}
if(marker != null)
{
int idx = node.Children.FindIndex(f => f.FileName == marker);
if(idx >= 0) node.Children.RemoveRange(0, idx + 1);
}
context = enumerator = node.Children.GetEnumerator();
}
while(enumerator.MoveNext())
{
FileEntry entry = enumerator.Current;
if(entry is null) continue;
if(entry.IsRomSet)
{
long romSetId = vfs.GetRomSetId(entry.FileName);
if(romSetId <= 0) continue;
RomSet romSet = vfs.GetRomSet(romSetId);
if(romSet is null) continue;
entry.Info = new FileInfo
{
CreationTime = (ulong)romSet.CreatedOn.ToUniversalTime().ToFileTimeUtc(),
FileAttributes = (uint)(FileAttributes.Directory | FileAttributes.Compressed),
LastWriteTime = (ulong)romSet.UpdatedOn.ToUniversalTime().ToFileTimeUtc()
};
}
fileName = entry.FileName;
fileInfo = entry.Info;
return true;
}
return false;
}
public override int GetSecurityByName(string fileName, out uint fileAttributes, ref byte[] securityDescriptor)
{
fileAttributes = 0;
string[] pieces = vfs.SplitPath(fileName);
// Root directory
if(pieces.Length == 0)
{
fileAttributes = (uint)(FileAttributes.Directory | FileAttributes.Compressed);
if(securityDescriptor == null) return STATUS_SUCCESS;
string rootSddl = "O:BAG:BAD:P(A;;FA;;;SY)(A;;FA;;;BA)(A;;FA;;;WD)";
var rootSecurityDescriptor = new RawSecurityDescriptor(rootSddl);
byte[] fileSecurity = new byte[rootSecurityDescriptor.BinaryLength];
rootSecurityDescriptor.GetBinaryForm(fileSecurity, 0);
securityDescriptor = fileSecurity;
return STATUS_SUCCESS;
}
long romSetId = vfs.GetRomSetId(pieces[0]);
if(romSetId <= 0) return STATUS_OBJECT_NAME_NOT_FOUND;
RomSet romSet = vfs.GetRomSet(romSetId);
if(romSet == null) return STATUS_OBJECT_NAME_NOT_FOUND;
// ROM Set
if(pieces.Length == 1)
{
fileAttributes = (uint)(FileAttributes.Directory | FileAttributes.Compressed);
return STATUS_SUCCESS;
}
CachedMachine machine = vfs.GetMachine(romSetId, pieces[1]);
if(machine == null) return STATUS_OBJECT_NAME_NOT_FOUND;
// Machine
if(pieces.Length == 2)
{
fileAttributes = (uint)(FileAttributes.Directory | FileAttributes.Compressed);
return STATUS_SUCCESS;
}
long handle = 0;
CachedFile file = vfs.GetFile(machine.Id, pieces[2]);
if(file != null)
{
if(pieces.Length > 3) return STATUS_INVALID_DEVICE_REQUEST;
if(file.Sha384 == null) return STATUS_OBJECT_NAME_NOT_FOUND;
handle = vfs.Open(file.Sha384, (long)file.Size);
if(handle <= 0) return STATUS_OBJECT_NAME_NOT_FOUND;
fileAttributes = (uint)(FileAttributes.Normal | FileAttributes.Compressed | FileAttributes.ReadOnly);
return STATUS_SUCCESS;
}
CachedDisk disk = vfs.GetDisk(machine.Id, pieces[2]);
if(disk != null)
{
if(pieces.Length > 3) return STATUS_INVALID_DEVICE_REQUEST;
if(disk.Sha1 == null && disk.Md5 == null) return STATUS_OBJECT_NAME_NOT_FOUND;
handle = vfs.OpenDisk(disk.Sha1, disk.Md5);
if(handle <= 0) return STATUS_OBJECT_NAME_NOT_FOUND;
fileAttributes = (uint)(FileAttributes.Normal | FileAttributes.Compressed | FileAttributes.ReadOnly);
return STATUS_SUCCESS;
}
CachedMedia media = vfs.GetMedia(machine.Id, pieces[2]);
if(media == null) return STATUS_OBJECT_NAME_NOT_FOUND;
if(pieces.Length > 3) return STATUS_INVALID_DEVICE_REQUEST;
if(media.Sha256 == null && media.Sha1 == null && media.Md5 == null) return STATUS_OBJECT_NAME_NOT_FOUND;
handle = vfs.OpenMedia(media.Sha256, media.Sha1, media.Md5);
if(handle <= 0) return STATUS_OBJECT_NAME_NOT_FOUND;
fileAttributes = (uint)(FileAttributes.Normal | FileAttributes.Compressed | FileAttributes.ReadOnly);
return STATUS_SUCCESS;
}
sealed class FileEntry
{
public string FileName { get; set; }
public FileInfo Info { get; set; }
public bool IsRomSet { get; set; }
}
sealed class FileNode
{
public FileInfo Info { get; set; }
public FileInfo ParentInfo { get; set; }
public string FileName { get; set; }
public string Path { get; set; }
public long Handle { get; set; }
public List<FileEntry> Children { get; set; }
public bool IsDirectory { get; set; }
public long RomSetId { get; set; }
public ulong MachineId { get; set; }
}
}