using System; using System.Collections.Generic; using System.IO; using System.Linq; //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) /// /// 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 { /// /// Set this with an instance which can construct archive handlers as necessary for archive handling. /// public static IHawkFileArchiveHandler ArchiveHandlerFactory; /// /// 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()) { MemoryStream ms = new MemoryStream((int)stream.Length); stream.CopyTo(ms); return ms.GetBuffer(); } } } /// /// returns whether a bound file exists. if there is no bound file, it can't exist /// public bool Exists { get { return exists; } } /// /// gets the directory containing the root /// public string Directory { get { return Path.GetDirectoryName(rootPath); } } /// /// 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; } /// /// indicates whether this instance is bound /// public bool IsBound { get { return boundStream != null; } } /// /// returns the complete canonical full path ("c:\path\to\archive|member") of the bound file /// public string CanonicalFullPath { get { return MakeCanonicalName(rootPath, memberPath); } } /// /// returns the complete canonical name ("archive|member") of the bound file /// public string CanonicalName { get { return MakeCanonicalName(Path.GetFileName(rootPath), memberPath); } } /// /// returns the virtual name of the bound file (disregarding the archive) /// public string Name { get { return GetBoundNameFromCanonical(MakeCanonicalName(rootPath, memberPath)); } } /// /// returns the extension of Name /// public string Extension { get { return Path.GetExtension(Name).ToUpper(); } } /// /// Indicates whether this file is an archive /// public bool IsArchive { get { return extractor != null; } } int? BoundIndex; public int? GetBoundIndex() { return BoundIndex; } //public class ArchiveItem //{ // public string name; // public long size; // public int index; //} public IList ArchiveItems { get { if (!IsArchive) throw new InvalidOperationException("Cant get archive items from non-archive"); return archiveItems; } } /// /// 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 = new string[] { }; //--- bool exists; bool rootExists; string rootPath; string memberPath; Stream rootStream, boundStream; IHawkFileArchiveHandler extractor; List archiveItems; public HawkFile() { } 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) { string[] 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.archiveIndex); } /// /// 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; else 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) /// 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 this. use BindSoleItemOf or the archive chooser instead /// public HawkFile BindFirstOf(params string[] extensions) { return BindByExtensionCore(true, extensions); } 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 string 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; } 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).Substring(1).ToLower() == ext.ToLower())) { return; } if (ArchiveHandlerFactory.CheckSignature(path, out offset, out isExecutable)) { extractor = ArchiveHandlerFactory.Construct(path); try { ScanArchive(); } catch { extractor.Dispose(); extractor = null; archiveItems = null; } } } public void Dispose() { Unbind(); if (extractor != null) extractor.Dispose(); if (rootStream != null) rootStream.Dispose(); extractor = null; rootStream = null; } /// /// is the supplied path a canonical name including an archive? /// 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) { string[] 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; else return string.Format("{0}|{1}", root, member); } } //class HawkFile /// /// 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); } /// /// Members returned by IHawkFileArchiveHandler /// public class HawkFileArchiveItem { /// /// member name /// public string name; /// /// size of member file /// public long size; /// /// the index of this archive item /// public int index; /// /// the index WITHIN THE ARCHIVE (for internal tracking by a IHawkFileArchiveHandler) of the member /// public int archiveIndex; } } //namespace BizHawk.Common