@ -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 | |||
{ | |||
/// <summary> | |||
/// The entry point type for BSIPA's Doorstop injector. | |||
/// </summary> | |||
// 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>(); | |||
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 | |||
{ | |||
/// <summary> | |||
/// The entry point type for BSIPA's Doorstop injector. | |||
/// </summary> | |||
// 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>(); | |||
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(); | |||
} | |||
} | |||
} |
@ -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<AlmostVersion> | |||
{ | |||
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()); | |||
} | |||
} | |||
} |
@ -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<string> | |||
{ | |||
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<string[]>(reader); | |||
var list = JsonSerializer.Deserialize<string[]>(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); | |||
} | |||
} | |||
} |
@ -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<VersionRange?> | |||
{ | |||
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()); | |||
} | |||
} | |||
} |
@ -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<Version?> | |||
{ | |||
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()); | |||
} | |||
} | |||
} |
@ -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<string, string> 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<string, string>(); | |||
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<FileInfo> TraverseTree(string root, Func<string, bool>? dirValidator = null) | |||
{ | |||
if (dirValidator == null) dirValidator = s => true; | |||
var dirs = new Stack<string>(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<string, string> 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<string, string>(); | |||
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<FileInfo> TraverseTree(string root, Func<string, bool>? dirValidator = null) | |||
{ | |||
if (dirValidator == null) dirValidator = s => true; | |||
var dirs = new Stack<string>(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; | |||
} | |||
} | |||
} | |||
} | |||
} |