2017-11-02 00:29:20 -07:00
using System ;
using System.Collections.Generic ;
2020-06-10 22:37:19 -07:00
using System.IO ;
2020-07-15 09:41:59 -07:00
using System.Text.RegularExpressions ;
2017-11-02 00:29:20 -07:00
using SabreTools.Library.Data ;
2020-08-01 22:10:29 -07:00
using SabreTools.Library.DatFiles ;
2017-11-02 15:44:15 -07:00
using SabreTools.Library.DatItems ;
2020-08-01 23:04:11 -07:00
using SabreTools.Library.IO ;
2017-11-02 00:29:20 -07:00
using SabreTools.Library.Tools ;
2020-07-15 09:41:59 -07:00
using SharpCompress.Compressors.Xz ;
2017-11-02 00:29:20 -07:00
namespace SabreTools.Library.FileTypes
{
2019-02-08 20:51:44 -08:00
/// <summary>
/// Represents a TorrentXZ archive for reading and writing
/// </summary>
public class XZArchive : BaseArchive
{
#region Constructors
/// <summary>
/// Create a new TorrentGZipArchive with no base file
/// </summary>
public XZArchive ( )
: base ( )
{
this . Type = FileType . XZArchive ;
}
/// <summary>
/// Create a new TorrentGZipArchive from the given file
/// </summary>
/// <param name="filename">Name of the file to use as an archive</param>
/// <param name="read">True for opening file as read, false for opening file as write</param>
/// <param name="getHashes">True if hashes for this file should be calculated, false otherwise (default)</param>
public XZArchive ( string filename , bool getHashes = false )
: base ( filename , getHashes )
{
this . Type = FileType . XZArchive ;
}
#endregion
#region Extraction
/// <summary>
/// Attempt to extract a file as an archive
/// </summary>
/// <param name="outDir">Output directory for archive extraction</param>
/// <returns>True if the extraction was a success, false otherwise</returns>
public override bool CopyAll ( string outDir )
{
bool encounteredErrors = true ;
try
{
// Create the temp directory
Directory . CreateDirectory ( outDir ) ;
2020-07-15 09:41:59 -07:00
// Decompress the _filename stream
FileStream outstream = FileExtensions . TryCreate ( Path . Combine ( outDir , Path . GetFileNameWithoutExtension ( this . Filename ) ) ) ;
var xz = new XZStream ( File . OpenRead ( this . Filename ) ) ;
xz . CopyTo ( outstream ) ;
// Dispose of the streams
outstream . Dispose ( ) ;
xz . Dispose ( ) ;
2019-02-08 20:51:44 -08:00
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 ;
}
/// <summary>
/// Attempt to extract a file from an archive
/// </summary>
/// <param name="entryName">Name of the entry to be extracted</param>
/// <param name="outDir">Output directory for archive extraction</param>
/// <returns>Name of the extracted file, null on error</returns>
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
2020-07-15 09:41:59 -07:00
FileStream fs = FileExtensions . TryCreate ( realEntry ) ;
2019-02-08 20:51:44 -08:00
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 ;
}
/// <summary>
/// Attempt to extract a stream from an archive
/// </summary>
/// <param name="entryName">Name of the entry to be extracted</param>
/// <param name="realEntry">Output representing the entry name that was found</param>
/// <returns>MemoryStream representing the entry, null on error</returns>
public override ( MemoryStream , string ) CopyToStream ( string entryName )
{
MemoryStream ms = new MemoryStream ( ) ;
2020-07-15 09:41:59 -07:00
string realEntry ;
2019-02-08 20:51:44 -08:00
try
{
2020-07-15 09:41:59 -07:00
// Decompress the _filename stream
realEntry = Path . GetFileNameWithoutExtension ( this . Filename ) ;
var xz = new XZStream ( File . OpenRead ( this . Filename ) ) ;
// Write the file out
byte [ ] xbuffer = new byte [ _bufferSize ] ;
int xlen ;
while ( ( xlen = xz . Read ( xbuffer , 0 , _bufferSize ) ) > 0 )
2019-02-08 20:51:44 -08:00
{
2020-07-15 09:41:59 -07:00
ms . Write ( xbuffer , 0 , xlen ) ;
ms . Flush ( ) ;
2019-02-08 20:51:44 -08:00
}
2020-07-15 09:41:59 -07:00
// Dispose of the streams
xz . Dispose ( ) ;
2019-02-08 20:51:44 -08:00
}
catch ( Exception ex )
{
Globals . Logger . Error ( ex . ToString ( ) ) ;
ms = null ;
realEntry = null ;
}
return ( ms , realEntry ) ;
}
#endregion
#region Information
/// <summary>
/// Generate a list of DatItem objects from the header values in an archive
/// </summary>
/// <param name="omitFromScan">Hash representing the hashes that should be skipped</param>
/// <param name="date">True if entry dates should be included, false otherwise (default)</param>
/// <returns>List of DatItem objects representing the found data</returns>
/// <remarks>TODO: All instances of Hash.DeepHashes should be made into 0x0 eventually</remarks>
public override List < BaseFile > GetChildren ( Hash omitFromScan = Hash . DeepHashes , bool date = false )
{
2020-07-15 09:41:59 -07:00
if ( _children = = null | | _children . Count = = 0 )
{
_children = new List < BaseFile > ( ) ;
string gamename = Path . GetFileNameWithoutExtension ( this . Filename ) ;
BaseFile possibleTxz = GetTorrentXZFileInfo ( ) ;
// If it was, then add it to the outputs and continue
if ( possibleTxz ! = null & & possibleTxz . Filename ! = null )
{
_children . Add ( possibleTxz ) ;
}
else
{
try
{
// If secure hashes are disabled, do a quickscan
if ( omitFromScan = = Hash . SecureHashes )
{
BaseFile tempRom = new BaseFile ( )
{
Filename = gamename ,
} ;
BinaryReader br = new BinaryReader ( FileExtensions . TryOpenRead ( this . Filename ) ) ;
br . BaseStream . Seek ( - 8 , SeekOrigin . End ) ;
tempRom . CRC = br . ReadBytesBigEndian ( 4 ) ;
tempRom . Size = br . ReadInt32BigEndian ( ) ;
br . Dispose ( ) ;
_children . Add ( tempRom ) ;
}
// Otherwise, use the stream directly
else
{
var xzStream = new XZStream ( File . OpenRead ( this . Filename ) ) ;
2020-08-28 01:13:55 -07:00
BaseFile xzEntryRom = xzStream . GetInfo ( omitFromScan : omitFromScan , asFiles : TreatAsFiles . AaruFormats | TreatAsFiles . CHDs ) ;
2020-07-15 09:41:59 -07:00
xzEntryRom . Filename = gamename ;
xzEntryRom . Parent = gamename ;
_children . Add ( xzEntryRom ) ;
xzStream . Dispose ( ) ;
}
}
catch ( Exception ex )
{
Globals . Logger . Error ( ex . ToString ( ) ) ;
return null ;
}
}
}
return _children ;
2019-02-08 20:51:44 -08:00
}
/// <summary>
/// Generate a list of empty folders in an archive
/// </summary>
/// <param name="input">Input file to get data from</param>
/// <returns>List of empty folders in the archive</returns>
public override List < string > GetEmptyFolders ( )
{
2020-07-15 09:41:59 -07:00
// XZ files don't contain directories
return new List < string > ( ) ;
2019-02-08 20:51:44 -08:00
}
/// <summary>
/// Check whether the input file is a standardized format
/// </summary>
public override bool IsTorrent ( )
{
2020-07-15 09:41:59 -07:00
// Check for the file existing first
if ( ! File . Exists ( this . Filename ) )
return false ;
string datum = Path . GetFileName ( this . Filename ) . ToLowerInvariant ( ) ;
// Check if the name is the right length
2020-08-31 23:01:51 -07:00
if ( ! Regex . IsMatch ( datum , @"^[0-9a-f]{" + Constants . SHA1Length + @"}\.xz" ) )
2020-07-15 09:41:59 -07:00
{
Globals . Logger . Warning ( $"Non SHA-1 filename found, skipping: '{Path.GetFullPath(this.Filename)}'" ) ;
return false ;
}
return true ;
}
/// <summary>
/// Retrieve file information for a single torrent XZ file
/// </summary>
/// <returns>Populated DatItem object if success, empty one on error</returns>
public BaseFile GetTorrentXZFileInfo ( )
{
// Check for the file existing first
if ( ! File . Exists ( this . Filename ) )
return null ;
string datum = Path . GetFileName ( this . Filename ) . ToLowerInvariant ( ) ;
// Check if the name is the right length
2020-08-31 23:01:51 -07:00
if ( ! Regex . IsMatch ( datum , @"^[0-9a-f]{" + Constants . SHA1Length + @"}\.xz" ) )
2020-07-15 09:41:59 -07:00
{
Globals . Logger . Warning ( $"Non SHA-1 filename found, skipping: '{Path.GetFullPath(this.Filename)}'" ) ;
return null ;
}
BaseFile baseFile = new BaseFile
{
Filename = Path . GetFileNameWithoutExtension ( this . Filename ) . ToLowerInvariant ( ) ,
2020-08-31 23:01:51 -07:00
SHA1 = Utilities . StringToByteArray ( Path . GetFileNameWithoutExtension ( this . Filename ) ) ,
2020-07-15 09:41:59 -07:00
Parent = Path . GetFileNameWithoutExtension ( this . Filename ) . ToLowerInvariant ( ) ,
} ;
return baseFile ;
2019-02-08 20:51:44 -08:00
}
#endregion
#region Writing
/// <summary>
/// Write an input file to a torrent XZ file
/// </summary>
/// <param name="inputFile">Input filename to be moved</param>
/// <param name="outDir">Output directory to build to</param>
/// <param name="rom">DatItem representing the new information</param>
/// <param name="date">True if the date from the DAT should be used if available, false otherwise (default)</param>
2020-08-18 23:39:13 -07:00
/// <param name="depth">Positive value for depth of the output depot, defaults to 4</param>
2019-02-08 20:51:44 -08:00
/// <returns>True if the write was a success, false otherwise</returns>
/// <remarks>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.</remarks>
2020-08-18 23:39:13 -07:00
public override bool Write ( string inputFile , string outDir , Rom rom , bool date = false , int depth = 4 )
2019-02-08 20:51:44 -08:00
{
2020-07-15 09:41:59 -07:00
// Check that the input file exists
if ( ! File . Exists ( inputFile ) )
{
Globals . Logger . Warning ( $"File '{inputFile}' does not exist!" ) ;
return false ;
}
inputFile = Path . GetFullPath ( inputFile ) ;
2019-02-08 20:51:44 -08:00
// Get the file stream for the file and write out
2020-08-18 23:39:13 -07:00
return Write ( FileExtensions . TryOpenRead ( inputFile ) , outDir , rom , date , depth ) ;
2019-02-08 20:51:44 -08:00
}
/// <summary>
/// Write an input file to a torrent XZ archive
/// </summary>
/// <param name="inputStream">Input stream to be moved</param>
/// <param name="outDir">Output directory to build to</param>
/// <param name="rom">DatItem representing the new information</param>
/// <param name="date">True if the date from the DAT should be used if available, false otherwise (default)</param>
2020-08-18 23:39:13 -07:00
/// <param name="depth">Positive value for depth of the output depot, defaults to 4</param>
2019-02-08 20:51:44 -08:00
/// <returns>True if the archive was written properly, false otherwise</returns>
2020-08-18 23:39:13 -07:00
public override bool Write ( Stream inputStream , string outDir , Rom rom , bool date = false , int depth = 4 )
2019-02-08 20:51:44 -08:00
{
bool success = false ;
// If the stream is not readable, return
if ( ! inputStream . CanRead )
return success ;
2020-07-15 09:41:59 -07:00
// Make sure the output directory exists
if ( ! Directory . Exists ( outDir ) )
Directory . CreateDirectory ( outDir ) ;
2019-02-08 20:51:44 -08:00
2020-07-15 09:41:59 -07:00
outDir = Path . GetFullPath ( outDir ) ;
2019-02-08 20:51:44 -08:00
2020-07-15 09:41:59 -07:00
// Now get the Rom info for the file so we have hashes and size
rom = new Rom ( inputStream . GetInfo ( keepReadOpen : true ) ) ;
2019-02-08 20:51:44 -08:00
2020-07-15 09:41:59 -07:00
// Get the output file name
2020-08-31 23:01:51 -07:00
string outfile = Path . Combine ( outDir , PathExtensions . GetDepotPath ( rom . SHA1 , depth ) ) ;
2020-08-18 23:39:13 -07:00
outfile = outfile . Replace ( ".gz" , ".xz" ) ;
2019-02-08 20:51:44 -08:00
2020-08-18 23:39:13 -07:00
// Check to see if the folder needs to be created
if ( ! Directory . Exists ( Path . GetDirectoryName ( outfile ) ) )
Directory . CreateDirectory ( Path . GetDirectoryName ( outfile ) ) ;
2019-02-08 20:51:44 -08:00
2020-07-15 09:41:59 -07:00
// If the output file exists, don't try to write again
if ( ! File . Exists ( outfile ) )
2019-02-08 20:51:44 -08:00
{
2020-07-15 09:41:59 -07:00
// Compress the input stream
XZStream outputStream = new XZStream ( FileExtensions . TryCreate ( outfile ) ) ;
inputStream . CopyTo ( outputStream ) ;
2019-02-08 20:51:44 -08:00
2020-07-15 09:41:59 -07:00
// Dispose of everything
outputStream . Dispose ( ) ;
2019-02-08 20:51:44 -08:00
}
return true ;
}
/// <summary>
/// Write a set of input files to a torrent XZ archive (assuming the same output archive name)
/// </summary>
/// <param name="inputFiles">Input files to be moved</param>
/// <param name="outDir">Output directory to build to</param>
/// <param name="rom">DatItem representing the new information</param>
/// <param name="date">True if the date from the DAT should be used if available, false otherwise (default)</param>
/// <param name="romba">True if files should be output in Romba depot folders, false otherwise</param>
/// <returns>True if the archive was written properly, false otherwise</returns>
public override bool Write ( List < string > inputFiles , string outDir , List < Rom > roms , bool date = false , bool romba = false )
{
2020-07-15 09:41:59 -07:00
throw new NotImplementedException ( ) ;
2019-02-08 20:51:44 -08:00
}
#endregion
}
2017-11-02 00:29:20 -07:00
}