diff --git a/Headerer/Database.cs b/Headerer/Database.cs new file mode 100644 index 00000000..50cdb5b5 --- /dev/null +++ b/Headerer/Database.cs @@ -0,0 +1,43 @@ +using System.IO; +using Microsoft.Data.Sqlite; +using SabreTools.IO; + +namespace Headerer +{ + internal static class Database + { + #region Constants + + public static string HeadererFileName = Path.Combine(PathTool.GetRuntimeDirectory(), "Headerer.sqlite"); + public static string HeadererConnectionString = $"Data Source={HeadererFileName};"; + + #endregion + + /// + /// Ensure that the database exists and has the proper schema + /// + public static void EnsureDatabase() + { + // Make sure the file exists + if (!File.Exists(HeadererFileName)) + File.Create(HeadererFileName); + + // Open the database connection + SqliteConnection dbc = new(HeadererConnectionString); + dbc.Open(); + + // Make sure the database has the correct schema + string query = @" +CREATE TABLE IF NOT EXISTS data ( + 'sha1' TEXT NOT NULL, + 'header' TEXT NOT NULL, + 'type' TEXT NOT NULL, + PRIMARY KEY (sha1, header, type) +)"; + SqliteCommand slc = new(query, dbc); + slc.ExecuteNonQuery(); + slc.Dispose(); + dbc.Dispose(); + } + } +} \ No newline at end of file diff --git a/Headerer/Features/Extract.cs b/Headerer/Extract.cs similarity index 57% rename from Headerer/Features/Extract.cs rename to Headerer/Extract.cs index 7ed8ce50..0863d7e9 100644 --- a/Headerer/Features/Extract.cs +++ b/Headerer/Extract.cs @@ -1,62 +1,14 @@ -using System.IO; -using System.Collections.Generic; +using System; +using System.IO; using Microsoft.Data.Sqlite; using SabreTools.Hashing; -using SabreTools.Help; -using SabreTools.IO; using SabreTools.IO.Extensions; using SabreTools.Skippers; -namespace Headerer.Features +namespace Headerer { - internal class Extract : BaseFeature + internal static class Extract { - public const string Value = "Extract"; - - public Extract() - { - Name = Value; - Flags.AddRange(["ex", "extract"]); - Description = "Extract and remove copier headers"; - _featureType = ParameterType.Flag; - LongDescription = @"This will detect, store, and remove copier headers from a file or folder of files. The headers are backed up and collated by the hash of the unheadered file. Files are then output without the detected copier header alongside the originals with the suffix .new. No input files are altered in the process. Only uncompressed files will be processed. - -The following systems have headers that this program can work with: - - Atari 7800 - - Atari Lynx - - Commodore PSID Music - - NEC PC - Engine / TurboGrafx 16 - - Nintendo Famicom / Nintendo Entertainment System - - Nintendo Famicom Disk System - - Nintendo Super Famicom / Super Nintendo Entertainment System - - Nintendo Super Famicom / Super Nintendo Entertainment System SPC"; - - // Common Features - AddCommonFeatures(); - - AddFeature(OutputDirStringInput); - AddFeature(NoStoreHeaderFlag); - } - - public override bool ProcessFeatures(Dictionary features) - { - // If the base fails, just fail out - if (!base.ProcessFeatures(features)) - return false; - - // Get feature flags - bool nostore = GetBoolean(features, NoStoreHeaderValue); - - // Get only files from the inputs - List files = PathTool.GetFilesOnly(Inputs); - foreach (ParentablePath file in files) - { - DetectTransformStore(file.CurrentPath, OutputDir, nostore); - } - - return true; - } - /// /// Detect header skipper compliance and create an output file /// @@ -64,13 +16,13 @@ The following systems have headers that this program can work with: /// Output directory to write the file to, empty means the same directory as the input file /// True if headers should not be stored in the database, false otherwise /// True if the output file was created, false otherwise - private bool DetectTransformStore(string file, string? outDir, bool nostore) + public static bool DetectTransformStore(string file, string? outDir, bool nostore) { // Create the output directory if it doesn't exist if (!string.IsNullOrWhiteSpace(outDir) && !Directory.Exists(outDir)) Directory.CreateDirectory(outDir); - logger.User($"\nGetting skipper information for '{file}'"); + Console.WriteLine($"\nGetting skipper information for '{file}'"); // Get the skipper rule that matches the file, if any SkipperMatch.Init(); @@ -80,7 +32,7 @@ The following systems have headers that this program can work with: if (rule.Tests == null || rule.Tests.Length == 0 || rule.Operation != HeaderSkipOperation.None) return false; - logger.User("File has a valid copier header"); + Console.WriteLine("File has a valid copier header"); // Get the header bytes from the file first string hstr; @@ -122,17 +74,17 @@ The following systems have headers that this program can work with: /// String representing the header bytes /// SHA-1 of the deheadered file /// Name of the source skipper file - private void AddHeaderToDatabase(string header, string SHA1, string source) + private static void AddHeaderToDatabase(string header, string SHA1, string source) { // Ensure the database exists - EnsureDatabase(); + Database.EnsureDatabase(); // Open the database connection - SqliteConnection dbc = new(HeadererConnectionString); + SqliteConnection dbc = new(Database.HeadererConnectionString); dbc.Open(); string query = $"SELECT * FROM data WHERE sha1='{SHA1}' AND header='{header}'"; - SqliteCommand slc = new(query, dbc); + var slc = new SqliteCommand(query, dbc); SqliteDataReader sldr = slc.ExecuteReader(); bool exists = sldr.HasRows; @@ -140,7 +92,7 @@ The following systems have headers that this program can work with: { query = $"INSERT INTO data (sha1, header, type) VALUES ('{SHA1}', '{header}', '{source}')"; slc = new SqliteCommand(query, dbc); - logger.Verbose($"Result of inserting header: {slc.ExecuteNonQuery()}"); + Console.WriteLine($"Result of inserting header: {slc.ExecuteNonQuery()}"); // TODO: Gate behind debug flag } // Dispose of database objects diff --git a/Headerer/Feature.cs b/Headerer/Feature.cs new file mode 100644 index 00000000..2f03d756 --- /dev/null +++ b/Headerer/Feature.cs @@ -0,0 +1,10 @@ +namespace Headerer +{ + internal enum Feature + { + NONE = 0, + + Extract, + Restore, + } +} \ No newline at end of file diff --git a/Headerer/Features/BaseFeature.cs b/Headerer/Features/BaseFeature.cs deleted file mode 100644 index 0649fedc..00000000 --- a/Headerer/Features/BaseFeature.cs +++ /dev/null @@ -1,169 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using Microsoft.Data.Sqlite; -using SabreTools.Core.Tools; -using SabreTools.Help; -using SabreTools.IO; -using SabreTools.IO.Logging; - -namespace Headerer.Features -{ - internal class BaseFeature : TopLevel - { - #region Logging - - /// - /// Logging object - /// - protected Logger logger = new(); - - #endregion - - #region Constants - - public static string HeadererFileName = Path.Combine(PathTool.GetRuntimeDirectory(), "Headerer.sqlite"); - public static string HeadererConnectionString = $"Data Source={HeadererFileName};"; - - #endregion - - #region Features - - #region Flag features - - internal const string NoStoreHeaderValue = "no-store-header"; - internal static Feature NoStoreHeaderFlag - { - get - { - return new Feature( - NoStoreHeaderValue, - new List() { "-nsh", "--no-store-header" }, - "Don't store the extracted header", - ParameterType.Flag, - longDescription: "By default, all headers that are removed from files are backed up in the database. This flag allows users to skip that step entirely, avoiding caching the headers at all."); - } - } - - internal const string ScriptValue = "script"; - internal static Feature ScriptFlag - { - get - { - return new Feature( - ScriptValue, - new List() { "-sc", "--script" }, - "Enable script mode (no clear screen)", - ParameterType.Flag, - "For times when SabreTools is being used in a scripted environment, the user may not want the screen to be cleared every time that it is called. This flag allows the user to skip clearing the screen on run just like if the console was being redirected."); - } - } - - #endregion - - #region String features - - internal const string LogLevelStringValue = "log-level"; - internal static Feature LogLevelStringInput - { - get - { - return new Feature( - LogLevelStringValue, - new List() { "-ll", "--log-level" }, - "Set the lowest log level for output", - ParameterType.String, - longDescription: @"Set the lowest log level for output. -Possible values are: Verbose, User, Warning, Error"); - } - } - - internal const string OutputDirStringValue = "output-dir"; - internal static Feature OutputDirStringInput - { - get - { - return new Feature( - OutputDirStringValue, - new List() { "-out", "--output-dir" }, - "Set output directory", - ParameterType.String, - longDescription: "This sets an output folder to be used when the files are created. If a path is not defined, the runtime directory is used instead."); - } - } - - #endregion - - #endregion - - #region Fields - - /// - /// Lowest log level for output - /// - public LogLevel LogLevel { get; protected set; } - - /// - /// Output directory - /// - protected string? OutputDir { get; set; } - - /// - /// Determines if scripting mode is enabled - /// - public bool ScriptMode { get; protected set; } - - #endregion - - #region Add Feature Groups - - /// - /// Add common features - /// - protected void AddCommonFeatures() - { - AddFeature(ScriptFlag); - AddFeature(LogLevelStringInput); - } - - #endregion - - public override bool ProcessFeatures(Dictionary features) - { - LogLevel = GetString(features, LogLevelStringValue).AsLogLevel(); - OutputDir = GetString(features, OutputDirStringValue)?.Trim('"'); - ScriptMode = GetBoolean(features, ScriptValue); - return true; - } - - #region Protected Helpers - - /// - /// Ensure that the database exists and has the proper schema - /// - protected static void EnsureDatabase() - { - // Make sure the file exists - if (!File.Exists(HeadererFileName)) - File.Create(HeadererFileName); - - // Open the database connection - SqliteConnection dbc = new(HeadererConnectionString); - dbc.Open(); - - // Make sure the database has the correct schema - string query = @" -CREATE TABLE IF NOT EXISTS data ( - 'sha1' TEXT NOT NULL, - 'header' TEXT NOT NULL, - 'type' TEXT NOT NULL, - PRIMARY KEY (sha1, header, type) -)"; - SqliteCommand slc = new(query, dbc); - slc.ExecuteNonQuery(); - slc.Dispose(); - dbc.Dispose(); - } - - #endregion - } -} diff --git a/Headerer/Features/DisplayHelp.cs b/Headerer/Features/DisplayHelp.cs deleted file mode 100644 index def83ef2..00000000 --- a/Headerer/Features/DisplayHelp.cs +++ /dev/null @@ -1,35 +0,0 @@ -using SabreTools.Help; - -namespace Headerer.Features -{ - internal class DisplayHelp : BaseFeature - { - public const string Value = "Help"; - - public DisplayHelp() - { - Name = Value; - Flags.AddRange(["?", "h", "help"]); - Description = "Show this help"; - _featureType = ParameterType.Flag; - LongDescription = "Built-in to most of the programs is a basic help text."; - } - - public override bool ProcessArgs(string[] args, FeatureSet help) - { - // If we had something else after help - if (args.Length > 1) - { - help.OutputIndividualFeature(args[1]); - return true; - } - - // Otherwise, show generic help - else - { - help.OutputGenericHelp(); - return true; - } - } - } -} diff --git a/Headerer/Features/DisplayHelpDetailed.cs b/Headerer/Features/DisplayHelpDetailed.cs deleted file mode 100644 index 639cec12..00000000 --- a/Headerer/Features/DisplayHelpDetailed.cs +++ /dev/null @@ -1,35 +0,0 @@ -using SabreTools.Help; - -namespace Headerer.Features -{ - internal class DisplayHelpDetailed : BaseFeature - { - public const string Value = "Help (Detailed)"; - - public DisplayHelpDetailed() - { - Name = Value; - Flags.AddRange(["??", "hd", "help-detailed"]); - Description = "Show this detailed help"; - _featureType = ParameterType.Flag; - LongDescription = "Display a detailed help text to the screen."; - } - - public override bool ProcessArgs(string[] args, FeatureSet help) - { - // If we had something else after help - if (args.Length > 1) - { - help.OutputIndividualFeature(args[1], includeLongDescription: true); - return true; - } - - // Otherwise, show generic help - else - { - help.OutputAllHelp(); - return true; - } - } - } -} diff --git a/Headerer/Headerer.csproj b/Headerer/Headerer.csproj index b2f5281a..4d281bba 100644 --- a/Headerer/Headerer.csproj +++ b/Headerer/Headerer.csproj @@ -35,11 +35,6 @@ net6.0;net7.0;net8.0 - - - - - diff --git a/Headerer/Options.cs b/Headerer/Options.cs new file mode 100644 index 00000000..837dba1a --- /dev/null +++ b/Headerer/Options.cs @@ -0,0 +1,129 @@ +using System; +using System.Collections.Generic; + +namespace Headerer +{ + internal sealed class Options + { + #region Properties + + /// + /// Set of input paths to use for operations + /// + public List InputPaths { get; private set; } = []; + + /// + /// Represents the feature being called + /// + public Feature Feature { get; private set; } = Feature.NONE; + + /// + /// Optional output directory + /// + public string? OutputDir { get; private set; } + + #region Extraction + + /// + /// Disable storing copier headers on extract + /// + public bool NoStoreHeader { get; private set; } + + #endregion + + #endregion + + /// + /// Parse commandline arguments into an Options object + /// + public static Options? ParseOptions(string[] args) + { + // If we have invalid arguments + if (args == null || args.Length == 0) + return null; + + // Create an Options object + var options = new Options(); + + // Get the first argument as a feature flag + string featureName = args[0]; + switch (featureName) + { + case "ex": + case "extract": + options.Feature = Feature.Extract; + break; + + case "re": + case "restore": + options.Feature = Feature.Restore; + break; + + default: + Console.WriteLine($"{featureName} is not a recognized feature"); + return null; + } + + // Parse the options and paths + int index = 1; + for (; index < args.Length; index++) + { + string arg = args[index]; + switch (arg) + { + case "-o": + case "--outdir": + options.OutputDir = index + 1 < args.Length ? args[++index] : string.Empty; + break; + + #region Extraction + + case "-nsh": + case "--no-store-header": + options.NoStoreHeader = true; + break; + + #endregion + + default: + options.InputPaths.Add(arg); + break; + } + } + + // Validate we have any input paths to work on + if (options.InputPaths.Count == 0) + { + Console.WriteLine("At least one path is required!"); + return null; + } + + return options; + } + + /// + /// Display help text + /// + /// Additional error text to display, can be null to ignore + public static void DisplayHelp(string? err = null) + { + if (!string.IsNullOrEmpty(err)) + Console.WriteLine($"Error: {err}"); + + Console.WriteLine("Headerer - Remove, store, and restore copier headers"); + Console.WriteLine(); + Console.WriteLine("Headerer.exe file|directory ..."); + Console.WriteLine(); + Console.WriteLine("Features:"); + Console.WriteLine("ex, extract Extract and remove copier headers"); + Console.WriteLine("re, restore Restore header to file based on SHA-1"); + Console.WriteLine(); + Console.WriteLine("Common options:"); + Console.WriteLine("-?, -h, --help Display this help text and quit"); + Console.WriteLine("-o, --outdir [PATH] Set output directory"); + Console.WriteLine(); + Console.WriteLine("Extraction options:"); + Console.WriteLine("-nsh, --no-store-header Set output path for extraction (required)"); + } + } +} \ No newline at end of file diff --git a/Headerer/Program.cs b/Headerer/Program.cs index 6445fbd8..df8edd8f 100644 --- a/Headerer/Program.cs +++ b/Headerer/Program.cs @@ -1,179 +1,44 @@ -using System; -using System.Collections.Generic; -using System.IO; -using Headerer.Features; -using SabreTools.Core; -using SabreTools.Help; -using SabreTools.IO; -using SabreTools.IO.Logging; - -namespace Headerer +namespace Headerer { public class Program { - #region Static Variables - - /// - /// Help object that determines available functionality - /// - private static FeatureSet? _help; - - /// - /// Logging object - /// - private static readonly Logger logger = new(); - - #endregion - /// /// Entry point for the SabreTools application /// /// String array representing command line parameters public static void Main(string[] args) { - // Perform initial setup and verification - LoggerImpl.SetFilename(Path.Combine(PathTool.GetRuntimeDirectory(), "logs", "headerer.log"), true); - LoggerImpl.AppendPrefix = true; - LoggerImpl.LowestLogLevel = LogLevel.VERBOSE; - LoggerImpl.ThrowOnError = false; - LoggerImpl.Start(); - - // Create a new Help object for this program - _help = RetrieveHelp(); - - // Credits take precidence over all - if (new List(args).Contains("--credits")) + // Validate the arguments + if (args == null || args.Length == 0) { - FeatureSet.OutputCredits(); - LoggerImpl.Close(); + Options.DisplayHelp("One input file path required"); return; } - // If there's no arguments, show help - if (args.Length == 0) + // Get the options from the arguments + var options = Options.ParseOptions(args); + + // If we have an invalid state + if (options == null) { - _help.OutputGenericHelp(); - LoggerImpl.Close(); + Options.DisplayHelp(); return; } - // Get the first argument as a feature flag - string featureName = args[0]; - - // Verify that the flag is valid - if (!_help.TopLevelFlag(featureName)) + // Loop through the input paths + foreach (string inputPath in options.InputPaths) { - logger.User($"'{featureName}' is not valid feature flag"); - _help.OutputIndividualFeature(featureName); - LoggerImpl.Close(); - return; - } + // TODO: Do something with the output success flags + switch (options.Feature) + { + case Feature.Extract: + _ = Extract.DetectTransformStore(inputPath, options.OutputDir, options.NoStoreHeader); + break; - // Get the proper name for the feature - featureName = _help.GetFeatureName(featureName); - - // Get the associated feature - BaseFeature feature = (_help[featureName] as BaseFeature)!; - - // If we had the help feature first - if (featureName == DisplayHelp.Value || featureName == DisplayHelpDetailed.Value) - { - feature.ProcessArgs(args, _help); - LoggerImpl.Close(); - return; - } - - // Now verify that all other flags are valid - if (!feature.ProcessArgs(args, _help)) - { - LoggerImpl.Close(); - return; - } - - // Set the new log level based on settings - LoggerImpl.LowestLogLevel = feature.LogLevel; - - // If output is being redirected or we are in script mode, don't allow clear screens - if (!Console.IsOutputRedirected && feature.ScriptMode) - { - Console.Clear(); - Globals.SetConsoleHeader("Headerer"); - } - - // Now process the current feature - Dictionary features = _help.GetEnabledFeatures(); - bool success = false; - switch (featureName) - { - // No-op as these should be caught - case DisplayHelp.Value: - case DisplayHelpDetailed.Value: - break; - - // Require input verification - case Extract.Value: - case Restore.Value: - VerifyInputs(feature.Inputs, feature); - success = feature.ProcessFeatures(features); - break; - - // If nothing is set, show the help - default: - _help.OutputGenericHelp(); - break; - } - - // If the feature failed, output help - if (!success) - { - logger.Error("An error occurred during processing!"); - _help.OutputIndividualFeature(featureName); - } - - LoggerImpl.Close(); - return; - } - - /// - /// Generate a Help object for this program - /// - /// Populated Help object - private static FeatureSet RetrieveHelp() - { - // Create and add the header to the Help object - string barrier = "-----------------------------------------"; - List helpHeader = - [ - "Headerer - Remove, store, and restore copier headers", - barrier, - "Usage: Headerer [option] [flags] [filename|dirname] ...", - string.Empty - ]; - - // Create the base help object with header - var help = new FeatureSet(helpHeader); - - // Add all of the features - help.Add(new DisplayHelp()); - help.Add(new DisplayHelpDetailed()); - help.Add(new Extract()); - help.Add(new Restore()); - - return help; - } - - /// - /// Verify that there are inputs, show help otherwise - /// - /// List of inputs - /// Name of the current feature - private static void VerifyInputs(List inputs, BaseFeature feature) - { - if (inputs.Count == 0) - { - logger.Error("This feature requires at least one input"); - _help?.OutputIndividualFeature(feature.Name); - Environment.Exit(0); + case Feature.Restore: + _ = Restore.RestoreHeader(inputPath, options.OutputDir); + break; + } } } } diff --git a/Headerer/Features/Restore.cs b/Headerer/Restore.cs similarity index 65% rename from Headerer/Features/Restore.cs rename to Headerer/Restore.cs index 4aa37ef1..28c07230 100644 --- a/Headerer/Features/Restore.cs +++ b/Headerer/Restore.cs @@ -1,64 +1,21 @@ -using System.IO; +using System; using System.Collections.Generic; +using System.IO; using Microsoft.Data.Sqlite; using SabreTools.Hashing; -using SabreTools.Help; -using SabreTools.IO; using SabreTools.IO.Extensions; -namespace Headerer.Features +namespace Headerer { - internal class Restore : BaseFeature + internal static class Restore { - public const string Value = "Restore"; - - public Restore() - { - Name = Value; - Flags.AddRange(["re", "restore"]); - Description = "Restore header to file based on SHA-1"; - _featureType = ParameterType.Flag; - LongDescription = @"This will make use of stored copier headers and reapply them to files if they match the included hash. More than one header can be applied to a file, so they will be output to new files, suffixed with .newX, where X is a number. No input files are altered in the process. Only uncompressed files will be processed. - -The following systems have headers that this program can work with: - - Atari 7800 - - Atari Lynx - - Commodore PSID Music - - NEC PC - Engine / TurboGrafx 16 - - Nintendo Famicom / Nintendo Entertainment System - - Nintendo Famicom Disk System - - Nintendo Super Famicom / Super Nintendo Entertainment System - - Nintendo Super Famicom / Super Nintendo Entertainment System SPC"; - - // Common Features - AddCommonFeatures(); - - AddFeature(OutputDirStringInput); - } - - public override bool ProcessFeatures(Dictionary features) - { - // If the base fails, just fail out - if (!base.ProcessFeatures(features)) - return false; - - // Get only files from the inputs - List files = PathTool.GetFilesOnly(Inputs); - foreach (ParentablePath file in files) - { - RestoreHeader(file.CurrentPath, OutputDir); - } - - return true; - } - /// /// Detect and replace header(s) to the given file /// /// Name of the file to be parsed /// Output directory to write the file to, empty means the same directory as the input file /// True if a header was found and appended, false otherwise - public bool RestoreHeader(string file, string? outDir) + public static bool RestoreHeader(string file, string? outDir) { // Create the output directory if it doesn't exist if (!string.IsNullOrWhiteSpace(outDir) && !Directory.Exists(outDir)) @@ -78,9 +35,9 @@ The following systems have headers that this program can work with: for (int i = 0; i < headers.Count; i++) { string outputFile = (string.IsNullOrWhiteSpace(outDir) ? $"{Path.GetFullPath(file)}.new" : Path.Combine(outDir, Path.GetFileName(file))) + i; - logger.User($"Creating reheadered file: {outputFile}"); + Console.WriteLine($"Creating reheadered file: {outputFile}"); AppendBytes(file, outputFile, ByteArrayExtensions.StringToByteArray(headers[i]), null); - logger.User("Reheadered file created!"); + Console.WriteLine("Reheadered file created!"); } return true; @@ -91,33 +48,33 @@ The following systems have headers that this program can work with: /// /// SHA-1 of the deheadered file /// List of strings representing the headers to add - private List RetrieveHeadersFromDatabase(string SHA1) + private static List RetrieveHeadersFromDatabase(string SHA1) { // Ensure the database exists - EnsureDatabase(); + Database.EnsureDatabase(); // Open the database connection - SqliteConnection dbc = new SqliteConnection(HeadererConnectionString); + var dbc = new SqliteConnection(Database.HeadererConnectionString); dbc.Open(); // Create the output list of headers List headers = []; string query = $"SELECT header, type FROM data WHERE sha1='{SHA1}'"; - SqliteCommand slc = new SqliteCommand(query, dbc); + var slc = new SqliteCommand(query, dbc); SqliteDataReader sldr = slc.ExecuteReader(); if (sldr.HasRows) { while (sldr.Read()) { - logger.Verbose($"Found match with rom type '{sldr.GetString(1)}'"); + Console.WriteLine($"Found match with rom type '{sldr.GetString(1)}'"); // TODO: Gate behind debug flag headers.Add(sldr.GetString(0)); } } else { - logger.Warning("No matching header could be found!"); + Console.Error.WriteLine("No matching header could be found!"); } // Dispose of database objects