diff --git a/Il2CppInspector.CLI/Il2CppInspector.CLI.csproj b/Il2CppInspector.CLI/Il2CppInspector.CLI.csproj index 2943827..5731468 100644 --- a/Il2CppInspector.CLI/Il2CppInspector.CLI.csproj +++ b/Il2CppInspector.CLI/Il2CppInspector.CLI.csproj @@ -5,7 +5,8 @@ netcoreapp3.1 true - true + + false win-x64 false 2020.2.1 diff --git a/Il2CppInspector.CLI/PluginOptions.cs b/Il2CppInspector.CLI/PluginOptions.cs new file mode 100644 index 0000000..37f420c --- /dev/null +++ b/Il2CppInspector.CLI/PluginOptions.cs @@ -0,0 +1,190 @@ +/* + Copyright 2020 Katy Coe - http://www.djkaty.com - https://github.com/djkaty + + All rights reserved. +*/ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Reflection; +using System.Reflection.Emit; +using CommandLine; +using Il2CppInspector.PluginAPI.V100; +using CommandLine.Text; +using System.IO; + +namespace Il2CppInspector.CLI +{ + // This ridiculous hack converts options from our plugin API to options classes that CommandLineParser can process and back again + internal static class PluginOptions + { + // Create an auto-property + private static PropertyBuilder CreateAutoProperty(TypeBuilder tb, string propertyName, Type propertyType) { + FieldBuilder fieldBuilder = tb.DefineField("_" + propertyName, propertyType, FieldAttributes.Private); + + PropertyBuilder propertyBuilder = tb.DefineProperty(propertyName, PropertyAttributes.HasDefault, propertyType, null); + MethodBuilder getPropMthdBldr = tb.DefineMethod("get_" + propertyName, + MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig, propertyType, Type.EmptyTypes); + ILGenerator getIl = getPropMthdBldr.GetILGenerator(); + + getIl.Emit(OpCodes.Ldarg_0); + getIl.Emit(OpCodes.Ldfld, fieldBuilder); + getIl.Emit(OpCodes.Ret); + + MethodBuilder setPropMthdBldr = + tb.DefineMethod("set_" + propertyName, + MethodAttributes.Public | + MethodAttributes.SpecialName | + MethodAttributes.HideBySig, + null, new[] { propertyType }); + + ILGenerator setIl = setPropMthdBldr.GetILGenerator(); + setIl.Emit(OpCodes.Ldarg_0); + setIl.Emit(OpCodes.Ldarg_1); + setIl.Emit(OpCodes.Stfld, fieldBuilder); + setIl.Emit(OpCodes.Ret); + + propertyBuilder.SetGetMethod(getPropMthdBldr); + propertyBuilder.SetSetMethod(setPropMthdBldr); + + return propertyBuilder; + } + + // Create a CommandLineParser-friendly attributed class of options from a loaded plugin + private static Type CreateOptionsFromPlugin(IPlugin plugin) { + // Name of class to create + var tn = plugin.Id + "Options"; + + // Create class and default constructor + var ab = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName(Guid.NewGuid().ToString()), AssemblyBuilderAccess.Run); + var mb = ab.DefineDynamicModule("MainModule"); + var pluginOptionClass = mb.DefineType(tn, + TypeAttributes.Public | + TypeAttributes.Class | + TypeAttributes.AutoClass | + TypeAttributes.AnsiClass | + TypeAttributes.BeforeFieldInit | + TypeAttributes.AutoLayout, + null); + pluginOptionClass.DefineDefaultConstructor(MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName); + + // Create VerbAttribute with plugin ID + var verbCtorInfo = typeof(VerbAttribute).GetConstructor(new Type[] { typeof(string) }); + var verbHelpPropInfo = typeof(VerbAttribute).GetProperty("HelpText"); + var verbAttBuilder = new CustomAttributeBuilder(verbCtorInfo, new object[] { plugin.Id }, + new PropertyInfo[] { verbHelpPropInfo }, new object[] { plugin.Description }); + pluginOptionClass.SetCustomAttribute(verbAttBuilder); + + // Create auto-property for each option + foreach (var option in plugin.Options) { + var optionType = option.GetType().GetProperty("Value").PropertyType; + var optionValue = option.Value; + + // Hex numbers are strings that will be validated later + if (option is IPluginOptionNumber n && n.Style == PluginOptionNumberStyle.Hex) { + optionType = typeof(string); + optionValue = string.Format("0x{0:x}", optionValue); + } + + var pluginOptionProperty = CreateAutoProperty(pluginOptionClass, option.Name, optionType); + var optCtorInfo = typeof(OptionAttribute).GetConstructor(new Type[] { typeof(string) }); + var optHelpPropInfo = typeof(OptionAttribute).GetProperty("HelpText"); + var optDefaultInfo = typeof(OptionAttribute).GetProperty("Default"); + var optRequiredInfo = typeof(OptionAttribute).GetProperty("Required"); + var attBuilder = new CustomAttributeBuilder(optCtorInfo, new object[] { option.Name }, + new PropertyInfo[] { optHelpPropInfo, optDefaultInfo, optRequiredInfo }, + // Booleans are always optional + new object[] { option.Description, optionValue, option.Value is bool? false : option.Required }); + pluginOptionProperty.SetCustomAttribute(attBuilder); + } + return pluginOptionClass.CreateTypeInfo().AsType(); + } + + // Get plugin option classes + public static Type[] GetPluginOptionTypes() { + // Don't do anything if there are no loaded plugins + var plugins = PluginManager.AvailablePlugins; + + if (!plugins.Any()) + return Array.Empty(); + + // Create CommandLine-friendly option classes for each plugin + return plugins.Select(p => CreateOptionsFromPlugin(p)).ToArray(); + } + + // Parse all options for all plugins + public static bool ParsePluginOptions(IEnumerable pluginOptions, Type[] optionsTypes) { + + // Run CommandLine parser on each set of plugin options + foreach (var options in pluginOptions) { + + var selectedPlugin = options.Split(' ')[0].ToLower(); + + // Cause an error on the first plugin arguments if no plugins are loaded + if (optionsTypes.Length == 0) { + Console.Error.WriteLine($"Plugin '{selectedPlugin}' does not exist or is not loaded"); + return false; + } + + // Parse plugin arguments + var parser = new Parser(with => { + with.HelpWriter = null; + with.CaseSensitive = false; + with.AutoHelp = false; + with.AutoVersion = false; + }); + var result = parser.ParseArguments(options.Split(' '), optionsTypes); + + // Print plugin help if parsing failed + if (result is NotParsed notParsed) { + if (!(notParsed.Errors.First() is BadVerbSelectedError)) { + var helpText = HelpText.AutoBuild(result, h => { + h.Heading = $"Usage for plugin '{selectedPlugin}':"; + h.Copyright = string.Empty; + h.AutoHelp = false; + h.AutoVersion = false; + return h; + }, e => e); + Console.Error.WriteLine(helpText); + } else { + Console.Error.WriteLine($"Plugin '{selectedPlugin}' does not exist or is not loaded"); + } + return false; + } + + // Get plugin arguments and write them to plugin options class + var optionsObject = (result as Parsed).Value; + var plugin = PluginManager.AvailablePlugins.First(p => optionsObject.GetType().FullName == p.Id + "Options"); + + foreach (var prop in optionsObject.GetType().GetProperties()) { + var targetProp = plugin.Options.First(x => x.Name == prop.Name); + + // Validate hex strings + if (targetProp is IPluginOptionNumber n && n.Style == PluginOptionNumberStyle.Hex) { + try { + n.Value = Convert.ToUInt64((string) prop.GetValue(optionsObject), 16); + } + catch (Exception ex) when (ex is ArgumentException || ex is FormatException || ex is OverflowException) { + Console.Error.WriteLine($"{prop.Name} must be a 32 or 64-bit hex value (optionally starting with '0x')"); + } + } + + // TODO: Validate choices + // TODO: Validate path names - https://stackoverflow.com/questions/422090/in-c-sharp-check-that-filename-is-possibly-valid-not-that-it-exists + + // All other input types + else { + targetProp.Value = prop.GetValue(optionsObject); + } + } + + // Enable plugin + PluginManager.AsInstance.ManagedPlugins.First(p => p.Plugin == plugin).Enabled = true; + } + return true; + } + } +} diff --git a/Il2CppInspector.CLI/Program.cs b/Il2CppInspector.CLI/Program.cs index a63d8d6..d3af8a6 100644 --- a/Il2CppInspector.CLI/Program.cs +++ b/Il2CppInspector.CLI/Program.cs @@ -1,13 +1,20 @@ -// Copyright (c) 2017-2020 Katy Coe - https://www.djkaty.com - https://github.com/djkaty -// All rights reserved +/* + Copyright 2017-2020 Katy Coe - http://www.djkaty.com - https://github.com/djkaty + + All rights reserved. +*/ using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; +using System.Reflection; +using System.Reflection.Emit; using System.Runtime.InteropServices; +using System.Xml.Schema; using CommandLine; +using CommandLine.Text; using Il2CppInspector.Cpp; using Il2CppInspector.Cpp.UnityHeaders; using Il2CppInspector.Model; @@ -98,6 +105,9 @@ namespace Il2CppInspector.CLI [Option("unity-version", Required = false, HelpText = "Version of Unity used to create the input files, if known. Used to enhance Python, C++ and JSON output. If not specified, a close match will be inferred automatically.", Default = null)] public UnityVersion UnityVersion { get; set; } + + [Option("plugins", Required = false, HelpText = "Specify options for plugins. Enclose each plugin's configuration in quotes as follows: --plugins \"pluginone --option1 value1 --option2 value2\" \"plugintwo --option...\". Use --plugins to get help on a specific plugin")] + public IEnumerable PluginOptions { get; set; } } // Adapted from: https://stackoverflow.com/questions/16376191/measuring-code-execution-time @@ -119,19 +129,44 @@ namespace Il2CppInspector.CLI } } - public static int Main(string[] args) => - Parser.Default.ParseArguments(args).MapResult( - options => Run(options), - _ => 1); + public static void Main(string[] args) { + var parser = new Parser(config => config.HelpWriter = null); + var result = parser.ParseArguments(args); + result.WithParsed(options => Run(options)) + .WithNotParsed(errors => DisplayHelp(result, errors)); + } + + private static int DisplayHelp(ParserResult result, IEnumerable errors) { + Console.Error.WriteLine(HelpText.AutoBuild(result)); + + var help = new HelpText(); + help.Heading = "Available plugins:"; + help.Copyright = string.Empty; + help.AddDashesToOption = false; + help.AdditionalNewLineAfterOption = true; + help.MaximumDisplayWidth = 80; + help.AutoHelp = false; + help.AutoVersion = false; + + var pluginOptions = PluginOptions.GetPluginOptionTypes(); + if (pluginOptions.Any()) + Console.Error.WriteLine(help.AddVerbs(PluginOptions.GetPluginOptionTypes())); + return 1; + } private static int Run(Options options) { + // Banner var asmInfo = FileVersionInfo.GetVersionInfo(System.Reflection.Assembly.GetEntryAssembly().Location); Console.WriteLine(asmInfo.ProductName); Console.WriteLine("Version " + asmInfo.ProductVersion); Console.WriteLine(asmInfo.LegalCopyright); Console.WriteLine(""); - + + // Check plugin options are valid + if (!PluginOptions.ParsePluginOptions(options.PluginOptions, PluginOptions.GetPluginOptionTypes())) + return 1; + // Check script target is valid if (!PythonScript.GetAvailableTargets().Contains(options.ScriptTarget)) { Console.Error.WriteLine($"Script target {options.ScriptTarget} is invalid."); @@ -196,6 +231,16 @@ namespace Il2CppInspector.CLI Console.WriteLine("Using Unity assemblies at " + unityAssembliesPath); } + // Set plugin handlers + PluginManager.ErrorHandler += (s, e) => { + Console.Error.WriteLine($"The plugin {e.Plugin.Name} encountered an error while executing {e.Operation}: {e.Exception.Message}." + + " The application will continue but may not behave as expected."); + }; + + PluginManager.StatusHandler += (s, e) => { + Console.WriteLine("Plugin " + e.Plugin.Name + ": " + e.Text); + }; + // Check that specified binary files exist foreach (var file in options.BinaryFiles) if (!File.Exists(file)) {