diff --git a/IPA.Injector/Injector.cs b/IPA.Injector/Injector.cs index 6c3fc030..76c89690 100644 --- a/IPA.Injector/Injector.cs +++ b/IPA.Injector/Injector.cs @@ -1,332 +1,331 @@ -#nullable enable -using IPA.AntiMalware; -using IPA.Config; -using IPA.Injector.Backups; -using IPA.Loader; -using IPA.Logging; -using IPA.Utilities; -using Mono.Cecil; -using Mono.Cecil.Cil; -using System; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Threading.Tasks; -using UnityEngine; -using static IPA.Logging.Logger; -using MethodAttributes = Mono.Cecil.MethodAttributes; -#if NET3 -using Net3_Proxy; -using Path = Net3_Proxy.Path; -using File = Net3_Proxy.File; -using Directory = Net3_Proxy.Directory; -#endif - -namespace IPA.Injector -{ - /// - /// The entry point type for BSIPA's Doorstop injector. - /// - // ReSharper disable once UnusedMember.Global - internal static class Injector - { - private static Task? pluginAsyncLoadTask; - private static Task? permissionFixTask; - //private static string otherNewtonsoftJson = null; - - // ReSharper disable once UnusedParameter.Global - internal static void Main(string[] args) - { // entry point for doorstop - // At this point, literally nothing but mscorlib is loaded, - // and since this class doesn't have any static fields that - // aren't defined in mscorlib, we can control exactly what - // gets loaded. - _ = args; - try - { - var arguments = Environment.GetCommandLineArgs(); - MaybeInitializeConsole(arguments); - - SetupLibraryLoading(); - - EnsureDirectories(); - - // this is weird, but it prevents Mono from having issues loading the type. - // IMPORTANT: NO CALLS TO ANY LOGGER CAN HAPPEN BEFORE THIS - var unused = StandardLogger.PrintFilter; - #region // Above hack explanation - /* - * Due to an unknown bug in the version of Mono that Unity uses, if the first access to StandardLogger - * is a call to a constructor, then Mono fails to load the type correctly. However, if the first access is to - * the above static property (or maybe any, but I don't really know) it behaves as expected and works fine. - */ - #endregion - - Default.Debug("Initializing logger"); - - SelfConfig.ReadCommandLine(arguments); - SelfConfig.Load(); - DisabledConfig.Load(); - - if (AntiPiracy.IsInvalid(Environment.CurrentDirectory)) - { - Default.Error("Invalid installation; please buy the game to run BSIPA."); - - return; - } - - CriticalSection.Configure(); - - Logging.Logger.Injector.Debug("Prepping bootstrapper"); - - // make sure to load the game version and check boundaries before installing the bootstrap, because that uses the game assemblies property - GameVersionEarly.Load(); - SelfConfig.Instance.CheckVersionBoundary(); - - // updates backup - InstallBootstrapPatch(); - - AntiMalwareEngine.Initialize(); - - Updates.InstallPendingUpdates(); - - Loader.LibLoader.SetupAssemblyFilenames(true); - - pluginAsyncLoadTask = PluginLoader.LoadTask(); - permissionFixTask = PermissionFix.FixPermissions(new DirectoryInfo(Environment.CurrentDirectory)); - } - catch (Exception e) - { - Console.WriteLine(e); - } - } - - private static void MaybeInitializeConsole(string[] arguments) - { - var i = 0; - while (i < arguments.Length) - { - if (arguments[i++] == "--verbose") - { - if (i == arguments.Length) - { - WinConsole.Initialize(WinConsole.AttachParent); - return; - } - - WinConsole.Initialize(int.TryParse(arguments[i], out int processId) ? processId : WinConsole.AttachParent); - return; - } - } - } - - private static void EnsureDirectories() - { - string path; - if (!Directory.Exists(path = Path.Combine(Environment.CurrentDirectory, "UserData"))) - _ = Directory.CreateDirectory(path); - if (!Directory.Exists(path = Path.Combine(Environment.CurrentDirectory, "Plugins"))) - _ = Directory.CreateDirectory(path); - } - - private static void SetupLibraryLoading() - { - if (loadingDone) return; - loadingDone = true; - Loader.LibLoader.Configure(); - } - - private static void InstallBootstrapPatch() - { - var sw = Stopwatch.StartNew(); - - var cAsmName = Assembly.GetExecutingAssembly().GetName(); - var managedPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!; - - var dataDir = new DirectoryInfo(managedPath).Parent!.Name; - var gameName = dataDir.Substring(0, dataDir.Length - 5); - - Logging.Logger.Injector.Debug("Finding backup"); - var backupPath = Path.Combine(Environment.CurrentDirectory, "IPA", "Backups", gameName); - var bkp = BackupManager.FindLatestBackup(backupPath); - if (bkp == null) - Logging.Logger.Injector.Warn("No backup found! Was BSIPA installed using the installer?"); - - // TODO: Investigate if this ever worked properly. - // this is a critical section because if you exit in here, assembly can die - using var critSec = CriticalSection.ExecuteSection(); - - var readerParameters = new ReaderParameters - { - ReadWrite = false, - InMemory = true, - ReadingMode = ReadingMode.Immediate - }; - - Logging.Logger.Injector.Debug("Ensuring patch on UnityEngine.CoreModule exists"); - - #region Insert patch into UnityEngine.CoreModule.dll - - var unityPath = Path.Combine(managedPath, "UnityEngine.CoreModule.dll"); - - using var unityAsmDef = AssemblyDefinition.ReadAssembly(unityPath, readerParameters); - var unityModDef = unityAsmDef.MainModule; - - bool modified = false; - foreach (var asmref in unityModDef.AssemblyReferences) - { - if (asmref.Name == cAsmName.Name) - { - if (asmref.Version != cAsmName.Version) - { - asmref.Version = cAsmName.Version; - modified = true; - } - } - } - - var application = unityModDef.GetType("UnityEngine", "Camera"); - - if (application == null) - { - Logging.Logger.Injector.Critical("UnityEngine.CoreModule doesn't have a definition for UnityEngine.Camera!" - + "Nothing to patch to get ourselves into the Unity run cycle!"); - goto endPatchCoreModule; - } - - MethodDefinition? cctor = null; - foreach (var m in application.Methods) - if (m.IsRuntimeSpecialName && m.Name == ".cctor") - cctor = m; - - var cbs = unityModDef.ImportReference(((Action)CreateBootstrapper).Method); - - if (cctor == null) - { - cctor = new MethodDefinition(".cctor", - MethodAttributes.RTSpecialName | MethodAttributes.Static | MethodAttributes.SpecialName, - unityModDef.TypeSystem.Void); - application.Methods.Add(cctor); - modified = true; - - var ilp = cctor.Body.GetILProcessor(); - ilp.Emit(OpCodes.Call, cbs); - ilp.Emit(OpCodes.Ret); - } - else - { - var ilp = cctor.Body.GetILProcessor(); - for (var i = 0; i < Math.Min(2, cctor.Body.Instructions.Count); i++) - { - var ins = cctor.Body.Instructions[i]; - switch (i) - { - case 0 when ins.OpCode != OpCodes.Call: - ilp.Replace(ins, ilp.Create(OpCodes.Call, cbs)); - modified = true; - break; - - case 0: - { - var methodRef = ins.Operand as MethodReference; - if (methodRef?.FullName != cbs.FullName) - { - ilp.Replace(ins, ilp.Create(OpCodes.Call, cbs)); - modified = true; - } - - break; - } - case 1 when ins.OpCode != OpCodes.Ret: - ilp.Replace(ins, ilp.Create(OpCodes.Ret)); - modified = true; - break; - } - } - } - - if (modified) - { - string tempFilePath = Path.GetTempFileName(); - bkp?.Add(unityPath); - unityAsmDef.Write(tempFilePath); - File.Delete(unityPath); - File.Move(tempFilePath, unityPath); - } - endPatchCoreModule: - #endregion Insert patch into UnityEngine.CoreModule.dll - -#if BeatSaber - Logging.Logger.Injector.Debug("Ensuring anti-yeet patch exists"); - - var name = SelfConfig.GameAssemblies_.FirstOrDefault() ?? SelfConfig.GetDefaultGameAssemblies().First(); - var ascPath = Path.Combine(managedPath, name); - - try - { - using var ascAsmDef = AssemblyDefinition.ReadAssembly(ascPath, readerParameters); - var ascModDef = ascAsmDef.MainModule; - - var deleter = ascModDef.GetType("IPAPluginsDirDeleter"); - - if (deleter.Methods.Count > 0) - { - deleter.Methods.Clear(); // delete all methods - - string tempFilePath = Path.GetTempFileName(); - bkp?.Add(ascPath); - ascAsmDef.Write(tempFilePath); - File.Delete(ascPath); - File.Move(tempFilePath, ascPath); - } - } - catch (Exception e) - { - Logging.Logger.Injector.Warn($"Could not apply anti-yeet patch to {ascPath}"); - if (SelfConfig.Debug_.ShowHandledErrorStackTraces_) - Logging.Logger.Injector.Warn(e); - } -#endif - - sw.Stop(); - Logging.Logger.Injector.Info($"Installing bootstrapper took {sw.Elapsed}"); - } - - private static bool bootstrapped; - - private static void CreateBootstrapper() - { - if (bootstrapped) return; - bootstrapped = true; - - Application.logMessageReceivedThreaded += delegate (string condition, string stackTrace, LogType type) - { - var level = UnityLogRedirector.LogTypeToLevel(type); - UnityLogProvider.UnityLogger.Log(level, $"{condition}"); - UnityLogProvider.UnityLogger.Log(level, $"{stackTrace}"); - }; - - StdoutInterceptor.EnsureHarmonyLogging(); - - // need to reinit streams singe Unity seems to redirect stdout - StdoutInterceptor.RedirectConsole(); - - var bootstrapper = new GameObject("NonDestructiveBootstrapper").AddComponent(); - bootstrapper.Destroyed += Bootstrapper_Destroyed; - } - - private static bool loadingDone; - - private static void Bootstrapper_Destroyed() - { - // wait for plugins to finish loading - pluginAsyncLoadTask?.Wait(); - permissionFixTask?.Wait(); - - Default.Debug("Plugins loaded"); - Default.Debug(string.Join(", ", PluginLoader.PluginsMetadata.StrJP())); - _ = PluginComponent.Create(); - } - } -} +#nullable enable +using IPA.AntiMalware; +using IPA.Config; +using IPA.Injector.Backups; +using IPA.Loader; +using IPA.Logging; +using IPA.Utilities; +using Mono.Cecil; +using Mono.Cecil.Cil; +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using UnityEngine; +using static IPA.Logging.Logger; +using MethodAttributes = Mono.Cecil.MethodAttributes; +#if NET3 +using Net3_Proxy; +using Path = Net3_Proxy.Path; +using File = Net3_Proxy.File; +using Directory = Net3_Proxy.Directory; +#endif + +namespace IPA.Injector +{ + /// + /// The entry point type for BSIPA's Doorstop injector. + /// + // ReSharper disable once UnusedMember.Global + internal static class Injector + { + private static Task? pluginAsyncLoadTask; + private static Task? permissionFixTask; + + // ReSharper disable once UnusedParameter.Global + internal static void Main(string[] args) + { // entry point for doorstop + // At this point, literally nothing but mscorlib is loaded, + // and since this class doesn't have any static fields that + // aren't defined in mscorlib, we can control exactly what + // gets loaded. + _ = args; + try + { + var arguments = Environment.GetCommandLineArgs(); + MaybeInitializeConsole(arguments); + + SetupLibraryLoading(); + + EnsureDirectories(); + + // this is weird, but it prevents Mono from having issues loading the type. + // IMPORTANT: NO CALLS TO ANY LOGGER CAN HAPPEN BEFORE THIS + var unused = StandardLogger.PrintFilter; + #region // Above hack explanation + /* + * Due to an unknown bug in the version of Mono that Unity uses, if the first access to StandardLogger + * is a call to a constructor, then Mono fails to load the type correctly. However, if the first access is to + * the above static property (or maybe any, but I don't really know) it behaves as expected and works fine. + */ + #endregion + + Default.Debug("Initializing logger"); + + SelfConfig.ReadCommandLine(arguments); + SelfConfig.Load(); + DisabledConfig.Load(); + + if (AntiPiracy.IsInvalid(Environment.CurrentDirectory)) + { + Default.Error("Invalid installation; please buy the game to run BSIPA."); + + return; + } + + CriticalSection.Configure(); + + Logging.Logger.Injector.Debug("Prepping bootstrapper"); + + // make sure to load the game version and check boundaries before installing the bootstrap, because that uses the game assemblies property + GameVersionEarly.Load(); + SelfConfig.Instance.CheckVersionBoundary(); + + // updates backup + InstallBootstrapPatch(); + + AntiMalwareEngine.Initialize(); + + Updates.InstallPendingUpdates(); + + Loader.LibLoader.SetupAssemblyFilenames(true); + + pluginAsyncLoadTask = PluginLoader.LoadTask(); + permissionFixTask = PermissionFix.FixPermissions(new DirectoryInfo(Environment.CurrentDirectory)); + } + catch (Exception e) + { + Console.WriteLine(e); + } + } + + private static void MaybeInitializeConsole(string[] arguments) + { + var i = 0; + while (i < arguments.Length) + { + if (arguments[i++] == "--verbose") + { + if (i == arguments.Length) + { + WinConsole.Initialize(WinConsole.AttachParent); + return; + } + + WinConsole.Initialize(int.TryParse(arguments[i], out int processId) ? processId : WinConsole.AttachParent); + return; + } + } + } + + private static void EnsureDirectories() + { + string path; + if (!Directory.Exists(path = Path.Combine(Environment.CurrentDirectory, "UserData"))) + _ = Directory.CreateDirectory(path); + if (!Directory.Exists(path = Path.Combine(Environment.CurrentDirectory, "Plugins"))) + _ = Directory.CreateDirectory(path); + } + + private static void SetupLibraryLoading() + { + if (loadingDone) return; + loadingDone = true; + Loader.LibLoader.Configure(); + } + + private static void InstallBootstrapPatch() + { + var sw = Stopwatch.StartNew(); + + var cAsmName = Assembly.GetExecutingAssembly().GetName(); + var managedPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!; + + var dataDir = new DirectoryInfo(managedPath).Parent!.Name; + var gameName = dataDir.Substring(0, dataDir.Length - 5); + + Logging.Logger.Injector.Debug("Finding backup"); + var backupPath = Path.Combine(Environment.CurrentDirectory, "IPA", "Backups", gameName); + var bkp = BackupManager.FindLatestBackup(backupPath); + if (bkp == null) + Logging.Logger.Injector.Warn("No backup found! Was BSIPA installed using the installer?"); + + // TODO: Investigate if this ever worked properly. + // this is a critical section because if you exit in here, assembly can die + using var critSec = CriticalSection.ExecuteSection(); + + var readerParameters = new ReaderParameters + { + ReadWrite = false, + InMemory = true, + ReadingMode = ReadingMode.Immediate + }; + + Logging.Logger.Injector.Debug("Ensuring patch on UnityEngine.CoreModule exists"); + + #region Insert patch into UnityEngine.CoreModule.dll + + var unityPath = Path.Combine(managedPath, "UnityEngine.CoreModule.dll"); + + using var unityAsmDef = AssemblyDefinition.ReadAssembly(unityPath, readerParameters); + var unityModDef = unityAsmDef.MainModule; + + bool modified = false; + foreach (var asmref in unityModDef.AssemblyReferences) + { + if (asmref.Name == cAsmName.Name) + { + if (asmref.Version != cAsmName.Version) + { + asmref.Version = cAsmName.Version; + modified = true; + } + } + } + + var application = unityModDef.GetType("UnityEngine", "Camera"); + + if (application == null) + { + Logging.Logger.Injector.Critical("UnityEngine.CoreModule doesn't have a definition for UnityEngine.Camera!" + + "Nothing to patch to get ourselves into the Unity run cycle!"); + goto endPatchCoreModule; + } + + MethodDefinition? cctor = null; + foreach (var m in application.Methods) + if (m.IsRuntimeSpecialName && m.Name == ".cctor") + cctor = m; + + var cbs = unityModDef.ImportReference(((Action)CreateBootstrapper).Method); + + if (cctor == null) + { + cctor = new MethodDefinition(".cctor", + MethodAttributes.RTSpecialName | MethodAttributes.Static | MethodAttributes.SpecialName, + unityModDef.TypeSystem.Void); + application.Methods.Add(cctor); + modified = true; + + var ilp = cctor.Body.GetILProcessor(); + ilp.Emit(OpCodes.Call, cbs); + ilp.Emit(OpCodes.Ret); + } + else + { + var ilp = cctor.Body.GetILProcessor(); + for (var i = 0; i < Math.Min(2, cctor.Body.Instructions.Count); i++) + { + var ins = cctor.Body.Instructions[i]; + switch (i) + { + case 0 when ins.OpCode != OpCodes.Call: + ilp.Replace(ins, ilp.Create(OpCodes.Call, cbs)); + modified = true; + break; + + case 0: + { + var methodRef = ins.Operand as MethodReference; + if (methodRef?.FullName != cbs.FullName) + { + ilp.Replace(ins, ilp.Create(OpCodes.Call, cbs)); + modified = true; + } + + break; + } + case 1 when ins.OpCode != OpCodes.Ret: + ilp.Replace(ins, ilp.Create(OpCodes.Ret)); + modified = true; + break; + } + } + } + + if (modified) + { + string tempFilePath = Path.GetTempFileName(); + bkp?.Add(unityPath); + unityAsmDef.Write(tempFilePath); + File.Delete(unityPath); + File.Move(tempFilePath, unityPath); + } + endPatchCoreModule: + #endregion Insert patch into UnityEngine.CoreModule.dll + +#if BeatSaber + Logging.Logger.Injector.Debug("Ensuring anti-yeet patch exists"); + + var name = SelfConfig.GameAssemblies_.FirstOrDefault() ?? SelfConfig.GetDefaultGameAssemblies().First(); + var ascPath = Path.Combine(managedPath, name); + + try + { + using var ascAsmDef = AssemblyDefinition.ReadAssembly(ascPath, readerParameters); + var ascModDef = ascAsmDef.MainModule; + + var deleter = ascModDef.GetType("IPAPluginsDirDeleter"); + + if (deleter.Methods.Count > 0) + { + deleter.Methods.Clear(); // delete all methods + + string tempFilePath = Path.GetTempFileName(); + bkp?.Add(ascPath); + ascAsmDef.Write(tempFilePath); + File.Delete(ascPath); + File.Move(tempFilePath, ascPath); + } + } + catch (Exception e) + { + Logging.Logger.Injector.Warn($"Could not apply anti-yeet patch to {ascPath}"); + if (SelfConfig.Debug_.ShowHandledErrorStackTraces_) + Logging.Logger.Injector.Warn(e); + } +#endif + + sw.Stop(); + Logging.Logger.Injector.Info($"Installing bootstrapper took {sw.Elapsed}"); + } + + private static bool bootstrapped; + + private static void CreateBootstrapper() + { + if (bootstrapped) return; + bootstrapped = true; + + Application.logMessageReceivedThreaded += delegate (string condition, string stackTrace, LogType type) + { + var level = UnityLogRedirector.LogTypeToLevel(type); + UnityLogProvider.UnityLogger.Log(level, $"{condition}"); + UnityLogProvider.UnityLogger.Log(level, $"{stackTrace}"); + }; + + StdoutInterceptor.EnsureHarmonyLogging(); + + // need to reinit streams singe Unity seems to redirect stdout + StdoutInterceptor.RedirectConsole(); + + var bootstrapper = new GameObject("NonDestructiveBootstrapper").AddComponent(); + bootstrapper.Destroyed += Bootstrapper_Destroyed; + } + + private static bool loadingDone; + + private static void Bootstrapper_Destroyed() + { + // wait for plugins to finish loading + pluginAsyncLoadTask?.Wait(); + permissionFixTask?.Wait(); + + Default.Debug("Plugins loaded"); + Default.Debug(string.Join(", ", PluginLoader.PluginsMetadata.StrJP())); + _ = PluginComponent.Create(); + } + } +} diff --git a/IPA.Loader/Config/Providers/JsonConfigProvider.cs b/IPA.Loader/Config/Providers/JsonConfigProvider.cs index dd9c8dfb..ef324fd4 100644 --- a/IPA.Loader/Config/Providers/JsonConfigProvider.cs +++ b/IPA.Loader/Config/Providers/JsonConfigProvider.cs @@ -1,15 +1,13 @@ -using IPA.Config.Data; +#nullable enable +using IPA.Config.Data; using IPA.Logging; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using System; using System.Linq; -using System.Collections.Specialized; -using System.ComponentModel; using System.IO; using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Nodes; using Boolean = IPA.Config.Data.Boolean; -using System.Threading.Tasks; namespace IPA.Config.Providers { @@ -22,20 +20,14 @@ namespace IPA.Config.Providers public string Extension => "json"; - public Value Load(FileInfo file) + public Value? Load(FileInfo file) { if (!file.Exists) return Value.Null(); try { - JToken jtok; - using (var sreader = new StreamReader(file.OpenRead())) - { - using var jreader = new JsonTextReader(sreader); - jtok = JToken.ReadFrom(jreader); - } - - return VisitToValue(jtok); + using var fileStream = file.OpenRead(); + return VisitToValue(JsonNode.Parse(fileStream)); } catch (Exception e) { @@ -45,73 +37,46 @@ namespace IPA.Config.Providers } } - private Value VisitToValue(JToken tok) + private Value? VisitToValue(JsonNode? node) { - if (tok == null) return Value.Null(); + if (node == null) return Value.Null(); - switch (tok.Type) + switch (node.GetValueKind()) { - case JTokenType.Raw: // idk if the parser will normally emit a Raw type, but just to be safe - return VisitToValue(JToken.Parse((tok as JRaw).Value as string)); - case JTokenType.Undefined: - Logger.Config.Warn("Found JTokenType.Undefined"); - goto case JTokenType.Null; - case JTokenType.Bytes: // never used by Newtonsoft - Logger.Config.Warn("Found JTokenType.Bytes"); - goto case JTokenType.Null; - case JTokenType.Comment: // never used by Newtonsoft - Logger.Config.Warn("Found JTokenType.Comment"); - goto case JTokenType.Null; - case JTokenType.Constructor: // never used by Newtonsoft - Logger.Config.Warn("Found JTokenType.Constructor"); - goto case JTokenType.Null; - case JTokenType.Property: // never used by Newtonsoft - Logger.Config.Warn("Found JTokenType.Property"); - goto case JTokenType.Null; - case JTokenType.Null: + case JsonValueKind.Undefined: + Logger.Config.Warn($"Found {nameof(JsonValueKind)}.{nameof(JsonValueKind.Undefined)}"); + goto case JsonValueKind.Null; + case JsonValueKind.Null: return Value.Null(); - case JTokenType.Boolean: - return Value.Bool(((tok as JValue).Value as bool?) ?? false); - case JTokenType.String: - var val = (tok as JValue).Value; - if (val is string s) return Value.Text(s); - else if (val is char c) return Value.Text("" + c); - else return Value.Text(string.Empty); - case JTokenType.Integer: - val = (tok as JValue).Value; - if (val is long l) return Value.Integer(l); - else if (val is ulong u) return Value.Integer((long)u); - else return Value.Integer(0); - case JTokenType.Float: - val = (tok as JValue).Value; - if (val is decimal dec) return Value.Float(dec); - else if (val is double dou) return Value.Float((decimal)dou); - else if (val is float flo) return Value.Float((decimal)flo); - else return Value.Float(0); // default to 0 if something breaks - case JTokenType.Date: - val = (tok as JValue).Value; - if (val is DateTime dt) return Value.Text(dt.ToString()); - else if (val is DateTimeOffset dto) return Value.Text(dto.ToString()); - else return Value.Text("Unknown Date-type token"); - case JTokenType.TimeSpan: - val = (tok as JValue).Value; - if (val is TimeSpan ts) return Value.Text(ts.ToString()); - else return Value.Text("Unknown TimeSpan-type token"); - case JTokenType.Guid: - val = (tok as JValue).Value; - if (val is Guid g) return Value.Text(g.ToString()); - else return Value.Text("Unknown Guid-type token"); - case JTokenType.Uri: - val = (tok as JValue).Value; - if (val is Uri ur) return Value.Text(ur.ToString()); - else return Value.Text("Unknown Uri-type token"); - case JTokenType.Array: - return Value.From((tok as JArray).Select(VisitToValue)); - case JTokenType.Object: - return Value.From((tok as IEnumerable>) - .Select(kvp => new KeyValuePair(kvp.Key, VisitToValue(kvp.Value)))); + case JsonValueKind.True: + case JsonValueKind.False: + if (node.AsValue().TryGetValue(out var b)) + return Value.Bool(b); + return Value.Bool(false); + case JsonValueKind.String: + if (node.AsValue().TryGetValue(out var s)) + return Value.Text(s); + return Value.Text(string.Empty); + case JsonValueKind.Number: + var value = node.AsValue(); + if (value.TryGetValue(out var l)) + return Value.Integer(l); + if (value.TryGetValue(out var u)) + return Value.Integer((long)u); + if (value.TryGetValue(out var dec)) + return Value.Float(dec); + if (value.TryGetValue(out var dou)) + return Value.Float((decimal)dou); + if (value.TryGetValue(out var flo)) + return Value.Float((decimal)flo); + return Value.Float(0); // default to 0 if something breaks + case JsonValueKind.Array: + return Value.From(node.AsArray().Select(VisitToValue)); + case JsonValueKind.Object: + return Value.From(node.AsObject() + .Select(kvp => new KeyValuePair(kvp.Key, VisitToValue(kvp.Value)))); default: - throw new ArgumentException($"Unknown {nameof(JTokenType)} in parameter"); + throw new ArgumentException($"Unknown {nameof(JsonValueKind)} in parameter"); } } @@ -122,14 +87,18 @@ namespace IPA.Config.Providers try { - var tok = VisitToToken(value); + var jsonNode = VisitToNode(value); + using var fileStream = file.Open(FileMode.Create, FileAccess.Write); + using var jsonWriter = new Utf8JsonWriter(fileStream, new JsonWriterOptions { Indented = true }); - using var swriter = new StreamWriter(file.Open(FileMode.Create, FileAccess.Write)); - using var jwriter = new JsonTextWriter(swriter) + if (jsonNode == null) { - Formatting = Formatting.Indented - }; - tok.WriteTo(jwriter); + jsonWriter.WriteNullValue(); + } + else + { + jsonNode.WriteTo(jsonWriter); + } } catch (Exception e) { @@ -138,28 +107,28 @@ namespace IPA.Config.Providers } } - private JToken VisitToToken(Value val) - { + private JsonNode? VisitToNode(Value? val) + { switch (val) { case Text t: - return new JValue(t.Value); + return JsonValue.Create(t.Value); case Boolean b: - return new JValue(b.Value); + return JsonValue.Create(b.Value); case Integer i: - return new JValue(i.Value); + return JsonValue.Create(i.Value); case FloatingPoint f: - return new JValue(f.Value); + return JsonValue.Create(f.Value); case List l: - var jarr = new JArray(); - foreach (var tok in l.Select(VisitToToken)) jarr.Add(tok); + var jarr = new JsonArray(); + foreach (var tok in l.Select(VisitToNode)) jarr.Add(tok); return jarr; case Map m: - var jobj = new JObject(); - foreach (var kvp in m) jobj.Add(kvp.Key, VisitToToken(kvp.Value)); + var jobj = new JsonObject(); + foreach (var kvp in m) jobj.Add(kvp.Key, VisitToNode(kvp.Value)); return jobj; case null: - return JValue.CreateNull(); + return null; default: throw new ArgumentException($"Unsupported subtype of {nameof(Value)}"); } diff --git a/IPA.Loader/Config/SelfConfig.cs b/IPA.Loader/Config/SelfConfig.cs index af9ce2b4..a1c305f0 100644 --- a/IPA.Loader/Config/SelfConfig.cs +++ b/IPA.Loader/Config/SelfConfig.cs @@ -5,8 +5,8 @@ using IPA.Config.Stores; using IPA.Config.Stores.Attributes; using IPA.Config.Stores.Converters; // END: section ignore -using Newtonsoft.Json; using System.Collections.Generic; +using System.Text.Json.Serialization; namespace IPA.Config { diff --git a/IPA.Loader/IPA.Loader.csproj b/IPA.Loader/IPA.Loader.csproj index b8758a1e..d598a538 100644 --- a/IPA.Loader/IPA.Loader.csproj +++ b/IPA.Loader/IPA.Loader.csproj @@ -48,17 +48,11 @@ - - - - - - + + + diff --git a/IPA.Loader/JsonConverters/AlmostVersionConverter.cs b/IPA.Loader/JsonConverters/AlmostVersionConverter.cs index f2820705..93079ccc 100644 --- a/IPA.Loader/JsonConverters/AlmostVersionConverter.cs +++ b/IPA.Loader/JsonConverters/AlmostVersionConverter.cs @@ -1,22 +1,19 @@ using IPA.Utilities; -using Newtonsoft.Json; using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Text.Json; +using System.Text.Json.Serialization; namespace IPA.JsonConverters { internal class AlmostVersionConverter : JsonConverter { - public override AlmostVersion ReadJson(JsonReader reader, Type objectType, AlmostVersion existingValue, bool hasExistingValue, JsonSerializer serializer) => - reader.Value == null ? null : new AlmostVersion(reader.Value as string); + public override AlmostVersion Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => + reader.TokenType == JsonTokenType.Null ? null : new AlmostVersion(reader.GetString()); - public override void WriteJson(JsonWriter writer, AlmostVersion value, JsonSerializer serializer) + public override void Write(Utf8JsonWriter writer, AlmostVersion value, JsonSerializerOptions options) { - if (value == null) writer.WriteNull(); - else writer.WriteValue(value.ToString()); + if (value == null) writer.WriteNullValue(); + else writer.WriteStringValue(value.ToString()); } } } diff --git a/IPA.Loader/JsonConverters/FeaturesFieldConverter.cs b/IPA.Loader/JsonConverters/FeaturesFieldConverter.cs index d17c5781..efcc903e 100644 --- a/IPA.Loader/JsonConverters/FeaturesFieldConverter.cs +++ b/IPA.Loader/JsonConverters/FeaturesFieldConverter.cs @@ -1,14 +1,15 @@ using IPA.Logging; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; namespace IPA.JsonConverters { - internal class FeaturesFieldConverter : JsonConverter>> + internal class FeaturesFieldConverter : JsonConverter>> { [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void Assert([DoesNotReturnIf(false)] bool condition) @@ -17,26 +18,28 @@ namespace IPA.JsonConverters throw new InvalidOperationException(); } - public override Dictionary> ReadJson(JsonReader reader, Type objectType, Dictionary> existingValue, bool hasExistingValue, JsonSerializer serializer) + public override Dictionary> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - if (reader.TokenType == JsonToken.StartArray) + if (reader.TokenType == JsonTokenType.StartArray) { - _ = serializer.Deserialize(reader); + // TODO: Why? + _ = JsonSerializer.Deserialize(ref reader, options); Logger.Features.Warn("Encountered old features used. They no longer do anything, please move to the new format."); - return existingValue; + // TODO: Is there an alternative to existingValue? + return null; } - var dict = new Dictionary>(); - Assert(reader.TokenType == JsonToken.StartObject && reader.Read()); + var dict = new Dictionary>(); + Assert(reader.TokenType == JsonTokenType.StartObject && reader.Read()); - while (reader.TokenType == JsonToken.PropertyName) + while (reader.TokenType == JsonTokenType.PropertyName) { - var name = (string)reader.Value; + var name = reader.GetString(); Assert(reader.Read()); - var list = reader.TokenType == JsonToken.StartObject - ? (new() { serializer.Deserialize(reader) }) - : serializer.Deserialize>(reader); + var list = reader.TokenType == JsonTokenType.StartObject + ? (new() { JsonSerializer.Deserialize(ref reader, options) }) + : JsonSerializer.Deserialize>(ref reader, options); dict.Add(name, list); Assert(reader.Read()); @@ -45,9 +48,9 @@ namespace IPA.JsonConverters return dict; } - public override void WriteJson(JsonWriter writer, Dictionary> value, JsonSerializer serializer) + public override void Write(Utf8JsonWriter writer, Dictionary> value, JsonSerializerOptions options) { - serializer.Serialize(writer, value); + JsonSerializer.Serialize(writer, value, options); } } } diff --git a/IPA.Loader/JsonConverters/MultilineStringConverter.cs b/IPA.Loader/JsonConverters/MultilineStringConverter.cs index 38ef93e4..72a8caa2 100644 --- a/IPA.Loader/JsonConverters/MultilineStringConverter.cs +++ b/IPA.Loader/JsonConverters/MultilineStringConverter.cs @@ -1,32 +1,29 @@ -using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System; +using System.Text.Json; +using System.Text.Json.Serialization; namespace IPA.JsonConverters { internal class MultilineStringConverter : JsonConverter { - public override string ReadJson(JsonReader reader, Type objectType, string existingValue, bool hasExistingValue, JsonSerializer serializer) + public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - if (reader.TokenType == JsonToken.StartArray) + if (reader.TokenType == JsonTokenType.StartArray) { - var list = serializer.Deserialize(reader); + var list = JsonSerializer.Deserialize(ref reader, options); return string.Join("\n", list); } - else - return reader.Value as string; + + return reader.GetString(); } - public override void WriteJson(JsonWriter writer, string value, JsonSerializer serializer) + public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) { var list = value.Split('\n'); if (list.Length == 1) - serializer.Serialize(writer, value); + writer.WriteStringValue(value); else - serializer.Serialize(writer, list); + JsonSerializer.Serialize(writer, list, options); } } } diff --git a/IPA.Loader/JsonConverters/SemverRangeConverter.cs b/IPA.Loader/JsonConverters/SemverRangeConverter.cs index 71e84983..0adea41e 100644 --- a/IPA.Loader/JsonConverters/SemverRangeConverter.cs +++ b/IPA.Loader/JsonConverters/SemverRangeConverter.cs @@ -1,20 +1,20 @@ #nullable enable using System; -using System.Runtime.Remoting.Messaging; using Hive.Versioning; -using Newtonsoft.Json; +using System.Text.Json; +using System.Text.Json.Serialization; namespace IPA.JsonConverters { internal class SemverRangeConverter : JsonConverter { - public override VersionRange? ReadJson(JsonReader reader, Type objectType, VersionRange? existingValue, bool hasExistingValue, JsonSerializer serializer) - => reader.Value is not string s ? existingValue : new VersionRange(s); + public override VersionRange? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => reader.TokenType is not JsonTokenType.String ? null : new VersionRange(reader.GetString()!); - public override void WriteJson(JsonWriter writer, VersionRange? value, JsonSerializer serializer) + public override void Write(Utf8JsonWriter writer, VersionRange? value, JsonSerializerOptions options) { - if (value is null) writer.WriteNull(); - else writer.WriteValue(value.ToString()); + if (value is null) writer.WriteNullValue(); + else writer.WriteStringValue(value.ToString()); } } } diff --git a/IPA.Loader/JsonConverters/SemverVersionConverter.cs b/IPA.Loader/JsonConverters/SemverVersionConverter.cs index 40683f03..49abe09d 100644 --- a/IPA.Loader/JsonConverters/SemverVersionConverter.cs +++ b/IPA.Loader/JsonConverters/SemverVersionConverter.cs @@ -1,19 +1,20 @@ #nullable enable using System; -using Newtonsoft.Json; +using System.Text.Json; +using System.Text.Json.Serialization; using Version = Hive.Versioning.Version; namespace IPA.JsonConverters { internal class SemverVersionConverter : JsonConverter { - public override Version? ReadJson(JsonReader reader, Type objectType, Version? existingValue, bool hasExistingValue, JsonSerializer serializer) - => reader.Value is not string s ? existingValue : new Version(s); + public override Version? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => reader.TokenType is not JsonTokenType.String ? null : new Version(reader.GetString()!); - public override void WriteJson(JsonWriter writer, Version? value, JsonSerializer serializer) + public override void Write(Utf8JsonWriter writer, Version? value, JsonSerializerOptions options) { - if (value == null) writer.WriteNull(); - else writer.WriteValue(value.ToString()); + if (value is null) writer.WriteNullValue(); + else writer.WriteStringValue(value.ToString()); } } } diff --git a/IPA.Loader/Loader/Features/ConfigProviderFeature.cs b/IPA.Loader/Loader/Features/ConfigProviderFeature.cs index cd8e9cce..e92c23d5 100644 --- a/IPA.Loader/Loader/Features/ConfigProviderFeature.cs +++ b/IPA.Loader/Loader/Features/ConfigProviderFeature.cs @@ -1,8 +1,9 @@ #nullable enable -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using System; using System.IO; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; namespace IPA.Loader.Features { @@ -10,16 +11,17 @@ namespace IPA.Loader.Features { private class DataModel { - [JsonProperty("type", Required = Required.Always)] - public string TypeName = ""; + [JsonPropertyName("type")] + [JsonRequired] + public string TypeName { get; init; } = ""; } - protected override bool Initialize(PluginMetadata meta, JObject featureData) + protected override bool Initialize(PluginMetadata meta, JsonObject featureData) { DataModel data; try { - data = featureData.ToObject() ?? throw new InvalidOperationException("Feature data is null"); + data = featureData.Deserialize() ?? throw new InvalidOperationException("Feature data is null"); } catch (Exception e) { diff --git a/IPA.Loader/Loader/Features/DefineFeature.cs b/IPA.Loader/Loader/Features/DefineFeature.cs index f4d628f6..50b89d29 100644 --- a/IPA.Loader/Loader/Features/DefineFeature.cs +++ b/IPA.Loader/Loader/Features/DefineFeature.cs @@ -1,9 +1,10 @@ #nullable enable using IPA.Logging; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using System; using System.IO; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; namespace IPA.Loader.Features { @@ -13,23 +14,26 @@ namespace IPA.Loader.Features private class DataModel { - [JsonProperty("type", Required = Required.Always)] - public string TypeName = ""; - [JsonProperty("name", Required = Required.DisallowNull)] - public string? ActualName = null; + [JsonPropertyName("type")] + [JsonRequired] + public string TypeName { get; init; } = ""; + + [JsonPropertyName("name")] + // TODO: Originally DisallowNull + public string? ActualName { get; init; } public string Name => ActualName ?? TypeName; } private DataModel data = null!; - protected override bool Initialize(PluginMetadata meta, JObject featureData) + protected override bool Initialize(PluginMetadata meta, JsonObject featureData) { Logger.Features.Debug("Executing DefineFeature Init"); try { - data = featureData.ToObject() ?? throw new InvalidOperationException("Feature data is null"); + data = featureData.Deserialize() ?? throw new InvalidOperationException("Feature data is null"); } catch (Exception e) { diff --git a/IPA.Loader/Loader/Features/Feature.cs b/IPA.Loader/Loader/Features/Feature.cs index 80c84c84..1a6f4333 100644 --- a/IPA.Loader/Loader/Features/Feature.cs +++ b/IPA.Loader/Loader/Features/Feature.cs @@ -1,8 +1,7 @@ #nullable enable -using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Nodes; #if NET3 using Net3_Proxy; #endif @@ -27,7 +26,7 @@ namespace IPA.Loader.Features /// the metadata of the plugin that is being prepared /// the data provided with the feature /// if the feature is valid for the plugin, otherwise - protected abstract bool Initialize(PluginMetadata meta, JObject featureData); + protected abstract bool Initialize(PluginMetadata meta, JsonObject featureData); /// /// The message to be logged when the feature is not valid for a plugin. @@ -116,7 +115,7 @@ namespace IPA.Loader.Features private class EmptyFeature : Feature { - protected override bool Initialize(PluginMetadata meta, JObject featureData) + protected override bool Initialize(PluginMetadata meta, JsonObject featureData) { throw new NotImplementedException(); } @@ -128,9 +127,9 @@ namespace IPA.Loader.Features { public readonly PluginMetadata AppliedTo; public readonly string Name; - public readonly JObject Data; + public readonly JsonObject Data; - public Instance(PluginMetadata appliedTo, string name, JObject data) + public Instance(PluginMetadata appliedTo, string name, JsonObject data) { AppliedTo = appliedTo; Name = name; diff --git a/IPA.Loader/Loader/LibLoader.cs b/IPA.Loader/Loader/LibLoader.cs index 337eb5e6..28b29823 100644 --- a/IPA.Loader/Loader/LibLoader.cs +++ b/IPA.Loader/Loader/LibLoader.cs @@ -1,237 +1,234 @@ -#nullable enable -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Reflection; -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); - } - - static void AddDir(string path) - { - var pathEnvironmentVariable = Environment.GetEnvironmentVariable("Path"); - Environment.SetEnvironmentVariable("Path", path + Path.PathSeparator + pathEnvironmentVariable); - } - - 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")); - - // TODO: find a way to either safely remove Newtonsoft, or switch to a different JSON lib - _ = LoadLibrary(new AssemblyName("Newtonsoft.Json, Version=12.0.0.0, Culture=neutral")); - } - } - - 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; - } - } - } - } -} +#nullable enable +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Reflection; +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); + } + + static void AddDir(string path) + { + var pathEnvironmentVariable = Environment.GetEnvironmentVariable("Path"); + Environment.SetEnvironmentVariable("Path", path + Path.PathSeparator + pathEnvironmentVariable); + } + + 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")); + } + } + + 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; + } + } + } + } +} diff --git a/IPA.Loader/Loader/PluginLoader.cs b/IPA.Loader/Loader/PluginLoader.cs index f20d428d..18e30085 100644 --- a/IPA.Loader/Loader/PluginLoader.cs +++ b/IPA.Loader/Loader/PluginLoader.cs @@ -4,7 +4,6 @@ using IPA.Loader.Features; using IPA.Logging; using IPA.Utilities; using Mono.Cecil; -using Newtonsoft.Json; using System; using System.Collections.Generic; using System.IO; @@ -15,6 +14,8 @@ using System.Diagnostics.CodeAnalysis; using System.Diagnostics; using IPA.AntiMalware; using Hive.Versioning; +using IPA.JsonConverters; +using System.Text.Json; #if NET4 using Task = System.Threading.Tasks.Task; using TaskEx = System.Threading.Tasks.Task; @@ -37,13 +38,13 @@ namespace IPA.Loader internal static PluginMetadata SelfMeta = null!; internal static Task LoadTask() => - TaskEx.Run(() => + TaskEx.Run(async () => { YeetIfNeeded(); var sw = Stopwatch.StartNew(); - LoadMetadata(); + await LoadMetadata(); sw.Stop(); Logger.Loader.Info($"Loading metadata took {sw.Elapsed}"); @@ -83,10 +84,15 @@ namespace IPA.Loader private static readonly Regex embeddedTextDescriptionPattern = new(@"#!\[(.+)\]", RegexOptions.Compiled | RegexOptions.Singleline); - internal static void LoadMetadata() + private static async Task LoadMetadata() { string[] plugins = Directory.GetFiles(UnityGame.PluginsPath, "*.dll"); + var options = new JsonSerializerOptions + { + Converters = { new SemverRangeConverter(), new FeaturesFieldConverter() } + }; + try { var selfMeta = new PluginMetadata @@ -97,14 +103,10 @@ namespace IPA.Loader IsSelf = true }; - string manifest; - using (var manifestReader = - new StreamReader( - selfMeta.Assembly.GetManifestResourceStream(typeof(PluginLoader), "manifest.json") ?? - throw new InvalidOperationException())) - manifest = manifestReader.ReadToEnd(); + using var manifestStream = selfMeta.Assembly.GetManifestResourceStream(typeof(PluginLoader), "manifest.json") ?? + throw new InvalidOperationException(); - var manifestObj = JsonConvert.DeserializeObject(manifest); + var manifestObj = await JsonSerializer.DeserializeAsync(manifestStream, options).ConfigureAwait(false); selfMeta.Manifest = manifestObj ?? throw new InvalidOperationException("Deserialized manifest was null"); PluginsMetadata.Add(selfMeta); @@ -159,11 +161,9 @@ namespace IPA.Loader pluginNs = embedded.Name.Substring(0, embedded.Name.Length - manifestSuffix.Length); - string manifest; - using (var manifestReader = new StreamReader(embedded.GetResourceStream())) - manifest = manifestReader.ReadToEnd(); + using var manifestStream = embedded.GetResourceStream(); - pluginManifest = JsonConvert.DeserializeObject(manifest); + pluginManifest = await JsonSerializer.DeserializeAsync(manifestStream, options).ConfigureAwait(false); break; } @@ -277,7 +277,8 @@ namespace IPA.Loader IsBare = true, }; - var manifestObj = JsonConvert.DeserializeObject(File.ReadAllText(manifest)); + using var manifestStream = File.OpenRead(manifest); + var manifestObj = await JsonSerializer.DeserializeAsync(manifestStream, options).ConfigureAwait(false); if (manifestObj is null) { Logger.Loader.Error($"Bare manifest {Path.GetFileName(manifest)} deserialized to null"); @@ -329,12 +330,12 @@ namespace IPA.Loader } using var reader = new StreamReader(resc.GetResourceStream()); - description = reader.ReadToEnd(); + description = await reader.ReadToEndAsync().ConfigureAwait(false); } else { using var descriptionReader = new StreamReader(meta.Assembly.GetManifestResourceStream(name)); - description = descriptionReader.ReadToEnd(); + description = await descriptionReader.ReadToEndAsync().ConfigureAwait(false); } meta.Manifest.Description = description; @@ -809,7 +810,8 @@ namespace IPA.Loader internal static void InitFeatures() { - foreach (var meta in PluginsMetadata) + // TODO: Find out why this is null. + foreach (var meta in PluginsMetadata.Where(m => m.Manifest.Features is not null)) { foreach (var feature in meta.Manifest.Features .SelectMany(f => f.Value.Select(o => (f.Key, o))) diff --git a/IPA.Loader/Loader/PluginManifest.cs b/IPA.Loader/Loader/PluginManifest.cs index 96b6933d..ef61d8a6 100644 --- a/IPA.Loader/Loader/PluginManifest.cs +++ b/IPA.Loader/Loader/PluginManifest.cs @@ -2,11 +2,10 @@ using Hive.Versioning; using IPA.JsonConverters; using IPA.Utilities; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using SemVer; using System; using System.Collections.Generic; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; using AlmostVersionConverter = IPA.JsonConverters.AlmostVersionConverter; using Version = Hive.Versioning.Version; #if NET3 @@ -18,69 +17,91 @@ namespace IPA.Loader { internal class PluginManifest { - [JsonProperty("name", Required = Required.Always)] - public string Name = null!; + [JsonPropertyName("name")] + [JsonRequired] + public string Name { get; init; } = null!; - [JsonProperty("id", Required = Required.AllowNull)] // TODO: on major version bump, make this always - public string? Id; + [JsonPropertyName("id")] + [JsonRequired] // TODO: Originally AllowNull + public string? Id { get; set; } - [JsonProperty("description", Required = Required.Always), JsonConverter(typeof(MultilineStringConverter))] - public string Description = null!; + [JsonPropertyName("description")] + [JsonRequired] + [JsonConverter(typeof(MultilineStringConverter))] + public string Description { get; set; } = null!; - [JsonProperty("version", Required = Required.Always), JsonConverter(typeof(SemverVersionConverter))] - public Version Version = null!; + [JsonPropertyName("version")] + [JsonRequired] + [JsonConverter(typeof(SemverVersionConverter))] + public Version Version { get; init; } = null!; - [JsonProperty("gameVersion", Required = Required.DisallowNull), JsonConverter(typeof(AlmostVersionConverter))] - public AlmostVersion? GameVersion; + [JsonPropertyName("gameVersion")] + [JsonRequired] // TODO: Originally DisallowNull + [JsonConverter(typeof(AlmostVersionConverter))] + public AlmostVersion? GameVersion { get; init; } - [JsonProperty("author", Required = Required.Always)] - public string Author = null!; + [JsonPropertyName("author")] + [JsonRequired] + public string Author { get; init; } = null!; - [JsonProperty("dependsOn", Required = Required.DisallowNull, ItemConverterType = typeof(SemverRangeConverter))] - public Dictionary Dependencies = new(); + [JsonPropertyName("dependsOn")] + [JsonRequired] // TODO: Originally DisallowNull + public Dictionary Dependencies { get; init; } = new(); - [JsonProperty("conflictsWith", Required = Required.DisallowNull, ItemConverterType = typeof(SemverRangeConverter))] - public Dictionary Conflicts = new(); + [JsonPropertyName("conflictsWith")] + // TODO: Originally DisallowNull + public Dictionary Conflicts { get; init; } = new(); - [JsonProperty("features", Required = Required.DisallowNull), JsonConverter(typeof(FeaturesFieldConverter))] - public Dictionary> Features = new(); + [JsonPropertyName("features")] + // TODO: Originally DisallowNull + public Dictionary> Features { get; init; } = new(); - [JsonProperty("loadBefore", Required = Required.DisallowNull)] - public string[] LoadBefore = Array.Empty(); + [JsonPropertyName("loadBefore")] + // TODO: Originally DisallowNull + public string[] LoadBefore { get; init; } = Array.Empty(); - [JsonProperty("loadAfter", Required = Required.DisallowNull)] - public string[] LoadAfter = Array.Empty(); + [JsonPropertyName("loadAfter")] + // TODO: Originally DisallowNull + public string[] LoadAfter { get; init; } = Array.Empty(); - [JsonProperty("icon", Required = Required.DisallowNull)] - public string? IconPath = null; + [JsonPropertyName("icon")] + // TODO: Originally DisallowNull + public string? IconPath { get; init; } - [JsonProperty("files", Required = Required.DisallowNull)] - public string[] Files = Array.Empty(); + [JsonPropertyName("files")] + // TODO: Originally DisallowNull + public string[] Files { get; init; } = Array.Empty(); [Serializable] public class LinksObject { - [JsonProperty("project-home", Required = Required.DisallowNull)] - public Uri? ProjectHome = null; + [JsonPropertyName("project-home")] + // TODO: Originally DisallowNull + public Uri? ProjectHome { get; init; } - [JsonProperty("project-source", Required = Required.DisallowNull)] - public Uri? ProjectSource = null; + [JsonPropertyName("project-source")] + // TODO: Originally DisallowNull + public Uri? ProjectSource { get; init; } - [JsonProperty("donate", Required = Required.DisallowNull)] - public Uri? Donate = null; + [JsonPropertyName("donate")] + // TODO: Originally DisallowNull + public Uri? Donate { get; init; } } - [JsonProperty("links", Required = Required.DisallowNull)] - public LinksObject? Links = null; + [JsonPropertyName("links")] + // TODO: Originally DisallowNull + public LinksObject? Links { get; init; } [Serializable] public class MiscObject { - [JsonProperty("plugin-hint", Required = Required.DisallowNull)] - public string? PluginMainHint = null; + [JsonPropertyName("plugin-hint")] + // TODO: Originally DisallowNull + public string? PluginMainHint { get; init; } } - [JsonProperty("misc", Required = Required.DisallowNull)] - public MiscObject? Misc = null; + [JsonPropertyName("misc")] + // TODO: Originally DisallowNull + public MiscObject? Misc { get; init; } } } \ No newline at end of file diff --git a/IPA.Loader/Updating/BeatMods/ApiEndpoint.cs b/IPA.Loader/Updating/BeatMods/ApiEndpoint.cs index e8a5a690..7d86991c 100644 --- a/IPA.Loader/Updating/BeatMods/ApiEndpoint.cs +++ b/IPA.Loader/Updating/BeatMods/ApiEndpoint.cs @@ -3,8 +3,6 @@ using System.Collections.Generic; using System.Linq; using IPA.JsonConverters; using IPA.Utilities; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using SemVer; using Version = SemVer.Version; @@ -133,7 +131,7 @@ namespace IPA.Updating.BeatMods [JsonProperty("link")] public Uri Link; - + #pragma warning restore CS0649 [Serializable] diff --git a/IPA.Loader/Updating/BeatMods/Updater.cs b/IPA.Loader/Updating/BeatMods/Updater.cs index 33b4596c..2ed89e35 100644 --- a/IPA.Loader/Updating/BeatMods/Updater.cs +++ b/IPA.Loader/Updating/BeatMods/Updater.cs @@ -15,7 +15,6 @@ using IPA.Loader; using IPA.Loader.Features; using IPA.Utilities; using IPA.Utilities.Async; -using Newtonsoft.Json; using SemVer; using UnityEngine; using UnityEngine.Networking; @@ -268,7 +267,7 @@ namespace IPA.Updating.BeatMods Logger.updater.Debug($"Phantom Dependency: {dep}"); yield return ResolveDependencyRanges(depList); - + foreach (var dep in depList.Value) Logger.updater.Debug($"Dependency: {dep}"); @@ -292,7 +291,7 @@ namespace IPA.Updating.BeatMods var dep = list.Value[i]; var mod = new Ref(null); - + yield return GetModInfo(dep.Name, "", mod); try { mod.Verify(); } @@ -381,12 +380,12 @@ namespace IPA.Updating.BeatMods .Select(mod => mod.Version).Max(); // (2.1) get the max version dep.Resolved = ver != null; if (dep.Resolved) dep.ResolvedVersion = ver; // (2.2) - dep.Has = dep.Resolved && dep.Version == dep.ResolvedVersion; + dep.Has = dep.Resolved && dep.Version == dep.ResolvedVersion; } } internal void CheckDependencies(Ref> list) - { + { var toDl = new List(); foreach (var dep in list.Value) @@ -432,8 +431,8 @@ namespace IPA.Updating.BeatMods /// internal delegate void InstallFailed(DependencyObject obj, Exception error); - internal void StartDownload(IEnumerable download, DownloadStart downloadStart = null, - DownloadProgress downloadProgress = null, DownloadFailed downloadFail = null, DownloadFinish downloadFinish = null, + internal void StartDownload(IEnumerable download, DownloadStart downloadStart = null, + DownloadProgress downloadProgress = null, DownloadFailed downloadFail = null, DownloadFinish downloadFinish = null, InstallFailed installFail = null, InstallFinish installFinish = null) { foreach (var item in download) @@ -457,7 +456,7 @@ namespace IPA.Updating.BeatMods yield break; } - var releaseName = UnityGame.ReleaseType == UnityGame.Release.Steam + var releaseName = UnityGame.ReleaseType == UnityGame.Release.Steam ? ApiEndpoint.Mod.DownloadsObject.TypeSteam : ApiEndpoint.Mod.DownloadsObject.TypeOculus; var platformFile = mod.Value.Downloads.First(f => f.Type == ApiEndpoint.Mod.DownloadsObject.TypeUniversal || f.Type == releaseName); @@ -658,7 +657,7 @@ namespace IPA.Updating.BeatMods FileInfo targetFile = new FileInfo(Path.Combine(targetDir, entry.FileName)); Directory.CreateDirectory(targetFile.DirectoryName ?? throw new InvalidOperationException()); - if (item.LocalPluginMeta != null && + if (item.LocalPluginMeta != null && Utils.GetRelativePath(targetFile.FullName, targetDir) == Utils.GetRelativePath(item.LocalPluginMeta?.File.FullName, UnityGame.InstallPath)) shouldDeleteOldFile = false; // overwriting old file, no need to delete @@ -676,7 +675,7 @@ namespace IPA.Updating.BeatMods } } } - + if (shouldDeleteOldFile && item.LocalPluginMeta != null) File.AppendAllLines(Path.Combine(targetDir, SpecialDeletionsFile), new[] { Utils.GetRelativePath(item.LocalPluginMeta?.File.FullName, UnityGame.InstallPath) }); } @@ -730,7 +729,7 @@ namespace IPA.Updating.BeatMods { } } - + [Serializable] internal class BeatmodsInterceptException : Exception {