Use CommandLine library for executable

This commit is contained in:
Matt Nadareski
2025-10-06 08:57:26 -04:00
parent 1a69113af7
commit 07676f4dcc
9 changed files with 419 additions and 182 deletions

194
NDecrypt/BaseFeature.cs Normal file
View File

@@ -0,0 +1,194 @@
using System;
using System.Collections.Generic;
using System.IO;
using NDecrypt.Core;
using SabreTools.CommandLine;
using SabreTools.CommandLine.Inputs;
namespace NDecrypt
{
internal abstract class BaseFeature : Feature
{
#region Common Inputs
protected const string ConfigName = "config";
protected readonly StringInput ConfigString = new(ConfigName, ["-c", "--config"], "Path to config.json");
protected const string DevelopmentName = "development";
protected readonly FlagInput DevelopmentFlag = new(DevelopmentName, ["-d", "--development"], "Enable using development keys, if available");
protected const string ForceName = "force";
protected readonly FlagInput ForceFlag = new(ForceName, ["-f", "--force"], "Force operation by avoiding sanity checks");
protected const string HashName = "hash";
protected readonly FlagInput HashFlag = new(HashName, "--hash", "Output size and hashes to a companion file");
protected const string OverwriteName = "overwrite";
protected readonly FlagInput OverwriteFlag = new(OverwriteName, ["-o", "--overwrite"], "Overwrite input files instead of creating new ones");
#endregion
/// <summary>
/// Mapping of reusable tools
/// </summary>
private readonly Dictionary<FileType, ITool> _tools = [];
protected BaseFeature(string name, string[] flags, string description, string? detailed = null)
: base(name, flags, description, detailed)
{
}
/// <inheritdoc/>
public override bool Execute()
{
// Initialize required pieces
InitializeTools();
for (int i = 0; i < Inputs.Count; i++)
{
if (File.Exists(Inputs[i]))
{
ProcessFile(Inputs[i]);
}
else if (Directory.Exists(Inputs[i]))
{
foreach (string file in Directory.GetFiles(Inputs[i], "*", SearchOption.AllDirectories))
{
ProcessFile(file);
}
}
else
{
Console.WriteLine($"{Inputs[i]} is not a file or folder. Please check your spelling and formatting and try again.");
}
}
return true;
}
/// <inheritdoc/>
public override bool VerifyInputs() => Inputs.Count > 0;
/// <summary>
/// Process a single file path
/// </summary>
/// <param name="input">File path to process</param>
protected abstract void ProcessFile(string input);
/// <summary>
/// Initialize the tools to be used by the feature
/// </summary>
private void InitializeTools()
{
var decryptArgs = new DecryptArgs(GetString(ConfigName));
_tools[FileType.NDS] = new DSTool(decryptArgs);
_tools[FileType.N3DS] = new ThreeDSTool(GetBoolean(DevelopmentName), decryptArgs);
}
/// <summary>
/// Derive the encryption tool to be used for the given file
/// </summary>
/// <param name="filename">Filename to derive the tool from</param>
protected ITool? DeriveTool(string filename)
{
if (!File.Exists(filename))
{
Console.WriteLine($"{filename} does not exist! Skipping...");
return null;
}
FileType type = DetermineFileType(filename);
return type switch
{
FileType.NDS => _tools[FileType.NDS],
FileType.NDSi => _tools[FileType.NDS],
FileType.iQueDS => _tools[FileType.NDS],
FileType.N3DS => _tools[FileType.N3DS],
_ => null,
};
}
/// <summary>
/// Derive an output filename from the input, if possible
/// </summary>
/// <param name="filename">Name of the input file to derive from</param>
/// <param name="extension">Preferred extension set by the feature implementation</param>
/// <returns>Output filename based on the input</returns>
protected static string GetOutputFile(string filename, string extension)
{
// Empty filenames are passed back
if (filename.Length == 0)
return filename;
// TODO: Replace the suffix instead of just appending
// TODO: Ensure that the input and output aren't the same
// If the extension does not include a leading period
if (!extension.StartsWith("."))
extension = $".{extension}";
// Append the extension and return
return $"{filename}{extension}";
}
/// <summary>
/// Write out the hashes of a file to a named file
/// </summary>
/// <param name="filename">Filename to get hashes for/param>
protected static void WriteHashes(string filename)
{
// If the file doesn't exist, don't try anything
if (!File.Exists(filename))
return;
// Get the hash string from the file
string? hashString = HashingHelper.GetInfo(filename);
if (hashString == null)
return;
// Open the output file and write the hashes
using var fs = File.Open(Path.GetFullPath(filename) + ".hash", FileMode.Create, FileAccess.Write, FileShare.None);
using var sw = new StreamWriter(fs);
sw.Write(hashString);
}
/// <summary>
/// Determine the file type from the filename extension
/// </summary>
/// <param name="filename">Filename to derive the type from</param>
/// <returns>FileType value, if possible</returns>
private FileType DetermineFileType(string filename)
{
if (filename.EndsWith(".nds", StringComparison.OrdinalIgnoreCase) // Standard carts
|| filename.EndsWith(".nds.dec", StringComparison.OrdinalIgnoreCase) // Carts/images with secure area decrypted
|| filename.EndsWith(".nds.enc", StringComparison.OrdinalIgnoreCase) // Carts/images with secure area encrypted
|| filename.EndsWith(".srl", StringComparison.OrdinalIgnoreCase)) // Development carts/images
{
Console.WriteLine("File recognized as Nintendo DS");
return FileType.NDS;
}
else if (filename.EndsWith(".dsi", StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine("File recognized as Nintendo DSi");
return FileType.NDSi;
}
else if (filename.EndsWith(".ids", StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine("File recognized as iQue DS");
return FileType.iQueDS;
}
else if (filename.EndsWith(".3ds", StringComparison.OrdinalIgnoreCase) // Standard carts
|| filename.EndsWith(".3ds.dec", StringComparison.OrdinalIgnoreCase) // Decrypted carts/images
|| filename.EndsWith(".3ds.enc", StringComparison.OrdinalIgnoreCase) // Encrypted carts/images
|| filename.EndsWith(".cci", StringComparison.OrdinalIgnoreCase)) // Development carts/images
{
Console.WriteLine("File recognized as Nintendo 3DS");
return FileType.N3DS;
}
Console.WriteLine($"Unrecognized file format for {filename}. Expected *.nds, *.srl, *.dsi, *.3ds, *.cci");
return FileType.NULL;
}
}
}

View File

@@ -0,0 +1,57 @@
using System;
namespace NDecrypt
{
internal sealed class DecryptFeature : BaseFeature
{
#region Feature Definition
public const string DisplayName = "decrypt";
private static readonly string[] _flags = ["d", "decrypt"];
private const string _description = "Decrypt the input files";
#endregion
public DecryptFeature()
: base(DisplayName, _flags, _description)
{
RequiresInputs = true;
Add(ConfigString);
Add(DevelopmentFlag);
Add(ForceFlag);
Add(HashFlag);
// TODO: Include this when enabled
// Add(OverwriteFlag);
}
/// <inheritdoc/>
protected override void ProcessFile(string input)
{
// Attempt to derive the tool for the path
var tool = DeriveTool(input);
if (tool == null)
return;
// Derive the output filename, if required
string? output = null;
if (!GetBoolean(OverwriteName))
output = GetOutputFile(input, ".dec");
Console.WriteLine($"Processing {input}");
if (!tool.DecryptFile(input, output, GetBoolean(ForceName)))
{
Console.WriteLine("Decryption failed!");
return;
}
// Output the file hashes, if expected
if (GetBoolean(HashName))
WriteHashes(input);
}
}
}

View File

@@ -0,0 +1,57 @@
using System;
namespace NDecrypt
{
internal sealed class EncryptFeature : BaseFeature
{
#region Feature Definition
public const string DisplayName = "encrypt";
private static readonly string[] _flags = ["e", "encrypt"];
private const string _description = "Encrypt the input files";
#endregion
public EncryptFeature()
: base(DisplayName, _flags, _description)
{
RequiresInputs = true;
Add(ConfigString);
Add(DevelopmentFlag);
Add(ForceFlag);
Add(HashFlag);
// TODO: Include this when enabled
// Add(OverwriteFlag);
}
/// <inheritdoc/>
protected override void ProcessFile(string input)
{
// Attempt to derive the tool for the path
var tool = DeriveTool(input);
if (tool == null)
return;
// Derive the output filename, if required
string? output = null;
if (!GetBoolean(OverwriteName))
output = GetOutputFile(input, ".enc");
Console.WriteLine($"Processing {input}");
if (!tool.EncryptFile(input, output, GetBoolean(ForceName)))
{
Console.WriteLine("Encryption failed!");
return;
}
// Output the file hashes, if expected
if (GetBoolean(HashName))
WriteHashes(input);
}
}
}

View File

@@ -3,7 +3,7 @@ namespace NDecrypt
/// <summary>
/// Functionality to use from the program
/// </summary>
internal enum Feature
internal enum FeatureFlag
{
NULL,
Decrypt,

View File

@@ -27,7 +27,7 @@ namespace NDecrypt
+ $"CRC-32: {(hashDict.ContainsKey(HashType.CRC32) ? hashDict[HashType.CRC32] : string.Empty)}\n"
+ $"MD5: {(hashDict.ContainsKey(HashType.MD5) ? hashDict[HashType.MD5] : string.Empty)}\n"
+ $"SHA-1: {(hashDict.ContainsKey(HashType.SHA1) ? hashDict[HashType.SHA1] : string.Empty)}\n"
+ $"CSHA-256: {(hashDict.ContainsKey(HashType.SHA256) ? hashDict[HashType.SHA256] : string.Empty)}\n";
+ $"SHA-256: {(hashDict.ContainsKey(HashType.SHA256) ? hashDict[HashType.SHA256] : string.Empty)}\n";
}
}
}

