Decompiled source of RunicVelocity v1.0.0

RunicVelocity.dll

Decompiled 9 hours ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using Microsoft.CodeAnalysis;
using Mono.Cecil;
using Mono.Collections.Generic;
using RunicVelocity.Contracts;
using RunicVelocity.Core;
using RunicVelocity.Integration;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: AssemblyTitle("Runic Velocity")]
[assembly: AssemblyDescription("Change-aware bounded startup profiling and plugin manifest caching")]
[assembly: AssemblyCompany("Chazman")]
[assembly: AssemblyProduct("Runic Velocity")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0")]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: AssemblyVersion("1.0.0.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
	internal sealed class RefSafetyRulesAttribute : Attribute
	{
		public readonly int Version;

		public RefSafetyRulesAttribute(int P_0)
		{
			Version = P_0;
		}
	}
}
namespace RunicVelocity
{
	internal static class VelocityConfig
	{
		internal static ConfigEntry<bool> Enabled { get; private set; }

		internal static ConfigEntry<bool> WarmManifest { get; private set; }

		internal static ConfigEntry<int> MaximumFiles { get; private set; }

		internal static ConfigEntry<bool> DetailedTracing { get; private set; }

		internal static void Bind(ConfigFile config)
		{
			//IL_005b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0065: Expected O, but got Unknown
			Enabled = config.Bind<bool>("General", "Enabled", true, "Enable bounded startup measurement and the integrity-checked local manifest cache. When false, no timeline, worker, module, or service is started.");
			WarmManifest = config.Bind<bool>("Manifest", "WarmCache", true, "Reuse a cached SHA-256 and plugin metadata only when the exact relative path, size, and UTC modification time are unchanged. A server challenge may still require fresh hashing.");
			MaximumFiles = config.Bind<int>("Manifest", "MaximumFiles", 2048, new ConfigDescription("Maximum DLLs considered in one bounded scan.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 4096), Array.Empty<object>()));
			DetailedTracing = config.Bind<bool>("Diagnostics", "DetailedTracing", false, "Log one bounded manifest summary after the background scan. File paths are not logged.");
		}
	}
	[BepInPlugin("chazman.RunicVelocity", "Runic Velocity", "1.0.0")]
	public sealed class Plugin : BaseUnityPlugin
	{
		public const string Guid = "chazman.RunicVelocity";

		public const string Name = "Runic Velocity";

		public const string Version = "1.0.0";

		private VelocityRuntime _runtime;

		internal static ManualLogSource Log { get; private set; }

		private void Awake()
		{
			Log = ((BaseUnityPlugin)this).Logger;
			VelocityConfig.Bind(((BaseUnityPlugin)this).Config);
			ConfigEntry<bool> enabled = VelocityConfig.Enabled;
			if (enabled != null && !enabled.Value)
			{
				((BaseUnityPlugin)this).Logger.LogInfo((object)"Runic Velocity is disabled; no timeline or manifest worker was started.");
				return;
			}
			try
			{
				_runtime = new VelocityRuntime();
				_runtime.Start();
				((BaseUnityPlugin)this).Logger.LogInfo((object)"Runic Velocity v1.0.0 ready. Manifest hashing runs on a worker and unchanged local files may reuse integrity metadata. Third-party initialization remains untouched.");
			}
			catch (Exception ex)
			{
				((BaseUnityPlugin)this).Logger.LogError((object)("Runic Velocity failed closed; BepInEx startup remains unchanged. " + ex));
				Shutdown();
			}
		}

		private void Update()
		{
			try
			{
				_runtime?.Tick();
			}
			catch (Exception ex)
			{
				((BaseUnityPlugin)this).Logger.LogError((object)("Velocity diagnostics stopped after an unexpected error. " + ex.Message));
				_runtime?.Stop();
				_runtime = null;
			}
		}

		private void OnDestroy()
		{
			Shutdown();
		}

		private void Shutdown()
		{
			try
			{
				_runtime?.Stop();
			}
			catch
			{
			}
			_runtime = null;
		}
	}
}
namespace RunicVelocity.Integration
{
	internal sealed class VelocityRuntime
	{
		private static readonly long ProcessOrigin = Stopwatch.GetTimestamp();

		private readonly StartupTimeline _timeline = new StartupTimeline(ProcessOrigin);

		private readonly object _gate = new object();

		private Task<PluginManifestSnapshot> _scan;

		private CancellationTokenSource _cancellation;

		private int _generation;

		private bool _logged;

		private bool _enabled;

		private bool _firstUpdatePending;

		private bool _mainMenuPending;

		private bool _networkPending;

		private bool _worldPending;

		private bool _playerPending;

		private bool _scanPending;

		internal void Start()
		{
			_enabled = VelocityConfig.Enabled?.Value ?? true;
			if (_enabled)
			{
				_firstUpdatePending = true;
				_mainMenuPending = true;
				_networkPending = true;
				_worldPending = true;
				_playerPending = true;
				_timeline.Record("plugin-awake", 0L);
				BeginScan();
			}
		}

