From 9889004340495af859ef1b21ce76f7a738bd660c Mon Sep 17 00:00:00 2001 From: Bepis <36346617+bbepis@users.noreply.github.com> Date: Sat, 31 Jul 2021 19:25:48 +1000 Subject: [PATCH] Initial code commit --- NStrip.sln | 25 +++ NStrip/App.config | 6 + NStrip/AssemblyStripper.cs | 96 +++++++++ NStrip/NArgs.cs | 320 ++++++++++++++++++++++++++++++ NStrip/NStrip.csproj | 65 ++++++ NStrip/Program.cs | 144 ++++++++++++++ NStrip/Properties/AssemblyInfo.cs | 36 ++++ NStrip/packages.config | 5 + 8 files changed, 697 insertions(+) create mode 100644 NStrip.sln create mode 100644 NStrip/App.config create mode 100644 NStrip/AssemblyStripper.cs create mode 100644 NStrip/NArgs.cs create mode 100644 NStrip/NStrip.csproj create mode 100644 NStrip/Program.cs create mode 100644 NStrip/Properties/AssemblyInfo.cs create mode 100644 NStrip/packages.config diff --git a/NStrip.sln b/NStrip.sln new file mode 100644 index 0000000..93f3d6d --- /dev/null +++ b/NStrip.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30011.22 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NStrip", "NStrip\NStrip.csproj", "{CE5BFF3E-2350-46AE-A54E-0631D89565F4}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {CE5BFF3E-2350-46AE-A54E-0631D89565F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CE5BFF3E-2350-46AE-A54E-0631D89565F4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CE5BFF3E-2350-46AE-A54E-0631D89565F4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CE5BFF3E-2350-46AE-A54E-0631D89565F4}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {505788DE-B387-43E7-90DB-98E6903B88EE} + EndGlobalSection +EndGlobal diff --git a/NStrip/App.config b/NStrip/App.config new file mode 100644 index 0000000..88fa402 --- /dev/null +++ b/NStrip/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/NStrip/AssemblyStripper.cs b/NStrip/AssemblyStripper.cs new file mode 100644 index 0000000..e148b54 --- /dev/null +++ b/NStrip/AssemblyStripper.cs @@ -0,0 +1,96 @@ +using System.Collections.Generic; +using Mono.Cecil; +using Mono.Cecil.Cil; + +namespace NStrip +{ + public static class AssemblyStripper + { + static IEnumerable GetAllTypeDefinitions(AssemblyDefinition assembly) + { + var typeQueue = new Queue(assembly.MainModule.Types); + + while (typeQueue.Count > 0) + { + var type = typeQueue.Dequeue(); + + yield return type; + + foreach (var nestedType in type.NestedTypes) + typeQueue.Enqueue(nestedType); + } + } + + static void ClearMethodBodies(TypeReference voidTypeReference, ICollection methods) + { + foreach (MethodDefinition method in methods) + { + if (!method.HasBody) + continue; + + MethodBody body = new MethodBody(method); + var il = body.GetILProcessor(); + + // There's multiple ways we could handle this: + // - Only provide a ret. Smallest size, however if .NET tries to load this assembly during runtime it might fail. + // - Providing a value and ret (what we currently do). Slightly more space, however .NET should be fine loading it. + // - Null body, i.e. mark everything as extern. Should theoretically work when loaded into .NET and be the smallest size, + // but the size of assembly remains the same. Might be a bug within Mono.Cecil. + + if (method.ReturnType.IsPrimitive) + { + il.Emit(OpCodes.Ldc_I4_0); + } + else if (method.ReturnType != voidTypeReference) + { + il.Emit(OpCodes.Ldnull); + } + + il.Emit(OpCodes.Ret); + + method.Body = body; + + method.AggressiveInlining = false; + method.NoInlining = true; + } + } + + public static void StripAssembly(AssemblyDefinition assembly) + { + if (!assembly.MainModule.TryGetTypeReference("System.Void", out var voidTypeReference)) + { + voidTypeReference = assembly.MainModule.ImportReference(typeof(void)); + } + + foreach (TypeDefinition type in GetAllTypeDefinitions(assembly)) + { + if (type.IsEnum || type.IsInterface) + continue; + + ClearMethodBodies(voidTypeReference, type.Methods); + } + + assembly.MainModule.Resources.Clear(); + } + + public static void MakePublic(AssemblyDefinition assembly, IList typeNameBlacklist) + { + foreach (var type in GetAllTypeDefinitions(assembly)) + { + if (typeNameBlacklist.Contains(type.Name)) + continue; + + if (type.IsNested) + type.IsNestedPublic = true; + else + type.IsPublic = true; + + foreach (var method in type.Methods) + method.IsPublic = true; + + foreach (var field in type.Fields) + field.IsPublic = true; + } + } + } +} \ No newline at end of file diff --git a/NStrip/NArgs.cs b/NStrip/NArgs.cs new file mode 100644 index 0000000..33d7f28 --- /dev/null +++ b/NStrip/NArgs.cs @@ -0,0 +1,320 @@ +/* + NArgs + The MIT License (MIT) + + Copyright(c) 2021 Bepis + + Permission is hereby granted, free of charge, to any person obtaining a copy of + this software and associated documentation files (the "Software"), to deal in + the Software without restriction, including without limitation the rights to + use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + the Software, and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace NArgs +{ + /// + /// Command-line argument parser. + /// + public static class Arguments + { + /// + /// Parses arguments and constructs an object. + /// + /// The type of the object to construct. Must inherit from + /// The command-line arguments to parse. + /// + public static T Parse(string[] args) where T : IArgumentCollection, new() + { + Dictionary> valueSwitches = new Dictionary>(); + Dictionary> boolSwitches = new Dictionary>(); + + var config = new T { Values = new List() }; + + var commandProps = GetCommandProperties(); + + foreach (var kv in commandProps) + { + if (kv.Value.PropertyType == typeof(bool)) + { + boolSwitches.Add(kv.Key, x => kv.Value.SetValue(config, x)); + } + else if (kv.Value.PropertyType == typeof(string)) + { + valueSwitches.Add(kv.Key, x => kv.Value.SetValue(config, x)); + } + else if (typeof(IList).IsAssignableFrom(kv.Value.PropertyType)) + { + if (kv.Value.GetValue(config) == null) + { + kv.Value.SetValue(config, new List()); + } + + valueSwitches.Add(kv.Key, x => + { + var list = (IList)kv.Value.GetValue(config); + list.Add(x); + }); + } + } + + CommandDefinitionAttribute previousSwitchDefinition = null; + bool valuesOnly = false; + + foreach (string arg in args) + { + if (arg == "--") + { + // no more switches, only values + valuesOnly = true; + + continue; + } + + if (valuesOnly) + { + config.Values.Add(arg); + continue; + } + + if (arg.StartsWith("-") + || arg.StartsWith("--")) + { + string previousSwitch; + + if (arg.StartsWith("--")) + previousSwitch = arg.Substring(2); + else + previousSwitch = arg.Substring(1); + + if (boolSwitches.Keys.TryFirst(x + => x.LongArg.Equals(previousSwitch, StringComparison.InvariantCultureIgnoreCase) + || x.ShortArg?.Equals(previousSwitch, StringComparison.InvariantCultureIgnoreCase) == true, + out var definition)) + { + boolSwitches[definition](true); + previousSwitch = null; + + continue; + } + + if (valueSwitches.Keys.TryFirst(x + => x.LongArg.Equals(previousSwitch, StringComparison.InvariantCultureIgnoreCase) + || x.ShortArg?.Equals(previousSwitch, StringComparison.InvariantCultureIgnoreCase) == true, + out definition)) + { + previousSwitchDefinition = definition; + + continue; + } + + Console.WriteLine("Unrecognized command line option: " + arg); + throw new Exception(); + } + + if (previousSwitchDefinition != null) + { + valueSwitches[previousSwitchDefinition](arg); + previousSwitchDefinition = null; + } + else + { + config.Values.Add(arg); + } + } + + foreach (var kv in commandProps) + { + if (!kv.Key.Required) + continue; + + if (kv.Value.PropertyType == typeof(string)) + if (kv.Value.GetValue(config) == null) + throw new ArgumentException($"Required argument not provided: {kv.Key.LongArg}"); + + if (kv.Value.PropertyType == typeof(IList)) + if (((IList)kv.Value.GetValue(config)).Count == 0) + throw new ArgumentException($"Required argument not provided: {kv.Key.LongArg}"); + } + + return config; + } + + /// + /// Generates a string to be printed as console help text. + /// + /// The type of the arguments object to create help instructions for. Must inherit from + /// The copyright text to add at the top, if any. + /// The usage text to add at the top, if any. + public static string PrintLongHelp(string copyrightText = null, string usageText = null) where T : IArgumentCollection + { + var commands = GetCommandProperties(); + + var builder = new StringBuilder(); + + if (copyrightText != null) + builder.AppendLine(copyrightText); + + if (usageText != null) + builder.AppendLine(usageText); + + builder.AppendLine(); + builder.AppendLine(); + + foreach (var command in commands.OrderBy(x => x.Key.ShortArg ?? "zzzz").ThenBy(x => x.Key.LongArg)) + { + var valueString = string.Empty; + + if (command.Value.PropertyType == typeof(IList) + || command.Value.PropertyType == typeof(string)) + { + valueString = " "; + } + + string listing = command.Key.ShortArg != null + ? $" -{command.Key.ShortArg}{valueString}, --{command.Key.LongArg}{valueString}" + : $" --{command.Key.LongArg}{valueString}"; + + const int listingWidth = 45; + const int descriptionWidth = 65; + + string listingWidthString = "".PadLeft(listingWidth); + + builder.Append(listing.PadRight(listingWidth)); + + if (listing.Length > listingWidth - 3) + { + builder.AppendLine(); + builder.Append(listingWidthString); + } + + int lineLength = 0; + int lastIndex = 0; + int currentIndex = 0; + + while ((currentIndex = command.Key.Description.IndexOf(' ', currentIndex + 1)) != -1) + { + if (currentIndex - lastIndex >= descriptionWidth) + { + var descriptionSubstring = command.Key.Description.Substring(lastIndex, lineLength); + builder.AppendLine(descriptionSubstring); + builder.Append(listingWidthString); + + lastIndex += lineLength + 1; + } + + lineLength = currentIndex - lastIndex; + } + + if (lineLength > 0) + { + var remainingSubstring = command.Key.Description.Substring(lastIndex); + builder.AppendLine(remainingSubstring); + } + + builder.AppendLine(); + } + + builder.AppendLine(); + + return builder.ToString(); + } + + private static Dictionary GetCommandProperties() + { + var commands = new Dictionary(); + + foreach (var prop in typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public)) + { + var commandDef = prop.GetCustomAttribute(); + + if (commandDef == null) + continue; + + commands.Add(commandDef, prop); + } + + return commands; + } + + private static bool TryFirst(this IEnumerable enumerable, Func predicate, out T value) + { + foreach (var item in enumerable) + { + if (predicate(item)) + { + value = item; + return true; + } + } + + value = default; + return false; + } + } + + /// + /// Specifies an object is an argument collection. + /// + public interface IArgumentCollection + { + /// + /// A list of values that were passed in as arguments, but not associated with an option. + /// + IList Values { get; set; } + } + + public class CommandDefinitionAttribute : Attribute + { + /// + /// The short version of an option, e.g. "-a". Optional. + /// + public string ShortArg { get; set; } + + /// + /// The long version of an option, e.g. "--append". Required. + /// + public string LongArg { get; set; } + + /// + /// Whether or not to fail parsing if this argument has not been provided. + /// + public bool Required { get; set; } = false; + + /// + /// The description of the option, to be used in the help text. + /// + public string Description { get; set; } = null; + + /// The long version of an option, e.g. "--append". + public CommandDefinitionAttribute(string longArg) + { + LongArg = longArg; + } + + /// The short version of an option, e.g. "-a". + /// The long version of an option, e.g. "--append". + public CommandDefinitionAttribute(string shortArg, string longArg) + { + ShortArg = shortArg; + LongArg = longArg; + } + } +} \ No newline at end of file diff --git a/NStrip/NStrip.csproj b/NStrip/NStrip.csproj new file mode 100644 index 0000000..63aac86 --- /dev/null +++ b/NStrip/NStrip.csproj @@ -0,0 +1,65 @@ + + + + + Debug + AnyCPU + {CE5BFF3E-2350-46AE-A54E-0631D89565F4} + Exe + NStrip + NStrip + v4.5.2 + 512 + true + true + + + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + 8 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + 8 + + + + ..\packages\Mono.Cecil.0.11.4\lib\net40\Mono.Cecil.dll + + + + + + + + + + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + \ No newline at end of file diff --git a/NStrip/Program.cs b/NStrip/Program.cs new file mode 100644 index 0000000..7bad862 --- /dev/null +++ b/NStrip/Program.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Mono.Cecil; +using NArgs; + +namespace NStrip +{ + class Program + { + static void LogError(string message) + { + var oldColor = Console.ForegroundColor; + Console.ForegroundColor = ConsoleColor.Red; + + Console.WriteLine(message); + + Console.ForegroundColor = oldColor; + } + + static void LogMessage(string message) + { + Console.WriteLine(message); + } + + static void Main(string[] args) + { + NRedirectArguments arguments = Arguments.Parse(args); + + if (arguments.Values.Count == 0 || arguments.Help) + { + LogMessage(Arguments.PrintLongHelp( + "NStrip v1.0.0, by Bepis", + "Usage: NStrip [options] (<.NET .exe / .dll> | ) [ | ]")); + return; + } + + string path = arguments.Values[0]; + + string outputPath = arguments.Values.Count >= 2 ? arguments.Values[1] : null; + + var resolver = new DefaultAssemblyResolver(); + + foreach (var dependency in arguments.Dependencies) + resolver.AddSearchDirectory(dependency); + + var readerParams = new ReaderParameters() + { + AssemblyResolver = resolver + }; + + if (Directory.Exists(path)) + { + resolver.AddSearchDirectory(path); + + foreach (var file in Directory.EnumerateFiles(path, "*.dll")) + { + string fileOutputPath = outputPath != null + ? Path.Combine(outputPath, Path.GetFileName(file)) + : file; + + if (!arguments.Overwrite && outputPath == null) + fileOutputPath = AppendToEndOfFileName(file, "-nstrip"); + + StripAssembly(file, fileOutputPath, arguments.NoStrip, arguments.Public, arguments.Blacklist, readerParams); + } + } + else if (File.Exists(path)) + { + resolver.AddSearchDirectory(Path.GetDirectoryName(path)); + + string fileOutputPath = outputPath ?? + (arguments.Overwrite ? path : AppendToEndOfFileName(path, "-nstrip")); + + StripAssembly(path, fileOutputPath, arguments.NoStrip, arguments.Public, arguments.Blacklist, readerParams); + } + else + { + LogError($"Could not find path {path}"); + } + + LogMessage("Finished!"); + } + + static void StripAssembly(string assemblyPath, string outputPath, bool noStrip, bool makePublic, IList typeNameBlacklist, ReaderParameters readerParams) + { + LogMessage($"Stripping {assemblyPath}"); + using var memoryStream = new MemoryStream(File.ReadAllBytes(assemblyPath)); + using var assemblyDefinition = AssemblyDefinition.ReadAssembly(memoryStream, readerParams); + + if (!noStrip) + AssemblyStripper.StripAssembly(assemblyDefinition); + + if (makePublic) + AssemblyStripper.MakePublic(assemblyDefinition, typeNameBlacklist); + + // We write to a memory stream first to ensure that Mono.Cecil doesn't have any errors when producing the assembly. + // Otherwise, if we're overwriting the same assembly and it fails, it will overwrite with a 0 byte file + + using var tempStream = new MemoryStream(); + + assemblyDefinition.Write(tempStream); + + if (noStrip && !makePublic) + return; + + tempStream.Position = 0; + using var outputStream = File.Open(outputPath, FileMode.Create); + + tempStream.CopyTo(outputStream); + } + + static string AppendToEndOfFileName(string path, string appendedString) + { + return Path.Combine( + Path.GetDirectoryName(path), + $"{Path.GetFileNameWithoutExtension(path)}{appendedString}.{Path.GetExtension(path)}" + ); + } + + private class NRedirectArguments : IArgumentCollection + { + public IList Values { get; set; } + + [CommandDefinition("h", "help", Description = "Prints help text")] + public bool Help { get; set; } + + [CommandDefinition("p", "public", Description = "Changes visibility of all types, nested types, methods and fields to public.")] + public bool Public { get; set; } + + [CommandDefinition("d", "dependencies", Description = "A folder that contains dependency libraries for the target assembly/assemblies. Add this if the assembly you're working on does not have all of it's dependencies in the same folder. Can be specified multiple times.")] + public IList Dependencies { get; set; } + + [CommandDefinition("b", "blacklist", Description = "Specify this to blacklist specific short type names from being publicized if you're encountering issues with types conflicting. Can be specified multiple times.")] + public IList Blacklist { get; set; } + + [CommandDefinition("n", "no-strip", Description = "Does not strip assemblies. If this is not being used with --public, assemblies are not modified/saved")] + public bool NoStrip { get; set; } + + [CommandDefinition("o", "overwrite", Description = "Instead of appending \"-nstrip\" to the output assembly name, overwrite the file in-place. Does nothing if an output file/directory is specified, as \"-nstrip\" is not appended in the first place.")] + public bool Overwrite { get; set; } + } + } +} \ No newline at end of file diff --git a/NStrip/Properties/AssemblyInfo.cs b/NStrip/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..f1c18aa --- /dev/null +++ b/NStrip/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("NStrip")] +[assembly: AssemblyDescription(".NET Assembly stripper, publicizer and general utility tool")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("BepInEx")] +[assembly: AssemblyProduct("NStrip")] +[assembly: AssemblyCopyright("Copyright © BepInEx Team 2021")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("ce5bff3e-2350-46ae-a54e-0631d89565f4")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/NStrip/packages.config b/NStrip/packages.config new file mode 100644 index 0000000..b1e18e3 --- /dev/null +++ b/NStrip/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file