2020-10-30 23:56:27 -07:00
using System ;
2021-07-18 09:44:23 -07:00
using System.Collections.Concurrent ;
2020-10-30 23:56:27 -07:00
using System.Collections.Generic ;
using System.IO ;
using System.Linq ;
2021-03-02 15:10:52 -08:00
using System.Reflection ;
2021-07-18 09:44:23 -07:00
using System.Threading.Tasks ;
2020-10-30 23:56:27 -07:00
using BurnOutSharp.FileType ;
2021-08-25 15:09:42 -07:00
using BurnOutSharp.Tools ;
2020-10-30 23:56:27 -07:00
namespace BurnOutSharp
{
public class Scanner
{
/// <summary>
/// Optional progress callback during scanning
/// </summary>
2020-11-12 22:47:33 -08:00
public IProgress < ProtectionProgress > FileProgress { get ; set ; } = null ;
2020-10-30 23:56:27 -07:00
/// <summary>
2021-08-24 15:19:23 -07:00
/// Determines if debug information is output or not
2020-10-30 23:56:27 -07:00
/// </summary>
2021-08-24 15:19:23 -07:00
public bool IncludeDebug { get ; set ; } = false ;
2020-10-30 23:56:27 -07:00
/// <summary>
/// Determines whether all files are scanned or just executables are
/// </summary>
2022-03-02 10:17:50 -08:00
/// <remarks>
/// With the improvements to executable scannning, this should probably be removed in
/// a future update.
/// </remarks>
2020-10-30 23:56:27 -07:00
public bool ScanAllFiles { get ; set ; } = false ;
/// <summary>
/// Determines whether archives are decompressed and scanned
/// </summary>
public bool ScanArchives { get ; set ; } = true ;
2020-10-31 14:46:08 -07:00
/// <summary>
/// Determines if packers are counted as detected protections or not
/// </summary>
2020-10-31 14:48:25 -07:00
public bool ScanPackers { get ; set ; } = false ;
2020-10-31 14:46:08 -07:00
2021-03-02 15:10:52 -08:00
/// <summary>
/// Cache for all IPathCheck types
/// </summary>
2021-03-23 16:43:23 -07:00
private static readonly IEnumerable < IPathCheck > pathCheckClasses = InitPathCheckClasses ( ) ;
2021-03-02 15:10:52 -08:00
2020-10-30 23:56:27 -07:00
/// <summary>
/// Constructor
/// </summary>
/// <param name="fileProgress">Optional progress callback</param>
2020-11-12 22:47:33 -08:00
public Scanner ( IProgress < ProtectionProgress > fileProgress = null )
2020-10-30 23:56:27 -07:00
{
FileProgress = fileProgress ;
}
2020-10-31 00:06:41 -07:00
/// <summary>
2020-10-31 13:20:54 -07:00
/// Scan a single path and get all found protections
2020-10-31 00:06:41 -07:00
/// </summary>
2020-10-31 13:20:54 -07:00
/// <param name="path">Path to scan</param>
/// <returns>Dictionary of list of strings representing the found protections</returns>
2021-07-18 09:44:23 -07:00
public ConcurrentDictionary < string , ConcurrentQueue < string > > GetProtections ( string path )
2020-10-31 00:06:41 -07:00
{
2020-10-31 13:20:54 -07:00
return GetProtections ( new List < string > { path } ) ;
2020-10-31 00:06:41 -07:00
}
2020-10-30 23:56:27 -07:00
/// <summary>
/// Scan the list of paths and get all found protections
/// </summary>
/// <returns>Dictionary of list of strings representing the found protections</returns>
2021-07-18 09:44:23 -07:00
public ConcurrentDictionary < string , ConcurrentQueue < string > > GetProtections ( List < string > paths )
2020-10-30 23:56:27 -07:00
{
// If we have no paths, we can't scan
2020-10-31 13:20:54 -07:00
if ( paths = = null | | ! paths . Any ( ) )
2020-10-30 23:56:27 -07:00
return null ;
2021-08-25 20:25:45 -07:00
// Set a starting starting time for debug output
DateTime startTime = DateTime . UtcNow ;
2020-10-31 14:00:31 -07:00
// Checkpoint
2020-11-12 22:47:33 -08:00
FileProgress ? . Report ( new ProtectionProgress ( null , 0 , null ) ) ;
2020-10-31 14:00:31 -07:00
// Temp variables for reporting
string tempFilePath = Path . GetTempPath ( ) ;
string tempFilePathWithGuid = Path . Combine ( tempFilePath , Guid . NewGuid ( ) . ToString ( ) ) ;
2020-10-30 23:56:27 -07:00
// Loop through each path and get the returned values
2021-07-18 09:44:23 -07:00
var protections = new ConcurrentDictionary < string , ConcurrentQueue < string > > ( ) ;
2020-10-31 13:20:54 -07:00
foreach ( string path in paths )
2020-10-30 23:56:27 -07:00
{
// Directories scan each internal file individually
if ( Directory . Exists ( path ) )
{
// Enumerate all files at first for easier access
var files = Directory . EnumerateFiles ( path , "*" , SearchOption . AllDirectories ) . ToList ( ) ;
// Scan for path-detectable protections
2021-03-19 15:48:53 -07:00
var directoryPathProtections = GetDirectoryPathProtections ( path , files ) ;
2020-10-31 14:00:31 -07:00
Utilities . AppendToDictionary ( protections , directoryPathProtections ) ;
2020-10-30 23:56:27 -07:00
// Scan each file in directory separately
2021-03-22 21:25:14 -07:00
for ( int i = 0 ; i < files . Count ; i + + )
2020-10-30 23:56:27 -07:00
{
2020-10-31 14:00:31 -07:00
// Get the current file
string file = files . ElementAt ( i ) ;
// Get the reportable file name
string reportableFileName = file ;
if ( reportableFileName . StartsWith ( tempFilePath ) )
reportableFileName = reportableFileName . Substring ( tempFilePathWithGuid . Length ) ;
// Checkpoint
2021-03-22 21:25:14 -07:00
FileProgress ? . Report ( new ProtectionProgress ( reportableFileName , i / ( float ) files . Count , "Checking file" + ( file ! = reportableFileName ? " from archive" : string . Empty ) ) ) ;
2020-10-31 14:00:31 -07:00
2020-10-30 23:56:27 -07:00
// Scan for path-detectable protections
2021-03-19 15:48:53 -07:00
var filePathProtections = GetFilePathProtections ( file ) ;
2020-10-31 14:00:31 -07:00
Utilities . AppendToDictionary ( protections , filePathProtections ) ;
2020-10-30 23:56:27 -07:00
// Scan for content-detectable protections
var fileProtections = GetInternalProtections ( file ) ;
if ( fileProtections ! = null & & fileProtections . Any ( ) )
{
foreach ( string key in fileProtections . Keys )
{
if ( ! protections . ContainsKey ( key ) )
2021-07-18 09:44:23 -07:00
protections [ key ] = new ConcurrentQueue < string > ( ) ;
2020-10-30 23:56:27 -07:00
protections [ key ] . AddRange ( fileProtections [ key ] ) ;
}
}
2020-10-31 14:00:31 -07:00
// Checkpoint
2021-07-18 09:44:23 -07:00
protections . TryGetValue ( file , out ConcurrentQueue < string > fullProtectionList ) ;
2020-10-31 14:00:31 -07:00
string fullProtection = ( fullProtectionList ! = null & & fullProtectionList . Any ( ) ? string . Join ( ", " , fullProtectionList ) : null ) ;
2021-03-22 21:25:14 -07:00
FileProgress ? . Report ( new ProtectionProgress ( reportableFileName , ( i + 1 ) / ( float ) files . Count , fullProtection ? ? string . Empty ) ) ;
2020-10-31 14:00:31 -07:00
}
2020-10-30 23:56:27 -07:00
}
// Scan a single file by itself
else if ( File . Exists ( path ) )
{
2020-10-31 14:00:31 -07:00
// Get the reportable file name
string reportableFileName = path ;
if ( reportableFileName . StartsWith ( tempFilePath ) )
reportableFileName = reportableFileName . Substring ( tempFilePathWithGuid . Length ) ;
// Checkpoint
2020-11-12 22:47:33 -08:00
FileProgress ? . Report ( new ProtectionProgress ( reportableFileName , 0 , "Checking file" + ( path ! = reportableFileName ? " from archive" : string . Empty ) ) ) ;
2020-10-31 14:00:31 -07:00
2020-10-30 23:56:27 -07:00
// Scan for path-detectable protections
2021-03-19 15:48:53 -07:00
var filePathProtections = GetFilePathProtections ( path ) ;
2020-10-31 14:00:31 -07:00
Utilities . AppendToDictionary ( protections , filePathProtections ) ;
2020-10-30 23:56:27 -07:00
// Scan for content-detectable protections
var fileProtections = GetInternalProtections ( path ) ;
if ( fileProtections ! = null & & fileProtections . Any ( ) )
{
foreach ( string key in fileProtections . Keys )
{
if ( ! protections . ContainsKey ( key ) )
2021-07-18 09:44:23 -07:00
protections [ key ] = new ConcurrentQueue < string > ( ) ;
2020-10-30 23:56:27 -07:00
protections [ key ] . AddRange ( fileProtections [ key ] ) ;
}
}
2020-10-31 14:00:31 -07:00
// Checkpoint
2021-07-18 09:44:23 -07:00
protections . TryGetValue ( path , out ConcurrentQueue < string > fullProtectionList ) ;
2020-10-31 14:00:31 -07:00
string fullProtection = ( fullProtectionList ! = null & & fullProtectionList . Any ( ) ? string . Join ( ", " , fullProtectionList ) : null ) ;
2020-11-12 22:47:33 -08:00
FileProgress ? . Report ( new ProtectionProgress ( reportableFileName , 1 , fullProtection ? ? string . Empty ) ) ;
2020-10-30 23:56:27 -07:00
}
// Throw on an invalid path
else
{
2020-10-31 14:14:35 -07:00
Console . WriteLine ( $"{path} is not a directory or file, skipping..." ) ;
//throw new FileNotFoundException($"{path} is not a directory or file, skipping...");
2020-10-30 23:56:27 -07:00
}
}
2020-10-31 21:20:16 -07:00
// Clear out any empty keys
Utilities . ClearEmptyKeys ( protections ) ;
2021-08-25 20:25:45 -07:00
// If we're in debug, output the elasped time to console
if ( IncludeDebug )
Console . WriteLine ( $"Time elapsed: {DateTime.UtcNow.Subtract(startTime)}" ) ;
2020-10-30 23:56:27 -07:00
return protections ;
}
2021-03-19 15:56:07 -07:00
2020-10-30 23:56:27 -07:00
/// <summary>
/// Get the path-detectable protections associated with a single path
/// </summary>
2021-03-19 15:48:53 -07:00
/// <param name="path">Path of the directory to scan</param>
/// <param name="files">Files contained within</param>
2020-10-30 23:56:27 -07:00
/// <returns>Dictionary of list of strings representing the found protections</returns>
2021-07-18 09:44:23 -07:00
private ConcurrentDictionary < string , ConcurrentQueue < string > > GetDirectoryPathProtections ( string path , List < string > files )
2020-10-30 23:56:27 -07:00
{
2021-07-18 09:44:23 -07:00
// Create an empty queue for protections
var protections = new ConcurrentQueue < string > ( ) ;
2021-03-02 15:10:52 -08:00
2021-07-17 22:31:29 -07:00
// Preprocess the list of files
files = files . Select ( f = > f . Replace ( '\\' , '/' ) ) . ToList ( ) ;
2021-03-19 15:48:53 -07:00
// Iterate through all path checks
2021-07-18 09:44:23 -07:00
Parallel . ForEach ( pathCheckClasses , pathCheckClass = >
2021-03-02 15:10:52 -08:00
{
2021-07-18 09:44:23 -07:00
ConcurrentQueue < string > protection = pathCheckClass . CheckDirectoryPath ( path , files ) ;
2021-03-23 13:35:12 -07:00
if ( protection ! = null )
protections . AddRange ( protection ) ;
2021-07-18 09:44:23 -07:00
} ) ;
2021-03-02 15:10:52 -08:00
2021-03-19 15:48:53 -07:00
// Create and return the dictionary
2021-07-18 09:44:23 -07:00
return new ConcurrentDictionary < string , ConcurrentQueue < string > >
2021-03-19 15:48:53 -07:00
{
[path] = protections
} ;
}
/// <summary>
/// Get the path-detectable protections associated with a single path
/// </summary>
/// <param name="path">Path of the file to scan</param>
/// <returns>Dictionary of list of strings representing the found protections</returns>
2021-07-18 09:44:23 -07:00
private ConcurrentDictionary < string , ConcurrentQueue < string > > GetFilePathProtections ( string path )
2021-03-19 15:48:53 -07:00
{
2021-07-18 09:44:23 -07:00
// Create an empty queue for protections
var protections = new ConcurrentQueue < string > ( ) ;
2021-03-19 15:48:53 -07:00
2021-03-02 15:10:52 -08:00
// Iterate through all path checks
2021-07-18 09:44:23 -07:00
Parallel . ForEach ( pathCheckClasses , pathCheckClass = >
2021-03-02 15:10:52 -08:00
{
2021-07-17 22:31:29 -07:00
string protection = pathCheckClass . CheckFilePath ( path . Replace ( "\\" , "/" ) ) ;
2021-03-02 15:10:52 -08:00
if ( ! string . IsNullOrWhiteSpace ( protection ) )
2021-07-18 09:44:23 -07:00
protections . Enqueue ( protection ) ;
} ) ;
2020-10-30 23:56:27 -07:00
// Create and return the dictionary
2021-07-18 09:44:23 -07:00
return new ConcurrentDictionary < string , ConcurrentQueue < string > >
2020-10-30 23:56:27 -07:00
{
[path] = protections
} ;
}
/// <summary>
/// Get the content-detectable protections associated with a single path
/// </summary>
/// <param name="file">Path to the file to scan</param>
/// <returns>Dictionary of list of strings representing the found protections</returns>
2021-07-18 09:44:23 -07:00
private ConcurrentDictionary < string , ConcurrentQueue < string > > GetInternalProtections ( string file )
2020-10-30 23:56:27 -07:00
{
// Quick sanity check before continuing
if ( ! File . Exists ( file ) )
return null ;
2022-03-15 21:30:46 -07:00
// Initialize the protections found
2021-07-18 09:44:23 -07:00
var protections = new ConcurrentDictionary < string , ConcurrentQueue < string > > ( ) ;
2020-10-30 23:56:27 -07:00
// Get the extension for certain checks
string extension = Path . GetExtension ( file ) . ToLower ( ) . TrimStart ( '.' ) ;
// Open the file and begin scanning
2021-09-01 22:22:14 -07:00
try
2020-10-30 23:56:27 -07:00
{
2021-09-01 22:22:14 -07:00
using ( FileStream fs = File . OpenRead ( file ) )
2020-10-30 23:56:27 -07:00
{
2021-09-01 22:22:14 -07:00
// Get the first 16 bytes for matching
byte [ ] magic = new byte [ 16 ] ;
try
2020-10-31 00:06:41 -07:00
{
2021-09-01 22:22:14 -07:00
fs . Read ( magic , 0 , 16 ) ;
2021-09-13 23:16:57 -07:00
fs . Seek ( 0 , SeekOrigin . Begin ) ;
2020-10-31 00:06:41 -07:00
}
2021-09-01 22:22:14 -07:00
catch
2020-10-31 00:06:41 -07:00
{
2021-09-01 22:22:14 -07:00
// We don't care what the issue was, we can't read or seek the file
return null ;
2020-10-31 00:06:41 -07:00
}
2020-10-30 23:56:27 -07:00
2021-09-01 22:22:14 -07:00
#region Non - Archive File Types
2020-10-30 23:56:27 -07:00
2021-09-01 22:22:14 -07:00
// Executable
if ( ScanAllFiles | | new Executable ( ) . ShouldScan ( magic ) )
2020-10-31 00:06:41 -07:00
{
2021-09-01 22:22:14 -07:00
var subProtections = new Executable ( ) . Scan ( this , fs , file ) ;
2020-10-31 14:00:31 -07:00
Utilities . AppendToDictionary ( protections , subProtections ) ;
2021-07-21 13:40:32 -07:00
}
2021-09-01 22:22:14 -07:00
// Text-based files
if ( ScanAllFiles | | new Textfile ( ) . ShouldScan ( magic , extension ) )
2021-07-21 13:40:32 -07:00
{
2021-09-01 22:22:14 -07:00
var subProtections = new Textfile ( ) . Scan ( this , fs , file ) ;
2021-07-21 13:40:32 -07:00
Utilities . AppendToDictionary ( protections , subProtections ) ;
2020-10-31 00:06:41 -07:00
}
2020-10-30 23:56:27 -07:00
2021-09-01 22:22:14 -07:00
#endregion
2020-10-30 23:56:27 -07:00
2021-09-01 22:22:14 -07:00
#region Archive File Types
2020-10-30 23:56:27 -07:00
2021-09-01 22:22:14 -07:00
// If we're scanning archives, we have a few to try out
if ( ScanArchives )
2020-10-31 00:06:41 -07:00
{
2021-09-01 22:22:14 -07:00
// 7-Zip archive
if ( new SevenZip ( ) . ShouldScan ( magic ) )
{
var subProtections = new SevenZip ( ) . Scan ( this , fs , file ) ;
Utilities . PrependToKeys ( subProtections , file ) ;
Utilities . AppendToDictionary ( protections , subProtections ) ;
}
2020-10-30 23:56:27 -07:00
2021-09-01 22:22:14 -07:00
// BFPK archive
if ( new BFPK ( ) . ShouldScan ( magic ) )
{
var subProtections = new BFPK ( ) . Scan ( this , fs , file ) ;
Utilities . PrependToKeys ( subProtections , file ) ;
Utilities . AppendToDictionary ( protections , subProtections ) ;
}
2020-10-30 23:56:27 -07:00
2021-09-01 22:22:14 -07:00
// BZip2
if ( new BZip2 ( ) . ShouldScan ( magic ) )
{
var subProtections = new BZip2 ( ) . Scan ( this , fs , file ) ;
Utilities . PrependToKeys ( subProtections , file ) ;
Utilities . AppendToDictionary ( protections , subProtections ) ;
}
2020-10-30 23:56:27 -07:00
2021-09-01 22:22:14 -07:00
// GZIP
if ( new GZIP ( ) . ShouldScan ( magic ) )
{
var subProtections = new GZIP ( ) . Scan ( this , fs , file ) ;
Utilities . PrependToKeys ( subProtections , file ) ;
Utilities . AppendToDictionary ( protections , subProtections ) ;
}
2020-10-30 23:56:27 -07:00
2021-09-01 22:22:14 -07:00
// InstallShield Archive V3 (Z)
if ( file ! = null & & new InstallShieldArchiveV3 ( ) . ShouldScan ( magic ) )
{
var subProtections = new InstallShieldArchiveV3 ( ) . Scan ( this , file ) ;
Utilities . PrependToKeys ( subProtections , file ) ;
Utilities . AppendToDictionary ( protections , subProtections ) ;
}
2020-10-30 23:56:27 -07:00
2021-09-01 22:22:14 -07:00
// InstallShield Cabinet
if ( file ! = null & & new InstallShieldCAB ( ) . ShouldScan ( magic ) )
{
var subProtections = new InstallShieldCAB ( ) . Scan ( this , file ) ;
Utilities . PrependToKeys ( subProtections , file ) ;
Utilities . AppendToDictionary ( protections , subProtections ) ;
}
2020-10-30 23:56:27 -07:00
2021-09-01 22:22:14 -07:00
// Microsoft Cabinet
if ( file ! = null & & new MicrosoftCAB ( ) . ShouldScan ( magic ) )
{
var subProtections = new MicrosoftCAB ( ) . Scan ( this , file ) ;
Utilities . PrependToKeys ( subProtections , file ) ;
Utilities . AppendToDictionary ( protections , subProtections ) ;
}
// MSI
if ( file ! = null & & new MSI ( ) . ShouldScan ( magic ) )
{
var subProtections = new MSI ( ) . Scan ( this , file ) ;
Utilities . PrependToKeys ( subProtections , file ) ;
Utilities . AppendToDictionary ( protections , subProtections ) ;
}
// MPQ archive
if ( file ! = null & & new MPQ ( ) . ShouldScan ( magic ) )
{
var subProtections = new MPQ ( ) . Scan ( this , file ) ;
Utilities . PrependToKeys ( subProtections , file ) ;
Utilities . AppendToDictionary ( protections , subProtections ) ;
}
// PKZIP archive (and derivatives)
if ( new PKZIP ( ) . ShouldScan ( magic ) )
{
var subProtections = new PKZIP ( ) . Scan ( this , fs , file ) ;
Utilities . PrependToKeys ( subProtections , file ) ;
Utilities . AppendToDictionary ( protections , subProtections ) ;
}
// RAR archive
if ( new RAR ( ) . ShouldScan ( magic ) )
{
var subProtections = new RAR ( ) . Scan ( this , fs , file ) ;
Utilities . PrependToKeys ( subProtections , file ) ;
Utilities . AppendToDictionary ( protections , subProtections ) ;
}
// Tape Archive
if ( new TapeArchive ( ) . ShouldScan ( magic ) )
{
var subProtections = new TapeArchive ( ) . Scan ( this , fs , file ) ;
Utilities . PrependToKeys ( subProtections , file ) ;
Utilities . AppendToDictionary ( protections , subProtections ) ;
}
// Valve archive formats
if ( file ! = null & & new Valve ( ) . ShouldScan ( magic ) )
{
var subProtections = new Valve ( ) . Scan ( this , file ) ;
Utilities . PrependToKeys ( subProtections , file ) ;
Utilities . AppendToDictionary ( protections , subProtections ) ;
}
// XZ
if ( new XZ ( ) . ShouldScan ( magic ) )
{
var subProtections = new XZ ( ) . Scan ( this , fs , file ) ;
Utilities . PrependToKeys ( subProtections , file ) ;
Utilities . AppendToDictionary ( protections , subProtections ) ;
}
2020-10-31 00:06:41 -07:00
}
2020-10-30 23:56:27 -07:00
2021-09-01 22:22:14 -07:00
#endregion
}
}
2021-09-10 13:59:35 -07:00
catch ( Exception ex )
2021-09-01 22:22:14 -07:00
{
Utilities . AppendToDictionary ( protections , file , "[Exception opening file, please try again]" ) ;
2020-10-30 23:56:27 -07:00
}
2021-09-01 22:22:14 -07:00
2020-10-30 23:56:27 -07:00
2020-10-31 21:20:16 -07:00
// Clear out any empty keys
Utilities . ClearEmptyKeys ( protections ) ;
2020-10-30 23:56:27 -07:00
return protections ;
}
2021-03-19 15:48:53 -07:00
/// <summary>
/// Initialize all IPathCheck implementations
/// </summary>
2021-03-23 10:04:09 -07:00
private static IEnumerable < IPathCheck > InitPathCheckClasses ( )
2021-03-19 15:48:53 -07:00
{
2021-03-23 10:04:09 -07:00
return Assembly . GetExecutingAssembly ( ) . GetTypes ( )
. Where ( t = > t . IsClass & & t . GetInterface ( nameof ( IPathCheck ) ) ! = null )
. Select ( t = > Activator . CreateInstance ( t ) as IPathCheck ) ;
2021-03-19 15:48:53 -07:00
}
2020-10-30 23:56:27 -07:00
}
}