		internal void Tick()
		{
			if (!_enabled)
			{
				return;
			}
			if (_firstUpdatePending)
			{
				_timeline.Record("first-update", 0L);
				_firstUpdatePending = false;
			}
			if (_mainMenuPending && (Object)(object)FejdStartup.instance != (Object)null)
			{
				_timeline.Record("main-menu-instance", 0L);
				_mainMenuPending = false;
			}
			if (_networkPending && (Object)(object)ZNet.instance != (Object)null)
			{
				_timeline.Record("network-session-instance", 0L);
				_networkPending = false;
			}
			if (_worldPending && (Object)(object)Game.instance != (Object)null)
			{
				_timeline.Record("world-game-instance", 0L);
				_worldPending = false;
			}
			if (_playerPending && (Object)(object)Player.m_localPlayer != (Object)null)
			{
				_timeline.Record("local-player-ready", 0L);
				_playerPending = false;
			}
			if (!_firstUpdatePending && !_mainMenuPending && !_networkPending && !_worldPending && !_playerPending && !_scanPending)
			{
				return;
			}
			Task<PluginManifestSnapshot> scan;
			lock (_gate)
			{
				scan = _scan;
			}
			if (scan == null || !scan.IsCompleted)
			{
				return;
			}
			PluginManifestSnapshot pluginManifestSnapshot;
			try
			{
				pluginManifestSnapshot = scan.GetAwaiter().GetResult();
			}
			catch
			{
				pluginManifestSnapshot = new PluginManifestSnapshot(Array.Empty<PluginManifestEntry>(), 0, 0, truncated: true, "scan-failed");
			}
			lock (_gate)
			{
				if (_scan != scan)
				{
					return;
				}
				_scan = null;
				_scanPending = false;
			}
			_timeline.Record("manifest-scan-complete", 0L);
			if (_logged)
			{
				return;
			}
			ConfigEntry<bool> detailedTracing = VelocityConfig.DetailedTracing;
			if (detailedTracing != null && detailedTracing.Value)
			{
				_logged = true;
				ManualLogSource log = Plugin.Log;
				if (log != null)
				{
					log.LogInfo((object)($"Velocity manifest: {pluginManifestSnapshot.Entries.Count} DLLs, " + $"{pluginManifestSnapshot.ReusedFiles} reused, {pluginManifestSnapshot.HashedFiles} hashed, status={pluginManifestSnapshot.Status}."));
				}
			}
		}

		internal void Stop()
		{
			_enabled = false;
			Interlocked.Increment(ref _generation);
			try
			{
				_cancellation?.Cancel();
			}
			catch
			{
			}
			try
			{
				_cancellation?.Dispose();
			}
			catch
			{
			}
			_cancellation = null;
			lock (_gate)
			{
				_scan = null;
			}
			_scanPending = false;
		}

		private void BeginScan()
		{
			int num = Interlocked.Increment(ref _generation);
			try
			{
				_cancellation?.Cancel();
			}
			catch
			{
			}
			try
			{
				_cancellation?.Dispose();
			}
			catch
			{
			}
			_cancellation = new CancellationTokenSource();
			CancellationToken cancellationToken = _cancellation.Token;
			string root = Paths.PluginPath;
			string cache = Path.Combine(Paths.ConfigPath, "RunicVelocity.manifest-cache.bin");
			bool warm = VelocityConfig.WarmManifest?.Value ?? true;
			int maximum = VelocityConfig.MaximumFiles?.Value ?? 2048;
			_timeline.Record("manifest-scan-start", 0L);
			_scanPending = true;
			PluginManifestScanner scanner = new PluginManifestScanner();
			Task<PluginManifestSnapshot> scan = Task.Run(() => scanner.Scan(root, cache, warm, maximum, cancellationToken), cancellationToken);
			lock (_gate)
			{
				if (num == _generation)
				{
					_scan = scan;
				}
			}
		}
	}
}
namespace RunicVelocity.Core
{
	internal static class ManifestCacheCodec
	{
		private const int Magic = 1381387331;

		private const int Schema = 2;

		private const int MaximumDependencies = 64;

		private const int MaximumIdentityCharacters = 256;

		private const int MaximumClassificationCharacters = 64;

		private static readonly Encoding StrictUtf8 = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);

		internal static bool TryRead(string path, out Dictionary<string, PluginManifestEntry> entries)
		{
			entries = new Dictionary<string, PluginManifestEntry>(ManifestCachePolicy.PathComparer);
			try
			{
				if (!TryReadStableBounded(path, out var bytes))
				{
					return false;
				}
				byte[] array;
				using (MemoryStream memoryStream = new MemoryStream(bytes, writable: false))
				{
					using BinaryReader binaryReader = new BinaryReader(memoryStream, Encoding.UTF8, leaveOpen: false);
					if (binaryReader.ReadInt32() != 1381387331 || binaryReader.ReadInt32() != 2)
					{
						return false;
					}
					int num = binaryReader.ReadInt32();
					if (num <= 0 || num > 8388564)
					{
						return false;
					}
					array = binaryReader.ReadBytes(num);
					byte[] array2 = binaryReader.ReadBytes(32);
					if (array.Length != num || array2.Length != 32 || memoryStream.Position != memoryStream.Length)
					{
						return false;
					}
					using SHA256 sHA = SHA256.Create();
					if (!ConstantTimeEquals(array2, sHA.ComputeHash(array)))
					{
						return false;
					}
				}
				using MemoryStream memoryStream2 = new MemoryStream(array, writable: false);
				using BinaryReader binaryReader2 = new BinaryReader(memoryStream2, Encoding.UTF8, leaveOpen: false);
				int num2 = binaryReader2.ReadInt32();
				if (num2 < 0 || num2 > 4096)
				{
					return false;
				}
				for (int i = 0; i < num2; i++)
				{
					string text = ReadBounded(binaryReader2, 1024);
					long num3 = binaryReader2.ReadInt64();
					long num4 = binaryReader2.ReadInt64();
					string text2 = ReadBounded(binaryReader2, 64);
					string pluginId = ReadBounded(binaryReader2, 256);
					string pluginVersion = ReadBounded(binaryReader2, 256);
					string classification = ReadBounded(binaryReader2, 64);
					long lastManifestScanUtcTicks = binaryReader2.ReadInt64();
					int num5 = binaryReader2.ReadInt32();
					if (num5 < 0 || num5 > 64)
					{
						return false;
					}
					string[] array3 = new string[num5];
					for (int j = 0; j < num5; j++)
					{
						array3[j] = ReadBounded(binaryReader2, 256);
					}
					if (!ManifestCachePolicy.IsSafeRelativePath(text) || num3 < 0 || num4 < 0 || !ManifestCachePolicy.IsSha256(text2) || entries.ContainsKey(text))
					{
						return false;
					}
					entries.Add(text, new PluginManifestEntry(text, num3, num4, text2, pluginId, pluginVersion, array3, classification, lastManifestScanUtcTicks));
				}
				if (memoryStream2.Position != memoryStream2.Length)
				{
					return false;
				}
				return true;
			}
			catch
			{
				entries.Clear();
				return false;
			}
		}

		private static bool TryReadStableBounded(string path, out byte[] bytes)
		{
			bytes = null;
			try
			{
				FileInfo fileInfo = new FileInfo(path);
				if (!fileInfo.Exists || fileInfo.Length <= 0 || fileInfo.Length > 8388608 || fileInfo.Length > int.MaxValue)
				{
					return false;
				}
				long length = fileInfo.Length;
				DateTime lastWriteTimeUtc = fileInfo.LastWriteTimeUtc;
				bytes = new byte[(int)length];
				using (FileStream fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, Math.Max(1, Math.Min(65536, bytes.Length)), FileOptions.SequentialScan))
				{
					if (fileStream.Length != length)
					{
						bytes = null;
						return false;
					}
					int num;
					for (int i = 0; i < bytes.Length; i += num)
					{
						num = fileStream.Read(bytes, i, bytes.Length - i);
						if (num <= 0)
						{
							bytes = null;
							return false;
						}
					}
					if (fileStream.ReadByte() != -1 || fileStream.Length != length)
					{
						bytes = null;
						return false;
					}
				}
				fileInfo.Refresh();
				if (!fileInfo.Exists || fileInfo.Length != length || fileInfo.LastWriteTimeUtc != lastWriteTimeUtc)
				{
					bytes = null;
					return false;
				}
				return true;
			}
			catch
			{
				bytes = null;
				return false;
			}
		}

		internal static bool TryWrite(string path, IReadOnlyList<PluginManifestEntry> entries, CancellationToken cancellationToken = default(CancellationToken))
		{
			if (string.IsNullOrWhiteSpace(path) || entries == null || entries.Count > 4096)
			{
				return false;
			}
			string text = path + ".pending";
			try
			{
				cancellationToken.ThrowIfCancellationRequested();
				HashSet<string> hashSet = new HashSet<string>(ManifestCachePolicy.PathComparer);
				byte[] array;
				using (MemoryStream memoryStream = new MemoryStream())
				{
					using BinaryWriter binaryWriter = new BinaryWriter(memoryStream, Encoding.UTF8, leaveOpen: true);
					binaryWriter.Write(entries.Count);
					for (int i = 0; i < entries.Count; i++)
					{
						cancellationToken.ThrowIfCancellationRequested();
						PluginManifestEntry pluginManifestEntry = entries[i];
						if (!Validate(pluginManifestEntry) || !hashSet.Add(pluginManifestEntry.RelativePath))
						{
							return false;
						}
						WriteBounded(binaryWriter, pluginManifestEntry.RelativePath, 1024);
						binaryWriter.Write(pluginManifestEntry.Length);
						binaryWriter.Write(pluginManifestEntry.LastWriteUtcTicks);
						WriteBounded(binaryWriter, pluginManifestEntry.Sha256, 64);
						WriteBounded(binaryWriter, pluginManifestEntry.PluginId, 256);
						WriteBounded(binaryWriter, pluginManifestEntry.PluginVersion, 256);
						WriteBounded(binaryWriter, pluginManifestEntry.Classification, 64);
						binaryWriter.Write(pluginManifestEntry.LastManifestScanUtcTicks);
						binaryWriter.Write(pluginManifestEntry.Dependencies.Count);
						for (int j = 0; j < pluginManifestEntry.Dependencies.Count; j++)
						{
							WriteBounded(binaryWriter, pluginManifestEntry.Dependencies[j], 256);
						}
					}
					binaryWriter.Flush();
					if (memoryStream.Length > 8388564)
					{
						return false;
					}
					array = memoryStream.ToArray();
				}
				byte[] array2;
				using (SHA256 sHA = SHA256.Create())
				{
					using MemoryStream memoryStream2 = new MemoryStream();
					using BinaryWriter binaryWriter2 = new BinaryWriter(memoryStream2, Encoding.UTF8, leaveOpen: true);
					binaryWriter2.Write(1381387331);
					binaryWriter2.Write(2);
					binaryWriter2.Write(array.Length);
					binaryWriter2.Write(array);
					binaryWriter2.Write(sHA.ComputeHash(array));
					binaryWriter2.Flush();
					if (memoryStream2.Length > 8388608)
					{
						return false;
					}
					array2 = memoryStream2.ToArray();
				}
				string directoryName = Path.GetDirectoryName(path);
				if (string.IsNullOrEmpty(directoryName))
				{
					return false;
				}
				Directory.CreateDirectory(directoryName);
				using (FileStream fileStream = new FileStream(text, FileMode.Create, FileAccess.Write, FileShare.None))
				{
					int num;
					for (int k = 0; k < array2.Length; k += num)
					{
						cancellationToken.ThrowIfCancellationRequested();
						num = Math.Min(65536, array2.Length - k);
						fileStream.Write(array2, k, num);
					}
					fileStream.Flush(flushToDisk: true);
				}
				cancellationToken.ThrowIfCancellationRequested();
				if (File.Exists(path))
				{
					File.Replace(text, path, null);
				}
				else
				{
					File.Move(text, path);
				}
				return true;
			}
			catch (OperationCanceledException)
			{
				try
				{
					if (File.Exists(text))
					{
						File.Delete(text);
					}
				}
				catch
				{
				}
				throw;
			}
			catch
			{
				try
				{
					if (File.Exists(text))
					{
						File.Delete(text);
					}
				}
				catch
				{
				}
				return false;
			}
		}

		private static bool Validate(PluginManifestEntry entry)
		{
			if (entry == null || !ManifestCachePolicy.IsSafeRelativePath(entry.RelativePath) || entry.Length < 0 || entry.LastWriteUtcTicks < 0 || !ManifestCachePolicy.IsSha256(entry.Sha256) || entry.PluginId.Length > 256 || entry.PluginVersion.Length > 256 || entry.Classification.Length > 64 || entry.Dependencies.Count > 64)
			{
				return false;
			}
			for (int i = 0; i < entry.Dependencies.Count; i++)
			{
				if ((entry.Dependencies[i] ?? string.Empty).Length > 256)
				{
					return false;
				}
			}
			return true;
		}

		private static string ReadBounded(BinaryReader reader, int maximum)
		{
			if (reader == null || maximum < 0)
			{
				throw new InvalidDataException("Manifest string bound is invalid.");
			}
			int maximum2 = checked(maximum * 4);
			int num = Read7BitEncodedInt(reader, maximum2);
			Stream baseStream = reader.BaseStream;
			if (baseStream.CanSeek && (num < 0 || num > baseStream.Length - baseStream.Position))
			{
				throw new EndOfStreamException("Manifest string exceeds the remaining payload.");
			}
			byte[] array = reader.ReadBytes(num);
			if (array.Length != num)
			{
				throw new EndOfStreamException("Manifest string ended early.");
			}
			string text = StrictUtf8.GetString(array);
			if (text.Length > maximum)
			{
				throw new InvalidDataException("Manifest field exceeded its bound.");
			}
			return text;
		}

		private static void WriteBounded(BinaryWriter writer, string value, int maximum)
		{
			value = value ?? string.Empty;
			if (value.Length > maximum)
			{
				throw new InvalidDataException("Manifest field exceeded its bound.");
			}
			byte[] bytes = StrictUtf8.GetBytes(value);
			if (bytes.Length > checked(maximum * 4))
			{
				throw new InvalidDataException("Manifest field exceeded its UTF-8 bound.");
			}
			Write7BitEncodedInt(writer, bytes.Length);
			writer.Write(bytes);
		}

		private static int Read7BitEncodedInt(BinaryReader reader, int maximum)
		{
			uint num = 0u;
			for (int i = 0; i < 5; i++)
			{
				byte b = reader.ReadByte();
				if (i == 4 && (b & 0xF0) != 0)
				{
					throw new InvalidDataException("Manifest string length is not canonical.");
				}
				num |= (uint)((b & 0x7F) << i * 7);
				if ((b & 0x80) == 0)
				{
					if (i > 0 && b == 0)
					{
						throw new InvalidDataException("Manifest string length is not canonical.");
					}
					if (num > maximum)
					{
						throw new InvalidDataException("Manifest string byte length exceeded its bound.");
					}
					return (int)num;
				}
			}
			throw new InvalidDataException("Manifest string length is not canonical.");
		}

		private static void Write7BitEncodedInt(BinaryWriter writer, int value)
		{
			uint num;
			for (num = checked((uint)value); num >= 128; num >>= 7)
			{
				writer.Write((byte)((num & 0x7F) | 0x80));
			}
			writer.Write((byte)num);
		}

