Further disassociate Headerer

This commit is contained in:
Matt Nadareski
2024-10-24 20:26:18 -04:00
parent 2b1cf8dfa5
commit 0c1a148593
10 changed files with 229 additions and 517 deletions

43
Headerer/Database.cs Normal file
View File

@@ -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
/// <summary>
/// Ensure that the database exists and has the proper schema
/// </summary>
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();
}
}
}

View File

@@ -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<string, Feature?> 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<ParentablePath> files = PathTool.GetFilesOnly(Inputs);
foreach (ParentablePath file in files)
{
DetectTransformStore(file.CurrentPath, OutputDir, nostore);
}
return true;
}
/// <summary>
/// Detect header skipper compliance and create an output file
/// </summary>
@@ -64,13 +16,13 @@ The following systems have headers that this program can work with:
/// <param name="outDir">Output directory to write the file to, empty means the same directory as the input file</param>
/// <param name="nostore">True if headers should not be stored in the database, false otherwise</param>
/// <returns>True if the output file was created, false otherwise</returns>
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:
/// <param name="header">String representing the header bytes</param>
/// <param name="SHA1">SHA-1 of the deheadered file</param>
/// <param name="type">Name of the source skipper file</param>
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

10
Headerer/Feature.cs Normal file
View File

@@ -0,0 +1,10 @@
namespace Headerer
{
internal enum Feature
{
NONE = 0,
Extract,
Restore,
}
}

View File

@@ -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
/// <summary>
/// Logging object
/// </summary>
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<string>() { "-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<string>() { "-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<string>() { "-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<string>() { "-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
/// <summary>
/// Lowest log level for output
/// </summary>
public LogLevel LogLevel { get; protected set; }
/// <summary>
/// Output directory
/// </summary>
protected string? OutputDir { get; set; }
/// <summary>
/// Determines if scripting mode is enabled
/// </summary>
public bool ScriptMode { get; protected set; }
#endregion
#region Add Feature Groups
/// <summary>
/// Add common features
/// </summary>
protected void AddCommonFeatures()
{
AddFeature(ScriptFlag);
AddFeature(LogLevelStringInput);
}
#endregion
public override bool ProcessFeatures(Dictionary<string, Feature?> features)
{
LogLevel = GetString(features, LogLevelStringValue).AsLogLevel();
OutputDir = GetString(features, OutputDirStringValue)?.Trim('"');
ScriptMode = GetBoolean(features, ScriptValue);
return true;
}
#region Protected Helpers
/// <summary>
/// Ensure that the database exists and has the proper schema
/// </summary>
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
}
}

View File

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

View File

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

View File

@@ -35,11 +35,6 @@
<TargetFrameworks>net6.0;net7.0;net8.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\SabreTools.Core\SabreTools.Core.csproj" />
<ProjectReference Include="..\SabreTools.Help\SabreTools.Help.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Data.Sqlite" Version="8.0.10" />
<PackageReference Include="SabreTools.Hashing" Version="1.2.2" />

129
Headerer/Options.cs Normal file
View File

@@ -0,0 +1,129 @@
using System;
using System.Collections.Generic;
namespace Headerer
{
internal sealed class Options
{
#region Properties
/// <summary>
/// Set of input paths to use for operations
/// </summary>
public List<string> InputPaths { get; private set; } = [];
/// <summary>
/// Represents the feature being called
/// </summary>
public Feature Feature { get; private set; } = Feature.NONE;
/// <summary>
/// Optional output directory
/// </summary>
public string? OutputDir { get; private set; }
#region Extraction
/// <summary>
/// Disable storing copier headers on extract
/// </summary>
public bool NoStoreHeader { get; private set; }
#endregion
#endregion
/// <summary>
/// Parse commandline arguments into an Options object
/// </summary>
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;
}
/// <summary>
/// Display help text
/// </summary>
/// <param name="err">Additional error text to display, can be null to ignore</param>
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 <features> <options> 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)");
}
}
}

View File

@@ -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
/// <summary>
/// Help object that determines available functionality
/// </summary>
private static FeatureSet? _help;
/// <summary>
/// Logging object
/// </summary>
private static readonly Logger logger = new();
#endregion
/// <summary>
/// Entry point for the SabreTools application
/// </summary>
/// <param name="args">String array representing command line parameters</param>
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<string>(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;
}
// 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)
// TODO: Do something with the output success flags
switch (options.Feature)
{
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<string, Feature?> features = _help.GetEnabledFeatures();
bool success = false;
switch (featureName)
{
// No-op as these should be caught
case DisplayHelp.Value:
case DisplayHelpDetailed.Value:
case Feature.Extract:
_ = Extract.DetectTransformStore(inputPath, options.OutputDir, options.NoStoreHeader);
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();
case Feature.Restore:
_ = Restore.RestoreHeader(inputPath, options.OutputDir);
break;
}
// If the feature failed, output help
if (!success)
{
logger.Error("An error occurred during processing!");
_help.OutputIndividualFeature(featureName);
}
LoggerImpl.Close();
return;
}
/// <summary>
/// Generate a Help object for this program
/// </summary>
/// <returns>Populated Help object</returns>
private static FeatureSet RetrieveHelp()
{
// Create and add the header to the Help object
string barrier = "-----------------------------------------";
List<string> 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;
}
/// <summary>
/// Verify that there are inputs, show help otherwise
/// </summary>
/// <param name="inputs">List of inputs</param>
/// <param name="feature">Name of the current feature</param>
private static void VerifyInputs(List<string> inputs, BaseFeature feature)
{
if (inputs.Count == 0)
{
logger.Error("This feature requires at least one input");
_help?.OutputIndividualFeature(feature.Name);
Environment.Exit(0);
}
}
}

View File

@@ -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<string, Feature?> features)
{
// If the base fails, just fail out
if (!base.ProcessFeatures(features))
return false;
// Get only files from the inputs
List<ParentablePath> files = PathTool.GetFilesOnly(Inputs);
foreach (ParentablePath file in files)
{
RestoreHeader(file.CurrentPath, OutputDir);
}
return true;
}
/// <summary>
/// Detect and replace header(s) to the given file
/// </summary>
/// <param name="file">Name of the file to be parsed</param>
/// <param name="outDir">Output directory to write the file to, empty means the same directory as the input file</param>
/// <returns>True if a header was found and appended, false otherwise</returns>
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:
/// </summary>
/// <param name="SHA1">SHA-1 of the deheadered file</param>
/// <returns>List of strings representing the headers to add</returns>
private List<string> RetrieveHeadersFromDatabase(string SHA1)
private static List<string> 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<string> 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