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