		private static bool ConstantTimeEquals(byte[] left, byte[] right)
		{
			if (left == null || right == null || left.Length != right.Length)
			{
				return false;
			}
			int num = 0;
			for (int i = 0; i < left.Length; i++)
			{
				num |= left[i] ^ right[i];
			}
			return num == 0;
		}
	}
	internal static class ManifestCachePolicy
	{
		internal const int MaximumFiles = 4096;

		internal const int MaximumDirectories = 4096;

		internal const long MaximumFileBytes = 536870912L;

		internal const long MaximumScanBytes = 4294967296L;

		internal const long MaximumMetadataFileBytes = 67108864L;

		internal const int MaximumCacheBytes = 8388608;

		internal const int MaximumPathCharacters = 1024;

		internal static bool UsesCaseInsensitivePaths => RuntimeInformation.IsOSPlatform(OSPlatform.Windows);

		internal static StringComparer PathComparer => PathComparerFor(UsesCaseInsensitivePaths);

		internal static StringComparison PathComparison
		{
			get
			{
				if (!UsesCaseInsensitivePaths)
				{
					return StringComparison.Ordinal;
				}
				return StringComparison.OrdinalIgnoreCase;
			}
		}

		internal static StringComparer PathComparerFor(bool caseInsensitive)
		{
			if (!caseInsensitive)
			{
				return StringComparer.Ordinal;
			}
			return StringComparer.OrdinalIgnoreCase;
		}

		internal static bool CanReuse(PluginManifestEntry cached, long currentLength, long currentLastWriteUtcTicks)
		{
			if (cached != null && cached.Length == currentLength && cached.LastWriteUtcTicks == currentLastWriteUtcTicks)
			{
				return IsSha256(cached.Sha256);
			}
			return false;
		}

		internal static bool CanAdmitScanBytes(long alreadyAdmitted, long nextFileBytes)
		{
			if (alreadyAdmitted >= 0 && nextFileBytes >= 0 && nextFileBytes <= 536870912)
			{
				return alreadyAdmitted <= 4294967296L - nextFileBytes;
			}
			return false;
		}

		internal static bool IsSafeRelativePath(string value)
		{
			if (!string.IsNullOrWhiteSpace(value) && value.Length <= 1024 && !Path.IsPathRooted(value) && value.IndexOf('\0') < 0)
			{
				return !HasParentSegment(value);
			}
			return false;
		}

		internal static bool IsSha256(string value)
		{
			if (value == null || value.Length != 64)
			{
				return false;
			}
			foreach (char c in value)
			{
				if ((c < '0' || c > '9') && (c < 'A' || c > 'F'))
				{
					return false;
				}
			}
			return true;
		}

		internal static string Classification(string pluginId, string relativePath)
		{
			if (!string.IsNullOrEmpty(pluginId) && pluginId.StartsWith("chazman.Runic", StringComparison.Ordinal))
			{
				return "runic-plugin";
			}
			if (!string.IsNullOrEmpty(pluginId))
			{
				return "third-party-plugin";
			}
			return "library";
		}

		private static bool HasParentSegment(string value)
		{
			string[] array = value.Replace('\\', '/').Split('/');
			for (int i = 0; i < array.Length; i++)
			{
				if (array[i] == "..")
				{
					return true;
				}
			}
			return false;
		}
	}
	internal sealed class PluginManifestScanner
	{
		private const string PluginAttribute = "BepInEx.BepInPlugin";

		private const string DependencyAttribute = "BepInEx.BepInDependency";

		internal PluginManifestSnapshot Scan(string root, string cachePath, bool allowWarmCache, int configuredMaximumFiles, CancellationToken cancellationToken = default(CancellationToken))
		{
			int maximum = Math.Max(1, Math.Min(4096, configuredMaximumFiles));
			if (!allowWarmCache || !ManifestCacheCodec.TryRead(cachePath, out var entries))
			{
				entries = new Dictionary<string, PluginManifestEntry>(ManifestCachePolicy.PathComparer);
			}
			bool truncated;
			List<string> list = EnumerateDlls(root, maximum, cancellationToken, out truncated);
			List<PluginManifestEntry> list2 = new List<PluginManifestEntry>(list.Count);
			int num = 0;
			int num2 = 0;
			bool metadataPartial = false;
			long num3 = 0L;
			long ticks = DateTime.UtcNow.Ticks;
			byte[] buffer = new byte[131072];
			foreach (string item in list)
			{
				cancellationToken.ThrowIfCancellationRequested();
				FileInfo fileInfo;
				string text;
				try
				{
					fileInfo = new FileInfo(item);
					text = RelativePath(root, item);
				}
				catch
				{
					truncated = true;
					continue;
				}
				if (!ManifestCachePolicy.IsSafeRelativePath(text) || !fileInfo.Exists || !ManifestCachePolicy.CanAdmitScanBytes(num3, fileInfo.Length))
				{
					truncated = true;
					continue;
				}
				num3 += fileInfo.Length;
				if (entries.TryGetValue(text, out var value) && ManifestCachePolicy.CanReuse(value, fileInfo.Length, fileInfo.LastWriteTimeUtc.Ticks))
				{
					list2.Add(new PluginManifestEntry(text, value.Length, value.LastWriteUtcTicks, value.Sha256, value.PluginId, value.PluginVersion, value.Dependencies, value.Classification, ticks));
					num2++;
					continue;
				}
				long length = fileInfo.Length;
				long ticks2 = fileInfo.LastWriteTimeUtc.Ticks;
				bool inspectMetadata = length <= 67108864;
				if (!TryReadEvidence(item, length, buffer, inspectMetadata, cancellationToken, out var sha, out var pluginId, out var version, out var dependencies, out var metadataComplete))
				{
					truncated = true;
					continue;
				}
				if (!metadataComplete)
				{
					metadataPartial = true;
				}
				fileInfo.Refresh();
				if (!fileInfo.Exists || fileInfo.Length != length || fileInfo.LastWriteTimeUtc.Ticks != ticks2)
				{
					truncated = true;
					continue;
				}
				list2.Add(new PluginManifestEntry(text, length, ticks2, sha, pluginId, version, dependencies, ManifestCachePolicy.Classification(pluginId, text), ticks));
				num++;
			}
			list2.Sort(delegate(PluginManifestEntry left, PluginManifestEntry right)
			{
				int num4 = ManifestCachePolicy.PathComparer.Compare(left.RelativePath, right.RelativePath);
				return (num4 == 0) ? StringComparer.Ordinal.Compare(left.RelativePath, right.RelativePath) : num4;
			});
			cancellationToken.ThrowIfCancellationRequested();
			bool cacheWritten = ManifestCacheCodec.TryWrite(cachePath, list2, cancellationToken);
			string status = Status(truncated, metadataPartial, cacheWritten, num, num2);
			return new PluginManifestSnapshot(list2, num, num2, truncated, status);
		}

		private static string Status(bool truncated, bool metadataPartial, bool cacheWritten, int hashed, int reused)
		{
			if (truncated)
			{
				return "bounded-partial";
			}
			if (!cacheWritten)
			{
				return "manifest-cache-write-failed";
			}
			if (metadataPartial)
			{
				return "hash-complete-metadata-partial";
			}
			if (reused > 0)
			{
				if (hashed <= 0)
				{
					return "warm-cache-reused";
				}
				return "mixed-fresh-and-warm";
			}
			return "freshly-hashed";
		}

		private static List<string> EnumerateDlls(string root, int maximum, CancellationToken cancellationToken, out bool truncated)
		{
			truncated = false;
			List<string> list = new List<string>(maximum);
			if (string.IsNullOrWhiteSpace(root) || !Directory.Exists(root))
			{
				truncated = true;
				return list;
			}
			Queue<string> queue = new Queue<string>();
			queue.Enqueue(Path.GetFullPath(root));
			int num = 0;
			int num2 = 1;
			while (queue.Count > 0)
			{
				cancellationToken.ThrowIfCancellationRequested();
				if (++num > 4096)
				{
					truncated = true;
					break;
				}
				string path = queue.Dequeue();
				try
				{
					foreach (string item in Directory.EnumerateFiles(path, "*.dll", SearchOption.TopDirectoryOnly))
					{
						cancellationToken.ThrowIfCancellationRequested();
						if (list.Count >= maximum)
						{
							truncated = true;
							break;
						}
						list.Add(item);
					}
					if (list.Count >= maximum)
					{
						break;
					}
					foreach (string item2 in Directory.EnumerateDirectories(path, "*", SearchOption.TopDirectoryOnly))
					{
						cancellationToken.ThrowIfCancellationRequested();
						if (num2 >= 4096)
						{
							truncated = true;
							break;
						}
						if ((File.GetAttributes(item2) & FileAttributes.ReparsePoint) == 0)
						{
							queue.Enqueue(item2);
							num2++;
						}
					}
					continue;
				}
				catch
				{
					truncated = true;
					continue;
				}
			}
			return list;
		}

		private static string RelativePath(string root, string fullPath)
		{
			string text = Path.GetFullPath(root).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar;
			string fullPath2 = Path.GetFullPath(fullPath);
			if (!fullPath2.StartsWith(text, ManifestCachePolicy.PathComparison))
			{
				return string.Empty;
			}
			return fullPath2.Substring(text.Length).Replace(Path.DirectorySeparatorChar, '/');
		}

		private static bool TryReadEvidence(string path, long expectedLength, byte[] buffer, bool inspectMetadata, CancellationToken cancellationToken, out string sha, out string pluginId, out string version, out string[] dependencies, out bool metadataComplete)
		{
			sha = string.Empty;
			pluginId = string.Empty;
			version = string.Empty;
			dependencies = Array.Empty<string>();
			metadataComplete = false;
			try
			{
				if (expectedLength < 0 || expectedLength > 536870912 || buffer == null || buffer.Length == 0)
				{
					return false;
				}
				using SHA256 sHA = SHA256.Create();
				using FileStream fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, buffer.Length, FileOptions.SequentialScan);
				if (fileStream.Length != expectedLength)
				{
					return false;
				}
				long num = expectedLength;
				while (num > 0)
				{
					cancellationToken.ThrowIfCancellationRequested();
					int count = (int)Math.Min(buffer.Length, num);
					int num2 = fileStream.Read(buffer, 0, count);
					if (num2 <= 0)
					{
						return false;
					}
					sHA.TransformBlock(buffer, 0, num2, buffer, 0);
					num -= num2;
				}
				if (fileStream.ReadByte() != -1 || fileStream.Length != expectedLength)
				{
					return false;
				}
				sHA.TransformFinalBlock(Array.Empty<byte>(), 0, 0);
				sha = BitConverter.ToString(sHA.Hash).Replace("-", string.Empty);
				if (!ManifestCachePolicy.IsSha256(sha))
				{
					return false;
				}
				if (inspectMetadata)
				{
					cancellationToken.ThrowIfCancellationRequested();
					fileStream.Position = 0L;
					metadataComplete = ReadMetadata(fileStream, cancellationToken, out pluginId, out version, out dependencies);
				}
				return true;
			}
			catch (OperationCanceledException)
			{
				throw;
			}
			catch
			{
				return false;
			}
		}

