diff --git a/Il2CppInspector.Redux.CLI/CliClient.cs b/Il2CppInspector.Redux.CLI/CliClient.cs new file mode 100644 index 0000000..b88f5fa --- /dev/null +++ b/Il2CppInspector.Redux.CLI/CliClient.cs @@ -0,0 +1,136 @@ +using System.Threading.Channels; +using Il2CppInspector.Redux.FrontendCore; +using Microsoft.AspNetCore.SignalR.Client; +using Spectre.Console; + +namespace Il2CppInspector.Redux.CLI; + +public class CliClient : IDisposable +{ + public bool ImportCompleted { get; private set; } + + private volatile int _finishedLoadingCount = 0; + + private readonly HubConnection _connection; + private readonly List _commandListeners = []; + + private Channel? _logMessageChannel; + + public CliClient(HubConnection connection) + { + _connection = connection; + + _commandListeners.Add(_connection.On(nameof(UiClient.ShowLogMessage), ShowLogMessage)); + _commandListeners.Add(_connection.On(nameof(UiClient.BeginLoading), BeginLoading)); + _commandListeners.Add(_connection.On(nameof(UiClient.FinishLoading), FinishLoading)); + _commandListeners.Add(_connection.On(nameof(UiClient.ShowInfoToast), ShowInfoToast)); + _commandListeners.Add(_connection.On(nameof(UiClient.ShowSuccessToast), ShowSuccessToast)); + _commandListeners.Add(_connection.On(nameof(UiClient.ShowErrorToast), ShowErrorToast)); + _commandListeners.Add(_connection.On(nameof(UiClient.OnImportCompleted), OnImportCompleted)); + } + + public async ValueTask OnUiLaunched(CancellationToken cancellationToken = default) + { + await _connection.InvokeAsync(nameof(Il2CppHub.OnUiLaunched), cancellationToken); + } + + public async ValueTask SubmitInputFiles(List inputFiles, CancellationToken cancellationToken = default) + { + await _connection.InvokeAsync(nameof(Il2CppHub.SubmitInputFiles), inputFiles, cancellationToken); + } + + public async ValueTask QueueExport(string exportTypeId, string outputDirectory, Dictionary settings, + CancellationToken cancellationToken = default) + { + await _connection.InvokeAsync(nameof(Il2CppHub.QueueExport), exportTypeId, outputDirectory, settings, cancellationToken); + } + + public async ValueTask StartExport(CancellationToken cancellationToken = default) + { + await _connection.InvokeAsync(nameof(Il2CppHub.StartExport), cancellationToken); + } + + public async ValueTask> GetPotentialUnityVersions(CancellationToken cancellationToken = default) + => await _connection.InvokeAsync>(nameof(Il2CppHub.GetPotentialUnityVersions), cancellationToken); + + public async ValueTask ExportIl2CppFiles(string outputDirectory, CancellationToken cancellationToken = default) + { + await _connection.InvokeAsync(nameof(Il2CppHub.ExportIl2CppFiles), outputDirectory, cancellationToken); + } + + public async ValueTask GetInspectorVersion(CancellationToken cancellationToken = default) + => await _connection.InvokeAsync(nameof(Il2CppHub.GetInspectorVersion), cancellationToken); + + public async ValueTask WaitForLoadingToFinishAsync(CancellationToken cancellationToken = default) + { + var currentLoadingCount = _finishedLoadingCount; + while (_finishedLoadingCount == currentLoadingCount) + await Task.Delay(10, cancellationToken); + } + + private async Task ShowLogMessage(string message) + { + if (_logMessageChannel == null) + { + AnsiConsole.MarkupLine($"[white bold]{message}[/]"); + return; + } + + await _logMessageChannel.Writer.WriteAsync(message); + } + + private void BeginLoading() + { + _logMessageChannel = Channel.CreateUnbounded(new UnboundedChannelOptions + { + SingleReader = true, + SingleWriter = true, + AllowSynchronousContinuations = true + }); + + AnsiConsole.Status() + .Spinner(Spinner.Known.Triangle) + .StartAsync("Loading", async ctx => + { + await foreach (var newLogMessage in _logMessageChannel.Reader.ReadAllAsync()) + { + ctx.Status(newLogMessage); + } + }); + } + + private void FinishLoading() + { + _logMessageChannel?.Writer.Complete(); + Interlocked.Increment(ref _finishedLoadingCount); + } + + private static void ShowInfoToast(string message) + { + AnsiConsole.MarkupLineInterpolated($"[bold white]INFO: {message}[/]"); + } + + private static void ShowSuccessToast(string message) + { + AnsiConsole.MarkupLineInterpolated($"[bold][green]SUCCESS: [/] [white]{message}[/][/]"); + } + + private static void ShowErrorToast(string message) + { + AnsiConsole.MarkupLineInterpolated($"[bold][red]ERROR: [/] [white]{message}[/][/]"); + } + + private void OnImportCompleted() + { + ImportCompleted = true; + } + + + public void Dispose() + { + GC.SuppressFinalize(this); + + foreach (var listener in _commandListeners) + listener.Dispose(); + } +} \ No newline at end of file diff --git a/Il2CppInspector.Redux.CLI/Commands/BaseCommand.cs b/Il2CppInspector.Redux.CLI/Commands/BaseCommand.cs new file mode 100644 index 0000000..6987149 --- /dev/null +++ b/Il2CppInspector.Redux.CLI/Commands/BaseCommand.cs @@ -0,0 +1,38 @@ +using Il2CppInspector.Redux.FrontendCore; +using Microsoft.AspNetCore.SignalR.Client; +using Spectre.Console.Cli; + +namespace Il2CppInspector.Redux.CLI.Commands; + +internal abstract class BaseCommand(PortProvider portProvider) : AsyncCommand where T : CommandSettings +{ + private const string HubPath = "/il2cpp"; // TODO: Make this into a shared constant + + private readonly int _serverPort = portProvider.Port; + + protected abstract Task ExecuteAsync(CliClient client, T settings); + + public override async Task ExecuteAsync(CommandContext context, T settings) + { + var connection = new HubConnectionBuilder().WithUrl($"http://localhost:{_serverPort}{HubPath}") + .AddJsonProtocol(options => + { + options.PayloadSerializerOptions.TypeInfoResolverChain.Insert(0, + FrontendCoreJsonSerializerContext.Default); + }) + .Build(); + + await connection.StartAsync(); + + int result; + using (var client = new CliClient(connection)) + { + await client.OnUiLaunched(); + result = await ExecuteAsync(client, settings); + } + + await connection.StopAsync(); + + return result; + } +} \ No newline at end of file diff --git a/Il2CppInspector.Redux.CLI/Commands/InteractiveCommand.cs b/Il2CppInspector.Redux.CLI/Commands/InteractiveCommand.cs new file mode 100644 index 0000000..e61447a --- /dev/null +++ b/Il2CppInspector.Redux.CLI/Commands/InteractiveCommand.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNetCore.SignalR.Client; +using Spectre.Console; +using Spectre.Console.Cli; + +namespace Il2CppInspector.Redux.CLI.Commands; + +internal class InteractiveCommand(PortProvider portProvider) : BaseCommand(portProvider) +{ + public class Options : CommandSettings; + + protected override async Task ExecuteAsync(CliClient client, Options settings) + { + await Task.Delay(1000); + await AnsiConsole.AskAsync("meow?"); + return 0; + } +} \ No newline at end of file diff --git a/Il2CppInspector.Redux.CLI/Commands/ManualCommand.cs b/Il2CppInspector.Redux.CLI/Commands/ManualCommand.cs new file mode 100644 index 0000000..7bc888e --- /dev/null +++ b/Il2CppInspector.Redux.CLI/Commands/ManualCommand.cs @@ -0,0 +1,21 @@ +using Spectre.Console; +using Spectre.Console.Cli; + +namespace Il2CppInspector.Redux.CLI.Commands; + +internal abstract class ManualCommand(PortProvider portProvider) : BaseCommand(portProvider) where T : ManualCommandOptions +{ + public override ValidationResult Validate(CommandContext context, T settings) + { + foreach (var inputPath in settings.InputPaths) + { + if (!Path.Exists(inputPath)) + return ValidationResult.Error($"Provided input path {inputPath} does not exit."); + } + + if (File.Exists(settings.OutputPath)) + return ValidationResult.Error("Provided output path already exists as a file."); + + return ValidationResult.Success(); + } +} \ No newline at end of file diff --git a/Il2CppInspector.Redux.CLI/Commands/ManualCommandOptions.cs b/Il2CppInspector.Redux.CLI/Commands/ManualCommandOptions.cs new file mode 100644 index 0000000..b8902c8 --- /dev/null +++ b/Il2CppInspector.Redux.CLI/Commands/ManualCommandOptions.cs @@ -0,0 +1,15 @@ +using System.ComponentModel; +using Spectre.Console.Cli; + +namespace Il2CppInspector.Redux.CLI.Commands; + +internal class ManualCommandOptions : CommandSettings +{ + [CommandArgument(0, "")] + [Description("Paths to the input files. Will be subsequently loaded until binary and metadata were found.")] + public string[] InputPaths { get; init; } = []; + + [CommandOption("-o|--output")] + [Description("Path to the output folder")] + public string OutputPath { get; init; } = ""; +} \ No newline at end of file diff --git a/Il2CppInspector.Redux.CLI/Commands/ProcessCommand.cs b/Il2CppInspector.Redux.CLI/Commands/ProcessCommand.cs new file mode 100644 index 0000000..cd325d2 --- /dev/null +++ b/Il2CppInspector.Redux.CLI/Commands/ProcessCommand.cs @@ -0,0 +1,172 @@ +using Il2CppInspector.Cpp; +using Il2CppInspector.Redux.FrontendCore.Outputs; +using Spectre.Console; +using Spectre.Console.Cli; + +namespace Il2CppInspector.Redux.CLI.Commands; + +internal class ProcessCommand(PortProvider portProvider) : ManualCommand(portProvider) +{ + // NOTE: There might be a better option than replicating all available flags here (and in the TS UI). + // Investigate this in the future. + + public class Option : ManualCommandOptions + { + // C++ Scaffolding + [CommandOption("--output-cpp-scaffolding")] + public bool CppScaffolding { get; init; } = false; + + [CommandOption("--unity-version")] + public string? UnityVersion { get; init; } + + [CommandOption("--compiler-type")] + public CppCompilerType CompilerType { get; init; } = CppCompilerType.GCC; + + // C# stub + [CommandOption("-s|--output-csharp-stub")] + public bool CSharpStubs { get; init; } = false; + + [CommandOption("--layout")] + public CSharpLayout Layout { get; init; } = CSharpLayout.SingleFile; + + [CommandOption("--flatten-hierarchy")] + public bool FlattenHierarchy { get; init; } = false; + + [CommandOption("--sorting-mode")] + public TypeSortingMode SortingMode { get; init; } = TypeSortingMode.Alphabetical; + + [CommandOption("--suppress-metadata")] + public bool SuppressMetadata { get; init; } = false; + + [CommandOption("--compilable")] + public bool MustCompile { get; init; } = false; + + [CommandOption("--separate-assembly-attributes")] + public bool SeparateAssemblyAttributes { get; init; } = true; + + // Disassembler metadata + [CommandOption("-m|--output-disassembler-metadata")] + public bool DisassemblerMetadata { get; init; } = false; + + [CommandOption("--disassembler")] + public DisassemblerType Disassembler { get; init; } = DisassemblerType.IDA; + + // Dummy DLL output + [CommandOption("-d|--output-dummy-dlls")] + public bool DummyDlls { get; init; } = false; + + // Visual Studio solution + [CommandOption("--output-vs-solution")] + public bool VsSolution { get; init; } = false; + + [CommandOption("--unity-path")] + public string? UnityPath { get; init; } + + [CommandOption("--unity-assemblies-path")] + public string? UnityAssembliesPath { get; init; } + + [CommandOption("--extract-il2cpp-files")] + public string? ExtractIl2CppFilesPath { get; init; } + } + + protected override async Task ExecuteAsync(CliClient client, Option settings) + { + var inspectorVersion = await client.GetInspectorVersion(); + AnsiConsole.MarkupLineInterpolated($"Using inspector [gray]{inspectorVersion}[/]"); + + await client.SubmitInputFiles(settings.InputPaths.ToList()); + await client.WaitForLoadingToFinishAsync(); + if (!client.ImportCompleted) + { + AnsiConsole.MarkupLine("[bold][red]FAILED[/] to load IL2CPP data from the given inputs.[/]"); + return 1; + } + + if (settings.ExtractIl2CppFilesPath != null) + { + await client.ExportIl2CppFiles(settings.ExtractIl2CppFilesPath); + await client.WaitForLoadingToFinishAsync(); + } + + var unityVersions = await client.GetPotentialUnityVersions(); + + if (settings.CppScaffolding) + { + var directory = Path.Join(settings.OutputPath, "cpp"); + await client.QueueExport(CppScaffoldingOutput.Id, directory, new Dictionary + { + ["unityversion"] = settings.UnityVersion ?? unityVersions.First(), + ["compilertype"] = settings.CompilerType.ToString() + }); + } + + if (settings.CSharpStubs) + { + var directory = Path.Join(settings.OutputPath, "cs"); + await client.QueueExport(CSharpStubOutput.Id, directory, new Dictionary + { + ["layout"] = settings.Layout.ToString(), + ["flattenhierarchy"] = settings.FlattenHierarchy.ToString(), + ["sortingmode"] = settings.SortingMode.ToString(), + ["suppressmetadata"] = settings.SuppressMetadata.ToString(), + ["mustcompile"] = settings.MustCompile.ToString(), + ["separateassemblyattributes"] = settings.SeparateAssemblyAttributes.ToString() + }); + } + + if (settings.DisassemblerMetadata) + { + await client.QueueExport(DisassemblerMetadataOutput.Id, settings.OutputPath, + new Dictionary + { + ["disassembler"] = settings.Disassembler.ToString(), + ["unityversion"] = settings.UnityVersion ?? unityVersions.First() + }); + } + + if (settings.DummyDlls) + { + var directory = Path.Join(settings.OutputPath, "dll"); + await client.QueueExport(DummyDllOutput.Id, directory, new Dictionary + { + ["suppressmetadata"] = settings.SuppressMetadata.ToString() + }); + } + + if (settings.VsSolution) + { + var directory = Path.Join(settings.OutputPath, "vs"); + await client.QueueExport(VsSolutionOutput.Id, directory, new Dictionary + { + ["unitypath"] = settings.UnityPath ?? "", + ["unityassembliespath"] = settings.UnityAssembliesPath ?? "" + }); + } + + await client.StartExport(); + await client.WaitForLoadingToFinishAsync(); + return 0; + } + + public override ValidationResult Validate(CommandContext context, Option settings) + { + if (settings.UnityPath != null && !Path.Exists(settings.UnityPath)) + return ValidationResult.Error($"Provided Unity path {settings.UnityPath} does not exist."); + + if (settings.UnityAssembliesPath != null && !Path.Exists(settings.UnityAssembliesPath)) + return ValidationResult.Error($"Provided Unity assemblies path {settings.UnityAssembliesPath} does not exist."); + + if (settings.ExtractIl2CppFilesPath != null && File.Exists(settings.ExtractIl2CppFilesPath)) + return ValidationResult.Error( + $"Provided extracted IL2CPP files path {settings.ExtractIl2CppFilesPath} already exists as a file."); + + if (settings is + { + CppScaffolding: false, CSharpStubs: false, DisassemblerMetadata: false, DummyDlls: false, + VsSolution: false + }) + return ValidationResult.Error("At least one output format must be specified."); + + return base.Validate(context, settings); + } +} \ No newline at end of file diff --git a/Il2CppInspector.Redux.CLI/Il2CppInspector.Redux.CLI.csproj b/Il2CppInspector.Redux.CLI/Il2CppInspector.Redux.CLI.csproj new file mode 100644 index 0000000..eddc656 --- /dev/null +++ b/Il2CppInspector.Redux.CLI/Il2CppInspector.Redux.CLI.csproj @@ -0,0 +1,20 @@ + + + + net9.0 + enable + enable + true + true + + + + + + + + + + + + diff --git a/Il2CppInspector.Redux.CLI/PortProvider.cs b/Il2CppInspector.Redux.CLI/PortProvider.cs new file mode 100644 index 0000000..56f28a7 --- /dev/null +++ b/Il2CppInspector.Redux.CLI/PortProvider.cs @@ -0,0 +1,6 @@ +namespace Il2CppInspector.Redux.CLI; + +internal sealed class PortProvider(int port) +{ + public int Port => port; +} \ No newline at end of file diff --git a/Il2CppInspector.Redux.CLI/Program.cs b/Il2CppInspector.Redux.CLI/Program.cs new file mode 100644 index 0000000..f8d8f7a --- /dev/null +++ b/Il2CppInspector.Redux.CLI/Program.cs @@ -0,0 +1,46 @@ +using Il2CppInspector.Redux.CLI; +using Il2CppInspector.Redux.CLI.Commands; +using Il2CppInspector.Redux.FrontendCore; +using Microsoft.AspNetCore.SignalR; +using Spectre.Console.Cli; + +var builder = WebApplication.CreateSlimBuilder(); + +builder.Services.ConfigureHttpJsonOptions(options => +{ + options.SerializerOptions.TypeInfoResolverChain.Insert(0, FrontendCoreJsonSerializerContext.Default); +}); + +builder.Services.Configure(options => +{ + options.PayloadSerializerOptions.TypeInfoResolverChain.Insert(0, FrontendCoreJsonSerializerContext.Default); +}); + +builder.Services.AddFrontendCore(); +builder.Logging.ClearProviders(); + +var app = builder.Build(); + +app.UseCors(); + +app.MapFrontendCore(); + +await app.StartAsync(); + +var serverUrl = app.Urls.First(); +var port = new Uri(serverUrl).Port; + +var commandServiceProvider = new ServiceCollection(); +commandServiceProvider.AddSingleton(new PortProvider(port)); + +var commandTypeRegistrar = new ServiceTypeRegistrar(commandServiceProvider); +var consoleApp = new CommandApp(commandTypeRegistrar); + +consoleApp.Configure(config => +{ + config.AddCommand("process") + .WithDescription("Processes the provided input data into one or more output formats."); +}); + +await consoleApp.RunAsync(args); +await app.StopAsync(); \ No newline at end of file diff --git a/Il2CppInspector.Redux.CLI/Properties/launchSettings.json b/Il2CppInspector.Redux.CLI/Properties/launchSettings.json new file mode 100644 index 0000000..1677dac --- /dev/null +++ b/Il2CppInspector.Redux.CLI/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "profiles": { + "WSL": { + "commandName": "WSL2", + "launchUrl": "http://localhost:5118/", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "ASPNETCORE_URLS": "http://localhost:5118" + }, + "distributionName": "" + }, + "http": { + "commandName": "Project", + "commandLineArgs": "process -d --disassembler ghidra M:\\Downloads\\Reversing\\NotYetAnalyzedAPKs\\pokemon_friends\\libil2cpp.so M:\\Downloads\\Reversing\\NotYetAnalyzedAPKs\\pokemon_friends\\global-metadata.dat", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:5118" + } + }, + "$schema": "https://json.schemastore.org/launchsettings.json" +} \ No newline at end of file diff --git a/Il2CppInspector.Redux.CLI/ServiceTypeRegistrar.cs b/Il2CppInspector.Redux.CLI/ServiceTypeRegistrar.cs new file mode 100644 index 0000000..a595c55 --- /dev/null +++ b/Il2CppInspector.Redux.CLI/ServiceTypeRegistrar.cs @@ -0,0 +1,30 @@ +using Spectre.Console.Cli; + +namespace Il2CppInspector.Redux.CLI; + +public class ServiceTypeRegistrar(IServiceCollection serviceCollection) : ITypeRegistrar +{ + private readonly IServiceCollection _serviceCollection = serviceCollection; + private ServiceTypeResolver? _resolver; + + public void Register(Type service, Type implementation) + { + _serviceCollection.AddSingleton(service, implementation); + } + + public void RegisterInstance(Type service, object implementation) + { + _serviceCollection.AddSingleton(service, implementation); + } + + public void RegisterLazy(Type service, Func factory) + { + _serviceCollection.AddSingleton(service, _ => factory()); + } + + public ITypeResolver Build() + { + _resolver ??= new ServiceTypeResolver(_serviceCollection.BuildServiceProvider()); + return _resolver; + } +} \ No newline at end of file diff --git a/Il2CppInspector.Redux.CLI/ServiceTypeResolver.cs b/Il2CppInspector.Redux.CLI/ServiceTypeResolver.cs new file mode 100644 index 0000000..303115c --- /dev/null +++ b/Il2CppInspector.Redux.CLI/ServiceTypeResolver.cs @@ -0,0 +1,13 @@ +using Spectre.Console.Cli; + +namespace Il2CppInspector.Redux.CLI; + +public class ServiceTypeResolver(IServiceProvider serviceProvider) : ITypeResolver +{ + private readonly IServiceProvider _serviceProvider = serviceProvider; + + public object? Resolve(Type? type) + => type == null + ? null + : _serviceProvider.GetService(type); +} \ No newline at end of file diff --git a/Il2CppInspector.Redux.CLI/appsettings.Development.json b/Il2CppInspector.Redux.CLI/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/Il2CppInspector.Redux.CLI/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Il2CppInspector.Redux.CLI/appsettings.json b/Il2CppInspector.Redux.CLI/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/Il2CppInspector.Redux.CLI/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/Il2CppInspector.sln b/Il2CppInspector.sln index 62a20c7..d35a361 100644 --- a/Il2CppInspector.sln +++ b/Il2CppInspector.sln @@ -48,6 +48,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Il2CppInspector.Redux.GUI", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Il2CppInspector.Redux.FrontendCore", "Il2CppInspector.Redux.FrontendCore\Il2CppInspector.Redux.FrontendCore.csproj", "{D80D0FCE-4C9C-4BF1-8936-71DFB0B2D86A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Il2CppInspector.Redux.CLI", "Il2CppInspector.Redux.CLI\Il2CppInspector.Redux.CLI.csproj", "{8FA27979-23F4-4A4E-8B69-503A20BF1344}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -88,6 +90,10 @@ Global {D80D0FCE-4C9C-4BF1-8936-71DFB0B2D86A}.Debug|Any CPU.Build.0 = Debug|Any CPU {D80D0FCE-4C9C-4BF1-8936-71DFB0B2D86A}.Release|Any CPU.ActiveCfg = Release|Any CPU {D80D0FCE-4C9C-4BF1-8936-71DFB0B2D86A}.Release|Any CPU.Build.0 = Release|Any CPU + {8FA27979-23F4-4A4E-8B69-503A20BF1344}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8FA27979-23F4-4A4E-8B69-503A20BF1344}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8FA27979-23F4-4A4E-8B69-503A20BF1344}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8FA27979-23F4-4A4E-8B69-503A20BF1344}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE