using System; using System.Collections.Generic; using System.IO; using System.Linq; using BizHawk.Common.StringExtensions; // the HawkFile class is excessively engineered with the IHawkFileArchiveHandler to decouple the archive handling from the basic file handling. // This is so we could drop in an unamanged dearchiver library optionally later as a performance optimization without ruining the portability of the code. // Also, we want to be able to use HawkFiles in BizHawk.Common withuot bringing in a large 7-zip dependency namespace BizHawk.Common { // TODO: // split into "bind" and "open (the bound thing)" // scan archive to flatten interior directories down to a path (maintain our own archive item list) /// /// Bridge between HawkFile and the frontend's implementation of archive management /// public interface IHawkFileArchiveHandler : IDisposable { // TODO - could this receive a hawkfile itself? possibly handy, in very clever scenarios of mounting fake files bool CheckSignature(string fileName, out int offset, out bool isExecutable); List Scan(); IHawkFileArchiveHandler Construct(string path); void ExtractFile(int index, Stream stream); } /// /// HawkFile allows a variety of objects (actual files, archive members) to be treated as normal filesystem objects to be opened, closed, and read. /// It can understand paths in 'canonical' format which includes /path/to/archive.zip|member.rom as well as /path/to/file.rom /// When opening an archive, it won't always be clear automatically which member should actually be used. /// Therefore there is a concept of 'binding' where a HawkFile attaches itself to an archive member which is the file that it will actually be using. /// public sealed class HawkFile : IDisposable { private bool _exists; private bool _rootExists; private string _rootPath; private string _memberPath; private Stream _rootStream, _boundStream; private IHawkFileArchiveHandler _extractor; private bool _isArchive; private List _archiveItems; private int? _boundIndex; public HawkFile() { } /// /// Set this with an instance which can construct archive handlers as necessary for archive handling. /// public static IHawkFileArchiveHandler ArchiveHandlerFactory { get; set; } /// /// Gets a value indicating whether a bound file exists. if there is no bound file, it can't exist. /// NOTE: this isn't set until the file is Opened. Not too great... /// public bool Exists => _exists; /// /// Gets the directory containing the root /// public string Directory => Path.GetDirectoryName(_rootPath); /// /// Gets a value indicating whether this instance is bound /// public bool IsBound => _boundStream != null; /// /// returns the complete canonical full path ("c:\path\to\archive|member") of the bound file /// public string CanonicalFullPath => MakeCanonicalName(_rootPath, _memberPath); /// /// returns the complete canonical name ("archive|member") of the bound file /// public string CanonicalName => MakeCanonicalName(Path.GetFileName(_rootPath), _memberPath); /// /// returns the virtual name of the bound file (disregarding the archive). /// Useful as a basic content identifier. /// public string Name => GetBoundNameFromCanonical(MakeCanonicalName(_rootPath, _memberPath)); /// /// returns the complete full path of the bound file, excluding the archive member portion /// public string FullPathWithoutMember => _rootPath; /// /// returns the member path part of the bound file /// public string ArchiveMemberPath => _memberPath; /// /// returns the extension of Name /// public string Extension => Path.GetExtension(Name).ToUpper(); /// /// Indicates whether this file is an archive /// public bool IsArchive => _isArchive; /// /// Indicates whether the file is an archive member (IsArchive && IsBound[to member]) /// public bool IsArchiveMember => IsArchive && IsBound; public IList ArchiveItems { get { if (!IsArchive) { throw new InvalidOperationException("Cant get archive items from non-archive"); } return _archiveItems; } } /// /// returns a stream for the currently bound file /// public Stream GetStream() { if (_boundStream == null) { throw new InvalidOperationException("HawkFile: Can't call GetStream() before youve successfully bound something!"); } return _boundStream; } public int? GetBoundIndex() { return _boundIndex; } /// /// Utility: Uses full HawkFile processing to determine whether a file exists at the provided path /// public static bool ExistsAt(string path) { using (var file = new HawkFile(path)) { return file.Exists; } } /// /// Utility: attempts to read all the content from the provided path. /// public static byte[] ReadAllBytes(string path) { using (var file = new HawkFile(path)) { if (!file.Exists) { throw new FileNotFoundException(path); } using (Stream stream = file.GetStream()) { var ms = new MemoryStream((int)stream.Length); stream.CopyTo(ms); return ms.GetBuffer(); } } } /// /// attempts to read all the content from the file /// public byte[] ReadAllBytes() { using (Stream stream = GetStream()) { var ms = new MemoryStream((int)stream.Length); stream.CopyTo(ms); return ms.GetBuffer(); } } /// /// these extensions won't even be tried as archives (removes spurious archive detects since some of the signatures are pretty damn weak) /// public string[] NonArchiveExtensions = { ".smc", ".sfc", ".dll" }; /// /// Parses the given filename to create an un-opened HawkFile with some information available about its path constitution /// public void Parse(string path) { bool isArchivePath = IsCanonicalArchivePath(path); if (isArchivePath) { var parts = path.Split('|'); path = parts[0]; _memberPath = parts[1]; // we're gonna assume, on parsing, that this is _isArchive = true; } _rootPath = path; } /// /// Parses the given filename and then opens it. This may take a while (the archive may be accessed and scanned). /// public void Open(string path) { if (_rootPath != null) { throw new InvalidOperationException("Don't reopen a HawkFile."); } string autobind = null; bool isArchivePath = IsCanonicalArchivePath(path); if (isArchivePath) { var parts = path.Split('|'); path = parts[0]; autobind = parts[1]; } var fi = new FileInfo(path); _rootExists = fi.Exists; if (fi.Exists == false) { return; } _rootPath = path; _exists = true; AnalyzeArchive(path); if (_extractor == null) { _rootStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read); // we could autobind here, but i dont want to // bind it later with the desired extensions. } if (autobind == null) { // non-archive files can be automatically bound this way if (!isArchivePath) { BindRoot(); } } else { autobind = autobind.ToUpperInvariant(); if (_extractor != null) { var scanResults = _extractor.Scan(); for (int i = 0; i < scanResults.Count; i++) { if (scanResults[i].Name.ToUpperInvariant() == autobind) { BindArchiveMember(i); return; } } } _exists = false; } } /// /// Makes a new HawkFile based on the provided path. /// public HawkFile(string path) { Open(path); } /// /// binds the specified ArchiveItem which you should have gotten by interrogating an archive hawkfile /// public HawkFile BindArchiveMember(HawkFileArchiveItem item) { return BindArchiveMember(item.Index); } /// /// finds an ArchiveItem with the specified name (path) within the archive; returns null if it doesnt exist /// public HawkFileArchiveItem FindArchiveMember(string name) { return ArchiveItems.FirstOrDefault(ai => ai.Name == name); } /// /// binds a path within the archive; returns null if that path didnt exist. /// public HawkFile BindArchiveMember(string name) { var ai = FindArchiveMember(name); if (ai == null) { return null; } return BindArchiveMember(ai); } /// /// binds the selected archive index /// public HawkFile BindArchiveMember(int index) { if (!_rootExists) { return this; } if (_boundStream != null) { throw new InvalidOperationException("stream already bound!"); } _boundStream = new MemoryStream(); int archiveIndex = _archiveItems[index].ArchiveIndex; _extractor.ExtractFile(archiveIndex, _boundStream); _boundStream.Position = 0; _memberPath = _archiveItems[index].Name; // TODO - maybe go through our own list of names? maybe not, its indexes dont match.. Console.WriteLine("HawkFile bound " + CanonicalFullPath); _boundIndex = archiveIndex; return this; } /// /// Removes any existing binding /// public void Unbind() { if (_boundStream != null && _boundStream != _rootStream) { _boundStream.Close(); } _boundStream = null; _memberPath = null; _boundIndex = null; } /// /// causes the root to be bound (in the case of non-archive files) /// private void BindRoot() { _boundStream = _rootStream; Console.WriteLine("HawkFile bound " + CanonicalFullPath); } /// /// Binds the first item in the archive (or the file itself). Supposing that there is anything in the archive. /// public HawkFile BindFirst() { BindFirstOf(); return this; } /// /// binds one of the supplied extensions if there is only one match in the archive /// public HawkFile BindSoleItemOf(params string[] extensions) { return BindByExtensionCore(false, extensions); } /// /// Binds the first item in the archive (or the file itself) if the extension matches one of the supplied templates. /// You probably should not use use BindSoleItemOf or the archive chooser instead /// public HawkFile BindFirstOf(params string[] extensions) { return BindByExtensionCore(true, extensions); } private HawkFile BindByExtensionCore(bool first, params string[] extensions) { if (!_rootExists) { return this; } if (_boundStream != null) { throw new InvalidOperationException("stream already bound!"); } if (_extractor == null) { // open uncompressed file var extension = Path.GetExtension(_rootPath).Substring(1).ToUpperInvariant(); if (extensions.Length == 0 || extension.In(extensions)) { BindRoot(); } return this; } var candidates = new List(); for (int i = 0; i < _archiveItems.Count; i++) { var e = _archiveItems[i]; var extension = Path.GetExtension(e.Name).ToUpperInvariant(); extension = extension.TrimStart('.'); if (extensions.Length == 0 || extension.In(extensions)) { if (first) { BindArchiveMember(i); return this; } candidates.Add(i); } } if (candidates.Count == 1) { BindArchiveMember(candidates[0]); } return this; } private void ScanArchive() { _archiveItems = _extractor.Scan(); } private void AnalyzeArchive(string path) { // no archive handler == no analysis if (ArchiveHandlerFactory == null) { return; } int offset; bool isExecutable; if (NonArchiveExtensions.Any(ext => Path.GetExtension(path).ToLower() == ext.ToLower())) { return; } if (ArchiveHandlerFactory.CheckSignature(path, out offset, out isExecutable)) { _extractor = ArchiveHandlerFactory.Construct(path); try { ScanArchive(); _isArchive = true; } catch { _extractor.Dispose(); _extractor = null; _archiveItems = null; } } } public void Dispose() { Unbind(); _extractor?.Dispose(); _rootStream?.Dispose(); _extractor = null; _rootStream = null; } /// /// is the supplied path a canonical name including an archive? /// private static bool IsCanonicalArchivePath(string path) { return path.IndexOf('|') != -1; } /// /// Repairs paths from an archive which contain offensive characters /// public static string Util_FixArchiveFilename(string fn) { return fn.Replace('\\', '/'); } /// /// converts a canonical name to a bound name (the bound part, whether or not it is an archive) /// static string GetBoundNameFromCanonical(string canonical) { var parts = canonical.Split('|'); return parts[parts.Length - 1]; } /// /// makes a canonical name from two parts /// string MakeCanonicalName(string root, string member) { if (member == null) { return root; } return $"{root}|{member}"; } public static IEnumerable Util_ResolveLinks(IEnumerable paths) { return paths.Select(f => Win32PInvokes.ResolveShortcut(f)); } public static string Util_ResolveLink(string path) { return Win32PInvokes.ResolveShortcut(path); } } /// /// Members returned by IHawkFileArchiveHandler /// public class HawkFileArchiveItem { /// /// Gets or sets the member name /// public string Name { get; set; } /// /// Gets or sets the size of member file /// public long Size { get; set; } /// /// Gets or sets the index of this archive item /// public int Index { get; set; } /// /// Gets or sets the index WITHIN THE ARCHIVE (for internal tracking by a IHawkFileArchiveHandler) of the member /// public int ArchiveIndex { get; set; } } }