		private static bool ReadMetadata(Stream stream, CancellationToken cancellationToken, out string pluginId, out string version, out string[] dependencies)
		{
			//IL_0017: Unknown result type (might be due to invalid IL or missing references)
			//IL_001c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0023: Unknown result type (might be due to invalid IL or missing references)
			//IL_002a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0036: Expected O, but got Unknown
			//IL_0053: Unknown result type (might be due to invalid IL or missing references)
			//IL_0058: Unknown result type (might be due to invalid IL or missing references)
			//IL_00dc: Unknown result type (might be due to invalid IL or missing references)
			//IL_00e1: Unknown result type (might be due to invalid IL or missing references)
			//IL_0135: Unknown result type (might be due to invalid IL or missing references)
			//IL_013a: Unknown result type (might be due to invalid IL or missing references)
			//IL_020f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0214: Unknown result type (might be due to invalid IL or missing references)
			//IL_01a3: Unknown result type (might be due to invalid IL or missing references)
			//IL_01a8: Unknown result type (might be due to invalid IL or missing references)
			//IL_01c5: Unknown result type (might be due to invalid IL or missing references)
			//IL_01ca: Unknown result type (might be due to invalid IL or missing references)
			pluginId = string.Empty;
			version = string.Empty;
			dependencies = Array.Empty<string>();
			try
			{
				AssemblyDefinition val = AssemblyDefinition.ReadAssembly(stream, new ReaderParameters
				{
					ReadingMode = (ReadingMode)2,
					ReadSymbols = false,
					InMemory = false
				});
				try
				{
					SortedSet<string> sortedSet = new SortedSet<string>(StringComparer.Ordinal);
					Queue<TypeDefinition> queue = new Queue<TypeDefinition>();
					Enumerator<TypeDefinition> enumerator = val.MainModule.Types.GetEnumerator();
					try
					{
						while (enumerator.MoveNext())
						{
							TypeDefinition current = enumerator.Current;
							cancellationToken.ThrowIfCancellationRequested();
							if (queue.Count >= 65536)
							{
								throw new InvalidDataException("Metadata pending-type bound exceeded.");
							}
							queue.Enqueue(current);
						}
					}
					finally
					{
						((IDisposable)enumerator/*cast due to .constrained prefix*/).Dispose();
					}
					int num = 0;
					int num2 = 0;
					while (queue.Count > 0)
					{
						cancellationToken.ThrowIfCancellationRequested();
						if (++num > 65536)
						{
							throw new InvalidDataException("Metadata type bound exceeded.");
						}
						TypeDefinition val2 = queue.Dequeue();
						enumerator = val2.NestedTypes.GetEnumerator();
						try
						{
							while (enumerator.MoveNext())
							{
								TypeDefinition current2 = enumerator.Current;
								cancellationToken.ThrowIfCancellationRequested();
								if (queue.Count >= 65536)
								{
									throw new InvalidDataException("Metadata pending-type bound exceeded.");
								}
								queue.Enqueue(current2);
							}
						}
						finally
						{
							((IDisposable)enumerator/*cast due to .constrained prefix*/).Dispose();
						}
						Enumerator<CustomAttribute> enumerator2 = val2.CustomAttributes.GetEnumerator();
						try
						{
							while (enumerator2.MoveNext())
							{
								CustomAttribute current3 = enumerator2.Current;
								cancellationToken.ThrowIfCancellationRequested();
								if (++num2 > 262144)
								{
									throw new InvalidDataException("Metadata attribute bound exceeded.");
								}
								CustomAttributeArgument val3;
								if (((MemberReference)current3.AttributeType).FullName == "BepInEx.BepInPlugin" && current3.ConstructorArguments.Count >= 3 && string.IsNullOrEmpty(pluginId))
								{
									val3 = current3.ConstructorArguments[0];
									pluginId = BoundedExact(((CustomAttributeArgument)(ref val3)).Value as string);
									val3 = current3.ConstructorArguments[2];
									version = BoundedExact(((CustomAttributeArgument)(ref val3)).Value as string);
								}
								else
								{
									if (!(((MemberReference)current3.AttributeType).FullName == "BepInEx.BepInDependency") || current3.ConstructorArguments.Count < 1)
									{
										continue;
									}
									val3 = current3.ConstructorArguments[0];
									string text = BoundedExact(((CustomAttributeArgument)(ref val3)).Value as string);
									if (!string.IsNullOrEmpty(text) && !sortedSet.Contains(text))
									{
										if (sortedSet.Count >= 64)
										{
											throw new InvalidDataException("Metadata dependency bound exceeded.");
										}
										sortedSet.Add(text);
									}
								}
							}
						}
						finally
						{
							((IDisposable)enumerator2/*cast due to .constrained prefix*/).Dispose();
						}
					}
					dependencies = sortedSet.ToArray();
					return true;
				}
				finally
				{
					((IDisposable)val)?.Dispose();
				}
			}
			catch (OperationCanceledException)
			{
				throw;
			}
			catch
			{
				pluginId = string.Empty;
				version = string.Empty;
				dependencies = Array.Empty<string>();
				return false;
			}
		}

