├── .github └── workflows │ └── ci-release-build.yml ├── .gitignore ├── LICENSE ├── NStrip.sln ├── NStrip ├── AssemblyStripper.cs ├── NArgs.cs ├── NStrip.csproj ├── Program.cs └── Properties │ └── AssemblyInfo.cs └── README.md /.github/workflows/ci-release-build.yml: -------------------------------------------------------------------------------- 1 | on: push 2 | name: Build & Release 3 | 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Checkout 9 | uses: actions/checkout@master 10 | - name: Setup dotnet 11 | uses: actions/setup-dotnet@master 12 | with: 13 | dotnet-version: '6.0.x' 14 | include-prerelease: true 15 | - name: Build 16 | shell: bash 17 | run: | 18 | dotnet build NStrip -c Release 19 | mkdir -p publish 20 | cp -a NStrip/bin/Release/*/NStrip.exe publish/ 21 | - name: Upload 22 | uses: actions/upload-artifact@master 23 | with: 24 | name: NStrip 25 | path: publish/NStrip.exe 26 | - name: Release 27 | uses: softprops/action-gh-release@master 28 | if: startsWith(github.ref, 'refs/tags') 29 | with: 30 | draft: true 31 | prerelease: false 32 | files: | 33 | publish/NStrip.exe 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # Rider 7 | .idea 8 | 9 | # User-specific files 10 | *.rsuser 11 | *.suo 12 | *.user 13 | *.userosscache 14 | *.sln.docstates 15 | 16 | # User-specific files (MonoDevelop/Xamarin Studio) 17 | *.userprefs 18 | 19 | # Mono auto generated files 20 | mono_crash.* 21 | 22 | # Build results 23 | [Dd]ebug/ 24 | [Dd]ebugPublic/ 25 | [Rr]elease/ 26 | [Rr]eleases/ 27 | x64/ 28 | x86/ 29 | [Aa][Rr][Mm]/ 30 | [Aa][Rr][Mm]64/ 31 | bld/ 32 | [Bb]in/ 33 | [Oo]bj/ 34 | [Ll]og/ 35 | [Ll]ogs/ 36 | 37 | # Visual Studio 2015/2017 cache/options directory 38 | .vs/ 39 | # Uncomment if you have tasks that create the project's static files in wwwroot 40 | #wwwroot/ 41 | 42 | # Visual Studio 2017 auto generated files 43 | Generated\ Files/ 44 | 45 | # MSTest test Results 46 | [Tt]est[Rr]esult*/ 47 | [Bb]uild[Ll]og.* 48 | 49 | # NUnit 50 | *.VisualState.xml 51 | TestResult.xml 52 | nunit-*.xml 53 | 54 | # Build Results of an ATL Project 55 | [Dd]ebugPS/ 56 | [Rr]eleasePS/ 57 | dlldata.c 58 | 59 | # Benchmark Results 60 | BenchmarkDotNet.Artifacts/ 61 | 62 | # .NET Core 63 | project.lock.json 64 | project.fragment.lock.json 65 | artifacts/ 66 | 67 | # StyleCop 68 | StyleCopReport.xml 69 | 70 | # Files built by Visual Studio 71 | *_i.c 72 | *_p.c 73 | *_h.h 74 | *.ilk 75 | *.meta 76 | *.obj 77 | *.iobj 78 | *.pch 79 | *.pdb 80 | *.ipdb 81 | *.pgc 82 | *.pgd 83 | *.rsp 84 | *.sbr 85 | *.tlb 86 | *.tli 87 | *.tlh 88 | *.tmp 89 | *.tmp_proj 90 | *_wpftmp.csproj 91 | *.log 92 | *.vspscc 93 | *.vssscc 94 | .builds 95 | *.pidb 96 | *.svclog 97 | *.scc 98 | 99 | # Chutzpah Test files 100 | _Chutzpah* 101 | 102 | # Visual C++ cache files 103 | ipch/ 104 | *.aps 105 | *.ncb 106 | *.opendb 107 | *.opensdf 108 | *.sdf 109 | *.cachefile 110 | *.VC.db 111 | *.VC.VC.opendb 112 | 113 | # Visual Studio profiler 114 | *.psess 115 | *.vsp 116 | *.vspx 117 | *.sap 118 | 119 | # Visual Studio Trace Files 120 | *.e2e 121 | 122 | # TFS 2012 Local Workspace 123 | $tf/ 124 | 125 | # Guidance Automation Toolkit 126 | *.gpState 127 | 128 | # ReSharper is a .NET coding add-in 129 | _ReSharper*/ 130 | *.[Rr]e[Ss]harper 131 | *.DotSettings.user 132 | 133 | # TeamCity is a build add-in 134 | _TeamCity* 135 | 136 | # DotCover is a Code Coverage Tool 137 | *.dotCover 138 | 139 | # AxoCover is a Code Coverage Tool 140 | .axoCover/* 141 | !.axoCover/settings.json 142 | 143 | # Visual Studio code coverage results 144 | *.coverage 145 | *.coveragexml 146 | 147 | # NCrunch 148 | _NCrunch_* 149 | .*crunch*.local.xml 150 | nCrunchTemp_* 151 | 152 | # MightyMoose 153 | *.mm.* 154 | AutoTest.Net/ 155 | 156 | # Web workbench (sass) 157 | .sass-cache/ 158 | 159 | # Installshield output folder 160 | [Ee]xpress/ 161 | 162 | # DocProject is a documentation generator add-in 163 | DocProject/buildhelp/ 164 | DocProject/Help/*.HxT 165 | DocProject/Help/*.HxC 166 | DocProject/Help/*.hhc 167 | DocProject/Help/*.hhk 168 | DocProject/Help/*.hhp 169 | DocProject/Help/Html2 170 | DocProject/Help/html 171 | 172 | # Click-Once directory 173 | publish/ 174 | 175 | # Publish Web Output 176 | *.[Pp]ublish.xml 177 | *.azurePubxml 178 | # Note: Comment the next line if you want to checkin your web deploy settings, 179 | # but database connection strings (with potential passwords) will be unencrypted 180 | *.pubxml 181 | *.publishproj 182 | 183 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 184 | # checkin your Azure Web App publish settings, but sensitive information contained 185 | # in these scripts will be unencrypted 186 | PublishScripts/ 187 | 188 | # NuGet Packages 189 | *.nupkg 190 | # NuGet Symbol Packages 191 | *.snupkg 192 | # The packages folder can be ignored because of Package Restore 193 | **/[Pp]ackages/* 194 | # except build/, which is used as an MSBuild target. 195 | !**/[Pp]ackages/build/ 196 | # Uncomment if necessary however generally it will be regenerated when needed 197 | #!**/[Pp]ackages/repositories.config 198 | # NuGet v3's project.json files produces more ignorable files 199 | *.nuget.props 200 | *.nuget.targets 201 | 202 | # Microsoft Azure Build Output 203 | csx/ 204 | *.build.csdef 205 | 206 | # Microsoft Azure Emulator 207 | ecf/ 208 | rcf/ 209 | 210 | # Windows Store app package directories and files 211 | AppPackages/ 212 | BundleArtifacts/ 213 | Package.StoreAssociation.xml 214 | _pkginfo.txt 215 | *.appx 216 | *.appxbundle 217 | *.appxupload 218 | 219 | # Visual Studio cache files 220 | # files ending in .cache can be ignored 221 | *.[Cc]ache 222 | # but keep track of directories ending in .cache 223 | !?*.[Cc]ache/ 224 | 225 | # Others 226 | ClientBin/ 227 | ~$* 228 | *~ 229 | *.dbmdl 230 | *.dbproj.schemaview 231 | *.jfm 232 | *.pfx 233 | *.publishsettings 234 | orleans.codegen.cs 235 | 236 | # Including strong name files can present a security risk 237 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 238 | #*.snk 239 | 240 | # Since there are multiple workflows, uncomment next line to ignore bower_components 241 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 242 | #bower_components/ 243 | 244 | # RIA/Silverlight projects 245 | Generated_Code/ 246 | 247 | # Backup & report files from converting an old project file 248 | # to a newer Visual Studio version. Backup files are not needed, 249 | # because we have git ;-) 250 | _UpgradeReport_Files/ 251 | Backup*/ 252 | UpgradeLog*.XML 253 | UpgradeLog*.htm 254 | ServiceFabricBackup/ 255 | *.rptproj.bak 256 | 257 | # SQL Server files 258 | *.mdf 259 | *.ldf 260 | *.ndf 261 | 262 | # Business Intelligence projects 263 | *.rdl.data 264 | *.bim.layout 265 | *.bim_*.settings 266 | *.rptproj.rsuser 267 | *- [Bb]ackup.rdl 268 | *- [Bb]ackup ([0-9]).rdl 269 | *- [Bb]ackup ([0-9][0-9]).rdl 270 | 271 | # Microsoft Fakes 272 | FakesAssemblies/ 273 | 274 | # GhostDoc plugin setting file 275 | *.GhostDoc.xml 276 | 277 | # Node.js Tools for Visual Studio 278 | .ntvs_analysis.dat 279 | node_modules/ 280 | 281 | # Visual Studio 6 build log 282 | *.plg 283 | 284 | # Visual Studio 6 workspace options file 285 | *.opt 286 | 287 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 288 | *.vbw 289 | 290 | # Visual Studio LightSwitch build output 291 | **/*.HTMLClient/GeneratedArtifacts 292 | **/*.DesktopClient/GeneratedArtifacts 293 | **/*.DesktopClient/ModelManifest.xml 294 | **/*.Server/GeneratedArtifacts 295 | **/*.Server/ModelManifest.xml 296 | _Pvt_Extensions 297 | 298 | # Paket dependency manager 299 | .paket/paket.exe 300 | paket-files/ 301 | 302 | # FAKE - F# Make 303 | .fake/ 304 | 305 | # CodeRush personal settings 306 | .cr/personal 307 | 308 | # Python Tools for Visual Studio (PTVS) 309 | __pycache__/ 310 | *.pyc 311 | 312 | # Cake - Uncomment if you are using it 313 | # tools/** 314 | # !tools/packages.config 315 | 316 | # Tabs Studio 317 | *.tss 318 | 319 | # Telerik's JustMock configuration file 320 | *.jmconfig 321 | 322 | # BizTalk build output 323 | *.btp.cs 324 | *.btm.cs 325 | *.odx.cs 326 | *.xsd.cs 327 | 328 | # OpenCover UI analysis results 329 | OpenCover/ 330 | 331 | # Azure Stream Analytics local run output 332 | ASALocalRun/ 333 | 334 | # MSBuild Binary and Structured Log 335 | *.binlog 336 | 337 | # NVidia Nsight GPU debugger configuration file 338 | *.nvuser 339 | 340 | # MFractors (Xamarin productivity tool) working folder 341 | .mfractor/ 342 | 343 | # Local History for Visual Studio 344 | .localhistory/ 345 | 346 | # BeatPulse healthcheck temp database 347 | healthchecksdb 348 | 349 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 350 | MigrationBackup/ 351 | 352 | # Ionide (cross platform F# VS Code tools) working folder 353 | .ionide/ 354 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 BepInEx 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /NStrip.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30011.22 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NStrip", "NStrip\NStrip.csproj", "{CE5BFF3E-2350-46AE-A54E-0631D89565F4}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {CE5BFF3E-2350-46AE-A54E-0631D89565F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {CE5BFF3E-2350-46AE-A54E-0631D89565F4}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {CE5BFF3E-2350-46AE-A54E-0631D89565F4}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {CE5BFF3E-2350-46AE-A54E-0631D89565F4}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {505788DE-B387-43E7-90DB-98E6903B88EE} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /NStrip/AssemblyStripper.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using Mono.Cecil; 4 | using Mono.Cecil.Cil; 5 | 6 | namespace NStrip 7 | { 8 | public enum StripType 9 | { 10 | ThrowNull, 11 | ValueRet, 12 | OnlyRet, 13 | EmptyBody, 14 | Extern 15 | } 16 | 17 | public static class AssemblyStripper 18 | { 19 | static IEnumerable GetAllTypeDefinitions(AssemblyDefinition assembly) 20 | { 21 | var typeQueue = new Queue(assembly.MainModule.Types); 22 | 23 | while (typeQueue.Count > 0) 24 | { 25 | var type = typeQueue.Dequeue(); 26 | 27 | yield return type; 28 | 29 | foreach (var nestedType in type.NestedTypes) 30 | typeQueue.Enqueue(nestedType); 31 | } 32 | } 33 | 34 | static void ClearMethodBodies(TypeReference voidTypeReference, ICollection methods, StripType stripType) 35 | { 36 | foreach (MethodDefinition method in methods) 37 | { 38 | if (!method.HasBody) 39 | continue; 40 | 41 | if (stripType == StripType.Extern) 42 | { 43 | method.Body = null; 44 | method.IsRuntime = true; 45 | method.IsIL = false; 46 | } 47 | else 48 | { 49 | MethodBody body = new MethodBody(method); 50 | var il = body.GetILProcessor(); 51 | 52 | if (stripType == StripType.ValueRet) 53 | { 54 | if (method.ReturnType.IsPrimitive) 55 | { 56 | il.Emit(OpCodes.Ldc_I4_0); 57 | } 58 | else if (method.ReturnType != voidTypeReference) 59 | { 60 | il.Emit(OpCodes.Ldnull); 61 | } 62 | 63 | il.Emit(OpCodes.Ret); 64 | } 65 | else if (stripType == StripType.OnlyRet) 66 | { 67 | il.Emit(OpCodes.Ret); 68 | } 69 | else if (stripType == StripType.ThrowNull) 70 | { 71 | il.Emit(OpCodes.Ldnull); 72 | il.Emit(OpCodes.Throw); 73 | } 74 | else if (stripType == StripType.EmptyBody) 75 | { 76 | il.Clear(); 77 | } 78 | 79 | method.Body = body; 80 | 81 | // Probably not necessary but just in case 82 | method.AggressiveInlining = false; 83 | method.NoInlining = true; 84 | } 85 | } 86 | } 87 | 88 | public static void StripAssembly(AssemblyDefinition assembly, StripType stripType, bool keepResources) 89 | { 90 | var voidTypeReference = assembly.MainModule.TypeSystem.Void; 91 | 92 | foreach (TypeDefinition type in GetAllTypeDefinitions(assembly)) 93 | { 94 | if (type.IsEnum || type.IsInterface) 95 | continue; 96 | 97 | ClearMethodBodies(voidTypeReference, type.Methods, stripType); 98 | } 99 | 100 | if (!keepResources) 101 | assembly.MainModule.Resources.Clear(); 102 | } 103 | 104 | public static void MakePublic(AssemblyDefinition assembly, IList typeNameBlacklist, bool includeCompilerGenerated, 105 | bool excludeCgEvents, bool removeReadOnly, bool unityNonSerialized) 106 | { 107 | bool checkCompilerGeneratedAttribute(IMemberDefinition member) 108 | { 109 | return member.CustomAttributes.Any(x => 110 | x.AttributeType.FullName == "System.Runtime.CompilerServices.CompilerGeneratedAttribute"); 111 | } 112 | 113 | MethodReference nonSerializedAttributeConstructor = null; 114 | 115 | if (unityNonSerialized) 116 | { 117 | var scope = assembly.MainModule.AssemblyReferences.OrderByDescending(a => a.Version).FirstOrDefault(a => a.Name == "mscorlib"); 118 | var attributeType = new TypeReference("System", "NonSerializedAttribute", assembly.MainModule, scope); 119 | 120 | nonSerializedAttributeConstructor = new MethodReference(".ctor", assembly.MainModule.TypeSystem.Void, attributeType) 121 | { 122 | HasThis = true, 123 | }; 124 | } 125 | 126 | foreach (var type in GetAllTypeDefinitions(assembly)) 127 | { 128 | if (typeNameBlacklist.Contains(type.Name)) 129 | continue; 130 | 131 | if (!includeCompilerGenerated && checkCompilerGeneratedAttribute(type)) 132 | continue; 133 | 134 | if (type.IsNested) 135 | type.IsNestedPublic = true; 136 | else 137 | type.IsPublic = true; 138 | 139 | foreach (var method in type.Methods) 140 | { 141 | if (!includeCompilerGenerated && 142 | (checkCompilerGeneratedAttribute(method) || method.IsCompilerControlled)) 143 | continue; 144 | 145 | method.IsPublic = true; 146 | } 147 | 148 | foreach (var field in type.Fields) 149 | { 150 | if (!includeCompilerGenerated && 151 | (checkCompilerGeneratedAttribute(field) || field.IsCompilerControlled)) 152 | continue; 153 | 154 | if (includeCompilerGenerated && excludeCgEvents) 155 | { 156 | if (type.Events.Any(x => x.Name == field.Name)) 157 | continue; 158 | } 159 | 160 | if (nonSerializedAttributeConstructor != null && !field.IsPublic && !field.CustomAttributes.Any(a => a.AttributeType.FullName == "UnityEngine.SerializeField")) 161 | { 162 | field.IsNotSerialized = true; 163 | field.CustomAttributes.Add(new CustomAttribute(nonSerializedAttributeConstructor)); 164 | } 165 | 166 | field.IsPublic = true; 167 | 168 | if (removeReadOnly) 169 | field.IsInitOnly = false; 170 | } 171 | } 172 | } 173 | } 174 | } -------------------------------------------------------------------------------- /NStrip/NArgs.cs: -------------------------------------------------------------------------------- 1 | /* 2 | NArgs 3 | The MIT License (MIT) 4 | 5 | Copyright(c) 2021 Bepis 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of 8 | this software and associated documentation files (the "Software"), to deal in 9 | the Software without restriction, including without limitation the rights to 10 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 11 | the Software, and to permit persons to whom the Software is furnished to do so, 12 | subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 19 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 20 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 21 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 22 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | */ 24 | 25 | using System; 26 | using System.Collections.Generic; 27 | using System.Linq; 28 | using System.Reflection; 29 | using System.Text; 30 | 31 | namespace NArgs 32 | { 33 | /// 34 | /// Command-line argument parser. 35 | /// 36 | public static class Arguments 37 | { 38 | /// 39 | /// Parses arguments and constructs an object. 40 | /// 41 | /// The type of the object to construct. Must inherit from 42 | /// The command-line arguments to parse. 43 | /// 44 | public static T Parse(string[] args) where T : IArgumentCollection, new() 45 | { 46 | Dictionary> valueSwitches = new Dictionary>(); 47 | Dictionary> boolSwitches = new Dictionary>(); 48 | 49 | var config = new T { Values = new List() }; 50 | 51 | var commandProps = GetCommandProperties(); 52 | 53 | foreach (var kv in commandProps) 54 | { 55 | if (kv.Value.PropertyType == typeof(bool)) 56 | { 57 | boolSwitches.Add(kv.Key, x => kv.Value.SetValue(config, x)); 58 | } 59 | else if (kv.Value.PropertyType == typeof(string)) 60 | { 61 | valueSwitches.Add(kv.Key, x => kv.Value.SetValue(config, x)); 62 | } 63 | else if (typeof(IList).IsAssignableFrom(kv.Value.PropertyType)) 64 | { 65 | if (kv.Value.GetValue(config) == null) 66 | { 67 | kv.Value.SetValue(config, new List()); 68 | } 69 | 70 | valueSwitches.Add(kv.Key, x => 71 | { 72 | var list = (IList)kv.Value.GetValue(config); 73 | list.Add(x); 74 | }); 75 | } 76 | else if (typeof(Enum).IsAssignableFrom(kv.Value.PropertyType)) 77 | { 78 | valueSwitches.Add(kv.Key, x => 79 | { 80 | if (!TryParseEnum(kv.Value.PropertyType, x, true, out var value)) 81 | throw new ArgumentException("Invalid value for argument: " + x); 82 | 83 | kv.Value.SetValue(config, value); 84 | }); 85 | } 86 | } 87 | 88 | CommandDefinitionAttribute previousSwitchDefinition = null; 89 | bool valuesOnly = false; 90 | 91 | foreach (string arg in args) 92 | { 93 | if (arg == "--") 94 | { 95 | // no more switches, only values 96 | valuesOnly = true; 97 | 98 | continue; 99 | } 100 | 101 | if (valuesOnly) 102 | { 103 | config.Values.Add(arg); 104 | continue; 105 | } 106 | 107 | if (arg.StartsWith("-") 108 | || arg.StartsWith("--")) 109 | { 110 | string previousSwitch; 111 | 112 | if (arg.StartsWith("--")) 113 | previousSwitch = arg.Substring(2); 114 | else 115 | previousSwitch = arg.Substring(1); 116 | 117 | if (boolSwitches.Keys.TryFirst(x 118 | => x.LongArg.Equals(previousSwitch, StringComparison.InvariantCultureIgnoreCase) 119 | || x.ShortArg?.Equals(previousSwitch, StringComparison.InvariantCultureIgnoreCase) == true, 120 | out var definition)) 121 | { 122 | boolSwitches[definition](true); 123 | previousSwitch = null; 124 | 125 | continue; 126 | } 127 | 128 | if (valueSwitches.Keys.TryFirst(x 129 | => x.LongArg.Equals(previousSwitch, StringComparison.InvariantCultureIgnoreCase) 130 | || x.ShortArg?.Equals(previousSwitch, StringComparison.InvariantCultureIgnoreCase) == true, 131 | out definition)) 132 | { 133 | previousSwitchDefinition = definition; 134 | 135 | continue; 136 | } 137 | 138 | Console.WriteLine("Unrecognized command line option: " + arg); 139 | throw new Exception(); 140 | } 141 | 142 | if (previousSwitchDefinition != null) 143 | { 144 | valueSwitches[previousSwitchDefinition](arg); 145 | previousSwitchDefinition = null; 146 | } 147 | else 148 | { 149 | config.Values.Add(arg); 150 | } 151 | } 152 | 153 | foreach (var kv in commandProps) 154 | { 155 | if (!kv.Key.Required) 156 | continue; 157 | 158 | if (kv.Value.PropertyType == typeof(string)) 159 | if (kv.Value.GetValue(config) == null) 160 | throw new ArgumentException($"Required argument not provided: {kv.Key.LongArg}"); 161 | 162 | if (kv.Value.PropertyType == typeof(IList)) 163 | if (((IList)kv.Value.GetValue(config)).Count == 0) 164 | throw new ArgumentException($"Required argument not provided: {kv.Key.LongArg}"); 165 | } 166 | 167 | return config; 168 | } 169 | 170 | /// 171 | /// Generates a string to be printed as console help text. 172 | /// 173 | /// The type of the arguments object to create help instructions for. Must inherit from 174 | /// The copyright text to add at the top, if any. 175 | /// The usage text to add at the top, if any. 176 | public static string PrintLongHelp(string copyrightText = null, string usageText = null) where T : IArgumentCollection 177 | { 178 | var commands = GetCommandProperties(); 179 | 180 | var builder = new StringBuilder(); 181 | 182 | if (copyrightText != null) 183 | builder.AppendLine(copyrightText); 184 | 185 | if (usageText != null) 186 | builder.AppendLine(usageText); 187 | 188 | builder.AppendLine(); 189 | builder.AppendLine(); 190 | 191 | var orderedCommands = commands 192 | .OrderByDescending(x => x.Key.Order) 193 | .ThenBy(x => x.Key.ShortArg ?? "zzzz") 194 | .ThenBy(x => x.Key.LongArg); 195 | 196 | foreach (var command in orderedCommands) 197 | { 198 | var valueString = string.Empty; 199 | 200 | if (command.Value.PropertyType == typeof(IList) 201 | || command.Value.PropertyType == typeof(string)) 202 | { 203 | valueString = " "; 204 | } 205 | else if (typeof(Enum).IsAssignableFrom(command.Value.PropertyType)) 206 | { 207 | valueString = $" ({string.Join(" | ", Enum.GetNames(command.Value.PropertyType))})"; 208 | } 209 | 210 | string listing = command.Key.ShortArg != null 211 | ? $" -{command.Key.ShortArg}, --{command.Key.LongArg}{valueString}" 212 | : $" --{command.Key.LongArg}{valueString}"; 213 | 214 | const int listingWidth = 45; 215 | const int descriptionWidth = 65; 216 | 217 | string listingWidthString = "".PadLeft(listingWidth); 218 | 219 | builder.Append(listing.PadRight(listingWidth)); 220 | 221 | if (listing.Length > listingWidth - 3) 222 | { 223 | builder.AppendLine(); 224 | builder.Append(listingWidthString); 225 | } 226 | 227 | if (!string.IsNullOrEmpty(command.Key.Description)) 228 | { 229 | BuildArgumentDescription(builder, command.Key.Description, listingWidth, descriptionWidth); 230 | } 231 | 232 | builder.AppendLine(); 233 | } 234 | 235 | builder.AppendLine(); 236 | 237 | return builder.ToString(); 238 | } 239 | 240 | private static void BuildArgumentDescription(StringBuilder builder, string description, int listingWidth, int descriptionWidth) 241 | { 242 | int lineLength = 0; 243 | int lineStartIndex = 0; 244 | int lastValidLength = 0; 245 | 246 | for (var index = 0; index < description.Length; index++) 247 | { 248 | char c = description[index]; 249 | 250 | void PrintLine() 251 | { 252 | var descriptionSubstring = description.Substring(lineStartIndex, lastValidLength); 253 | builder.AppendLine(descriptionSubstring); 254 | builder.Append(' ', listingWidth); 255 | 256 | lineStartIndex = 1 + index - (lineLength - lastValidLength); 257 | lineLength = 1 + index - lineStartIndex; 258 | lastValidLength = lineLength; 259 | } 260 | 261 | if ((c == ' ' && lineLength >= descriptionWidth) | c == '\n') 262 | { 263 | bool printAgain = false; 264 | 265 | if (c == '\n' && lineLength < descriptionWidth) 266 | lastValidLength = lineLength; 267 | else if (c == '\n') 268 | printAgain = true; 269 | 270 | PrintLine(); 271 | 272 | if (printAgain) 273 | { 274 | // This works and I'm not sure how. 275 | 276 | lastValidLength--; 277 | lineLength--; 278 | PrintLine(); 279 | lastValidLength++; 280 | lineLength++; 281 | } 282 | 283 | continue; 284 | } 285 | 286 | if (c == ' ') 287 | lastValidLength = lineLength; 288 | 289 | lineLength++; 290 | } 291 | 292 | if (lineLength > 0) 293 | { 294 | var remainingSubstring = description.Substring(lineStartIndex); 295 | builder.AppendLine(remainingSubstring); 296 | } 297 | } 298 | 299 | private static Dictionary GetCommandProperties() 300 | { 301 | var commands = new Dictionary(); 302 | 303 | foreach (var prop in typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public)) 304 | { 305 | var commandDef = prop.GetCustomAttribute(); 306 | 307 | if (commandDef == null) 308 | continue; 309 | 310 | commands.Add(commandDef, prop); 311 | } 312 | 313 | return commands; 314 | } 315 | 316 | private static bool TryFirst(this IEnumerable enumerable, Func predicate, out T value) 317 | { 318 | foreach (var item in enumerable) 319 | { 320 | if (predicate(item)) 321 | { 322 | value = item; 323 | return true; 324 | } 325 | } 326 | 327 | value = default; 328 | return false; 329 | } 330 | 331 | private static MethodInfo GenericTryParseMethodInfo = null; 332 | private static bool TryParseEnum(Type enumType, string value, bool caseSensitive, out object val) 333 | { 334 | // Workaround for non-generic Enum.TryParse not being present below .NET 5 335 | 336 | if (GenericTryParseMethodInfo == null) 337 | { 338 | GenericTryParseMethodInfo = typeof(Enum).GetMethods(BindingFlags.Public | BindingFlags.Static) 339 | .First(x => x.Name == "TryParse" && x.GetGenericArguments().Length == 1 && 340 | x.GetParameters().Length == 3); 341 | } 342 | 343 | var objectArray = new object[] { value, caseSensitive, null }; 344 | 345 | var result = GenericTryParseMethodInfo.MakeGenericMethod(enumType) 346 | .Invoke(null, objectArray); 347 | 348 | val = objectArray[2]; 349 | return (bool)result; 350 | } 351 | } 352 | 353 | /// 354 | /// Specifies an object is an argument collection. 355 | /// 356 | public interface IArgumentCollection 357 | { 358 | /// 359 | /// A list of values that were passed in as arguments, but not associated with an option. 360 | /// 361 | IList Values { get; set; } 362 | } 363 | 364 | public class CommandDefinitionAttribute : Attribute 365 | { 366 | /// 367 | /// The short version of an option, e.g. "-a". Optional. 368 | /// 369 | public string ShortArg { get; set; } 370 | 371 | /// 372 | /// The long version of an option, e.g. "--append". Required. 373 | /// 374 | public string LongArg { get; set; } 375 | 376 | /// 377 | /// Whether or not to fail parsing if this argument has not been provided. 378 | /// 379 | public bool Required { get; set; } = false; 380 | 381 | /// 382 | /// The description of the option, to be used in the help text. 383 | /// 384 | public string Description { get; set; } = null; 385 | 386 | /// 387 | /// Used in ordering this command in the help list. 388 | /// 389 | public int Order { get; set; } = 0; 390 | 391 | /// The long version of an option, e.g. "--append". 392 | public CommandDefinitionAttribute(string longArg) 393 | { 394 | LongArg = longArg; 395 | } 396 | 397 | /// The short version of an option, e.g. "-a". 398 | /// The long version of an option, e.g. "--append". 399 | public CommandDefinitionAttribute(string shortArg, string longArg) 400 | { 401 | ShortArg = shortArg; 402 | LongArg = longArg; 403 | } 404 | } 405 | } -------------------------------------------------------------------------------- /NStrip/NStrip.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Exe 6 | net452 7 | 8.0 8 | disable 9 | disable 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /NStrip/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using Mono.Cecil; 5 | using NArgs; 6 | 7 | namespace NStrip 8 | { 9 | class Program 10 | { 11 | internal const string ShortVersion = "1.4.0"; 12 | internal const string LongVersion = ShortVersion + ".0"; 13 | 14 | static void LogError(string message) 15 | { 16 | var oldColor = Console.ForegroundColor; 17 | Console.ForegroundColor = ConsoleColor.Red; 18 | 19 | Console.WriteLine(message); 20 | 21 | Console.ForegroundColor = oldColor; 22 | } 23 | 24 | static void LogMessage(string message) 25 | { 26 | Console.WriteLine(message); 27 | } 28 | 29 | static int Main(string[] args) 30 | { 31 | NStripArguments arguments = Arguments.Parse(args); 32 | 33 | if (arguments.Values.Count == 0 || arguments.Help) 34 | { 35 | LogMessage(Arguments.PrintLongHelp( 36 | $"NStrip v{ShortVersion}, by Bepis", 37 | "Usage: NStrip [options] (<.NET .exe / .dll> | ) [ | ]")); 38 | return 1; 39 | } 40 | 41 | string path = arguments.Values[0]; 42 | 43 | string outputPath = arguments.Values.Count >= 2 ? arguments.Values[1] : null; 44 | 45 | var resolver = new DefaultAssemblyResolver(); 46 | 47 | foreach (var dependency in arguments.Dependencies) 48 | resolver.AddSearchDirectory(dependency); 49 | 50 | var readerParams = new ReaderParameters() 51 | { 52 | AssemblyResolver = resolver 53 | }; 54 | 55 | if (Directory.Exists(path)) 56 | { 57 | resolver.AddSearchDirectory(path); 58 | bool completedWithoutError = true; 59 | List failedFiles = new List(); 60 | 61 | foreach (var file in Directory.EnumerateFiles(path, "*.dll")) 62 | { 63 | string fileOutputPath = outputPath != null 64 | ? Path.Combine(outputPath, Path.GetFileName(file)) 65 | : file; 66 | 67 | if (!arguments.Overwrite && outputPath == null) 68 | fileOutputPath = AppendToEndOfFileName(file, "-nstrip"); 69 | 70 | if (!StripAssembly(file, fileOutputPath, arguments, readerParams)) 71 | { 72 | completedWithoutError = false; 73 | failedFiles.Add(fileOutputPath); 74 | } 75 | } 76 | 77 | if (!completedWithoutError) 78 | { 79 | LogError("Some files failed to process!"); 80 | LogError($"Files:\n{string.Join("\n", failedFiles)}"); 81 | return 1; 82 | } 83 | } 84 | else if (File.Exists(path)) 85 | { 86 | resolver.AddSearchDirectory(Path.GetDirectoryName(path)); 87 | 88 | string fileOutputPath = outputPath ?? 89 | (arguments.Overwrite ? path : AppendToEndOfFileName(path, "-nstrip")); 90 | 91 | if (!StripAssembly(path, fileOutputPath, arguments, readerParams)) 92 | { 93 | throw new Exception($"Failed to process {path} when it was the only file needing it!"); 94 | } 95 | } 96 | else 97 | { 98 | LogError($"Could not find path {path}"); 99 | } 100 | 101 | LogMessage("Finished!"); 102 | return 0; 103 | } 104 | 105 | static bool StripAssembly(string assemblyPath, string outputPath, NStripArguments arguments, ReaderParameters readerParams) 106 | { 107 | LogMessage($"Stripping {assemblyPath}"); 108 | try 109 | { 110 | 111 | using var memoryStream = new MemoryStream(File.ReadAllBytes(assemblyPath)); 112 | using var assemblyDefinition = AssemblyDefinition.ReadAssembly(memoryStream, readerParams); 113 | 114 | if (!arguments.NoStrip) 115 | AssemblyStripper.StripAssembly(assemblyDefinition, arguments.StripType, arguments.KeepResources); 116 | 117 | if (arguments.Public) 118 | AssemblyStripper.MakePublic(assemblyDefinition, arguments.Blacklist, arguments.IncludeCompilerGenerated, 119 | arguments.ExcludeCompilerGeneratedEvents, arguments.RemoveReadOnlyAttribute, arguments.UnityNonSerialized); 120 | 121 | // We write to a memory stream first to ensure that Mono.Cecil doesn't have any errors when producing the assembly. 122 | // Otherwise, if we're overwriting the same assembly and it fails, it will overwrite with a 0 byte file 123 | 124 | using var tempStream = new MemoryStream(); 125 | 126 | assemblyDefinition.Write(tempStream); 127 | 128 | if (arguments.NoStrip && !arguments.Public) 129 | return true;; 130 | 131 | tempStream.Position = 0; 132 | using var outputStream = File.Open(outputPath, FileMode.Create); 133 | 134 | tempStream.CopyTo(outputStream); 135 | return true; 136 | } 137 | catch (Exception ex) 138 | { 139 | LogError($"Failed to strip assembly {assemblyPath}: \n{ex}"); 140 | return false; 141 | } 142 | } 143 | 144 | static string AppendToEndOfFileName(string path, string appendedString) 145 | { 146 | return Path.Combine( 147 | Path.GetDirectoryName(path), 148 | $"{Path.GetFileNameWithoutExtension(path)}{appendedString}{Path.GetExtension(path)}" 149 | ); 150 | } 151 | 152 | private class NStripArguments : IArgumentCollection 153 | { 154 | public IList Values { get; set; } 155 | 156 | [CommandDefinition("h", "help", Description = "Prints help text", Order = 1)] 157 | public bool Help { get; set; } 158 | 159 | [CommandDefinition("p", "public", Description = "Changes visibility of all types, nested types, methods and fields to public.")] 160 | public bool Public { get; set; } 161 | 162 | [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.")] 163 | public IList Dependencies { get; set; } 164 | 165 | [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.")] 166 | public IList Blacklist { get; set; } 167 | 168 | [CommandDefinition("n", "no-strip", Description = "Does not strip assemblies. If this is not being used with --public, assemblies are not modified/saved.")] 169 | public bool NoStrip { get; set; } 170 | 171 | [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.")] 172 | public bool Overwrite { get; set; } 173 | 174 | [CommandDefinition("keep-resources", Description = "Keeps manifest resources intact instead of removing them when stripping.")] 175 | public bool KeepResources { get; set; } 176 | 177 | [CommandDefinition("t", "strip-type", Description = "The type of stripping to perform.\n\nValueRet: Returns a dummy value and ret opcode. Largest but runtime-safe.\nOnlyRet: Only adds a ret opcode. Slightly smaller than ValueRet but may not be runtime-safe.\nEmptyBody: No opcodes in body. Slightly smaller again but is not runtime-safe.\nThrowNull: Makes all methods throw null. Runtime-safe and is the MS standard. Default.\nExtern: Marks all methods as extern, and removes their bodies. Smallest size, but not runtime-safe and might not be compile-time safe.")] 178 | public StripType StripType { get; set; } 179 | 180 | [CommandDefinition("cg", "include-compiler-generated", Description = "When publicizing, also publicize compiler generated types and members. By default, this does not occur. Does nothing if not publicizing.", Order = -1)] 181 | public bool IncludeCompilerGenerated { get; set; } 182 | 183 | [CommandDefinition("cg-exclude-events", Description = "To be used in conjunction with -cg. Will not publicize fields that are used to back auto-generated events.", Order = -1)] 184 | public bool ExcludeCompilerGeneratedEvents { get; set; } 185 | 186 | [CommandDefinition("remove-readonly", Description = "Remove the readonly attribute from fields. Only works with the mono runtime, other runtimes complain about access violations.")] 187 | public bool RemoveReadOnlyAttribute { get; set; } 188 | 189 | [CommandDefinition("unity-non-serialized", Description = "Prevents Unity from implicitly serializing publicized fields. For use within the Unity Editor")] 190 | public bool UnityNonSerialized { get; set; } 191 | } 192 | } 193 | } -------------------------------------------------------------------------------- /NStrip/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | [assembly: AssemblyDescription(".NET Assembly stripper, publicizer and general utility tool")] 4 | [assembly: AssemblyCopyright("Copyright © BepInEx Team 2021")] 5 | [assembly: AssemblyTrademark("")] 6 | [assembly: AssemblyCulture("")] 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NStrip 2 | .NET Assembly stripper, publicizer and general utility tool 3 | 4 | Note: This is less maintained than the BepInEx stripping tool available here: https://github.com/BepInEx/BepInEx.AssemblyPublicizer 5 | 6 | ## Usage 7 | The general usage of NStrip is `NStrip [options] ()`. Input and output can be a file or a folder, but they have to match. Output is optional. 8 | 9 | - `-h | --help` prints help text. 10 | - `-p | --public` sets all types, methods, properties and fields to public. 11 | - `-n | --no-strip` does not strip assemblies. If not used in conjunction with `-p`, this tool will write nothing. 12 | - `-d | --dependencies ` specifies a folder that contains additional dependencies for the target assemblies, if they are currently not in the same folder as the target assembly. Mono.Cecil will fail to output files if it cannot find all dependencies for the target assemblies. Can be specified multiple times 13 | - `-b | --blacklist` is a blacklist for type name publicization. For example, `-b "Type"` will prevent types with the name "Type" from becoming public, which can help if types that are publicizised conflict with already public types and can cause issues with compilation. 14 | - `-o | --overwrite` will overwrite target assemblies instead of appending `-nstrip` to the end of the filename. Does nothing if `` is specified. 15 | - `-cg | --include-compiler-generated` will publicize compiler generated members & types (they are not made public by default). `-p` is required for this to be useful. 16 | - `--cg-exclude-events` is used in conjunction with `-cg` if you wish to exclude event backing fields from being publicized, as they typically have the same name and can cause compilation issues. 17 | - `--keep-resources` will not strip manifest resources when stripping an assembly. 18 | - `-t | --strip-type` specifies the type of method body stripping that will be used: 19 | - `ThrowNull`: Makes all methods throw null. Runtime-safe and is the MS standard. Default. 20 | - `ValueRet`: Returns a dummy value and ret opcode. Largest but runtime-safe. 21 | - `OnlyRet`: Only adds a ret opcode. Slightly smaller than ValueRet but may not be runtime-safe. 22 | - `EmptyBody`: No opcodes in body. Slightly smaller again but is not runtime-safe. 23 | - `Extern`: Marks all methods as extern, and removes their bodies. Smallest size, but not runtime-safe and might not be compile-time safe. 24 | - `--remove-readonly` removes the readonly attribute from fields. Only works with the mono runtime, other runtimes will complain about access violations. 25 | - `--unity-non-serialized` prevents Unity from implicitly serializing publicized fields. For use within the Unity Editor 26 | 27 | ## Credits 28 | Uses NArgs from https://github.com/bbepis/NArgs 29 | --------------------------------------------------------------------------------