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;
- }
- }
}
}