#nullable enable using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Reflection; using System.Runtime.InteropServices; using System.Linq; using IPA.Logging; using IPA.Utilities; using Mono.Cecil; using IPA.AntiMalware; using IPA.Config; #if NET3 using Net3_Proxy; using Directory = Net3_Proxy.Directory; using Path = Net3_Proxy.Path; using File = Net3_Proxy.File; #endif namespace IPA.Loader { internal class CecilLibLoader : BaseAssemblyResolver { private static readonly string CurrentAssemblyName = Assembly.GetExecutingAssembly().GetName().Name; private static readonly string CurrentAssemblyPath = Assembly.GetExecutingAssembly().Location; public override AssemblyDefinition Resolve(AssemblyNameReference name, ReaderParameters parameters) { LibLoader.SetupAssemblyFilenames(); if (name.Name == CurrentAssemblyName) return AssemblyDefinition.ReadAssembly(CurrentAssemblyPath, parameters); if (LibLoader.FilenameLocations.TryGetValue($"{name.Name}.dll", out var path)) { if (File.Exists(path)) return AssemblyDefinition.ReadAssembly(path, parameters); } else if (LibLoader.FilenameLocations.TryGetValue($"{name.Name}.{name.Version}.dll", out path)) { if (File.Exists(path)) return AssemblyDefinition.ReadAssembly(path, parameters); } return base.Resolve(name, parameters); } } internal static class LibLoader { internal static string LibraryPath => Path.Combine(Environment.CurrentDirectory, "Libs"); internal static string NativeLibraryPath => Path.Combine(LibraryPath, "Native"); internal static Dictionary FilenameLocations = null!; internal static void Configure() { SetupAssemblyFilenames(true); AppDomain.CurrentDomain.AssemblyResolve -= AssemblyLibLoader; AppDomain.CurrentDomain.AssemblyResolve += AssemblyLibLoader; } internal static void SetupAssemblyFilenames(bool force = false) { if (FilenameLocations == null || force) { FilenameLocations = new Dictionary(); foreach (var fn in TraverseTree(LibraryPath, s => s != NativeLibraryPath)) { if (FilenameLocations.ContainsKey(fn.Name)) Log(Logger.Level.Critical, $"Multiple instances of {fn.Name} exist in Libs! Ignoring {fn.FullName}"); else FilenameLocations.Add(fn.Name, fn.FullName); } if (!SetDefaultDllDirectories(LoadLibraryFlags.LOAD_LIBRARY_SEARCH_USER_DIRS | LoadLibraryFlags.LOAD_LIBRARY_SEARCH_SYSTEM32 | LoadLibraryFlags.LOAD_LIBRARY_SEARCH_DEFAULT_DIRS | LoadLibraryFlags.LOAD_LIBRARY_SEARCH_APPLICATION_DIR)) { var err = new Win32Exception(); Log(Logger.Level.Critical, $"Error configuring DLL search path"); Log(Logger.Level.Critical, err); return; } static void AddDir(string path) { var retPtr = AddDllDirectory(path); if (retPtr == IntPtr.Zero) { var err = new Win32Exception(); Log(Logger.Level.Warning, $"Could not add DLL directory {path}"); Log(Logger.Level.Warning, err); } } if (Directory.Exists(NativeLibraryPath)) { AddDir(NativeLibraryPath); _ = TraverseTree(NativeLibraryPath, dir => { // this is a terrible hack for iterating directories AddDir(dir); return true; }).All(f => true); // force it to iterate all } //var unityData = Directory.EnumerateDirectories(Environment.CurrentDirectory, "*_Data").First(); //AddDir(Path.Combine(unityData, "Plugins")); foreach (var dir in Environment.GetEnvironmentVariable("path") .Split(Path.PathSeparator) .Select(Environment.ExpandEnvironmentVariables)) { AddDir(dir); } } } public static Assembly? AssemblyLibLoader(object source, ResolveEventArgs e) { var asmName = new AssemblyName(e.Name); return LoadLibrary(asmName); } internal static Assembly? LoadLibrary(AssemblyName asmName) { Log(Logger.Level.Debug, $"Resolving library {asmName}"); SetupAssemblyFilenames(); var testFile = $"{asmName.Name}.dll"; Log(Logger.Level.Debug, $"Looking for file {asmName.Name}.dll"); if (FilenameLocations.TryGetValue(testFile, out var path)) { Log(Logger.Level.Debug, $"Found file {testFile} as {path}"); return LoadSafe(path); } else if (FilenameLocations.TryGetValue(testFile = $"{asmName.Name}.{asmName.Version}.dll", out path)) { Log(Logger.Level.Debug, $"Found file {testFile} as {path}"); Log(Logger.Level.Warning, $"File {testFile} should be renamed to just {asmName.Name}.dll"); return LoadSafe(path); } Log(Logger.Level.Critical, $"No library {asmName} found"); return null; } private static Assembly? LoadSafe(string path) { if (!File.Exists(path)) { Log(Logger.Level.Critical, $"{path} no longer exists!"); return null; } if (AntiMalwareEngine.IsInitialized) { var result = AntiMalwareEngine.Engine.ScanFile(new FileInfo(path)); if (result is ScanResult.Detected) { Log(Logger.Level.Error, $"Scan of '{path}' found malware; not loading"); return null; } if (!SelfConfig.AntiMalware_.RunPartialThreatCode_ && result is not ScanResult.KnownSafe and not ScanResult.NotDetected) { Log(Logger.Level.Error, $"Scan of '{path}' found partial threat; not loading. To load this, enable AntiMalware.RunPartialThreatCode in the config."); return null; } } return Assembly.LoadFrom(path); } internal static void Log(Logger.Level lvl, string message) { // multiple proxy methods to delay loading of assemblies until it's done if (Logger.LogCreated) AssemblyLibLoaderCallLogger(lvl, message); else if (((byte)lvl & (byte)StandardLogger.PrintFilter) != 0) Console.WriteLine($"[{lvl}] {message}"); } internal static void Log(Logger.Level lvl, Exception message) { // multiple proxy methods to delay loading of assemblies until it's done if (Logger.LogCreated) AssemblyLibLoaderCallLogger(lvl, message); else if (((byte)lvl & (byte)StandardLogger.PrintFilter) != 0) Console.WriteLine($"[{lvl}] {message}"); } private static void AssemblyLibLoaderCallLogger(Logger.Level lvl, string message) => Logger.libLoader.Log(lvl, message); private static void AssemblyLibLoaderCallLogger(Logger.Level lvl, Exception message) => Logger.libLoader.Log(lvl, message); // https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/file-system/how-to-iterate-through-a-directory-tree private static IEnumerable TraverseTree(string root, Func? dirValidator = null) { if (dirValidator == null) dirValidator = s => true; var dirs = new Stack(32); if (!Directory.Exists(root)) throw new ArgumentException("Directory does not exist", nameof(root)); dirs.Push(root); while (dirs.Count > 0) { string currentDir = dirs.Pop(); string[] subDirs; try { subDirs = Directory.GetDirectories(currentDir); } catch (UnauthorizedAccessException) { continue; } catch (DirectoryNotFoundException) { continue; } string[] files; try { files = Directory.GetFiles(currentDir); } catch (UnauthorizedAccessException) { continue; } catch (DirectoryNotFoundException) { continue; } foreach (string str in subDirs) if (dirValidator(str)) dirs.Push(str); foreach (string file in files) { FileInfo nextValue; try { nextValue = new FileInfo(file); } catch (FileNotFoundException) { continue; } yield return nextValue; } } } [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] #if NET461 [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] #endif private static extern IntPtr AddDllDirectory(string lpPathName); [Flags] private enum LoadLibraryFlags : uint { None = 0, LOAD_LIBRARY_SEARCH_APPLICATION_DIR = 0x00000200, LOAD_LIBRARY_SEARCH_DEFAULT_DIRS = 0x00001000, LOAD_LIBRARY_SEARCH_SYSTEM32 = 0x00000800, LOAD_LIBRARY_SEARCH_USER_DIRS = 0x00000400, } [DllImport("kernel32.dll", SetLastError = true)] #if NET461 [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] #endif private static extern bool SetDefaultDllDirectories(LoadLibraryFlags dwFlags); } }