From ab508eebe88c4b99c99fbefbca552c1e10765d81 Mon Sep 17 00:00:00 2001 From: Matt Nadareski Date: Tue, 7 Oct 2025 13:44:27 -0400 Subject: [PATCH] Use main feature pattern --- ProtectionScan/Features/MainFeature.cs | 185 +++++++++++++++++++++++ ProtectionScan/Program.cs | 198 +++++-------------------- 2 files changed, 222 insertions(+), 161 deletions(-) create mode 100644 ProtectionScan/Features/MainFeature.cs diff --git a/ProtectionScan/Features/MainFeature.cs b/ProtectionScan/Features/MainFeature.cs new file mode 100644 index 00000000..3c89bcc2 --- /dev/null +++ b/ProtectionScan/Features/MainFeature.cs @@ -0,0 +1,185 @@ +using System; +using System.Collections.Generic; +using System.IO; +using BinaryObjectScanner; +using SabreTools.CommandLine; +using SabreTools.CommandLine.Inputs; + +namespace ProtectionScan.Features +{ + internal sealed class MainFeature : Feature + { + #region Feature Definition + + public const string DisplayName = "main"; + + /// Flags are unused + private static readonly string[] _flags = []; + + /// Description is unused + private const string _description = ""; + + #endregion + + #region Inputs + + private const string _debugName = "debug"; + internal readonly FlagInput DebugInput = new(_debugName, ["-d", "--debug"], "Enable debug mode"); + + private const string _noArchivesName = "no-archives"; + internal readonly FlagInput NoArchivesInput = new(_noArchivesName, ["-na", "--no-archives"], "Disable scanning archives"); + + private const string _noContentsName = "no-contents"; + internal readonly FlagInput NoContentsInput = new(_noContentsName, ["-nc", "--no-contents"], "Disable scanning for content checks"); + + private const string _noPathsName = "no-paths"; + internal readonly FlagInput NoPathsInput = new(_noPathsName, ["-np", "--no-paths"], "Disable scanning for path checks"); + + private const string _noSubdirsName = "no-subdirs"; + internal readonly FlagInput NoSubdirsInput = new(_noSubdirsName, ["-ns", "--no-subdirs"], "Disable scanning subdirectories"); + + #endregion + + public MainFeature() + : base(DisplayName, _flags, _description) + { + RequiresInputs = true; + + Add(DebugInput); + Add(NoContentsInput); + Add(NoArchivesInput); + Add(NoPathsInput); + Add(NoSubdirsInput); + } + + /// + public override bool Execute() + { + // Create progress indicator + var fileProgress = new Progress(); + fileProgress.ProgressChanged += Changed; + + // Create scanner for all paths + var scanner = new Scanner( + !GetBoolean(_noArchivesName), + !GetBoolean(_noContentsName), + !GetBoolean(_noPathsName), + !GetBoolean(_noSubdirsName), + !GetBoolean(_debugName), + fileProgress); + + // Loop through the input paths + for (int i = 0; i < Inputs.Count; i++) + { + string arg = Inputs[i]; + GetAndWriteProtections(scanner, arg); + } + + return true; + } + + /// + public override bool VerifyInputs() => Inputs.Count > 0; + + /// + /// Protection progress changed handler + /// + private static void Changed(object? source, ProtectionProgress value) + { + string prefix = string.Empty; + for (int i = 0; i < value.Depth; i++) + { + prefix += "--> "; + } + + Console.WriteLine($"{prefix}{value.Percentage * 100:N2}%: {value.Filename} - {value.Protection}"); + } + + /// + /// Wrapper to get and log protections for a single path + /// + /// Scanner object to use + /// File or directory path + private static void GetAndWriteProtections(Scanner scanner, string path) + { + // Normalize by getting the full path + path = Path.GetFullPath(path); + + // An invalid path can't be scanned + if (!Directory.Exists(path) && !File.Exists(path)) + { + Console.WriteLine($"{path} does not exist, skipping..."); + return; + } + + try + { + var protections = scanner.GetProtections(path); + WriteProtectionResultFile(path, protections); + } + catch (Exception ex) + { + try + { + using var sw = new StreamWriter(File.OpenWrite($"exception-{DateTime.Now:yyyy-MM-dd_HHmmss.ffff}.txt")); + sw.WriteLine(ex); + } + catch + { + Console.WriteLine("Could not open exception log file for writing. See original message below:"); + Console.WriteLine(ex); + } + } + } + + /// + /// Write the protection results from a single path to file, if possible + /// + /// File or directory path + /// Dictionary of protections found, if any + private static void WriteProtectionResultFile(string path, Dictionary> protections) + { + if (protections == null) + { + Console.WriteLine($"No protections found for {path}"); + return; + } + + // Attempt to open a protection file for writing + StreamWriter? sw = null; + try + { + sw = new StreamWriter(File.OpenWrite($"protection-{DateTime.Now:yyyy-MM-dd_HHmmss.ffff}.txt")); + } + catch + { + Console.WriteLine("Could not open protection log file for writing. Only a console log will be provided."); + } + + // Sort the keys for consistent output + string[] keys = [.. protections.Keys]; + Array.Sort(keys); + + // Loop over all keys + foreach (string key in keys) + { + // Skip over files with no protection + var value = protections[key]; + if (value.Count == 0) + continue; + + // Sort the detected protections for consistent output + string[] fileProtections = [.. value]; + Array.Sort(fileProtections); + + // Format and output the line + string line = $"{key}: {string.Join(", ", fileProtections)}"; + Console.WriteLine(line); + sw?.WriteLine(line); + } + + // Dispose of the writer + sw?.Dispose(); + } + } +} diff --git a/ProtectionScan/Program.cs b/ProtectionScan/Program.cs index 752fa139..8154ed82 100644 --- a/ProtectionScan/Program.cs +++ b/ProtectionScan/Program.cs @@ -1,38 +1,23 @@ using System; using System.Collections.Generic; -using System.IO; -using BinaryObjectScanner; +using ProtectionScan.Features; using SabreTools.CommandLine; -using SabreTools.CommandLine.Inputs; +using SabreTools.CommandLine.Features; namespace ProtectionScan { - class Program + public static class Program { - #region Constants - - private const string _debugName = "debug"; - private const string _helpName = "help"; - private const string _noArchivesName = "no-archives"; - private const string _noContentsName = "no-contents"; - private const string _noPathsName = "no-paths"; - private const string _noSubdirsName = "no-subdirs"; - - #endregion - - static void Main(string[] args) + public static void Main(string[] args) { #if NET462_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER // Register the codepages System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance); #endif - // Create progress indicator - var fileProgress = new Progress(); - fileProgress.ProgressChanged += Changed; - // Create the command set - var commandSet = CreateCommands(); + var mainFeature = new MainFeature(); + var commandSet = CreateCommands(mainFeature); // If we have no args, show the help and quit if (args == null || args.Length == 0) @@ -41,47 +26,39 @@ namespace ProtectionScan return; } - // Loop through and process the options - int firstFileIndex = 0; - for (; firstFileIndex < args.Length; firstFileIndex++) - { - string arg = args[firstFileIndex]; + // Cache the first argument and starting index + string featureName = args[0]; - var input = commandSet.GetTopLevel(arg); - if (input == null) + // Try processing the standalone arguments + var topLevel = commandSet.GetTopLevel(featureName); + switch (topLevel) + { + // Standalone Options + case Help help: help.ProcessArgs(args, 0, commandSet); return; + + // Default Behavior + default: + if (!mainFeature.ProcessArgs(args, 0)) + { + commandSet.OutputAllHelp(); + return; + } + else if (!mainFeature.VerifyInputs()) + { + Console.Error.WriteLine("At least one input is required"); + commandSet.OutputAllHelp(); + return; + } + + mainFeature.Execute(); break; - - input.ProcessInput(args, ref firstFileIndex); - } - - // If help was specified - if (commandSet.GetBoolean(_helpName)) - { - commandSet.OutputAllHelp(); - return; - } - - // Create scanner for all paths - var scanner = new Scanner( - !commandSet.GetBoolean(_noArchivesName), - !commandSet.GetBoolean(_noContentsName), - !commandSet.GetBoolean(_noPathsName), - !commandSet.GetBoolean(_noSubdirsName), - !commandSet.GetBoolean(_debugName), - fileProgress); - - // Loop through the input paths - for (int i = firstFileIndex; i < args.Length; i++) - { - string arg = args[i]; - GetAndWriteProtections(scanner, arg); } } /// /// Create the command set for the program /// - private static CommandSet CreateCommands() + private static CommandSet CreateCommands(MainFeature mainFeature) { List header = [ "Protection Scanner", @@ -92,115 +69,14 @@ namespace ProtectionScan var commandSet = new CommandSet(header); - commandSet.Add(new FlagInput(_helpName, ["-?", "-h", "--help"], "Display this help text")); - commandSet.Add(new FlagInput(_debugName, ["-d", "--debug"], "Enable debug mode")); - commandSet.Add(new FlagInput(_noContentsName, ["-nc", "--no-contents"], "Disable scanning for content checks")); - commandSet.Add(new FlagInput(_noArchivesName, ["-na", "--no-archives"], "Disable scanning archives")); - commandSet.Add(new FlagInput(_noPathsName, ["-np", "--no-paths"], "Disable scanning for path checks")); - commandSet.Add(new FlagInput(_noSubdirsName, ["-ns", "--no-subdirs"], "Disable scanning subdirectories")); + commandSet.Add(new Help(["-?", "-h", "--help"])); + commandSet.Add(mainFeature.DebugInput); + commandSet.Add(mainFeature.NoContentsInput); + commandSet.Add(mainFeature.NoArchivesInput); + commandSet.Add(mainFeature.NoPathsInput); + commandSet.Add(mainFeature.NoSubdirsInput); return commandSet; } - - /// - /// Wrapper to get and log protections for a single path - /// - /// Scanner object to use - /// File or directory path - private static void GetAndWriteProtections(Scanner scanner, string path) - { - // Normalize by getting the full path - path = Path.GetFullPath(path); - - // An invalid path can't be scanned - if (!Directory.Exists(path) && !File.Exists(path)) - { - Console.WriteLine($"{path} does not exist, skipping..."); - return; - } - - try - { - var protections = scanner.GetProtections(path); - WriteProtectionResultFile(path, protections); - } - catch (Exception ex) - { - try - { - using var sw = new StreamWriter(File.OpenWrite($"exception-{DateTime.Now:yyyy-MM-dd_HHmmss.ffff}.txt")); - sw.WriteLine(ex); - } - catch - { - Console.WriteLine("Could not open exception log file for writing. See original message below:"); - Console.WriteLine(ex); - } - } - } - - /// - /// Write the protection results from a single path to file, if possible - /// - /// File or directory path - /// Dictionary of protections found, if any - private static void WriteProtectionResultFile(string path, Dictionary> protections) - { - if (protections == null) - { - Console.WriteLine($"No protections found for {path}"); - return; - } - - // Attempt to open a protection file for writing - StreamWriter? sw = null; - try - { - sw = new StreamWriter(File.OpenWrite($"protection-{DateTime.Now:yyyy-MM-dd_HHmmss.ffff}.txt")); - } - catch - { - Console.WriteLine("Could not open protection log file for writing. Only a console log will be provided."); - } - - // Sort the keys for consistent output - string[] keys = [.. protections.Keys]; - Array.Sort(keys); - - // Loop over all keys - foreach (string key in keys) - { - // Skip over files with no protection - var value = protections[key]; - if (value.Count == 0) - continue; - - // Sort the detected protections for consistent output - string[] fileProtections = [.. value]; - Array.Sort(fileProtections); - - // Format and output the line - string line = $"{key}: {string.Join(", ", fileProtections)}"; - Console.WriteLine(line); - sw?.WriteLine(line); - } - - // Dispose of the writer - sw?.Dispose(); - } - - /// - /// Protection progress changed handler - /// - private static void Changed(object? source, ProtectionProgress value) - { - string prefix = string.Empty; - for (int i = 0; i < value.Depth; i++) - { - prefix += "--> "; - } - - Console.WriteLine($"{prefix}{value.Percentage * 100:N2}%: {value.Filename} - {value.Protection}"); - } } }