From b09198708e9938271f214bb098d4a1781c6ab06f Mon Sep 17 00:00:00 2001 From: Matt Nadareski Date: Fri, 8 Feb 2019 20:51:44 -0800 Subject: [PATCH] [FileTypes/] Cleanup tabs AND variables --- SabreTools.Library/FileTypes/BaseArchive.cs | 196 +- SabreTools.Library/FileTypes/BaseFile.cs | 234 +-- SabreTools.Library/FileTypes/CHDFile.cs | 974 +++++----- .../FileTypes/CoreRarArchive.cs | 380 ++-- SabreTools.Library/FileTypes/Folder.cs | 516 +++--- SabreTools.Library/FileTypes/GZipArchive.cs | 1048 +++++------ SabreTools.Library/FileTypes/LRZipArchive.cs | 260 +-- SabreTools.Library/FileTypes/LZ4Archive.cs | 260 +-- SabreTools.Library/FileTypes/RarArchive.cs | 18 +- .../FileTypes/SevenZipArchive.cs | 1472 +++++++-------- SabreTools.Library/FileTypes/TapeArchive.cs | 1232 ++++++------- SabreTools.Library/FileTypes/XZArchive.cs | 1260 ++++++------- SabreTools.Library/FileTypes/ZPAQArchive.cs | 260 +-- SabreTools.Library/FileTypes/ZipArchive.cs | 1638 ++++++++--------- SabreTools.Library/FileTypes/ZstdArchive.cs | 260 +-- 15 files changed, 4986 insertions(+), 5022 deletions(-) diff --git a/SabreTools.Library/FileTypes/BaseArchive.cs b/SabreTools.Library/FileTypes/BaseArchive.cs index ba76c42c..8ba4d0c3 100644 --- a/SabreTools.Library/FileTypes/BaseArchive.cs +++ b/SabreTools.Library/FileTypes/BaseArchive.cs @@ -12,123 +12,123 @@ using Stream = System.IO.Stream; namespace SabreTools.Library.FileTypes { - public abstract class BaseArchive : Folder - { - #region Protected instance variables + public abstract class BaseArchive : Folder + { + #region Protected instance variables - // Buffer size used by archives - protected const int _bufferSize = 4096 * 128; + // Buffer size used by archives + protected const int _bufferSize = 4096 * 128; - #endregion + #endregion - #region Construtors + #region Construtors - /// - /// Create a new Archive with no base file - /// - public BaseArchive() - { - } + /// + /// Create a new Archive with no base file + /// + public BaseArchive() + { + } - /// - /// Create a new Archive from the given file - /// - /// Name of the file to use as an archive - /// True if hashes for this file should be calculated, false otherwise (default) - public BaseArchive(string filename, bool getHashes = false) - : base(filename, getHashes) - { - } + /// + /// Create a new Archive from the given file + /// + /// Name of the file to use as an archive + /// True if hashes for this file should be calculated, false otherwise (default) + public BaseArchive(string filename, bool getHashes = false) + : base(filename, getHashes) + { + } - #endregion + #endregion - #region Extraction + #region Extraction - /// - /// Attempt to extract a file as an archive - /// - /// Output directory for archive extraction - /// True if the extraction was a success, false otherwise - public override abstract bool CopyAll(string outDir); + /// + /// Attempt to extract a file as an archive + /// + /// Output directory for archive extraction + /// True if the extraction was a success, false otherwise + public override abstract bool CopyAll(string outDir); - /// - /// Attempt to extract an entry from an archive - /// - /// Name of the entry to be extracted - /// Output directory for archive extraction - /// Name of the extracted file, null on error - public override abstract string CopyToFile(string entryName, string outDir); + /// + /// Attempt to extract an entry from an archive + /// + /// Name of the entry to be extracted + /// Output directory for archive extraction + /// Name of the extracted file, null on error + public override abstract string CopyToFile(string entryName, string outDir); - /// - /// Attempt to extract a stream from an archive - /// - /// Name of the entry to be extracted - /// Output representing the entry name that was found - /// MemoryStream representing the entry, null on error - public override abstract (MemoryStream, string) CopyToStream(string entryName); + /// + /// Attempt to extract a stream from an archive + /// + /// Name of the entry to be extracted + /// Output representing the entry name that was found + /// MemoryStream representing the entry, null on error + public override abstract (MemoryStream, string) CopyToStream(string entryName); - #endregion + #endregion - #region Information + #region Information - /// - /// Generate a list of DatItem objects from the header values in an archive - /// - /// Hash representing the hashes that should be skipped - /// True if entry dates should be included, false otherwise (default) - /// List of DatItem objects representing the found data - /// TODO: All instances of Hash.DeepHashes should be made into 0x0 eventually - public override abstract List GetChildren(Hash omitFromScan = Hash.DeepHashes, bool date = false); + /// + /// Generate a list of DatItem objects from the header values in an archive + /// + /// Hash representing the hashes that should be skipped + /// True if entry dates should be included, false otherwise (default) + /// List of DatItem objects representing the found data + /// TODO: All instances of Hash.DeepHashes should be made into 0x0 eventually + public override abstract List GetChildren(Hash omitFromScan = Hash.DeepHashes, bool date = false); - /// - /// Generate a list of empty folders in an archive - /// - /// Input file to get data from - /// List of empty folders in the archive - public override abstract List GetEmptyFolders(); + /// + /// Generate a list of empty folders in an archive + /// + /// Input file to get data from + /// List of empty folders in the archive + public override abstract List GetEmptyFolders(); - /// - /// Check whether the input file is a standardized format - /// - public abstract bool IsTorrent(); + /// + /// Check whether the input file is a standardized format + /// + public abstract bool IsTorrent(); - #endregion + #endregion - #region Writing + #region Writing - /// - /// Write an input file to an archive - /// - /// Input filename to be moved - /// Output directory to build to - /// DatItem representing the new information - /// True if the date from the DAT should be used if available, false otherwise (default) - /// True if files should be output in Romba depot folders, false otherwise - /// True if the archive was written properly, false otherwise - public override abstract bool Write(string inputFile, string outDir, Rom rom, bool date = false, bool romba = false); + /// + /// Write an input file to an archive + /// + /// Input filename to be moved + /// Output directory to build to + /// DatItem representing the new information + /// True if the date from the DAT should be used if available, false otherwise (default) + /// True if files should be output in Romba depot folders, false otherwise + /// True if the archive was written properly, false otherwise + public override abstract bool Write(string inputFile, string outDir, Rom rom, bool date = false, bool romba = false); - /// - /// Write an input stream to an archive - /// - /// Input stream to be moved - /// Output directory to build to - /// DatItem representing the new information - /// True if the date from the DAT should be used if available, false otherwise (default) - /// True if files should be output in Romba depot folders, false otherwise - /// True if the archive was written properly, false otherwise - public override abstract bool Write(Stream inputStream, string outDir, Rom rom, bool date = false, bool romba = false); + /// + /// Write an input stream to an archive + /// + /// Input stream to be moved + /// Output directory to build to + /// DatItem representing the new information + /// True if the date from the DAT should be used if available, false otherwise (default) + /// True if files should be output in Romba depot folders, false otherwise + /// True if the archive was written properly, false otherwise + public override abstract bool Write(Stream inputStream, string outDir, Rom rom, bool date = false, bool romba = false); - /// - /// Write a set of input files to an archive (assuming the same output archive name) - /// - /// Input files to be moved - /// Output directory to build to - /// DatItem representing the new information - /// True if the date from the DAT should be used if available, false otherwise (default) - /// True if files should be output in Romba depot folders, false otherwise - /// True if the archive was written properly, false otherwise - public override abstract bool Write(List inputFiles, string outDir, List roms, bool date = false, bool romba = false); + /// + /// Write a set of input files to an archive (assuming the same output archive name) + /// + /// Input files to be moved + /// Output directory to build to + /// DatItem representing the new information + /// True if the date from the DAT should be used if available, false otherwise (default) + /// True if files should be output in Romba depot folders, false otherwise + /// True if the archive was written properly, false otherwise + public override abstract bool Write(List inputFiles, string outDir, List roms, bool date = false, bool romba = false); - #endregion - } + #endregion + } } diff --git a/SabreTools.Library/FileTypes/BaseFile.cs b/SabreTools.Library/FileTypes/BaseFile.cs index d625639d..f3e01481 100644 --- a/SabreTools.Library/FileTypes/BaseFile.cs +++ b/SabreTools.Library/FileTypes/BaseFile.cs @@ -9,151 +9,115 @@ using Stream = System.IO.Stream; namespace SabreTools.Library.FileTypes { - public class BaseFile - { - #region Protected instance variables + public class BaseFile + { + #region Publicly facing variables - protected FileType _fileType; - protected string _filename; - protected string _parent; - protected string _date; + // TODO: Get all of these values automatically so there is no public "set" + public FileType Type { get; protected set; } + public string Filename { get; set; } + public string Parent { get; set; } + public string Date { get; set; } + public long? Size { get; set; } + public byte[] CRC { get; set; } + public byte[] MD5 { get; set; } + public byte[] SHA1 { get; set; } + public byte[] SHA256 { get; set; } + public byte[] SHA384 { get; set; } + public byte[] SHA512 { get; set; } - // External hash values for the file - protected long? _size; - protected byte[] _crc; - protected byte[] _md5; - protected byte[] _sha1; - protected byte[] _sha256; - protected byte[] _sha384; - protected byte[] _sha512; + #endregion - #endregion + #region Construtors - #region Publicly facing variables + /// + /// Create a new BaseFile with no base file + /// + public BaseFile() + { + } - // TODO: Get all of these values automatically so there is no public "set" - public FileType Type - { - get { return _fileType; } - } - public string Filename - { - get { return _filename; } - set { _filename = value; } - } - public string Parent - { - get { return _parent; } - set { _parent = value; } - } - public string Date - { - get { return _date; } - set { _date = value; } - } - public long? Size - { - get { return _size; } - set { _size = value; } - } - public byte[] CRC - { - get { return _crc; } - set { _crc = value; } - } - public byte[] MD5 - { - get { return _md5; } - set { _md5 = value; } - } - public byte[] SHA1 - { - get { return _sha1; } - set { _sha1 = value; } - } - public byte[] SHA256 - { - get { return _sha256; } - set { _sha256 = value; } - } - public byte[] SHA384 - { - get { return _sha384; } - set { _sha384 = value; } - } - public byte[] SHA512 - { - get { return _sha512; } - set { _sha512 = value; } - } + /// + /// Create a new BaseFile from the given file + /// + /// Name of the file to use + /// True if hashes for this file should be calculated (default), false otherwise + public BaseFile(string filename, bool getHashes = true) + { + this.Filename = filename; - #endregion + if (getHashes) + { + BaseFile temp = Utilities.GetFileInfo(this.Filename); - #region Construtors + if (temp != null) + { + this.Parent = temp.Parent; + this.Date = temp.Date; + this.CRC = temp.CRC; + this.MD5 = temp.MD5; + this.SHA1 = temp.SHA1; + this.SHA256 = temp.SHA256; + this.SHA384 = temp.SHA384; + this.SHA512 = temp.SHA512; + } + } + } - /// - /// Create a new BaseFile with no base file - /// - public BaseFile() - { - } + /// + /// Create a new BaseFile from the given file + /// + /// Name of the file to use + /// Stream to populate information from + /// True if hashes for this file should be calculated (default), false otherwise + public BaseFile(string filename, Stream stream, bool getHashes = true) + { + this.Filename = filename; - /// - /// Create a new BaseFile from the given file - /// - /// Name of the file to use - /// True if hashes for this file should be calculated (default), false otherwise - public BaseFile(string filename, bool getHashes = true) - { - this._filename = filename; + if (getHashes) + { + BaseFile temp = Utilities.GetStreamInfo(stream, stream.Length); - if (getHashes) - { - BaseFile temp = Utilities.GetFileInfo(_filename); + if(temp != null) + { + this.Parent = temp.Parent; + this.Date = temp.Date; + this.CRC = temp.CRC; + this.MD5 = temp.MD5; + this.SHA1 = temp.SHA1; + this.SHA256 = temp.SHA256; + this.SHA384 = temp.SHA384; + this.SHA512 = temp.SHA512; + } + } + + } - if (temp != null) - { - this._parent = temp.Parent; - this._date = temp.Date; - this._crc = temp.CRC; - this._md5 = temp.MD5; - this._sha1 = temp.SHA1; - this._sha256 = temp.SHA256; - this._sha384 = temp.SHA384; - this._sha512 = temp.SHA512; - } - } - } + /// + /// Create a new BaseFile from the given metadata + /// + /// Name of the file to use + /// Parent folder or archive + /// File date + /// CRC hash as a byte array + /// MD5 hash as a byte array + /// SHA-1 hash as a byte array + /// SHA-256 hash as a byte array + /// SHA-384 hash as a byte array + /// SHA-512 hash as a byte array + public BaseFile(string filename, string parent, string date, byte[] crc, byte[] md5, byte[] sha1, byte[] sha256, byte[] sha384, byte[] sha512) + { + this.Filename = filename; + this.Parent = parent; + this.Date = date; + this.CRC = crc; + this.MD5 = md5; + this.SHA1 = sha1; + this.SHA256 = sha256; + this.SHA384 = sha384; + this.SHA512 = sha512; + } - /// - /// Create a new BaseFile from the given file - /// - /// Name of the file to use - /// Stream to populate information from - /// True if hashes for this file should be calculated (default), false otherwise - public BaseFile(string filename, Stream stream, bool getHashes = true) - { - this._filename = filename; - - if (getHashes) - { - BaseFile temp = Utilities.GetStreamInfo(stream, stream.Length); - - if(temp != null) - { - this._parent = temp.Parent; - this._date = temp.Date; - this._crc = temp.CRC; - this._md5 = temp.MD5; - this._sha1 = temp.SHA1; - this._sha256 = temp.SHA256; - this._sha384 = temp.SHA384; - this._sha512 = temp.SHA512; - } - } - - } - - #endregion - } + #endregion + } } diff --git a/SabreTools.Library/FileTypes/CHDFile.cs b/SabreTools.Library/FileTypes/CHDFile.cs index 7fee31a2..a08ec7e9 100644 --- a/SabreTools.Library/FileTypes/CHDFile.cs +++ b/SabreTools.Library/FileTypes/CHDFile.cs @@ -11,491 +11,491 @@ using Stream = System.IO.Stream; namespace SabreTools.Library.FileTypes { - /// - /// This is code adapted from chd.h and chd.cpp in MAME - /// Additional archival code from https://github.com/rtissera/libchdr/blob/master/src/chd.h - /// - /// - /// ---------------------------------------------- - /// Common CHD Header: - /// 0x00-0x07 - CHD signature - /// 0x08-0x0B - Header size - /// 0x0C-0x0F - CHD version - /// ---------------------------------------------- - /// CHD v1 header layout: - /// 0x10-0x13 - Flags (1: Has parent MD5, 2: Disallow writes) - /// 0x14-0x17 - Compression - /// 0x18-0x1B - 512-byte sectors per hunk - /// 0x1C-0x1F - Hunk count - /// 0x20-0x23 - Hard disk cylinder count - /// 0x24-0x27 - Hard disk head count - /// 0x28-0x2B - Hard disk sector count - /// 0x2C-0x3B - MD5 - /// 0x3C-0x4B - Parent MD5 - /// ---------------------------------------------- - /// CHD v2 header layout: - /// 0x10-0x13 - Flags (1: Has parent MD5, 2: Disallow writes) - /// 0x14-0x17 - Compression - /// 0x18-0x1B - seclen-byte sectors per hunk - /// 0x1C-0x1F - Hunk count - /// 0x20-0x23 - Hard disk cylinder count - /// 0x24-0x27 - Hard disk head count - /// 0x28-0x2B - Hard disk sector count - /// 0x2C-0x3B - MD5 - /// 0x3C-0x4B - Parent MD5 - /// 0x4C-0x4F - Number of bytes per sector (seclen) - /// ---------------------------------------------- - /// CHD v3 header layout: - /// 0x10-0x13 - Flags (1: Has parent SHA-1, 2: Disallow writes) - /// 0x14-0x17 - Compression - /// 0x18-0x1B - Hunk count - /// 0x1C-0x23 - Logical Bytes - /// 0x24-0x2C - Metadata Offset - /// ... - /// 0x4C-0x4F - Hunk Bytes - /// 0x50-0x63 - SHA-1 - /// 0x64-0x77 - Parent SHA-1 - /// 0x78-0x87 - Map - /// ---------------------------------------------- - /// CHD v4 header layout: - /// 0x10-0x13 - Flags (1: Has parent SHA-1, 2: Disallow writes) - /// 0x14-0x17 - Compression - /// 0x18-0x1B - Hunk count - /// 0x1C-0x23 - Logical Bytes - /// 0x24-0x2C - Metadata Offset - /// ... - /// 0x2C-0x2F - Hunk Bytes - /// 0x30-0x43 - SHA-1 - /// 0x44-0x57 - Parent SHA-1 - /// 0x58-0x6b - Raw SHA-1 - /// 0x6c-0x7b - Map - /// ---------------------------------------------- - /// CHD v5 header layout: - /// 0x10-0x13 - Compression format 1 - /// 0x14-0x17 - Compression format 2 - /// 0x18-0x1B - Compression format 3 - /// 0x1C-0x1F - Compression format 4 - /// 0x20-0x27 - Logical Bytes - /// 0x28-0x2F - Map Offset - /// 0x30-0x37 - Metadata Offset - /// 0x38-0x3B - Hunk Bytes - /// 0x3C-0x3F - Unit Bytes - /// 0x40-0x53 - Raw SHA-1 - /// 0x54-0x67 - SHA-1 - /// 0x68-0x7b - Parent SHA-1 - /// ---------------------------------------------- - /// - public class CHDFile : BaseFile - { - #region Private instance variables - - // Core parameters from the header - private byte[] m_signature; // signature - private uint m_headersize; // size of the header - private uint m_version; // version of the header - private ulong m_logicalbytes; // logical size of the raw CHD data in bytes - private ulong m_mapoffset; // offset of map - private ulong m_metaoffset; // offset to first metadata bit - private uint m_sectorsperhunk; // number of sectors per hunk - private uint m_hunkbytes; // size of each raw hunk in bytes - private ulong m_hunkcount; // number of hunks represented - private uint m_unitbytes; // size of each unit in bytes - private ulong m_unitcount; // number of units represented - private CHD_CODEC[] m_compression = new CHD_CODEC[4]; // array of compression types used - - // map information - private uint m_mapentrybytes; // length of each entry in a map - - // additional required vars - private uint? _headerVersion; - private BinaryReader m_br; // Binary reader representing the CHD stream - - #endregion - - #region Pubically facing variables - - public uint? Version - { - get - { - if (_headerVersion == null) - { - _headerVersion = ValidateHeaderVersion(); - } - - return _headerVersion; - } - } - - #endregion - - #region Constructors - - /// - /// Create a new, blank CHDFile - /// - public CHDFile() - { - this._fileType = FileType.CHD; - } - - /// - /// Create a new CHDFile from an input file - /// - /// - public CHDFile(string filename) - : this(Utilities.TryOpenRead(filename)) - { - } - - /// - /// Create a new CHDFile from an input stream - /// - /// Stream representing the CHD file - public CHDFile(Stream chdstream) - { - _fileType = FileType.CHD; - m_br = new BinaryReader(chdstream); - - _headerVersion = ValidateHeaderVersion(); - - if (_headerVersion != null) - { - byte[] hash = GetHashFromHeader(); - - if (hash != null) - { - if (hash.Length == Constants.MD5Length) - { - _md5 = hash; - } - else if (hash.Length == Constants.SHA1Length) - { - _sha1 = hash; - } - } - } - } - - #endregion - - #region Header Parsing - - /// - /// Validate the initial signature, version, and header size - /// - /// Unsigned int containing the version number, null if invalid - private uint? ValidateHeaderVersion() - { - try - { - // Seek to the beginning to make sure we're reading the correct bytes - m_br.BaseStream.Seek(0, SeekOrigin.Begin); - - // Read and verify the CHD signature - m_signature = m_br.ReadBytes(8); - - // If no signature could be read, return null - if (m_signature == null || m_signature.Length == 0) - { - return null; - } - - if (!m_signature.StartsWith(Constants.CHDSignature, exact: true)) - { - // throw CHDERR_INVALID_FILE; - return null; - } - - // Get the header size and version - m_headersize = m_br.ReadUInt32Reverse(); - m_version = m_br.ReadUInt32Reverse(); - - // If we have an invalid combination of size and version - if ((m_version == 1 && m_headersize != Constants.CHD_V1_HEADER_SIZE) - || (m_version == 2 && m_headersize != Constants.CHD_V2_HEADER_SIZE) - || (m_version == 3 && m_headersize != Constants.CHD_V3_HEADER_SIZE) - || (m_version == 4 && m_headersize != Constants.CHD_V4_HEADER_SIZE) - || (m_version == 5 && m_headersize != Constants.CHD_V5_HEADER_SIZE) - || (m_version < 1 || m_version > 5)) - { - // throw CHDERR_UNSUPPORTED_VERSION; - return null; - } - - return m_version; - } - catch - { - return null; - } - } - - /// - /// Get the internal MD5 (v1, v2) or SHA-1 (v3, v4, v5) from the CHD - /// - /// MD5 as a byte array, null on error - private byte[] GetHashFromHeader() - { - // Validate the header by default just in case - uint? version = ValidateHeaderVersion(); - - // Now get the hash, if possible - byte[] hash; - - // Now parse the rest of the header according to the version - try - { - switch (version) - { - case 1: - hash = ParseCHDv1Header(); - break; - case 2: - hash = ParseCHDv2Header(); - break; - case 3: - hash = ParseCHDv3Header(); - break; - case 4: - hash = ParseCHDv4Header(); - break; - case 5: - hash = ParseCHDv5Header(); - break; - case null: - default: - // throw CHDERR_INVALID_FILE; - return null; - } - } - catch - { - // throw CHDERR_INVALID_FILE; - return null; - } - - return hash; - } - - /// - /// Parse a CHD v1 header - /// - /// The extracted MD5 on success, null otherwise - private byte[] ParseCHDv1Header() - { - // Seek to after the signature to make sure we're reading the correct bytes - m_br.BaseStream.Seek(16, SeekOrigin.Begin); - - // Set the blank MD5 hash - byte[] md5 = new byte[16]; - - // Set offsets and defaults - m_mapoffset = 0; - m_mapentrybytes = 0; - - // Read the CHD flags - uint flags = m_br.ReadUInt32Reverse(); - - // Determine compression - switch (m_br.ReadUInt32()) - { - case 0: m_compression[0] = CHD_CODEC.NONE; break; - case 1: m_compression[0] = CHD_CODEC.ZLIB; break; - case 2: m_compression[0] = CHD_CODEC.ZLIB; break; - case 3: m_compression[0] = CHD_CODEC.AVHUFF; break; - default: /* throw CHDERR_UNKNOWN_COMPRESSION; */ return null; - } - - m_compression[1] = m_compression[2] = m_compression[3] = CHD_CODEC.NONE; - - m_sectorsperhunk = m_br.ReadUInt32Reverse(); - m_hunkcount = m_br.ReadUInt32Reverse(); - m_br.ReadUInt32Reverse(); // Cylinder count - m_br.ReadUInt32Reverse(); // Head count - m_br.ReadUInt32Reverse(); // Sector count - - md5 = m_br.ReadBytes(16); - m_br.ReadBytes(16); // Parent MD5 - - return md5; - } - - /// - /// Parse a CHD v2 header - /// - /// The extracted MD5 on success, null otherwise - private byte[] ParseCHDv2Header() - { - // Seek to after the signature to make sure we're reading the correct bytes - m_br.BaseStream.Seek(16, SeekOrigin.Begin); - - // Set the blank MD5 hash - byte[] md5 = new byte[16]; - - // Set offsets and defaults - m_mapoffset = 0; - m_mapentrybytes = 0; - - // Read the CHD flags - uint flags = m_br.ReadUInt32Reverse(); - - // Determine compression - switch (m_br.ReadUInt32()) - { - case 0: m_compression[0] = CHD_CODEC.NONE; break; - case 1: m_compression[0] = CHD_CODEC.ZLIB; break; - case 2: m_compression[0] = CHD_CODEC.ZLIB; break; - case 3: m_compression[0] = CHD_CODEC.AVHUFF; break; - default: /* throw CHDERR_UNKNOWN_COMPRESSION; */ return null; - } - - m_compression[1] = m_compression[2] = m_compression[3] = CHD_CODEC.NONE; - - m_sectorsperhunk = m_br.ReadUInt32Reverse(); - m_hunkcount = m_br.ReadUInt32Reverse(); - m_br.ReadUInt32Reverse(); // Cylinder count - m_br.ReadUInt32Reverse(); // Head count - m_br.ReadUInt32Reverse(); // Sector count - - md5 = m_br.ReadBytes(16); - m_br.ReadBytes(16); // Parent MD5 - m_br.ReadUInt32Reverse(); // Sector size - - return md5; - } - - /// - /// Parse a CHD v3 header - /// - /// The extracted SHA-1 on success, null otherwise - private byte[] ParseCHDv3Header() - { - // Seek to after the signature to make sure we're reading the correct bytes - m_br.BaseStream.Seek(16, SeekOrigin.Begin); - - // Set the blank SHA-1 hash - byte[] sha1 = new byte[20]; - - // Set offsets and defaults - m_mapoffset = 120; - m_mapentrybytes = 16; - - // Read the CHD flags - uint flags = m_br.ReadUInt32Reverse(); - - // Determine compression - switch (m_br.ReadUInt32()) - { - case 0: m_compression[0] = CHD_CODEC.NONE; break; - case 1: m_compression[0] = CHD_CODEC.ZLIB; break; - case 2: m_compression[0] = CHD_CODEC.ZLIB; break; - case 3: m_compression[0] = CHD_CODEC.AVHUFF; break; - default: /* throw CHDERR_UNKNOWN_COMPRESSION; */ return null; - } - - m_compression[1] = m_compression[2] = m_compression[3] = CHD_CODEC.NONE; - - m_hunkcount = m_br.ReadUInt32Reverse(); - m_logicalbytes = m_br.ReadUInt64Reverse(); - m_metaoffset = m_br.ReadUInt32Reverse(); - - m_br.BaseStream.Seek(76, SeekOrigin.Begin); - m_hunkbytes = m_br.ReadUInt32Reverse(); - - m_br.BaseStream.Seek(Constants.CHDv3SHA1Offset, SeekOrigin.Begin); - sha1 = m_br.ReadBytes(20); - - // guess at the units based on snooping the metadata - // m_unitbytes = guess_unitbytes(); - m_unitcount = (m_logicalbytes + m_unitbytes - 1) / m_unitbytes; - - return sha1; - } - - /// - /// Parse a CHD v4 header - /// - /// The extracted SHA-1 on success, null otherwise - private byte[] ParseCHDv4Header() - { - // Seek to after the signature to make sure we're reading the correct bytes - m_br.BaseStream.Seek(16, SeekOrigin.Begin); - - // Set the blank SHA-1 hash - byte[] sha1 = new byte[20]; - - // Set offsets and defaults - m_mapoffset = 108; - m_mapentrybytes = 16; - - // Read the CHD flags - uint flags = m_br.ReadUInt32Reverse(); - - // Determine compression - switch (m_br.ReadUInt32()) - { - case 0: m_compression[0] = CHD_CODEC.NONE; break; - case 1: m_compression[0] = CHD_CODEC.ZLIB; break; - case 2: m_compression[0] = CHD_CODEC.ZLIB; break; - case 3: m_compression[0] = CHD_CODEC.AVHUFF; break; - default: /* throw CHDERR_UNKNOWN_COMPRESSION; */ return null; - } - - m_compression[1] = m_compression[2] = m_compression[3] = CHD_CODEC.NONE; - - m_hunkcount = m_br.ReadUInt32Reverse(); - m_logicalbytes = m_br.ReadUInt64Reverse(); - m_metaoffset = m_br.ReadUInt32Reverse(); - - m_br.BaseStream.Seek(44, SeekOrigin.Begin); - m_hunkbytes = m_br.ReadUInt32Reverse(); - - m_br.BaseStream.Seek(Constants.CHDv4SHA1Offset, SeekOrigin.Begin); - sha1 = m_br.ReadBytes(20); - - // guess at the units based on snooping the metadata - // m_unitbytes = guess_unitbytes(); - m_unitcount = (m_logicalbytes + m_unitbytes - 1) / m_unitbytes; - return sha1; - } - - /// - /// Parse a CHD v5 header - /// - /// The extracted SHA-1 on success, null otherwise - private byte[] ParseCHDv5Header() - { - // Seek to after the signature to make sure we're reading the correct bytes - m_br.BaseStream.Seek(16, SeekOrigin.Begin); - - // Set the blank SHA-1 hash - byte[] sha1 = new byte[20]; - - // Determine compression - m_compression[0] = (CHD_CODEC)m_br.ReadUInt32Reverse(); - m_compression[1] = (CHD_CODEC)m_br.ReadUInt32Reverse(); - m_compression[2] = (CHD_CODEC)m_br.ReadUInt32Reverse(); - m_compression[3] = (CHD_CODEC)m_br.ReadUInt32Reverse(); - - m_logicalbytes = m_br.ReadUInt64Reverse(); - m_mapoffset = m_br.ReadUInt64Reverse(); - m_metaoffset = m_br.ReadUInt64Reverse(); - m_hunkbytes = m_br.ReadUInt32Reverse(); - m_hunkcount = (m_logicalbytes + m_hunkbytes - 1) / m_hunkbytes; - m_unitbytes = m_br.ReadUInt32Reverse(); - m_unitcount = (m_logicalbytes + m_unitbytes - 1) / m_unitbytes; - - // m_allow_writes = !compressed(); - - // determine properties of map entries - // m_mapentrybytes = compressed() ? 12 : 4; - - m_br.BaseStream.Seek(Constants.CHDv5SHA1Offset, SeekOrigin.Begin); - sha1 = m_br.ReadBytes(20); - return sha1; - } - - #endregion - } + /// + /// This is code adapted from chd.h and chd.cpp in MAME + /// Additional archival code from https://github.com/rtissera/libchdr/blob/master/src/chd.h + /// + /// + /// ---------------------------------------------- + /// Common CHD Header: + /// 0x00-0x07 - CHD signature + /// 0x08-0x0B - Header size + /// 0x0C-0x0F - CHD version + /// ---------------------------------------------- + /// CHD v1 header layout: + /// 0x10-0x13 - Flags (1: Has parent MD5, 2: Disallow writes) + /// 0x14-0x17 - Compression + /// 0x18-0x1B - 512-byte sectors per hunk + /// 0x1C-0x1F - Hunk count + /// 0x20-0x23 - Hard disk cylinder count + /// 0x24-0x27 - Hard disk head count + /// 0x28-0x2B - Hard disk sector count + /// 0x2C-0x3B - MD5 + /// 0x3C-0x4B - Parent MD5 + /// ---------------------------------------------- + /// CHD v2 header layout: + /// 0x10-0x13 - Flags (1: Has parent MD5, 2: Disallow writes) + /// 0x14-0x17 - Compression + /// 0x18-0x1B - seclen-byte sectors per hunk + /// 0x1C-0x1F - Hunk count + /// 0x20-0x23 - Hard disk cylinder count + /// 0x24-0x27 - Hard disk head count + /// 0x28-0x2B - Hard disk sector count + /// 0x2C-0x3B - MD5 + /// 0x3C-0x4B - Parent MD5 + /// 0x4C-0x4F - Number of bytes per sector (seclen) + /// ---------------------------------------------- + /// CHD v3 header layout: + /// 0x10-0x13 - Flags (1: Has parent SHA-1, 2: Disallow writes) + /// 0x14-0x17 - Compression + /// 0x18-0x1B - Hunk count + /// 0x1C-0x23 - Logical Bytes + /// 0x24-0x2C - Metadata Offset + /// ... + /// 0x4C-0x4F - Hunk Bytes + /// 0x50-0x63 - SHA-1 + /// 0x64-0x77 - Parent SHA-1 + /// 0x78-0x87 - Map + /// ---------------------------------------------- + /// CHD v4 header layout: + /// 0x10-0x13 - Flags (1: Has parent SHA-1, 2: Disallow writes) + /// 0x14-0x17 - Compression + /// 0x18-0x1B - Hunk count + /// 0x1C-0x23 - Logical Bytes + /// 0x24-0x2C - Metadata Offset + /// ... + /// 0x2C-0x2F - Hunk Bytes + /// 0x30-0x43 - SHA-1 + /// 0x44-0x57 - Parent SHA-1 + /// 0x58-0x6b - Raw SHA-1 + /// 0x6c-0x7b - Map + /// ---------------------------------------------- + /// CHD v5 header layout: + /// 0x10-0x13 - Compression format 1 + /// 0x14-0x17 - Compression format 2 + /// 0x18-0x1B - Compression format 3 + /// 0x1C-0x1F - Compression format 4 + /// 0x20-0x27 - Logical Bytes + /// 0x28-0x2F - Map Offset + /// 0x30-0x37 - Metadata Offset + /// 0x38-0x3B - Hunk Bytes + /// 0x3C-0x3F - Unit Bytes + /// 0x40-0x53 - Raw SHA-1 + /// 0x54-0x67 - SHA-1 + /// 0x68-0x7b - Parent SHA-1 + /// ---------------------------------------------- + /// + public class CHDFile : BaseFile + { + #region Private instance variables + + // Core parameters from the header + private byte[] m_signature; // signature + private uint m_headersize; // size of the header + private uint m_version; // version of the header + private ulong m_logicalbytes; // logical size of the raw CHD data in bytes + private ulong m_mapoffset; // offset of map + private ulong m_metaoffset; // offset to first metadata bit + private uint m_sectorsperhunk; // number of sectors per hunk + private uint m_hunkbytes; // size of each raw hunk in bytes + private ulong m_hunkcount; // number of hunks represented + private uint m_unitbytes; // size of each unit in bytes + private ulong m_unitcount; // number of units represented + private CHD_CODEC[] m_compression = new CHD_CODEC[4]; // array of compression types used + + // map information + private uint m_mapentrybytes; // length of each entry in a map + + // additional required vars + private uint? _headerVersion; + private BinaryReader m_br; // Binary reader representing the CHD stream + + #endregion + + #region Pubically facing variables + + public uint? Version + { + get + { + if (_headerVersion == null) + { + _headerVersion = ValidateHeaderVersion(); + } + + return _headerVersion; + } + } + + #endregion + + #region Constructors + + /// + /// Create a new, blank CHDFile + /// + public CHDFile() + { + this.Type = FileType.CHD; + } + + /// + /// Create a new CHDFile from an input file + /// + /// + public CHDFile(string filename) + : this(Utilities.TryOpenRead(filename)) + { + } + + /// + /// Create a new CHDFile from an input stream + /// + /// Stream representing the CHD file + public CHDFile(Stream chdstream) + { + this.Type = FileType.CHD; + m_br = new BinaryReader(chdstream); + + _headerVersion = ValidateHeaderVersion(); + + if (_headerVersion != null) + { + byte[] hash = GetHashFromHeader(); + + if (hash != null) + { + if (hash.Length == Constants.MD5Length) + { + this.MD5 = hash; + } + else if (hash.Length == Constants.SHA1Length) + { + this.SHA1 = hash; + } + } + } + } + + #endregion + + #region Header Parsing + + /// + /// Validate the initial signature, version, and header size + /// + /// Unsigned int containing the version number, null if invalid + private uint? ValidateHeaderVersion() + { + try + { + // Seek to the beginning to make sure we're reading the correct bytes + m_br.BaseStream.Seek(0, SeekOrigin.Begin); + + // Read and verify the CHD signature + m_signature = m_br.ReadBytes(8); + + // If no signature could be read, return null + if (m_signature == null || m_signature.Length == 0) + { + return null; + } + + if (!m_signature.StartsWith(Constants.CHDSignature, exact: true)) + { + // throw CHDERR_INVALID_FILE; + return null; + } + + // Get the header size and version + m_headersize = m_br.ReadUInt32Reverse(); + m_version = m_br.ReadUInt32Reverse(); + + // If we have an invalid combination of size and version + if ((m_version == 1 && m_headersize != Constants.CHD_V1_HEADER_SIZE) + || (m_version == 2 && m_headersize != Constants.CHD_V2_HEADER_SIZE) + || (m_version == 3 && m_headersize != Constants.CHD_V3_HEADER_SIZE) + || (m_version == 4 && m_headersize != Constants.CHD_V4_HEADER_SIZE) + || (m_version == 5 && m_headersize != Constants.CHD_V5_HEADER_SIZE) + || (m_version < 1 || m_version > 5)) + { + // throw CHDERR_UNSUPPORTED_VERSION; + return null; + } + + return m_version; + } + catch + { + return null; + } + } + + /// + /// Get the internal MD5 (v1, v2) or SHA-1 (v3, v4, v5) from the CHD + /// + /// MD5 as a byte array, null on error + private byte[] GetHashFromHeader() + { + // Validate the header by default just in case + uint? version = ValidateHeaderVersion(); + + // Now get the hash, if possible + byte[] hash; + + // Now parse the rest of the header according to the version + try + { + switch (version) + { + case 1: + hash = ParseCHDv1Header(); + break; + case 2: + hash = ParseCHDv2Header(); + break; + case 3: + hash = ParseCHDv3Header(); + break; + case 4: + hash = ParseCHDv4Header(); + break; + case 5: + hash = ParseCHDv5Header(); + break; + case null: + default: + // throw CHDERR_INVALID_FILE; + return null; + } + } + catch + { + // throw CHDERR_INVALID_FILE; + return null; + } + + return hash; + } + + /// + /// Parse a CHD v1 header + /// + /// The extracted MD5 on success, null otherwise + private byte[] ParseCHDv1Header() + { + // Seek to after the signature to make sure we're reading the correct bytes + m_br.BaseStream.Seek(16, SeekOrigin.Begin); + + // Set the blank MD5 hash + byte[] md5 = new byte[16]; + + // Set offsets and defaults + m_mapoffset = 0; + m_mapentrybytes = 0; + + // Read the CHD flags + uint flags = m_br.ReadUInt32Reverse(); + + // Determine compression + switch (m_br.ReadUInt32()) + { + case 0: m_compression[0] = CHD_CODEC.NONE; break; + case 1: m_compression[0] = CHD_CODEC.ZLIB; break; + case 2: m_compression[0] = CHD_CODEC.ZLIB; break; + case 3: m_compression[0] = CHD_CODEC.AVHUFF; break; + default: /* throw CHDERR_UNKNOWN_COMPRESSION; */ return null; + } + + m_compression[1] = m_compression[2] = m_compression[3] = CHD_CODEC.NONE; + + m_sectorsperhunk = m_br.ReadUInt32Reverse(); + m_hunkcount = m_br.ReadUInt32Reverse(); + m_br.ReadUInt32Reverse(); // Cylinder count + m_br.ReadUInt32Reverse(); // Head count + m_br.ReadUInt32Reverse(); // Sector count + + md5 = m_br.ReadBytes(16); + m_br.ReadBytes(16); // Parent MD5 + + return md5; + } + + /// + /// Parse a CHD v2 header + /// + /// The extracted MD5 on success, null otherwise + private byte[] ParseCHDv2Header() + { + // Seek to after the signature to make sure we're reading the correct bytes + m_br.BaseStream.Seek(16, SeekOrigin.Begin); + + // Set the blank MD5 hash + byte[] md5 = new byte[16]; + + // Set offsets and defaults + m_mapoffset = 0; + m_mapentrybytes = 0; + + // Read the CHD flags + uint flags = m_br.ReadUInt32Reverse(); + + // Determine compression + switch (m_br.ReadUInt32()) + { + case 0: m_compression[0] = CHD_CODEC.NONE; break; + case 1: m_compression[0] = CHD_CODEC.ZLIB; break; + case 2: m_compression[0] = CHD_CODEC.ZLIB; break; + case 3: m_compression[0] = CHD_CODEC.AVHUFF; break; + default: /* throw CHDERR_UNKNOWN_COMPRESSION; */ return null; + } + + m_compression[1] = m_compression[2] = m_compression[3] = CHD_CODEC.NONE; + + m_sectorsperhunk = m_br.ReadUInt32Reverse(); + m_hunkcount = m_br.ReadUInt32Reverse(); + m_br.ReadUInt32Reverse(); // Cylinder count + m_br.ReadUInt32Reverse(); // Head count + m_br.ReadUInt32Reverse(); // Sector count + + md5 = m_br.ReadBytes(16); + m_br.ReadBytes(16); // Parent MD5 + m_br.ReadUInt32Reverse(); // Sector size + + return md5; + } + + /// + /// Parse a CHD v3 header + /// + /// The extracted SHA-1 on success, null otherwise + private byte[] ParseCHDv3Header() + { + // Seek to after the signature to make sure we're reading the correct bytes + m_br.BaseStream.Seek(16, SeekOrigin.Begin); + + // Set the blank SHA-1 hash + byte[] sha1 = new byte[20]; + + // Set offsets and defaults + m_mapoffset = 120; + m_mapentrybytes = 16; + + // Read the CHD flags + uint flags = m_br.ReadUInt32Reverse(); + + // Determine compression + switch (m_br.ReadUInt32()) + { + case 0: m_compression[0] = CHD_CODEC.NONE; break; + case 1: m_compression[0] = CHD_CODEC.ZLIB; break; + case 2: m_compression[0] = CHD_CODEC.ZLIB; break; + case 3: m_compression[0] = CHD_CODEC.AVHUFF; break; + default: /* throw CHDERR_UNKNOWN_COMPRESSION; */ return null; + } + + m_compression[1] = m_compression[2] = m_compression[3] = CHD_CODEC.NONE; + + m_hunkcount = m_br.ReadUInt32Reverse(); + m_logicalbytes = m_br.ReadUInt64Reverse(); + m_metaoffset = m_br.ReadUInt32Reverse(); + + m_br.BaseStream.Seek(76, SeekOrigin.Begin); + m_hunkbytes = m_br.ReadUInt32Reverse(); + + m_br.BaseStream.Seek(Constants.CHDv3SHA1Offset, SeekOrigin.Begin); + sha1 = m_br.ReadBytes(20); + + // guess at the units based on snooping the metadata + // m_unitbytes = guess_unitbytes(); + m_unitcount = (m_logicalbytes + m_unitbytes - 1) / m_unitbytes; + + return sha1; + } + + /// + /// Parse a CHD v4 header + /// + /// The extracted SHA-1 on success, null otherwise + private byte[] ParseCHDv4Header() + { + // Seek to after the signature to make sure we're reading the correct bytes + m_br.BaseStream.Seek(16, SeekOrigin.Begin); + + // Set the blank SHA-1 hash + byte[] sha1 = new byte[20]; + + // Set offsets and defaults + m_mapoffset = 108; + m_mapentrybytes = 16; + + // Read the CHD flags + uint flags = m_br.ReadUInt32Reverse(); + + // Determine compression + switch (m_br.ReadUInt32()) + { + case 0: m_compression[0] = CHD_CODEC.NONE; break; + case 1: m_compression[0] = CHD_CODEC.ZLIB; break; + case 2: m_compression[0] = CHD_CODEC.ZLIB; break; + case 3: m_compression[0] = CHD_CODEC.AVHUFF; break; + default: /* throw CHDERR_UNKNOWN_COMPRESSION; */ return null; + } + + m_compression[1] = m_compression[2] = m_compression[3] = CHD_CODEC.NONE; + + m_hunkcount = m_br.ReadUInt32Reverse(); + m_logicalbytes = m_br.ReadUInt64Reverse(); + m_metaoffset = m_br.ReadUInt32Reverse(); + + m_br.BaseStream.Seek(44, SeekOrigin.Begin); + m_hunkbytes = m_br.ReadUInt32Reverse(); + + m_br.BaseStream.Seek(Constants.CHDv4SHA1Offset, SeekOrigin.Begin); + sha1 = m_br.ReadBytes(20); + + // guess at the units based on snooping the metadata + // m_unitbytes = guess_unitbytes(); + m_unitcount = (m_logicalbytes + m_unitbytes - 1) / m_unitbytes; + return sha1; + } + + /// + /// Parse a CHD v5 header + /// + /// The extracted SHA-1 on success, null otherwise + private byte[] ParseCHDv5Header() + { + // Seek to after the signature to make sure we're reading the correct bytes + m_br.BaseStream.Seek(16, SeekOrigin.Begin); + + // Set the blank SHA-1 hash + byte[] sha1 = new byte[20]; + + // Determine compression + m_compression[0] = (CHD_CODEC)m_br.ReadUInt32Reverse(); + m_compression[1] = (CHD_CODEC)m_br.ReadUInt32Reverse(); + m_compression[2] = (CHD_CODEC)m_br.ReadUInt32Reverse(); + m_compression[3] = (CHD_CODEC)m_br.ReadUInt32Reverse(); + + m_logicalbytes = m_br.ReadUInt64Reverse(); + m_mapoffset = m_br.ReadUInt64Reverse(); + m_metaoffset = m_br.ReadUInt64Reverse(); + m_hunkbytes = m_br.ReadUInt32Reverse(); + m_hunkcount = (m_logicalbytes + m_hunkbytes - 1) / m_hunkbytes; + m_unitbytes = m_br.ReadUInt32Reverse(); + m_unitcount = (m_logicalbytes + m_unitbytes - 1) / m_unitbytes; + + // m_allow_writes = !compressed(); + + // determine properties of map entries + // m_mapentrybytes = compressed() ? 12 : 4; + + m_br.BaseStream.Seek(Constants.CHDv5SHA1Offset, SeekOrigin.Begin); + sha1 = m_br.ReadBytes(20); + return sha1; + } + + #endregion + } } diff --git a/SabreTools.Library/FileTypes/CoreRarArchive.cs b/SabreTools.Library/FileTypes/CoreRarArchive.cs index 8081878f..47ccbb2e 100644 --- a/SabreTools.Library/FileTypes/CoreRarArchive.cs +++ b/SabreTools.Library/FileTypes/CoreRarArchive.cs @@ -117,223 +117,223 @@ using SabreTools.Library.DatItems; /// namespace SabreTools.Library.FileTypes { - public class CoreRarArchive : BaseArchive - { - // SFX Module Information - public byte[] SFX; + public class CoreRarArchive : BaseArchive + { + // SFX Module Information + public byte[] SFX; - // Standard Header Information - public uint HeaderCRC32; - public uint HeaderSize; // vint - public RarHeaderFlags HeaderFlags; // vint - public uint ExtraAreaSize; // vint - public RarArchiveFlags ArchiveFlags; // vint - public uint VolumeNumber; // vint - public byte[] ExtraArea; + // Standard Header Information + public uint HeaderCRC32; + public uint HeaderSize; // vint + public RarHeaderFlags HeaderFlags; // vint + public uint ExtraAreaSize; // vint + public RarArchiveFlags ArchiveFlags; // vint + public uint VolumeNumber; // vint + public byte[] ExtraArea; - // Encryption Header Information - public uint EncryptionHeaderCRC32; - public uint EncryptionHeaderSize; // vint - public RarHeaderFlags EncryptionHeaderFlags; // vint - public uint EncryptionVersion; // vint - public uint EncryptionFlags; // vint - public byte KDFCount; - public byte[] Salt = new byte[16]; - public byte[] CheckValue = new byte[12]; + // Encryption Header Information + public uint EncryptionHeaderCRC32; + public uint EncryptionHeaderSize; // vint + public RarHeaderFlags EncryptionHeaderFlags; // vint + public uint EncryptionVersion; // vint + public uint EncryptionFlags; // vint + public byte KDFCount; + public byte[] Salt = new byte[16]; + public byte[] CheckValue = new byte[12]; - // Locator Information - public uint LocatorSize; // vint - public uint LocatorFlags; // vint - public uint QuickOpenOffset; // vint - public uint RecoveryRecordOffset; // vint + // Locator Information + public uint LocatorSize; // vint + public uint LocatorFlags; // vint + public uint QuickOpenOffset; // vint + public uint RecoveryRecordOffset; // vint - // Entry Information - public List Entries = new List(); + // Entry Information + public List Entries = new List(); - #region Unimplemented methods + #region Unimplemented methods - public override bool CopyAll(string outDir) - { - throw new NotImplementedException(); - } + public override bool CopyAll(string outDir) + { + throw new NotImplementedException(); + } - public override string CopyToFile(string entryName, string outDir) - { - throw new NotImplementedException(); - } + public override string CopyToFile(string entryName, string outDir) + { + throw new NotImplementedException(); + } - public override (MemoryStream, string) CopyToStream(string entryName) - { - throw new NotImplementedException(); - } + public override (MemoryStream, string) CopyToStream(string entryName) + { + throw new NotImplementedException(); + } - public override List GetChildren(Hash omitFromScan = Hash.DeepHashes, bool date = false) - { - throw new NotImplementedException(); - } + public override List GetChildren(Hash omitFromScan = Hash.DeepHashes, bool date = false) + { + throw new NotImplementedException(); + } - public override List GetEmptyFolders() - { - throw new NotImplementedException(); - } + public override List GetEmptyFolders() + { + throw new NotImplementedException(); + } - public override bool IsTorrent() - { - throw new NotImplementedException(); - } + public override bool IsTorrent() + { + throw new NotImplementedException(); + } - public override bool Write(string inputFile, string outDir, Rom rom, bool date = false, bool romba = false) - { - throw new NotImplementedException(); - } + public override bool Write(string inputFile, string outDir, Rom rom, bool date = false, bool romba = false) + { + throw new NotImplementedException(); + } - public override bool Write(Stream inputStream, string outDir, Rom rom, bool date = false, bool romba = false) - { - throw new NotImplementedException(); - } + public override bool Write(Stream inputStream, string outDir, Rom rom, bool date = false, bool romba = false) + { + throw new NotImplementedException(); + } - public override bool Write(List inputFiles, string outDir, List roms, bool date = false, bool romba = false) - { - throw new NotImplementedException(); - } + public override bool Write(List inputFiles, string outDir, List roms, bool date = false, bool romba = false) + { + throw new NotImplementedException(); + } - #endregion - } + #endregion + } - public class CoreRarArchiveEntry - { - // Standard Entry Information - public uint HeaderCRC32; - public uint HeaderSize; // vint - public RarHeaderType HeaderType; // vint - public RarHeaderFlags HeaderFlags; // vint - public uint ExtraAreaSize; // vint - public uint DataAreaSize; // vint - public RarFileFlags FileFlags; // vint - public uint UnpackedSize; // vint - public uint Attributes; // vint - public uint mtime; - public uint DataCRC32; - public uint CompressionInformation; // vint - public uint HostOS; // vint - public uint NameLength; // vint - public byte[] Name; - public byte[] DataArea; + public class CoreRarArchiveEntry + { + // Standard Entry Information + public uint HeaderCRC32; + public uint HeaderSize; // vint + public RarHeaderType HeaderType; // vint + public RarHeaderFlags HeaderFlags; // vint + public uint ExtraAreaSize; // vint + public uint DataAreaSize; // vint + public RarFileFlags FileFlags; // vint + public uint UnpackedSize; // vint + public uint Attributes; // vint + public uint mtime; + public uint DataCRC32; + public uint CompressionInformation; // vint + public uint HostOS; // vint + public uint NameLength; // vint + public byte[] Name; + public byte[] DataArea; - // File Encryption Information - public uint EncryptionSize; // vint - public RarEncryptionFlags EncryptionFlags; // vint - public byte KDFCount; - public byte[] Salt = new byte[16]; - public byte[] IV = new byte[16]; - public byte[] CheckValue = new byte[12]; + // File Encryption Information + public uint EncryptionSize; // vint + public RarEncryptionFlags EncryptionFlags; // vint + public byte KDFCount; + public byte[] Salt = new byte[16]; + public byte[] IV = new byte[16]; + public byte[] CheckValue = new byte[12]; - // File Hash Information - public uint HashSize; // vint - public uint HashType; // vint - public byte[] HashData = new byte[32]; + // File Hash Information + public uint HashSize; // vint + public uint HashType; // vint + public byte[] HashData = new byte[32]; - // File Time Information - public uint TimeSize; // vint - public RarTimeFlags TimeFlags; // vint - public uint TimeMtime; - public ulong TimeMtime64; - public uint TimeCtime; - public ulong TimeCtime64; - public uint TimeLtime; - public ulong TimeLtime64; + // File Time Information + public uint TimeSize; // vint + public RarTimeFlags TimeFlags; // vint + public uint TimeMtime; + public ulong TimeMtime64; + public uint TimeCtime; + public ulong TimeCtime64; + public uint TimeLtime; + public ulong TimeLtime64; - // File Version Information - public uint VersionSize; // vint - public const uint VersionFlags = 0; // vint - public uint VersionNumber; // vint + // File Version Information + public uint VersionSize; // vint + public const uint VersionFlags = 0; // vint + public uint VersionNumber; // vint - // File System Redirection Record - public uint RedirectionSize; // vint - public RarRedirectionType RedirectionType; // vint - public uint RedirectionFlags; // vint - public uint RedirectionNameLength; // vint - public byte[] RedirectionName; + // File System Redirection Record + public uint RedirectionSize; // vint + public RarRedirectionType RedirectionType; // vint + public uint RedirectionFlags; // vint + public uint RedirectionNameLength; // vint + public byte[] RedirectionName; - // Unix Owner Record - public uint UnixOwnerSize; // vint - public RarUnixOwnerRecordFlags UnixOwnerFlags; // vint - public uint UnixOwnerUserNameLength; // vint - public byte[] UnixOwnerUserName; - public uint UnixOwnerGroupNameLength; // vint - public byte[] UnixOwnerGroupName; - public uint UnixOwnerUserId; // vint - public uint UnixOwnerGroupId; // vint + // Unix Owner Record + public uint UnixOwnerSize; // vint + public RarUnixOwnerRecordFlags UnixOwnerFlags; // vint + public uint UnixOwnerUserNameLength; // vint + public byte[] UnixOwnerUserName; + public uint UnixOwnerGroupNameLength; // vint + public byte[] UnixOwnerGroupName; + public uint UnixOwnerUserId; // vint + public uint UnixOwnerGroupId; // vint - // Service Data Information - public uint ServiceSize; // vint - public byte[] ServiceData; - } + // Service Data Information + public uint ServiceSize; // vint + public byte[] ServiceData; + } - // BELOW ARE CONCRETE IMPLEMENTATIONS OF HEADER DETAILS + // BELOW ARE CONCRETE IMPLEMENTATIONS OF HEADER DETAILS - /// - /// General archive block format used by all RAR block types - /// - public class GeneralArchiveBlockFormat - { - public uint HeaderCRC32; - public uint HeaderSize; // vint - public HeaderType HeaderType; - public HeaderFlags HeaderFlags; - public ulong ExtraAreaSize; // vint - public ulong DataAreaSize; // vint - public byte[] ExtraArea; - public byte[] DataArea; - } + /// + /// General archive block format used by all RAR block types + /// + public class GeneralArchiveBlockFormat + { + public uint HeaderCRC32; + public uint HeaderSize; // vint + public HeaderType HeaderType; + public HeaderFlags HeaderFlags; + public ulong ExtraAreaSize; // vint + public ulong DataAreaSize; // vint + public byte[] ExtraArea; + public byte[] DataArea; + } - /// - /// General extra area format used by all RAR extra area records - /// - public class GeneralExtraAreaFormat - { - public ulong Size; // vint - public ulong Type; // vint - public byte[] Data; - } + /// + /// General extra area format used by all RAR extra area records + /// + public class GeneralExtraAreaFormat + { + public ulong Size; // vint + public ulong Type; // vint + public byte[] Data; + } - /// - /// Encryption header only present in encrypted archives - /// - /// Every proceeding header is started from 16 byte AES-256 - /// initialization vectors followed by encrypted header data - /// - public class ArchiveEncryptionHeader : GeneralArchiveBlockFormat - { - public new HeaderType HeaderType = HeaderType.ArchiveEncryptionHeader; - public ulong EncryptionVersion; // vint - public ulong EncryptionFlags; // vint - } + /// + /// Encryption header only present in encrypted archives + /// + /// Every proceeding header is started from 16 byte AES-256 + /// initialization vectors followed by encrypted header data + /// + public class ArchiveEncryptionHeader : GeneralArchiveBlockFormat + { + public new HeaderType HeaderType = HeaderType.ArchiveEncryptionHeader; + public ulong EncryptionVersion; // vint + public ulong EncryptionFlags; // vint + } - /// - /// Types of archive header - /// - public enum HeaderType : ulong // vint - { - MainArchiveHeader = 1, - FileHeader = 2, - ServiceHeader = 3, - ArchiveEncryptionHeader = 4, - EndOfArchiveHeader = 5, - } + /// + /// Types of archive header + /// + public enum HeaderType : ulong // vint + { + MainArchiveHeader = 1, + FileHeader = 2, + ServiceHeader = 3, + ArchiveEncryptionHeader = 4, + EndOfArchiveHeader = 5, + } - /// - /// Flags common for all headers - /// - [Flags] - public enum HeaderFlags : ulong // vint - { - ExtraAreaIsPresentInEndOfHeader = 0x0001, - DataAreaIsPresentInEndOfHeader = 0x0002, - BlocksWithUnknownType = 0x0004, // this flag must be skipped when updating an archive - DataAreaIsContinuingFromPreviousVolume = 0x0008, - DataAreaIsContinuingInNextVolume = 0x0010, - BlockDependsOnPrecedingFileBlock = 0x0020, - PreserveChildBlockIfHostBlockIsModified = 0x0040, - } + /// + /// Flags common for all headers + /// + [Flags] + public enum HeaderFlags : ulong // vint + { + ExtraAreaIsPresentInEndOfHeader = 0x0001, + DataAreaIsPresentInEndOfHeader = 0x0002, + BlocksWithUnknownType = 0x0004, // this flag must be skipped when updating an archive + DataAreaIsContinuingFromPreviousVolume = 0x0008, + DataAreaIsContinuingInNextVolume = 0x0010, + BlockDependsOnPrecedingFileBlock = 0x0020, + PreserveChildBlockIfHostBlockIsModified = 0x0040, + } } diff --git a/SabreTools.Library/FileTypes/Folder.cs b/SabreTools.Library/FileTypes/Folder.cs index 973c3719..ba0f0cd3 100644 --- a/SabreTools.Library/FileTypes/Folder.cs +++ b/SabreTools.Library/FileTypes/Folder.cs @@ -23,305 +23,305 @@ using Stream = System.IO.Stream; namespace SabreTools.Library.FileTypes { - /// - /// Represents a folder for reading and writing - /// - public class Folder : BaseFile - { - #region Protected instance variables + /// + /// Represents a folder for reading and writing + /// + public class Folder : BaseFile + { + #region Protected instance variables - protected List _children; + protected List _children; - #endregion + #endregion - #region Constructors + #region Constructors - /// - /// Create a new folder with no base file - /// - public Folder() - : base() - { - _fileType = FileType.Folder; - } + /// + /// Create a new folder with no base file + /// + public Folder() + : base() + { + this.Type = FileType.Folder; + } - /// - /// Create a new folder from the given file - /// - /// Name of the file to use as an archive - /// True for opening file as read, false for opening file as write - /// True if hashes for this file should be calculated, false otherwise (default) - public Folder(string filename, bool getHashes = false) - : base(filename, getHashes) - { - _fileType = FileType.Folder; - } + /// + /// Create a new folder from the given file + /// + /// Name of the file to use as an archive + /// True for opening file as read, false for opening file as write + /// True if hashes for this file should be calculated, false otherwise (default) + public Folder(string filename, bool getHashes = false) + : base(filename, getHashes) + { + this.Type = FileType.Folder; + } - #endregion + #endregion - #region Extraction + #region Extraction - /// - /// Attempt to extract a file as an archive - /// - /// Output directory for archive extraction - /// True if the extraction was a success, false otherwise - public virtual bool CopyAll(string outDir) - { - // Copy all files from the current folder to the output directory recursively - try - { - // Make sure the folders exist - Directory.CreateDirectory(_filename); - Directory.CreateDirectory(outDir); + /// + /// Attempt to extract a file as an archive + /// + /// Output directory for archive extraction + /// True if the extraction was a success, false otherwise + public virtual bool CopyAll(string outDir) + { + // Copy all files from the current folder to the output directory recursively + try + { + // Make sure the folders exist + Directory.CreateDirectory(this.Filename); + Directory.CreateDirectory(outDir); - Directory.Copy(_filename, outDir, true, PathFormat.FullPath); - } - catch (Exception ex) - { - Globals.Logger.Error(ex.ToString()); - return false; - } + Directory.Copy(this.Filename, outDir, true, PathFormat.FullPath); + } + catch (Exception ex) + { + Globals.Logger.Error(ex.ToString()); + return false; + } - return true; - } + return true; + } - /// - /// Attempt to extract a file from an archive - /// - /// Name of the entry to be extracted - /// Output directory for archive extraction - /// Name of the extracted file, null on error - public virtual string CopyToFile(string entryName, string outDir) - { - string realentry = null; + /// + /// Attempt to extract a file from an archive + /// + /// Name of the entry to be extracted + /// Output directory for archive extraction + /// Name of the extracted file, null on error + public virtual string CopyToFile(string entryName, string outDir) + { + string realentry = null; - // Copy single file from the current folder to the output directory, if exists - try - { - // Make sure the folders exist - Directory.CreateDirectory(_filename); - Directory.CreateDirectory(outDir); + // Copy single file from the current folder to the output directory, if exists + try + { + // Make sure the folders exist + Directory.CreateDirectory(this.Filename); + Directory.CreateDirectory(outDir); - // Get all files from the input directory - List files = Utilities.RetrieveFiles(_filename, new List()); + // Get all files from the input directory + List files = Utilities.RetrieveFiles(this.Filename, new List()); - // Now sort through to find the first file that matches - string match = files.Where(s => s.EndsWith(entryName)).FirstOrDefault(); + // Now sort through to find the first file that matches + string match = files.Where(s => s.EndsWith(entryName)).FirstOrDefault(); - // If we had a file, copy that over to the new name - if (!String.IsNullOrWhiteSpace(match)) - { - realentry = match; - File.Copy(match, Path.Combine(outDir, entryName)); - } - } - catch (Exception ex) - { - Globals.Logger.Error(ex.ToString()); - return realentry; - } + // If we had a file, copy that over to the new name + if (!String.IsNullOrWhiteSpace(match)) + { + realentry = match; + File.Copy(match, Path.Combine(outDir, entryName)); + } + } + catch (Exception ex) + { + Globals.Logger.Error(ex.ToString()); + return realentry; + } - return realentry; - } + return realentry; + } - /// - /// Attempt to extract a stream from an archive - /// - /// Name of the entry to be extracted - /// Output representing the entry name that was found - /// MemoryStream representing the entry, null on error - public virtual (MemoryStream, string) CopyToStream(string entryName) - { - MemoryStream ms = new MemoryStream(); - string realentry = null; + /// + /// Attempt to extract a stream from an archive + /// + /// Name of the entry to be extracted + /// Output representing the entry name that was found + /// MemoryStream representing the entry, null on error + public virtual (MemoryStream, string) CopyToStream(string entryName) + { + MemoryStream ms = new MemoryStream(); + string realentry = null; - // Copy single file from the current folder to the output directory, if exists - try - { - // Make sure the folders exist - Directory.CreateDirectory(_filename); + // Copy single file from the current folder to the output directory, if exists + try + { + // Make sure the folders exist + Directory.CreateDirectory(this.Filename); - // Get all files from the input directory - List files = Utilities.RetrieveFiles(_filename, new List()); + // Get all files from the input directory + List files = Utilities.RetrieveFiles(this.Filename, new List()); - // Now sort through to find the first file that matches - string match = files.Where(s => s.EndsWith(entryName)).FirstOrDefault(); + // Now sort through to find the first file that matches + string match = files.Where(s => s.EndsWith(entryName)).FirstOrDefault(); - // If we had a file, copy that over to the new name - if (!String.IsNullOrWhiteSpace(match)) - { - Utilities.TryOpenRead(match).CopyTo(ms); - realentry = match; - } - } - catch (Exception ex) - { - Globals.Logger.Error(ex.ToString()); - return (ms, realentry); - } + // If we had a file, copy that over to the new name + if (!String.IsNullOrWhiteSpace(match)) + { + Utilities.TryOpenRead(match).CopyTo(ms); + realentry = match; + } + } + catch (Exception ex) + { + Globals.Logger.Error(ex.ToString()); + return (ms, realentry); + } - return (ms, realentry); - } + return (ms, realentry); + } - #endregion + #endregion - #region Information + #region Information - /// - /// Generate a list of immediate children from the current folder - /// - /// Hash representing the hashes that should be skipped - /// True if entry dates should be included, false otherwise (default) - /// List of BaseFile objects representing the found data - /// TODO: All instances of Hash.DeepHashes should be made into 0x0 eventually - public virtual List GetChildren(Hash omitFromScan = Hash.DeepHashes, bool date = false) - { - if (_children == null || _children.Count == 0) - { - _children = new List(); - foreach (string file in Directory.EnumerateFiles(_filename, "*", SearchOption.TopDirectoryOnly)) - { - BaseFile nf = Utilities.GetFileInfo(file, omitFromScan: omitFromScan, date: date); - _children.Add(nf); - } - foreach (string dir in Directory.EnumerateDirectories(_filename, "*", SearchOption.TopDirectoryOnly)) - { - Folder fl = new Folder(dir); - _children.Add(fl); - } - } + /// + /// Generate a list of immediate children from the current folder + /// + /// Hash representing the hashes that should be skipped + /// True if entry dates should be included, false otherwise (default) + /// List of BaseFile objects representing the found data + /// TODO: All instances of Hash.DeepHashes should be made into 0x0 eventually + public virtual List GetChildren(Hash omitFromScan = Hash.DeepHashes, bool date = false) + { + if (_children == null || _children.Count == 0) + { + _children = new List(); + foreach (string file in Directory.EnumerateFiles(this.Filename, "*", SearchOption.TopDirectoryOnly)) + { + BaseFile nf = Utilities.GetFileInfo(file, omitFromScan: omitFromScan, date: date); + _children.Add(nf); + } + foreach (string dir in Directory.EnumerateDirectories(this.Filename, "*", SearchOption.TopDirectoryOnly)) + { + Folder fl = new Folder(dir); + _children.Add(fl); + } + } - return _children; - } + return _children; + } - /// - /// Generate a list of empty folders in an archive - /// - /// Input file to get data from - /// List of empty folders in the folder - public virtual List GetEmptyFolders() - { - return Utilities.GetEmptyDirectories(_filename).ToList(); - } + /// + /// Generate a list of empty folders in an archive + /// + /// Input file to get data from + /// List of empty folders in the folder + public virtual List GetEmptyFolders() + { + return Utilities.GetEmptyDirectories(this.Filename).ToList(); + } - #endregion + #endregion - #region Writing + #region Writing - /// - /// Write an input file to an output folder - /// - /// Input filename to be moved - /// Output directory to build to - /// DatItem representing the new information - /// True if the date from the DAT should be used if available, false otherwise (default) - /// True if files should be output in Romba depot folders, false otherwise - /// True if the write was a success, false otherwise - /// This works for now, but it can be sped up by using Ionic.Zip or another zlib wrapper that allows for header values built-in. See edc's code. - public virtual bool Write(string inputFile, string outDir, Rom rom, bool date = false, bool romba = false) - { - FileStream fs = Utilities.TryOpenRead(inputFile); - return Write(fs, outDir, rom, date, romba); - } + /// + /// Write an input file to an output folder + /// + /// Input filename to be moved + /// Output directory to build to + /// DatItem representing the new information + /// True if the date from the DAT should be used if available, false otherwise (default) + /// True if files should be output in Romba depot folders, false otherwise + /// True if the write was a success, false otherwise + /// This works for now, but it can be sped up by using Ionic.Zip or another zlib wrapper that allows for header values built-in. See edc's code. + public virtual bool Write(string inputFile, string outDir, Rom rom, bool date = false, bool romba = false) + { + FileStream fs = Utilities.TryOpenRead(inputFile); + return Write(fs, outDir, rom, date, romba); + } - /// - /// Write an input stream to an output folder - /// - /// Input stream to be moved - /// Output directory to build to - /// DatItem representing the new information - /// True if the date from the DAT should be used if available, false otherwise (default) - /// True if files should be output in Romba depot folders, false otherwise - /// True if the write was a success, false otherwise - /// This works for now, but it can be sped up by using Ionic.Zip or another zlib wrapper that allows for header values built-in. See edc's code. - public virtual bool Write(Stream inputStream, string outDir, Rom rom, bool date = false, bool romba = false) - { - bool success = false; + /// + /// Write an input stream to an output folder + /// + /// Input stream to be moved + /// Output directory to build to + /// DatItem representing the new information + /// True if the date from the DAT should be used if available, false otherwise (default) + /// True if files should be output in Romba depot folders, false otherwise + /// True if the write was a success, false otherwise + /// This works for now, but it can be sped up by using Ionic.Zip or another zlib wrapper that allows for header values built-in. See edc's code. + public virtual bool Write(Stream inputStream, string outDir, Rom rom, bool date = false, bool romba = false) + { + bool success = false; - // If either input is null or empty, return - if (inputStream == null || rom == null || rom.Name == null) - { - return success; - } + // If either input is null or empty, return + if (inputStream == null || rom == null || rom.Name == null) + { + return success; + } - // If the stream is not readable, return - if (!inputStream.CanRead) - { - return success; - } + // If the stream is not readable, return + if (!inputStream.CanRead) + { + return success; + } - // Set internal variables - FileStream outputStream = null; + // Set internal variables + FileStream outputStream = null; - // Get the output folder name from the first rebuild rom - string fileName = Path.Combine(outDir, Utilities.RemovePathUnsafeCharacters(rom.MachineName), Utilities.RemovePathUnsafeCharacters(rom.Name)); + // Get the output folder name from the first rebuild rom + string fileName = Path.Combine(outDir, Utilities.RemovePathUnsafeCharacters(rom.MachineName), Utilities.RemovePathUnsafeCharacters(rom.Name)); - try - { - // If the full output path doesn't exist, create it - if (!Directory.Exists(Path.GetDirectoryName(fileName))) - { - Directory.CreateDirectory(Path.GetDirectoryName(fileName)); - } + try + { + // If the full output path doesn't exist, create it + if (!Directory.Exists(Path.GetDirectoryName(fileName))) + { + Directory.CreateDirectory(Path.GetDirectoryName(fileName)); + } - // Overwrite output files by default - outputStream = Utilities.TryCreate(fileName); + // Overwrite output files by default + outputStream = Utilities.TryCreate(fileName); - // If the output stream isn't null - if (outputStream != null) - { - // Copy the input stream to the output - inputStream.Seek(0, SeekOrigin.Begin); - int bufferSize = 4096 * 128; - byte[] ibuffer = new byte[bufferSize]; - int ilen; - while ((ilen = inputStream.Read(ibuffer, 0, bufferSize)) > 0) - { - outputStream.Write(ibuffer, 0, ilen); - outputStream.Flush(); - } - outputStream.Dispose(); + // If the output stream isn't null + if (outputStream != null) + { + // Copy the input stream to the output + inputStream.Seek(0, SeekOrigin.Begin); + int bufferSize = 4096 * 128; + byte[] ibuffer = new byte[bufferSize]; + int ilen; + while ((ilen = inputStream.Read(ibuffer, 0, bufferSize)) > 0) + { + outputStream.Write(ibuffer, 0, ilen); + outputStream.Flush(); + } + outputStream.Dispose(); - if (rom.ItemType == ItemType.Rom) - { - if (date && !String.IsNullOrWhiteSpace(((Rom)rom).Date)) - { - File.SetCreationTime(fileName, DateTime.Parse(((Rom)rom).Date)); - } - } + if (rom.ItemType == ItemType.Rom) + { + if (date && !String.IsNullOrWhiteSpace(((Rom)rom).Date)) + { + File.SetCreationTime(fileName, DateTime.Parse(((Rom)rom).Date)); + } + } - success = true; - } - } - catch (Exception ex) - { - Console.WriteLine(ex); - success = false; - } - finally - { - inputStream.Dispose(); - outputStream?.Dispose(); - } + success = true; + } + } + catch (Exception ex) + { + Console.WriteLine(ex); + success = false; + } + finally + { + inputStream.Dispose(); + outputStream?.Dispose(); + } - return success; - } + return success; + } - /// - /// Write a set of input files to an output folder (assuming the same output archive name) - /// - /// Input files to be moved - /// Output directory to build to - /// DatItem representing the new information - /// True if the date from the DAT should be used if available, false otherwise (default) - /// True if files should be output in Romba depot folders, false otherwise - /// True if the archive was written properly, false otherwise - public virtual bool Write(List inputFiles, string outDir, List roms, bool date = false, bool romba = false) - { - throw new NotImplementedException(); - } + /// + /// Write a set of input files to an output folder (assuming the same output archive name) + /// + /// Input files to be moved + /// Output directory to build to + /// DatItem representing the new information + /// True if the date from the DAT should be used if available, false otherwise (default) + /// True if files should be output in Romba depot folders, false otherwise + /// True if the archive was written properly, false otherwise + public virtual bool Write(List inputFiles, string outDir, List roms, bool date = false, bool romba = false) + { + throw new NotImplementedException(); + } - #endregion - } + #endregion + } } diff --git a/SabreTools.Library/FileTypes/GZipArchive.cs b/SabreTools.Library/FileTypes/GZipArchive.cs index cbd47bf7..0314ab23 100644 --- a/SabreTools.Library/FileTypes/GZipArchive.cs +++ b/SabreTools.Library/FileTypes/GZipArchive.cs @@ -24,528 +24,528 @@ using Ionic.Zlib; namespace SabreTools.Library.FileTypes { - /// - /// Represents a TorrentGZip archive for reading and writing - /// - public class GZipArchive : BaseArchive - { - #region Constructors - - /// - /// Create a new TorrentGZipArchive with no base file - /// - public GZipArchive() - : base() - { - _fileType = FileType.GZipArchive; - } - - /// - /// Create a new TorrentGZipArchive from the given file - /// - /// Name of the file to use as an archive - /// True for opening file as read, false for opening file as write - /// True if hashes for this file should be calculated, false otherwise (default) - public GZipArchive(string filename, bool getHashes = false) - : base(filename, getHashes) - { - _fileType = FileType.GZipArchive; - } - - #endregion - - #region Extraction - - /// - /// Attempt to extract a file as an archive - /// - /// Output directory for archive extraction - /// True if the extraction was a success, false otherwise - public override bool CopyAll(string outDir) - { - bool encounteredErrors = true; - - try - { - // Create the temp directory - Directory.CreateDirectory(outDir); - - // Decompress the _filename stream - FileStream outstream = Utilities.TryCreate(Path.Combine(outDir, Path.GetFileNameWithoutExtension(_filename))); - GZipStream gzstream = new GZipStream(Utilities.TryOpenRead(_filename), Ionic.Zlib.CompressionMode.Decompress); - gzstream.CopyTo(outstream); - - // Dispose of the streams - outstream.Dispose(); - gzstream.Dispose(); - - encounteredErrors = false; - } - catch (EndOfStreamException) - { - // Catch this but don't count it as an error because SharpCompress is unsafe - } - catch (InvalidOperationException) - { - encounteredErrors = true; - } - catch (Exception ex) - { - Globals.Logger.Error(ex.ToString()); - encounteredErrors = true; - } - - return encounteredErrors; - } - - /// - /// Attempt to extract a file from an archive - /// - /// Name of the entry to be extracted - /// Output directory for archive extraction - /// Name of the extracted file, null on error - public override string CopyToFile(string entryName, string outDir) - { - // Try to extract a stream using the given information - (MemoryStream ms, string realEntry) = CopyToStream(entryName); - - // If the memory stream and the entry name are both non-null, we write to file - if (ms != null && realEntry != null) - { - realEntry = Path.Combine(outDir, realEntry); - - // Create the output subfolder now - Directory.CreateDirectory(Path.GetDirectoryName(realEntry)); - - // Now open and write the file if possible - FileStream fs = Utilities.TryCreate(realEntry); - if (fs != null) - { - ms.Seek(0, SeekOrigin.Begin); - byte[] zbuffer = new byte[_bufferSize]; - int zlen; - while ((zlen = ms.Read(zbuffer, 0, _bufferSize)) > 0) - { - fs.Write(zbuffer, 0, zlen); - fs.Flush(); - } - - ms?.Dispose(); - fs?.Dispose(); - } - else - { - ms?.Dispose(); - fs?.Dispose(); - realEntry = null; - } - } - - return realEntry; - } - - /// - /// Attempt to extract a stream from an archive - /// - /// Name of the entry to be extracted - /// Output representing the entry name that was found - /// MemoryStream representing the entry, null on error - public override (MemoryStream, string) CopyToStream(string entryName) - { - MemoryStream ms = new MemoryStream(); - string realEntry = null; - - try - { - // Decompress the _filename stream - realEntry = Path.GetFileNameWithoutExtension(_filename); - GZipStream gzstream = new GZipStream(Utilities.TryOpenRead(_filename), Ionic.Zlib.CompressionMode.Decompress); - - // Write the file out - byte[] gbuffer = new byte[_bufferSize]; - int glen; - while ((glen = gzstream.Read(gbuffer, 0, _bufferSize)) > 0) - { - - ms.Write(gbuffer, 0, glen); - ms.Flush(); - } - - // Dispose of the streams - gzstream.Dispose(); - } - catch (Exception ex) - { - Globals.Logger.Error(ex.ToString()); - ms = null; - realEntry = null; - } - - return (ms, realEntry); - } - - #endregion - - #region Information - - /// - /// Generate a list of DatItem objects from the header values in an archive - /// - /// Hash representing the hashes that should be skipped - /// True if entry dates should be included, false otherwise (default) - /// List of DatItem objects representing the found data - /// TODO: All instances of Hash.DeepHashes should be made into 0x0 eventually - public override List GetChildren(Hash omitFromScan = Hash.DeepHashes, bool date = false) - { - if (_children == null || _children.Count == 0) - { - _children = new List(); - - string gamename = Path.GetFileNameWithoutExtension(_filename); - - BaseFile possibleTgz = GetTorrentGZFileInfo(); - - // If it was, then add it to the outputs and continue - if (possibleTgz != null && possibleTgz.Filename != null) - { - _children.Add(possibleTgz); - } - else - { - try - { - // If secure hashes are disabled, do a quickscan - if (omitFromScan == Hash.SecureHashes) - { - BaseFile tempRom = new BaseFile() - { - Filename = gamename, - }; - BinaryReader br = new BinaryReader(Utilities.TryOpenRead(_filename)); - br.BaseStream.Seek(-8, SeekOrigin.End); - byte[] headercrc = br.ReadBytesReverse(4); - tempRom.CRC = headercrc; - tempRom.Size = br.ReadInt32Reverse(); - br.Dispose(); - - _children.Add(tempRom); - } - // Otherwise, use the stream directly - else - { - GZipStream gzstream = new GZipStream(Utilities.TryOpenRead(_filename), Ionic.Zlib.CompressionMode.Decompress); - BaseFile gzipEntryRom = Utilities.GetStreamInfo(gzstream, gzstream.Length, omitFromScan: omitFromScan); - gzipEntryRom.Filename = gzstream.FileName; - gzipEntryRom.Parent = gamename; - gzipEntryRom.Date = (date && gzstream.LastModified != null ? gzstream.LastModified?.ToString("yyyy/MM/dd hh:mm:ss") : null); - _children.Add(gzipEntryRom); - gzstream.Dispose(); - } - } - catch (Exception ex) - { - Globals.Logger.Error(ex.ToString()); - return null; - } - } - } - - return _children; - } - - /// - /// Generate a list of empty folders in an archive - /// - /// Input file to get data from - /// List of empty folders in the archive - public override List GetEmptyFolders() - { - // GZip files don't contain directories - return new List(); - } - - /// - /// Check whether the input file is a standardized format - /// - public override bool IsTorrent() - { - // Check for the file existing first - if (!File.Exists(_filename)) - { - return false; - } - - string datum = Path.GetFileName(_filename).ToLowerInvariant(); - long filesize = new FileInfo(_filename).Length; - - // If we have the romba depot files, just skip them gracefully - if (datum == ".romba_size" || datum == ".romba_size.backup") - { - Globals.Logger.Verbose("Romba depot file found, skipping: {0}", _filename); - return false; - } - - // Check if the name is the right length - if (!Regex.IsMatch(datum, @"^[0-9a-f]{" + Constants.SHA1Length + @"}\.gz")) // TODO: When updating to SHA-256, this needs to update to Constants.SHA256Length - { - Globals.Logger.Warning("Non SHA-1 filename found, skipping: '{0}'", Path.GetFullPath(_filename)); - return false; - } - - // Check if the file is at least the minimum length - if (filesize < 40 /* bytes */) - { - Globals.Logger.Warning("Possibly corrupt file '{0}' with size {1}", Path.GetFullPath(_filename), Utilities.GetBytesReadable(filesize)); - return false; - } - - // Get the Romba-specific header data - byte[] header; // Get preamble header for checking - byte[] headermd5; // MD5 - byte[] headercrc; // CRC - ulong headersz; // Int64 size - BinaryReader br = new BinaryReader(Utilities.TryOpenRead(_filename)); - header = br.ReadBytes(12); - headermd5 = br.ReadBytes(16); - headercrc = br.ReadBytes(4); - headersz = br.ReadUInt64(); - br.Dispose(); - - // If the header is not correct, return a blank rom - bool correct = true; - for (int i = 0; i < header.Length; i++) - { - // This is a temp fix to ignore the modification time and OS until romba can be fixed - if (i == 4 || i == 5 || i == 6 || i == 7 || i == 9) - { - continue; - } - correct &= (header[i] == Constants.TorrentGZHeader[i]); - } - if (!correct) - { - return false; - } - - return true; - } - - /// - /// Retrieve file information for a single torrent GZ file - /// - /// Populated DatItem object if success, empty one on error - public BaseFile GetTorrentGZFileInfo() - { - // Check for the file existing first - if (!File.Exists(_filename)) - { - return null; - } - - string datum = Path.GetFileName(_filename).ToLowerInvariant(); - long filesize = new FileInfo(_filename).Length; - - // If we have the romba depot files, just skip them gracefully - if (datum == ".romba_size" || datum == ".romba_size.backup") - { - Globals.Logger.Verbose("Romba depot file found, skipping: {0}", _filename); - return null; - } - - // Check if the name is the right length - if (!Regex.IsMatch(datum, @"^[0-9a-f]{" + Constants.SHA1Length + @"}\.gz")) // TODO: When updating to SHA-256, this needs to update to Constants.SHA256Length - { - Globals.Logger.Warning("Non SHA-1 filename found, skipping: '{0}'", Path.GetFullPath(_filename)); - return null; - } - - // Check if the file is at least the minimum length - if (filesize < 40 /* bytes */) - { - Globals.Logger.Warning("Possibly corrupt file '{0}' with size {1}", Path.GetFullPath(_filename), Utilities.GetBytesReadable(filesize)); - return null; - } - - // Get the Romba-specific header data - byte[] header; // Get preamble header for checking - byte[] headermd5; // MD5 - byte[] headercrc; // CRC - ulong headersz; // Int64 size - BinaryReader br = new BinaryReader(Utilities.TryOpenRead(_filename)); - header = br.ReadBytes(12); - headermd5 = br.ReadBytes(16); - headercrc = br.ReadBytes(4); - headersz = br.ReadUInt64(); - br.Dispose(); - - // If the header is not correct, return a blank rom - bool correct = true; - for (int i = 0; i < header.Length; i++) - { - // This is a temp fix to ignore the modification time and OS until romba can be fixed - if (i == 4 || i == 5 || i == 6 || i == 7 || i == 9) - { - continue; - } - correct &= (header[i] == Constants.TorrentGZHeader[i]); - } - if (!correct) - { - return null; - } - - // Now convert the data and get the right position - long extractedsize = (long)headersz; - - BaseFile baseFile = new BaseFile - { - Filename = Path.GetFileNameWithoutExtension(_filename).ToLowerInvariant(), - Size = extractedsize, - CRC = headercrc, - MD5 = headermd5, - SHA1 = Utilities.StringToByteArray(Path.GetFileNameWithoutExtension(_filename)), // TODO: When updating to SHA-256, this needs to update to SHA256 - - Parent = Path.GetFileNameWithoutExtension(_filename).ToLowerInvariant(), - }; - - return baseFile; - } - - #endregion - - #region Writing - - /// - /// Write an input file to a torrent GZ file - /// - /// Input filename to be moved - /// Output directory to build to - /// DatItem representing the new information - /// True if the date from the DAT should be used if available, false otherwise (default) - /// True if files should be output in Romba depot folders, false otherwise - /// True if the write was a success, false otherwise - /// This works for now, but it can be sped up by using Ionic.Zip or another zlib wrapper that allows for header values built-in. See edc's code. - public override bool Write(string inputFile, string outDir, Rom rom = null, bool date = false, bool romba = false) - { - // Check that the input file exists - if (!File.Exists(inputFile)) - { - Globals.Logger.Warning("File '{0}' does not exist!", inputFile); - return false; - } - inputFile = Path.GetFullPath(inputFile); - - // Get the file stream for the file and write out - return Write(Utilities.TryOpenRead(inputFile), outDir, rom, date, romba); - } - - /// - /// Write an input stream to a torrent GZ file - /// - /// Input stream to be moved - /// Output directory to build to - /// DatItem representing the new information - /// True if the date from the DAT should be used if available, false otherwise (default) - /// True if files should be output in Romba depot folders, false otherwise - /// True if the write was a success, false otherwise - /// This works for now, but it can be sped up by using Ionic.Zip or another zlib wrapper that allows for header values built-in. See edc's code. - public override bool Write(Stream inputStream, string outDir, Rom rom = null, bool date = false, bool romba = false) - { - bool success = false; - - // If the stream is not readable, return - if (!inputStream.CanRead) - { - return success; - } - - // Make sure the output directory exists - if (!Directory.Exists(outDir)) - { - Directory.CreateDirectory(outDir); - } - outDir = Path.GetFullPath(outDir); - - // Now get the Rom info for the file so we have hashes and size - rom = new Rom(Utilities.GetStreamInfo(inputStream, inputStream.Length, keepReadOpen: true)); - - // Get the output file name - string outfile = null; - - // If we have a romba output, add the romba path - if (romba) - { - outfile = Path.Combine(outDir, Utilities.GetRombaPath(rom.SHA1)); // TODO: When updating to SHA-256, this needs to update to SHA256 - - // Check to see if the folder needs to be created - if (!Directory.Exists(Path.GetDirectoryName(outfile))) - { - Directory.CreateDirectory(Path.GetDirectoryName(outfile)); - } - } - // Otherwise, we're just rebuilding to the main directory - else - { - outfile = Path.Combine(outDir, rom.SHA1 + ".gz"); // TODO: When updating to SHA-256, this needs to update to SHA256 - } - - // If the output file exists, don't try to write again - if (!File.Exists(outfile)) - { - // Compress the input stream - FileStream outputStream = Utilities.TryCreate(outfile); - - // Open the output file for writing - BinaryWriter sw = new BinaryWriter(outputStream); - - // Write standard header and TGZ info - byte[] data = Constants.TorrentGZHeader - .Concat(Utilities.StringToByteArray(rom.MD5)) // MD5 - .Concat(Utilities.StringToByteArray(rom.CRC)) // CRC - .ToArray(); - sw.Write(data); - sw.Write((ulong)rom.Size); // Long size (Unsigned, Mirrored) - - // Now create a deflatestream from the input file - DeflateStream ds = new DeflateStream(outputStream, Ionic.Zlib.CompressionMode.Compress, Ionic.Zlib.CompressionLevel.BestCompression, true); - - // Copy the input stream to the output - byte[] ibuffer = new byte[_bufferSize]; - int ilen; - while ((ilen = inputStream.Read(ibuffer, 0, _bufferSize)) > 0) - { - ds.Write(ibuffer, 0, ilen); - ds.Flush(); - } - ds.Dispose(); - - // Now write the standard footer - sw.Write(Utilities.StringToByteArray(rom.CRC).Reverse().ToArray()); - sw.Write((uint)rom.Size); - - // Dispose of everything - sw.Dispose(); - outputStream.Dispose(); - inputStream.Dispose(); - } - - return true; - } - - /// - /// Write a set of input files to a torrent GZ archive (assuming the same output archive name) - /// - /// Input files to be moved - /// Output directory to build to - /// DatItem representing the new information - /// True if the date from the DAT should be used if available, false otherwise (default) - /// True if files should be output in Romba depot folders, false otherwise - /// True if the archive was written properly, false otherwise - public override bool Write(List inputFiles, string outDir, List roms, bool date = false, bool romba = false) - { - throw new NotImplementedException(); - } - - #endregion - } + /// + /// Represents a TorrentGZip archive for reading and writing + /// + public class GZipArchive : BaseArchive + { + #region Constructors + + /// + /// Create a new TorrentGZipArchive with no base file + /// + public GZipArchive() + : base() + { + this.Type = FileType.GZipArchive; + } + + /// + /// Create a new TorrentGZipArchive from the given file + /// + /// Name of the file to use as an archive + /// True for opening file as read, false for opening file as write + /// True if hashes for this file should be calculated, false otherwise (default) + public GZipArchive(string filename, bool getHashes = false) + : base(filename, getHashes) + { + this.Type = FileType.GZipArchive; + } + + #endregion + + #region Extraction + + /// + /// Attempt to extract a file as an archive + /// + /// Output directory for archive extraction + /// True if the extraction was a success, false otherwise + public override bool CopyAll(string outDir) + { + bool encounteredErrors = true; + + try + { + // Create the temp directory + Directory.CreateDirectory(outDir); + + // Decompress the _filename stream + FileStream outstream = Utilities.TryCreate(Path.Combine(outDir, Path.GetFileNameWithoutExtension(this.Filename))); + GZipStream gzstream = new GZipStream(Utilities.TryOpenRead(this.Filename), Ionic.Zlib.CompressionMode.Decompress); + gzstream.CopyTo(outstream); + + // Dispose of the streams + outstream.Dispose(); + gzstream.Dispose(); + + encounteredErrors = false; + } + catch (EndOfStreamException) + { + // Catch this but don't count it as an error because SharpCompress is unsafe + } + catch (InvalidOperationException) + { + encounteredErrors = true; + } + catch (Exception ex) + { + Globals.Logger.Error(ex.ToString()); + encounteredErrors = true; + } + + return encounteredErrors; + } + + /// + /// Attempt to extract a file from an archive + /// + /// Name of the entry to be extracted + /// Output directory for archive extraction + /// Name of the extracted file, null on error + public override string CopyToFile(string entryName, string outDir) + { + // Try to extract a stream using the given information + (MemoryStream ms, string realEntry) = CopyToStream(entryName); + + // If the memory stream and the entry name are both non-null, we write to file + if (ms != null && realEntry != null) + { + realEntry = Path.Combine(outDir, realEntry); + + // Create the output subfolder now + Directory.CreateDirectory(Path.GetDirectoryName(realEntry)); + + // Now open and write the file if possible + FileStream fs = Utilities.TryCreate(realEntry); + if (fs != null) + { + ms.Seek(0, SeekOrigin.Begin); + byte[] zbuffer = new byte[_bufferSize]; + int zlen; + while ((zlen = ms.Read(zbuffer, 0, _bufferSize)) > 0) + { + fs.Write(zbuffer, 0, zlen); + fs.Flush(); + } + + ms?.Dispose(); + fs?.Dispose(); + } + else + { + ms?.Dispose(); + fs?.Dispose(); + realEntry = null; + } + } + + return realEntry; + } + + /// + /// Attempt to extract a stream from an archive + /// + /// Name of the entry to be extracted + /// Output representing the entry name that was found + /// MemoryStream representing the entry, null on error + public override (MemoryStream, string) CopyToStream(string entryName) + { + MemoryStream ms = new MemoryStream(); + string realEntry = null; + + try + { + // Decompress the _filename stream + realEntry = Path.GetFileNameWithoutExtension(this.Filename); + GZipStream gzstream = new GZipStream(Utilities.TryOpenRead(this.Filename), Ionic.Zlib.CompressionMode.Decompress); + + // Write the file out + byte[] gbuffer = new byte[_bufferSize]; + int glen; + while ((glen = gzstream.Read(gbuffer, 0, _bufferSize)) > 0) + { + + ms.Write(gbuffer, 0, glen); + ms.Flush(); + } + + // Dispose of the streams + gzstream.Dispose(); + } + catch (Exception ex) + { + Globals.Logger.Error(ex.ToString()); + ms = null; + realEntry = null; + } + + return (ms, realEntry); + } + + #endregion + + #region Information + + /// + /// Generate a list of DatItem objects from the header values in an archive + /// + /// Hash representing the hashes that should be skipped + /// True if entry dates should be included, false otherwise (default) + /// List of DatItem objects representing the found data + /// TODO: All instances of Hash.DeepHashes should be made into 0x0 eventually + public override List GetChildren(Hash omitFromScan = Hash.DeepHashes, bool date = false) + { + if (_children == null || _children.Count == 0) + { + _children = new List(); + + string gamename = Path.GetFileNameWithoutExtension(this.Filename); + + BaseFile possibleTgz = GetTorrentGZFileInfo(); + + // If it was, then add it to the outputs and continue + if (possibleTgz != null && possibleTgz.Filename != null) + { + _children.Add(possibleTgz); + } + else + { + try + { + // If secure hashes are disabled, do a quickscan + if (omitFromScan == Hash.SecureHashes) + { + BaseFile tempRom = new BaseFile() + { + Filename = gamename, + }; + BinaryReader br = new BinaryReader(Utilities.TryOpenRead(this.Filename)); + br.BaseStream.Seek(-8, SeekOrigin.End); + byte[] headercrc = br.ReadBytesReverse(4); + tempRom.CRC = headercrc; + tempRom.Size = br.ReadInt32Reverse(); + br.Dispose(); + + _children.Add(tempRom); + } + // Otherwise, use the stream directly + else + { + GZipStream gzstream = new GZipStream(Utilities.TryOpenRead(this.Filename), Ionic.Zlib.CompressionMode.Decompress); + BaseFile gzipEntryRom = Utilities.GetStreamInfo(gzstream, gzstream.Length, omitFromScan: omitFromScan); + gzipEntryRom.Filename = gzstream.FileName; + gzipEntryRom.Parent = gamename; + gzipEntryRom.Date = (date && gzstream.LastModified != null ? gzstream.LastModified?.ToString("yyyy/MM/dd hh:mm:ss") : null); + _children.Add(gzipEntryRom); + gzstream.Dispose(); + } + } + catch (Exception ex) + { + Globals.Logger.Error(ex.ToString()); + return null; + } + } + } + + return _children; + } + + /// + /// Generate a list of empty folders in an archive + /// + /// Input file to get data from + /// List of empty folders in the archive + public override List GetEmptyFolders() + { + // GZip files don't contain directories + return new List(); + } + + /// + /// Check whether the input file is a standardized format + /// + public override bool IsTorrent() + { + // Check for the file existing first + if (!File.Exists(this.Filename)) + { + return false; + } + + string datum = Path.GetFileName(this.Filename).ToLowerInvariant(); + long filesize = new FileInfo(this.Filename).Length; + + // If we have the romba depot files, just skip them gracefully + if (datum == ".romba_size" || datum == ".romba_size.backup") + { + Globals.Logger.Verbose("Romba depot file found, skipping: {0}", this.Filename); + return false; + } + + // Check if the name is the right length + if (!Regex.IsMatch(datum, @"^[0-9a-f]{" + Constants.SHA1Length + @"}\.gz")) // TODO: When updating to SHA-256, this needs to update to Constants.SHA256Length + { + Globals.Logger.Warning("Non SHA-1 filename found, skipping: '{0}'", Path.GetFullPath(this.Filename)); + return false; + } + + // Check if the file is at least the minimum length + if (filesize < 40 /* bytes */) + { + Globals.Logger.Warning("Possibly corrupt file '{0}' with size {1}", Path.GetFullPath(this.Filename), Utilities.GetBytesReadable(filesize)); + return false; + } + + // Get the Romba-specific header data + byte[] header; // Get preamble header for checking + byte[] headermd5; // MD5 + byte[] headercrc; // CRC + ulong headersz; // Int64 size + BinaryReader br = new BinaryReader(Utilities.TryOpenRead(this.Filename)); + header = br.ReadBytes(12); + headermd5 = br.ReadBytes(16); + headercrc = br.ReadBytes(4); + headersz = br.ReadUInt64(); + br.Dispose(); + + // If the header is not correct, return a blank rom + bool correct = true; + for (int i = 0; i < header.Length; i++) + { + // This is a temp fix to ignore the modification time and OS until romba can be fixed + if (i == 4 || i == 5 || i == 6 || i == 7 || i == 9) + { + continue; + } + correct &= (header[i] == Constants.TorrentGZHeader[i]); + } + if (!correct) + { + return false; + } + + return true; + } + + /// + /// Retrieve file information for a single torrent GZ file + /// + /// Populated DatItem object if success, empty one on error + public BaseFile GetTorrentGZFileInfo() + { + // Check for the file existing first + if (!File.Exists(this.Filename)) + { + return null; + } + + string datum = Path.GetFileName(this.Filename).ToLowerInvariant(); + long filesize = new FileInfo(this.Filename).Length; + + // If we have the romba depot files, just skip them gracefully + if (datum == ".romba_size" || datum == ".romba_size.backup") + { + Globals.Logger.Verbose("Romba depot file found, skipping: {0}", this.Filename); + return null; + } + + // Check if the name is the right length + if (!Regex.IsMatch(datum, @"^[0-9a-f]{" + Constants.SHA1Length + @"}\.gz")) // TODO: When updating to SHA-256, this needs to update to Constants.SHA256Length + { + Globals.Logger.Warning("Non SHA-1 filename found, skipping: '{0}'", Path.GetFullPath(this.Filename)); + return null; + } + + // Check if the file is at least the minimum length + if (filesize < 40 /* bytes */) + { + Globals.Logger.Warning("Possibly corrupt file '{0}' with size {1}", Path.GetFullPath(this.Filename), Utilities.GetBytesReadable(filesize)); + return null; + } + + // Get the Romba-specific header data + byte[] header; // Get preamble header for checking + byte[] headermd5; // MD5 + byte[] headercrc; // CRC + ulong headersz; // Int64 size + BinaryReader br = new BinaryReader(Utilities.TryOpenRead(this.Filename)); + header = br.ReadBytes(12); + headermd5 = br.ReadBytes(16); + headercrc = br.ReadBytes(4); + headersz = br.ReadUInt64(); + br.Dispose(); + + // If the header is not correct, return a blank rom + bool correct = true; + for (int i = 0; i < header.Length; i++) + { + // This is a temp fix to ignore the modification time and OS until romba can be fixed + if (i == 4 || i == 5 || i == 6 || i == 7 || i == 9) + { + continue; + } + correct &= (header[i] == Constants.TorrentGZHeader[i]); + } + if (!correct) + { + return null; + } + + // Now convert the data and get the right position + long extractedsize = (long)headersz; + + BaseFile baseFile = new BaseFile + { + Filename = Path.GetFileNameWithoutExtension(this.Filename).ToLowerInvariant(), + Size = extractedsize, + CRC = headercrc, + MD5 = headermd5, + SHA1 = Utilities.StringToByteArray(Path.GetFileNameWithoutExtension(this.Filename)), // TODO: When updating to SHA-256, this needs to update to SHA256 + + Parent = Path.GetFileNameWithoutExtension(this.Filename).ToLowerInvariant(), + }; + + return baseFile; + } + + #endregion + + #region Writing + + /// + /// Write an input file to a torrent GZ file + /// + /// Input filename to be moved + /// Output directory to build to + /// DatItem representing the new information + /// True if the date from the DAT should be used if available, false otherwise (default) + /// True if files should be output in Romba depot folders, false otherwise + /// True if the write was a success, false otherwise + /// This works for now, but it can be sped up by using Ionic.Zip or another zlib wrapper that allows for header values built-in. See edc's code. + public override bool Write(string inputFile, string outDir, Rom rom = null, bool date = false, bool romba = false) + { + // Check that the input file exists + if (!File.Exists(inputFile)) + { + Globals.Logger.Warning("File '{0}' does not exist!", inputFile); + return false; + } + inputFile = Path.GetFullPath(inputFile); + + // Get the file stream for the file and write out + return Write(Utilities.TryOpenRead(inputFile), outDir, rom, date, romba); + } + + /// + /// Write an input stream to a torrent GZ file + /// + /// Input stream to be moved + /// Output directory to build to + /// DatItem representing the new information + /// True if the date from the DAT should be used if available, false otherwise (default) + /// True if files should be output in Romba depot folders, false otherwise + /// True if the write was a success, false otherwise + /// This works for now, but it can be sped up by using Ionic.Zip or another zlib wrapper that allows for header values built-in. See edc's code. + public override bool Write(Stream inputStream, string outDir, Rom rom = null, bool date = false, bool romba = false) + { + bool success = false; + + // If the stream is not readable, return + if (!inputStream.CanRead) + { + return success; + } + + // Make sure the output directory exists + if (!Directory.Exists(outDir)) + { + Directory.CreateDirectory(outDir); + } + outDir = Path.GetFullPath(outDir); + + // Now get the Rom info for the file so we have hashes and size + rom = new Rom(Utilities.GetStreamInfo(inputStream, inputStream.Length, keepReadOpen: true)); + + // Get the output file name + string outfile = null; + + // If we have a romba output, add the romba path + if (romba) + { + outfile = Path.Combine(outDir, Utilities.GetRombaPath(rom.SHA1)); // TODO: When updating to SHA-256, this needs to update to SHA256 + + // Check to see if the folder needs to be created + if (!Directory.Exists(Path.GetDirectoryName(outfile))) + { + Directory.CreateDirectory(Path.GetDirectoryName(outfile)); + } + } + // Otherwise, we're just rebuilding to the main directory + else + { + outfile = Path.Combine(outDir, rom.SHA1 + ".gz"); // TODO: When updating to SHA-256, this needs to update to SHA256 + } + + // If the output file exists, don't try to write again + if (!File.Exists(outfile)) + { + // Compress the input stream + FileStream outputStream = Utilities.TryCreate(outfile); + + // Open the output file for writing + BinaryWriter sw = new BinaryWriter(outputStream); + + // Write standard header and TGZ info + byte[] data = Constants.TorrentGZHeader + .Concat(Utilities.StringToByteArray(rom.MD5)) // MD5 + .Concat(Utilities.StringToByteArray(rom.CRC)) // CRC + .ToArray(); + sw.Write(data); + sw.Write((ulong)rom.Size); // Long size (Unsigned, Mirrored) + + // Now create a deflatestream from the input file + DeflateStream ds = new DeflateStream(outputStream, Ionic.Zlib.CompressionMode.Compress, Ionic.Zlib.CompressionLevel.BestCompression, true); + + // Copy the input stream to the output + byte[] ibuffer = new byte[_bufferSize]; + int ilen; + while ((ilen = inputStream.Read(ibuffer, 0, _bufferSize)) > 0) + { + ds.Write(ibuffer, 0, ilen); + ds.Flush(); + } + ds.Dispose(); + + // Now write the standard footer + sw.Write(Utilities.StringToByteArray(rom.CRC).Reverse().ToArray()); + sw.Write((uint)rom.Size); + + // Dispose of everything + sw.Dispose(); + outputStream.Dispose(); + inputStream.Dispose(); + } + + return true; + } + + /// + /// Write a set of input files to a torrent GZ archive (assuming the same output archive name) + /// + /// Input files to be moved + /// Output directory to build to + /// DatItem representing the new information + /// True if the date from the DAT should be used if available, false otherwise (default) + /// True if files should be output in Romba depot folders, false otherwise + /// True if the archive was written properly, false otherwise + public override bool Write(List inputFiles, string outDir, List roms, bool date = false, bool romba = false) + { + throw new NotImplementedException(); + } + + #endregion + } } diff --git a/SabreTools.Library/FileTypes/LRZipArchive.cs b/SabreTools.Library/FileTypes/LRZipArchive.cs index 4ffb5c5b..7a611f55 100644 --- a/SabreTools.Library/FileTypes/LRZipArchive.cs +++ b/SabreTools.Library/FileTypes/LRZipArchive.cs @@ -13,152 +13,152 @@ using Stream = System.IO.Stream; namespace SabreTools.Library.FileTypes { - /// - /// Represents a TorrentLRZip archive for reading and writing - /// - /// TODO: Implement from source at https://github.com/ckolivas/lrzip - public class LRZipArchive : BaseArchive - { - #region Constructors + /// + /// Represents a TorrentLRZip archive for reading and writing + /// + /// TODO: Implement from source at https://github.com/ckolivas/lrzip + public class LRZipArchive : BaseArchive + { + #region Constructors - /// - /// Create a new LRZipArchive with no base file - /// - public LRZipArchive() - : base() - { - _fileType = FileType.LRZipArchive; - } + /// + /// Create a new LRZipArchive with no base file + /// + public LRZipArchive() + : base() + { + this.Type = FileType.LRZipArchive; + } - /// - /// Create a new LRZipArchive from the given file - /// - /// Name of the file to use as an archive - /// True if hashes for this file should be calculated, false otherwise (default) - public LRZipArchive(string filename, bool getHashes = false) - : base(filename, getHashes) - { - _fileType = FileType.LRZipArchive; - } + /// + /// Create a new LRZipArchive from the given file + /// + /// Name of the file to use as an archive + /// True if hashes for this file should be calculated, false otherwise (default) + public LRZipArchive(string filename, bool getHashes = false) + : base(filename, getHashes) + { + this.Type = FileType.LRZipArchive; + } - #endregion + #endregion - #region Extraction + #region Extraction - /// - /// Attempt to extract a file as an archive - /// - /// Output directory for archive extraction - /// True if the extraction was a success, false otherwise - public override bool CopyAll(string outDir) - { - throw new NotImplementedException(); - } + /// + /// Attempt to extract a file as an archive + /// + /// Output directory for archive extraction + /// True if the extraction was a success, false otherwise + public override bool CopyAll(string outDir) + { + throw new NotImplementedException(); + } - /// - /// Attempt to extract a file from an archive - /// - /// Name of the entry to be extracted - /// Output directory for archive extraction - /// Name of the extracted file, null on error - public override string CopyToFile(string entryName, string outDir) - { - throw new NotImplementedException(); - } + /// + /// Attempt to extract a file from an archive + /// + /// Name of the entry to be extracted + /// Output directory for archive extraction + /// Name of the extracted file, null on error + public override string CopyToFile(string entryName, string outDir) + { + throw new NotImplementedException(); + } - /// - /// Attempt to extract a stream from an archive - /// - /// Name of the entry to be extracted - /// Output representing the entry name that was found - /// MemoryStream representing the entry, null on error - public override (MemoryStream, string) CopyToStream(string entryName) - { - throw new NotImplementedException(); - } + /// + /// Attempt to extract a stream from an archive + /// + /// Name of the entry to be extracted + /// Output representing the entry name that was found + /// MemoryStream representing the entry, null on error + public override (MemoryStream, string) CopyToStream(string entryName) + { + throw new NotImplementedException(); + } - #endregion + #endregion - #region Information + #region Information - /// - /// Generate a list of DatItem objects from the header values in an archive - /// - /// Hash representing the hashes that should be skipped - /// True if entry dates should be included, false otherwise (default) - /// List of DatItem objects representing the found data - /// TODO: All instances of Hash.DeepHashes should be made into 0x0 eventually - public override List GetChildren(Hash omitFromScan = Hash.DeepHashes, bool date = false) - { - throw new NotImplementedException(); - } + /// + /// Generate a list of DatItem objects from the header values in an archive + /// + /// Hash representing the hashes that should be skipped + /// True if entry dates should be included, false otherwise (default) + /// List of DatItem objects representing the found data + /// TODO: All instances of Hash.DeepHashes should be made into 0x0 eventually + public override List GetChildren(Hash omitFromScan = Hash.DeepHashes, bool date = false) + { + throw new NotImplementedException(); + } - /// - /// Generate a list of empty folders in an archive - /// - /// Input file to get data from - /// List of empty folders in the archive - public override List GetEmptyFolders() - { - throw new NotImplementedException(); - } + /// + /// Generate a list of empty folders in an archive + /// + /// Input file to get data from + /// List of empty folders in the archive + public override List GetEmptyFolders() + { + throw new NotImplementedException(); + } - /// - /// Check whether the input file is a standardized format - /// - public override bool IsTorrent() - { - throw new NotImplementedException(); - } + /// + /// Check whether the input file is a standardized format + /// + public override bool IsTorrent() + { + throw new NotImplementedException(); + } - #endregion + #endregion - #region Writing + #region Writing - /// - /// Write an input file to a torrent LRZip file - /// - /// Input filename to be moved - /// Output directory to build to - /// DatItem representing the new information - /// True if the date from the DAT should be used if available, false otherwise (default) - /// True if files should be output in Romba depot folders, false otherwise - /// True if the write was a success, false otherwise - /// This works for now, but it can be sped up by using Ionic.Zip or another zlib wrapper that allows for header values built-in. See edc's code. - public override bool Write(string inputFile, string outDir, Rom rom, bool date = false, bool romba = false) - { - throw new NotImplementedException(); - } + /// + /// Write an input file to a torrent LRZip file + /// + /// Input filename to be moved + /// Output directory to build to + /// DatItem representing the new information + /// True if the date from the DAT should be used if available, false otherwise (default) + /// True if files should be output in Romba depot folders, false otherwise + /// True if the write was a success, false otherwise + /// This works for now, but it can be sped up by using Ionic.Zip or another zlib wrapper that allows for header values built-in. See edc's code. + public override bool Write(string inputFile, string outDir, Rom rom, bool date = false, bool romba = false) + { + throw new NotImplementedException(); + } - /// - /// Write an input stream to a torrent LRZip file - /// - /// Input stream to be moved - /// Output directory to build to - /// DatItem representing the new information - /// True if the date from the DAT should be used if available, false otherwise (default) - /// True if files should be output in Romba depot folders, false otherwise - /// True if the write was a success, false otherwise - /// This works for now, but it can be sped up by using Ionic.Zip or another zlib wrapper that allows for header values built-in. See edc's code. - public override bool Write(Stream inputStream, string outDir, Rom rom, bool date = false, bool romba = false) - { - throw new NotImplementedException(); - } + /// + /// Write an input stream to a torrent LRZip file + /// + /// Input stream to be moved + /// Output directory to build to + /// DatItem representing the new information + /// True if the date from the DAT should be used if available, false otherwise (default) + /// True if files should be output in Romba depot folders, false otherwise + /// True if the write was a success, false otherwise + /// This works for now, but it can be sped up by using Ionic.Zip or another zlib wrapper that allows for header values built-in. See edc's code. + public override bool Write(Stream inputStream, string outDir, Rom rom, bool date = false, bool romba = false) + { + throw new NotImplementedException(); + } - /// - /// Write a set of input files to a torrent LRZip archive (assuming the same output archive name) - /// - /// Input files to be moved - /// Output directory to build to - /// DatItem representing the new information - /// True if the date from the DAT should be used if available, false otherwise (default) - /// True if files should be output in Romba depot folders, false otherwise - /// True if the archive was written properly, false otherwise - public override bool Write(List inputFiles, string outDir, List roms, bool date = false, bool romba = false) - { - throw new NotImplementedException(); - } + /// + /// Write a set of input files to a torrent LRZip archive (assuming the same output archive name) + /// + /// Input files to be moved + /// Output directory to build to + /// DatItem representing the new information + /// True if the date from the DAT should be used if available, false otherwise (default) + /// True if files should be output in Romba depot folders, false otherwise + /// True if the archive was written properly, false otherwise + public override bool Write(List inputFiles, string outDir, List roms, bool date = false, bool romba = false) + { + throw new NotImplementedException(); + } - #endregion - } + #endregion + } } diff --git a/SabreTools.Library/FileTypes/LZ4Archive.cs b/SabreTools.Library/FileTypes/LZ4Archive.cs index b307549a..c7949581 100644 --- a/SabreTools.Library/FileTypes/LZ4Archive.cs +++ b/SabreTools.Library/FileTypes/LZ4Archive.cs @@ -13,152 +13,152 @@ using Stream = System.IO.Stream; namespace SabreTools.Library.FileTypes { - /// - /// Represents a TorrentLRZip archive for reading and writing - /// - /// TODO: Implement from source at https://github.com/lz4/lz4 - public class LZ4Archive : BaseArchive - { - #region Constructors + /// + /// Represents a TorrentLRZip archive for reading and writing + /// + /// TODO: Implement from source at https://github.com/lz4/lz4 + public class LZ4Archive : BaseArchive + { + #region Constructors - /// - /// Create a new LZ4Archive with no base file - /// - public LZ4Archive() - : base() - { - _fileType = FileType.LZ4Archive; - } + /// + /// Create a new LZ4Archive with no base file + /// + public LZ4Archive() + : base() + { + this.Type = FileType.LZ4Archive; + } - /// - /// Create a new LZ4Archive from the given file - /// - /// Name of the file to use as an archive - /// True if hashes for this file should be calculated, false otherwise (default) - public LZ4Archive(string filename, bool getHashes = false) - : base(filename, getHashes) - { - _fileType = FileType.LZ4Archive; - } + /// + /// Create a new LZ4Archive from the given file + /// + /// Name of the file to use as an archive + /// True if hashes for this file should be calculated, false otherwise (default) + public LZ4Archive(string filename, bool getHashes = false) + : base(filename, getHashes) + { + this.Type = FileType.LZ4Archive; + } - #endregion + #endregion - #region Extraction + #region Extraction - /// - /// Attempt to extract a file as an archive - /// - /// Output directory for archive extraction - /// True if the extraction was a success, false otherwise - public override bool CopyAll(string outDir) - { - throw new NotImplementedException(); - } + /// + /// Attempt to extract a file as an archive + /// + /// Output directory for archive extraction + /// True if the extraction was a success, false otherwise + public override bool CopyAll(string outDir) + { + throw new NotImplementedException(); + } - /// - /// Attempt to extract a file from an archive - /// - /// Name of the entry to be extracted - /// Output directory for archive extraction - /// Name of the extracted file, null on error - public override string CopyToFile(string entryName, string outDir) - { - throw new NotImplementedException(); - } + /// + /// Attempt to extract a file from an archive + /// + /// Name of the entry to be extracted + /// Output directory for archive extraction + /// Name of the extracted file, null on error + public override string CopyToFile(string entryName, string outDir) + { + throw new NotImplementedException(); + } - /// - /// Attempt to extract a stream from an archive - /// - /// Name of the entry to be extracted - /// Output representing the entry name that was found - /// MemoryStream representing the entry, null on error - public override (MemoryStream, string) CopyToStream(string entryName) - { - throw new NotImplementedException(); - } + /// + /// Attempt to extract a stream from an archive + /// + /// Name of the entry to be extracted + /// Output representing the entry name that was found + /// MemoryStream representing the entry, null on error + public override (MemoryStream, string) CopyToStream(string entryName) + { + throw new NotImplementedException(); + } - #endregion + #endregion - #region Information + #region Information - /// - /// Generate a list of DatItem objects from the header values in an archive - /// - /// Hash representing the hashes that should be skipped - /// True if entry dates should be included, false otherwise (default) - /// List of DatItem objects representing the found data - /// TODO: All instances of Hash.DeepHashes should be made into 0x0 eventually - public override List GetChildren(Hash omitFromScan = Hash.DeepHashes, bool date = false) - { - throw new NotImplementedException(); - } + /// + /// Generate a list of DatItem objects from the header values in an archive + /// + /// Hash representing the hashes that should be skipped + /// True if entry dates should be included, false otherwise (default) + /// List of DatItem objects representing the found data + /// TODO: All instances of Hash.DeepHashes should be made into 0x0 eventually + public override List GetChildren(Hash omitFromScan = Hash.DeepHashes, bool date = false) + { + throw new NotImplementedException(); + } - /// - /// Generate a list of empty folders in an archive - /// - /// Input file to get data from - /// List of empty folders in the archive - public override List GetEmptyFolders() - { - throw new NotImplementedException(); - } + /// + /// Generate a list of empty folders in an archive + /// + /// Input file to get data from + /// List of empty folders in the archive + public override List GetEmptyFolders() + { + throw new NotImplementedException(); + } - /// - /// Check whether the input file is a standardized format - /// - public override bool IsTorrent() - { - throw new NotImplementedException(); - } + /// + /// Check whether the input file is a standardized format + /// + public override bool IsTorrent() + { + throw new NotImplementedException(); + } - #endregion + #endregion - #region Writing + #region Writing - /// - /// Write an input file to a torrent LZ4 file - /// - /// Input filename to be moved - /// Output directory to build to - /// DatItem representing the new information - /// True if the date from the DAT should be used if available, false otherwise (default) - /// True if files should be output in Romba depot folders, false otherwise - /// True if the write was a success, false otherwise - /// This works for now, but it can be sped up by using Ionic.Zip or another zlib wrapper that allows for header values built-in. See edc's code. - public override bool Write(string inputFile, string outDir, Rom rom, bool date = false, bool romba = false) - { - throw new NotImplementedException(); - } + /// + /// Write an input file to a torrent LZ4 file + /// + /// Input filename to be moved + /// Output directory to build to + /// DatItem representing the new information + /// True if the date from the DAT should be used if available, false otherwise (default) + /// True if files should be output in Romba depot folders, false otherwise + /// True if the write was a success, false otherwise + /// This works for now, but it can be sped up by using Ionic.Zip or another zlib wrapper that allows for header values built-in. See edc's code. + public override bool Write(string inputFile, string outDir, Rom rom, bool date = false, bool romba = false) + { + throw new NotImplementedException(); + } - /// - /// Write an input stream to a torrent LZ4 file - /// - /// Input stream to be moved - /// Output directory to build to - /// DatItem representing the new information - /// True if the date from the DAT should be used if available, false otherwise (default) - /// True if files should be output in Romba depot folders, false otherwise - /// True if the write was a success, false otherwise - /// This works for now, but it can be sped up by using Ionic.Zip or another zlib wrapper that allows for header values built-in. See edc's code. - public override bool Write(Stream inputStream, string outDir, Rom rom, bool date = false, bool romba = false) - { - throw new NotImplementedException(); - } + /// + /// Write an input stream to a torrent LZ4 file + /// + /// Input stream to be moved + /// Output directory to build to + /// DatItem representing the new information + /// True if the date from the DAT should be used if available, false otherwise (default) + /// True if files should be output in Romba depot folders, false otherwise + /// True if the write was a success, false otherwise + /// This works for now, but it can be sped up by using Ionic.Zip or another zlib wrapper that allows for header values built-in. See edc's code. + public override bool Write(Stream inputStream, string outDir, Rom rom, bool date = false, bool romba = false) + { + throw new NotImplementedException(); + } - /// - /// Write a set of input files to a torrent LZ4 archive (assuming the same output archive name) - /// - /// Input files to be moved - /// Output directory to build to - /// DatItem representing the new information - /// True if the date from the DAT should be used if available, false otherwise (default) - /// True if files should be output in Romba depot folders, false otherwise - /// True if the archive was written properly, false otherwise - public override bool Write(List inputFiles, string outDir, List roms, bool date = false, bool romba = false) - { - throw new NotImplementedException(); - } + /// + /// Write a set of input files to a torrent LZ4 archive (assuming the same output archive name) + /// + /// Input files to be moved + /// Output directory to build to + /// DatItem representing the new information + /// True if the date from the DAT should be used if available, false otherwise (default) + /// True if files should be output in Romba depot folders, false otherwise + /// True if the archive was written properly, false otherwise + public override bool Write(List inputFiles, string outDir, List roms, bool date = false, bool romba = false) + { + throw new NotImplementedException(); + } - #endregion - } + #endregion + } } diff --git a/SabreTools.Library/FileTypes/RarArchive.cs b/SabreTools.Library/FileTypes/RarArchive.cs index 425d22c5..24824d27 100644 --- a/SabreTools.Library/FileTypes/RarArchive.cs +++ b/SabreTools.Library/FileTypes/RarArchive.cs @@ -37,7 +37,7 @@ namespace SabreTools.Library.FileTypes public RarArchive() : base() { - _fileType = FileType.RarArchive; + this.Type = FileType.RarArchive; } /// @@ -49,7 +49,7 @@ namespace SabreTools.Library.FileTypes public RarArchive(string filename, bool getHashes = false) : base(filename, getHashes) { - _fileType = FileType.RarArchive; + this.Type = FileType.RarArchive; } #endregion @@ -71,7 +71,7 @@ namespace SabreTools.Library.FileTypes Directory.CreateDirectory(outDir); // Extract all files to the temp directory - SharpCompress.Archives.Rar.RarArchive ra = SharpCompress.Archives.Rar.RarArchive.Open(_filename); + SharpCompress.Archives.Rar.RarArchive ra = SharpCompress.Archives.Rar.RarArchive.Open(this.Filename); foreach (RarArchiveEntry entry in ra.Entries) { entry.WriteToDirectory(outDir, new ExtractionOptions { PreserveFileTime = true, ExtractFullPath = true, Overwrite = true }); @@ -155,7 +155,7 @@ namespace SabreTools.Library.FileTypes try { - SharpCompress.Archives.Rar.RarArchive ra = SharpCompress.Archives.Rar.RarArchive.Open(_filename, new ReaderOptions { LeaveStreamOpen = false, }); + SharpCompress.Archives.Rar.RarArchive ra = SharpCompress.Archives.Rar.RarArchive.Open(this.Filename, new ReaderOptions { LeaveStreamOpen = false, }); foreach (RarArchiveEntry entry in ra.Entries) { if (entry != null && !entry.IsDirectory && entry.Key.Contains(entryName)) @@ -191,11 +191,11 @@ namespace SabreTools.Library.FileTypes public override List GetChildren(Hash omitFromScan = Hash.DeepHashes, bool date = false) { List found = new List(); - string gamename = Path.GetFileNameWithoutExtension(_filename); + string gamename = Path.GetFileNameWithoutExtension(this.Filename); try { - SharpCompress.Archives.Rar.RarArchive ra = SharpCompress.Archives.Rar.RarArchive.Open(Utilities.TryOpenRead(_filename)); + SharpCompress.Archives.Rar.RarArchive ra = SharpCompress.Archives.Rar.RarArchive.Open(Utilities.TryOpenRead(this.Filename)); foreach (RarArchiveEntry entry in ra.Entries.Where(e => e != null && !e.IsDirectory)) { // If secure hashes are disabled, do a quickscan @@ -242,12 +242,12 @@ namespace SabreTools.Library.FileTypes /// TODO: Write the rest of this RAR file handling public void GetRarFileInfo() { - if (!File.Exists(_filename)) + if (!File.Exists(this.Filename)) { return; } - BinaryReader br = new BinaryReader(Utilities.TryOpenRead(_filename)); + BinaryReader br = new BinaryReader(Utilities.TryOpenRead(this.Filename)); // Check for the signature first (Skipping the SFX Module) byte[] signature = br.ReadBytes(8); @@ -464,7 +464,7 @@ namespace SabreTools.Library.FileTypes try { - SharpCompress.Archives.Rar.RarArchive ra = SharpCompress.Archives.Rar.RarArchive.Open(_filename, new ReaderOptions { LeaveStreamOpen = false }); + SharpCompress.Archives.Rar.RarArchive ra = SharpCompress.Archives.Rar.RarArchive.Open(this.Filename, new ReaderOptions { LeaveStreamOpen = false }); List rarEntries = ra.Entries.OrderBy(e => e.Key, new NaturalSort.NaturalReversedComparer()).ToList(); string lastRarEntry = null; foreach (RarArchiveEntry entry in rarEntries) diff --git a/SabreTools.Library/FileTypes/SevenZipArchive.cs b/SabreTools.Library/FileTypes/SevenZipArchive.cs index da8773b4..824467ea 100644 --- a/SabreTools.Library/FileTypes/SevenZipArchive.cs +++ b/SabreTools.Library/FileTypes/SevenZipArchive.cs @@ -26,740 +26,740 @@ using SharpCompress.Readers; namespace SabreTools.Library.FileTypes { - /// - /// Represents a Torrent7zip archive for reading and writing - /// - /// TODO: Torrent 7-zip: https://sourceforge.net/p/t7z/code/HEAD/tree/ - public class SevenZipArchive : BaseArchive - { - #region Constructors - - /// - /// Create a new TorrentSevenZipArchive with no base file - /// - public SevenZipArchive() - : base() - { - _fileType = FileType.SevenZipArchive; - } - - /// - /// Create a new TorrentSevenZipArchive from the given file - /// - /// Name of the file to use as an archive - /// True for opening file as read, false for opening file as write - /// True if hashes for this file should be calculated, false otherwise (default) - public SevenZipArchive(string filename, bool getHashes = false) - : base(filename, getHashes) - { - _fileType = FileType.SevenZipArchive; - } - - #endregion - - #region Extraction - - /// - /// Attempt to extract a file as an archive - /// - /// Output directory for archive extraction - /// True if the extraction was a success, false otherwise - public override bool CopyAll(string outDir) - { - bool encounteredErrors = true; - - try - { - // Create the temp directory - Directory.CreateDirectory(outDir); - - // Extract all files to the temp directory - SharpCompress.Archives.SevenZip.SevenZipArchive sza = SharpCompress.Archives.SevenZip.SevenZipArchive.Open(Utilities.TryOpenRead(_filename)); - foreach (SevenZipArchiveEntry entry in sza.Entries) - { - entry.WriteToDirectory(outDir, new ExtractionOptions { PreserveFileTime = true, ExtractFullPath = true, Overwrite = true }); - } - encounteredErrors = false; - sza.Dispose(); - } - catch (EndOfStreamException) - { - // Catch this but don't count it as an error because SharpCompress is unsafe - } - catch (InvalidOperationException) - { - encounteredErrors = true; - } - catch (Exception ex) - { - Globals.Logger.Error(ex.ToString()); - encounteredErrors = true; - } - - return encounteredErrors; - } - - /// - /// Attempt to extract a file from an archive - /// - /// Name of the entry to be extracted - /// Output directory for archive extraction - /// Name of the extracted file, null on error - public override string CopyToFile(string entryName, string outDir) - { - // Try to extract a stream using the given information - (MemoryStream ms, string realEntry) = CopyToStream(entryName); - - // If the memory stream and the entry name are both non-null, we write to file - if (ms != null && realEntry != null) - { - realEntry = Path.Combine(outDir, realEntry); - - // Create the output subfolder now - Directory.CreateDirectory(Path.GetDirectoryName(realEntry)); - - // Now open and write the file if possible - FileStream fs = Utilities.TryCreate(realEntry); - if (fs != null) - { - ms.Seek(0, SeekOrigin.Begin); - byte[] zbuffer = new byte[_bufferSize]; - int zlen; - while ((zlen = ms.Read(zbuffer, 0, _bufferSize)) > 0) - { - fs.Write(zbuffer, 0, zlen); - fs.Flush(); - } - - ms?.Dispose(); - fs?.Dispose(); - } - else - { - ms?.Dispose(); - fs?.Dispose(); - realEntry = null; - } - } - - return realEntry; - } - - /// - /// Attempt to extract a stream from an archive - /// - /// Name of the entry to be extracted - /// Output representing the entry name that was found - /// MemoryStream representing the entry, null on error - public override (MemoryStream, string) CopyToStream(string entryName) - { - MemoryStream ms = new MemoryStream(); - string realEntry = null; - - try - { - SharpCompress.Archives.SevenZip.SevenZipArchive sza = SharpCompress.Archives.SevenZip.SevenZipArchive.Open(_filename, new ReaderOptions { LeaveStreamOpen = false, }); - foreach (SevenZipArchiveEntry entry in sza.Entries) - { - if (entry != null && !entry.IsDirectory && entry.Key.Contains(entryName)) - { - // Write the file out - realEntry = entry.Key; - entry.WriteTo(ms); - break; - } - } - sza.Dispose(); - } - catch (Exception ex) - { - Globals.Logger.Error(ex.ToString()); - ms = null; - realEntry = null; - } - - return (ms, realEntry); - } - - #endregion - - #region Information - - /// - /// Generate a list of DatItem objects from the header values in an archive - /// - /// Hash representing the hashes that should be skipped - /// True if entry dates should be included, false otherwise (default) - /// List of DatItem objects representing the found data - /// TODO: All instances of Hash.DeepHashes should be made into 0x0 eventually - public override List GetChildren(Hash omitFromScan = Hash.DeepHashes, bool date = false) - { - List found = new List(); - string gamename = Path.GetFileNameWithoutExtension(_filename); - - try - { - SharpCompress.Archives.SevenZip.SevenZipArchive sza = SharpCompress.Archives.SevenZip.SevenZipArchive.Open(Utilities.TryOpenRead(_filename)); - foreach (SevenZipArchiveEntry entry in sza.Entries.Where(e => e != null && !e.IsDirectory)) - { - // If secure hashes are disabled, do a quickscan - if (omitFromScan == Hash.SecureHashes) - { - found.Add(new BaseFile - { - Filename = entry.Key, - Size = entry.Size, - CRC = BitConverter.GetBytes(entry.Crc), - Date = (date && entry.LastModifiedTime != null ? entry.LastModifiedTime?.ToString("yyyy/MM/dd hh:mm:ss") : null), - - Parent = gamename, - }); - } - // Otherwise, use the stream directly - else - { - Stream entryStream = entry.OpenEntryStream(); - BaseFile sevenZipEntryRom = Utilities.GetStreamInfo(entryStream, entry.Size, omitFromScan: omitFromScan); - sevenZipEntryRom.Filename = entry.Key; - sevenZipEntryRom.Parent = gamename; - sevenZipEntryRom.Date = (date && entry.LastModifiedTime != null ? entry.LastModifiedTime?.ToString("yyyy/MM/dd hh:mm:ss") : null); - found.Add(sevenZipEntryRom); - entryStream.Dispose(); - } - } - - // Dispose of the archive - sza.Dispose(); - } - catch (Exception ex) - { - Globals.Logger.Error(ex.ToString()); - return null; - } - - return found; - } - - /// - /// Generate a list of empty folders in an archive - /// - /// Input file to get data from - /// List of empty folders in the archive - public override List GetEmptyFolders() - { - List empties = new List(); - - try - { - SharpCompress.Archives.SevenZip.SevenZipArchive sza = SharpCompress.Archives.SevenZip.SevenZipArchive.Open(_filename, new ReaderOptions { LeaveStreamOpen = false }); - List sevenZipEntries = sza.Entries.OrderBy(e => e.Key, new NaturalSort.NaturalReversedComparer()).ToList(); - string lastSevenZipEntry = null; - foreach (SevenZipArchiveEntry entry in sevenZipEntries) - { - if (entry != null) - { - // If the current is a superset of last, we skip it - if (lastSevenZipEntry != null && lastSevenZipEntry.StartsWith(entry.Key)) - { - // No-op - } - // If the entry is a directory, we add it - else if (entry.IsDirectory) - { - empties.Add(entry.Key); - lastSevenZipEntry = entry.Key; - } - } - } - } - catch (Exception ex) - { - Globals.Logger.Error(ex.ToString()); - } - - return empties; - } - - /// - /// Check whether the input file is a standardized format - /// - /// TODO: Finish reading T7z information - public override bool IsTorrent() - { - bool ist7z = false; - - if (File.Exists(_filename)) - { - try - { - Stream fread = Utilities.TryOpenRead(_filename); - uint ar, offs = 0; - fread.Seek(0, SeekOrigin.Begin); - byte[] buffer = new byte[128]; - ar = (uint)fread.Read(buffer, 0, 4 + Constants.Torrent7ZipSignature.Length + 4); - if (ar < (4 + Constants.Torrent7ZipSignature.Length + 4)) - { - if (ar >= Constants.Torrent7ZipSignature.Length + 4) - { - ar -= (uint)(Constants.Torrent7ZipSignature.Length + 4); - } - if (ar <= Constants.Torrent7ZipHeader.Length) - { - ar = (uint)Constants.Torrent7ZipHeader.Length; - } - // memset(buffer+offs+ar,0,crcsz-ar) - } - - fread.Dispose(); - } - catch - { - Globals.Logger.Warning("File '{0}' could not be opened", _filename); - ist7z = false; - } - } - - return ist7z; - } - - #endregion - - #region Writing - - /// - /// Write an input file to a torrent7z archive - /// - /// Input filename to be moved - /// Output directory to build to - /// DatItem representing the new information - /// True if the date from the DAT should be used if available, false otherwise (default) - /// True if files should be output in Romba depot folders, false otherwise - /// True if the archive was written properly, false otherwise - public override bool Write(string inputFile, string outDir, Rom rom, bool date = false, bool romba = false) - { - // Get the file stream for the file and write out - return Write(Utilities.TryOpenRead(inputFile), outDir, rom, date: date); - } - - /// - /// Write an input file to a torrent7z archive - /// - /// Input stream to be moved - /// Output directory to build to - /// DatItem representing the new information - /// True if the date from the DAT should be used if available, false otherwise (default) - /// True if files should be output in Romba depot folders, false otherwise - /// True if the archive was written properly, false otherwise - public override bool Write(Stream inputStream, string outDir, Rom rom, bool date = false, bool romba = false) - { - bool success = false; - string tempFile = Path.Combine(outDir, "tmp" + Guid.NewGuid().ToString()); - - // If either input is null or empty, return - if (inputStream == null || rom == null || rom.Name == null) - { - return success; - } - - // If the stream is not readable, return - if (!inputStream.CanRead) - { - return success; - } - - // Seek to the beginning of the stream - inputStream.Seek(0, SeekOrigin.Begin); - - // Get the output archive name from the first rebuild rom - string archiveFileName = Path.Combine(outDir, Utilities.RemovePathUnsafeCharacters(rom.MachineName) + (rom.MachineName.EndsWith(".7z") ? "" : ".7z")); - - // Set internal variables - SevenZipBase.SetLibraryPath("7za.dll"); - SevenZipExtractor oldZipFile = null; - SevenZipCompressor zipFile; - - try - { - // If the full output path doesn't exist, create it - if (!Directory.Exists(Path.GetDirectoryName(tempFile))) - { - Directory.CreateDirectory(Path.GetDirectoryName(tempFile)); - } - - // If the archive doesn't exist, create it and put the single file - if (!File.Exists(archiveFileName)) - { - zipFile = new SevenZipCompressor() - { - ArchiveFormat = OutArchiveFormat.SevenZip, - CompressionLevel = CompressionLevel.Normal, - }; - - // Create the temp directory - string tempPath = Path.Combine(outDir, Guid.NewGuid().ToString()); - if (!Directory.Exists(tempPath)) - { - Directory.CreateDirectory(tempPath); - } - - // Create a stream dictionary - Dictionary dict = new Dictionary(); - dict.Add(rom.Name, inputStream); - - // Now add the stream - zipFile.CompressStreamDictionary(dict, tempFile); - } - - // Otherwise, sort the input files and write out in the correct order - else - { - // Open the old archive for reading - using (oldZipFile = new SevenZipExtractor(archiveFileName)) - { - // Map all inputs to index - Dictionary inputIndexMap = new Dictionary(); - - // If the old one doesn't contain the new file, then add it - if (!oldZipFile.ArchiveFileNames.Contains(rom.Name.Replace('\\', '/'))) - { - inputIndexMap.Add(rom.Name.Replace('\\', '/'), -1); - } - - // Then add all of the old entries to it too - for (int i = 0; i < oldZipFile.FilesCount; i++) - { - inputIndexMap.Add(oldZipFile.ArchiveFileNames[i], i); - } - - // If the number of entries is the same as the old archive, skip out - if (inputIndexMap.Keys.Count <= oldZipFile.FilesCount) - { - success = true; - return success; - } - - // Otherwise, process the old zipfile - zipFile = new SevenZipCompressor() - { - ArchiveFormat = OutArchiveFormat.SevenZip, - CompressionLevel = CompressionLevel.Normal, - }; - - // Get the order for the entries with the new file - List keys = inputIndexMap.Keys.ToList(); - keys.Sort(ZipFile.TorrentZipStringCompare); - - // Copy over all files to the new archive - foreach (string key in keys) - { - // Get the index mapped to the key - int index = inputIndexMap[key]; - - // If we have the input file, add it now - if (index < 0) - { - // Create a stream dictionary - Dictionary dict = new Dictionary(); - dict.Add(rom.Name, inputStream); - - // Now add the stream - zipFile.CompressStreamDictionary(dict, tempFile); - } - - // Otherwise, copy the file from the old archive - else - { - Stream oldZipFileEntryStream = new MemoryStream(); - oldZipFile.ExtractFile(index, oldZipFileEntryStream); - oldZipFileEntryStream.Seek(0, SeekOrigin.Begin); - - // Create a stream dictionary - Dictionary dict = new Dictionary(); - dict.Add(oldZipFile.ArchiveFileNames[index], oldZipFileEntryStream); - - // Now add the stream - zipFile.CompressStreamDictionary(dict, tempFile); - oldZipFileEntryStream.Dispose(); - } - - // After the first file, make sure we're in append mode - zipFile.CompressionMode = CompressionMode.Append; - } - } - } - - success = true; - } - catch (Exception ex) - { - Console.WriteLine(ex); - success = false; - } - finally - { - inputStream?.Dispose(); - } - - // If the old file exists, delete it and replace - if (File.Exists(archiveFileName)) - { - Utilities.TryDeleteFile(archiveFileName); - } - File.Move(tempFile, archiveFileName); - - // Now make the file T7Z - // TODO: Add ACTUAL T7Z compatible code - - BinaryWriter bw = new BinaryWriter(Utilities.TryOpenReadWrite(archiveFileName)); - bw.Seek(0, SeekOrigin.Begin); - bw.Write(Constants.Torrent7ZipHeader); - bw.Seek(0, SeekOrigin.End); - - using (oldZipFile = new SevenZipExtractor(Utilities.TryOpenReadWrite(archiveFileName))) - { - - // Get the correct signature to use (Default 0, Unicode 1, SingleFile 2, StripFileNames 4) - byte[] tempsig = Constants.Torrent7ZipSignature; - if (oldZipFile.FilesCount > 1) - { - tempsig[16] = 0x2; - } - else - { - tempsig[16] = 0; - } - - bw.Write(tempsig); - bw.Flush(); - bw.Dispose(); - } - - return true; - } - - /// - /// Write a set of input files to a torrent7z archive (assuming the same output archive name) - /// - /// Input files to be moved - /// Output directory to build to - /// DatItem representing the new information - /// True if the date from the DAT should be used if available, false otherwise (default) - /// True if files should be output in Romba depot folders, false otherwise - /// True if the archive was written properly, false otherwise - public override bool Write(List inputFiles, string outDir, List roms, bool date = false, bool romba = false) - { - bool success = false; - string tempFile = Path.Combine(outDir, "tmp" + Guid.NewGuid().ToString()); - - // If either list of roms is null or empty, return - if (inputFiles == null || roms == null || inputFiles.Count == 0 || roms.Count == 0) - { - return success; - } - - // If the number of inputs is less than the number of available roms, return - if (inputFiles.Count < roms.Count) - { - return success; - } - - // If one of the files doesn't exist, return - foreach (string file in inputFiles) - { - if (!File.Exists(file)) - { - return success; - } - } - - // Get the output archive name from the first rebuild rom - string archiveFileName = Path.Combine(outDir, Utilities.RemovePathUnsafeCharacters(roms[0].MachineName) + (roms[0].MachineName.EndsWith(".7z") ? "" : ".7z")); - - // Set internal variables - SevenZipBase.SetLibraryPath("7za.dll"); - SevenZipExtractor oldZipFile; - SevenZipCompressor zipFile; - - try - { - // If the full output path doesn't exist, create it - if (!Directory.Exists(Path.GetDirectoryName(archiveFileName))) - { - Directory.CreateDirectory(Path.GetDirectoryName(archiveFileName)); - } - - // If the archive doesn't exist, create it and put the single file - if (!File.Exists(archiveFileName)) - { - zipFile = new SevenZipCompressor() - { - ArchiveFormat = OutArchiveFormat.SevenZip, - CompressionLevel = CompressionLevel.Normal, - }; - - // Map all inputs to index - Dictionary inputIndexMap = new Dictionary(); - for (int i = 0; i < inputFiles.Count; i++) - { - inputIndexMap.Add(roms[i].Name.Replace('\\', '/'), i); - } - - // Sort the keys in TZIP order - List keys = inputIndexMap.Keys.ToList(); - keys.Sort(ZipFile.TorrentZipStringCompare); - - // Create the temp directory - string tempPath = Path.Combine(outDir, Guid.NewGuid().ToString()); - if (!Directory.Exists(tempPath)) - { - Directory.CreateDirectory(tempPath); - } - - // Now add all of the files in order - foreach (string key in keys) - { - string newkey = Path.Combine(tempPath, key); - - File.Move(inputFiles[inputIndexMap[key]], newkey); - zipFile.CompressFiles(tempFile, newkey); - File.Move(newkey, inputFiles[inputIndexMap[key]]); - - // After the first file, make sure we're in append mode - zipFile.CompressionMode = CompressionMode.Append; - } - - Utilities.CleanDirectory(tempPath); - Utilities.TryDeleteDirectory(tempPath); - } - - // Otherwise, sort the input files and write out in the correct order - else - { - // Open the old archive for reading - using (oldZipFile = new SevenZipExtractor(archiveFileName)) - { - // Map all inputs to index - Dictionary inputIndexMap = new Dictionary(); - for (int i = 0; i < inputFiles.Count; i++) - { - // If the old one contains the new file, then just skip out - if (oldZipFile.ArchiveFileNames.Contains(roms[i].Name.Replace('\\', '/'))) - { - continue; - } - - inputIndexMap.Add(roms[i].Name.Replace('\\', '/'), -(i + 1)); - } - - // Then add all of the old entries to it too - for (int i = 0; i < oldZipFile.FilesCount; i++) - { - inputIndexMap.Add(oldZipFile.ArchiveFileNames[i], i); - } - - // If the number of entries is the same as the old archive, skip out - if (inputIndexMap.Keys.Count <= oldZipFile.FilesCount) - { - success = true; - return success; - } - - // Otherwise, process the old zipfile - zipFile = new SevenZipCompressor() - { - ArchiveFormat = OutArchiveFormat.SevenZip, - CompressionLevel = CompressionLevel.Normal, - }; - - // Get the order for the entries with the new file - List keys = inputIndexMap.Keys.ToList(); - keys.Sort(ZipFile.TorrentZipStringCompare); - - // Copy over all files to the new archive - foreach (string key in keys) - { - // Get the index mapped to the key - int index = inputIndexMap[key]; - - // If we have the input file, add it now - if (index < 0) - { - FileStream inputStream = Utilities.TryOpenRead(inputFiles[-index - 1]); - - // Create a stream dictionary - Dictionary dict = new Dictionary(); - dict.Add(key, inputStream); - - // Now add the stream - zipFile.CompressStreamDictionary(dict, tempFile); - } - - // Otherwise, copy the file from the old archive - else - { - Stream oldZipFileEntryStream = new MemoryStream(); - oldZipFile.ExtractFile(index, oldZipFileEntryStream); - oldZipFileEntryStream.Seek(0, SeekOrigin.Begin); - - // Create a stream dictionary - Dictionary dict = new Dictionary(); - dict.Add(oldZipFile.ArchiveFileNames[index], oldZipFileEntryStream); - - // Now add the stream - zipFile.CompressStreamDictionary(dict, tempFile); - oldZipFileEntryStream.Dispose(); - } - - // After the first file, make sure we're in append mode - zipFile.CompressionMode = CompressionMode.Append; - } - } - } - - success = true; - } - catch (Exception ex) - { - Console.WriteLine(ex); - success = false; - } - - // If the old file exists, delete it and replace - if (File.Exists(archiveFileName)) - { - Utilities.TryDeleteFile(archiveFileName); - } - File.Move(tempFile, archiveFileName); - - // Now make the file T7Z - // TODO: Add ACTUAL T7Z compatible code - - BinaryWriter bw = new BinaryWriter(Utilities.TryOpenReadWrite(archiveFileName)); - bw.Seek(0, SeekOrigin.Begin); - bw.Write(Constants.Torrent7ZipHeader); - bw.Seek(0, SeekOrigin.End); - - using (oldZipFile = new SevenZipExtractor(Utilities.TryOpenReadWrite(archiveFileName))) - { - // Get the correct signature to use (Default 0, Unicode 1, SingleFile 2, StripFileNames 4) - byte[] tempsig = Constants.Torrent7ZipSignature; - if (oldZipFile.FilesCount > 1) - { - tempsig[16] = 0x2; - } - else - { - tempsig[16] = 0; - } - - bw.Write(tempsig); - bw.Flush(); - bw.Dispose(); - } - - return true; - } - - #endregion - } + /// + /// Represents a Torrent7zip archive for reading and writing + /// + /// TODO: Torrent 7-zip: https://sourceforge.net/p/t7z/code/HEAD/tree/ + public class SevenZipArchive : BaseArchive + { + #region Constructors + + /// + /// Create a new TorrentSevenZipArchive with no base file + /// + public SevenZipArchive() + : base() + { + this.Type = FileType.SevenZipArchive; + } + + /// + /// Create a new TorrentSevenZipArchive from the given file + /// + /// Name of the file to use as an archive + /// True for opening file as read, false for opening file as write + /// True if hashes for this file should be calculated, false otherwise (default) + public SevenZipArchive(string filename, bool getHashes = false) + : base(filename, getHashes) + { + this.Type = FileType.SevenZipArchive; + } + + #endregion + + #region Extraction + + /// + /// Attempt to extract a file as an archive + /// + /// Output directory for archive extraction + /// True if the extraction was a success, false otherwise + public override bool CopyAll(string outDir) + { + bool encounteredErrors = true; + + try + { + // Create the temp directory + Directory.CreateDirectory(outDir); + + // Extract all files to the temp directory + SharpCompress.Archives.SevenZip.SevenZipArchive sza = SharpCompress.Archives.SevenZip.SevenZipArchive.Open(Utilities.TryOpenRead(this.Filename)); + foreach (SevenZipArchiveEntry entry in sza.Entries) + { + entry.WriteToDirectory(outDir, new ExtractionOptions { PreserveFileTime = true, ExtractFullPath = true, Overwrite = true }); + } + encounteredErrors = false; + sza.Dispose(); + } + catch (EndOfStreamException) + { + // Catch this but don't count it as an error because SharpCompress is unsafe + } + catch (InvalidOperationException) + { + encounteredErrors = true; + } + catch (Exception ex) + { + Globals.Logger.Error(ex.ToString()); + encounteredErrors = true; + } + + return encounteredErrors; + } + + /// + /// Attempt to extract a file from an archive + /// + /// Name of the entry to be extracted + /// Output directory for archive extraction + /// Name of the extracted file, null on error + public override string CopyToFile(string entryName, string outDir) + { + // Try to extract a stream using the given information + (MemoryStream ms, string realEntry) = CopyToStream(entryName); + + // If the memory stream and the entry name are both non-null, we write to file + if (ms != null && realEntry != null) + { + realEntry = Path.Combine(outDir, realEntry); + + // Create the output subfolder now + Directory.CreateDirectory(Path.GetDirectoryName(realEntry)); + + // Now open and write the file if possible + FileStream fs = Utilities.TryCreate(realEntry); + if (fs != null) + { + ms.Seek(0, SeekOrigin.Begin); + byte[] zbuffer = new byte[_bufferSize]; + int zlen; + while ((zlen = ms.Read(zbuffer, 0, _bufferSize)) > 0) + { + fs.Write(zbuffer, 0, zlen); + fs.Flush(); + } + + ms?.Dispose(); + fs?.Dispose(); + } + else + { + ms?.Dispose(); + fs?.Dispose(); + realEntry = null; + } + } + + return realEntry; + } + + /// + /// Attempt to extract a stream from an archive + /// + /// Name of the entry to be extracted + /// Output representing the entry name that was found + /// MemoryStream representing the entry, null on error + public override (MemoryStream, string) CopyToStream(string entryName) + { + MemoryStream ms = new MemoryStream(); + string realEntry = null; + + try + { + SharpCompress.Archives.SevenZip.SevenZipArchive sza = SharpCompress.Archives.SevenZip.SevenZipArchive.Open(this.Filename, new ReaderOptions { LeaveStreamOpen = false, }); + foreach (SevenZipArchiveEntry entry in sza.Entries) + { + if (entry != null && !entry.IsDirectory && entry.Key.Contains(entryName)) + { + // Write the file out + realEntry = entry.Key; + entry.WriteTo(ms); + break; + } + } + sza.Dispose(); + } + catch (Exception ex) + { + Globals.Logger.Error(ex.ToString()); + ms = null; + realEntry = null; + } + + return (ms, realEntry); + } + + #endregion + + #region Information + + /// + /// Generate a list of DatItem objects from the header values in an archive + /// + /// Hash representing the hashes that should be skipped + /// True if entry dates should be included, false otherwise (default) + /// List of DatItem objects representing the found data + /// TODO: All instances of Hash.DeepHashes should be made into 0x0 eventually + public override List GetChildren(Hash omitFromScan = Hash.DeepHashes, bool date = false) + { + List found = new List(); + string gamename = Path.GetFileNameWithoutExtension(this.Filename); + + try + { + SharpCompress.Archives.SevenZip.SevenZipArchive sza = SharpCompress.Archives.SevenZip.SevenZipArchive.Open(Utilities.TryOpenRead(this.Filename)); + foreach (SevenZipArchiveEntry entry in sza.Entries.Where(e => e != null && !e.IsDirectory)) + { + // If secure hashes are disabled, do a quickscan + if (omitFromScan == Hash.SecureHashes) + { + found.Add(new BaseFile + { + Filename = entry.Key, + Size = entry.Size, + CRC = BitConverter.GetBytes(entry.Crc), + Date = (date && entry.LastModifiedTime != null ? entry.LastModifiedTime?.ToString("yyyy/MM/dd hh:mm:ss") : null), + + Parent = gamename, + }); + } + // Otherwise, use the stream directly + else + { + Stream entryStream = entry.OpenEntryStream(); + BaseFile sevenZipEntryRom = Utilities.GetStreamInfo(entryStream, entry.Size, omitFromScan: omitFromScan); + sevenZipEntryRom.Filename = entry.Key; + sevenZipEntryRom.Parent = gamename; + sevenZipEntryRom.Date = (date && entry.LastModifiedTime != null ? entry.LastModifiedTime?.ToString("yyyy/MM/dd hh:mm:ss") : null); + found.Add(sevenZipEntryRom); + entryStream.Dispose(); + } + } + + // Dispose of the archive + sza.Dispose(); + } + catch (Exception ex) + { + Globals.Logger.Error(ex.ToString()); + return null; + } + + return found; + } + + /// + /// Generate a list of empty folders in an archive + /// + /// Input file to get data from + /// List of empty folders in the archive + public override List GetEmptyFolders() + { + List empties = new List(); + + try + { + SharpCompress.Archives.SevenZip.SevenZipArchive sza = SharpCompress.Archives.SevenZip.SevenZipArchive.Open(this.Filename, new ReaderOptions { LeaveStreamOpen = false }); + List sevenZipEntries = sza.Entries.OrderBy(e => e.Key, new NaturalSort.NaturalReversedComparer()).ToList(); + string lastSevenZipEntry = null; + foreach (SevenZipArchiveEntry entry in sevenZipEntries) + { + if (entry != null) + { + // If the current is a superset of last, we skip it + if (lastSevenZipEntry != null && lastSevenZipEntry.StartsWith(entry.Key)) + { + // No-op + } + // If the entry is a directory, we add it + else if (entry.IsDirectory) + { + empties.Add(entry.Key); + lastSevenZipEntry = entry.Key; + } + } + } + } + catch (Exception ex) + { + Globals.Logger.Error(ex.ToString()); + } + + return empties; + } + + /// + /// Check whether the input file is a standardized format + /// + /// TODO: Finish reading T7z information + public override bool IsTorrent() + { + bool ist7z = false; + + if (File.Exists(this.Filename)) + { + try + { + Stream fread = Utilities.TryOpenRead(this.Filename); + uint ar, offs = 0; + fread.Seek(0, SeekOrigin.Begin); + byte[] buffer = new byte[128]; + ar = (uint)fread.Read(buffer, 0, 4 + Constants.Torrent7ZipSignature.Length + 4); + if (ar < (4 + Constants.Torrent7ZipSignature.Length + 4)) + { + if (ar >= Constants.Torrent7ZipSignature.Length + 4) + { + ar -= (uint)(Constants.Torrent7ZipSignature.Length + 4); + } + if (ar <= Constants.Torrent7ZipHeader.Length) + { + ar = (uint)Constants.Torrent7ZipHeader.Length; + } + // memset(buffer+offs+ar,0,crcsz-ar) + } + + fread.Dispose(); + } + catch + { + Globals.Logger.Warning("File '{0}' could not be opened", this.Filename); + ist7z = false; + } + } + + return ist7z; + } + + #endregion + + #region Writing + + /// + /// Write an input file to a torrent7z archive + /// + /// Input filename to be moved + /// Output directory to build to + /// DatItem representing the new information + /// True if the date from the DAT should be used if available, false otherwise (default) + /// True if files should be output in Romba depot folders, false otherwise + /// True if the archive was written properly, false otherwise + public override bool Write(string inputFile, string outDir, Rom rom, bool date = false, bool romba = false) + { + // Get the file stream for the file and write out + return Write(Utilities.TryOpenRead(inputFile), outDir, rom, date: date); + } + + /// + /// Write an input file to a torrent7z archive + /// + /// Input stream to be moved + /// Output directory to build to + /// DatItem representing the new information + /// True if the date from the DAT should be used if available, false otherwise (default) + /// True if files should be output in Romba depot folders, false otherwise + /// True if the archive was written properly, false otherwise + public override bool Write(Stream inputStream, string outDir, Rom rom, bool date = false, bool romba = false) + { + bool success = false; + string tempFile = Path.Combine(outDir, "tmp" + Guid.NewGuid().ToString()); + + // If either input is null or empty, return + if (inputStream == null || rom == null || rom.Name == null) + { + return success; + } + + // If the stream is not readable, return + if (!inputStream.CanRead) + { + return success; + } + + // Seek to the beginning of the stream + inputStream.Seek(0, SeekOrigin.Begin); + + // Get the output archive name from the first rebuild rom + string archiveFileName = Path.Combine(outDir, Utilities.RemovePathUnsafeCharacters(rom.MachineName) + (rom.MachineName.EndsWith(".7z") ? "" : ".7z")); + + // Set internal variables + SevenZipBase.SetLibraryPath("7za.dll"); + SevenZipExtractor oldZipFile = null; + SevenZipCompressor zipFile; + + try + { + // If the full output path doesn't exist, create it + if (!Directory.Exists(Path.GetDirectoryName(tempFile))) + { + Directory.CreateDirectory(Path.GetDirectoryName(tempFile)); + } + + // If the archive doesn't exist, create it and put the single file + if (!File.Exists(archiveFileName)) + { + zipFile = new SevenZipCompressor() + { + ArchiveFormat = OutArchiveFormat.SevenZip, + CompressionLevel = CompressionLevel.Normal, + }; + + // Create the temp directory + string tempPath = Path.Combine(outDir, Guid.NewGuid().ToString()); + if (!Directory.Exists(tempPath)) + { + Directory.CreateDirectory(tempPath); + } + + // Create a stream dictionary + Dictionary dict = new Dictionary(); + dict.Add(rom.Name, inputStream); + + // Now add the stream + zipFile.CompressStreamDictionary(dict, tempFile); + } + + // Otherwise, sort the input files and write out in the correct order + else + { + // Open the old archive for reading + using (oldZipFile = new SevenZipExtractor(archiveFileName)) + { + // Map all inputs to index + Dictionary inputIndexMap = new Dictionary(); + + // If the old one doesn't contain the new file, then add it + if (!oldZipFile.ArchiveFileNames.Contains(rom.Name.Replace('\\', '/'))) + { + inputIndexMap.Add(rom.Name.Replace('\\', '/'), -1); + } + + // Then add all of the old entries to it too + for (int i = 0; i < oldZipFile.FilesCount; i++) + { + inputIndexMap.Add(oldZipFile.ArchiveFileNames[i], i); + } + + // If the number of entries is the same as the old archive, skip out + if (inputIndexMap.Keys.Count <= oldZipFile.FilesCount) + { + success = true; + return success; + } + + // Otherwise, process the old zipfile + zipFile = new SevenZipCompressor() + { + ArchiveFormat = OutArchiveFormat.SevenZip, + CompressionLevel = CompressionLevel.Normal, + }; + + // Get the order for the entries with the new file + List keys = inputIndexMap.Keys.ToList(); + keys.Sort(ZipFile.TorrentZipStringCompare); + + // Copy over all files to the new archive + foreach (string key in keys) + { + // Get the index mapped to the key + int index = inputIndexMap[key]; + + // If we have the input file, add it now + if (index < 0) + { + // Create a stream dictionary + Dictionary dict = new Dictionary(); + dict.Add(rom.Name, inputStream); + + // Now add the stream + zipFile.CompressStreamDictionary(dict, tempFile); + } + + // Otherwise, copy the file from the old archive + else + { + Stream oldZipFileEntryStream = new MemoryStream(); + oldZipFile.ExtractFile(index, oldZipFileEntryStream); + oldZipFileEntryStream.Seek(0, SeekOrigin.Begin); + + // Create a stream dictionary + Dictionary dict = new Dictionary(); + dict.Add(oldZipFile.ArchiveFileNames[index], oldZipFileEntryStream); + + // Now add the stream + zipFile.CompressStreamDictionary(dict, tempFile); + oldZipFileEntryStream.Dispose(); + } + + // After the first file, make sure we're in append mode + zipFile.CompressionMode = CompressionMode.Append; + } + } + } + + success = true; + } + catch (Exception ex) + { + Console.WriteLine(ex); + success = false; + } + finally + { + inputStream?.Dispose(); + } + + // If the old file exists, delete it and replace + if (File.Exists(archiveFileName)) + { + Utilities.TryDeleteFile(archiveFileName); + } + File.Move(tempFile, archiveFileName); + + // Now make the file T7Z + // TODO: Add ACTUAL T7Z compatible code + + BinaryWriter bw = new BinaryWriter(Utilities.TryOpenReadWrite(archiveFileName)); + bw.Seek(0, SeekOrigin.Begin); + bw.Write(Constants.Torrent7ZipHeader); + bw.Seek(0, SeekOrigin.End); + + using (oldZipFile = new SevenZipExtractor(Utilities.TryOpenReadWrite(archiveFileName))) + { + + // Get the correct signature to use (Default 0, Unicode 1, SingleFile 2, StripFileNames 4) + byte[] tempsig = Constants.Torrent7ZipSignature; + if (oldZipFile.FilesCount > 1) + { + tempsig[16] = 0x2; + } + else + { + tempsig[16] = 0; + } + + bw.Write(tempsig); + bw.Flush(); + bw.Dispose(); + } + + return true; + } + + /// + /// Write a set of input files to a torrent7z archive (assuming the same output archive name) + /// + /// Input files to be moved + /// Output directory to build to + /// DatItem representing the new information + /// True if the date from the DAT should be used if available, false otherwise (default) + /// True if files should be output in Romba depot folders, false otherwise + /// True if the archive was written properly, false otherwise + public override bool Write(List inputFiles, string outDir, List roms, bool date = false, bool romba = false) + { + bool success = false; + string tempFile = Path.Combine(outDir, "tmp" + Guid.NewGuid().ToString()); + + // If either list of roms is null or empty, return + if (inputFiles == null || roms == null || inputFiles.Count == 0 || roms.Count == 0) + { + return success; + } + + // If the number of inputs is less than the number of available roms, return + if (inputFiles.Count < roms.Count) + { + return success; + } + + // If one of the files doesn't exist, return + foreach (string file in inputFiles) + { + if (!File.Exists(file)) + { + return success; + } + } + + // Get the output archive name from the first rebuild rom + string archiveFileName = Path.Combine(outDir, Utilities.RemovePathUnsafeCharacters(roms[0].MachineName) + (roms[0].MachineName.EndsWith(".7z") ? "" : ".7z")); + + // Set internal variables + SevenZipBase.SetLibraryPath("7za.dll"); + SevenZipExtractor oldZipFile; + SevenZipCompressor zipFile; + + try + { + // If the full output path doesn't exist, create it + if (!Directory.Exists(Path.GetDirectoryName(archiveFileName))) + { + Directory.CreateDirectory(Path.GetDirectoryName(archiveFileName)); + } + + // If the archive doesn't exist, create it and put the single file + if (!File.Exists(archiveFileName)) + { + zipFile = new SevenZipCompressor() + { + ArchiveFormat = OutArchiveFormat.SevenZip, + CompressionLevel = CompressionLevel.Normal, + }; + + // Map all inputs to index + Dictionary inputIndexMap = new Dictionary(); + for (int i = 0; i < inputFiles.Count; i++) + { + inputIndexMap.Add(roms[i].Name.Replace('\\', '/'), i); + } + + // Sort the keys in TZIP order + List keys = inputIndexMap.Keys.ToList(); + keys.Sort(ZipFile.TorrentZipStringCompare); + + // Create the temp directory + string tempPath = Path.Combine(outDir, Guid.NewGuid().ToString()); + if (!Directory.Exists(tempPath)) + { + Directory.CreateDirectory(tempPath); + } + + // Now add all of the files in order + foreach (string key in keys) + { + string newkey = Path.Combine(tempPath, key); + + File.Move(inputFiles[inputIndexMap[key]], newkey); + zipFile.CompressFiles(tempFile, newkey); + File.Move(newkey, inputFiles[inputIndexMap[key]]); + + // After the first file, make sure we're in append mode + zipFile.CompressionMode = CompressionMode.Append; + } + + Utilities.CleanDirectory(tempPath); + Utilities.TryDeleteDirectory(tempPath); + } + + // Otherwise, sort the input files and write out in the correct order + else + { + // Open the old archive for reading + using (oldZipFile = new SevenZipExtractor(archiveFileName)) + { + // Map all inputs to index + Dictionary inputIndexMap = new Dictionary(); + for (int i = 0; i < inputFiles.Count; i++) + { + // If the old one contains the new file, then just skip out + if (oldZipFile.ArchiveFileNames.Contains(roms[i].Name.Replace('\\', '/'))) + { + continue; + } + + inputIndexMap.Add(roms[i].Name.Replace('\\', '/'), -(i + 1)); + } + + // Then add all of the old entries to it too + for (int i = 0; i < oldZipFile.FilesCount; i++) + { + inputIndexMap.Add(oldZipFile.ArchiveFileNames[i], i); + } + + // If the number of entries is the same as the old archive, skip out + if (inputIndexMap.Keys.Count <= oldZipFile.FilesCount) + { + success = true; + return success; + } + + // Otherwise, process the old zipfile + zipFile = new SevenZipCompressor() + { + ArchiveFormat = OutArchiveFormat.SevenZip, + CompressionLevel = CompressionLevel.Normal, + }; + + // Get the order for the entries with the new file + List keys = inputIndexMap.Keys.ToList(); + keys.Sort(ZipFile.TorrentZipStringCompare); + + // Copy over all files to the new archive + foreach (string key in keys) + { + // Get the index mapped to the key + int index = inputIndexMap[key]; + + // If we have the input file, add it now + if (index < 0) + { + FileStream inputStream = Utilities.TryOpenRead(inputFiles[-index - 1]); + + // Create a stream dictionary + Dictionary dict = new Dictionary(); + dict.Add(key, inputStream); + + // Now add the stream + zipFile.CompressStreamDictionary(dict, tempFile); + } + + // Otherwise, copy the file from the old archive + else + { + Stream oldZipFileEntryStream = new MemoryStream(); + oldZipFile.ExtractFile(index, oldZipFileEntryStream); + oldZipFileEntryStream.Seek(0, SeekOrigin.Begin); + + // Create a stream dictionary + Dictionary dict = new Dictionary(); + dict.Add(oldZipFile.ArchiveFileNames[index], oldZipFileEntryStream); + + // Now add the stream + zipFile.CompressStreamDictionary(dict, tempFile); + oldZipFileEntryStream.Dispose(); + } + + // After the first file, make sure we're in append mode + zipFile.CompressionMode = CompressionMode.Append; + } + } + } + + success = true; + } + catch (Exception ex) + { + Console.WriteLine(ex); + success = false; + } + + // If the old file exists, delete it and replace + if (File.Exists(archiveFileName)) + { + Utilities.TryDeleteFile(archiveFileName); + } + File.Move(tempFile, archiveFileName); + + // Now make the file T7Z + // TODO: Add ACTUAL T7Z compatible code + + BinaryWriter bw = new BinaryWriter(Utilities.TryOpenReadWrite(archiveFileName)); + bw.Seek(0, SeekOrigin.Begin); + bw.Write(Constants.Torrent7ZipHeader); + bw.Seek(0, SeekOrigin.End); + + using (oldZipFile = new SevenZipExtractor(Utilities.TryOpenReadWrite(archiveFileName))) + { + // Get the correct signature to use (Default 0, Unicode 1, SingleFile 2, StripFileNames 4) + byte[] tempsig = Constants.Torrent7ZipSignature; + if (oldZipFile.FilesCount > 1) + { + tempsig[16] = 0x2; + } + else + { + tempsig[16] = 0; + } + + bw.Write(tempsig); + bw.Flush(); + bw.Dispose(); + } + + return true; + } + + #endregion + } } diff --git a/SabreTools.Library/FileTypes/TapeArchive.cs b/SabreTools.Library/FileTypes/TapeArchive.cs index 31b6d5b9..d2793e94 100644 --- a/SabreTools.Library/FileTypes/TapeArchive.cs +++ b/SabreTools.Library/FileTypes/TapeArchive.cs @@ -26,620 +26,620 @@ using SharpCompress.Writers; namespace SabreTools.Library.FileTypes { - /// - /// Represents a Torrent7zip archive for reading and writing - /// - /// TODO: Don't try to read entries to MemoryStream during write - public class TapeArchive : BaseArchive - { - #region Constructors - - /// - /// Create a new TorrentTarArchive with no base file - /// - public TapeArchive() - : base() - { - _fileType = FileType.TapeArchive; - } - - /// - /// Create a new TorrentTarArchive from the given file - /// - /// Name of the file to use as an archive - /// True for opening file as read, false for opening file as write - /// True if hashes for this file should be calculated, false otherwise (default) - public TapeArchive(string filename, bool getHashes = false) - : base(filename, getHashes) - { - _fileType = FileType.TapeArchive; - } - - #endregion - - #region Extraction - - /// - /// Attempt to extract a file as an archive - /// - /// Output directory for archive extraction - /// True if the extraction was a success, false otherwise - public override bool CopyAll(string outDir) - { - bool encounteredErrors = true; - - try - { - // Create the temp directory - Directory.CreateDirectory(outDir); - - // Extract all files to the temp directory - TarArchive ta = TarArchive.Open(_filename); - foreach (TarArchiveEntry entry in ta.Entries) - { - entry.WriteToDirectory(outDir, new ExtractionOptions { PreserveFileTime = true, ExtractFullPath = true, Overwrite = true }); - } - encounteredErrors = false; - ta.Dispose(); - } - catch (EndOfStreamException) - { - // Catch this but don't count it as an error because SharpCompress is unsafe - } - catch (InvalidOperationException) - { - encounteredErrors = true; - } - catch (Exception ex) - { - Globals.Logger.Error(ex.ToString()); - encounteredErrors = true; - } - - return encounteredErrors; - } - - /// - /// Attempt to extract a file from an archive - /// - /// Name of the entry to be extracted - /// Output directory for archive extraction - /// Name of the extracted file, null on error - public override string CopyToFile(string entryName, string outDir) - { - // Try to extract a stream using the given information - (MemoryStream ms, string realEntry) = CopyToStream(entryName); - - // If the memory stream and the entry name are both non-null, we write to file - if (ms != null && realEntry != null) - { - realEntry = Path.Combine(outDir, realEntry); - - // Create the output subfolder now - Directory.CreateDirectory(Path.GetDirectoryName(realEntry)); - - // Now open and write the file if possible - FileStream fs = Utilities.TryCreate(realEntry); - if (fs != null) - { - ms.Seek(0, SeekOrigin.Begin); - byte[] zbuffer = new byte[_bufferSize]; - int zlen; - while ((zlen = ms.Read(zbuffer, 0, _bufferSize)) > 0) - { - fs.Write(zbuffer, 0, zlen); - fs.Flush(); - } - - ms?.Dispose(); - fs?.Dispose(); - } - else - { - ms?.Dispose(); - fs?.Dispose(); - realEntry = null; - } - } - - return realEntry; - } - - /// - /// Attempt to extract a stream from an archive - /// - /// Name of the entry to be extracted - /// Output representing the entry name that was found - /// MemoryStream representing the entry, null on error - public override (MemoryStream, string) CopyToStream(string entryName) - { - MemoryStream ms = new MemoryStream(); - string realEntry = null; - - try - { - TarArchive ta = TarArchive.Open(_filename, new ReaderOptions { LeaveStreamOpen = false, }); - foreach (TarArchiveEntry entry in ta.Entries) - { - if (entry != null && !entry.IsDirectory && entry.Key.Contains(entryName)) - { - // Write the file out - realEntry = entry.Key; - entry.WriteTo(ms); - } - } - ta.Dispose(); - } - catch (Exception ex) - { - Globals.Logger.Error(ex.ToString()); - ms = null; - realEntry = null; - } - - return (ms, realEntry); - } - - #endregion - - #region Information - - /// - /// Generate a list of DatItem objects from the header values in an archive - /// - /// Hash representing the hashes that should be skipped - /// True if entry dates should be included, false otherwise (default) - /// List of DatItem objects representing the found data - /// TODO: All instances of Hash.DeepHashes should be made into 0x0 eventually - public override List GetChildren(Hash omitFromScan = Hash.DeepHashes, bool date = false) - { - List found = new List(); - string gamename = Path.GetFileNameWithoutExtension(_filename); - - try - { - TarArchive ta = TarArchive.Open(Utilities.TryOpenRead(_filename)); - foreach (TarArchiveEntry entry in ta.Entries.Where(e => e != null && !e.IsDirectory)) - { - // If secure hashes are disabled, do a quickscan - if (omitFromScan == Hash.SecureHashes) - { - found.Add(new BaseFile - { - Filename = entry.Key, - Size = entry.Size, - CRC = BitConverter.GetBytes(entry.Crc), - Date = (date && entry.LastModifiedTime != null ? entry.LastModifiedTime?.ToString("yyyy/MM/dd hh:mm:ss") : null), - - Parent = gamename, - }); - } - // Otherwise, use the stream directly - else - { - Stream entryStream = entry.OpenEntryStream(); - BaseFile tarEntryRom = Utilities.GetStreamInfo(entryStream, entry.Size, omitFromScan: omitFromScan); - tarEntryRom.Filename = entry.Key; - tarEntryRom.Parent = gamename; - tarEntryRom.Date = entry.LastModifiedTime?.ToString("yyyy/MM/dd hh:mm:ss"); - found.Add(tarEntryRom); - entryStream.Dispose(); - } - } - - // Dispose of the archive - ta.Dispose(); - } - catch (Exception ex) - { - Globals.Logger.Error(ex.ToString()); - return null; - } - - return found; - } - - /// - /// Generate a list of empty folders in an archive - /// - /// Input file to get data from - /// List of empty folders in the archive - public override List GetEmptyFolders() - { - List empties = new List(); - - try - { - TarArchive ta = TarArchive.Open(_filename, new ReaderOptions { LeaveStreamOpen = false }); - List tarEntries = ta.Entries.OrderBy(e => e.Key, new NaturalSort.NaturalReversedComparer()).ToList(); - string lastTarEntry = null; - foreach (TarArchiveEntry entry in tarEntries) - { - if (entry != null) - { - // If the current is a superset of last, we skip it - if (lastTarEntry != null && lastTarEntry.StartsWith(entry.Key)) - { - // No-op - } - // If the entry is a directory, we add it - else if (entry.IsDirectory) - { - empties.Add(entry.Key); - lastTarEntry = entry.Key; - } - } - } - } - catch (Exception ex) - { - Globals.Logger.Error(ex.ToString()); - } - - return empties; - } - - /// - /// Check whether the input file is a standardized format - /// - public override bool IsTorrent() - { - throw new NotImplementedException(); - } - - #endregion - - #region Writing - - /// - /// Write an input file to a tape archive - /// - /// Input filename to be moved - /// Output directory to build to - /// DatItem representing the new information - /// True if the date from the DAT should be used if available, false otherwise (default) - /// True if files should be output in Romba depot folders, false otherwise - /// True if the archive was written properly, false otherwise - public override bool Write(string inputFile, string outDir, Rom rom, bool date = false, bool romba = false) - { - // Get the file stream for the file and write out - return Write(Utilities.TryOpenRead(inputFile), outDir, rom, date: date); - } - - /// - /// Write an input stream to a tape archive - /// - /// Input stream to be moved - /// Output directory to build to - /// DatItem representing the new information - /// True if the date from the DAT should be used if available, false otherwise (default) - /// True if files should be output in Romba depot folders, false otherwise - /// True if the archive was written properly, false otherwise - public override bool Write(Stream inputStream, string outDir, Rom rom, bool date = false, bool romba = false) - { - bool success = false; - string tempFile = Path.Combine(outDir, "tmp" + Guid.NewGuid().ToString()); - - // If either input is null or empty, return - if (inputStream == null || rom == null || rom.Name == null) - { - return success; - } - - // If the stream is not readable, return - if (!inputStream.CanRead) - { - return success; - } - - // Get the output archive name from the first rebuild rom - string archiveFileName = Path.Combine(outDir, Utilities.RemovePathUnsafeCharacters(rom.MachineName) + (rom.MachineName.EndsWith(".tar") ? "" : ".tar")); - - // Set internal variables - TarArchive oldTarFile = TarArchive.Create(); - TarArchive tarFile = TarArchive.Create(); - - try - { - // If the full output path doesn't exist, create it - if (!Directory.Exists(Path.GetDirectoryName(archiveFileName))) - { - Directory.CreateDirectory(Path.GetDirectoryName(archiveFileName)); - } - - // If the archive doesn't exist, create it and put the single file - if (!File.Exists(archiveFileName)) - { - // Get temporary date-time if possible - DateTime? usableDate = null; - if (date && !String.IsNullOrWhiteSpace(rom.Date) && DateTime.TryParse(rom.Date.Replace('\\', '/'), out DateTime dt)) - { - usableDate = dt; - } - - // Copy the input stream to the output - inputStream.Seek(0, SeekOrigin.Begin); - tarFile.AddEntry(rom.Name, inputStream, size: rom.Size, modified: usableDate); - } - - // Otherwise, sort the input files and write out in the correct order - else - { - // Open the old archive for reading - oldTarFile = TarArchive.Open(archiveFileName); - - // Get a list of all current entries - List entries = oldTarFile.Entries.Select(i => i.Key).ToList(); - - // Map all inputs to index - Dictionary inputIndexMap = new Dictionary(); - - // If the old one doesn't contain the new file, then add it - if (!entries.Contains(rom.Name.Replace('\\', '/'))) - { - inputIndexMap.Add(rom.Name.Replace('\\', '/'), -1); - } - - // Then add all of the old entries to it too - for (int i = 0; i < entries.Count; i++) - { - inputIndexMap.Add(entries[i], i); - } - - // If the number of entries is the same as the old archive, skip out - if (inputIndexMap.Keys.Count <= entries.Count) - { - success = true; - return success; - } - - // Get the order for the entries with the new file - List keys = inputIndexMap.Keys.ToList(); - keys.Sort(ZipFile.TorrentZipStringCompare); - - // Copy over all files to the new archive - foreach (string key in keys) - { - // Get the index mapped to the key - int index = inputIndexMap[key]; - - // Get temporary date-time if possible - DateTime? usableDate = null; - if (date && !String.IsNullOrWhiteSpace(rom.Date) && DateTime.TryParse(rom.Date.Replace('\\', '/'), out DateTime dt)) - { - usableDate = dt; - } - - // If we have the input file, add it now - if (index < 0) - { - // Copy the input file to the output - inputStream.Seek(0, SeekOrigin.Begin); - tarFile.AddEntry(rom.Name, inputStream, size: rom.Size, modified: usableDate); - } - - // Otherwise, copy the file from the old archive - else - { - // Get the stream from the original archive - TarArchiveEntry tae = oldTarFile.Entries.ElementAt(index); - MemoryStream entry = new MemoryStream(); - tae.OpenEntryStream().CopyTo(entry); - - // Copy the input stream to the output - tarFile.AddEntry(key, entry, size: tae.Size, modified: tae.LastModifiedTime); - } - } - } - - // Close the output tar file - tarFile.SaveTo(tempFile, new WriterOptions(CompressionType.None)); - - success = true; - } - catch (Exception ex) - { - Console.WriteLine(ex); - success = false; - } - finally - { - inputStream.Dispose(); - tarFile.Dispose(); - oldTarFile.Dispose(); - } - - // If the old file exists, delete it and replace - if (File.Exists(archiveFileName)) - { - Utilities.TryDeleteFile(archiveFileName); - } - File.Move(tempFile, archiveFileName); - - return success; - } - - /// - /// Write a set of input files to a tape archive (assuming the same output archive name) - /// - /// Input files to be moved - /// Output directory to build to - /// DatItem representing the new information - /// True if the date from the DAT should be used if available, false otherwise (default) - /// True if files should be output in Romba depot folders, false otherwise - /// True if the archive was written properly, false otherwise - public override bool Write(List inputFiles, string outDir, List roms, bool date = false, bool romba = false) - { - bool success = false; - string tempFile = Path.Combine(outDir, "tmp" + Guid.NewGuid().ToString()); - - // If either list of roms is null or empty, return - if (inputFiles == null || roms == null || inputFiles.Count == 0 || roms.Count == 0) - { - return success; - } - - // If the number of inputs is less than the number of available roms, return - if (inputFiles.Count < roms.Count) - { - return success; - } - - // If one of the files doesn't exist, return - foreach (string file in inputFiles) - { - if (!File.Exists(file)) - { - return success; - } - } - - // Get the output archive name from the first rebuild rom - string archiveFileName = Path.Combine(outDir, Utilities.RemovePathUnsafeCharacters(roms[0].MachineName) + (roms[0].MachineName.EndsWith(".tar") ? "" : ".tar")); - - // Set internal variables - TarArchive oldTarFile = TarArchive.Create(); - TarArchive tarFile = TarArchive.Create(); - - try - { - // If the full output path doesn't exist, create it - if (!Directory.Exists(Path.GetDirectoryName(archiveFileName))) - { - Directory.CreateDirectory(Path.GetDirectoryName(archiveFileName)); - } - - // If the archive doesn't exist, create it and put the single file - if (!File.Exists(archiveFileName)) - { - // Map all inputs to index - Dictionary inputIndexMap = new Dictionary(); - for (int i = 0; i < inputFiles.Count; i++) - { - inputIndexMap.Add(roms[i].Name.Replace('\\', '/'), i); - } - - // Sort the keys in TZIP order - List keys = inputIndexMap.Keys.ToList(); - keys.Sort(ZipFile.TorrentZipStringCompare); - - // Now add all of the files in order - foreach (string key in keys) - { - // Get the index mapped to the key - int index = inputIndexMap[key]; - - // Get temporary date-time if possible - DateTime? usableDate = null; - if (date && !String.IsNullOrWhiteSpace(roms[index].Date) && DateTime.TryParse(roms[index].Date.Replace('\\', '/'), out DateTime dt)) - { - usableDate = dt; - } - - // Copy the input stream to the output - tarFile.AddEntry(roms[index].Name, Utilities.TryOpenRead(inputFiles[index]), size: roms[index].Size, modified: usableDate); - } - } - - // Otherwise, sort the input files and write out in the correct order - else - { - // Open the old archive for reading - oldTarFile = TarArchive.Open(archiveFileName); - - // Get a list of all current entries - List entries = oldTarFile.Entries.Select(i => i.Key).ToList(); - - // Map all inputs to index - Dictionary inputIndexMap = new Dictionary(); - for (int i = 0; i < inputFiles.Count; i++) - { - // If the old one contains the new file, then just skip out - if (entries.Contains(roms[i].Name.Replace('\\', '/'))) - { - continue; - } - - inputIndexMap.Add(roms[i].Name.Replace('\\', '/'), -(i + 1)); - } - - // Then add all of the old entries to it too - for (int i = 0; i < entries.Count; i++) - { - inputIndexMap.Add(entries[i], i); - } - - // If the number of entries is the same as the old archive, skip out - if (inputIndexMap.Keys.Count <= entries.Count) - { - success = true; - return success; - } - - // Get the order for the entries with the new file - List keys = inputIndexMap.Keys.ToList(); - keys.Sort(ZipFile.TorrentZipStringCompare); - - // Copy over all files to the new archive - foreach (string key in keys) - { - // Get the index mapped to the key - int index = inputIndexMap[key]; - - // If we have the input file, add it now - if (index < 0) - { - // Get temporary date-time if possible - DateTime? usableDate = null; - if (date && !String.IsNullOrWhiteSpace(roms[-index - 1].Date) && DateTime.TryParse(roms[-index - 1].Date.Replace('\\', '/'), out DateTime dt)) - { - usableDate = dt; - } - - // Copy the input file to the output - tarFile.AddEntry(roms[-index - 1].Name, Utilities.TryOpenRead(inputFiles[-index - 1]), size: roms[-index - 1].Size, modified: usableDate); - } - - // Otherwise, copy the file from the old archive - else - { - // Get the stream from the original archive - TarArchiveEntry tae = oldTarFile.Entries.ElementAt(index); - MemoryStream entry = new MemoryStream(); - tae.OpenEntryStream().CopyTo(entry); - - // Copy the input stream to the output - tarFile.AddEntry(key, entry, size: tae.Size, modified: tae.LastModifiedTime); - } - } - } - - // Close the output tar file - tarFile.SaveTo(tempFile, new WriterOptions(CompressionType.None)); - - success = true; - } - catch (Exception ex) - { - Console.WriteLine(ex); - success = false; - } - finally - { - tarFile.Dispose(); - oldTarFile.Dispose(); - } - - // If the old file exists, delete it and replace - if (File.Exists(archiveFileName)) - { - Utilities.TryDeleteFile(archiveFileName); - } - File.Move(tempFile, archiveFileName); - - return true; - } - - #endregion - } + /// + /// Represents a Torrent7zip archive for reading and writing + /// + /// TODO: Don't try to read entries to MemoryStream during write + public class TapeArchive : BaseArchive + { + #region Constructors + + /// + /// Create a new TorrentTarArchive with no base file + /// + public TapeArchive() + : base() + { + this.Type = FileType.TapeArchive; + } + + /// + /// Create a new TorrentTarArchive from the given file + /// + /// Name of the file to use as an archive + /// True for opening file as read, false for opening file as write + /// True if hashes for this file should be calculated, false otherwise (default) + public TapeArchive(string filename, bool getHashes = false) + : base(filename, getHashes) + { + this.Type = FileType.TapeArchive; + } + + #endregion + + #region Extraction + + /// + /// Attempt to extract a file as an archive + /// + /// Output directory for archive extraction + /// True if the extraction was a success, false otherwise + public override bool CopyAll(string outDir) + { + bool encounteredErrors = true; + + try + { + // Create the temp directory + Directory.CreateDirectory(outDir); + + // Extract all files to the temp directory + TarArchive ta = TarArchive.Open(this.Filename); + foreach (TarArchiveEntry entry in ta.Entries) + { + entry.WriteToDirectory(outDir, new ExtractionOptions { PreserveFileTime = true, ExtractFullPath = true, Overwrite = true }); + } + encounteredErrors = false; + ta.Dispose(); + } + catch (EndOfStreamException) + { + // Catch this but don't count it as an error because SharpCompress is unsafe + } + catch (InvalidOperationException) + { + encounteredErrors = true; + } + catch (Exception ex) + { + Globals.Logger.Error(ex.ToString()); + encounteredErrors = true; + } + + return encounteredErrors; + } + + /// + /// Attempt to extract a file from an archive + /// + /// Name of the entry to be extracted + /// Output directory for archive extraction + /// Name of the extracted file, null on error + public override string CopyToFile(string entryName, string outDir) + { + // Try to extract a stream using the given information + (MemoryStream ms, string realEntry) = CopyToStream(entryName); + + // If the memory stream and the entry name are both non-null, we write to file + if (ms != null && realEntry != null) + { + realEntry = Path.Combine(outDir, realEntry); + + // Create the output subfolder now + Directory.CreateDirectory(Path.GetDirectoryName(realEntry)); + + // Now open and write the file if possible + FileStream fs = Utilities.TryCreate(realEntry); + if (fs != null) + { + ms.Seek(0, SeekOrigin.Begin); + byte[] zbuffer = new byte[_bufferSize]; + int zlen; + while ((zlen = ms.Read(zbuffer, 0, _bufferSize)) > 0) + { + fs.Write(zbuffer, 0, zlen); + fs.Flush(); + } + + ms?.Dispose(); + fs?.Dispose(); + } + else + { + ms?.Dispose(); + fs?.Dispose(); + realEntry = null; + } + } + + return realEntry; + } + + /// + /// Attempt to extract a stream from an archive + /// + /// Name of the entry to be extracted + /// Output representing the entry name that was found + /// MemoryStream representing the entry, null on error + public override (MemoryStream, string) CopyToStream(string entryName) + { + MemoryStream ms = new MemoryStream(); + string realEntry = null; + + try + { + TarArchive ta = TarArchive.Open(this.Filename, new ReaderOptions { LeaveStreamOpen = false, }); + foreach (TarArchiveEntry entry in ta.Entries) + { + if (entry != null && !entry.IsDirectory && entry.Key.Contains(entryName)) + { + // Write the file out + realEntry = entry.Key; + entry.WriteTo(ms); + } + } + ta.Dispose(); + } + catch (Exception ex) + { + Globals.Logger.Error(ex.ToString()); + ms = null; + realEntry = null; + } + + return (ms, realEntry); + } + + #endregion + + #region Information + + /// + /// Generate a list of DatItem objects from the header values in an archive + /// + /// Hash representing the hashes that should be skipped + /// True if entry dates should be included, false otherwise (default) + /// List of DatItem objects representing the found data + /// TODO: All instances of Hash.DeepHashes should be made into 0x0 eventually + public override List GetChildren(Hash omitFromScan = Hash.DeepHashes, bool date = false) + { + List found = new List(); + string gamename = Path.GetFileNameWithoutExtension(this.Filename); + + try + { + TarArchive ta = TarArchive.Open(Utilities.TryOpenRead(this.Filename)); + foreach (TarArchiveEntry entry in ta.Entries.Where(e => e != null && !e.IsDirectory)) + { + // If secure hashes are disabled, do a quickscan + if (omitFromScan == Hash.SecureHashes) + { + found.Add(new BaseFile + { + Filename = entry.Key, + Size = entry.Size, + CRC = BitConverter.GetBytes(entry.Crc), + Date = (date && entry.LastModifiedTime != null ? entry.LastModifiedTime?.ToString("yyyy/MM/dd hh:mm:ss") : null), + + Parent = gamename, + }); + } + // Otherwise, use the stream directly + else + { + Stream entryStream = entry.OpenEntryStream(); + BaseFile tarEntryRom = Utilities.GetStreamInfo(entryStream, entry.Size, omitFromScan: omitFromScan); + tarEntryRom.Filename = entry.Key; + tarEntryRom.Parent = gamename; + tarEntryRom.Date = entry.LastModifiedTime?.ToString("yyyy/MM/dd hh:mm:ss"); + found.Add(tarEntryRom); + entryStream.Dispose(); + } + } + + // Dispose of the archive + ta.Dispose(); + } + catch (Exception ex) + { + Globals.Logger.Error(ex.ToString()); + return null; + } + + return found; + } + + /// + /// Generate a list of empty folders in an archive + /// + /// Input file to get data from + /// List of empty folders in the archive + public override List GetEmptyFolders() + { + List empties = new List(); + + try + { + TarArchive ta = TarArchive.Open(this.Filename, new ReaderOptions { LeaveStreamOpen = false }); + List tarEntries = ta.Entries.OrderBy(e => e.Key, new NaturalSort.NaturalReversedComparer()).ToList(); + string lastTarEntry = null; + foreach (TarArchiveEntry entry in tarEntries) + { + if (entry != null) + { + // If the current is a superset of last, we skip it + if (lastTarEntry != null && lastTarEntry.StartsWith(entry.Key)) + { + // No-op + } + // If the entry is a directory, we add it + else if (entry.IsDirectory) + { + empties.Add(entry.Key); + lastTarEntry = entry.Key; + } + } + } + } + catch (Exception ex) + { + Globals.Logger.Error(ex.ToString()); + } + + return empties; + } + + /// + /// Check whether the input file is a standardized format + /// + public override bool IsTorrent() + { + throw new NotImplementedException(); + } + + #endregion + + #region Writing + + /// + /// Write an input file to a tape archive + /// + /// Input filename to be moved + /// Output directory to build to + /// DatItem representing the new information + /// True if the date from the DAT should be used if available, false otherwise (default) + /// True if files should be output in Romba depot folders, false otherwise + /// True if the archive was written properly, false otherwise + public override bool Write(string inputFile, string outDir, Rom rom, bool date = false, bool romba = false) + { + // Get the file stream for the file and write out + return Write(Utilities.TryOpenRead(inputFile), outDir, rom, date: date); + } + + /// + /// Write an input stream to a tape archive + /// + /// Input stream to be moved + /// Output directory to build to + /// DatItem representing the new information + /// True if the date from the DAT should be used if available, false otherwise (default) + /// True if files should be output in Romba depot folders, false otherwise + /// True if the archive was written properly, false otherwise + public override bool Write(Stream inputStream, string outDir, Rom rom, bool date = false, bool romba = false) + { + bool success = false; + string tempFile = Path.Combine(outDir, "tmp" + Guid.NewGuid().ToString()); + + // If either input is null or empty, return + if (inputStream == null || rom == null || rom.Name == null) + { + return success; + } + + // If the stream is not readable, return + if (!inputStream.CanRead) + { + return success; + } + + // Get the output archive name from the first rebuild rom + string archiveFileName = Path.Combine(outDir, Utilities.RemovePathUnsafeCharacters(rom.MachineName) + (rom.MachineName.EndsWith(".tar") ? "" : ".tar")); + + // Set internal variables + TarArchive oldTarFile = TarArchive.Create(); + TarArchive tarFile = TarArchive.Create(); + + try + { + // If the full output path doesn't exist, create it + if (!Directory.Exists(Path.GetDirectoryName(archiveFileName))) + { + Directory.CreateDirectory(Path.GetDirectoryName(archiveFileName)); + } + + // If the archive doesn't exist, create it and put the single file + if (!File.Exists(archiveFileName)) + { + // Get temporary date-time if possible + DateTime? usableDate = null; + if (date && !String.IsNullOrWhiteSpace(rom.Date) && DateTime.TryParse(rom.Date.Replace('\\', '/'), out DateTime dt)) + { + usableDate = dt; + } + + // Copy the input stream to the output + inputStream.Seek(0, SeekOrigin.Begin); + tarFile.AddEntry(rom.Name, inputStream, size: rom.Size, modified: usableDate); + } + + // Otherwise, sort the input files and write out in the correct order + else + { + // Open the old archive for reading + oldTarFile = TarArchive.Open(archiveFileName); + + // Get a list of all current entries + List entries = oldTarFile.Entries.Select(i => i.Key).ToList(); + + // Map all inputs to index + Dictionary inputIndexMap = new Dictionary(); + + // If the old one doesn't contain the new file, then add it + if (!entries.Contains(rom.Name.Replace('\\', '/'))) + { + inputIndexMap.Add(rom.Name.Replace('\\', '/'), -1); + } + + // Then add all of the old entries to it too + for (int i = 0; i < entries.Count; i++) + { + inputIndexMap.Add(entries[i], i); + } + + // If the number of entries is the same as the old archive, skip out + if (inputIndexMap.Keys.Count <= entries.Count) + { + success = true; + return success; + } + + // Get the order for the entries with the new file + List keys = inputIndexMap.Keys.ToList(); + keys.Sort(ZipFile.TorrentZipStringCompare); + + // Copy over all files to the new archive + foreach (string key in keys) + { + // Get the index mapped to the key + int index = inputIndexMap[key]; + + // Get temporary date-time if possible + DateTime? usableDate = null; + if (date && !String.IsNullOrWhiteSpace(rom.Date) && DateTime.TryParse(rom.Date.Replace('\\', '/'), out DateTime dt)) + { + usableDate = dt; + } + + // If we have the input file, add it now + if (index < 0) + { + // Copy the input file to the output + inputStream.Seek(0, SeekOrigin.Begin); + tarFile.AddEntry(rom.Name, inputStream, size: rom.Size, modified: usableDate); + } + + // Otherwise, copy the file from the old archive + else + { + // Get the stream from the original archive + TarArchiveEntry tae = oldTarFile.Entries.ElementAt(index); + MemoryStream entry = new MemoryStream(); + tae.OpenEntryStream().CopyTo(entry); + + // Copy the input stream to the output + tarFile.AddEntry(key, entry, size: tae.Size, modified: tae.LastModifiedTime); + } + } + } + + // Close the output tar file + tarFile.SaveTo(tempFile, new WriterOptions(CompressionType.None)); + + success = true; + } + catch (Exception ex) + { + Console.WriteLine(ex); + success = false; + } + finally + { + inputStream.Dispose(); + tarFile.Dispose(); + oldTarFile.Dispose(); + } + + // If the old file exists, delete it and replace + if (File.Exists(archiveFileName)) + { + Utilities.TryDeleteFile(archiveFileName); + } + File.Move(tempFile, archiveFileName); + + return success; + } + + /// + /// Write a set of input files to a tape archive (assuming the same output archive name) + /// + /// Input files to be moved + /// Output directory to build to + /// DatItem representing the new information + /// True if the date from the DAT should be used if available, false otherwise (default) + /// True if files should be output in Romba depot folders, false otherwise + /// True if the archive was written properly, false otherwise + public override bool Write(List inputFiles, string outDir, List roms, bool date = false, bool romba = false) + { + bool success = false; + string tempFile = Path.Combine(outDir, "tmp" + Guid.NewGuid().ToString()); + + // If either list of roms is null or empty, return + if (inputFiles == null || roms == null || inputFiles.Count == 0 || roms.Count == 0) + { + return success; + } + + // If the number of inputs is less than the number of available roms, return + if (inputFiles.Count < roms.Count) + { + return success; + } + + // If one of the files doesn't exist, return + foreach (string file in inputFiles) + { + if (!File.Exists(file)) + { + return success; + } + } + + // Get the output archive name from the first rebuild rom + string archiveFileName = Path.Combine(outDir, Utilities.RemovePathUnsafeCharacters(roms[0].MachineName) + (roms[0].MachineName.EndsWith(".tar") ? "" : ".tar")); + + // Set internal variables + TarArchive oldTarFile = TarArchive.Create(); + TarArchive tarFile = TarArchive.Create(); + + try + { + // If the full output path doesn't exist, create it + if (!Directory.Exists(Path.GetDirectoryName(archiveFileName))) + { + Directory.CreateDirectory(Path.GetDirectoryName(archiveFileName)); + } + + // If the archive doesn't exist, create it and put the single file + if (!File.Exists(archiveFileName)) + { + // Map all inputs to index + Dictionary inputIndexMap = new Dictionary(); + for (int i = 0; i < inputFiles.Count; i++) + { + inputIndexMap.Add(roms[i].Name.Replace('\\', '/'), i); + } + + // Sort the keys in TZIP order + List keys = inputIndexMap.Keys.ToList(); + keys.Sort(ZipFile.TorrentZipStringCompare); + + // Now add all of the files in order + foreach (string key in keys) + { + // Get the index mapped to the key + int index = inputIndexMap[key]; + + // Get temporary date-time if possible + DateTime? usableDate = null; + if (date && !String.IsNullOrWhiteSpace(roms[index].Date) && DateTime.TryParse(roms[index].Date.Replace('\\', '/'), out DateTime dt)) + { + usableDate = dt; + } + + // Copy the input stream to the output + tarFile.AddEntry(roms[index].Name, Utilities.TryOpenRead(inputFiles[index]), size: roms[index].Size, modified: usableDate); + } + } + + // Otherwise, sort the input files and write out in the correct order + else + { + // Open the old archive for reading + oldTarFile = TarArchive.Open(archiveFileName); + + // Get a list of all current entries + List entries = oldTarFile.Entries.Select(i => i.Key).ToList(); + + // Map all inputs to index + Dictionary inputIndexMap = new Dictionary(); + for (int i = 0; i < inputFiles.Count; i++) + { + // If the old one contains the new file, then just skip out + if (entries.Contains(roms[i].Name.Replace('\\', '/'))) + { + continue; + } + + inputIndexMap.Add(roms[i].Name.Replace('\\', '/'), -(i + 1)); + } + + // Then add all of the old entries to it too + for (int i = 0; i < entries.Count; i++) + { + inputIndexMap.Add(entries[i], i); + } + + // If the number of entries is the same as the old archive, skip out + if (inputIndexMap.Keys.Count <= entries.Count) + { + success = true; + return success; + } + + // Get the order for the entries with the new file + List keys = inputIndexMap.Keys.ToList(); + keys.Sort(ZipFile.TorrentZipStringCompare); + + // Copy over all files to the new archive + foreach (string key in keys) + { + // Get the index mapped to the key + int index = inputIndexMap[key]; + + // If we have the input file, add it now + if (index < 0) + { + // Get temporary date-time if possible + DateTime? usableDate = null; + if (date && !String.IsNullOrWhiteSpace(roms[-index - 1].Date) && DateTime.TryParse(roms[-index - 1].Date.Replace('\\', '/'), out DateTime dt)) + { + usableDate = dt; + } + + // Copy the input file to the output + tarFile.AddEntry(roms[-index - 1].Name, Utilities.TryOpenRead(inputFiles[-index - 1]), size: roms[-index - 1].Size, modified: usableDate); + } + + // Otherwise, copy the file from the old archive + else + { + // Get the stream from the original archive + TarArchiveEntry tae = oldTarFile.Entries.ElementAt(index); + MemoryStream entry = new MemoryStream(); + tae.OpenEntryStream().CopyTo(entry); + + // Copy the input stream to the output + tarFile.AddEntry(key, entry, size: tae.Size, modified: tae.LastModifiedTime); + } + } + } + + // Close the output tar file + tarFile.SaveTo(tempFile, new WriterOptions(CompressionType.None)); + + success = true; + } + catch (Exception ex) + { + Console.WriteLine(ex); + success = false; + } + finally + { + tarFile.Dispose(); + oldTarFile.Dispose(); + } + + // If the old file exists, delete it and replace + if (File.Exists(archiveFileName)) + { + Utilities.TryDeleteFile(archiveFileName); + } + File.Move(tempFile, archiveFileName); + + return true; + } + + #endregion + } } diff --git a/SabreTools.Library/FileTypes/XZArchive.cs b/SabreTools.Library/FileTypes/XZArchive.cs index 6c74ae8a..2055c670 100644 --- a/SabreTools.Library/FileTypes/XZArchive.cs +++ b/SabreTools.Library/FileTypes/XZArchive.cs @@ -26,634 +26,634 @@ using SharpCompress.Readers; namespace SabreTools.Library.FileTypes { - /// - /// Represents a TorrentXZ archive for reading and writing - /// - /// TODO: Wait for XZ write to be enabled by SevenZipSharp library - public class XZArchive : BaseArchive - { - #region Constructors - - /// - /// Create a new TorrentGZipArchive with no base file - /// - public XZArchive() - : base() - { - _fileType = FileType.XZArchive; - } - - /// - /// Create a new TorrentGZipArchive from the given file - /// - /// Name of the file to use as an archive - /// True for opening file as read, false for opening file as write - /// True if hashes for this file should be calculated, false otherwise (default) - public XZArchive(string filename, bool getHashes = false) - : base(filename, getHashes) - { - _fileType = FileType.XZArchive; - } - - #endregion - - #region Extraction - - /// - /// Attempt to extract a file as an archive - /// - /// Output directory for archive extraction - /// True if the extraction was a success, false otherwise - public override bool CopyAll(string outDir) - { - bool encounteredErrors = true; - - try - { - // Create the temp directory - Directory.CreateDirectory(outDir); - - // Extract all files to the temp directory - SharpCompress.Archives.SevenZip.SevenZipArchive sza = SharpCompress.Archives.SevenZip.SevenZipArchive.Open(Utilities.TryOpenRead(_filename)); - foreach (SevenZipArchiveEntry entry in sza.Entries) - { - entry.WriteToDirectory(outDir, new ExtractionOptions { PreserveFileTime = true, ExtractFullPath = true, Overwrite = true }); - } - encounteredErrors = false; - sza.Dispose(); - } - catch (EndOfStreamException) - { - // Catch this but don't count it as an error because SharpCompress is unsafe - } - catch (InvalidOperationException) - { - encounteredErrors = true; - } - catch (Exception ex) - { - Globals.Logger.Error(ex.ToString()); - encounteredErrors = true; - } - - return encounteredErrors; - } - - /// - /// Attempt to extract a file from an archive - /// - /// Name of the entry to be extracted - /// Output directory for archive extraction - /// Name of the extracted file, null on error - public override string CopyToFile(string entryName, string outDir) - { - // Try to extract a stream using the given information - (MemoryStream ms, string realEntry) = CopyToStream(entryName); - - // If the memory stream and the entry name are both non-null, we write to file - if (ms != null && realEntry != null) - { - realEntry = Path.Combine(outDir, realEntry); - - // Create the output subfolder now - Directory.CreateDirectory(Path.GetDirectoryName(realEntry)); - - // Now open and write the file if possible - FileStream fs = Utilities.TryCreate(realEntry); - if (fs != null) - { - ms.Seek(0, SeekOrigin.Begin); - byte[] zbuffer = new byte[_bufferSize]; - int zlen; - while ((zlen = ms.Read(zbuffer, 0, _bufferSize)) > 0) - { - fs.Write(zbuffer, 0, zlen); - fs.Flush(); - } - - ms?.Dispose(); - fs?.Dispose(); - } - else - { - ms?.Dispose(); - fs?.Dispose(); - realEntry = null; - } - } - - return realEntry; - } - - /// - /// Attempt to extract a stream from an archive - /// - /// Name of the entry to be extracted - /// Output representing the entry name that was found - /// MemoryStream representing the entry, null on error - public override (MemoryStream, string) CopyToStream(string entryName) - { - MemoryStream ms = new MemoryStream(); - string realEntry = null; - - try - { - SharpCompress.Archives.SevenZip.SevenZipArchive sza = SharpCompress.Archives.SevenZip.SevenZipArchive.Open(_filename, new ReaderOptions { LeaveStreamOpen = false, }); - foreach (SevenZipArchiveEntry entry in sza.Entries) - { - if (entry != null && !entry.IsDirectory && entry.Key.Contains(entryName)) - { - // Write the file out - realEntry = entry.Key; - entry.WriteTo(ms); - break; - } - } - sza.Dispose(); - } - catch (Exception ex) - { - Globals.Logger.Error(ex.ToString()); - ms = null; - realEntry = null; - } - - return (ms, realEntry); - } - - #endregion - - #region Information - - /// - /// Generate a list of DatItem objects from the header values in an archive - /// - /// Hash representing the hashes that should be skipped - /// True if entry dates should be included, false otherwise (default) - /// List of DatItem objects representing the found data - /// TODO: All instances of Hash.DeepHashes should be made into 0x0 eventually - public override List GetChildren(Hash omitFromScan = Hash.DeepHashes, bool date = false) - { - throw new NotImplementedException(); - } - - /// - /// Generate a list of empty folders in an archive - /// - /// Input file to get data from - /// List of empty folders in the archive - public override List GetEmptyFolders() - { - throw new NotImplementedException(); - } - - /// - /// Check whether the input file is a standardized format - /// - public override bool IsTorrent() - { - throw new NotImplementedException(); - } - - #endregion - - #region Writing - - /// - /// Write an input file to a torrent XZ file - /// - /// Input filename to be moved - /// Output directory to build to - /// DatItem representing the new information - /// True if the date from the DAT should be used if available, false otherwise (default) - /// True if files should be output in Romba depot folders, false otherwise - /// True if the write was a success, false otherwise - /// This works for now, but it can be sped up by using Ionic.Zip or another zlib wrapper that allows for header values built-in. See edc's code. - public override bool Write(string inputFile, string outDir, Rom rom, bool date = false, bool romba = false) - { - // Get the file stream for the file and write out - return Write(Utilities.TryOpenRead(inputFile), outDir, rom, date: date); - } - - /// - /// Write an input file to a torrent XZ archive - /// - /// Input stream to be moved - /// Output directory to build to - /// DatItem representing the new information - /// True if the date from the DAT should be used if available, false otherwise (default) - /// True if files should be output in Romba depot folders, false otherwise - /// True if the archive was written properly, false otherwise - public override bool Write(Stream inputStream, string outDir, Rom rom, bool date = false, bool romba = false) - { - bool success = false; - string tempFile = Path.Combine(outDir, "tmp" + Guid.NewGuid().ToString()); - - // If either input is null or empty, return - if (inputStream == null || rom == null || rom.Name == null) - { - return success; - } - - // If the stream is not readable, return - if (!inputStream.CanRead) - { - return success; - } - - // Seek to the beginning of the stream - inputStream.Seek(0, SeekOrigin.Begin); - - // Get the output archive name from the first rebuild rom - string archiveFileName = Path.Combine(outDir, Utilities.RemovePathUnsafeCharacters(rom.MachineName) + (rom.MachineName.EndsWith(".xz") ? "" : ".xz")); - - // Set internal variables - SevenZipBase.SetLibraryPath("7za.dll"); - SevenZipExtractor oldZipFile = null; - SevenZipCompressor zipFile; - - try - { - // If the full output path doesn't exist, create it - if (!Directory.Exists(Path.GetDirectoryName(tempFile))) - { - Directory.CreateDirectory(Path.GetDirectoryName(tempFile)); - } - - // If the archive doesn't exist, create it and put the single file - if (!File.Exists(archiveFileName)) - { - zipFile = new SevenZipCompressor() - { - ArchiveFormat = OutArchiveFormat.XZ, - CompressionLevel = CompressionLevel.Normal, - }; - - // Create the temp directory - string tempPath = Path.Combine(outDir, Guid.NewGuid().ToString()); - if (!Directory.Exists(tempPath)) - { - Directory.CreateDirectory(tempPath); - } - - // Create a stream dictionary - Dictionary dict = new Dictionary(); - dict.Add(rom.Name, inputStream); - - // Now add the stream - zipFile.CompressStreamDictionary(dict, tempFile); - } - - // Otherwise, sort the input files and write out in the correct order - else - { - // Open the old archive for reading - using (oldZipFile = new SevenZipExtractor(archiveFileName)) - { - // Map all inputs to index - Dictionary inputIndexMap = new Dictionary(); - - // If the old one doesn't contain the new file, then add it - if (!oldZipFile.ArchiveFileNames.Contains(rom.Name.Replace('\\', '/'))) - { - inputIndexMap.Add(rom.Name.Replace('\\', '/'), -1); - } - - // Then add all of the old entries to it too - for (int i = 0; i < oldZipFile.FilesCount; i++) - { - inputIndexMap.Add(oldZipFile.ArchiveFileNames[i], i); - } - - // If the number of entries is the same as the old archive, skip out - if (inputIndexMap.Keys.Count <= oldZipFile.FilesCount) - { - success = true; - return success; - } - - // Otherwise, process the old zipfile - zipFile = new SevenZipCompressor() - { - ArchiveFormat = OutArchiveFormat.XZ, - CompressionLevel = CompressionLevel.Normal, - }; - - // Get the order for the entries with the new file - List keys = inputIndexMap.Keys.ToList(); - keys.Sort(ZipFile.TorrentZipStringCompare); - - // Copy over all files to the new archive - foreach (string key in keys) - { - // Get the index mapped to the key - int index = inputIndexMap[key]; - - // If we have the input file, add it now - if (index < 0) - { - // Create a stream dictionary - Dictionary dict = new Dictionary(); - dict.Add(rom.Name, inputStream); - - // Now add the stream - zipFile.CompressStreamDictionary(dict, tempFile); - } - - // Otherwise, copy the file from the old archive - else - { - Stream oldZipFileEntryStream = new MemoryStream(); - oldZipFile.ExtractFile(index, oldZipFileEntryStream); - oldZipFileEntryStream.Seek(0, SeekOrigin.Begin); - - // Create a stream dictionary - Dictionary dict = new Dictionary(); - dict.Add(oldZipFile.ArchiveFileNames[index], oldZipFileEntryStream); - - // Now add the stream - zipFile.CompressStreamDictionary(dict, tempFile); - oldZipFileEntryStream.Dispose(); - } - - // After the first file, make sure we're in append mode - zipFile.CompressionMode = CompressionMode.Append; - } - } - } - - success = true; - } - catch (Exception ex) - { - Console.WriteLine(ex); - success = false; - } - finally - { - inputStream?.Dispose(); - } - - // If the old file exists, delete it and replace - if (File.Exists(archiveFileName)) - { - Utilities.TryDeleteFile(archiveFileName); - } - File.Move(tempFile, archiveFileName); - - // Now make the file T7Z - // TODO: Add ACTUAL T7Z compatible code - - BinaryWriter bw = new BinaryWriter(Utilities.TryOpenReadWrite(archiveFileName)); - bw.Seek(0, SeekOrigin.Begin); - bw.Write(Constants.Torrent7ZipHeader); - bw.Seek(0, SeekOrigin.End); - - using (oldZipFile = new SevenZipExtractor(Utilities.TryOpenReadWrite(archiveFileName))) - { - - // Get the correct signature to use (Default 0, Unicode 1, SingleFile 2, StripFileNames 4) - byte[] tempsig = Constants.Torrent7ZipSignature; - if (oldZipFile.FilesCount > 1) - { - tempsig[16] = 0x2; - } - else - { - tempsig[16] = 0; - } - - bw.Write(tempsig); - bw.Flush(); - bw.Dispose(); - } - - return true; - } - - /// - /// Write a set of input files to a torrent XZ archive (assuming the same output archive name) - /// - /// Input files to be moved - /// Output directory to build to - /// DatItem representing the new information - /// True if the date from the DAT should be used if available, false otherwise (default) - /// True if files should be output in Romba depot folders, false otherwise - /// True if the archive was written properly, false otherwise - public override bool Write(List inputFiles, string outDir, List roms, bool date = false, bool romba = false) - { - bool success = false; - string tempFile = Path.Combine(outDir, "tmp" + Guid.NewGuid().ToString()); - - // If either list of roms is null or empty, return - if (inputFiles == null || roms == null || inputFiles.Count == 0 || roms.Count == 0) - { - return success; - } - - // If the number of inputs is less than the number of available roms, return - if (inputFiles.Count < roms.Count) - { - return success; - } - - // If one of the files doesn't exist, return - foreach (string file in inputFiles) - { - if (!File.Exists(file)) - { - return success; - } - } - - // Get the output archive name from the first rebuild rom - string archiveFileName = Path.Combine(outDir, Utilities.RemovePathUnsafeCharacters(roms[0].MachineName) + (roms[0].MachineName.EndsWith(".xz") ? "" : ".xz")); - - // Set internal variables - SevenZipBase.SetLibraryPath("7za.dll"); - SevenZipExtractor oldZipFile; - SevenZipCompressor zipFile; - - try - { - // If the full output path doesn't exist, create it - if (!Directory.Exists(Path.GetDirectoryName(archiveFileName))) - { - Directory.CreateDirectory(Path.GetDirectoryName(archiveFileName)); - } - - // If the archive doesn't exist, create it and put the single file - if (!File.Exists(archiveFileName)) - { - zipFile = new SevenZipCompressor() - { - ArchiveFormat = OutArchiveFormat.XZ, - CompressionLevel = CompressionLevel.Normal, - }; - - // Map all inputs to index - Dictionary inputIndexMap = new Dictionary(); - for (int i = 0; i < inputFiles.Count; i++) - { - inputIndexMap.Add(roms[i].Name.Replace('\\', '/'), i); - } - - // Sort the keys in TZIP order - List keys = inputIndexMap.Keys.ToList(); - keys.Sort(ZipFile.TorrentZipStringCompare); - - // Create the temp directory - string tempPath = Path.Combine(outDir, Guid.NewGuid().ToString()); - if (!Directory.Exists(tempPath)) - { - Directory.CreateDirectory(tempPath); - } - - // Now add all of the files in order - foreach (string key in keys) - { - string newkey = Path.Combine(tempPath, key); - - File.Move(inputFiles[inputIndexMap[key]], newkey); - zipFile.CompressFiles(tempFile, newkey); - File.Move(newkey, inputFiles[inputIndexMap[key]]); - - // After the first file, make sure we're in append mode - zipFile.CompressionMode = CompressionMode.Append; - } - - Utilities.CleanDirectory(tempPath); - Utilities.TryDeleteDirectory(tempPath); - } - - // Otherwise, sort the input files and write out in the correct order - else - { - // Open the old archive for reading - using (oldZipFile = new SevenZipExtractor(archiveFileName)) - { - // Map all inputs to index - Dictionary inputIndexMap = new Dictionary(); - for (int i = 0; i < inputFiles.Count; i++) - { - // If the old one contains the new file, then just skip out - if (oldZipFile.ArchiveFileNames.Contains(roms[i].Name.Replace('\\', '/'))) - { - continue; - } - - inputIndexMap.Add(roms[i].Name.Replace('\\', '/'), -(i + 1)); - } - - // Then add all of the old entries to it too - for (int i = 0; i < oldZipFile.FilesCount; i++) - { - inputIndexMap.Add(oldZipFile.ArchiveFileNames[i], i); - } - - // If the number of entries is the same as the old archive, skip out - if (inputIndexMap.Keys.Count <= oldZipFile.FilesCount) - { - success = true; - return success; - } - - // Otherwise, process the old zipfile - zipFile = new SevenZipCompressor() - { - ArchiveFormat = OutArchiveFormat.XZ, - CompressionLevel = CompressionLevel.Normal, - }; - - // Get the order for the entries with the new file - List keys = inputIndexMap.Keys.ToList(); - keys.Sort(ZipFile.TorrentZipStringCompare); - - // Copy over all files to the new archive - foreach (string key in keys) - { - // Get the index mapped to the key - int index = inputIndexMap[key]; - - // If we have the input file, add it now - if (index < 0) - { - FileStream inputStream = Utilities.TryOpenRead(inputFiles[-index - 1]); - - // Create a stream dictionary - Dictionary dict = new Dictionary(); - dict.Add(key, inputStream); - - // Now add the stream - zipFile.CompressStreamDictionary(dict, tempFile); - } - - // Otherwise, copy the file from the old archive - else - { - Stream oldZipFileEntryStream = new MemoryStream(); - oldZipFile.ExtractFile(index, oldZipFileEntryStream); - oldZipFileEntryStream.Seek(0, SeekOrigin.Begin); - - // Create a stream dictionary - Dictionary dict = new Dictionary(); - dict.Add(oldZipFile.ArchiveFileNames[index], oldZipFileEntryStream); - - // Now add the stream - zipFile.CompressStreamDictionary(dict, tempFile); - oldZipFileEntryStream.Dispose(); - } - - // After the first file, make sure we're in append mode - zipFile.CompressionMode = CompressionMode.Append; - } - } - } - - success = true; - } - catch (Exception ex) - { - Console.WriteLine(ex); - success = false; - } - - // If the old file exists, delete it and replace - if (File.Exists(archiveFileName)) - { - Utilities.TryDeleteFile(archiveFileName); - } - File.Move(tempFile, archiveFileName); - - // Now make the file T7Z - // TODO: Add ACTUAL T7Z compatible code - - BinaryWriter bw = new BinaryWriter(Utilities.TryOpenReadWrite(archiveFileName)); - bw.Seek(0, SeekOrigin.Begin); - bw.Write(Constants.Torrent7ZipHeader); - bw.Seek(0, SeekOrigin.End); - - using (oldZipFile = new SevenZipExtractor(Utilities.TryOpenReadWrite(archiveFileName))) - { - // Get the correct signature to use (Default 0, Unicode 1, SingleFile 2, StripFileNames 4) - byte[] tempsig = Constants.Torrent7ZipSignature; - if (oldZipFile.FilesCount > 1) - { - tempsig[16] = 0x2; - } - else - { - tempsig[16] = 0; - } - - bw.Write(tempsig); - bw.Flush(); - bw.Dispose(); - } - - return true; - } - - #endregion - } + /// + /// Represents a TorrentXZ archive for reading and writing + /// + /// TODO: Wait for XZ write to be enabled by SevenZipSharp library + public class XZArchive : BaseArchive + { + #region Constructors + + /// + /// Create a new TorrentGZipArchive with no base file + /// + public XZArchive() + : base() + { + this.Type = FileType.XZArchive; + } + + /// + /// Create a new TorrentGZipArchive from the given file + /// + /// Name of the file to use as an archive + /// True for opening file as read, false for opening file as write + /// True if hashes for this file should be calculated, false otherwise (default) + public XZArchive(string filename, bool getHashes = false) + : base(filename, getHashes) + { + this.Type = FileType.XZArchive; + } + + #endregion + + #region Extraction + + /// + /// Attempt to extract a file as an archive + /// + /// Output directory for archive extraction + /// True if the extraction was a success, false otherwise + public override bool CopyAll(string outDir) + { + bool encounteredErrors = true; + + try + { + // Create the temp directory + Directory.CreateDirectory(outDir); + + // Extract all files to the temp directory + SharpCompress.Archives.SevenZip.SevenZipArchive sza = SharpCompress.Archives.SevenZip.SevenZipArchive.Open(Utilities.TryOpenRead(this.Filename)); + foreach (SevenZipArchiveEntry entry in sza.Entries) + { + entry.WriteToDirectory(outDir, new ExtractionOptions { PreserveFileTime = true, ExtractFullPath = true, Overwrite = true }); + } + encounteredErrors = false; + sza.Dispose(); + } + catch (EndOfStreamException) + { + // Catch this but don't count it as an error because SharpCompress is unsafe + } + catch (InvalidOperationException) + { + encounteredErrors = true; + } + catch (Exception ex) + { + Globals.Logger.Error(ex.ToString()); + encounteredErrors = true; + } + + return encounteredErrors; + } + + /// + /// Attempt to extract a file from an archive + /// + /// Name of the entry to be extracted + /// Output directory for archive extraction + /// Name of the extracted file, null on error + public override string CopyToFile(string entryName, string outDir) + { + // Try to extract a stream using the given information + (MemoryStream ms, string realEntry) = CopyToStream(entryName); + + // If the memory stream and the entry name are both non-null, we write to file + if (ms != null && realEntry != null) + { + realEntry = Path.Combine(outDir, realEntry); + + // Create the output subfolder now + Directory.CreateDirectory(Path.GetDirectoryName(realEntry)); + + // Now open and write the file if possible + FileStream fs = Utilities.TryCreate(realEntry); + if (fs != null) + { + ms.Seek(0, SeekOrigin.Begin); + byte[] zbuffer = new byte[_bufferSize]; + int zlen; + while ((zlen = ms.Read(zbuffer, 0, _bufferSize)) > 0) + { + fs.Write(zbuffer, 0, zlen); + fs.Flush(); + } + + ms?.Dispose(); + fs?.Dispose(); + } + else + { + ms?.Dispose(); + fs?.Dispose(); + realEntry = null; + } + } + + return realEntry; + } + + /// + /// Attempt to extract a stream from an archive + /// + /// Name of the entry to be extracted + /// Output representing the entry name that was found + /// MemoryStream representing the entry, null on error + public override (MemoryStream, string) CopyToStream(string entryName) + { + MemoryStream ms = new MemoryStream(); + string realEntry = null; + + try + { + SharpCompress.Archives.SevenZip.SevenZipArchive sza = SharpCompress.Archives.SevenZip.SevenZipArchive.Open(this.Filename, new ReaderOptions { LeaveStreamOpen = false, }); + foreach (SevenZipArchiveEntry entry in sza.Entries) + { + if (entry != null && !entry.IsDirectory && entry.Key.Contains(entryName)) + { + // Write the file out + realEntry = entry.Key; + entry.WriteTo(ms); + break; + } + } + sza.Dispose(); + } + catch (Exception ex) + { + Globals.Logger.Error(ex.ToString()); + ms = null; + realEntry = null; + } + + return (ms, realEntry); + } + + #endregion + + #region Information + + /// + /// Generate a list of DatItem objects from the header values in an archive + /// + /// Hash representing the hashes that should be skipped + /// True if entry dates should be included, false otherwise (default) + /// List of DatItem objects representing the found data + /// TODO: All instances of Hash.DeepHashes should be made into 0x0 eventually + public override List GetChildren(Hash omitFromScan = Hash.DeepHashes, bool date = false) + { + throw new NotImplementedException(); + } + + /// + /// Generate a list of empty folders in an archive + /// + /// Input file to get data from + /// List of empty folders in the archive + public override List GetEmptyFolders() + { + throw new NotImplementedException(); + } + + /// + /// Check whether the input file is a standardized format + /// + public override bool IsTorrent() + { + throw new NotImplementedException(); + } + + #endregion + + #region Writing + + /// + /// Write an input file to a torrent XZ file + /// + /// Input filename to be moved + /// Output directory to build to + /// DatItem representing the new information + /// True if the date from the DAT should be used if available, false otherwise (default) + /// True if files should be output in Romba depot folders, false otherwise + /// True if the write was a success, false otherwise + /// This works for now, but it can be sped up by using Ionic.Zip or another zlib wrapper that allows for header values built-in. See edc's code. + public override bool Write(string inputFile, string outDir, Rom rom, bool date = false, bool romba = false) + { + // Get the file stream for the file and write out + return Write(Utilities.TryOpenRead(inputFile), outDir, rom, date: date); + } + + /// + /// Write an input file to a torrent XZ archive + /// + /// Input stream to be moved + /// Output directory to build to + /// DatItem representing the new information + /// True if the date from the DAT should be used if available, false otherwise (default) + /// True if files should be output in Romba depot folders, false otherwise + /// True if the archive was written properly, false otherwise + public override bool Write(Stream inputStream, string outDir, Rom rom, bool date = false, bool romba = false) + { + bool success = false; + string tempFile = Path.Combine(outDir, "tmp" + Guid.NewGuid().ToString()); + + // If either input is null or empty, return + if (inputStream == null || rom == null || rom.Name == null) + { + return success; + } + + // If the stream is not readable, return + if (!inputStream.CanRead) + { + return success; + } + + // Seek to the beginning of the stream + inputStream.Seek(0, SeekOrigin.Begin); + + // Get the output archive name from the first rebuild rom + string archiveFileName = Path.Combine(outDir, Utilities.RemovePathUnsafeCharacters(rom.MachineName) + (rom.MachineName.EndsWith(".xz") ? "" : ".xz")); + + // Set internal variables + SevenZipBase.SetLibraryPath("7za.dll"); + SevenZipExtractor oldZipFile = null; + SevenZipCompressor zipFile; + + try + { + // If the full output path doesn't exist, create it + if (!Directory.Exists(Path.GetDirectoryName(tempFile))) + { + Directory.CreateDirectory(Path.GetDirectoryName(tempFile)); + } + + // If the archive doesn't exist, create it and put the single file + if (!File.Exists(archiveFileName)) + { + zipFile = new SevenZipCompressor() + { + ArchiveFormat = OutArchiveFormat.XZ, + CompressionLevel = CompressionLevel.Normal, + }; + + // Create the temp directory + string tempPath = Path.Combine(outDir, Guid.NewGuid().ToString()); + if (!Directory.Exists(tempPath)) + { + Directory.CreateDirectory(tempPath); + } + + // Create a stream dictionary + Dictionary dict = new Dictionary(); + dict.Add(rom.Name, inputStream); + + // Now add the stream + zipFile.CompressStreamDictionary(dict, tempFile); + } + + // Otherwise, sort the input files and write out in the correct order + else + { + // Open the old archive for reading + using (oldZipFile = new SevenZipExtractor(archiveFileName)) + { + // Map all inputs to index + Dictionary inputIndexMap = new Dictionary(); + + // If the old one doesn't contain the new file, then add it + if (!oldZipFile.ArchiveFileNames.Contains(rom.Name.Replace('\\', '/'))) + { + inputIndexMap.Add(rom.Name.Replace('\\', '/'), -1); + } + + // Then add all of the old entries to it too + for (int i = 0; i < oldZipFile.FilesCount; i++) + { + inputIndexMap.Add(oldZipFile.ArchiveFileNames[i], i); + } + + // If the number of entries is the same as the old archive, skip out + if (inputIndexMap.Keys.Count <= oldZipFile.FilesCount) + { + success = true; + return success; + } + + // Otherwise, process the old zipfile + zipFile = new SevenZipCompressor() + { + ArchiveFormat = OutArchiveFormat.XZ, + CompressionLevel = CompressionLevel.Normal, + }; + + // Get the order for the entries with the new file + List keys = inputIndexMap.Keys.ToList(); + keys.Sort(ZipFile.TorrentZipStringCompare); + + // Copy over all files to the new archive + foreach (string key in keys) + { + // Get the index mapped to the key + int index = inputIndexMap[key]; + + // If we have the input file, add it now + if (index < 0) + { + // Create a stream dictionary + Dictionary dict = new Dictionary(); + dict.Add(rom.Name, inputStream); + + // Now add the stream + zipFile.CompressStreamDictionary(dict, tempFile); + } + + // Otherwise, copy the file from the old archive + else + { + Stream oldZipFileEntryStream = new MemoryStream(); + oldZipFile.ExtractFile(index, oldZipFileEntryStream); + oldZipFileEntryStream.Seek(0, SeekOrigin.Begin); + + // Create a stream dictionary + Dictionary dict = new Dictionary(); + dict.Add(oldZipFile.ArchiveFileNames[index], oldZipFileEntryStream); + + // Now add the stream + zipFile.CompressStreamDictionary(dict, tempFile); + oldZipFileEntryStream.Dispose(); + } + + // After the first file, make sure we're in append mode + zipFile.CompressionMode = CompressionMode.Append; + } + } + } + + success = true; + } + catch (Exception ex) + { + Console.WriteLine(ex); + success = false; + } + finally + { + inputStream?.Dispose(); + } + + // If the old file exists, delete it and replace + if (File.Exists(archiveFileName)) + { + Utilities.TryDeleteFile(archiveFileName); + } + File.Move(tempFile, archiveFileName); + + // Now make the file T7Z + // TODO: Add ACTUAL T7Z compatible code + + BinaryWriter bw = new BinaryWriter(Utilities.TryOpenReadWrite(archiveFileName)); + bw.Seek(0, SeekOrigin.Begin); + bw.Write(Constants.Torrent7ZipHeader); + bw.Seek(0, SeekOrigin.End); + + using (oldZipFile = new SevenZipExtractor(Utilities.TryOpenReadWrite(archiveFileName))) + { + + // Get the correct signature to use (Default 0, Unicode 1, SingleFile 2, StripFileNames 4) + byte[] tempsig = Constants.Torrent7ZipSignature; + if (oldZipFile.FilesCount > 1) + { + tempsig[16] = 0x2; + } + else + { + tempsig[16] = 0; + } + + bw.Write(tempsig); + bw.Flush(); + bw.Dispose(); + } + + return true; + } + + /// + /// Write a set of input files to a torrent XZ archive (assuming the same output archive name) + /// + /// Input files to be moved + /// Output directory to build to + /// DatItem representing the new information + /// True if the date from the DAT should be used if available, false otherwise (default) + /// True if files should be output in Romba depot folders, false otherwise + /// True if the archive was written properly, false otherwise + public override bool Write(List inputFiles, string outDir, List roms, bool date = false, bool romba = false) + { + bool success = false; + string tempFile = Path.Combine(outDir, "tmp" + Guid.NewGuid().ToString()); + + // If either list of roms is null or empty, return + if (inputFiles == null || roms == null || inputFiles.Count == 0 || roms.Count == 0) + { + return success; + } + + // If the number of inputs is less than the number of available roms, return + if (inputFiles.Count < roms.Count) + { + return success; + } + + // If one of the files doesn't exist, return + foreach (string file in inputFiles) + { + if (!File.Exists(file)) + { + return success; + } + } + + // Get the output archive name from the first rebuild rom + string archiveFileName = Path.Combine(outDir, Utilities.RemovePathUnsafeCharacters(roms[0].MachineName) + (roms[0].MachineName.EndsWith(".xz") ? "" : ".xz")); + + // Set internal variables + SevenZipBase.SetLibraryPath("7za.dll"); + SevenZipExtractor oldZipFile; + SevenZipCompressor zipFile; + + try + { + // If the full output path doesn't exist, create it + if (!Directory.Exists(Path.GetDirectoryName(archiveFileName))) + { + Directory.CreateDirectory(Path.GetDirectoryName(archiveFileName)); + } + + // If the archive doesn't exist, create it and put the single file + if (!File.Exists(archiveFileName)) + { + zipFile = new SevenZipCompressor() + { + ArchiveFormat = OutArchiveFormat.XZ, + CompressionLevel = CompressionLevel.Normal, + }; + + // Map all inputs to index + Dictionary inputIndexMap = new Dictionary(); + for (int i = 0; i < inputFiles.Count; i++) + { + inputIndexMap.Add(roms[i].Name.Replace('\\', '/'), i); + } + + // Sort the keys in TZIP order + List keys = inputIndexMap.Keys.ToList(); + keys.Sort(ZipFile.TorrentZipStringCompare); + + // Create the temp directory + string tempPath = Path.Combine(outDir, Guid.NewGuid().ToString()); + if (!Directory.Exists(tempPath)) + { + Directory.CreateDirectory(tempPath); + } + + // Now add all of the files in order + foreach (string key in keys) + { + string newkey = Path.Combine(tempPath, key); + + File.Move(inputFiles[inputIndexMap[key]], newkey); + zipFile.CompressFiles(tempFile, newkey); + File.Move(newkey, inputFiles[inputIndexMap[key]]); + + // After the first file, make sure we're in append mode + zipFile.CompressionMode = CompressionMode.Append; + } + + Utilities.CleanDirectory(tempPath); + Utilities.TryDeleteDirectory(tempPath); + } + + // Otherwise, sort the input files and write out in the correct order + else + { + // Open the old archive for reading + using (oldZipFile = new SevenZipExtractor(archiveFileName)) + { + // Map all inputs to index + Dictionary inputIndexMap = new Dictionary(); + for (int i = 0; i < inputFiles.Count; i++) + { + // If the old one contains the new file, then just skip out + if (oldZipFile.ArchiveFileNames.Contains(roms[i].Name.Replace('\\', '/'))) + { + continue; + } + + inputIndexMap.Add(roms[i].Name.Replace('\\', '/'), -(i + 1)); + } + + // Then add all of the old entries to it too + for (int i = 0; i < oldZipFile.FilesCount; i++) + { + inputIndexMap.Add(oldZipFile.ArchiveFileNames[i], i); + } + + // If the number of entries is the same as the old archive, skip out + if (inputIndexMap.Keys.Count <= oldZipFile.FilesCount) + { + success = true; + return success; + } + + // Otherwise, process the old zipfile + zipFile = new SevenZipCompressor() + { + ArchiveFormat = OutArchiveFormat.XZ, + CompressionLevel = CompressionLevel.Normal, + }; + + // Get the order for the entries with the new file + List keys = inputIndexMap.Keys.ToList(); + keys.Sort(ZipFile.TorrentZipStringCompare); + + // Copy over all files to the new archive + foreach (string key in keys) + { + // Get the index mapped to the key + int index = inputIndexMap[key]; + + // If we have the input file, add it now + if (index < 0) + { + FileStream inputStream = Utilities.TryOpenRead(inputFiles[-index - 1]); + + // Create a stream dictionary + Dictionary dict = new Dictionary(); + dict.Add(key, inputStream); + + // Now add the stream + zipFile.CompressStreamDictionary(dict, tempFile); + } + + // Otherwise, copy the file from the old archive + else + { + Stream oldZipFileEntryStream = new MemoryStream(); + oldZipFile.ExtractFile(index, oldZipFileEntryStream); + oldZipFileEntryStream.Seek(0, SeekOrigin.Begin); + + // Create a stream dictionary + Dictionary dict = new Dictionary(); + dict.Add(oldZipFile.ArchiveFileNames[index], oldZipFileEntryStream); + + // Now add the stream + zipFile.CompressStreamDictionary(dict, tempFile); + oldZipFileEntryStream.Dispose(); + } + + // After the first file, make sure we're in append mode + zipFile.CompressionMode = CompressionMode.Append; + } + } + } + + success = true; + } + catch (Exception ex) + { + Console.WriteLine(ex); + success = false; + } + + // If the old file exists, delete it and replace + if (File.Exists(archiveFileName)) + { + Utilities.TryDeleteFile(archiveFileName); + } + File.Move(tempFile, archiveFileName); + + // Now make the file T7Z + // TODO: Add ACTUAL T7Z compatible code + + BinaryWriter bw = new BinaryWriter(Utilities.TryOpenReadWrite(archiveFileName)); + bw.Seek(0, SeekOrigin.Begin); + bw.Write(Constants.Torrent7ZipHeader); + bw.Seek(0, SeekOrigin.End); + + using (oldZipFile = new SevenZipExtractor(Utilities.TryOpenReadWrite(archiveFileName))) + { + // Get the correct signature to use (Default 0, Unicode 1, SingleFile 2, StripFileNames 4) + byte[] tempsig = Constants.Torrent7ZipSignature; + if (oldZipFile.FilesCount > 1) + { + tempsig[16] = 0x2; + } + else + { + tempsig[16] = 0; + } + + bw.Write(tempsig); + bw.Flush(); + bw.Dispose(); + } + + return true; + } + + #endregion + } } diff --git a/SabreTools.Library/FileTypes/ZPAQArchive.cs b/SabreTools.Library/FileTypes/ZPAQArchive.cs index 6e34f189..03d72ace 100644 --- a/SabreTools.Library/FileTypes/ZPAQArchive.cs +++ b/SabreTools.Library/FileTypes/ZPAQArchive.cs @@ -13,152 +13,152 @@ using Stream = System.IO.Stream; namespace SabreTools.Library.FileTypes { - /// - /// Represents a ZPAQArchive archive for reading and writing - /// - /// TODO: Implement from source at https://github.com/zpaq/zpaq - In progress as external DLL - public class ZPAQArchive : BaseArchive - { - #region Constructors + /// + /// Represents a ZPAQArchive archive for reading and writing + /// + /// TODO: Implement from source at https://github.com/zpaq/zpaq - In progress as external DLL + public class ZPAQArchive : BaseArchive + { + #region Constructors - /// - /// Create a new ZPAQArchive with no base file - /// - public ZPAQArchive() - : base() - { - _fileType = FileType.ZPAQArchive; - } + /// + /// Create a new ZPAQArchive with no base file + /// + public ZPAQArchive() + : base() + { + this.Type = FileType.ZPAQArchive; + } - /// - /// Create a new ZPAQArchive from the given file - /// - /// Name of the file to use as an archive - /// True if hashes for this file should be calculated, false otherwise (default) - public ZPAQArchive(string filename, bool getHashes = false) - : base(filename, getHashes) - { - _fileType = FileType.ZPAQArchive; - } + /// + /// Create a new ZPAQArchive from the given file + /// + /// Name of the file to use as an archive + /// True if hashes for this file should be calculated, false otherwise (default) + public ZPAQArchive(string filename, bool getHashes = false) + : base(filename, getHashes) + { + this.Type = FileType.ZPAQArchive; + } - #endregion + #endregion - #region Extraction + #region Extraction - /// - /// Attempt to extract a file as an archive - /// - /// Output directory for archive extraction - /// True if the extraction was a success, false otherwise - public override bool CopyAll(string outDir) - { - throw new NotImplementedException(); - } + /// + /// Attempt to extract a file as an archive + /// + /// Output directory for archive extraction + /// True if the extraction was a success, false otherwise + public override bool CopyAll(string outDir) + { + throw new NotImplementedException(); + } - /// - /// Attempt to extract a file from an archive - /// - /// Name of the entry to be extracted - /// Output directory for archive extraction - /// Name of the extracted file, null on error - public override string CopyToFile(string entryName, string outDir) - { - throw new NotImplementedException(); - } + /// + /// Attempt to extract a file from an archive + /// + /// Name of the entry to be extracted + /// Output directory for archive extraction + /// Name of the extracted file, null on error + public override string CopyToFile(string entryName, string outDir) + { + throw new NotImplementedException(); + } - /// - /// Attempt to extract a stream from an archive - /// - /// Name of the entry to be extracted - /// Output representing the entry name that was found - /// MemoryStream representing the entry, null on error - public override (MemoryStream, string) CopyToStream(string entryName) - { - throw new NotImplementedException(); - } + /// + /// Attempt to extract a stream from an archive + /// + /// Name of the entry to be extracted + /// Output representing the entry name that was found + /// MemoryStream representing the entry, null on error + public override (MemoryStream, string) CopyToStream(string entryName) + { + throw new NotImplementedException(); + } - #endregion + #endregion - #region Information + #region Information - /// - /// Generate a list of DatItem objects from the header values in an archive - /// - /// Hash representing the hashes that should be skipped - /// True if entry dates should be included, false otherwise (default) - /// List of DatItem objects representing the found data - /// TODO: All instances of Hash.DeepHashes should be made into 0x0 eventually - public override List GetChildren(Hash omitFromScan = Hash.DeepHashes, bool date = false) - { - throw new NotImplementedException(); - } + /// + /// Generate a list of DatItem objects from the header values in an archive + /// + /// Hash representing the hashes that should be skipped + /// True if entry dates should be included, false otherwise (default) + /// List of DatItem objects representing the found data + /// TODO: All instances of Hash.DeepHashes should be made into 0x0 eventually + public override List GetChildren(Hash omitFromScan = Hash.DeepHashes, bool date = false) + { + throw new NotImplementedException(); + } - /// - /// Generate a list of empty folders in an archive - /// - /// Input file to get data from - /// List of empty folders in the archive - public override List GetEmptyFolders() - { - throw new NotImplementedException(); - } + /// + /// Generate a list of empty folders in an archive + /// + /// Input file to get data from + /// List of empty folders in the archive + public override List GetEmptyFolders() + { + throw new NotImplementedException(); + } - /// - /// Check whether the input file is a standardized format - /// - public override bool IsTorrent() - { - throw new NotImplementedException(); - } + /// + /// Check whether the input file is a standardized format + /// + public override bool IsTorrent() + { + throw new NotImplementedException(); + } - #endregion + #endregion - #region Writing + #region Writing - /// - /// Write an input file to a torrent ZPAQ file - /// - /// Input filename to be moved - /// Output directory to build to - /// DatItem representing the new information - /// True if the date from the DAT should be used if available, false otherwise (default) - /// True if files should be output in Romba depot folders, false otherwise - /// True if the write was a success, false otherwise - /// This works for now, but it can be sped up by using Ionic.Zip or another zlib wrapper that allows for header values built-in. See edc's code. - public override bool Write(string inputFile, string outDir, Rom rom, bool date = false, bool romba = false) - { - throw new NotImplementedException(); - } + /// + /// Write an input file to a torrent ZPAQ file + /// + /// Input filename to be moved + /// Output directory to build to + /// DatItem representing the new information + /// True if the date from the DAT should be used if available, false otherwise (default) + /// True if files should be output in Romba depot folders, false otherwise + /// True if the write was a success, false otherwise + /// This works for now, but it can be sped up by using Ionic.Zip or another zlib wrapper that allows for header values built-in. See edc's code. + public override bool Write(string inputFile, string outDir, Rom rom, bool date = false, bool romba = false) + { + throw new NotImplementedException(); + } - /// - /// Write an input stream to a torrent ZPAQ file - /// - /// Input stream to be moved - /// Output directory to build to - /// DatItem representing the new information - /// True if the date from the DAT should be used if available, false otherwise (default) - /// True if files should be output in Romba depot folders, false otherwise - /// True if the write was a success, false otherwise - /// This works for now, but it can be sped up by using Ionic.Zip or another zlib wrapper that allows for header values built-in. See edc's code. - public override bool Write(Stream inputStream, string outDir, Rom rom, bool date = false, bool romba = false) - { - throw new NotImplementedException(); - } + /// + /// Write an input stream to a torrent ZPAQ file + /// + /// Input stream to be moved + /// Output directory to build to + /// DatItem representing the new information + /// True if the date from the DAT should be used if available, false otherwise (default) + /// True if files should be output in Romba depot folders, false otherwise + /// True if the write was a success, false otherwise + /// This works for now, but it can be sped up by using Ionic.Zip or another zlib wrapper that allows for header values built-in. See edc's code. + public override bool Write(Stream inputStream, string outDir, Rom rom, bool date = false, bool romba = false) + { + throw new NotImplementedException(); + } - /// - /// Write a set of input files to a torrent ZPAQ archive (assuming the same output archive name) - /// - /// Input files to be moved - /// Output directory to build to - /// DatItem representing the new information - /// True if the date from the DAT should be used if available, false otherwise (default) - /// True if files should be output in Romba depot folders, false otherwise - /// True if the archive was written properly, false otherwise - public override bool Write(List inputFiles, string outDir, List roms, bool date = false, bool romba = false) - { - throw new NotImplementedException(); - } + /// + /// Write a set of input files to a torrent ZPAQ archive (assuming the same output archive name) + /// + /// Input files to be moved + /// Output directory to build to + /// DatItem representing the new information + /// True if the date from the DAT should be used if available, false otherwise (default) + /// True if files should be output in Romba depot folders, false otherwise + /// True if the archive was written properly, false otherwise + public override bool Write(List inputFiles, string outDir, List roms, bool date = false, bool romba = false) + { + throw new NotImplementedException(); + } - #endregion - } + #endregion + } } diff --git a/SabreTools.Library/FileTypes/ZipArchive.cs b/SabreTools.Library/FileTypes/ZipArchive.cs index 60ef3be0..699ec232 100644 --- a/SabreTools.Library/FileTypes/ZipArchive.cs +++ b/SabreTools.Library/FileTypes/ZipArchive.cs @@ -22,823 +22,823 @@ using SharpCompress.Readers; namespace SabreTools.Library.FileTypes { - /// - /// Represents a Zip archive for reading and writing - /// - public class ZipArchive : BaseArchive - { - #region Constructors - - /// - /// Create a new TorrentZipArchive with no base file - /// - public ZipArchive() - : base() - { - _fileType = FileType.ZipArchive; - } - - /// - /// Create a new TorrentZipArchive from the given file - /// - /// Name of the file to use as an archive - /// True for opening file as read, false for opening file as write - /// True if hashes for this file should be calculated, false otherwise (default) - public ZipArchive(string filename, bool getHashes = false) - : base(filename, getHashes) - { - _fileType = FileType.ZipArchive; - } - - #endregion - - #region Extraction - - /// - /// Attempt to extract a file as an archive - /// - /// Output directory for archive extraction - /// True if the extraction was a success, false otherwise - public override bool CopyAll(string outDir) - { - bool encounteredErrors = true; - - try - { - // Create the temp directory - Directory.CreateDirectory(outDir); - - // Extract all files to the temp directory - ZipFile zf = new ZipFile(); - ZipReturn zr = zf.Open(_filename, new FileInfo(_filename).LastWriteTime.Ticks, true); - if (zr != ZipReturn.ZipGood) - { - throw new Exception(ZipFile.ZipErrorMessageText(zr)); - } - - for (int i = 0; i < zf.EntriesCount && zr == ZipReturn.ZipGood; i++) - { - // Open the read stream - zr = zf.OpenReadStream(i, false, out Stream readStream, out ulong streamsize, out SabreTools.Library.Data.CompressionMethod cm, out uint lastMod); - - // Create the rest of the path, if needed - if (!String.IsNullOrWhiteSpace(Path.GetDirectoryName(zf.Entries[i].FileName))) - { - Directory.CreateDirectory(Path.Combine(outDir, Path.GetDirectoryName(zf.Entries[i].FileName))); - } - - // If the entry ends with a directory separator, continue to the next item, if any - if (zf.Entries[i].FileName.EndsWith(Path.DirectorySeparatorChar.ToString()) - || zf.Entries[i].FileName.EndsWith(Path.AltDirectorySeparatorChar.ToString()) - || zf.Entries[i].FileName.EndsWith(Path.PathSeparator.ToString())) - { - continue; - } - - FileStream writeStream = Utilities.TryCreate(Path.Combine(outDir, zf.Entries[i].FileName)); - - // If the stream is smaller than the buffer, just run one loop through to avoid issues - if (streamsize < _bufferSize) - { - byte[] ibuffer = new byte[streamsize]; - int ilen = readStream.Read(ibuffer, 0, (int)streamsize); - writeStream.Write(ibuffer, 0, ilen); - writeStream.Flush(); - } - // Otherwise, we do the normal loop - else - { - int realBufferSize = (streamsize < _bufferSize ? (int)streamsize : _bufferSize); - byte[] ibuffer = new byte[realBufferSize]; - int ilen; - while ((ilen = readStream.Read(ibuffer, 0, realBufferSize)) > 0) - { - writeStream.Write(ibuffer, 0, ilen); - writeStream.Flush(); - } - } - - zr = zf.CloseReadStream(); - writeStream.Dispose(); - } - zf.Close(); - encounteredErrors = false; - } - catch (EndOfStreamException) - { - // Catch this but don't count it as an error because SharpCompress is unsafe - } - catch (InvalidOperationException) - { - encounteredErrors = true; - } - catch (Exception ex) - { - Globals.Logger.Error(ex.ToString()); - encounteredErrors = true; - } - - return encounteredErrors; - } - - /// - /// Attempt to extract a file from an archive - /// - /// Name of the entry to be extracted - /// Output directory for archive extraction - /// Name of the extracted file, null on error - public override string CopyToFile(string entryName, string outDir) - { - // Try to extract a stream using the given information - (MemoryStream ms, string realEntry) = CopyToStream(entryName); - - // If the memory stream and the entry name are both non-null, we write to file - if (ms != null && realEntry != null) - { - realEntry = Path.Combine(outDir, realEntry); - - // Create the output subfolder now - Directory.CreateDirectory(Path.GetDirectoryName(realEntry)); - - // Now open and write the file if possible - FileStream fs = Utilities.TryCreate(realEntry); - if (fs != null) - { - ms.Seek(0, SeekOrigin.Begin); - byte[] zbuffer = new byte[_bufferSize]; - int zlen; - while ((zlen = ms.Read(zbuffer, 0, _bufferSize)) > 0) - { - fs.Write(zbuffer, 0, zlen); - fs.Flush(); - } - - ms?.Dispose(); - fs?.Dispose(); - } - else - { - ms?.Dispose(); - fs?.Dispose(); - realEntry = null; - } - } - - return realEntry; - } - - /// - /// Attempt to extract a stream from an archive - /// - /// Name of the entry to be extracted - /// Output representing the entry name that was found - /// MemoryStream representing the entry, null on error - public override (MemoryStream, string) CopyToStream(string entryName) - { - MemoryStream ms = new MemoryStream(); - string realEntry = null; - - try - { - ZipFile zf = new ZipFile(); - ZipReturn zr = zf.Open(_filename, new FileInfo(_filename).LastWriteTime.Ticks, true); - if (zr != ZipReturn.ZipGood) - { - throw new Exception(ZipFile.ZipErrorMessageText(zr)); - } - - for (int i = 0; i < zf.EntriesCount && zr == ZipReturn.ZipGood; i++) - { - if (zf.Entries[i].FileName.Contains(entryName)) - { - // Open the read stream - realEntry = zf.Entries[i].FileName; - zr = zf.OpenReadStream(i, false, out Stream readStream, out ulong streamsize, out SabreTools.Library.Data.CompressionMethod cm, out uint lastMod); - - // If the stream is smaller than the buffer, just run one loop through to avoid issues - if (streamsize < _bufferSize) - { - byte[] ibuffer = new byte[streamsize]; - int ilen = readStream.Read(ibuffer, 0, (int)streamsize); - ms.Write(ibuffer, 0, ilen); - ms.Flush(); - } - // Otherwise, we do the normal loop - else - { - byte[] ibuffer = new byte[_bufferSize]; - int ilen; - while (streamsize > _bufferSize) - { - ilen = readStream.Read(ibuffer, 0, _bufferSize); - ms.Write(ibuffer, 0, ilen); - ms.Flush(); - streamsize -= _bufferSize; - } - - ilen = readStream.Read(ibuffer, 0, (int)streamsize); - ms.Write(ibuffer, 0, ilen); - ms.Flush(); - } - - zr = zf.CloseReadStream(); - } - } - - zf.Dispose(); - } - catch (Exception ex) - { - Globals.Logger.Error(ex.ToString()); - ms = null; - realEntry = null; - } - - return (ms, realEntry); - } - - #endregion - - #region Information - - /// - /// Generate a list of DatItem objects from the header values in an archive - /// - /// Hash representing the hashes that should be skipped - /// True if entry dates should be included, false otherwise (default) - /// List of DatItem objects representing the found data - /// TODO: All instances of Hash.DeepHashes should be made into 0x0 eventually - public override List GetChildren(Hash omitFromScan = Hash.DeepHashes, bool date = false) - { - List found = new List(); - string gamename = Path.GetFileNameWithoutExtension(_filename); - - try - { - ZipFile zf = new ZipFile(); - ZipReturn zr = zf.Open(_filename, new FileInfo(_filename).LastWriteTime.Ticks, true); - if (zr != ZipReturn.ZipGood) - { - throw new Exception(ZipFile.ZipErrorMessageText(zr)); - } - - for (int i = 0; i < zf.EntriesCount; i++) - { - // Open the read stream - zr = zf.OpenReadStream(i, false, out Stream readStream, out ulong streamsize, out SabreTools.Library.Data.CompressionMethod cm, out uint lastMod); - - // If we get a read error, log it and continue - if (zr != ZipReturn.ZipGood) - { - Globals.Logger.Warning("An error occurred while reading archive {0}: Zip Error - {1}", _filename, zr); - continue; - } - - // If the entry ends with a directory separator, continue to the next item, if any - if (zf.Entries[i].FileName.EndsWith(Path.DirectorySeparatorChar.ToString()) - || zf.Entries[i].FileName.EndsWith(Path.AltDirectorySeparatorChar.ToString()) - || zf.Entries[i].FileName.EndsWith(Path.PathSeparator.ToString())) - { - continue; - } - - // If secure hashes are disabled, do a quickscan - if (omitFromScan == Hash.SecureHashes) - { - string newname = zf.Entries[i].FileName; - long newsize = (long)zf.Entries[i].UncompressedSize; - byte[] newcrc = zf.Entries[i].CRC.Reverse().ToArray(); - string convertedDate = Utilities.ConvertMsDosTimeFormatToDateTime(zf.Entries[i].LastMod).ToString("yyyy/MM/dd hh:mm:ss"); - - found.Add(new BaseFile - { - Filename = newname, - Size = newsize, - CRC = newcrc, - Date = (date ? convertedDate : null), - - Parent = gamename, - }); - } - // Otherwise, use the stream directly - else - { - BaseFile zipEntryRom = Utilities.GetStreamInfo(readStream, (long)zf.Entries[i].UncompressedSize, omitFromScan: omitFromScan, keepReadOpen: true); - zipEntryRom.Filename = zf.Entries[i].FileName; - zipEntryRom.Parent = gamename; - string convertedDate = Utilities.ConvertMsDosTimeFormatToDateTime(zf.Entries[i].LastMod).ToString("yyyy/MM/dd hh:mm:ss"); - zipEntryRom.Date = (date ? convertedDate : null); - found.Add(zipEntryRom); - } - } - - // Dispose of the archive - zr = zf.CloseReadStream(); - zf.Close(); - } - catch (Exception ex) - { - Globals.Logger.Error(ex.ToString()); - return null; - } - - return found; - } - - /// - /// Generate a list of empty folders in an archive - /// - /// Input file to get data from - /// List of empty folders in the archive - public override List GetEmptyFolders() - { - List empties = new List(); - - try - { - SharpCompress.Archives.Zip.ZipArchive za = SharpCompress.Archives.Zip.ZipArchive.Open(_filename, new ReaderOptions { LeaveStreamOpen = false }); - List zipEntries = za.Entries.OrderBy(e => e.Key, new NaturalSort.NaturalReversedComparer()).ToList(); - string lastZipEntry = null; - foreach (SharpCompress.Archives.Zip.ZipArchiveEntry entry in zipEntries) - { - if (entry != null) - { - // If the current is a superset of last, we skip it - if (lastZipEntry != null && lastZipEntry.StartsWith(entry.Key)) - { - // No-op - } - // If the entry is a directory, we add it - else - { - if (entry.IsDirectory) - { - empties.Add(entry.Key); - } - lastZipEntry = entry.Key; - } - } - } - } - catch (Exception ex) - { - Globals.Logger.Error(ex.ToString()); - } - - return empties; - } - - /// - /// Check whether the input file is a standardized format - /// - public override bool IsTorrent() - { - throw new NotImplementedException(); - } - - #endregion - - #region Writing - - /// - /// Write an input file to a torrentzip archive - /// - /// Input filename to be moved - /// Output directory to build to - /// DatItem representing the new information - /// True if the date from the DAT should be used if available, false otherwise (default) - /// True if files should be output in Romba depot folders, false otherwise - /// True if the archive was written properly, false otherwise - public override bool Write(string inputFile, string outDir, Rom rom, bool date = false, bool romba = false) - { - // Get the file stream for the file and write out - return Write(Utilities.TryOpenRead(inputFile), outDir, rom, date: date); - } - - /// - /// Write an input stream to a torrentzip archive - /// - /// Input filename to be moved - /// Output directory to build to - /// DatItem representing the new information - /// True if the date from the DAT should be used if available, false otherwise (default) - /// True if files should be output in Romba depot folders, false otherwise - /// True if the archive was written properly, false otherwise - public override bool Write(Stream inputStream, string outDir, Rom rom, bool date = false, bool romba = false) - { - bool success = false; - string tempFile = Path.Combine(outDir, "tmp" + Guid.NewGuid().ToString()); - - // If either input is null or empty, return - if (inputStream == null || rom == null || rom.Name == null) - { - return success; - } - - // If the stream is not readable, return - if (!inputStream.CanRead) - { - return success; - } - - // Seek to the beginning of the stream - inputStream.Seek(0, SeekOrigin.Begin); - - // Get the output archive name from the first rebuild rom - string archiveFileName = Path.Combine(outDir, Utilities.RemovePathUnsafeCharacters(rom.MachineName) + (rom.MachineName.EndsWith(".zip") ? "" : ".zip")); - - // Set internal variables - Stream writeStream = null; - ZipFile oldZipFile = new ZipFile(); - ZipFile zipFile = new ZipFile(); - ZipReturn zipReturn = ZipReturn.ZipGood; - - try - { - // If the full output path doesn't exist, create it - if (!Directory.Exists(Path.GetDirectoryName(archiveFileName))) - { - Directory.CreateDirectory(Path.GetDirectoryName(archiveFileName)); - } - - // If the archive doesn't exist, create it and put the single file - if (!File.Exists(archiveFileName)) - { - inputStream.Seek(0, SeekOrigin.Begin); - zipReturn = zipFile.Create(tempFile); - - // Open the input file for reading - ulong istreamSize = (ulong)(inputStream.Length); - - DateTime dt = DateTime.Now; - if (date && !String.IsNullOrWhiteSpace(rom.Date) && DateTime.TryParse(rom.Date.Replace('\\', '/'), out dt)) - { - uint msDosDateTime = Utilities.ConvertDateTimeToMsDosTimeFormat(dt); - zipFile.OpenWriteStream(false, false, rom.Name.Replace('\\', '/'), istreamSize, - SabreTools.Library.Data.CompressionMethod.Deflated, out writeStream, lastMod: msDosDateTime); - } - else - { - zipFile.OpenWriteStream(false, true, rom.Name.Replace('\\', '/'), istreamSize, SabreTools.Library.Data.CompressionMethod.Deflated, out writeStream); - } - - // Copy the input stream to the output - byte[] ibuffer = new byte[_bufferSize]; - int ilen; - while ((ilen = inputStream.Read(ibuffer, 0, _bufferSize)) > 0) - { - writeStream.Write(ibuffer, 0, ilen); - writeStream.Flush(); - } - inputStream.Dispose(); - zipFile.CloseWriteStream(Convert.ToUInt32(rom.CRC, 16)); - } - - // Otherwise, sort the input files and write out in the correct order - else - { - // Open the old archive for reading - oldZipFile.Open(archiveFileName, new FileInfo(archiveFileName).LastWriteTime.Ticks, true); - - // Map all inputs to index - Dictionary inputIndexMap = new Dictionary(); - - // If the old one doesn't contain the new file, then add it - if (!oldZipFile.Contains(rom.Name.Replace('\\', '/'))) - { - inputIndexMap.Add(rom.Name.Replace('\\', '/'), -1); - } - - // Then add all of the old entries to it too - for (int i = 0; i < oldZipFile.EntriesCount; i++) - { - inputIndexMap.Add(oldZipFile.Filename(i), i); - } - - // If the number of entries is the same as the old archive, skip out - if (inputIndexMap.Keys.Count <= oldZipFile.EntriesCount) - { - success = true; - return success; - } - - // Otherwise, process the old zipfile - zipFile.Create(tempFile); - - // Get the order for the entries with the new file - List keys = inputIndexMap.Keys.ToList(); - keys.Sort(ZipFile.TorrentZipStringCompare); - - // Copy over all files to the new archive - foreach (string key in keys) - { - // Get the index mapped to the key - int index = inputIndexMap[key]; - - // If we have the input file, add it now - if (index < 0) - { - // Open the input file for reading - ulong istreamSize = (ulong)(inputStream.Length); - - DateTime dt = DateTime.Now; - if (date && !String.IsNullOrWhiteSpace(rom.Date) && DateTime.TryParse(rom.Date.Replace('\\', '/'), out dt)) - { - uint msDosDateTime = Utilities.ConvertDateTimeToMsDosTimeFormat(dt); - zipFile.OpenWriteStream(false, false, rom.Name.Replace('\\', '/'), istreamSize, - SabreTools.Library.Data.CompressionMethod.Deflated, out writeStream, lastMod: msDosDateTime); - } - else - { - zipFile.OpenWriteStream(false, true, rom.Name.Replace('\\', '/'), istreamSize, SabreTools.Library.Data.CompressionMethod.Deflated, out writeStream); - } - - // Copy the input stream to the output - byte[] ibuffer = new byte[_bufferSize]; - int ilen; - while ((ilen = inputStream.Read(ibuffer, 0, _bufferSize)) > 0) - { - writeStream.Write(ibuffer, 0, ilen); - writeStream.Flush(); - } - inputStream.Dispose(); - zipFile.CloseWriteStream(Convert.ToUInt32(rom.CRC, 16)); - } - - // Otherwise, copy the file from the old archive - else - { - // Instantiate the streams - oldZipFile.OpenReadStream(index, false, out Stream zreadStream, out ulong istreamSize, out SabreTools.Library.Data.CompressionMethod icompressionMethod, out uint lastMod); - zipFile.OpenWriteStream(false, lastMod == Constants.TorrentZipFileDateTime, oldZipFile.Filename(index), - istreamSize, SabreTools.Library.Data.CompressionMethod.Deflated, out writeStream, lastMod: lastMod); - - // Copy the input stream to the output - byte[] ibuffer = new byte[_bufferSize]; - int ilen; - while ((ilen = zreadStream.Read(ibuffer, 0, _bufferSize)) > 0) - { - writeStream.Write(ibuffer, 0, ilen); - writeStream.Flush(); - } - zipFile.CloseWriteStream(BitConverter.ToUInt32(oldZipFile.CRC32(index), 0)); - } - } - } - - // Close the output zip file - zipFile.Close(); - - success = true; - } - catch (Exception ex) - { - Console.WriteLine(ex); - success = false; - } - finally - { - inputStream?.Dispose(); - zipFile.Dispose(); - oldZipFile.Dispose(); - } - - // If the old file exists, delete it and replace - if (File.Exists(archiveFileName)) - { - Utilities.TryDeleteFile(archiveFileName); - } - File.Move(tempFile, archiveFileName); - - return true; - } - - /// - /// Write a set of input files to a torrentzip archive (assuming the same output archive name) - /// - /// Input filenames to be moved - /// Output directory to build to - /// List of Rom representing the new information - /// True if the date from the DAT should be used if available, false otherwise (default) - /// True if files should be output in Romba depot folders, false otherwise - /// True if the archive was written properly, false otherwise - public override bool Write(List inputFiles, string outDir, List roms, bool date = false, bool romba = false) - { - bool success = false; - string tempFile = Path.Combine(outDir, "tmp" + Guid.NewGuid().ToString()); - - // If either list of roms is null or empty, return - if (inputFiles == null || roms == null || inputFiles.Count == 0 || roms.Count == 0) - { - return success; - } - - // If the number of inputs is less than the number of available roms, return - if (inputFiles.Count < roms.Count) - { - return success; - } - - // If one of the files doesn't exist, return - foreach (string file in inputFiles) - { - if (!File.Exists(file)) - { - return success; - } - } - - // Get the output archive name from the first rebuild rom - string archiveFileName = Path.Combine(outDir, Utilities.RemovePathUnsafeCharacters(roms[0].MachineName) + (roms[0].MachineName.EndsWith(".zip") ? "" : ".zip")); - - // Set internal variables - Stream writeStream = null; - ZipFile oldZipFile = new ZipFile(); - ZipFile zipFile = new ZipFile(); - ZipReturn zipReturn = ZipReturn.ZipGood; - - try - { - // If the full output path doesn't exist, create it - if (!Directory.Exists(Path.GetDirectoryName(archiveFileName))) - { - Directory.CreateDirectory(Path.GetDirectoryName(archiveFileName)); - } - - // If the archive doesn't exist, create it and put the single file - if (!File.Exists(archiveFileName)) - { - zipReturn = zipFile.Create(tempFile); - - // Map all inputs to index - Dictionary inputIndexMap = new Dictionary(); - for (int i = 0; i < inputFiles.Count; i++) - { - inputIndexMap.Add(roms[i].Name.Replace('\\', '/'), i); - } - - // Sort the keys in TZIP order - List keys = inputIndexMap.Keys.ToList(); - keys.Sort(ZipFile.TorrentZipStringCompare); - - // Now add all of the files in order - foreach (string key in keys) - { - // Get the index mapped to the key - int index = inputIndexMap[key]; - - // Open the input file for reading - Stream freadStream = Utilities.TryOpenRead(inputFiles[index]); - ulong istreamSize = (ulong)(new FileInfo(inputFiles[index]).Length); - - DateTime dt = DateTime.Now; - if (date && !String.IsNullOrWhiteSpace(roms[index].Date) && DateTime.TryParse(roms[index].Date.Replace('\\', '/'), out dt)) - { - uint msDosDateTime = Utilities.ConvertDateTimeToMsDosTimeFormat(dt); - zipFile.OpenWriteStream(false, false, roms[index].Name.Replace('\\', '/'), istreamSize, - SabreTools.Library.Data.CompressionMethod.Deflated, out writeStream, lastMod: msDosDateTime); - } - else - { - zipFile.OpenWriteStream(false, true, roms[index].Name.Replace('\\', '/'), istreamSize, SabreTools.Library.Data.CompressionMethod.Deflated, out writeStream); - } - - // Copy the input stream to the output - byte[] ibuffer = new byte[_bufferSize]; - int ilen; - while ((ilen = freadStream.Read(ibuffer, 0, _bufferSize)) > 0) - { - writeStream.Write(ibuffer, 0, ilen); - writeStream.Flush(); - } - freadStream.Dispose(); - zipFile.CloseWriteStream(Convert.ToUInt32(roms[index].CRC, 16)); - } - } - - // Otherwise, sort the input files and write out in the correct order - else - { - // Open the old archive for reading - oldZipFile.Open(archiveFileName, new FileInfo(archiveFileName).LastWriteTime.Ticks, true); - - // Map all inputs to index - Dictionary inputIndexMap = new Dictionary(); - for (int i = 0; i < inputFiles.Count; i++) - { - // If the old one contains the new file, then just skip out - if (oldZipFile.Contains(roms[i].Name.Replace('\\', '/'))) - { - continue; - } - - inputIndexMap.Add(roms[i].Name.Replace('\\', '/'), -(i + 1)); - } - - // Then add all of the old entries to it too - for (int i = 0; i < oldZipFile.EntriesCount; i++) - { - inputIndexMap.Add(oldZipFile.Filename(i), i); - } - - // If the number of entries is the same as the old archive, skip out - if (inputIndexMap.Keys.Count <= oldZipFile.EntriesCount) - { - success = true; - return success; - } - - // Otherwise, process the old zipfile - zipFile.Create(tempFile); - - // Get the order for the entries with the new file - List keys = inputIndexMap.Keys.ToList(); - keys.Sort(ZipFile.TorrentZipStringCompare); - - // Copy over all files to the new archive - foreach (string key in keys) - { - // Get the index mapped to the key - int index = inputIndexMap[key]; - - // If we have the input file, add it now - if (index < 0) - { - // Open the input file for reading - Stream freadStream = Utilities.TryOpenRead(inputFiles[-index - 1]); - ulong istreamSize = (ulong)(new FileInfo(inputFiles[-index - 1]).Length); - - DateTime dt = DateTime.Now; - if (date && !String.IsNullOrWhiteSpace(roms[-index - 1].Date) && DateTime.TryParse(roms[-index - 1].Date.Replace('\\', '/'), out dt)) - { - uint msDosDateTime = Utilities.ConvertDateTimeToMsDosTimeFormat(dt); - zipFile.OpenWriteStream(false, false, roms[-index - 1].Name.Replace('\\', '/'), istreamSize, - SabreTools.Library.Data.CompressionMethod.Deflated, out writeStream, lastMod: msDosDateTime); - } - else - { - zipFile.OpenWriteStream(false, true, roms[-index - 1].Name.Replace('\\', '/'), istreamSize, SabreTools.Library.Data.CompressionMethod.Deflated, out writeStream); - } - - // Copy the input stream to the output - byte[] ibuffer = new byte[_bufferSize]; - int ilen; - while ((ilen = freadStream.Read(ibuffer, 0, _bufferSize)) > 0) - { - writeStream.Write(ibuffer, 0, ilen); - writeStream.Flush(); - } - freadStream.Dispose(); - zipFile.CloseWriteStream(Convert.ToUInt32(roms[-index - 1].CRC, 16)); - } - - // Otherwise, copy the file from the old archive - else - { - // Instantiate the streams - oldZipFile.OpenReadStream(index, false, out Stream zreadStream, out ulong istreamSize, out SabreTools.Library.Data.CompressionMethod icompressionMethod, out uint lastMod); - zipFile.OpenWriteStream(false, lastMod == Constants.TorrentZipFileDateTime, oldZipFile.Filename(index), - istreamSize, SabreTools.Library.Data.CompressionMethod.Deflated, out writeStream, lastMod: lastMod); - - // Copy the input stream to the output - byte[] ibuffer = new byte[_bufferSize]; - int ilen; - while ((ilen = zreadStream.Read(ibuffer, 0, _bufferSize)) > 0) - { - writeStream.Write(ibuffer, 0, ilen); - writeStream.Flush(); - } - zipFile.CloseWriteStream(BitConverter.ToUInt32(oldZipFile.CRC32(index), 0)); - } - } - } - - // Close the output zip file - zipFile.Close(); - - success = true; - } - catch (Exception ex) - { - Console.WriteLine(ex); - success = false; - } - finally - { - zipFile.Dispose(); - oldZipFile.Dispose(); - } - - // If the old file exists, delete it and replace - if (File.Exists(archiveFileName)) - { - Utilities.TryDeleteFile(archiveFileName); - } - File.Move(tempFile, archiveFileName); - - return true; - } - - #endregion - } + /// + /// Represents a Zip archive for reading and writing + /// + public class ZipArchive : BaseArchive + { + #region Constructors + + /// + /// Create a new TorrentZipArchive with no base file + /// + public ZipArchive() + : base() + { + this.Type = FileType.ZipArchive; + } + + /// + /// Create a new TorrentZipArchive from the given file + /// + /// Name of the file to use as an archive + /// True for opening file as read, false for opening file as write + /// True if hashes for this file should be calculated, false otherwise (default) + public ZipArchive(string filename, bool getHashes = false) + : base(filename, getHashes) + { + this.Type = FileType.ZipArchive; + } + + #endregion + + #region Extraction + + /// + /// Attempt to extract a file as an archive + /// + /// Output directory for archive extraction + /// True if the extraction was a success, false otherwise + public override bool CopyAll(string outDir) + { + bool encounteredErrors = true; + + try + { + // Create the temp directory + Directory.CreateDirectory(outDir); + + // Extract all files to the temp directory + ZipFile zf = new ZipFile(); + ZipReturn zr = zf.Open(this.Filename, new FileInfo(this.Filename).LastWriteTime.Ticks, true); + if (zr != ZipReturn.ZipGood) + { + throw new Exception(ZipFile.ZipErrorMessageText(zr)); + } + + for (int i = 0; i < zf.EntriesCount && zr == ZipReturn.ZipGood; i++) + { + // Open the read stream + zr = zf.OpenReadStream(i, false, out Stream readStream, out ulong streamsize, out SabreTools.Library.Data.CompressionMethod cm, out uint lastMod); + + // Create the rest of the path, if needed + if (!String.IsNullOrWhiteSpace(Path.GetDirectoryName(zf.Entries[i].FileName))) + { + Directory.CreateDirectory(Path.Combine(outDir, Path.GetDirectoryName(zf.Entries[i].FileName))); + } + + // If the entry ends with a directory separator, continue to the next item, if any + if (zf.Entries[i].FileName.EndsWith(Path.DirectorySeparatorChar.ToString()) + || zf.Entries[i].FileName.EndsWith(Path.AltDirectorySeparatorChar.ToString()) + || zf.Entries[i].FileName.EndsWith(Path.PathSeparator.ToString())) + { + continue; + } + + FileStream writeStream = Utilities.TryCreate(Path.Combine(outDir, zf.Entries[i].FileName)); + + // If the stream is smaller than the buffer, just run one loop through to avoid issues + if (streamsize < _bufferSize) + { + byte[] ibuffer = new byte[streamsize]; + int ilen = readStream.Read(ibuffer, 0, (int)streamsize); + writeStream.Write(ibuffer, 0, ilen); + writeStream.Flush(); + } + // Otherwise, we do the normal loop + else + { + int realBufferSize = (streamsize < _bufferSize ? (int)streamsize : _bufferSize); + byte[] ibuffer = new byte[realBufferSize]; + int ilen; + while ((ilen = readStream.Read(ibuffer, 0, realBufferSize)) > 0) + { + writeStream.Write(ibuffer, 0, ilen); + writeStream.Flush(); + } + } + + zr = zf.CloseReadStream(); + writeStream.Dispose(); + } + zf.Close(); + encounteredErrors = false; + } + catch (EndOfStreamException) + { + // Catch this but don't count it as an error because SharpCompress is unsafe + } + catch (InvalidOperationException) + { + encounteredErrors = true; + } + catch (Exception ex) + { + Globals.Logger.Error(ex.ToString()); + encounteredErrors = true; + } + + return encounteredErrors; + } + + /// + /// Attempt to extract a file from an archive + /// + /// Name of the entry to be extracted + /// Output directory for archive extraction + /// Name of the extracted file, null on error + public override string CopyToFile(string entryName, string outDir) + { + // Try to extract a stream using the given information + (MemoryStream ms, string realEntry) = CopyToStream(entryName); + + // If the memory stream and the entry name are both non-null, we write to file + if (ms != null && realEntry != null) + { + realEntry = Path.Combine(outDir, realEntry); + + // Create the output subfolder now + Directory.CreateDirectory(Path.GetDirectoryName(realEntry)); + + // Now open and write the file if possible + FileStream fs = Utilities.TryCreate(realEntry); + if (fs != null) + { + ms.Seek(0, SeekOrigin.Begin); + byte[] zbuffer = new byte[_bufferSize]; + int zlen; + while ((zlen = ms.Read(zbuffer, 0, _bufferSize)) > 0) + { + fs.Write(zbuffer, 0, zlen); + fs.Flush(); + } + + ms?.Dispose(); + fs?.Dispose(); + } + else + { + ms?.Dispose(); + fs?.Dispose(); + realEntry = null; + } + } + + return realEntry; + } + + /// + /// Attempt to extract a stream from an archive + /// + /// Name of the entry to be extracted + /// Output representing the entry name that was found + /// MemoryStream representing the entry, null on error + public override (MemoryStream, string) CopyToStream(string entryName) + { + MemoryStream ms = new MemoryStream(); + string realEntry = null; + + try + { + ZipFile zf = new ZipFile(); + ZipReturn zr = zf.Open(this.Filename, new FileInfo(this.Filename).LastWriteTime.Ticks, true); + if (zr != ZipReturn.ZipGood) + { + throw new Exception(ZipFile.ZipErrorMessageText(zr)); + } + + for (int i = 0; i < zf.EntriesCount && zr == ZipReturn.ZipGood; i++) + { + if (zf.Entries[i].FileName.Contains(entryName)) + { + // Open the read stream + realEntry = zf.Entries[i].FileName; + zr = zf.OpenReadStream(i, false, out Stream readStream, out ulong streamsize, out SabreTools.Library.Data.CompressionMethod cm, out uint lastMod); + + // If the stream is smaller than the buffer, just run one loop through to avoid issues + if (streamsize < _bufferSize) + { + byte[] ibuffer = new byte[streamsize]; + int ilen = readStream.Read(ibuffer, 0, (int)streamsize); + ms.Write(ibuffer, 0, ilen); + ms.Flush(); + } + // Otherwise, we do the normal loop + else + { + byte[] ibuffer = new byte[_bufferSize]; + int ilen; + while (streamsize > _bufferSize) + { + ilen = readStream.Read(ibuffer, 0, _bufferSize); + ms.Write(ibuffer, 0, ilen); + ms.Flush(); + streamsize -= _bufferSize; + } + + ilen = readStream.Read(ibuffer, 0, (int)streamsize); + ms.Write(ibuffer, 0, ilen); + ms.Flush(); + } + + zr = zf.CloseReadStream(); + } + } + + zf.Dispose(); + } + catch (Exception ex) + { + Globals.Logger.Error(ex.ToString()); + ms = null; + realEntry = null; + } + + return (ms, realEntry); + } + + #endregion + + #region Information + + /// + /// Generate a list of DatItem objects from the header values in an archive + /// + /// Hash representing the hashes that should be skipped + /// True if entry dates should be included, false otherwise (default) + /// List of DatItem objects representing the found data + /// TODO: All instances of Hash.DeepHashes should be made into 0x0 eventually + public override List GetChildren(Hash omitFromScan = Hash.DeepHashes, bool date = false) + { + List found = new List(); + string gamename = Path.GetFileNameWithoutExtension(this.Filename); + + try + { + ZipFile zf = new ZipFile(); + ZipReturn zr = zf.Open(this.Filename, new FileInfo(this.Filename).LastWriteTime.Ticks, true); + if (zr != ZipReturn.ZipGood) + { + throw new Exception(ZipFile.ZipErrorMessageText(zr)); + } + + for (int i = 0; i < zf.EntriesCount; i++) + { + // Open the read stream + zr = zf.OpenReadStream(i, false, out Stream readStream, out ulong streamsize, out SabreTools.Library.Data.CompressionMethod cm, out uint lastMod); + + // If we get a read error, log it and continue + if (zr != ZipReturn.ZipGood) + { + Globals.Logger.Warning("An error occurred while reading archive {0}: Zip Error - {1}", this.Filename, zr); + continue; + } + + // If the entry ends with a directory separator, continue to the next item, if any + if (zf.Entries[i].FileName.EndsWith(Path.DirectorySeparatorChar.ToString()) + || zf.Entries[i].FileName.EndsWith(Path.AltDirectorySeparatorChar.ToString()) + || zf.Entries[i].FileName.EndsWith(Path.PathSeparator.ToString())) + { + continue; + } + + // If secure hashes are disabled, do a quickscan + if (omitFromScan == Hash.SecureHashes) + { + string newname = zf.Entries[i].FileName; + long newsize = (long)zf.Entries[i].UncompressedSize; + byte[] newcrc = zf.Entries[i].CRC.Reverse().ToArray(); + string convertedDate = Utilities.ConvertMsDosTimeFormatToDateTime(zf.Entries[i].LastMod).ToString("yyyy/MM/dd hh:mm:ss"); + + found.Add(new BaseFile + { + Filename = newname, + Size = newsize, + CRC = newcrc, + Date = (date ? convertedDate : null), + + Parent = gamename, + }); + } + // Otherwise, use the stream directly + else + { + BaseFile zipEntryRom = Utilities.GetStreamInfo(readStream, (long)zf.Entries[i].UncompressedSize, omitFromScan: omitFromScan, keepReadOpen: true); + zipEntryRom.Filename = zf.Entries[i].FileName; + zipEntryRom.Parent = gamename; + string convertedDate = Utilities.ConvertMsDosTimeFormatToDateTime(zf.Entries[i].LastMod).ToString("yyyy/MM/dd hh:mm:ss"); + zipEntryRom.Date = (date ? convertedDate : null); + found.Add(zipEntryRom); + } + } + + // Dispose of the archive + zr = zf.CloseReadStream(); + zf.Close(); + } + catch (Exception ex) + { + Globals.Logger.Error(ex.ToString()); + return null; + } + + return found; + } + + /// + /// Generate a list of empty folders in an archive + /// + /// Input file to get data from + /// List of empty folders in the archive + public override List GetEmptyFolders() + { + List empties = new List(); + + try + { + SharpCompress.Archives.Zip.ZipArchive za = SharpCompress.Archives.Zip.ZipArchive.Open(this.Filename, new ReaderOptions { LeaveStreamOpen = false }); + List zipEntries = za.Entries.OrderBy(e => e.Key, new NaturalSort.NaturalReversedComparer()).ToList(); + string lastZipEntry = null; + foreach (SharpCompress.Archives.Zip.ZipArchiveEntry entry in zipEntries) + { + if (entry != null) + { + // If the current is a superset of last, we skip it + if (lastZipEntry != null && lastZipEntry.StartsWith(entry.Key)) + { + // No-op + } + // If the entry is a directory, we add it + else + { + if (entry.IsDirectory) + { + empties.Add(entry.Key); + } + lastZipEntry = entry.Key; + } + } + } + } + catch (Exception ex) + { + Globals.Logger.Error(ex.ToString()); + } + + return empties; + } + + /// + /// Check whether the input file is a standardized format + /// + public override bool IsTorrent() + { + throw new NotImplementedException(); + } + + #endregion + + #region Writing + + /// + /// Write an input file to a torrentzip archive + /// + /// Input filename to be moved + /// Output directory to build to + /// DatItem representing the new information + /// True if the date from the DAT should be used if available, false otherwise (default) + /// True if files should be output in Romba depot folders, false otherwise + /// True if the archive was written properly, false otherwise + public override bool Write(string inputFile, string outDir, Rom rom, bool date = false, bool romba = false) + { + // Get the file stream for the file and write out + return Write(Utilities.TryOpenRead(inputFile), outDir, rom, date: date); + } + + /// + /// Write an input stream to a torrentzip archive + /// + /// Input filename to be moved + /// Output directory to build to + /// DatItem representing the new information + /// True if the date from the DAT should be used if available, false otherwise (default) + /// True if files should be output in Romba depot folders, false otherwise + /// True if the archive was written properly, false otherwise + public override bool Write(Stream inputStream, string outDir, Rom rom, bool date = false, bool romba = false) + { + bool success = false; + string tempFile = Path.Combine(outDir, "tmp" + Guid.NewGuid().ToString()); + + // If either input is null or empty, return + if (inputStream == null || rom == null || rom.Name == null) + { + return success; + } + + // If the stream is not readable, return + if (!inputStream.CanRead) + { + return success; + } + + // Seek to the beginning of the stream + inputStream.Seek(0, SeekOrigin.Begin); + + // Get the output archive name from the first rebuild rom + string archiveFileName = Path.Combine(outDir, Utilities.RemovePathUnsafeCharacters(rom.MachineName) + (rom.MachineName.EndsWith(".zip") ? "" : ".zip")); + + // Set internal variables + Stream writeStream = null; + ZipFile oldZipFile = new ZipFile(); + ZipFile zipFile = new ZipFile(); + ZipReturn zipReturn = ZipReturn.ZipGood; + + try + { + // If the full output path doesn't exist, create it + if (!Directory.Exists(Path.GetDirectoryName(archiveFileName))) + { + Directory.CreateDirectory(Path.GetDirectoryName(archiveFileName)); + } + + // If the archive doesn't exist, create it and put the single file + if (!File.Exists(archiveFileName)) + { + inputStream.Seek(0, SeekOrigin.Begin); + zipReturn = zipFile.Create(tempFile); + + // Open the input file for reading + ulong istreamSize = (ulong)(inputStream.Length); + + DateTime dt = DateTime.Now; + if (date && !String.IsNullOrWhiteSpace(rom.Date) && DateTime.TryParse(rom.Date.Replace('\\', '/'), out dt)) + { + uint msDosDateTime = Utilities.ConvertDateTimeToMsDosTimeFormat(dt); + zipFile.OpenWriteStream(false, false, rom.Name.Replace('\\', '/'), istreamSize, + SabreTools.Library.Data.CompressionMethod.Deflated, out writeStream, lastMod: msDosDateTime); + } + else + { + zipFile.OpenWriteStream(false, true, rom.Name.Replace('\\', '/'), istreamSize, SabreTools.Library.Data.CompressionMethod.Deflated, out writeStream); + } + + // Copy the input stream to the output + byte[] ibuffer = new byte[_bufferSize]; + int ilen; + while ((ilen = inputStream.Read(ibuffer, 0, _bufferSize)) > 0) + { + writeStream.Write(ibuffer, 0, ilen); + writeStream.Flush(); + } + inputStream.Dispose(); + zipFile.CloseWriteStream(Convert.ToUInt32(rom.CRC, 16)); + } + + // Otherwise, sort the input files and write out in the correct order + else + { + // Open the old archive for reading + oldZipFile.Open(archiveFileName, new FileInfo(archiveFileName).LastWriteTime.Ticks, true); + + // Map all inputs to index + Dictionary inputIndexMap = new Dictionary(); + + // If the old one doesn't contain the new file, then add it + if (!oldZipFile.Contains(rom.Name.Replace('\\', '/'))) + { + inputIndexMap.Add(rom.Name.Replace('\\', '/'), -1); + } + + // Then add all of the old entries to it too + for (int i = 0; i < oldZipFile.EntriesCount; i++) + { + inputIndexMap.Add(oldZipFile.Filename(i), i); + } + + // If the number of entries is the same as the old archive, skip out + if (inputIndexMap.Keys.Count <= oldZipFile.EntriesCount) + { + success = true; + return success; + } + + // Otherwise, process the old zipfile + zipFile.Create(tempFile); + + // Get the order for the entries with the new file + List keys = inputIndexMap.Keys.ToList(); + keys.Sort(ZipFile.TorrentZipStringCompare); + + // Copy over all files to the new archive + foreach (string key in keys) + { + // Get the index mapped to the key + int index = inputIndexMap[key]; + + // If we have the input file, add it now + if (index < 0) + { + // Open the input file for reading + ulong istreamSize = (ulong)(inputStream.Length); + + DateTime dt = DateTime.Now; + if (date && !String.IsNullOrWhiteSpace(rom.Date) && DateTime.TryParse(rom.Date.Replace('\\', '/'), out dt)) + { + uint msDosDateTime = Utilities.ConvertDateTimeToMsDosTimeFormat(dt); + zipFile.OpenWriteStream(false, false, rom.Name.Replace('\\', '/'), istreamSize, + SabreTools.Library.Data.CompressionMethod.Deflated, out writeStream, lastMod: msDosDateTime); + } + else + { + zipFile.OpenWriteStream(false, true, rom.Name.Replace('\\', '/'), istreamSize, SabreTools.Library.Data.CompressionMethod.Deflated, out writeStream); + } + + // Copy the input stream to the output + byte[] ibuffer = new byte[_bufferSize]; + int ilen; + while ((ilen = inputStream.Read(ibuffer, 0, _bufferSize)) > 0) + { + writeStream.Write(ibuffer, 0, ilen); + writeStream.Flush(); + } + inputStream.Dispose(); + zipFile.CloseWriteStream(Convert.ToUInt32(rom.CRC, 16)); + } + + // Otherwise, copy the file from the old archive + else + { + // Instantiate the streams + oldZipFile.OpenReadStream(index, false, out Stream zreadStream, out ulong istreamSize, out SabreTools.Library.Data.CompressionMethod icompressionMethod, out uint lastMod); + zipFile.OpenWriteStream(false, lastMod == Constants.TorrentZipFileDateTime, oldZipFile.Filename(index), + istreamSize, SabreTools.Library.Data.CompressionMethod.Deflated, out writeStream, lastMod: lastMod); + + // Copy the input stream to the output + byte[] ibuffer = new byte[_bufferSize]; + int ilen; + while ((ilen = zreadStream.Read(ibuffer, 0, _bufferSize)) > 0) + { + writeStream.Write(ibuffer, 0, ilen); + writeStream.Flush(); + } + zipFile.CloseWriteStream(BitConverter.ToUInt32(oldZipFile.CRC32(index), 0)); + } + } + } + + // Close the output zip file + zipFile.Close(); + + success = true; + } + catch (Exception ex) + { + Console.WriteLine(ex); + success = false; + } + finally + { + inputStream?.Dispose(); + zipFile.Dispose(); + oldZipFile.Dispose(); + } + + // If the old file exists, delete it and replace + if (File.Exists(archiveFileName)) + { + Utilities.TryDeleteFile(archiveFileName); + } + File.Move(tempFile, archiveFileName); + + return true; + } + + /// + /// Write a set of input files to a torrentzip archive (assuming the same output archive name) + /// + /// Input filenames to be moved + /// Output directory to build to + /// List of Rom representing the new information + /// True if the date from the DAT should be used if available, false otherwise (default) + /// True if files should be output in Romba depot folders, false otherwise + /// True if the archive was written properly, false otherwise + public override bool Write(List inputFiles, string outDir, List roms, bool date = false, bool romba = false) + { + bool success = false; + string tempFile = Path.Combine(outDir, "tmp" + Guid.NewGuid().ToString()); + + // If either list of roms is null or empty, return + if (inputFiles == null || roms == null || inputFiles.Count == 0 || roms.Count == 0) + { + return success; + } + + // If the number of inputs is less than the number of available roms, return + if (inputFiles.Count < roms.Count) + { + return success; + } + + // If one of the files doesn't exist, return + foreach (string file in inputFiles) + { + if (!File.Exists(file)) + { + return success; + } + } + + // Get the output archive name from the first rebuild rom + string archiveFileName = Path.Combine(outDir, Utilities.RemovePathUnsafeCharacters(roms[0].MachineName) + (roms[0].MachineName.EndsWith(".zip") ? "" : ".zip")); + + // Set internal variables + Stream writeStream = null; + ZipFile oldZipFile = new ZipFile(); + ZipFile zipFile = new ZipFile(); + ZipReturn zipReturn = ZipReturn.ZipGood; + + try + { + // If the full output path doesn't exist, create it + if (!Directory.Exists(Path.GetDirectoryName(archiveFileName))) + { + Directory.CreateDirectory(Path.GetDirectoryName(archiveFileName)); + } + + // If the archive doesn't exist, create it and put the single file + if (!File.Exists(archiveFileName)) + { + zipReturn = zipFile.Create(tempFile); + + // Map all inputs to index + Dictionary inputIndexMap = new Dictionary(); + for (int i = 0; i < inputFiles.Count; i++) + { + inputIndexMap.Add(roms[i].Name.Replace('\\', '/'), i); + } + + // Sort the keys in TZIP order + List keys = inputIndexMap.Keys.ToList(); + keys.Sort(ZipFile.TorrentZipStringCompare); + + // Now add all of the files in order + foreach (string key in keys) + { + // Get the index mapped to the key + int index = inputIndexMap[key]; + + // Open the input file for reading + Stream freadStream = Utilities.TryOpenRead(inputFiles[index]); + ulong istreamSize = (ulong)(new FileInfo(inputFiles[index]).Length); + + DateTime dt = DateTime.Now; + if (date && !String.IsNullOrWhiteSpace(roms[index].Date) && DateTime.TryParse(roms[index].Date.Replace('\\', '/'), out dt)) + { + uint msDosDateTime = Utilities.ConvertDateTimeToMsDosTimeFormat(dt); + zipFile.OpenWriteStream(false, false, roms[index].Name.Replace('\\', '/'), istreamSize, + SabreTools.Library.Data.CompressionMethod.Deflated, out writeStream, lastMod: msDosDateTime); + } + else + { + zipFile.OpenWriteStream(false, true, roms[index].Name.Replace('\\', '/'), istreamSize, SabreTools.Library.Data.CompressionMethod.Deflated, out writeStream); + } + + // Copy the input stream to the output + byte[] ibuffer = new byte[_bufferSize]; + int ilen; + while ((ilen = freadStream.Read(ibuffer, 0, _bufferSize)) > 0) + { + writeStream.Write(ibuffer, 0, ilen); + writeStream.Flush(); + } + freadStream.Dispose(); + zipFile.CloseWriteStream(Convert.ToUInt32(roms[index].CRC, 16)); + } + } + + // Otherwise, sort the input files and write out in the correct order + else + { + // Open the old archive for reading + oldZipFile.Open(archiveFileName, new FileInfo(archiveFileName).LastWriteTime.Ticks, true); + + // Map all inputs to index + Dictionary inputIndexMap = new Dictionary(); + for (int i = 0; i < inputFiles.Count; i++) + { + // If the old one contains the new file, then just skip out + if (oldZipFile.Contains(roms[i].Name.Replace('\\', '/'))) + { + continue; + } + + inputIndexMap.Add(roms[i].Name.Replace('\\', '/'), -(i + 1)); + } + + // Then add all of the old entries to it too + for (int i = 0; i < oldZipFile.EntriesCount; i++) + { + inputIndexMap.Add(oldZipFile.Filename(i), i); + } + + // If the number of entries is the same as the old archive, skip out + if (inputIndexMap.Keys.Count <= oldZipFile.EntriesCount) + { + success = true; + return success; + } + + // Otherwise, process the old zipfile + zipFile.Create(tempFile); + + // Get the order for the entries with the new file + List keys = inputIndexMap.Keys.ToList(); + keys.Sort(ZipFile.TorrentZipStringCompare); + + // Copy over all files to the new archive + foreach (string key in keys) + { + // Get the index mapped to the key + int index = inputIndexMap[key]; + + // If we have the input file, add it now + if (index < 0) + { + // Open the input file for reading + Stream freadStream = Utilities.TryOpenRead(inputFiles[-index - 1]); + ulong istreamSize = (ulong)(new FileInfo(inputFiles[-index - 1]).Length); + + DateTime dt = DateTime.Now; + if (date && !String.IsNullOrWhiteSpace(roms[-index - 1].Date) && DateTime.TryParse(roms[-index - 1].Date.Replace('\\', '/'), out dt)) + { + uint msDosDateTime = Utilities.ConvertDateTimeToMsDosTimeFormat(dt); + zipFile.OpenWriteStream(false, false, roms[-index - 1].Name.Replace('\\', '/'), istreamSize, + SabreTools.Library.Data.CompressionMethod.Deflated, out writeStream, lastMod: msDosDateTime); + } + else + { + zipFile.OpenWriteStream(false, true, roms[-index - 1].Name.Replace('\\', '/'), istreamSize, SabreTools.Library.Data.CompressionMethod.Deflated, out writeStream); + } + + // Copy the input stream to the output + byte[] ibuffer = new byte[_bufferSize]; + int ilen; + while ((ilen = freadStream.Read(ibuffer, 0, _bufferSize)) > 0) + { + writeStream.Write(ibuffer, 0, ilen); + writeStream.Flush(); + } + freadStream.Dispose(); + zipFile.CloseWriteStream(Convert.ToUInt32(roms[-index - 1].CRC, 16)); + } + + // Otherwise, copy the file from the old archive + else + { + // Instantiate the streams + oldZipFile.OpenReadStream(index, false, out Stream zreadStream, out ulong istreamSize, out SabreTools.Library.Data.CompressionMethod icompressionMethod, out uint lastMod); + zipFile.OpenWriteStream(false, lastMod == Constants.TorrentZipFileDateTime, oldZipFile.Filename(index), + istreamSize, SabreTools.Library.Data.CompressionMethod.Deflated, out writeStream, lastMod: lastMod); + + // Copy the input stream to the output + byte[] ibuffer = new byte[_bufferSize]; + int ilen; + while ((ilen = zreadStream.Read(ibuffer, 0, _bufferSize)) > 0) + { + writeStream.Write(ibuffer, 0, ilen); + writeStream.Flush(); + } + zipFile.CloseWriteStream(BitConverter.ToUInt32(oldZipFile.CRC32(index), 0)); + } + } + } + + // Close the output zip file + zipFile.Close(); + + success = true; + } + catch (Exception ex) + { + Console.WriteLine(ex); + success = false; + } + finally + { + zipFile.Dispose(); + oldZipFile.Dispose(); + } + + // If the old file exists, delete it and replace + if (File.Exists(archiveFileName)) + { + Utilities.TryDeleteFile(archiveFileName); + } + File.Move(tempFile, archiveFileName); + + return true; + } + + #endregion + } } diff --git a/SabreTools.Library/FileTypes/ZstdArchive.cs b/SabreTools.Library/FileTypes/ZstdArchive.cs index ba68aa0a..d72a9d31 100644 --- a/SabreTools.Library/FileTypes/ZstdArchive.cs +++ b/SabreTools.Library/FileTypes/ZstdArchive.cs @@ -13,152 +13,152 @@ using Stream = System.IO.Stream; namespace SabreTools.Library.FileTypes { - /// - /// Represents a ZstdArchive archive for reading and writing - /// - /// TODO: Implement from source at https://github.com/skbkontur/ZstdNet - public class ZstdArchive : BaseArchive - { - #region Constructors + /// + /// Represents a ZstdArchive archive for reading and writing + /// + /// TODO: Implement from source at https://github.com/skbkontur/ZstdNet + public class ZstdArchive : BaseArchive + { + #region Constructors - /// - /// Create a new ZstdArchive with no base file - /// - public ZstdArchive() - : base() - { - _fileType = FileType.ZstdArchive; - } + /// + /// Create a new ZstdArchive with no base file + /// + public ZstdArchive() + : base() + { + this.Type = FileType.ZstdArchive; + } - /// - /// Create a new ZstdArchive from the given file - /// - /// Name of the file to use as an archive - /// True if hashes for this file should be calculated, false otherwise (default) - public ZstdArchive(string filename, bool getHashes) - : base(filename, getHashes) - { - _fileType = FileType.ZstdArchive; - } + /// + /// Create a new ZstdArchive from the given file + /// + /// Name of the file to use as an archive + /// True if hashes for this file should be calculated, false otherwise (default) + public ZstdArchive(string filename, bool getHashes) + : base(filename, getHashes) + { + this.Type = FileType.ZstdArchive; + } - #endregion + #endregion - #region Extraction + #region Extraction - /// - /// Attempt to extract a file as an archive - /// - /// Output directory for archive extraction - /// True if the extraction was a success, false otherwise - public override bool CopyAll(string outDir) - { - throw new NotImplementedException(); - } + /// + /// Attempt to extract a file as an archive + /// + /// Output directory for archive extraction + /// True if the extraction was a success, false otherwise + public override bool CopyAll(string outDir) + { + throw new NotImplementedException(); + } - /// - /// Attempt to extract a file from an archive - /// - /// Name of the entry to be extracted - /// Output directory for archive extraction - /// Name of the extracted file, null on error - public override string CopyToFile(string entryName, string outDir) - { - throw new NotImplementedException(); - } + /// + /// Attempt to extract a file from an archive + /// + /// Name of the entry to be extracted + /// Output directory for archive extraction + /// Name of the extracted file, null on error + public override string CopyToFile(string entryName, string outDir) + { + throw new NotImplementedException(); + } - /// - /// Attempt to extract a stream from an archive - /// - /// Name of the entry to be extracted - /// Output representing the entry name that was found - /// MemoryStream representing the entry, null on error - public override (MemoryStream, string) CopyToStream(string entryName) - { - throw new NotImplementedException(); - } + /// + /// Attempt to extract a stream from an archive + /// + /// Name of the entry to be extracted + /// Output representing the entry name that was found + /// MemoryStream representing the entry, null on error + public override (MemoryStream, string) CopyToStream(string entryName) + { + throw new NotImplementedException(); + } - #endregion + #endregion - #region Information + #region Information - /// - /// Generate a list of DatItem objects from the header values in an archive - /// - /// Hash representing the hashes that should be skipped - /// True if entry dates should be included, false otherwise (default) - /// List of DatItem objects representing the found data - /// TODO: All instances of Hash.DeepHashes should be made into 0x0 eventually - public override List GetChildren(Hash omitFromScan = Hash.DeepHashes, bool date = false) - { - throw new NotImplementedException(); - } + /// + /// Generate a list of DatItem objects from the header values in an archive + /// + /// Hash representing the hashes that should be skipped + /// True if entry dates should be included, false otherwise (default) + /// List of DatItem objects representing the found data + /// TODO: All instances of Hash.DeepHashes should be made into 0x0 eventually + public override List GetChildren(Hash omitFromScan = Hash.DeepHashes, bool date = false) + { + throw new NotImplementedException(); + } - /// - /// Generate a list of empty folders in an archive - /// - /// Input file to get data from - /// List of empty folders in the archive - public override List GetEmptyFolders() - { - throw new NotImplementedException(); - } + /// + /// Generate a list of empty folders in an archive + /// + /// Input file to get data from + /// List of empty folders in the archive + public override List GetEmptyFolders() + { + throw new NotImplementedException(); + } - /// - /// Check whether the input file is a standardized format - /// - public override bool IsTorrent() - { - throw new NotImplementedException(); - } + /// + /// Check whether the input file is a standardized format + /// + public override bool IsTorrent() + { + throw new NotImplementedException(); + } - #endregion + #endregion - #region Writing + #region Writing - /// - /// Write an input file to a torrent Zstd file - /// - /// Input filename to be moved - /// Output directory to build to - /// DatItem representing the new information - /// True if the date from the DAT should be used if available, false otherwise (default) - /// True if files should be output in Romba depot folders, false otherwise - /// True if the write was a success, false otherwise - /// This works for now, but it can be sped up by using Ionic.Zip or another zlib wrapper that allows for header values built-in. See edc's code. - public override bool Write(string inputFile, string outDir, Rom rom, bool date = false, bool romba = false) - { - throw new NotImplementedException(); - } + /// + /// Write an input file to a torrent Zstd file + /// + /// Input filename to be moved + /// Output directory to build to + /// DatItem representing the new information + /// True if the date from the DAT should be used if available, false otherwise (default) + /// True if files should be output in Romba depot folders, false otherwise + /// True if the write was a success, false otherwise + /// This works for now, but it can be sped up by using Ionic.Zip or another zlib wrapper that allows for header values built-in. See edc's code. + public override bool Write(string inputFile, string outDir, Rom rom, bool date = false, bool romba = false) + { + throw new NotImplementedException(); + } - /// - /// Write an input stream to a torrent Zstd file - /// - /// Input stream to be moved - /// Output directory to build to - /// DatItem representing the new information - /// True if the date from the DAT should be used if available, false otherwise (default) - /// True if files should be output in Romba depot folders, false otherwise - /// True if the write was a success, false otherwise - /// This works for now, but it can be sped up by using Ionic.Zip or another zlib wrapper that allows for header values built-in. See edc's code. - public override bool Write(Stream inputStream, string outDir, Rom rom, bool date = false, bool romba = false) - { - throw new NotImplementedException(); - } + /// + /// Write an input stream to a torrent Zstd file + /// + /// Input stream to be moved + /// Output directory to build to + /// DatItem representing the new information + /// True if the date from the DAT should be used if available, false otherwise (default) + /// True if files should be output in Romba depot folders, false otherwise + /// True if the write was a success, false otherwise + /// This works for now, but it can be sped up by using Ionic.Zip or another zlib wrapper that allows for header values built-in. See edc's code. + public override bool Write(Stream inputStream, string outDir, Rom rom, bool date = false, bool romba = false) + { + throw new NotImplementedException(); + } - /// - /// Write a set of input files to a torrent Zstd archive (assuming the same output archive name) - /// - /// Input files to be moved - /// Output directory to build to - /// DatItem representing the new information - /// True if the date from the DAT should be used if available, false otherwise (default) - /// True if files should be output in Romba depot folders, false otherwise - /// True if the archive was written properly, false otherwise - public override bool Write(List inputFiles, string outDir, List roms, bool date = false, bool romba = false) - { - throw new NotImplementedException(); - } + /// + /// Write a set of input files to a torrent Zstd archive (assuming the same output archive name) + /// + /// Input files to be moved + /// Output directory to build to + /// DatItem representing the new information + /// True if the date from the DAT should be used if available, false otherwise (default) + /// True if files should be output in Romba depot folders, false otherwise + /// True if the archive was written properly, false otherwise + public override bool Write(List inputFiles, string outDir, List roms, bool date = false, bool romba = false) + { + throw new NotImplementedException(); + } - #endregion - } + #endregion + } }