2025-01-04 21:17:02 -05:00
using System.IO ;
using SabreTools.FileTypes.Aaru ;
using SabreTools.FileTypes.Archives ;
using SabreTools.FileTypes.CHD ;
using SabreTools.Hashing ;
using SabreTools.IO.Extensions ;
using SabreTools.Matching ;
using SabreTools.Skippers ;
2025-01-04 21:23:56 -05:00
using static SabreTools . FileTypes . Constants ;
2025-01-04 21:17:02 -05:00
namespace SabreTools.FileTypes
{
public static class FileTypeTool
{
#region File Info
2025-01-04 21:32:46 -05:00
/// <summary>
/// Retrieve file information for a single file
/// </summary>
/// <param name="input">Filename to get information from</param>
/// <param name="hashes">Hashes to include in the information</param>
2025-01-04 22:50:36 -05:00
/// <returns>Populated BaseFile object if success, empty on error</returns>
public static BaseFile GetInfo ( string input , HashType [ ] hashes )
2025-01-05 22:19:42 -05:00
= > GetInfo ( input , hashes , header : null ) ;
2025-01-04 21:32:46 -05:00
2025-01-04 21:17:02 -05:00
/// <summary>
/// Retrieve file information for a single file
/// </summary>
/// <param name="input">Filename to get information from</param>
/// <param name="hashes">Hashes to include in the information</param>
2025-01-05 22:19:42 -05:00
/// <param name="header">Populated string representing the name of the skipper to use, a blank string to use the first available checker, null otherwise</param>
2025-01-04 22:50:36 -05:00
/// <returns>Populated BaseFile object if success, empty on error</returns>
2025-01-05 22:19:42 -05:00
public static BaseFile GetInfo ( string input , HashType [ ] hashes , string? header )
2025-01-04 21:17:02 -05:00
{
// Add safeguard if file doesn't exist
if ( ! File . Exists ( input ) )
2025-01-04 22:50:36 -05:00
return new BaseFile ( ) ;
2025-01-04 21:17:02 -05:00
2025-01-04 23:26:06 -05:00
try
{
// Get input information
var fileType = GetFileType ( input ) ;
Stream inputStream = GetInfoStream ( input , header ) ;
2025-01-05 21:35:06 -05:00
BaseFile ? baseFile = GetBaseFile ( inputStream , fileType , hashes ) ;
2025-01-04 21:17:02 -05:00
2025-01-04 23:26:06 -05:00
// Dispose of the input stream
2025-01-05 20:38:52 -05:00
inputStream . Dispose ( ) ;
2025-01-04 21:17:02 -05:00
2025-01-04 23:26:06 -05:00
// Add unique data from the file
baseFile ! . Filename = Path . GetFileName ( input ) ;
baseFile . Date = new FileInfo ( input ) . LastWriteTime . ToString ( "yyyy/MM/dd HH:mm:ss" ) ;
2025-01-04 21:17:02 -05:00
2025-01-04 23:26:06 -05:00
return baseFile ;
}
catch
{
// Exceptions are currently not logged
// TODO: Log exceptions
return new BaseFile ( ) ;
}
2025-01-04 21:17:02 -05:00
}
2025-01-04 22:50:36 -05:00
/// <summary>
/// Retrieve file information for a single stream
/// </summary>
/// <param name="input">Stream to get information from</param>
/// <param name="hashes">Hashes to include in the information</param>
/// <returns>Populated BaseFile object if success, null on error</returns>
public static BaseFile GetInfo ( Stream ? input , HashType [ ] hashes )
= > GetInfo ( input , size : - 1 , hashes , keepReadOpen : false ) ;
/// <summary>
/// Retrieve file information for a single stream
/// </summary>
/// <param name="input">Stream to get information from</param>
/// <param name="size">Size of the input stream</param>
/// <param name="hashes">Hashes to include in the information</param>
/// <returns>Populated BaseFile object if success, null on error</returns>
public static BaseFile GetInfo ( Stream ? input , long size , HashType [ ] hashes )
= > GetInfo ( input , size , hashes , keepReadOpen : false ) ;
/// <summary>
/// Retrieve file information for a single stream
/// </summary>
/// <param name="input">Stream to get information from</param>
/// <param name="hashes">Hashes to include in the information</param>
/// <param name="keepReadOpen">Indicates if the underlying read stream should be kept open</param>
/// <returns>Populated BaseFile object if success, null on error</returns>
public static BaseFile GetInfo ( Stream ? input , HashType [ ] hashes , bool keepReadOpen )
= > GetInfo ( input , size : - 1 , hashes , keepReadOpen ) ;
2025-01-04 21:17:02 -05:00
/// <summary>
/// Retrieve file information for a single file
/// </summary>
2025-01-04 22:50:36 -05:00
/// <param name="input">Stream to get information from</param>
2025-01-04 21:17:02 -05:00
/// <param name="size">Size of the input stream</param>
/// <param name="hashes">Hashes to include in the information</param>
2025-01-04 22:50:36 -05:00
/// <param name="keepReadOpen">Indicates if the underlying read stream should be kept open</param>
2025-01-04 21:17:02 -05:00
/// <returns>Populated BaseFile object if success, empty one on error</returns>
2025-01-04 23:52:16 -05:00
public static BaseFile GetInfo ( Stream ? input , long size , HashType [ ] hashes , bool keepReadOpen )
2025-01-04 21:17:02 -05:00
{
// If we have no stream
if ( input = = null )
return new BaseFile ( ) ;
2025-01-04 23:26:06 -05:00
try
2025-01-04 21:17:02 -05:00
{
2025-01-04 23:26:06 -05:00
// If we want to automatically set the size
if ( size = = - 1 )
size = input . Length ;
// Run the hashing on the input stream
var hashDict = HashTool . GetStreamHashes ( input , hashes ) ;
if ( hashDict = = null )
return new BaseFile ( ) ;
// Create a base file with the resulting hashes
var baseFile = new BaseFile ( )
{
Size = size ,
CRC = hashDict . ContainsKey ( HashType . CRC32 ) ? hashDict [ HashType . CRC32 ] . FromHexString ( ) : null ,
MD5 = hashDict . ContainsKey ( HashType . MD5 ) ? hashDict [ HashType . MD5 ] . FromHexString ( ) : null ,
SHA1 = hashDict . ContainsKey ( HashType . SHA1 ) ? hashDict [ HashType . SHA1 ] . FromHexString ( ) : null ,
SHA256 = hashDict . ContainsKey ( HashType . SHA256 ) ? hashDict [ HashType . SHA256 ] . FromHexString ( ) : null ,
SHA384 = hashDict . ContainsKey ( HashType . SHA384 ) ? hashDict [ HashType . SHA384 ] . FromHexString ( ) : null ,
SHA512 = hashDict . ContainsKey ( HashType . SHA512 ) ? hashDict [ HashType . SHA512 ] . FromHexString ( ) : null ,
SpamSum = hashDict . ContainsKey ( HashType . SpamSum ) ? hashDict [ HashType . SpamSum ] . FromHexString ( ) : null ,
} ;
// Deal with the input stream
if ( ! keepReadOpen )
{
input . Close ( ) ;
input . Dispose ( ) ;
}
else
{
input . SeekIfPossible ( ) ;
}
return baseFile ;
2025-01-04 21:17:02 -05:00
}
2025-01-04 23:26:06 -05:00
catch
2025-01-04 21:17:02 -05:00
{
2025-01-04 23:26:06 -05:00
// Exceptions are currently not logged
// TODO: Log exceptions
return new BaseFile ( ) ;
2025-01-04 21:17:02 -05:00
}
}
2025-01-05 21:41:16 -05:00
/// <summary>
/// Copy all missing information from one BaseFile to another
/// </summary>
private static void CopyInformation ( BaseFile from , BaseFile to )
{
to . Filename ? ? = from . Filename ;
to . Parent ? ? = from . Parent ;
to . Date ? ? = from . Date ;
to . Size ? ? = from . Size ;
to . CRC ? ? = from . CRC ;
to . MD5 ? ? = from . MD5 ;
to . SHA1 ? ? = from . SHA1 ;
to . SHA256 ? ? = from . SHA256 ;
to . SHA384 ? ? = from . SHA384 ;
to . SHA512 ? ? = from . SHA512 ;
to . SpamSum ? ? = from . SpamSum ;
}
2025-01-05 20:38:52 -05:00
/// <summary>
/// Get the correct base file based on the type and filter options
/// </summary>
2025-01-05 21:35:06 -05:00
private static BaseFile ? GetBaseFile ( Stream inputStream , FileType ? fileType , HashType [ ] hashes )
2025-01-05 20:38:52 -05:00
{
2025-01-05 21:35:06 -05:00
// Get external file information
BaseFile ? baseFile = GetInfo ( inputStream , hashes , keepReadOpen : true ) ;
// Get internal hashes, if they exist
if ( fileType = = FileType . AaruFormat )
{
AaruFormat ? aif = AaruFormat . Create ( inputStream ) ;
if ( aif ! = null )
{
2025-01-05 21:41:16 -05:00
CopyInformation ( baseFile , aif ) ;
2025-01-05 21:35:06 -05:00
return aif ;
}
}
else if ( fileType = = FileType . CHD )
{
CHDFile ? chd = CHDFile . Create ( inputStream ) ;
if ( chd ! = null )
{
2025-01-05 21:41:16 -05:00
CopyInformation ( baseFile , chd ) ;
2025-01-05 21:35:06 -05:00
return chd ;
}
}
return baseFile ;
2025-01-05 20:38:52 -05:00
}
2025-01-04 23:23:22 -05:00
/// <summary>
/// Get the required stream for info hashing
/// </summary>
private static Stream GetInfoStream ( string input , string? header )
{
// Open the file directly
Stream inputStream = File . Open ( input , FileMode . Open , FileAccess . Read , FileShare . ReadWrite ) ;
if ( header = = null )
return inputStream ;
// Try to match the supplied header skipper
SkipperMatch . Init ( ) ;
var rule = SkipperMatch . GetMatchingRule ( input , Path . GetFileNameWithoutExtension ( header ) ) ;
// If there's no match, return the original stream
if ( rule . Tests = = null | | rule . Tests . Length = = 0 )
return inputStream ;
// Transform the stream and get the information from it
var outputStream = new MemoryStream ( ) ;
rule . TransformStream ( inputStream , outputStream , keepReadOpen : false , keepWriteOpen : true ) ;
return outputStream ;
}
2025-01-04 21:17:02 -05:00
#endregion
#region File Type
/// <summary>
/// Create an archive object from a filename, if possible
/// </summary>
/// <param name="input">Name of the file to create the archive from</param>
/// <returns>Archive object representing the inputs</returns>
public static BaseArchive ? CreateArchiveType ( string input )
{
FileType ? fileType = GetFileType ( input ) ;
return fileType switch
{
FileType . GZipArchive = > new GZipArchive ( input ) ,
FileType . RarArchive = > new RarArchive ( input ) ,
FileType . SevenZipArchive = > new SevenZipArchive ( input ) ,
FileType . TapeArchive = > new TapeArchive ( input ) ,
FileType . ZipArchive = > new ZipArchive ( input ) ,
_ = > null ,
} ;
}
/// <summary>
2025-01-04 22:09:53 -05:00
/// Create an IFolder object of the specified type, if possible
2025-01-04 21:17:02 -05:00
/// </summary>
/// <param name="outputFormat">OutputFormat representing the archive to create</param>
2025-01-04 22:09:53 -05:00
/// <returns>IFolder object representing the inputs</returns>
2025-01-04 22:10:52 -05:00
public static IParent ? CreateFolderType ( OutputFormat outputFormat )
2025-01-04 21:17:02 -05:00
{
return outputFormat switch
{
OutputFormat . Folder = > new Folder ( false ) ,
OutputFormat . ParentFolder = > new Folder ( true ) ,
OutputFormat . TapeArchive = > new TapeArchive ( ) ,
OutputFormat . Torrent7Zip = > new SevenZipArchive ( ) ,
OutputFormat . TorrentGzip = > new GZipArchive ( ) ,
OutputFormat . TorrentGzipRomba = > new GZipArchive ( ) ,
OutputFormat . TorrentRar = > new RarArchive ( ) ,
OutputFormat . TorrentXZ = > new XZArchive ( ) ,
OutputFormat . TorrentXZRomba = > new XZArchive ( ) ,
OutputFormat . TorrentZip = > new ZipArchive ( ) ,
_ = > null ,
} ;
}
/// <summary>
/// Returns the file type of an input file
/// </summary>
/// <param name="input">Input file to check</param>
/// <returns>FileType of inputted file (null on error)</returns>
public static FileType ? GetFileType ( string input )
{
// If the file is null, then we have no archive type
2025-01-04 23:30:45 -05:00
if ( string . IsNullOrEmpty ( input ) )
return null ;
2025-01-04 21:17:02 -05:00
// First line of defense is going to be the extension, for better or worse
if ( ! HasValidArchiveExtension ( input ) )
2025-01-04 23:30:45 -05:00
return null ;
2025-01-04 21:17:02 -05:00
// Read the first bytes of the file and get the magic number
2025-01-04 23:30:45 -05:00
byte [ ] magic ;
try
{
using Stream stream = File . Open ( input , FileMode . Open , FileAccess . Read , FileShare . ReadWrite ) ;
magic = stream . ReadBytes ( 8 ) ;
}
catch
{
// Exceptions are currently not logged
// TODO: Log exceptions
return null ;
}
2025-01-04 21:17:02 -05:00
// Now try to match it to a known signature
if ( magic . StartsWith ( SevenZipSignature ) )
{
2025-01-04 23:30:45 -05:00
return FileType . SevenZipArchive ;
2025-01-04 21:17:02 -05:00
}
else if ( magic . StartsWith ( AaruFormatSignature ) )
{
2025-01-04 23:30:45 -05:00
return FileType . AaruFormat ;
2025-01-04 21:17:02 -05:00
}
else if ( magic . StartsWith ( CHDSignature ) )
{
2025-01-04 23:30:45 -05:00
return FileType . CHD ;
2025-01-04 21:17:02 -05:00
}
else if ( magic . StartsWith ( GzSignature ) )
{
2025-01-04 23:30:45 -05:00
return FileType . GZipArchive ;
2025-01-04 21:17:02 -05:00
}
else if ( magic . StartsWith ( RarSignature )
| | magic . StartsWith ( RarFiveSignature ) )
{
2025-01-04 23:30:45 -05:00
return FileType . RarArchive ;
2025-01-04 21:17:02 -05:00
}
else if ( magic . StartsWith ( TarSignature )
| | magic . StartsWith ( TarZeroSignature ) )
{
2025-01-04 23:30:45 -05:00
return FileType . TapeArchive ;
2025-01-04 21:17:02 -05:00
}
else if ( magic . StartsWith ( XZSignature ) )
{
2025-01-04 23:30:45 -05:00
return FileType . XZArchive ;
2025-01-04 21:17:02 -05:00
}
else if ( magic . StartsWith ( Models . PKZIP . Constants . LocalFileHeaderSignatureBytes )
| | magic . StartsWith ( Models . PKZIP . Constants . EndOfCentralDirectoryRecordSignatureBytes )
| | magic . StartsWith ( Models . PKZIP . Constants . DataDescriptorSignatureBytes ) )
{
2025-01-04 23:30:45 -05:00
return FileType . ZipArchive ;
2025-01-04 21:17:02 -05:00
}
2025-01-04 23:30:45 -05:00
return null ;
2025-01-04 21:17:02 -05:00
}
/// <summary>
/// Get if the given path has a valid DAT extension
/// </summary>
/// <param name="path">Path to check</param>
/// <returns>True if the extension is valid, false otherwise</returns>
private static bool HasValidArchiveExtension ( string path )
{
// Get the extension from the path, if possible
string? ext = path . GetNormalizedExtension ( ) ;
// Check against the list of known archive extensions
return ext switch
{
// Aaruformat
"aaru" = > true ,
"aaruf" = > true ,
"aaruformat" = > true ,
"aif" = > true ,
"dicf" = > true ,
// Archive
"7z" = > true ,
"gz" = > true ,
"lzma" = > true ,
"rar" = > true ,
"rev" = > true ,
"r00" = > true ,
"r01" = > true ,
"tar" = > true ,
"tgz" = > true ,
"tlz" = > true ,
"zip" = > true ,
"zipx" = > true ,
// CHD
"chd" = > true ,
_ = > false ,
} ;
}
#endregion
}
}