@ -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 IPA.Utilities; | ||||
using Newtonsoft.Json; | |||||
using System; | 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 | namespace IPA.JsonConverters | ||||
{ | { | ||||
internal class AlmostVersionConverter : JsonConverter<AlmostVersion> | 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 | namespace IPA.JsonConverters | ||||
{ | { | ||||
internal class MultilineStringConverter : JsonConverter<string> | 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); | 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'); | var list = value.Split('\n'); | ||||
if (list.Length == 1) | if (list.Length == 1) | ||||
serializer.Serialize(writer, value); | |||||
writer.WriteStringValue(value); | |||||
else | else | ||||
serializer.Serialize(writer, list); | |||||
JsonSerializer.Serialize(writer, list, options); | |||||
} | } | ||||
} | } | ||||
} | } |
@ -1,20 +1,20 @@ | |||||
#nullable enable | #nullable enable | ||||
using System; | using System; | ||||
using System.Runtime.Remoting.Messaging; | |||||
using Hive.Versioning; | using Hive.Versioning; | ||||
using Newtonsoft.Json; | |||||
using System.Text.Json; | |||||
using System.Text.Json.Serialization; | |||||
namespace IPA.JsonConverters | namespace IPA.JsonConverters | ||||
{ | { | ||||
internal class SemverRangeConverter : JsonConverter<VersionRange?> | 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 | #nullable enable | ||||
using System; | using System; | ||||
using Newtonsoft.Json; | |||||
using System.Text.Json; | |||||
using System.Text.Json.Serialization; | |||||
using Version = Hive.Versioning.Version; | using Version = Hive.Versioning.Version; | ||||
namespace IPA.JsonConverters | namespace IPA.JsonConverters | ||||
{ | { | ||||
internal class SemverVersionConverter : JsonConverter<Version?> | 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; | |||||
} | |||||
} | |||||
} | |||||
} | |||||
} |