		private static string BoundedExact(string value)
		{
			if (string.IsNullOrEmpty(value))
			{
				return string.Empty;
			}
			if (value.Length > 256)
			{
				throw new InvalidDataException("Metadata identity bound exceeded.");
			}
			return value;
		}
	}
	internal sealed class StartupTimeline
	{
		internal const int MaximumStages = 64;

		private readonly object _gate = new object();

		private readonly long _origin;

		private readonly List<StartupStageSample> _stages = new List<StartupStageSample>();

		private readonly HashSet<string> _recorded = new HashSet<string>(StringComparer.Ordinal);

		internal StartupTimelineSnapshot Current
		{
			get
			{
				lock (_gate)
				{
					return new StartupTimelineSnapshot(_stages);
				}
			}
		}

		internal StartupTimeline(long origin)
		{
			_origin = ((origin > 0) ? origin : Stopwatch.GetTimestamp());
		}

		internal bool Record(string stageId, long timestamp = 0L)
		{
			if (string.IsNullOrWhiteSpace(stageId) || stageId.Length > 96)
			{
				return false;
			}
			lock (_gate)
			{
				if (_stages.Count >= 64 || !_recorded.Add(stageId))
				{
					return false;
				}
				long num = ((timestamp > 0) ? timestamp : Stopwatch.GetTimestamp());
				long num2 = Math.Max(0L, num - _origin);
				_stages.Add(new StartupStageSample(stageId, (double)num2 * 1000.0 / (double)Stopwatch.Frequency));
				return true;
			}
		}
	}
}
namespace RunicVelocity.Contracts
{
	public sealed class StartupStageSample
	{
		public string StageId { get; }

		public double MillisecondsFromAssemblyLoad { get; }

		public StartupStageSample(string stageId, double millisecondsFromAssemblyLoad)
		{
			if (string.IsNullOrWhiteSpace(stageId) || stageId.Length > 96 || millisecondsFromAssemblyLoad < 0.0 || double.IsNaN(millisecondsFromAssemblyLoad) || double.IsInfinity(millisecondsFromAssemblyLoad))
			{
				throw new ArgumentOutOfRangeException("stageId");
			}
			StageId = stageId;
			MillisecondsFromAssemblyLoad = millisecondsFromAssemblyLoad;
		}
	}
	public sealed class StartupTimelineSnapshot
	{
		public IReadOnlyList<StartupStageSample> Stages { get; }

		public StartupTimelineSnapshot(IEnumerable<StartupStageSample> stages)
		{
			Stages = new List<StartupStageSample>(stages ?? Array.Empty<StartupStageSample>()).AsReadOnly();
		}
	}
	public sealed class PluginManifestEntry
	{
		public string RelativePath { get; }

		public long Length { get; }

		public long LastWriteUtcTicks { get; }

		public string Sha256 { get; }

		public string PluginId { get; }

		public string PluginVersion { get; }

		public IReadOnlyList<string> Dependencies { get; }

		public string Classification { get; }

		public long LastManifestScanUtcTicks { get; }

		public PluginManifestEntry(string relativePath, long length, long lastWriteUtcTicks, string sha256, string pluginId, string pluginVersion, IEnumerable<string> dependencies, string classification, long lastManifestScanUtcTicks = 0L)
		{
			RelativePath = relativePath ?? string.Empty;
			Length = length;
			LastWriteUtcTicks = lastWriteUtcTicks;
			Sha256 = sha256 ?? string.Empty;
			PluginId = pluginId ?? string.Empty;
			PluginVersion = pluginVersion ?? string.Empty;
			Dependencies = new List<string>(dependencies ?? Array.Empty<string>()).AsReadOnly();
			Classification = classification ?? string.Empty;
			LastManifestScanUtcTicks = Math.Max(0L, lastManifestScanUtcTicks);
		}
	}
	public sealed class PluginManifestSnapshot
	{
		public IReadOnlyList<PluginManifestEntry> Entries { get; }

		public int HashedFiles { get; }

		public int ReusedFiles { get; }

		public bool Truncated { get; }

		public string Status { get; }

		public static PluginManifestSnapshot Empty { get; } = new PluginManifestSnapshot(Array.Empty<PluginManifestEntry>(), 0, 0, truncated: false, "not-started");

		public PluginManifestSnapshot(IEnumerable<PluginManifestEntry> entries, int hashedFiles, int reusedFiles, bool truncated, string status)
		{
			Entries = new List<PluginManifestEntry>(entries ?? Array.Empty<PluginManifestEntry>()).AsReadOnly();
			HashedFiles = Math.Max(0, hashedFiles);
			ReusedFiles = Math.Max(0, reusedFiles);
			Truncated = truncated;
			Status = status ?? string.Empty;
		}
	}
}