45
NDecrypt/InfoFeature.cs Normal file
View File

@@ -0,0 +1,45 @@
using System;
namespace NDecrypt
{
internal sealed class InfoFeature : BaseFeature
{
#region Feature Definition
public const string DisplayName = "info";
private static readonly string[] _flags = ["i", "info"];
private const string _description = "Output file information";
#endregion
public InfoFeature()
: base(DisplayName, _flags, _description)
{
RequiresInputs = true;
Add(HashFlag);
}
/// <inheritdoc/>
protected override void ProcessFile(string input)
{
// Attempt to derive the tool for the path
var tool = DeriveTool(input);
if (tool == null)
return;
Console.WriteLine($"Processing {input}");
string? infoString = tool.GetInformation(input);
infoString ??= "There was a problem getting file information!";
Console.WriteLine(infoString);
// Output the file hashes, if expected
if (GetBoolean(HashName))
WriteHashes(input);
}
}
}

View File

@@ -44,6 +44,7 @@
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="SabreTools.CommandLine" Version="[1.3.2]" />
</ItemGroup>
</Project>

View File

@@ -17,7 +17,7 @@ namespace NDecrypt
/// <summary>
/// Feature to process input files with
/// </summary>
public Feature Feature { get; private set; }
public FeatureFlag Feature { get; private set; }
/// <summary>
/// Path to config.json
@@ -77,17 +77,17 @@ namespace NDecrypt
case "d":
case "decrypt":
options.Feature = Feature.Decrypt;
options.Feature = FeatureFlag.Decrypt;
break;
case "e":
case "encrypt":
options.Feature = Feature.Encrypt;
options.Feature = FeatureFlag.Encrypt;
break;
case "i":
case "info":
options.Feature = Feature.Info;
options.Feature = FeatureFlag.Info;
break;
default:
@@ -190,7 +190,7 @@ namespace NDecrypt
Console.WriteLine("-d, --development Enable using development keys, if available");
Console.WriteLine("-f, --force Force operation by avoiding sanity checks");
Console.WriteLine("--hash Output size and hashes to a companion file");
// Console.WriteLine("-o, --overwrite Overwrite input files instead of creating new ones"); // TODO: Print this when enabled
// Console.WriteLine("-o, --overwrite Overwrite input files instead of creating new ones");
Console.WriteLine();
Console.WriteLine("<path> can be any file or folder that contains uncompressed items.");
Console.WriteLine("More than one path can be specified at a time.");

