using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; using BurnOutSharp.Interfaces; using BinaryObjectScanner.Interfaces; using BinaryObjectScanner.Utilities; using static BinaryObjectScanner.Utilities.Dictionary; namespace BurnOutSharp { public class Scanner { #region Options /// public bool ScanArchives => options?.ScanArchives ?? false; /// public bool ScanContents => options?.ScanContents ?? false; /// public bool ScanPackers => options?.ScanPackers ?? false; /// public bool ScanPaths => options?.ScanPaths ?? false; /// public bool IncludeDebug => options?.IncludeDebug ?? false; /// /// Options object for configuration /// private readonly Options options; #endregion /// /// Optional progress callback during scanning /// private readonly IProgress fileProgress; /// /// Constructor /// /// Enable scanning archive contents /// Enable including content detections in output /// Enable including packers in output /// Enable including path detections in output /// Enable including debug information /// Optional progress callback public Scanner(bool scanArchives, bool scanContents, bool scanPackers, bool scanPaths, bool includeDebug, IProgress fileProgress = null) { this.options = new Options { ScanArchives = scanArchives, ScanContents = scanContents, ScanPackers = scanPackers, ScanPaths = scanPaths, IncludeDebug = includeDebug, }; this.fileProgress = fileProgress; // Register the codepages Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); } #region Scanning /// /// Scan a single path and get all found protections /// /// Path to scan /// Dictionary of list of strings representing the found protections public ConcurrentDictionary> GetProtections(string path) { return GetProtections(new List { path }); } /// /// Scan the list of paths and get all found protections /// /// Dictionary of list of strings representing the found protections public ConcurrentDictionary> GetProtections(List paths) { // If we have no paths, we can't scan if (paths == null || !paths.Any()) return null; // Set a starting starting time for debug output DateTime startTime = DateTime.UtcNow; // Checkpoint this.fileProgress?.Report(new ProtectionProgress(null, 0, null)); // Temp variables for reporting string tempFilePath = Path.GetTempPath(); string tempFilePathWithGuid = Path.Combine(tempFilePath, Guid.NewGuid().ToString()); // Loop through each path and get the returned values var protections = new ConcurrentDictionary>(); foreach (string path in paths) { // 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 if (ScanPaths) { var directoryPathProtections = GetDirectoryPathProtections(path, files); AppendToDictionary(protections, directoryPathProtections); } // Scan each file in directory separately for (int i = 0; i < files.Count; i++) { // 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 this.fileProgress?.Report(new ProtectionProgress(reportableFileName, i / (float)files.Count, "Checking file" + (file != reportableFileName ? " from archive" : string.Empty))); // Scan for path-detectable protections if (ScanPaths) { var filePathProtections = GetFilePathProtections(file); AppendToDictionary(protections, filePathProtections); } // Scan for content-detectable protections var fileProtections = GetInternalProtections(file); if (fileProtections != null && fileProtections.Any()) { foreach (string key in fileProtections.Keys) { if (!protections.ContainsKey(key)) protections[key] = new ConcurrentQueue(); protections[key].AddRange(fileProtections[key]); } } // Checkpoint protections.TryGetValue(file, out ConcurrentQueue fullProtectionList); string fullProtection = (fullProtectionList != null && fullProtectionList.Any() ? string.Join(", ", fullProtectionList) : null); this.fileProgress?.Report(new ProtectionProgress(reportableFileName, (i + 1) / (float)files.Count, fullProtection ?? string.Empty)); } } // Scan a single file by itself else if (File.Exists(path)) { // Get the reportable file name string reportableFileName = path; if (reportableFileName.StartsWith(tempFilePath)) reportableFileName = reportableFileName.Substring(tempFilePathWithGuid.Length); // Checkpoint this.fileProgress?.Report(new ProtectionProgress(reportableFileName, 0, "Checking file" + (path != reportableFileName ? " from archive" : string.Empty))); // Scan for path-detectable protections if (ScanPaths) { var filePathProtections = GetFilePathProtections(path); AppendToDictionary(protections, filePathProtections); } // Scan for content-detectable protections var fileProtections = GetInternalProtections(path); if (fileProtections != null && fileProtections.Any()) { foreach (string key in fileProtections.Keys) { if (!protections.ContainsKey(key)) protections[key] = new ConcurrentQueue(); protections[key].AddRange(fileProtections[key]); } } // Checkpoint protections.TryGetValue(path, out ConcurrentQueue fullProtectionList); string fullProtection = (fullProtectionList != null && fullProtectionList.Any() ? string.Join(", ", fullProtectionList) : null); this.fileProgress?.Report(new ProtectionProgress(reportableFileName, 1, fullProtection ?? string.Empty)); } // Throw on an invalid path else { Console.WriteLine($"{path} is not a directory or file, skipping..."); //throw new FileNotFoundException($"{path} is not a directory or file, skipping..."); } } // Clear out any empty keys ClearEmptyKeys(protections); // If we're in debug, output the elasped time to console if (IncludeDebug) Console.WriteLine($"Time elapsed: {DateTime.UtcNow.Subtract(startTime)}"); return protections; } /// /// Get the path-detectable protections associated with a single path /// /// Path of the directory to scan /// Files contained within /// Dictionary of list of strings representing the found protections private ConcurrentDictionary> GetDirectoryPathProtections(string path, List files) { // Create an empty queue for protections var protections = new ConcurrentQueue(); // Preprocess the list of files files = files.Select(f => f.Replace('\\', '/')).ToList(); // Iterate through all path checks Parallel.ForEach(ScanningClasses.PathCheckClasses, pathCheckClass => { ConcurrentQueue protection = pathCheckClass.CheckDirectoryPath(path, files); if (protection != null) protections.AddRange(protection); }); // Create and return the dictionary return new ConcurrentDictionary> { [path] = protections }; } /// /// Get the path-detectable protections associated with a single path /// /// Path of the file to scan /// Dictionary of list of strings representing the found protections private ConcurrentDictionary> GetFilePathProtections(string path) { // Create an empty queue for protections var protections = new ConcurrentQueue(); // Iterate through all path checks Parallel.ForEach(ScanningClasses.PathCheckClasses, pathCheckClass => { string protection = pathCheckClass.CheckFilePath(path.Replace("\\", "/")); if (!string.IsNullOrWhiteSpace(protection)) protections.Enqueue(protection); }); // Create and return the dictionary return new ConcurrentDictionary> { [path] = protections }; } /// /// Get the content-detectable protections associated with a single path /// /// Path to the file to scan /// Dictionary of list of strings representing the found protections private ConcurrentDictionary> GetInternalProtections(string file) { // Quick sanity check before continuing if (!File.Exists(file)) return null; // Open the file and begin scanning try { using (FileStream fs = File.Open(file, FileMode.Open, FileAccess.Read, FileShare.Read)) { return GetInternalProtections(file, fs); } } catch (Exception ex) { if (IncludeDebug) Console.WriteLine(ex); var protections = new ConcurrentDictionary>(); AppendToDictionary(protections, file, IncludeDebug ? ex.ToString() : "[Exception opening file, please try again]"); ClearEmptyKeys(protections); return protections; } } /// /// Get the content-detectable protections associated with a single path /// /// Name of the source file of the stream, for tracking /// Stream to scan the contents of /// Dictionary of list of strings representing the found protections private ConcurrentDictionary> GetInternalProtections(string fileName, Stream stream) { // Quick sanity check before continuing if (stream == null || !stream.CanRead || !stream.CanSeek) return null; // Initialize the protections found var protections = new ConcurrentDictionary>(); // Get the extension for certain checks string extension = Path.GetExtension(fileName).ToLower().TrimStart('.'); // Open the file and begin scanning try { // Get the first 16 bytes for matching byte[] magic = new byte[16]; try { stream.Read(magic, 0, 16); stream.Seek(0, SeekOrigin.Begin); } catch (Exception ex) { if (IncludeDebug) Console.WriteLine(ex); return null; } // Get the file type either from magic number or extension SupportedFileType fileType = Tools.FileTypeTools.GetFileType(magic); if (fileType == SupportedFileType.UNKNOWN) fileType = Tools.FileTypeTools.GetFileType(extension); // If we still got unknown, just return null if (fileType == SupportedFileType.UNKNOWN) return null; #region Non-Archive File Types // Create a scannable for the given file type var scannable = CreateScannable(fileType); // If we're scanning file contents if (scannable != null && ScanContents) { var subProtections = scannable.Scan(this, stream, fileName); AppendToDictionary(protections, subProtections); } #endregion #region Archive File Types // Create an extractable for the given file type var extractable = CreateExtractable(fileType); // If we're scanning archives if (extractable != null && ScanArchives) { // If the extractable file itself fails try { // Extract and get the output path string tempPath = extractable.Extract(stream, fileName, IncludeDebug); if (tempPath == null) return null; // Collect and format all found protections var subProtections = GetProtections(tempPath); // If temp directory cleanup fails try { Directory.Delete(tempPath, true); } catch (Exception ex) { if (IncludeDebug) Console.WriteLine(ex); } // Prepare the returned protections StripFromKeys(protections, tempPath); PrependToKeys(subProtections, fileName); AppendToDictionary(protections, subProtections); return protections; } catch (Exception ex) { if (IncludeDebug) Console.WriteLine(ex); } } #endregion } catch (Exception ex) { if (IncludeDebug) Console.WriteLine(ex); AppendToDictionary(protections, fileName, IncludeDebug ? ex.ToString() : "[Exception opening file, please try again]"); } // Clear out any empty keys ClearEmptyKeys(protections); return protections; } #endregion #region Helpers /// /// Create an instance of an extractable based on file type /// private static IExtractable CreateExtractable(SupportedFileType fileType) { switch (fileType) { case SupportedFileType.BFPK: return new FileType.BFPK(); case SupportedFileType.BSP: return new FileType.BSP(); case SupportedFileType.BZip2: return new FileType.BZip2(); case SupportedFileType.CFB: return new FileType.CFB(); //case SupportedFileType.CIA: return new FileType.CIA(); //case SupportedFileType.Executable: return new FileType.Executable(); case SupportedFileType.GCF: return new FileType.GCF(); case SupportedFileType.GZIP: return new FileType.GZIP(); case SupportedFileType.InstallShieldArchiveV3: return new FileType.InstallShieldArchiveV3(); case SupportedFileType.InstallShieldCAB: return new FileType.InstallShieldCAB(); case SupportedFileType.MicrosoftCAB: return new FileType.MicrosoftCAB(); case SupportedFileType.MicrosoftLZ: return new FileType.MicrosoftLZ(); case SupportedFileType.MPQ: return new FileType.MPQ(); //case SupportedFileType.N3DS: return new FileType.N3DS(); //case SupportedFileType.NCF: return new FileType.NCF(); //case SupportedFileType.Nitro: return new FileType.Nitro(); case SupportedFileType.PAK: return new FileType.PAK(); case SupportedFileType.PFF: return new FileType.PFF(); case SupportedFileType.PKZIP: return new FileType.PKZIP(); //case SupportedFileType.PLJ: return new FileType.PLJ(); //case SupportedFileType.Quantum: return new FileType.Quantum(); case SupportedFileType.RAR: return new FileType.RAR(); case SupportedFileType.SevenZip: return new FileType.SevenZip(); case SupportedFileType.SFFS: return new FileType.SFFS(); case SupportedFileType.SGA: return new FileType.SGA(); case SupportedFileType.TapeArchive: return new FileType.TapeArchive(); case SupportedFileType.VBSP: return new FileType.VBSP(); case SupportedFileType.VPK: return new FileType.VPK(); case SupportedFileType.WAD: return new FileType.WAD(); case SupportedFileType.XZ: return new FileType.XZ(); case SupportedFileType.XZP: return new FileType.XZP(); default: return null; } } /// /// Create an instance of a scannable based on file type /// private static IScannable CreateScannable(SupportedFileType fileType) { switch (fileType) { case SupportedFileType.AACSMediaKeyBlock: return new FileType.AACSMediaKeyBlock(); case SupportedFileType.BDPlusSVM: return new FileType.BDPlusSVM(); //case SupportedFileType.CIA: return new FileType.CIA(); case SupportedFileType.Executable: return new FileType.Executable(); //case FileTypes.IniFile: return new FileType.IniFile(); case SupportedFileType.LDSCRYPT: return new FileType.LDSCRYPT(); //case SupportedFileType.N3DS: return new FileType.N3DS(); //case SupportedFileType.NCF: return new FileType.NCF(); //case SupportedFileType.Nitro: return new FileType.Nitro(); case SupportedFileType.PLJ: return new FileType.PLJ(); case SupportedFileType.SFFS: return new FileType.SFFS(); case SupportedFileType.Textfile: return new FileType.Textfile(); default: return null; } } #endregion } }