using System; using System.Collections.Generic; using System.IO; using BinaryObjectScanner.Data; using BinaryObjectScanner.Interfaces; using SabreTools.IO.Extensions; using SabreTools.Serialization.Interfaces; using SabreTools.Serialization.Wrappers; namespace BinaryObjectScanner { public class Scanner { #region Instance Variables /// /// Determines whether archives are decompressed and scanned /// private readonly bool _scanArchives; /// /// Determines if content matches are used /// private readonly bool _scanContents; /// /// Determines if path matches are used /// private readonly bool _scanPaths; /// /// Determines if subdirectories are scanned /// private readonly bool _scanSubdirectories; /// /// Determines if debug information is output /// private readonly bool _includeDebug; /// /// Optional progress callback during scanning /// private readonly IProgress? _fileProgress; #endregion /// /// Constructor /// /// Enable scanning archive contents /// Enable including content detections in output /// Enable including path detections in output /// Enable scanning subdirectories /// Enable including debug information /// Optional progress callback public Scanner(bool scanArchives, bool scanContents, bool scanPaths, bool scanSubdirectories, bool includeDebug, IProgress? fileProgress = null) { _scanArchives = scanArchives; _scanContents = scanContents; _scanPaths = scanPaths; _scanSubdirectories = scanSubdirectories; _includeDebug = includeDebug; _fileProgress = fileProgress; #if NET462_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER // Register the codepages System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance); #endif } #region Scanning /// /// Scan a single path and get all found protections /// /// Path to scan /// Dictionary of list of strings representing the found protections public Dictionary> GetProtections(string path) => GetProtectionsImpl(path, depth: 0).ToDictionary(); /// /// Scan the list of paths and get all found protections /// /// Paths to scan /// Dictionary of list of strings representing the found protections public Dictionary> GetProtections(List? paths) => GetProtectionsImpl(paths, depth: 0).ToDictionary(); /// /// Scan a single path and get all found protections /// /// Path to scan /// Depth of the current scanner pertaining to extracted data /// Dictionary of list of strings representing the found protections private ProtectionDictionary GetProtectionsImpl(string path, int depth) => GetProtectionsImpl([path], depth); /// /// Scan a single path and get all found protections /// /// Paths to scan /// Depth of the current scanner pertaining to extracted data /// Dictionary of list of strings representing the found protections private ProtectionDictionary GetProtectionsImpl(List? paths, int depth) { // If we have no paths, we can't scan if (paths == null || paths.Count == 0) { if (_includeDebug) Console.WriteLine("No paths found to scan, skipping..."); return []; } // Set a starting starting time for debug output DateTime startTime = DateTime.UtcNow; // Checkpoint _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 ProtectionDictionary(); foreach (string path in paths) { // Directories scan each internal file individually if (Directory.Exists(path)) { // Enumerate all files at first for easier access SearchOption searchOption = _scanSubdirectories ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; List files = [.. IOExtensions.SafeGetFiles(path, "*", searchOption)]; // Scan for path-detectable protections if (_scanPaths) { var directoryPathProtections = HandlePathChecks(path, files); protections.Append(directoryPathProtections); } // Scan each file in directory separately for (int i = 0; i < files.Count; i++) { // Get the current file string file = files[i]; // Get the reportable file name string reportableFileName = file; if (reportableFileName.StartsWith(tempFilePath)) reportableFileName = reportableFileName.Substring(tempFilePathWithGuid.Length); // Checkpoint _fileProgress?.Report(new ProtectionProgress(reportableFileName, depth, i / (float)files.Count, "Checking file" + (file != reportableFileName ? " from archive" : string.Empty))); // Scan for path-detectable protections if (_scanPaths) { var filePathProtections = HandlePathChecks(file, files: null); if (filePathProtections != null && filePathProtections.Count > 0) protections.Append(filePathProtections); } // Scan for content-detectable protections var fileProtections = GetInternalProtections(file, depth); if (fileProtections != null && fileProtections.Count > 0) protections.Append(fileProtections); // Checkpoint protections.TryGetValue(file, out var fullProtectionList); var fullProtection = fullProtectionList != null && fullProtectionList.Count > 0 ? string.Join(", ", [.. fullProtectionList]) : null; _fileProgress?.Report(new ProtectionProgress(reportableFileName, depth, (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 _fileProgress?.Report(new ProtectionProgress(reportableFileName, depth, 0, "Checking file" + (path != reportableFileName ? " from archive" : string.Empty))); // Scan for path-detectable protections if (_scanPaths) { var filePathProtections = HandlePathChecks(path, files: null); if (filePathProtections != null && filePathProtections.Count > 0) protections.Append(filePathProtections); } // Scan for content-detectable protections var fileProtections = GetInternalProtections(path, depth); if (fileProtections != null && fileProtections.Count > 0) protections.Append(fileProtections); // Checkpoint protections.TryGetValue(path, out var fullProtectionList); var fullProtection = fullProtectionList != null && fullProtectionList.Count > 0 ? string.Join(", ", [.. fullProtectionList]) : null; _fileProgress?.Report(new ProtectionProgress(reportableFileName, depth, 1, fullProtection ?? string.Empty)); } // Invalid path else { if (_includeDebug) Console.Error.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 protections.ClearEmptyKeys(); // 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 content-detectable protections associated with a single path /// /// Path to the file to scan /// Depth of the current scanner pertaining to extracted data /// Dictionary of list of strings representing the found protections private ProtectionDictionary GetInternalProtections(string file, int depth) { // Quick sanity check before continuing if (!File.Exists(file)) { if (_includeDebug) Console.WriteLine($"{file} does not exist, skipping..."); return []; } // Open the file and begin scanning try { using FileStream fs = File.Open(file, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); return GetInternalProtections(fs.Name, fs, depth); } catch (Exception ex) { if (_includeDebug) Console.WriteLine(ex); var protections = new ProtectionDictionary(); protections.Append(file, _includeDebug ? ex.ToString() : "[Exception opening file, please try again]"); 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 /// Depth of the current scanner pertaining to extracted data /// Dictionary of list of strings representing the found protections private ProtectionDictionary GetInternalProtections(string fileName, Stream stream, int depth) { // Quick sanity check before continuing if (!stream.CanRead) { if (_includeDebug) Console.WriteLine($"{fileName} does not have a readable stream, skipping..."); return []; } // Initialize the protections found var protections = new ProtectionDictionary(); // 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; try { magic = stream.ReadBytes(16); stream.Seek(0, SeekOrigin.Begin); } catch (Exception ex) { if (_includeDebug) Console.Error.WriteLine(ex); return []; } // Get the file type either from magic number or extension WrapperType fileType = WrapperFactory.GetFileType(magic, extension); if (fileType == WrapperType.UNKNOWN) { if (_includeDebug) Console.WriteLine($"{fileName} not a scannable file type, skipping..."); return []; } // Get the wrapper, if possible var wrapper = WrapperFactory.CreateWrapper(fileType, stream); #region Non-Archive File Types // Try to scan file contents var detectable = CreateDetectable(fileType, wrapper); if (_scanContents && detectable != null) { var subProtection = detectable.Detect(stream, fileName, _includeDebug); protections.Append(fileName, subProtection); } #endregion #region Archive File Types // If we're scanning archives if (_scanArchives && wrapper is IExtractable extractable) { // If the extractable file itself fails try { // Extract and get the output path string tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); Directory.CreateDirectory(tempPath); _ = extractable.Extract(tempPath, _includeDebug); // Check if any files extracted if (IOExtensions.SafeGetFiles(tempPath).Length > 0) { // Scan the output path var subProtections = GetProtectionsImpl(tempPath, depth + 1); // Prepare the returned values subProtections.StripFromKeys(tempPath); subProtections.PrependToKeys(fileName); // Append the values protections.Append(subProtections); } // If temp directory cleanup fails try { if (Directory.Exists(tempPath)) Directory.Delete(tempPath, true); } catch (Exception ex) { if (_includeDebug) Console.Error.WriteLine(ex); } } catch (Exception ex) { if (_includeDebug) Console.Error.WriteLine(ex); } } #endregion } catch (Exception ex) { if (_includeDebug) Console.WriteLine(ex); protections.Append(fileName, _includeDebug ? ex.ToString() : "[Exception opening file, please try again]"); } // Clear out any empty keys protections.ClearEmptyKeys(); return protections; } #endregion #region Path Handling /// /// Handle a single path based on all path check implementations /// /// Path of the file or directory to check /// Scanner object to use for options and scanning /// Set of protections in file, null on error private static ProtectionDictionary HandlePathChecks(string path, List? files) { // Create the output dictionary var protections = new ProtectionDictionary(); // Preprocess the list of files files = files? .ConvertAll(f => f.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar)); // Iterate through all checks StaticChecks.PathCheckClasses.IterateWithAction(checkClass => { var subProtections = PerformPathCheck(checkClass, path, files); protections.Append(path, subProtections); }); return protections; } /// /// Handle files based on an IPathCheck implementation /// /// IPathCheck class representing the file type /// Path of the file or directory to check /// Set of protections in path, empty on error private static List PerformPathCheck(IPathCheck impl, string? path, List? files) { // If we have an invalid path if (string.IsNullOrEmpty(path)) return []; // Setup the list var protections = new List(); // If we have a file path if (File.Exists(path)) { var protection = impl.CheckFilePath(path!); if (protection != null) protections.Add(protection); } // If we have a directory path if (Directory.Exists(path) && files != null && files.Count > 0) { var subProtections = impl.CheckDirectoryPath(path!, files); if (subProtections != null) protections.AddRange(subProtections); } return protections; } #endregion #region Helpers /// /// Create an instance of a detectable based on file type /// private static IDetectable? CreateDetectable(WrapperType fileType, IWrapper? wrapper) { // Use the wrapper before the type switch (wrapper) { case AACSMediaKeyBlock obj: return new FileType.AACSMediaKeyBlock(obj); case BDPlusSVM obj: return new FileType.BDPlusSVM(obj); // case CIA obj => new FileType.CIA(obj), case LinearExecutable obj: return new FileType.LinearExecutable(obj); case MSDOS obj: return new FileType.MSDOS(obj); // case N3DS obj: return new FileType.N3DS(obj); case NewExecutable obj: return new FileType.NewExecutable(obj); case PlayJAudioFile obj: return new FileType.PLJ(obj); case PortableExecutable obj: return new FileType.PortableExecutable(obj); } // Fall back on the file type for types not implemented in Serialization return fileType switch { // WrapperType.CIA => new FileType.CIA(), WrapperType.LDSCRYPT => new FileType.LDSCRYPT(), // WrapperType.N3DS => new FileType.N3DS(), WrapperType.RealArcadeInstaller => new FileType.RealArcadeInstaller(), WrapperType.RealArcadeMezzanine => new FileType.RealArcadeMezzanine(), WrapperType.SFFS => new FileType.SFFS(), WrapperType.Textfile => new FileType.Textfile(), _ => null, }; } #endregion } }