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-10-05 20:39:06 -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
{
2020-09-18 15:01:03 -07:00
#region Fields
/// <summary>
/// Positive value for depth of the output depot, defaults to 4
/// </summary>
public int Depth { get ; set ; } = 4 ;
#endregion
2019-02-08 20:51:44 -08:00
#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 ;
}
2020-09-15 12:12:13 -07:00
catch ( EndOfStreamException ex )
2019-02-08 20:51:44 -08:00
{
// Catch this but don't count it as an error because SharpCompress is unsafe
2020-09-15 17:09:35 -07:00
Globals . Logger . Verbose ( ex ) ;
2019-02-08 20:51:44 -08:00
}
2020-09-15 12:12:13 -07:00
catch ( InvalidOperationException ex )
2019-02-08 20:51:44 -08:00
{
2020-09-15 17:09:35 -07:00
Globals . Logger . Warning ( ex ) ;
2019-02-08 20:51:44 -08:00
encounteredErrors = true ;
}
catch ( Exception ex )
{
2020-09-15 14:38:37 -07:00
Globals . Logger . Error ( ex ) ;
2019-02-08 20:51:44 -08:00
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 )
{
2020-09-15 14:38:37 -07:00
Globals . Logger . Error ( ex ) ;
2019-02-08 20:51:44 -08:00
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>
/// <returns>List of DatItem objects representing the found data</returns>
2020-09-18 11:26:50 -07:00
public override List < BaseFile > GetChildren ( )
2019-02-08 20:51:44 -08:00
{
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
{
2020-10-05 20:39:06 -07:00
// Create a blank item for the entry
BaseFile xzEntryRom = new BaseFile ( ) ;
2020-09-18 01:50:44 -07:00
// Perform a quickscan, if flagged to
2020-10-05 20:39:06 -07:00
if ( this . AvailableHashes = = Hash . CRC )
2020-07-15 09:41:59 -07:00
{
2020-10-05 20:39:06 -07:00
xzEntryRom . Filename = gamename ;
using ( BinaryReader br = new BinaryReader ( FileExtensions . TryOpenRead ( this . Filename ) ) )
2020-07-15 09:41:59 -07:00
{
2020-10-05 20:39:06 -07:00
br . BaseStream . Seek ( - 8 , SeekOrigin . End ) ;
xzEntryRom . CRC = br . ReadBytesBigEndian ( 4 ) ;
xzEntryRom . Size = br . ReadInt32BigEndian ( ) ;
}
2020-07-15 09:41:59 -07:00
}
// Otherwise, use the stream directly
else
{
var xzStream = new XZStream ( File . OpenRead ( this . Filename ) ) ;
2020-10-05 20:39:06 -07:00
xzEntryRom = xzStream . GetInfo ( hashes : this . AvailableHashes ) ;
2020-07-15 09:41:59 -07:00
xzEntryRom . Filename = gamename ;
xzStream . Dispose ( ) ;
}
2020-10-05 20:39:06 -07:00
// Fill in comon details and add to the list
xzEntryRom . Parent = gamename ;
_children . Add ( xzEntryRom ) ;
2020-07-15 09:41:59 -07:00
}
catch ( Exception ex )
{
2020-09-15 14:38:37 -07:00
Globals . Logger . Error ( ex ) ;
2020-07-15 09:41:59 -07:00
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>
/// <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-09-18 15:01:03 -07:00
public override bool Write ( string inputFile , string outDir , Rom rom )
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-09-18 15:01:03 -07:00
return Write ( FileExtensions . TryOpenRead ( inputFile ) , outDir , rom ) ;
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>
/// <returns>True if the archive was written properly, false otherwise</returns>
2020-09-18 15:01:03 -07:00
public override bool Write ( Stream inputStream , string outDir , Rom rom )
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-09-18 15:01:03 -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>
/// <returns>True if the archive was written properly, false otherwise</returns>
2020-09-18 15:01:03 -07:00
public override bool Write ( List < string > inputFiles , string outDir , List < Rom > roms )
2019-02-08 20:51:44 -08:00
{
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
}