View File

@@ -1,204 +1,87 @@
using System;
using System.Collections.Generic;
using System.IO;
using NDecrypt.Core;
using SabreTools.CommandLine;
using SabreTools.CommandLine.Features;
namespace NDecrypt
{
class Program
{
/// <summary>
/// Mapping of reusable tools
/// </summary>
private static readonly Dictionary<FileType, ITool> _tools = [];
public static void Main(string[] args)
{
// Get the options from the arguments
var options = Options.ParseOptions(args);
var commandSet = CreateCommands();
// If we have an invalid state
if (options == null)
// If we have no args, show the help and quit
if (args == null || args.Length == 0)
{
Options.DisplayHelp();
commandSet.OutputAllHelp();
return;
}
// Initialize the decrypt args, if possible
var decryptArgs = new DecryptArgs(options.ConfigPath); ;
// Get the first argument as a feature flag
string featureName = args[0];
// Create reusable tools
_tools[FileType.NDS] = new DSTool(decryptArgs);
_tools[FileType.N3DS] = new ThreeDSTool(options.Development, decryptArgs);
for (int i = 0; i < options.InputPaths.Count; i++)
// Get the associated feature
var topLevel = commandSet.GetTopLevel(featureName);
if (topLevel == null || topLevel is not Feature feature)
{
if (File.Exists(options.InputPaths[i]))
{
ProcessFile(options.InputPaths[i], options);
}
else if (Directory.Exists(options.InputPaths[i]))
{
foreach (string file in Directory.GetFiles(options.InputPaths[i], "*", SearchOption.AllDirectories))
{
ProcessFile(file, options);
}
}
else
{
Console.WriteLine($"{options.InputPaths[i]} is not a file or folder. Please check your spelling and formatting and try again.");
}
Console.WriteLine($"'{featureName}' is not valid feature flag");
commandSet.OutputFeatureHelp(featureName);
return;
}
// Handle default help functionality
if (topLevel is Help helpFeature)
{
helpFeature.ProcessArgs(args, 0, commandSet);
return;
}
// Now verify that all other flags are valid
if (!feature.ProcessArgs(args, 1))
return;
// If inputs are required
if (feature.RequiresInputs && !feature.VerifyInputs())
{
commandSet.OutputFeatureHelp(topLevel.Name);
Environment.Exit(0);
}
// Now execute the current feature
if (!feature.Execute())
{
Console.Error.WriteLine("An error occurred during processing!");
commandSet.OutputFeatureHelp(topLevel.Name);
}
}
/// <summary>
/// Process a single file path
/// Create the command set for the program
/// </summary>
/// <param name="input">File path to process</param>
/// <param name="options">Options indicating how to process the file</param>
private static void ProcessFile(string input, Options options)
private static CommandSet CreateCommands()
{
// Attempt to derive the tool for the path
var tool = DeriveTool(input);
if (tool == null)
return;
List<string> header = [
"Cart Image Encrypt/Decrypt Tool",
string.Empty,
"NDecrypt <operation> [options] <path> ...",
string.Empty,
];
Console.WriteLine($"Processing {input}");
List<string> footer = [
string.Empty,
"<path> can be any file or folder that contains uncompressed items.",
"More than one path can be specified at a time.",
];
// Derive the output filename, if required
string? output = null;
if (!options.Overwrite)
output = GetOutputFile(input, options);
var commandSet = new CommandSet(header, footer);
// Encrypt or decrypt the file as requested
if (options.Feature == Feature.Encrypt && !tool.EncryptFile(input, output, options.Force))
{
Console.WriteLine("Encryption failed!");
return;
}
else if (options.Feature == Feature.Decrypt && !tool.DecryptFile(input, output, options.Force))
{
Console.WriteLine("Decryption failed!");
return;
}
else if (options.Feature == Feature.Info)
{
string? infoString = tool.GetInformation(input);
infoString ??= "There was a problem getting file information!";
commandSet.Add(new Help());
commandSet.Add(new EncryptFeature());
commandSet.Add(new DecryptFeature());
commandSet.Add(new InfoFeature());
Console.WriteLine(infoString);
}
// Output the file hashes, if expected
if (options.OutputHashes)
WriteHashes(input);
}
/// <summary>
/// Derive the encryption tool to be used for the given file
/// </summary>
/// <param name="filename">Filename to derive the tool from</param>
private static ITool? DeriveTool(string filename)
{
if (!File.Exists(filename))
{
Console.WriteLine($"{filename} does not exist! Skipping...");
return null;
}
FileType type = DetermineFileType(filename);
return type switch
{
FileType.NDS => _tools[FileType.NDS],
FileType.NDSi => _tools[FileType.NDS],
FileType.iQueDS => _tools[FileType.NDS],
FileType.N3DS => _tools[FileType.N3DS],
_ => null,
};
}
/// <summary>
/// Determine the file type from the filename extension
/// </summary>
/// <param name="filename">Filename to derive the type from</param>
/// <returns>FileType value, if possible</returns>
private static FileType DetermineFileType(string filename)
{
if (filename.EndsWith(".nds", StringComparison.OrdinalIgnoreCase) // Standard carts
|| filename.EndsWith(".nds.dec", StringComparison.OrdinalIgnoreCase) // Carts/images with secure area decrypted
|| filename.EndsWith(".nds.enc", StringComparison.OrdinalIgnoreCase) // Carts/images with secure area encrypted
|| filename.EndsWith(".srl", StringComparison.OrdinalIgnoreCase)) // Development carts/images
{
Console.WriteLine("File recognized as Nintendo DS");
return FileType.NDS;
}
else if (filename.EndsWith(".dsi", StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine("File recognized as Nintendo DSi");
return FileType.NDSi;
}
else if (filename.EndsWith(".ids", StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine("File recognized as iQue DS");
return FileType.iQueDS;
}
else if (filename.EndsWith(".3ds", StringComparison.OrdinalIgnoreCase) // Standard carts
|| filename.EndsWith(".3ds.dec", StringComparison.OrdinalIgnoreCase) // Decrypted carts/images
|| filename.EndsWith(".3ds.enc", StringComparison.OrdinalIgnoreCase) // Encrypted carts/images
|| filename.EndsWith(".cci", StringComparison.OrdinalIgnoreCase)) // Development carts/images
{
Console.WriteLine("File recognized as Nintendo 3DS");
return FileType.N3DS;
}
Console.WriteLine($"Unrecognized file format for {filename}. Expected *.nds, *.srl, *.dsi, *.3ds, *.cci");
return FileType.NULL;
}
/// <summary>
/// Derive an output filename from the input, if possible
/// </summary>
/// <param name="filename">Name of the input file to derive from</param>
/// <param name="options">Options indicating how to process the file</param>
/// <returns>Output filename based on the input</returns>
private static string GetOutputFile(string filename, Options options)
{
// Empty filenames are passed back
if (filename.Length == 0)
return filename;
// TODO: Replace the suffix instead of just appending
// TODO: Ensure that the input and output aren't the same
// Append '.enc' or '.dec' based on the feature
if (options.Feature == Feature.Decrypt)
filename += ".dec";
else if (options.Feature == Feature.Encrypt)
filename += ".enc";
// Return the reformatted name
return filename;
}
/// <summary>
/// Write out the hashes of a file to a named file
/// </summary>
/// <param name="filename">Filename to get hashes for/param>
private static void WriteHashes(string filename)
{
// If the file doesn't exist, don't try anything
if (!File.Exists(filename))
return;
// Get the hash string from the file
string? hashString = HashingHelper.GetInfo(filename);
if (hashString == null)
return;
// Open the output file and write the hashes
using var fs = File.Create(Path.GetFullPath(filename) + ".hash");
using var sw = new StreamWriter(fs);
sw.WriteLine(hashString);
return commandSet;
}
}
}