2020-07-15 09:41:59 -07:00
using System ;
using System.IO ;
using System.Text ;
using System.Text.RegularExpressions ;
using System.Xml ;
using System.Xml.Schema ;
using SabreTools.Library.Data ;
2020-08-01 22:10:29 -07:00
using SabreTools.Library.DatFiles ;
2020-07-15 09:41:59 -07:00
using SabreTools.Library.FileTypes ;
using SabreTools.Library.Skippers ;
2020-08-01 23:04:11 -07:00
namespace SabreTools.Library.IO
2020-07-15 09:41:59 -07:00
{
/// <summary>
/// Extensions to File functionality
/// </summary>
public static class FileExtensions
{
/// <summary>
/// Add an aribtrary number of bytes to the inputted file
/// </summary>
/// <param name="input">File to be appended to</param>
/// <param name="output">Outputted file</param>
/// <param name="bytesToAddToHead">Bytes to be added to head of file</param>
/// <param name="bytesToAddToTail">Bytes to be added to tail of file</param>
public static void AppendBytes ( string input , string output , byte [ ] bytesToAddToHead , byte [ ] bytesToAddToTail )
{
// If any of the inputs are invalid, skip
if ( ! File . Exists ( input ) )
return ;
#if NET_FRAMEWORK
using ( FileStream fsr = TryOpenRead ( input ) )
using ( FileStream fsw = TryOpenWrite ( output ) )
{
#else
using FileStream fsr = TryOpenRead ( input ) ;
using FileStream fsw = TryOpenWrite ( output ) ;
#endif
StreamExtensions . AppendBytes ( fsr , fsw , bytesToAddToHead , bytesToAddToTail ) ;
#if NET_FRAMEWORK
}
#endif
}
/// <summary>
/// Get what type of DAT the input file is
/// </summary>
/// <param name="filename">Name of the file to be parsed</param>
/// <returns>The DatFormat corresponding to the DAT</returns>
public static DatFormat GetDatFormat ( this string filename )
{
// Limit the output formats based on extension
if ( ! PathExtensions . HasValidDatExtension ( filename ) )
return 0 ;
// Get the extension from the filename
string ext = PathExtensions . GetNormalizedExtension ( filename ) ;
// Read the input file, if possible
Globals . Logger . Verbose ( $"Attempting to read file to get format: {filename}" ) ;
// Check if file exists
if ( ! File . Exists ( filename ) )
{
Globals . Logger . Warning ( $"File '{filename}' could not read from!" ) ;
return 0 ;
}
// Some formats should only require the extension to know
switch ( ext )
{
case "csv" :
return DatFormat . CSV ;
case "json" :
2020-09-07 22:40:27 -07:00
return DatFormat . SabreJSON ;
2020-07-15 09:41:59 -07:00
case "md5" :
return DatFormat . RedumpMD5 ;
#if NET_FRAMEWORK
case "ripemd160" :
return DatFormat . RedumpRIPEMD160 ;
#endif
case "sfv" :
return DatFormat . RedumpSFV ;
case "sha1" :
return DatFormat . RedumpSHA1 ;
case "sha256" :
return DatFormat . RedumpSHA256 ;
case "sha384" :
return DatFormat . RedumpSHA384 ;
case "sha512" :
return DatFormat . RedumpSHA512 ;
2020-09-04 15:02:15 -07:00
case "spamsum" :
return DatFormat . RedumpSpamSum ;
2020-07-15 09:41:59 -07:00
case "ssv" :
return DatFormat . SSV ;
case "tsv" :
return DatFormat . TSV ;
}
// For everything else, we need to read it
try
{
2020-07-28 16:17:54 -07:00
// Get the first two non-whitespace, non-comment lines to check, if possible
string first = string . Empty , second = string . Empty ;
2020-07-15 09:41:59 -07:00
2020-07-28 16:17:54 -07:00
try
{
using ( StreamReader sr = File . OpenText ( filename ) )
{
first = sr . ReadLine ( ) . ToLowerInvariant ( ) ;
while ( ( string . IsNullOrWhiteSpace ( first ) | | first . StartsWith ( "<!--" ) )
& & ! sr . EndOfStream )
{
first = sr . ReadLine ( ) . ToLowerInvariant ( ) ;
}
2020-07-15 09:41:59 -07:00
2020-07-28 16:17:54 -07:00
if ( ! sr . EndOfStream )
{
second = sr . ReadLine ( ) . ToLowerInvariant ( ) ;
while ( string . IsNullOrWhiteSpace ( second ) | | second . StartsWith ( "<!--" )
& & ! sr . EndOfStream )
{
second = sr . ReadLine ( ) . ToLowerInvariant ( ) ;
}
}
}
}
catch { }
2020-07-15 09:41:59 -07:00
// If we have an XML-based DAT
if ( first . Contains ( "<?xml" ) & & first . Contains ( "?>" ) )
{
if ( second . StartsWith ( "<!doctype datafile" ) )
return DatFormat . Logiqx ;
else if ( second . StartsWith ( "<!doctype mame" )
| | second . StartsWith ( "<!doctype m1" )
| | second . StartsWith ( "<mame" )
| | second . StartsWith ( "<m1" ) )
return DatFormat . Listxml ;
else if ( second . StartsWith ( "<!doctype softwaredb" ) )
return DatFormat . OpenMSX ;
else if ( second . StartsWith ( "<!doctype softwarelist" ) )
return DatFormat . SoftwareList ;
else if ( second . StartsWith ( "<!doctype sabredat" ) )
2020-09-07 22:57:44 -07:00
return DatFormat . SabreXML ;
2020-07-15 09:41:59 -07:00
else if ( ( second . StartsWith ( "<dat" ) & & ! second . StartsWith ( "<datafile" ) )
| | second . StartsWith ( "<?xml-stylesheet" ) )
return DatFormat . OfflineList ;
// Older and non-compliant DATs
else
return DatFormat . Logiqx ;
}
// If we have an SMDB (SHA-256, Filename, SHA-1, MD5, CRC32)
else if ( Regex . IsMatch ( first , @"[0-9a-f]{64}\t.*?\t[0-9a-f]{40}\t[0-9a-f]{32}\t[0-9a-f]{8}" ) )
return DatFormat . EverdriveSMDB ;
// If we have an INI-based DAT
else if ( first . Contains ( "[" ) & & first . Contains ( "]" ) )
return DatFormat . RomCenter ;
// If we have a listroms DAT
else if ( first . StartsWith ( "roms required for driver" ) )
return DatFormat . Listrom ;
// If we have a CMP-based DAT
else if ( first . Contains ( "clrmamepro" ) )
return DatFormat . ClrMamePro ;
else if ( first . Contains ( "romvault" ) )
return DatFormat . ClrMamePro ;
else if ( first . Contains ( "doscenter" ) )
return DatFormat . DOSCenter ;
else if ( first . Contains ( "#Name;Title;Emulator;CloneOf;Year;Manufacturer;Category;Players;Rotation;Control;Status;DisplayCount;DisplayType;AltRomname;AltTitle;Extra" ) )
return DatFormat . AttractMode ;
else
return DatFormat . ClrMamePro ;
}
2020-09-15 12:12:13 -07:00
catch ( Exception ex )
2020-07-15 09:41:59 -07:00
{
2020-09-15 17:09:35 -07:00
Globals . Logger . Warning ( ex , $"An exception occurred trying to figure out the format of '{filename}'" ) ;
2020-07-15 09:41:59 -07:00
return 0 ;
}
}
/// <summary>
/// Determines a text file's encoding by analyzing its byte order mark (BOM).
/// Defaults to ASCII when detection of the text file's endianness fails.
/// </summary>
/// <param name="filename">The text file to analyze.</param>
/// <returns>The detected encoding.</returns>
/// <link>http://stackoverflow.com/questions/3825390/effective-way-to-find-any-files-encoding</link>
public static Encoding GetEncoding ( string filename )
{
// Read the BOM
var bom = new byte [ 4 ] ;
FileStream file = FileExtensions . TryOpenRead ( filename ) ;
file . Read ( bom , 0 , 4 ) ;
file . Dispose ( ) ;
// Analyze the BOM
if ( bom [ 0 ] = = 0x2b & & bom [ 1 ] = = 0x2f & & bom [ 2 ] = = 0x76 ) return Encoding . UTF7 ;
if ( bom [ 0 ] = = 0xef & & bom [ 1 ] = = 0xbb & & bom [ 2 ] = = 0xbf ) return Encoding . UTF8 ;
if ( bom [ 0 ] = = 0xff & & bom [ 1 ] = = 0xfe ) return Encoding . Unicode ; //UTF-16LE
if ( bom [ 0 ] = = 0xfe & & bom [ 1 ] = = 0xff ) return Encoding . BigEndianUnicode ; //UTF-16BE
if ( bom [ 0 ] = = 0 & & bom [ 1 ] = = 0 & & bom [ 2 ] = = 0xfe & & bom [ 3 ] = = 0xff ) return Encoding . UTF32 ;
return Encoding . Default ;
}
/// <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 ( this string input )
{
FileType ? outFileType = null ;
// If the file is null, then we have no archive type
if ( input = = null )
return outFileType ;
// First line of defense is going to be the extension, for better or worse
if ( ! PathExtensions . HasValidArchiveExtension ( input ) )
return outFileType ;
// Read the first bytes of the file and get the magic number
try
{
byte [ ] magic = new byte [ 8 ] ;
BinaryReader br = new BinaryReader ( TryOpenRead ( input ) ) ;
magic = br . ReadBytes ( 8 ) ;
br . Dispose ( ) ;
// Now try to match it to a known signature
if ( magic . StartsWith ( Constants . SevenZipSignature ) )
{
outFileType = FileType . SevenZipArchive ;
}
2020-08-27 16:57:22 -07:00
else if ( magic . StartsWith ( Constants . AaruFormatSignature ) )
{
outFileType = FileType . AaruFormat ;
}
2020-07-15 09:41:59 -07:00
else if ( magic . StartsWith ( Constants . CHDSignature ) )
{
outFileType = FileType . CHD ;
}
else if ( magic . StartsWith ( Constants . GzSignature ) )
{
outFileType = FileType . GZipArchive ;
}
else if ( magic . StartsWith ( Constants . LRZipSignature ) )
{
outFileType = FileType . LRZipArchive ;
}
else if ( magic . StartsWith ( Constants . LZ4Signature )
| | magic . StartsWith ( Constants . LZ4SkippableMinSignature )
| | magic . StartsWith ( Constants . LZ4SkippableMaxSignature ) )
{
outFileType = FileType . LZ4Archive ;
}
else if ( magic . StartsWith ( Constants . RarSignature )
| | magic . StartsWith ( Constants . RarFiveSignature ) )
{
outFileType = FileType . RarArchive ;
}
else if ( magic . StartsWith ( Constants . TarSignature )
| | magic . StartsWith ( Constants . TarZeroSignature ) )
{
outFileType = FileType . TapeArchive ;
}
else if ( magic . StartsWith ( Constants . XZSignature ) )
{
outFileType = FileType . XZArchive ;
}
else if ( magic . StartsWith ( Constants . ZipSignature )
| | magic . StartsWith ( Constants . ZipSignatureEmpty )
| | magic . StartsWith ( Constants . ZipSignatureSpanned ) )
{
outFileType = FileType . ZipArchive ;
}
else if ( magic . StartsWith ( Constants . ZPAQSignature ) )
{
outFileType = FileType . ZPAQArchive ;
}
else if ( magic . StartsWith ( Constants . ZstdSignature ) )
{
outFileType = FileType . ZstdArchive ;
}
}
2020-09-15 12:12:13 -07:00
catch ( Exception ex )
2020-07-15 09:41:59 -07:00
{
2020-09-15 17:09:35 -07:00
Globals . Logger . Warning ( ex , $"An exception occurred determining file type of '{input}'" ) ;
2020-07-15 09:41:59 -07:00
}
return outFileType ;
}
/// <summary>
/// Returns if the first byte array starts with the second array
/// </summary>
/// <param name="arr1">First byte array to compare</param>
/// <param name="arr2">Second byte array to compare</param>
/// <param name="exact">True if the input arrays should match exactly, false otherwise (default)</param>
/// <returns>True if the first byte array starts with the second, false otherwise</returns>
2020-08-27 16:57:22 -07:00
public static bool StartsWith ( this byte [ ] arr1 , byte [ ] arr2 , bool exact = false )
2020-07-15 09:41:59 -07:00
{
// If we have any invalid inputs, we return false
if ( arr1 = = null | | arr2 = = null
| | arr1 . Length = = 0 | | arr2 . Length = = 0
| | arr2 . Length > arr1 . Length
| | ( exact & & arr1 . Length ! = arr2 . Length ) )
{
return false ;
}
// Otherwise, loop through and see
for ( int i = 0 ; i < arr2 . Length ; i + + )
{
if ( arr1 [ i ] ! = arr2 [ i ] )
return false ;
}
return true ;
}
/// <summary>
/// Retrieve file information for a single file
/// </summary>
/// <param name="input">Filename to get information from</param>
/// <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>
2020-08-27 20:56:50 -07:00
/// <param name="asFiles">TreatAsFiles representing special format scanning</param>
2020-07-15 09:41:59 -07:00
/// <returns>Populated BaseFile object if success, empty one on error</returns>
2020-09-18 11:26:50 -07:00
public static BaseFile GetInfo ( string input , string header = null , TreatAsFile asFiles = 0x00 )
2020-07-15 09:41:59 -07:00
{
// Add safeguard if file doesn't exist
if ( ! File . Exists ( input ) )
return null ;
2020-09-17 23:07:49 -07:00
// Get input information
var fileType = input . GetFileType ( ) ;
2020-09-18 00:45:08 -07:00
Stream inputStream = TryOpenRead ( input ) ;
2020-09-17 23:07:49 -07:00
2020-09-18 00:45:08 -07:00
// Try to match the supplied header skipper
2020-07-15 09:41:59 -07:00
if ( header ! = null )
{
2020-07-30 21:07:25 -07:00
SkipperRule rule = Transform . GetMatchingRule ( input , Path . GetFileNameWithoutExtension ( header ) ) ;
2020-07-15 09:41:59 -07:00
2020-09-18 00:45:08 -07:00
// If there's a match, transform the stream before getting info
2020-07-15 09:41:59 -07:00
if ( rule . Tests ! = null & & rule . Tests . Count ! = 0 )
{
2020-09-17 23:07:49 -07:00
// Create the output stream
2020-07-15 09:41:59 -07:00
MemoryStream outputStream = new MemoryStream ( ) ;
2020-09-18 00:45:08 -07:00
2020-07-15 09:41:59 -07:00
// Transform the stream and get the information from it
rule . TransformStream ( inputStream , outputStream , keepReadOpen : false , keepWriteOpen : true ) ;
2020-09-18 00:45:08 -07:00
inputStream = outputStream ;
2020-07-15 09:41:59 -07:00
}
}
2020-09-18 00:45:08 -07:00
// Get the info in the proper manner
BaseFile baseFile ;
if ( fileType = = FileType . AaruFormat & & ! asFiles . HasFlag ( TreatAsFile . AaruFormat ) )
baseFile = AaruFormat . Create ( inputStream ) ;
else if ( fileType = = FileType . CHD & & ! asFiles . HasFlag ( TreatAsFile . CHD ) )
baseFile = CHDFile . Create ( inputStream ) ;
2020-07-15 09:41:59 -07:00
else
2020-09-18 00:45:08 -07:00
baseFile = inputStream . GetInfo ( keepReadOpen : false ) ;
2020-07-15 09:41:59 -07:00
2020-09-17 23:07:49 -07:00
// Dispose of the input stream
2020-09-18 00:45:08 -07:00
inputStream ? . Dispose ( ) ;
2020-09-17 23:07:49 -07:00
2020-07-15 09:41:59 -07:00
// Add unique data from the file
baseFile . Filename = Path . GetFileName ( input ) ;
2020-09-18 11:26:50 -07:00
baseFile . Date = new FileInfo ( input ) . LastWriteTime . ToString ( "yyyy/MM/dd HH:mm:ss" ) ;
2020-07-15 09:41:59 -07:00
return baseFile ;
}
/// <summary>
/// Get the IniReader associated with a file, if possible
/// </summary>
/// <param name="filename">Name of the file to be parsed</param>
/// <param name="validateRows">True if rows should be in a proper format, false if invalid is okay</param>
/// <returns>The IniReader representing the (possibly converted) file, null otherwise</returns>
public static IniReader GetIniReader ( this string filename , bool validateRows )
{
Globals . Logger . Verbose ( $"Attempting to read file: {filename}" ) ;
// Check if file exists
if ( ! File . Exists ( filename ) )
{
Globals . Logger . Warning ( $"File '{filename}' could not read from!" ) ;
return null ;
}
IniReader ir = new IniReader ( filename )
{
ValidateRows = validateRows
} ;
return ir ;
}
/// <summary>
/// Get the XmlTextReader associated with a file, if possible
/// </summary>
/// <param name="filename">Name of the file to be parsed</param>
/// <returns>The XmlTextReader representing the (possibly converted) file, null otherwise</returns>
public static XmlReader GetXmlTextReader ( this string filename )
{
Globals . Logger . Verbose ( $"Attempting to read file: {filename}" ) ;
// Check if file exists
if ( ! File . Exists ( filename ) )
{
Globals . Logger . Warning ( $"File '{filename}' could not read from!" ) ;
return null ;
}
XmlReader xtr = XmlReader . Create ( filename , new XmlReaderSettings
{
CheckCharacters = false ,
DtdProcessing = DtdProcessing . Ignore ,
IgnoreComments = true ,
IgnoreWhitespace = true ,
ValidationFlags = XmlSchemaValidationFlags . None ,
ValidationType = ValidationType . None ,
} ) ;
return xtr ;
}
/// <summary>
/// Try to create a file for write, optionally throwing the error
/// </summary>
/// <param name="file">Name of the file to create</param>
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
/// <returns>An opened stream representing the file on success, null otherwise</returns>
public static FileStream TryCreate ( string file , bool throwOnError = false )
{
// Now wrap opening the file
try
{
return File . Open ( file , FileMode . Create , FileAccess . Write , FileShare . ReadWrite ) ;
}
catch ( Exception ex )
{
if ( throwOnError )
throw ex ;
else
return null ;
}
}
/// <summary>
/// Try to safely delete a file, optionally throwing the error
/// </summary>
/// <param name="file">Name of the file to delete</param>
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
/// <returns>True if the file didn't exist or could be deleted, false otherwise</returns>
public static bool TryDelete ( string file , bool throwOnError = false )
{
// Check if the file exists first
if ( ! File . Exists ( file ) )
return true ;
// Now wrap deleting the file
try
{
File . Delete ( file ) ;
return true ;
}
catch ( Exception ex )
{
if ( throwOnError )
throw ex ;
else
return false ;
}
}
/// <summary>
/// Try to open a file for read, optionally throwing the error
/// </summary>
/// <param name="file">Name of the file to open</param>
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
/// <returns>An opened stream representing the file on success, null otherwise</returns>
public static FileStream TryOpenRead ( string file , bool throwOnError = false )
{
// Check if the file exists first
if ( ! File . Exists ( file ) )
return null ;
// Now wrap opening the file
try
{
return File . Open ( file , FileMode . Open , FileAccess . Read , FileShare . ReadWrite ) ;
}
catch ( Exception ex )
{
if ( throwOnError )
throw ex ;
else
return null ;
}
}
/// <summary>
/// Try to open a file for read/write, optionally throwing the error
/// </summary>
/// <param name="file">Name of the file to open</param>
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
/// <returns>An opened stream representing the file on success, null otherwise</returns>
public static FileStream TryOpenReadWrite ( string file , bool throwOnError = false )
{
// Check if the file exists first
if ( ! File . Exists ( file ) )
return null ;
// Now wrap opening the file
try
{
return File . Open ( file , FileMode . Open , FileAccess . ReadWrite , FileShare . ReadWrite ) ;
}
catch ( Exception ex )
{
if ( throwOnError )
throw ex ;
else
return null ;
}
}
/// <summary>
/// Try to open an existing file for write, optionally throwing the error
/// </summary>
/// <param name="file">Name of the file to open</param>
/// <param name="throwOnError">True if the error that is thrown should be thrown back to the caller, false otherwise</param>
/// <returns>An opened stream representing the file on success, null otherwise</returns>
public static FileStream TryOpenWrite ( string file , bool throwOnError = false )
{
// Check if the file exists first
if ( ! File . Exists ( file ) )
return null ;
// Now wrap opening the file
try
{
return File . Open ( file , FileMode . Open , FileAccess . Write , FileShare . ReadWrite ) ;
}
catch ( Exception ex )
{
if ( throwOnError )
throw ex ;
else
return null ;
}
}
}
}