From 9f1c5e2bd2543fe4502c7a5be6b088e9143081d7 Mon Sep 17 00:00:00 2001 From: Matt Nadareski Date: Tue, 7 Oct 2025 10:04:19 -0400 Subject: [PATCH] Use main feature pattern with ExtractionTool --- ExtractionTool/Features/MainFeature.cs | 325 +++++++++++++++++++++++++ ExtractionTool/Options.cs | 54 ---- ExtractionTool/Program.cs | 295 +++------------------- 3 files changed, 357 insertions(+), 317 deletions(-) create mode 100644 ExtractionTool/Features/MainFeature.cs delete mode 100644 ExtractionTool/Options.cs diff --git a/ExtractionTool/Features/MainFeature.cs b/ExtractionTool/Features/MainFeature.cs new file mode 100644 index 00000000..f35521d5 --- /dev/null +++ b/ExtractionTool/Features/MainFeature.cs @@ -0,0 +1,325 @@ +using System; +using System.IO; +using SabreTools.CommandLine; +using SabreTools.CommandLine.Inputs; +using SabreTools.IO.Extensions; +using SabreTools.Serialization; +using SabreTools.Serialization.Wrappers; + +namespace ExtractionTool.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 _outputPathName = "output-path"; + internal readonly StringInput OutputPathInput = new(_outputPathName, ["-o", "--outdir"], "Set output path for extraction (required)"); + + #endregion + + #region Properties + + /// + /// Enable debug output for relevant operations + /// + public bool Debug { get; set; } + + /// + /// Output path for archive extraction + /// + public string OutputPath { get; set; } = string.Empty; + + #endregion + + public MainFeature() + : base(DisplayName, _flags, _description) + { + RequiresInputs = true; + + Add(DebugInput); + Add(OutputPathInput); + } + + /// + public override bool Execute() + { + // Get the options from the arguments + Debug = GetBoolean(_debugName); + OutputPath = GetString(_outputPathName) ?? string.Empty; + + // Validate the output path + if (!ValidateExtractionPath()) + return false; + + // Loop through the input paths + for (int i = 0; i < Inputs.Count; i++) + { + string arg = Inputs[i]; + ExtractPath(arg); + } + + return true; + } + + /// + public override bool VerifyInputs() => Inputs.Count > 0; + + /// + /// Wrapper to extract data for a single path + /// + /// File or directory path + private void ExtractPath(string path) + { + // Normalize by getting the full path + path = Path.GetFullPath(path); + Console.WriteLine($"Checking possible path: {path}"); + + // Check if the file or directory exists + if (File.Exists(path)) + { + ExtractFile(path); + } + else if (Directory.Exists(path)) + { + foreach (string file in path.SafeEnumerateFiles("*", SearchOption.AllDirectories)) + { + ExtractFile(file); + } + } + else + { + Console.WriteLine($"{path} does not exist, skipping..."); + } + } + + /// + /// Print information for a single file, if possible + /// + /// File path + private void ExtractFile(string file) + { + Console.WriteLine($"Attempting to extract all files from {file}"); + using Stream stream = File.Open(file, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + + // Get the extension for certain checks + string extension = Path.GetExtension(file).ToLower().TrimStart('.'); + + // Get the first 16 bytes for matching + byte[] magic = new byte[16]; + try + { + int read = stream.Read(magic, 0, 16); + stream.Seek(0, SeekOrigin.Begin); + } + catch (Exception ex) + { + if (Debug) Console.Error.WriteLine(ex); + return; + } + + // Get the file type + WrapperType ft = WrapperFactory.GetFileType(magic, extension); + var wrapper = WrapperFactory.CreateWrapper(ft, stream); + + // Create the output directory + Directory.CreateDirectory(OutputPath); + + // Print the preamble + Console.WriteLine($"Attempting to extract from '{wrapper?.Description() ?? "UNKNOWN"}'"); + Console.WriteLine(); + + switch (wrapper) + { + // 7-zip + case SevenZip sz: + sz.Extract(OutputPath, Debug); + break; + + // BFPK archive + case BFPK bfpk: + bfpk.Extract(OutputPath, Debug); + break; + + // BSP + case BSP bsp: + bsp.Extract(OutputPath, Debug); + break; + + // bzip2 + case BZip2 bzip2: + bzip2.Extract(OutputPath, Debug); + break; + + // CFB + case CFB cfb: + cfb.Extract(OutputPath, Debug); + break; + + // GCF + case GCF gcf: + gcf.Extract(OutputPath, Debug); + break; + + // gzip + case GZip gzip: + gzip.Extract(OutputPath, Debug); + break; + + // InstallShield Archive V3 (Z) + case InstallShieldArchiveV3 isv3: + isv3.Extract(OutputPath, Debug); + break; + + // IS-CAB archive + case InstallShieldCabinet iscab: + iscab.Extract(OutputPath, Debug); + break; + + // LZ-compressed file, KWAJ variant + case LZKWAJ kwaj: + kwaj.Extract(OutputPath, Debug); + break; + + // LZ-compressed file, QBasic variant + case LZQBasic qbasic: + qbasic.Extract(OutputPath, Debug); + break; + + // LZ-compressed file, SZDD variant + case LZSZDD szdd: + szdd.Extract(OutputPath, Debug); + break; + + // Microsoft Cabinet archive + case MicrosoftCabinet mscab: + mscab.Extract(OutputPath, Debug); + break; + + // MoPaQ (MPQ) archive + case MoPaQ mpq: + mpq.Extract(OutputPath, Debug); + break; + + // New Executable + case NewExecutable nex: + nex.Extract(OutputPath, Debug); + break; + + // PAK + case PAK pak: + pak.Extract(OutputPath, Debug); + break; + + // PFF + case PFF pff: + pff.Extract(OutputPath, Debug); + break; + + // PKZIP + case PKZIP pkzip: + pkzip.Extract(OutputPath, Debug); + break; + + // Portable Executable + case PortableExecutable pex: + pex.Extract(OutputPath, Debug); + break; + + // Quantum + case Quantum quantum: + quantum.Extract(OutputPath, Debug); + break; + + // RAR + case RAR rar: + rar.Extract(OutputPath, Debug); + break; + + // SGA + case SGA sga: + sga.Extract(OutputPath, Debug); + break; + + // Tape Archive + case TapeArchive tar: + tar.Extract(OutputPath, Debug); + break; + + // VBSP + case VBSP vbsp: + vbsp.Extract(OutputPath, Debug); + break; + + // VPK + case VPK vpk: + vpk.Extract(OutputPath, Debug); + break; + + // WAD3 + case WAD3 wad: + wad.Extract(OutputPath, Debug); + break; + + // xz + case XZ xz: + xz.Extract(OutputPath, Debug); + break; + + // XZP + case XZP xzp: + xzp.Extract(OutputPath, Debug); + break; + + // Everything else + default: + Console.WriteLine("Not a supported extractable file format, skipping..."); + Console.WriteLine(); + break; + } + } + + /// + /// Validate the extraction path + /// + private bool ValidateExtractionPath() + { + // Null or empty output path + if (string.IsNullOrEmpty(OutputPath)) + { + Console.WriteLine("Output directory required for extraction!"); + Console.WriteLine(); + return false; + } + + // Malformed output path or invalid location + try + { + OutputPath = Path.GetFullPath(OutputPath); + Directory.CreateDirectory(OutputPath); + } + catch + { + Console.WriteLine("Output directory could not be created!"); + Console.WriteLine(); + return false; + } + + return true; + } + } +} diff --git a/ExtractionTool/Options.cs b/ExtractionTool/Options.cs deleted file mode 100644 index e3df600a..00000000 --- a/ExtractionTool/Options.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using System.IO; - -namespace ExtractionTool -{ - /// - /// Set of options for the test executable - /// - internal sealed class Options - { - #region Properties - - /// - /// Enable debug output for relevant operations - /// - public bool Debug { get; set; } - - /// - /// Output path for archive extraction - /// - public string OutputPath { get; set; } = string.Empty; - - #endregion - - /// - /// Validate the extraction path - /// - public bool ValidateExtractionPath() - { - // Null or empty output path - if (string.IsNullOrEmpty(OutputPath)) - { - Console.WriteLine("Output directory required for extraction!"); - Console.WriteLine(); - return false; - } - - // Malformed output path or invalid location - try - { - OutputPath = Path.GetFullPath(OutputPath); - Directory.CreateDirectory(OutputPath); - } - catch - { - Console.WriteLine("Output directory could not be created!"); - Console.WriteLine(); - return false; - } - - return true; - } - } -} \ No newline at end of file diff --git a/ExtractionTool/Program.cs b/ExtractionTool/Program.cs index d9186297..3d945ca0 100644 --- a/ExtractionTool/Program.cs +++ b/ExtractionTool/Program.cs @@ -1,11 +1,8 @@ using System; using System.Collections.Generic; -using System.IO; +using ExtractionTool.Features; using SabreTools.CommandLine; -using SabreTools.CommandLine.Inputs; -using SabreTools.IO.Extensions; -using SabreTools.Serialization; -using SabreTools.Serialization.Wrappers; +using SabreTools.CommandLine.Features; namespace ExtractionTool { @@ -27,7 +24,8 @@ namespace ExtractionTool #endif // 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) @@ -36,52 +34,39 @@ namespace ExtractionTool 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; - } - - // Get the options from the arguments - var options = new Options - { - Debug = commandSet.GetBoolean(_debugName), - OutputPath = commandSet.GetString(_outputPathName) ?? string.Empty, - }; - - // Validate the output path - if (!options.ValidateExtractionPath()) - { - commandSet.OutputAllHelp(); - return; - } - - // Loop through the input paths - for (int i = firstFileIndex; i < args.Length; i++) - { - string arg = args[i]; - ExtractPath(arg, options); } } /// /// Create the command set for the program /// - private static CommandSet CreateCommands() + private static CommandSet CreateCommands(MainFeature mainFeature) { List header = [ "Extraction Tool", @@ -92,227 +77,11 @@ namespace ExtractionTool 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 StringInput(_outputPathName, ["-o", "--outdir"], "Set output path for extraction (required)")); + commandSet.Add(new Help(["-?", "-h", "--help"])); + commandSet.Add(mainFeature.DebugInput); + commandSet.Add(mainFeature.OutputPathInput); return commandSet; } - - /// - /// Wrapper to extract data for a single path - /// - /// File or directory path - /// User-defined options - private static void ExtractPath(string path, Options options) - { - // Normalize by getting the full path - path = Path.GetFullPath(path); - Console.WriteLine($"Checking possible path: {path}"); - - // Check if the file or directory exists - if (File.Exists(path)) - { - ExtractFile(path, options); - } - else if (Directory.Exists(path)) - { - foreach (string file in IOExtensions.SafeEnumerateFiles(path, "*", SearchOption.AllDirectories)) - { - ExtractFile(file, options); - } - } - else - { - Console.WriteLine($"{path} does not exist, skipping..."); - } - } - - /// - /// Print information for a single file, if possible - /// - /// File path - /// User-defined options - private static void ExtractFile(string file, Options options) - { - Console.WriteLine($"Attempting to extract all files from {file}"); - using Stream stream = File.Open(file, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); - - // Get the extension for certain checks - string extension = Path.GetExtension(file).ToLower().TrimStart('.'); - - // Get the first 16 bytes for matching - byte[] magic = new byte[16]; - try - { - int read = stream.Read(magic, 0, 16); - stream.Seek(0, SeekOrigin.Begin); - } - catch (Exception ex) - { - if (options.Debug) Console.Error.WriteLine(ex); - return; - } - - // Get the file type - WrapperType ft = WrapperFactory.GetFileType(magic, extension); - var wrapper = WrapperFactory.CreateWrapper(ft, stream); - - // Create the output directory - Directory.CreateDirectory(options.OutputPath); - - // Print the preamble - Console.WriteLine($"Attempting to extract from '{wrapper?.Description() ?? "UNKNOWN"}'"); - Console.WriteLine(); - - switch (wrapper) - { - // 7-zip - case SevenZip sz: - sz.Extract(options.OutputPath, options.Debug); - break; - - // BFPK archive - case BFPK bfpk: - bfpk.Extract(options.OutputPath, options.Debug); - break; - - // BSP - case BSP bsp: - bsp.Extract(options.OutputPath, options.Debug); - break; - - // bzip2 - case BZip2 bzip2: - bzip2.Extract(options.OutputPath, options.Debug); - break; - - // CFB - case CFB cfb: - cfb.Extract(options.OutputPath, options.Debug); - break; - - // GCF - case GCF gcf: - gcf.Extract(options.OutputPath, options.Debug); - break; - - // gzip - case GZip gzip: - gzip.Extract(options.OutputPath, options.Debug); - break; - - // InstallShield Archive V3 (Z) - case InstallShieldArchiveV3 isv3: - isv3.Extract(options.OutputPath, options.Debug); - break; - - // IS-CAB archive - case InstallShieldCabinet iscab: - iscab.Extract(options.OutputPath, options.Debug); - break; - - // LZ-compressed file, KWAJ variant - case LZKWAJ kwaj: - kwaj.Extract(options.OutputPath, options.Debug); - break; - - // LZ-compressed file, QBasic variant - case LZQBasic qbasic: - qbasic.Extract(options.OutputPath, options.Debug); - break; - - // LZ-compressed file, SZDD variant - case LZSZDD szdd: - szdd.Extract(options.OutputPath, options.Debug); - break; - - // Microsoft Cabinet archive - case MicrosoftCabinet mscab: - mscab.Extract(options.OutputPath, options.Debug); - break; - - // MoPaQ (MPQ) archive - case MoPaQ mpq: - mpq.Extract(options.OutputPath, options.Debug); - break; - - // New Executable - case NewExecutable nex: - nex.Extract(options.OutputPath, options.Debug); - break; - - // PAK - case PAK pak: - pak.Extract(options.OutputPath, options.Debug); - break; - - // PFF - case PFF pff: - pff.Extract(options.OutputPath, options.Debug); - break; - - // PKZIP - case PKZIP pkzip: - pkzip.Extract(options.OutputPath, options.Debug); - break; - - // Portable Executable - case PortableExecutable pex: - pex.Extract(options.OutputPath, options.Debug); - break; - - // Quantum - case Quantum quantum: - quantum.Extract(options.OutputPath, options.Debug); - break; - - // RAR - case RAR rar: - rar.Extract(options.OutputPath, options.Debug); - break; - - // SGA - case SGA sga: - sga.Extract(options.OutputPath, options.Debug); - break; - - // Tape Archive - case TapeArchive tar: - tar.Extract(options.OutputPath, options.Debug); - break; - - // VBSP - case VBSP vbsp: - vbsp.Extract(options.OutputPath, options.Debug); - break; - - // VPK - case VPK vpk: - vpk.Extract(options.OutputPath, options.Debug); - break; - - // WAD3 - case WAD3 wad: - wad.Extract(options.OutputPath, options.Debug); - break; - - // xz - case XZ xz: - xz.Extract(options.OutputPath, options.Debug); - break; - - // XZP - case XZP xzp: - xzp.Extract(options.OutputPath, options.Debug); - break; - - // Everything else - default: - Console.WriteLine("Not a supported extractable file format, skipping..."); - Console.WriteLine(); - break; - } - } } }