/* 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; } } }