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 Enabled { get; private set; } internal static ConfigEntry WarmManifest { get; private set; } internal static ConfigEntry MaximumFiles { get; private set; } internal static ConfigEntry 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("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("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("Manifest", "MaximumFiles", 2048, new ConfigDescription("Maximum DLLs considered in one bounded scan.", (AcceptableValueBase)(object)new AcceptableValueRange(1, 4096), Array.Empty())); DetailedTracing = config.Bind("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 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 _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 scan; lock (_gate) { scan = _scan; } if (scan == null || !scan.IsCompleted) { return; } PluginManifestSnapshot pluginManifestSnapshot; try { pluginManifestSnapshot = scan.GetAwaiter().GetResult(); } catch { pluginManifestSnapshot = new PluginManifestSnapshot(Array.Empty(), 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 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 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 entries) { entries = new Dictionary(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 entries, CancellationToken cancellationToken = default(CancellationToken)) { if (string.IsNullOrWhiteSpace(path) || entries == null || entries.Count > 4096) { return false; } string text = path + ".pending"; try { cancellationToken.ThrowIfCancellationRequested(); HashSet hashSet = new HashSet(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(ManifestCachePolicy.PathComparer); } bool truncated; List list = EnumerateDlls(root, maximum, cancellationToken, out truncated); List list2 = new List(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 EnumerateDlls(string root, int maximum, CancellationToken cancellationToken, out bool truncated) { truncated = false; List list = new List(maximum); if (string.IsNullOrWhiteSpace(root) || !Directory.Exists(root)) { truncated = true; return list; } Queue queue = new Queue(); 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(); 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(), 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(); try { AssemblyDefinition val = AssemblyDefinition.ReadAssembly(stream, new ReaderParameters { ReadingMode = (ReadingMode)2, ReadSymbols = false, InMemory = false }); try { SortedSet sortedSet = new SortedSet(StringComparer.Ordinal); Queue queue = new Queue(); Enumerator 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 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(); 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 _stages = new List(); private readonly HashSet _recorded = new HashSet(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 Stages { get; } public StartupTimelineSnapshot(IEnumerable stages) { Stages = new List(stages ?? Array.Empty()).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 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 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(dependencies ?? Array.Empty()).AsReadOnly(); Classification = classification ?? string.Empty; LastManifestScanUtcTicks = Math.Max(0L, lastManifestScanUtcTicks); } } public sealed class PluginManifestSnapshot { public IReadOnlyList 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(), 0, 0, truncated: false, "not-started"); public PluginManifestSnapshot(IEnumerable entries, int hashedFiles, int reusedFiles, bool truncated, string status) { Entries = new List(entries ?? Array.Empty()).AsReadOnly(); HashedFiles = Math.Max(0, hashedFiles); ReusedFiles = Math.Max(0, reusedFiles); Truncated = truncated; Status = status ?? string.Empty; } } }