Decompiled source of RunicSafety v1.0.10

plugins/RunicSafety/RunicSafety.dll

Decompiled a week ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Serialization.Json;
using System.Runtime.Versioning;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using BepInEx;
using BepInEx.Bootstrap;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using Runic.Localization;
using RunicSafety.Api;
using RunicSafety.Integration;
using RunicSafety.Services;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: AssemblyTitle("Runic Safety")]
[assembly: AssemblyDescription("Loss-prevention, compatibility, recovery planning, and migration safeguards for Valheim.")]
[assembly: AssemblyCompany("Chazman")]
[assembly: AssemblyProduct("Runic Safety")]
[assembly: AssemblyCopyright("Copyright © 2026 Chazman")]
[assembly: ComVisible(false)]
[assembly: Guid("8f1fa4b8-7f91-46cc-94a4-7b769e1af73d")]
[assembly: AssemblyFileVersion("1.0.9.0")]
[assembly: AssemblyInformationalVersion("1.0.9")]
[assembly: InternalsVisibleTo("RunicSafety.Tests")]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: AssemblyVersion("1.0.9.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 Runic.Localization
{
	internal sealed class LanguageCatalog
	{
		private readonly Dictionary<string, string> _english;

		private Dictionary<string, string> _selected = new Dictionary<string, string>();

		private readonly Func<string, string> _read;

		private string _language;

		internal LanguageCatalog(string english, Func<string, string> read)
		{
			_english = Parse(english);
			_read = read;
		}

		internal static Dictionary<string, string> Parse(string json)
		{
			if (string.IsNullOrWhiteSpace(json))
			{
				return new Dictionary<string, string>();
			}
			if (json.Length > 2097152)
			{
				throw new InvalidDataException("Language file exceeds 2 MiB.");
			}
			DataContractJsonSerializer dataContractJsonSerializer = new DataContractJsonSerializer(typeof(Dictionary<string, string>), new DataContractJsonSerializerSettings
			{
				UseSimpleDictionaryFormat = true
			});
			using MemoryStream stream = new MemoryStream(Encoding.UTF8.GetBytes(json.TrimStart('\ufeff')));
			return ((Dictionary<string, string>)dataContractJsonSerializer.ReadObject(stream)) ?? new Dictionary<string, string>();
		}

		internal void Select(string language)
		{
			language = (string.IsNullOrWhiteSpace(language) ? "English" : language);
			if (string.Equals(language, _language, StringComparison.Ordinal))
			{
				return;
			}
			_language = language;
			_selected = new Dictionary<string, string>();
			if (language.IndexOfAny(new char[4] { '/', '\\', ':', '\0' }) >= 0 || language == "." || language == "..")
			{
				return;
			}
			try
			{
				_selected = Parse(_read(language));
			}
			catch (Exception)
			{
			}
		}

		internal string Get(string key)
		{
			_english.TryGetValue(key, out var value);
			if (_selected.TryGetValue(key, out var value2) && !string.IsNullOrWhiteSpace(value2) && (value == null || SameFields(value, value2)))
			{
				return value2;
			}
			return value ?? key;
		}

		internal string English(string key)
		{
			if (!_english.TryGetValue(key, out var value))
			{
				return key;
			}
			return value;
		}

		internal string TranslateEnglish(string text)
		{
			foreach (KeyValuePair<string, string> item in _english)
			{
				if (item.Value == text)
				{
					return Get(item.Key);
				}
			}
			return text;
		}

		internal string Format(string key, params object[] args)
		{
			_english.TryGetValue(key, out var value);
			string text = Get(key);
			if (value != null && !SameFields(value, text))
			{
				text = value;
			}
			try
			{
				return string.Format(CultureInfo.CurrentCulture, text, args);
			}
			catch (FormatException)
			{
				try
				{
					return string.Format(CultureInfo.CurrentCulture, value ?? key, args);
				}
				catch (FormatException)
				{
					return value ?? key;
				}
			}
		}

		private static bool SameFields(string a, string b)
		{
			try
			{
				return Fields(a).SetEquals(Fields(b));
			}
			catch (FormatException)
			{
				return false;
			}
		}

		private static HashSet<int> Fields(string text)
		{
			HashSet<int> hashSet = new HashSet<int>();
			for (int i = 0; i < text.Length; i++)
			{
				if (text[i] == '}')
				{
					if (i + 1 >= text.Length || text[i + 1] != '}')
					{
						throw new FormatException();
					}
					i++;
				}
				else
				{
					if (text[i] != '{')
					{
						continue;
					}
					if (i + 1 < text.Length && text[i + 1] == '{')
					{
						i++;
						continue;
					}
					int num = ++i;
					for (; i < text.Length && char.IsDigit(text[i]); i++)
					{
					}
					if (num == i || !int.TryParse(text.Substring(num, i - num), out var result))
					{
						throw new FormatException();
					}
					hashSet.Add(result);
					for (; i < text.Length && text[i] != '}'; i++)
					{
						if (text[i] == '{')
						{
							throw new FormatException();
						}
					}
					if (i == text.Length)
					{
						throw new FormatException();
					}
				}
			}
			return hashSet;
		}
	}
	internal static class RunicText
	{
		private static readonly Assembly Owner = typeof(RunicText).Assembly;

		private static readonly string Module = Owner.GetName().Name;

		private static readonly LanguageCatalog Catalog = Create();

		private static LanguageCatalog Create()
		{
			using Stream stream = Owner.GetManifestResourceStream("Runic.Localization.English.json");
			using StreamReader streamReader = ((stream == null) ? null : new StreamReader(stream));
			return new LanguageCatalog(streamReader?.ReadToEnd() ?? "{}", ReadLanguage);
		}

		private static string ReadLanguage(string language)
		{
			string path = Path.Combine(Path.Combine(Path.GetDirectoryName(Owner.Location), "Translations", Module), language + ".json");
			if (!File.Exists(path))
			{
				return null;
			}
			return File.ReadAllText(path);
		}

		private static void SelectLanguage()
		{
			string language = "English";
			try
			{
				Localization instance = Localization.instance;
				language = ((instance != null) ? instance.GetSelectedLanguage() : null) ?? "English";
			}
			catch
			{
			}
			Catalog.Select(language);
		}

		internal static string Get(string key)
		{
			SelectLanguage();
			return Catalog.Get(key);
		}

		internal static string English(string key)
		{
			return Catalog.English(key);
		}

		internal static string TranslateEnglish(string text)
		{
			SelectLanguage();
			return Catalog.TranslateEnglish(text);
		}

		internal static string Format(string key, params object[] args)
		{
			SelectLanguage();
			return Catalog.Format(key, args);
		}
	}
}
namespace RunicSafety
{
	[BepInPlugin("chazman.RunicSafety", "Runic Safety", "1.0.9")]
	public sealed class Plugin : BaseUnityPlugin
	{
		public const string Guid = "chazman.RunicSafety";

		public const string Name = "Runic Safety";

		public const string Version = "1.0.9";

		public const string ModuleId = "runic.safety";

		public const string ProtocolVersion = "1.0";

		private Harmony _harmony;

		private CorrelatedDiagnosticBuffer _diagnostics;

		private bool _configurationSubscribed;

		private bool _shuttingDown;

		internal static bool RuntimeReady { get; private set; }

		internal static SafetyRuntime CurrentRuntime { get; private set; }

		private void Awake()
		{
			//IL_0074: Unknown result type (might be due to invalid IL or missing references)
			//IL_007e: Expected O, but got Unknown
			SafetyConfig.Bind(((BaseUnityPlugin)this).Config);
			SafetyConfig.Changed += OnConfigurationChanged;
			_configurationSubscribed = true;
			_diagnostics = new CorrelatedDiagnosticBuffer(256, null, ((BaseUnityPlugin)this).Logger);
			try
			{
				if (!ValheimContracts.Initialize(out var problem))
				{
					throw new MissingMethodException(problem);
				}
				CurrentRuntime = new SafetyRuntime(_diagnostics);
				CurrentRuntime.Initialize();
				SafetyIntegrationApi.Attach(CurrentRuntime);
				_harmony = new Harmony("chazman.RunicSafety");
				_harmony.PatchAll(typeof(Plugin).Assembly);
				RuntimeReady = true;
				((BaseUnityPlugin)this).Logger.LogInfo((object)("Runic Safety v1.0.9 ready for Valheim " + ValheimContracts.ReadGameVersion() + ". Confirmations, protected destinations, vanilla tombstone audits, and migration backups are standalone."));
			}
			catch (Exception ex)
			{
				((BaseUnityPlugin)this).Logger.LogError((object)("Runic Safety startup failed closed; all patched actions remain vanilla. " + ex.GetType().Name + ": " + ex.Message));
				ShutdownRuntime();
			}
		}

		private void OnConfigurationChanged()
		{
			if (!RuntimeReady || CurrentRuntime == null)
			{
				return;
			}
			try
			{
				CurrentRuntime.OnConfigurationChanged();
			}
			catch (Exception ex)
			{
				((BaseUnityPlugin)this).Logger.LogError((object)("Safety configuration refresh failed; the next action will use bounded defaults. " + ex.GetType().Name + ": " + ex.Message));
			}
		}

		private void OnDestroy()
		{
			ShutdownRuntime();
			if (_configurationSubscribed)
			{
				SafetyConfig.Changed -= OnConfigurationChanged;
				_configurationSubscribed = false;
			}
			SafetyConfig.Unbind();
			_diagnostics?.SetLog(null);
			_diagnostics = null;
		}

		private void ShutdownRuntime()
		{
			if (_shuttingDown)
			{
				return;
			}
			_shuttingDown = true;
			RuntimeReady = false;
			try
			{
				Harmony harmony = _harmony;
				if (harmony != null)
				{
					harmony.UnpatchSelf();
				}
			}
			catch (Exception ex)
			{
				((BaseUnityPlugin)this).Logger.LogWarning((object)("Safety patch cleanup failed: " + ex.Message));
			}
			_harmony = null;
			SafetyRuntime currentRuntime = CurrentRuntime;
			SafetyIntegrationApi.Detach(currentRuntime);
			try
			{
				currentRuntime?.Shutdown();
			}
			catch (Exception ex2)
			{
				((BaseUnityPlugin)this).Logger.LogWarning((object)("Safety runtime cleanup failed: " + ex2.Message));
			}
			CurrentRuntime = null;
			_shuttingDown = false;
		}
	}
	internal static class SafetyConfig
	{
		private const string DefaultRarePrefabs = "DragonEgg,DvergrKey,DvergrKeyFragment,QueenDrop,Sealbreaker,Wishbone,TrophyTheQueen,TrophySeekerQueen";

		private static readonly object Sync = new object();

		private static HashSet<string> _rarePrefabs = new HashSet<string>(StringComparer.Ordinal);

		private static ConfigFile _config;

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

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

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

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

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

		internal static ConfigEntry<float> ConfirmationWindowSeconds { get; private set; }

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

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

		internal static ConfigEntry<string> RarePrefabNames { get; private set; }

		internal static ConfigEntry<string> BackupRoot { get; private set; }

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

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

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

		internal static TimeSpan ConfirmationWindow => TimeSpan.FromSeconds(ConfirmationWindowSeconds?.Value ?? 4f);

		internal static event Action Changed;

		internal static void Bind(ConfigFile config)
		{
			//IL_00e7: Unknown result type (might be due to invalid IL or missing references)
			//IL_00f1: Expected O, but got Unknown
			//IL_01b3: Unknown result type (might be due to invalid IL or missing references)
			//IL_01bd: Expected O, but got Unknown
			//IL_01e9: Unknown result type (might be due to invalid IL or missing references)
			//IL_01f3: Expected O, but got Unknown
			//IL_0222: Unknown result type (might be due to invalid IL or missing references)
			//IL_022c: Expected O, but got Unknown
			if (config == null)
			{
				throw new ArgumentNullException("config");
			}
			Unbind();
			_config = config;
			Enabled = Bind(config, "General", "Enabled", value: true, RunicText.Get("text_6d6dc4701ff3"));
			ConfirmRareSacrifice = Bind(config, "Confirmations", "RareItemSacrifice", value: true, RunicText.Get("text_db8fc97fcad8"));
			ConfirmOccupiedContainer = Bind(config, "Confirmations", "OccupiedContainerDestruction", value: true, RunicText.Get("text_dcaeba799395"));
			ConfirmVehicleDestruction = Bind(config, "Confirmations", "ShipOrCartDestruction", value: true, RunicText.Get("text_63c9d8a90511"));
			ConfirmPortalOverwrite = Bind(config, "Confirmations", "PortalOverwrite", value: true, RunicText.Get("text_ff3cbd217b26"));
			ConfirmationWindowSeconds = Bind(config, "Confirmations", "RepeatWindowSeconds", 4f, new ConfigDescription(RunicText.Get("text_a70672868f2a"), (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 15f), Array.Empty<object>()));
			ProtectedDestinations = Bind(config, "Protected Items", "Enabled", value: true, RunicText.Get("text_5760922ac7e7"));
			AdministratorBypass = Bind(config, "Protected Items", "AdministratorBypass", value: false, RunicText.Get("text_e0ae37ad90b0"));
			RarePrefabNames = Bind(config, "Protected Items", "RarePrefabNames", "DragonEgg,DvergrKey,DvergrKeyFragment,QueenDrop,Sealbreaker,Wishbone,TrophyTheQueen,TrophySeekerQueen", RunicText.Get("text_bd4cbf5571a4"));
			BackupRoot = Bind(config, "Migration Backups", "RootDirectory", Path.Combine(Paths.ConfigPath, "RunicSafety", "backups"), RunicText.Get("text_b26307eac06d"));
			BackupRetention = Bind(config, "Migration Backups", "RetentionCount", 5, new ConfigDescription(RunicText.Get("text_c3e2c13bb9da"), (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 128), Array.Empty<object>()));
			BackupMaximumFiles = Bind(config, "Migration Backups", "MaximumFiles", 32, new ConfigDescription(RunicText.Get("text_65a8f954ff45"), (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 1024), Array.Empty<object>()));
			BackupMaximumMiB = Bind(config, "Migration Backups", "MaximumTotalMiB", 2048, new ConfigDescription(RunicText.Get("text_8611e5a3c6d4"), (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 65536), Array.Empty<object>()));
			RebuildRarePrefabs();
			config.SettingChanged += OnSettingChanged;
		}

		internal static void Unbind()
		{
			if (_config != null)
			{
				_config.SettingChanged -= OnSettingChanged;
			}
			_config = null;
			lock (Sync)
			{
				_rarePrefabs = new HashSet<string>(StringComparer.Ordinal);
			}
		}

		internal static bool IsRarePrefab(string prefabName)
		{
			if (string.IsNullOrEmpty(prefabName))
			{
				return false;
			}
			lock (Sync)
			{
				return _rarePrefabs.Contains(prefabName);
			}
		}

		internal static string SynchronizedRulesHash()
		{
			string[] value;
			lock (Sync)
			{
				value = new List<string>(_rarePrefabs).OrderBy<string, string>((string result) => result, StringComparer.Ordinal).ToArray();
			}
			string s = string.Join("|", (Enabled?.Value ?? true).ToString(CultureInfo.InvariantCulture), (ConfirmRareSacrifice?.Value ?? true).ToString(CultureInfo.InvariantCulture), (ConfirmOccupiedContainer?.Value ?? true).ToString(CultureInfo.InvariantCulture), (ConfirmVehicleDestruction?.Value ?? true).ToString(CultureInfo.InvariantCulture), (ConfirmPortalOverwrite?.Value ?? true).ToString(CultureInfo.InvariantCulture), (ConfirmationWindowSeconds?.Value ?? 4f).ToString("R", CultureInfo.InvariantCulture), (ProtectedDestinations?.Value ?? true).ToString(CultureInfo.InvariantCulture), (AdministratorBypass?.Value ?? false).ToString(CultureInfo.InvariantCulture), string.Join(",", value));
			using SHA256 sHA = SHA256.Create();
			byte[] array = sHA.ComputeHash(Encoding.UTF8.GetBytes(s));
			StringBuilder stringBuilder = new StringBuilder(array.Length * 2);
			byte[] array2 = array;
			foreach (byte b in array2)
			{
				stringBuilder.Append(b.ToString("x2", CultureInfo.InvariantCulture));
			}
			return stringBuilder.ToString();
		}

		private static ConfigEntry<T> Bind<T>(ConfigFile config, string section, string key, T value, string description)
		{
			//IL_000c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0016: Expected O, but got Unknown
			return Bind(config, section, key, value, new ConfigDescription(description, (AcceptableValueBase)null, Array.Empty<object>()));
		}

		private static ConfigEntry<T> Bind<T>(ConfigFile config, string section, string key, T value, ConfigDescription description)
		{
			return config.Bind<T>(section, key, value, description);
		}

		private static void OnSettingChanged(object sender, EventArgs eventArgs)
		{
			RebuildRarePrefabs();
			SafetyConfig.Changed?.Invoke();
		}

		private static void RebuildRarePrefabs()
		{
			string obj = RarePrefabNames?.Value ?? "DragonEgg,DvergrKey,DvergrKeyFragment,QueenDrop,Sealbreaker,Wishbone,TrophyTheQueen,TrophySeekerQueen";
			HashSet<string> hashSet = new HashSet<string>(StringComparer.Ordinal);
			string[] array = obj.Split(new char[2] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries);
			for (int i = 0; i < array.Length; i++)
			{
				if (hashSet.Count >= 256)
				{
					break;
				}
				string text = array[i].Trim();
				if (text.Length != 0 && text.Length <= 96 && IsSafePrefabName(text))
				{
					hashSet.Add(text);
				}
			}
			lock (Sync)
			{
				_rarePrefabs = hashSet;
			}
		}

		private static bool IsSafePrefabName(string value)
		{
			foreach (char c in value)
			{
				if (!char.IsLetterOrDigit(c) && c != '_' && c != '-')
				{
					return false;
				}
			}
			return true;
		}
	}
}
namespace RunicSafety.Services
{
	public sealed class CompatibilityGate : ICompatibilityGate
	{
		private readonly ISafetyDiagnosticService _diagnostics;

		private Func<bool> _remoteAdmissionAvailable;

		public bool RemoteAdmissionHookAvailable
		{
			get
			{
				try
				{
					return _remoteAdmissionAvailable();
				}
				catch (Exception)
				{
					return false;
				}
			}
		}

		public CompatibilityGate(ISafetyDiagnosticService diagnostics)
		{
			_diagnostics = diagnostics ?? throw new ArgumentNullException("diagnostics");
			_remoteAdmissionAvailable = () => false;
		}

		internal void SetRemoteAdmissionAvailability(Func<bool> available)
		{
			_remoteAdmissionAvailable = available ?? ((Func<bool>)(() => false));
		}

		public CompatibilityDecision Evaluate(CompatibilityIdentity local, CompatibilityIdentity remote, IEnumerable<KnownUnsafeCombination> knownUnsafe = null)
		{
			string correlation = _diagnostics.NewCorrelationId("compat");
			if (!Valid(local) || !Valid(remote))
			{
				return Result(CompatibilityOutcome.BlockedInvalidIdentity, "supply-complete-compatibility-metadata", correlation);
			}
			if (!string.Equals(local.GameVersion, remote.GameVersion, StringComparison.Ordinal))
			{
				return Result(CompatibilityOutcome.BlockedGameVersion, "match-valheim-versions", correlation);
			}
			if (!TryReadMajor(local.ProtocolVersion, out var major) || !TryReadMajor(remote.ProtocolVersion, out var major2))
			{
				return Result(CompatibilityOutcome.BlockedProtocol, "install-compatible-runic-safety-version", correlation);
			}
			if (major != major2)
			{
				return Result(CompatibilityOutcome.BlockedProtocol, "install-compatible-runic-safety-version", correlation);
			}
			if (!string.Equals(local.TopologyHash, remote.TopologyHash, StringComparison.Ordinal))
			{
				return Result(CompatibilityOutcome.BlockedTopology, "match-inventory-slot-topology", correlation);
			}
			if (!string.Equals(local.SynchronizedRulesHash, remote.SynchronizedRulesHash, StringComparison.Ordinal))
			{
				return Result(CompatibilityOutcome.BlockedSynchronizedRules, "match-server-safety-rules", correlation);
			}
			if (knownUnsafe != null)
			{
				int num = 0;
				foreach (KnownUnsafeCombination item in knownUnsafe)
				{
					if (++num > 128)
					{
						return Result(CompatibilityOutcome.BlockedInvalidIdentity, "reduce-known-combination-list", correlation);
					}
					if (item != null)
					{
						bool num2 = Matches(local, remote, item);
						bool flag = Matches(remote, local, item);
						if (num2 || flag)
						{
							return Result(CompatibilityOutcome.BlockedKnownCombination, Bound(item.RemediationCode, "remove-known-unsafe-combination"), correlation);
						}
					}
				}
			}
			return Result(CompatibilityOutcome.Compatible, "none", correlation);
		}

		private CompatibilityDecision Result(CompatibilityOutcome outcome, string remediation, string correlation)
		{
			_diagnostics.Record(correlation, "compatibility", outcome.ToString().ToLowerInvariant(), (outcome != CompatibilityOutcome.Compatible) ? SafetyDiagnosticSeverity.Warning : SafetyDiagnosticSeverity.Information);
			return new CompatibilityDecision(outcome, remediation, correlation);
		}

		private static bool Valid(CompatibilityIdentity identity)
		{
			if (identity == null || string.IsNullOrWhiteSpace(identity.ModuleId) || string.IsNullOrWhiteSpace(identity.SemanticVersion) || string.IsNullOrWhiteSpace(identity.ProtocolVersion) || string.IsNullOrWhiteSpace(identity.GameVersion) || identity.ModuleId.Length > 96 || identity.TopologyHash.Length > 128 || identity.SynchronizedRulesHash.Length > 128)
			{
				return false;
			}
			Version result;
			return Version.TryParse(identity.SemanticVersion.Split(new char[2] { '-', '+' }, 2)[0], out result);
		}

		private static bool Matches(CompatibilityIdentity local, CompatibilityIdentity remote, KnownUnsafeCombination candidate)
		{
			if (string.Equals(local.ModuleId, candidate.LocalModuleId, StringComparison.Ordinal) && string.Equals(local.SemanticVersion, candidate.LocalVersion, StringComparison.Ordinal) && string.Equals(remote.ModuleId, candidate.RemoteModuleId, StringComparison.Ordinal))
			{
				return string.Equals(remote.SemanticVersion, candidate.RemoteVersion, StringComparison.Ordinal);
			}
			return false;
		}

		private static string Bound(string value, string fallback)
		{
			if (string.IsNullOrWhiteSpace(value))
			{
				return fallback;
			}
			string text = value.Trim();
			if (text.Length > 64)
			{
				return text.Substring(0, 64);
			}
			return text;
		}

		private static bool TryReadMajor(string value, out int major)
		{
			major = 0;
			if (string.IsNullOrWhiteSpace(value))
			{
				return false;
			}
			string[] array = value.Trim().Split('.');
			if (array.Length >= 1 && int.TryParse(array[0], out major))
			{
				return major >= 0;
			}
			return false;
		}
	}
	public sealed class ContextualConfirmationService : IContextualConfirmationService
	{
		private readonly struct PendingEntry
		{
			internal string Fingerprint { get; }

			internal DateTime ExpiresUtc { get; }

			internal DateTime CreatedUtc { get; }

			internal PendingEntry(string fingerprint, DateTime expiresUtc, DateTime createdUtc)
			{
				Fingerprint = fingerprint;
				ExpiresUtc = expiresUtc;
				CreatedUtc = createdUtc;
			}
		}

		public const int DefaultCapacity = 128;

		private const int MaximumContextLength = 160;

		private const int MaximumFingerprintLength = 128;

		private readonly object _sync = new object();

		private readonly Dictionary<string, PendingEntry> _pending;

		private readonly ISafetyDiagnosticService _diagnostics;

		public int Capacity { get; }

		public int PendingCount
		{
			get
			{
				lock (_sync)
				{
					return _pending.Count;
				}
			}
		}

		public ContextualConfirmationService(ISafetyDiagnosticService diagnostics, int capacity = 128)
		{
			if (capacity < 8 || capacity > 1024)
			{
				throw new ArgumentOutOfRangeException("capacity");
			}
			_diagnostics = diagnostics ?? throw new ArgumentNullException("diagnostics");
			Capacity = capacity;
			_pending = new Dictionary<string, PendingEntry>(StringComparer.Ordinal);
		}

		public ConfirmationDecision Evaluate(ConfirmationRequest request, DateTime utcNow)
		{
			string correlationId = _diagnostics.NewCorrelationId("confirm");
			if (request == null || string.IsNullOrWhiteSpace(request.ContextKey) || request.ContextKey.Length > 160 || request.StateFingerprint.Length > 128 || request.Window < TimeSpan.FromMilliseconds(250.0) || request.Window > TimeSpan.FromSeconds(30.0))
			{
				_diagnostics.Record(correlationId, "confirmation", "invalid-request", SafetyDiagnosticSeverity.Warning);
				return new ConfirmationDecision(ConfirmationOutcome.Invalid, correlationId);
			}
			if (!request.Enabled)
			{
				_diagnostics.Record(correlationId, "confirmation", "disabled-proceed");
				return new ConfirmationDecision(ConfirmationOutcome.Proceed, correlationId);
			}
			DateTime dateTime = ((utcNow.Kind == DateTimeKind.Utc) ? utcNow : utcNow.ToUniversalTime());
			string key = ((int)request.Action).ToString(CultureInfo.InvariantCulture) + ":" + request.ContextKey;
			lock (_sync)
			{
				RemoveExpiredLocked(dateTime);
				if (_pending.TryGetValue(key, out var value) && value.ExpiresUtc >= dateTime && string.Equals(value.Fingerprint, request.StateFingerprint, StringComparison.Ordinal))
				{
					_pending.Remove(key);
					_diagnostics.Record(correlationId, "confirmation", "confirmed");
					return new ConfirmationDecision(ConfirmationOutcome.Proceed, correlationId);
				}
				if (_pending.Count >= Capacity)
				{
					EvictOldestLocked();
				}
				_pending[key] = new PendingEntry(request.StateFingerprint, dateTime + request.Window, dateTime);
			}
			_diagnostics.Record(correlationId, "confirmation", "repeat-required");
			return new ConfirmationDecision(ConfirmationOutcome.ConfirmAgain, correlationId);
		}

		public void Cancel(string contextKey)
		{
			if (string.IsNullOrEmpty(contextKey))
			{
				return;
			}
			lock (_sync)
			{
				List<string> list = new List<string>();
				foreach (string key in _pending.Keys)
				{
					if (key.EndsWith(":" + contextKey, StringComparison.Ordinal))
					{
						list.Add(key);
					}
				}
				foreach (string item in list)
				{
					_pending.Remove(item);
				}
			}
		}

		public void Clear()
		{
			lock (_sync)
			{
				_pending.Clear();
			}
		}

		private void RemoveExpiredLocked(DateTime now)
		{
			if (_pending.Count == 0)
			{
				return;
			}
			List<string> list = new List<string>();
			foreach (KeyValuePair<string, PendingEntry> item in _pending)
			{
				if (item.Value.ExpiresUtc < now)
				{
					list.Add(item.Key);
				}
			}
			foreach (string item2 in list)
			{
				_pending.Remove(item2);
			}
		}

		private void EvictOldestLocked()
		{
			string text = null;
			DateTime dateTime = DateTime.MaxValue;
			foreach (KeyValuePair<string, PendingEntry> item in _pending)
			{
				if (!(item.Value.CreatedUtc >= dateTime))
				{
					text = item.Key;
					dateTime = item.Value.CreatedUtc;
				}
			}
			if (text != null)
			{
				_pending.Remove(text);
			}
		}
	}
	public sealed class CorrelatedDiagnosticBuffer : ISafetyDiagnosticService
	{
		public const int DefaultCapacity = 256;

		private const int MaximumTokenLength = 64;

		private readonly object _sync = new object();

		private readonly SafetyDiagnosticEvent[] _events;

		private readonly Func<DateTime> _clock;

		private ManualLogSource _log;

		private int _start;

		private int _count;

		private long _sequence;

		public int Capacity => _events.Length;

		public int Count
		{
			get
			{
				lock (_sync)
				{
					return _count;
				}
			}
		}

		public CorrelatedDiagnosticBuffer(int capacity = 256, Func<DateTime> clock = null, ManualLogSource log = null)
		{
			if (capacity < 8 || capacity > 4096)
			{
				throw new ArgumentOutOfRangeException("capacity");
			}
			_events = new SafetyDiagnosticEvent[capacity];
			_clock = clock ?? ((Func<DateTime>)(() => DateTime.UtcNow));
			_log = log;
		}

		internal void SetLog(ManualLogSource log)
		{
			lock (_sync)
			{
				_log = log;
			}
		}

		public string NewCorrelationId(string category)
		{
			string text = Normalize(category, "transaction");
			long num;
			lock (_sync)
			{
				num = NextSequenceLocked();
			}
			return text + "-" + num.ToString("x16", CultureInfo.InvariantCulture);
		}

		public void Record(string correlationId, string category, string code, SafetyDiagnosticSeverity severity = SafetyDiagnosticSeverity.Information)
		{
			string correlationId2 = Normalize(correlationId, "uncorrelated");
			string category2 = Normalize(category, "unknown");
			string code2 = Normalize(code, "unspecified");
			SafetyDiagnosticEvent safetyDiagnosticEvent;
			ManualLogSource log;
			lock (_sync)
			{
				safetyDiagnosticEvent = new SafetyDiagnosticEvent(NextSequenceLocked(), _clock().ToUniversalTime(), correlationId2, category2, code2, severity);
				int num = (_start + _count) % _events.Length;
				if (_count == _events.Length)
				{
					num = _start;
					_start = (_start + 1) % _events.Length;
				}
				else
				{
					_count++;
				}
				_events[num] = safetyDiagnosticEvent;
				log = _log;
			}
			if (log != null)
			{
				string text = "[" + safetyDiagnosticEvent.CorrelationId + "] " + safetyDiagnosticEvent.Category + "/" + safetyDiagnosticEvent.Code;
				switch (severity)
				{
				case SafetyDiagnosticSeverity.Error:
					log.LogError((object)text);
					break;
				case SafetyDiagnosticSeverity.Warning:
					log.LogWarning((object)text);
					break;
				default:
					log.LogInfo((object)text);
					break;
				}
			}
		}

		public IReadOnlyList<SafetyDiagnosticEvent> Snapshot()
		{
			lock (_sync)
			{
				List<SafetyDiagnosticEvent> list = new List<SafetyDiagnosticEvent>(_count);
				for (int i = 0; i < _count; i++)
				{
					list.Add(_events[(_start + i) % _events.Length]);
				}
				return list.AsReadOnly();
			}
		}

		private long NextSequenceLocked()
		{
			if (_sequence == long.MaxValue)
			{
				_sequence = 0L;
			}
			return ++_sequence;
		}

		private static string Normalize(string value, string fallback)
		{
			if (string.IsNullOrWhiteSpace(value))
			{
				return fallback;
			}
			string text = value.Trim();
			int num = Math.Min(text.Length, 64);
			char[] array = new char[num];
			for (int i = 0; i < num; i++)
			{
				char c = text[i];
				array[i] = ((char.IsLetterOrDigit(c) || c == '-' || c == '_' || c == '.') ? c : '_');
			}
			return new string(array);
		}
	}
	internal static class InventoryProtectionAdapter
	{
		private const string PluginGuid = "chazman.RunicInventory";

		private const string ApiTypeName = "RunicInventory.Api.InventoryIntegrationApi";

		private static MethodInfo _method;

		private static MethodInfo _useMethod;

		private static object[] _arguments;

		internal static ItemLockState Resolve(object nativeItem, out bool integrationPresent)
		{
			return Resolve(nativeItem, ProtectionDestination.UnsupportedDestructivePath, out integrationPresent);
		}

		internal static ItemLockState Resolve(object nativeItem, ProtectionDestination destination, out bool integrationPresent)
		{
			integrationPresent = Chainloader.PluginInfos.ContainsKey("chazman.RunicInventory");
			if (!integrationPresent)
			{
				return ItemLockState.NotApplicable;
			}
			if (nativeItem == null || !TryResolveMethod())
			{
				return ItemLockState.Unknown;
			}
			try
			{
				_arguments[0] = nativeItem;
				_arguments[1] = 0;
				object obj = ((AllowsNativeUse(destination) && _useMethod != null) ? _useMethod : _method).Invoke(null, _arguments);
				bool flag = default(bool);
				int num;
				if (obj is bool)
				{
					flag = (bool)obj;
					num = 1;
				}
				else
				{
					num = 0;
				}
				int num2 = num & (flag ? 1 : 0);
				int num3 = ((_arguments[1] is int num4) ? num4 : 0);
				if (num2 == 0)
				{
					return ItemLockState.NotApplicable;
				}
				int result;
				switch (num3)
				{
				case 1:
					return ItemLockState.Unlocked;
				default:
					result = 3;
					break;
				case 2:
					result = 2;
					break;
				}
				return (ItemLockState)result;
			}
			catch
			{
				_method = null;
				_useMethod = null;
				_arguments = null;
				return ItemLockState.Unknown;
			}
			finally
			{
				if (_arguments != null)
				{
					_arguments[0] = null;
					_arguments[1] = 0;
				}
			}
		}

		private static bool TryResolveMethod()
		{
			if (_method != null)
			{
				return true;
			}
			if (!Chainloader.PluginInfos.TryGetValue("chazman.RunicInventory", out var value))
			{
				return false;
			}
			Type? api = ((value == null) ? null : ((object)value.Instance)?.GetType().Assembly.GetType("RunicInventory.Api.InventoryIntegrationApi", throwOnError: false));
			_method = ResolveMethod(api);
			_useMethod = ResolveUseMethod(api);
			if (_method == null)
			{
				return false;
			}
			_arguments = new object[2] { null, 0 };
			return true;
		}

		internal static MethodInfo ResolveMethod(Type api)
		{
			return ResolveNamedMethod(api, "TryGetProtection");
		}

		internal static MethodInfo ResolveUseMethod(Type api)
		{
			return ResolveNamedMethod(api, "TryGetUseProtection");
		}

		internal static bool AllowsNativeUse(ProtectionDestination destination)
		{
			if (destination != ProtectionDestination.SmelterInput && destination != ProtectionDestination.SmelterFuel && destination != ProtectionDestination.CookingStation && destination != ProtectionDestination.CookingFuel)
			{
				return destination == ProtectionDestination.Fermenter;
			}
			return true;
		}

		private static MethodInfo ResolveNamedMethod(Type api, string name)
		{
			MethodInfo methodInfo = api?.GetMethod(name, BindingFlags.Static | BindingFlags.Public, null, new Type[2]
			{
				typeof(object),
				typeof(int).MakeByRefType()
			}, null);
			if (!(methodInfo?.ReturnType == typeof(bool)))
			{
				return null;
			}
			return methodInfo;
		}
	}
	public sealed class MigrationBackupService : IMigrationBackupService
	{
		private readonly struct SourceState
		{
			internal string Path { get; }

			internal string LogicalName { get; }

			internal BackupFileMetadata Metadata { get; }

			internal SourceState(string path, string logicalName, BackupFileMetadata metadata)
			{
				Path = path;
				LogicalName = logicalName;
				Metadata = metadata;
			}
		}

		private readonly struct DirectorySourceState
		{
			internal string Path { get; }

			internal IReadOnlyList<string> Members { get; }

			internal DirectorySourceState(string path, IReadOnlyList<string> members)
			{
				Path = path;
				Members = members;
			}
		}

		private sealed class BackupSourceChangedException : IOException
		{
		}

		private readonly struct BackupDirectory
		{
			internal string Path { get; }

			internal DateTime CreatedUtc { get; }

			internal BackupDirectory(string path, DateTime createdUtc)
			{
				Path = path;
				CreatedUtc = createdUtc;
			}
		}

		private sealed class RootLease : IDisposable
		{
			private string _root;

			internal RootLease(string root)
			{
				_root = root;
			}

			public void Dispose()
			{
				ReleaseRoot(Interlocked.Exchange(ref _root, null));
			}
		}

		internal const string MarkerFileName = ".runicsafety-backup";

		internal const string ManifestFileName = "manifest.runic";

		internal const string RestoreFileName = "RESTORE.txt";

		private const string MarkerText = "RUNIC_SAFETY_BACKUP_V1";

		private const int BufferSize = 81920;

		private const int AbsoluteMaximumFiles = 1024;

		private const long AbsoluteMaximumBytes = 68719476736L;

		private const long MaximumManifestBytes = 16777216L;

		private const long MaximumRestoreBytes = 1048576L;

		private const long MaximumMarkerBytes = 128L;

		private const int MaximumConcurrentBackupRoots = 128;

		private const int AbsoluteMaximumSourceDirectories = 4096;

		private static readonly object RootGateSync = new object();

		private static readonly StringComparer RootPathComparer = ((Path.DirectorySeparatorChar == '\\') ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal);

		private static readonly StringComparison RootPathComparison = ((Path.DirectorySeparatorChar == '\\') ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);

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

		private static readonly HashSet<string> ActiveBackupRoots = new HashSet<string>(RootPathComparer);

		private readonly IBackupStorage _storage;

		private readonly ISafetyDiagnosticService _diagnostics;

		private readonly Func<DateTime> _clock;

		private readonly Func<MigrationBackupDefaults> _defaults;

		public string DefaultDestinationRoot => _defaults().DestinationRoot;

		public int DefaultRetentionCount => _defaults().RetentionCount;

		public int DefaultMaximumFiles => _defaults().MaximumFiles;

		public long DefaultMaximumTotalBytes => _defaults().MaximumTotalBytes;

		public MigrationBackupService(ISafetyDiagnosticService diagnostics)
		{
			checked
			{
				this..ctor(new PhysicalBackupStorage(), diagnostics, () => DateTime.UtcNow, () => new MigrationBackupDefaults(SafetyConfig.BackupRoot?.Value ?? Path.Combine(Paths.ConfigPath, "RunicSafety", "backups"), SafetyConfig.BackupRetention?.Value ?? 5, SafetyConfig.BackupMaximumFiles?.Value ?? 32, unchecked((long)(SafetyConfig.BackupMaximumMiB?.Value ?? 2048)) * 1024L * 1024));
			}
		}

		internal MigrationBackupService(IBackupStorage storage, ISafetyDiagnosticService diagnostics, Func<DateTime> clock, Func<MigrationBackupDefaults> defaults = null)
		{
			_storage = storage ?? throw new ArgumentNullException("storage");
			_diagnostics = diagnostics ?? throw new ArgumentNullException("diagnostics");
			_clock = clock ?? throw new ArgumentNullException("clock");
			_defaults = defaults ?? ((Func<MigrationBackupDefaults>)(() => new MigrationBackupDefaults(Path.Combine(Path.GetTempPath(), "RunicSafety", "backups"), 5, 32, 2147483648L)));
		}

		public MigrationBackupRequest CreateDefaultRequest(string migrationId, IEnumerable<MigrationBackupSource> sources)
		{
			MigrationBackupDefaults migrationBackupDefaults = _defaults();
			return new MigrationBackupRequest(migrationId, sources, migrationBackupDefaults.DestinationRoot, migrationBackupDefaults.RetentionCount, migrationBackupDefaults.MaximumTotalBytes, migrationBackupDefaults.MaximumFiles);
		}

		public MigrationBackupResult CreateBackup(MigrationBackupRequest request, CancellationToken cancellationToken)
		{
			string correlation = _diagnostics.NewCorrelationId("backup");
			if (!TryAcquireRootLease(request, correlation, out var canonicalRoot, out var lease, out var failure))
			{
				return failure;
			}
			using (lease)
			{
				return CreateBackupUnderLease(request, cancellationToken, correlation, canonicalRoot);
			}
		}

		private MigrationBackupResult CreateBackupUnderLease(MigrationBackupRequest request, CancellationToken cancellationToken, string correlation, string canonicalRoot)
		{
			string root = string.Empty;
			string text = string.Empty;
			List<MigrationBackupFile> list = new List<MigrationBackupFile>();
			try
			{
				if (!TryValidateRequest(request, out root, out var outcome, out var failureCode))
				{
					return Failure(outcome, correlation, failureCode, list);
				}
				if (!SameRoot(root, canonicalRoot))
				{
					return Failure(MigrationBackupOutcome.InvalidRequest, correlation, "backup-root-changed", list);
				}
				cancellationToken.ThrowIfCancellationRequested();
				List<SourceState> list2 = new List<SourceState>(request.Sources.Count);
				List<DirectorySourceState> list3 = new List<DirectorySourceState>();
				long totalBytes = 0L;
				HashSet<string> sourcePaths = new HashSet<string>(RootPathComparer);
				for (int i = 0; i < request.Sources.Count; i++)
				{
					cancellationToken.ThrowIfCancellationRequested();
					MigrationBackupSource migrationBackupSource = request.Sources[i];
					if (migrationBackupSource == null || string.IsNullOrWhiteSpace(migrationBackupSource.SourcePath))
					{
						return Failure(MigrationBackupOutcome.InvalidRequest, correlation, "source-path-required", list);
					}
					string fullPath = _storage.GetFullPath(migrationBackupSource.SourcePath);
					if (fullPath.Length > 4096)
					{
						return Failure(MigrationBackupOutcome.InvalidRequest, correlation, "source-path-too-long", list);
					}
					string text2 = BoundLogicalName(migrationBackupSource.LogicalName, i);
					if (_storage.FileExists(fullPath))
					{
						if (!TryAddSourceFile(fullPath, text2, request, sourcePaths, list2, ref totalBytes, out var outcome2, out var failureCode2))
						{
							return Failure(outcome2, correlation, failureCode2, list);
						}
						continue;
					}
					if (_storage.DirectoryExists(fullPath))
					{
						if (_storage.IsDirectoryReparsePoint(fullPath))
						{
							return Failure(MigrationBackupOutcome.InvalidRequest, correlation, "reparse-source-directory-refused", list);
						}
						if (PathsOverlap(root, fullPath))
						{
							return Failure(MigrationBackupOutcome.InvalidRequest, correlation, "source-directory-overlaps-backup-root", list);
						}
						if (!TryEnumerateDirectory(fullPath, request.MaximumFiles, cancellationToken, out var members, out var outcome3, out var failureCode3))
						{
							return Failure(outcome3, correlation, failureCode3, list);
						}
						if (members.Count == 0)
						{
							return Failure(MigrationBackupOutcome.InvalidRequest, correlation, "source-directory-empty", list);
						}
						for (int j = 0; j < members.Count; j++)
						{
							string path = members[j];
							string text3 = Path.GetRelativePath(fullPath, path).Replace(Path.DirectorySeparatorChar, '/');
							string logicalName = BoundLogicalName(text2 + "/" + text3, i);
							if (!TryAddSourceFile(path, logicalName, request, sourcePaths, list2, ref totalBytes, out var outcome4, out var failureCode4))
							{
								return Failure(outcome4, correlation, failureCode4, list);
							}
						}
						list3.Add(new DirectorySourceState(fullPath, members));
						continue;
					}
					return Failure(MigrationBackupOutcome.SourceMissing, correlation, "source-missing", list);
				}
				_storage.CreateDirectory(root);
				if (_storage.IsDirectoryReparsePoint(root))
				{
					return Failure(MigrationBackupOutcome.InvalidRequest, correlation, "reparse-backup-root-refused", list);
				}
				string text4 = _clock().ToUniversalTime().ToString("yyyyMMddTHHmmssfffffffZ", CultureInfo.InvariantCulture) + "-" + ShortCorrelation(correlation);
				text = _storage.Combine(root, ".partial-" + text4);
				string text5 = _storage.Combine(root, "backup-" + text4);
				if (_storage.DirectoryExists(text) || _storage.DirectoryExists(text5))
				{
					return Failure(MigrationBackupOutcome.IoFailure, correlation, "backup-name-collision", list);
				}
				_storage.CreateDirectory(text);
				for (int k = 0; k < list2.Count; k++)
				{
					cancellationToken.ThrowIfCancellationRequested();
					SourceState sourceState = list2[k];
					string text6 = CreateBackupName(k, sourceState.Path);
					string text7 = _storage.Combine(text, text6);
					string sha;
					try
					{
						sha = CopyAndHash(sourceState.Path, text7, sourceState.Metadata.Length, cancellationToken);
					}
					catch (BackupSourceChangedException)
					{
						return FailureWithCleanup(MigrationBackupOutcome.SourceChangedDuringCopy, correlation, "source-grew-during-copy", list, root, text);
					}
					BackupFileMetadata fileMetadata = _storage.GetFileMetadata(sourceState.Path);
					BackupFileMetadata fileMetadata2 = _storage.GetFileMetadata(text7);
					if (!sourceState.Metadata.StableEquals(fileMetadata) || fileMetadata2.Length != sourceState.Metadata.Length)
					{
						return FailureWithCleanup(MigrationBackupOutcome.SourceChangedDuringCopy, correlation, "source-changed-during-copy", list, root, text);
					}
					list.Add(new MigrationBackupFile(sourceState.LogicalName, sourceState.Path, text6, sourceState.Metadata.Length, sha));
				}
				for (int l = 0; l < list3.Count; l++)
				{
					cancellationToken.ThrowIfCancellationRequested();
					DirectorySourceState directorySourceState = list3[l];
					if (!TryEnumerateDirectory(directorySourceState.Path, request.MaximumFiles, cancellationToken, out var members2, out var _, out var _) || !SamePaths(directorySourceState.Members, members2))
					{
						return FailureWithCleanup(MigrationBackupOutcome.SourceChangedDuringCopy, correlation, "source-directory-changed-during-copy", list, root, text);
					}
				}
				for (int m = 0; m < list2.Count; m++)
				{
					cancellationToken.ThrowIfCancellationRequested();
					SourceState sourceState2 = list2[m];
					if (!sourceState2.Metadata.StableEquals(_storage.GetFileMetadata(sourceState2.Path)))
					{
						return FailureWithCleanup(MigrationBackupOutcome.SourceChangedDuringCopy, correlation, "source-changed-during-copy", list, root, text);
					}
				}
				cancellationToken.ThrowIfCancellationRequested();
				WriteManifest(text, request.MigrationId, correlation, list);
				WriteRestoreInstructions(text);
				_storage.WriteAllTextNew(_storage.Combine(text, ".runicsafety-backup"), "RUNIC_SAFETY_BACKUP_V1" + Environment.NewLine);
				_storage.MoveDirectory(text, text5);
				text = string.Empty;
				if (!ValidateBackupInternal(text5, request.MigrationId, correlation, list, out var failureCode6))
				{
					_diagnostics.Record(correlation, "migration-backup", "post-commit-validation-failed", SafetyDiagnosticSeverity.Error);
					return new MigrationBackupResult(MigrationBackupOutcome.IoFailure, text5, correlation, "post-commit-validation-failed-" + BoundCode(failureCode6), list.AsReadOnly());
				}
				try
				{
					ApplyRetention(root, text5, request.RetentionCount, list2);
				}
				catch (Exception)
				{
					_diagnostics.Record(correlation, "migration-backup", "retention-failed", SafetyDiagnosticSeverity.Warning);
				}
				MigrationBackupResult result = new MigrationBackupResult(MigrationBackupOutcome.Succeeded, text5, correlation, string.Empty, list.AsReadOnly());
				_diagnostics.Record(correlation, "migration-backup", "committed");
				return result;
			}
			catch (OperationCanceledException)
			{
				CleanupPartial(root, text);
				return Failure(MigrationBackupOutcome.Cancelled, correlation, "cancelled", list);
			}
			catch (Exception)
			{
				CleanupPartial(root, text);
				return Failure(MigrationBackupOutcome.IoFailure, correlation, "io-failure", list);
			}
		}

		public MigrationExecutionResult ExecuteAfterBackup(MigrationBackupRequest request, Action mutation, CancellationToken cancellationToken)
		{
			string correlation = _diagnostics.NewCorrelationId("backup");
			if (mutation == null)
			{
				return new MigrationExecutionResult(Failure(MigrationBackupOutcome.InvalidRequest, correlation, "mutation-required", new List<MigrationBackupFile>()), mutationInvoked: false, null);
			}
			if (!TryAcquireRootLease(request, correlation, out var canonicalRoot, out var lease, out var failure))
			{
				return new MigrationExecutionResult(failure, mutationInvoked: false, null);
			}
			using (lease)
			{
				MigrationBackupResult migrationBackupResult = CreateBackupUnderLease(request, cancellationToken, correlation, canonicalRoot);
				if (!migrationBackupResult.Succeeded)
				{
					return new MigrationExecutionResult(migrationBackupResult, mutationInvoked: false, null);
				}
				if (cancellationToken.IsCancellationRequested)
				{
					return new MigrationExecutionResult(CommittedFailure(migrationBackupResult, MigrationBackupOutcome.Cancelled, "cancelled-before-mutation"), mutationInvoked: false, null);
				}
				List<Stream> list = new List<Stream>(migrationBackupResult.Files.Count + 3);
				try
				{
					string fullPath = _storage.GetFullPath(migrationBackupResult.BackupDirectory);
					if (!IsDirectChild(canonicalRoot, fullPath))
					{
						return new MigrationExecutionResult(CommittedFailure(migrationBackupResult, MigrationBackupOutcome.IoFailure, "backup-root-proof-failed"), mutationInvoked: false, null);
					}
					ProtectFile(list, _storage.Combine(fullPath, ".runicsafety-backup"));
					ProtectFile(list, _storage.Combine(fullPath, "manifest.runic"));
					ProtectFile(list, _storage.Combine(fullPath, "RESTORE.txt"));
					for (int i = 0; i < migrationBackupResult.Files.Count; i++)
					{
						MigrationBackupFile migrationBackupFile = migrationBackupResult.Files[i];
						if (migrationBackupFile == null || !IsSimpleFileName(migrationBackupFile.BackupFileName))
						{
							throw new InvalidDataException("Backup result contains an unsafe filename.");
						}
						ProtectFile(list, _storage.Combine(fullPath, migrationBackupFile.BackupFileName));
					}
					string failureCode = "marker-unavailable";
					if (!ValidateBackupInternal(fullPath, request.MigrationId, migrationBackupResult.CorrelationId, migrationBackupResult.Files, out failureCode))
					{
						return new MigrationExecutionResult(CommittedFailure(migrationBackupResult, MigrationBackupOutcome.IoFailure, "pre-mutation-validation-failed-" + BoundCode(failureCode)), mutationInvoked: false, null);
					}
					if (cancellationToken.IsCancellationRequested)
					{
						return new MigrationExecutionResult(CommittedFailure(migrationBackupResult, MigrationBackupOutcome.Cancelled, "cancelled-before-mutation"), mutationInvoked: false, null);
					}
					try
					{
						mutation();
						_diagnostics.Record(migrationBackupResult.CorrelationId, "migration", "mutation-completed");
						return new MigrationExecutionResult(migrationBackupResult, mutationInvoked: true, null);
					}
					catch (Exception mutationFailure)
					{
						_diagnostics.Record(migrationBackupResult.CorrelationId, "migration", "mutation-threw", SafetyDiagnosticSeverity.Error);
						return new MigrationExecutionResult(migrationBackupResult, mutationInvoked: true, mutationFailure);
					}
				}
				catch (Exception)
				{
					return new MigrationExecutionResult(CommittedFailure(migrationBackupResult, MigrationBackupOutcome.IoFailure, "pre-mutation-validation-io-failure"), mutationInvoked: false, null);
				}
				finally
				{
					for (int num = list.Count - 1; num >= 0; num--)
					{
						list[num]?.Dispose();
					}
				}
			}
		}

		public bool ValidateBackup(string backupDirectory, out string failureCode)
		{
			return ValidateBackupInternal(backupDirectory, null, null, null, out failureCode);
		}

		private bool ValidateBackupInternal(string backupDirectory, string expectedMigrationId, string expectedCorrelationId, IReadOnlyList<MigrationBackupFile> expectedFiles, out string failureCode)
		{
			failureCode = string.Empty;
			try
			{
				if (string.IsNullOrWhiteSpace(backupDirectory))
				{
					failureCode = "backup-directory-required";
					return false;
				}
				string fullPath = _storage.GetFullPath(backupDirectory);
				if (!_storage.DirectoryExists(fullPath) || _storage.IsDirectoryReparsePoint(fullPath) || !_storage.FileExists(_storage.Combine(fullPath, ".runicsafety-backup")) || !_storage.FileExists(_storage.Combine(fullPath, "manifest.runic")) || !_storage.FileExists(_storage.Combine(fullPath, "RESTORE.txt")))
				{
					failureCode = "backup-incomplete";
					return false;
				}
				BackupFileMetadata fileMetadata = _storage.GetFileMetadata(_storage.Combine(fullPath, ".runicsafety-backup"));
				BackupFileMetadata fileMetadata2 = _storage.GetFileMetadata(_storage.Combine(fullPath, "manifest.runic"));
				BackupFileMetadata fileMetadata3 = _storage.GetFileMetadata(_storage.Combine(fullPath, "RESTORE.txt"));
				if (fileMetadata.IsReparsePoint || fileMetadata2.IsReparsePoint || fileMetadata3.IsReparsePoint || fileMetadata.Length > 128 || fileMetadata2.Length > 16777216 || fileMetadata3.Length > 1048576)
				{
					failureCode = "backup-metadata-file-limit";
					return false;
				}
				if (!string.Equals(ReadBoundedUtf8(_storage.Combine(fullPath, ".runicsafety-backup"), 128L).Trim(), "RUNIC_SAFETY_BACKUP_V1", StringComparison.Ordinal))
				{
					failureCode = "marker-invalid";
					return false;
				}
				if (!ReadBoundedUtf8(_storage.Combine(fullPath, "RESTORE.txt"), 1048576L).StartsWith("Runic Safety migration backup", StringComparison.Ordinal))
				{
					failureCode = "restore-instructions-invalid";
					return false;
				}
				string[] array = ReadBoundedUtf8Lines(_storage.Combine(fullPath, "manifest.runic"), 16777216L, 1040);
				if (array.Length < 4 || array.Length > 1040 || !string.Equals(array[0], "RUNIC_SAFETY_BACKUP_V1", StringComparison.Ordinal) || !array[1].StartsWith("MIGRATION\t", StringComparison.Ordinal) || !array[2].StartsWith("CORRELATION\t", StringComparison.Ordinal) || !array[3].StartsWith("CREATED_UTC\t", StringComparison.Ordinal))
				{
					failureCode = "manifest-invalid";
					return false;
				}
				string[] array2 = array[1].Split('\t');
				string[] array3 = array[2].Split('\t');
				string[] array4 = array[3].Split('\t');
				if (array2.Length != 2 || array3.Length != 2 || array4.Length != 2 || array2[1].Length > 256 || array3[1].Length > 256 || array4[1].Length > 64)
				{
					failureCode = "manifest-metadata-invalid";
					return false;
				}
				string text = Decode(array2[1]);
				string text2 = Decode(array3[1]);
				if (string.IsNullOrWhiteSpace(text) || text.Length > 96 || string.IsNullOrWhiteSpace(text2) || text2.Length > 128 || (expectedMigrationId != null && !string.Equals(text, expectedMigrationId, StringComparison.Ordinal)) || (expectedCorrelationId != null && !string.Equals(text2, expectedCorrelationId, StringComparison.Ordinal)) || !DateTime.TryParseExact(array4[1], "O", CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var _))
				{
					failureCode = "manifest-metadata-invalid";
					return false;
				}
				int num = 0;
				long num2 = 0L;
				HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
				Dictionary<string, MigrationBackupFile> dictionary = null;
				if (expectedFiles != null)
				{
					if (expectedFiles.Count < 1 || expectedFiles.Count > 1024)
					{
						failureCode = "expected-file-set-invalid";
						return false;
					}
					dictionary = new Dictionary<string, MigrationBackupFile>(expectedFiles.Count, StringComparer.OrdinalIgnoreCase);
					for (int i = 0; i < expectedFiles.Count; i++)
					{
						MigrationBackupFile migrationBackupFile = expectedFiles[i];
						if (migrationBackupFile == null || !IsSimpleFileName(migrationBackupFile.BackupFileName) || migrationBackupFile.Length < 0 || !IsSha256(migrationBackupFile.Sha256) || !dictionary.TryAdd(migrationBackupFile.BackupFileName, migrationBackupFile))
						{
							failureCode = "expected-file-set-invalid";
							return false;
						}
					}
				}
				for (int j = 4; j < array.Length; j++)
				{
					string text3 = array[j];
					if (!text3.StartsWith("FILE\t", StringComparison.Ordinal))
					{
						failureCode = "manifest-entry-invalid";
						return false;
					}
					if (++num > 1024)
					{
						failureCode = "manifest-file-limit";
						return false;
					}
					string[] array5 = text3.Split('\t');
					if (array5.Length != 6 || !long.TryParse(array5[4], NumberStyles.None, CultureInfo.InvariantCulture, out var result2) || result2 < 0 || array5[1].Length > 256 || array5[2].Length > 65536 || array5[3].Length > 256 || array5[5].Length != 64)
					{
						failureCode = "manifest-entry-invalid";
						return false;
					}
					string text4 = Decode(array5[1]);
					string text5 = Decode(array5[2]);
					string text6 = Decode(array5[3]);
					if (string.IsNullOrWhiteSpace(text4) || text4.Length > 96 || string.IsNullOrWhiteSpace(text5) || text5.Length > 4096 || !Path.IsPathRooted(text5) || !IsSimpleFileName(text6) || !hashSet.Add(text6) || !IsSha256(array5[5]))
					{
						failureCode = "manifest-path-invalid";
						return false;
					}
					if (dictionary != null && (!dictionary.TryGetValue(text6, out var value) || !string.Equals(value.LogicalName, text4, StringComparison.Ordinal) || !string.Equals(value.OriginalPath, text5, StringComparison.Ordinal) || value.Length != result2 || !string.Equals(value.Sha256, array5[5], StringComparison.OrdinalIgnoreCase)))
					{
						failureCode = "backup-file-set-changed";
						return false;
					}
					string path = _storage.Combine(fullPath, text6);
					if (!_storage.FileExists(path))
					{
						failureCode = "backup-file-missing-or-sized-wrong";
						return false;
					}
					BackupFileMetadata fileMetadata4 = _storage.GetFileMetadata(path);
					if (fileMetadata4.IsReparsePoint || fileMetadata4.Length != result2)
					{
						failureCode = "backup-file-missing-or-sized-wrong";
						return false;
					}
					try
					{
						num2 = checked(num2 + result2);
					}
					catch (OverflowException)
					{
						failureCode = "manifest-size-overflow";
						return false;
					}
					if (num2 > 68719476736L)
					{
						failureCode = "manifest-size-limit";
						return false;
					}
					if (!string.Equals(HashFile(path, result2, CancellationToken.None), array5[5], StringComparison.OrdinalIgnoreCase))
					{
						failureCode = "backup-hash-mismatch";
						return false;
					}
				}
				if (num == 0 || (dictionary != null && num != dictionary.Count))
				{
					failureCode = ((num == 0) ? "manifest-has-no-files" : "backup-file-set-changed");
					return false;
				}
				return true;
			}
			catch (Exception)
			{
				failureCode = "backup-validation-io-failure";
				return false;
			}
		}

		private bool TryValidateRequest(MigrationBackupRequest request, out string root, out MigrationBackupOutcome outcome, out string failureCode)
		{
			root = string.Empty;
			outcome = MigrationBackupOutcome.InvalidRequest;
			failureCode = "invalid-request";
			if (request == null || string.IsNullOrWhiteSpace(request.MigrationId) || request.MigrationId.Length > 96 || request.Sources == null || request.Sources.Count == 0 || string.IsNullOrWhiteSpace(request.DestinationRoot) || request.RetentionCount < 1 || request.RetentionCount > 128 || request.MaximumFiles < 1 || request.MaximumFiles > 1024 || request.MaximumTotalBytes < 1 || request.MaximumTotalBytes > 68719476736L)
			{
				return false;
			}
			if (request.Sources.Count > request.MaximumFiles)
			{
				outcome = MigrationBackupOutcome.FileLimitExceeded;
				failureCode = "configured-file-limit";
				return false;
			}
			root = _storage.GetFullPath(request.DestinationRoot);
			if (string.IsNullOrWhiteSpace(root))
			{
				return false;
			}
			return true;
		}

		private bool TryAddSourceFile(string path, string logicalName, MigrationBackupRequest request, HashSet<string> sourcePaths, List<SourceState> sources, ref long totalBytes, out MigrationBackupOutcome outcome, out string failureCode)
		{
			outcome = MigrationBackupOutcome.InvalidRequest;
			failureCode = "invalid-source";
			string fullPath = _storage.GetFullPath(path);
			if (fullPath.Length > 4096)
			{
				failureCode = "source-path-too-long";
				return false;
			}
			if (!_storage.FileExists(fullPath))
			{
				outcome = MigrationBackupOutcome.SourceMissing;
				failureCode = "source-missing";
				return false;
			}
			if (!sourcePaths.Add(fullPath))
			{
				failureCode = "duplicate-source";
				return false;
			}
			if (sources.Count >= request.MaximumFiles)
			{
				outcome = MigrationBackupOutcome.FileLimitExceeded;
				failureCode = "configured-file-limit";
				return false;
			}
			BackupFileMetadata fileMetadata = _storage.GetFileMetadata(fullPath);
			if (fileMetadata.IsReparsePoint)
			{
				failureCode = "reparse-source-refused";
				return false;
			}
			checked
			{
				try
				{
					totalBytes += fileMetadata.Length;
				}
				catch (OverflowException)
				{
					outcome = MigrationBackupOutcome.SizeLimitExceeded;
					failureCode = "source-size-overflow";
					return false;
				}
				if (totalBytes > request.MaximumTotalBytes)
				{
					outcome = MigrationBackupOutcome.SizeLimitExceeded;
					failureCode = "configured-size-limit";
					return false;
				}
				sources.Add(new SourceState(fullPath, logicalName, fileMetadata));
				failureCode = string.Empty;
				return true;
			}
		}

		private bool TryEnumerateDirectory(string sourceRoot, int maximumFiles, CancellationToken cancellationToken, out IReadOnlyList<string> members, out MigrationBackupOutcome outcome, out string failureCode)
		{
			members = Array.Empty<string>();
			outcome = MigrationBackupOutcome.InvalidRequest;
			failureCode = "source-directory-invalid";
			string fullPath = _storage.GetFullPath(sourceRoot);
			if (!_storage.DirectoryExists(fullPath))
			{
				outcome = MigrationBackupOutcome.SourceMissing;
				failureCode = "source-directory-missing";
				return false;
			}
			if (_storage.IsDirectoryReparsePoint(fullPath))
			{
				failureCode = "reparse-source-directory-refused";
				return false;
			}
			List<string> list = new List<string> { fullPath };
			HashSet<string> hashSet = new HashSet<string>(RootPathComparer) { fullPath };
			List<string> list2 = new List<string>();
			for (int i = 0; i < list.Count; i++)
			{
				cancellationToken.ThrowIfCancellationRequested();
				string text = list[i];
				if (!_storage.DirectoryExists(text) || _storage.IsDirectoryReparsePoint(text) || !IsSameOrNested(fullPath, text))
				{
					failureCode = "source-directory-tree-invalid";
					return false;
				}
				string[] array = _storage.GetFiles(text, "*") ?? Array.Empty<string>();
				Array.Sort(array, (IComparer<string>?)RootPathComparer);
				for (int j = 0; j < array.Length; j++)
				{
					cancellationToken.ThrowIfCancellationRequested();
					string fullPath2 = _storage.GetFullPath(array[j]);
					if (fullPath2.Length > 4096 || !IsNested(fullPath, fullPath2) || !_storage.FileExists(fullPath2))
					{
						failureCode = "source-directory-member-invalid";
						return false;
					}
					if (list2.Count >= maximumFiles || list2.Count >= 1024)
					{
						outcome = MigrationBackupOutcome.FileLimitExceeded;
						failureCode = "configured-file-limit";
						return false;
					}
					list2.Add(fullPath2);
				}
				string[] array2 = _storage.GetDirectories(text, "*") ?? Array.Empty<string>();
				Array.Sort(array2, (IComparer<string>?)RootPathComparer);
				for (int k = 0; k < array2.Length; k++)
				{
					cancellationToken.ThrowIfCancellationRequested();
					string fullPath3 = _storage.GetFullPath(array2[k]);
					if (fullPath3.Length > 4096 || !IsNested(fullPath, fullPath3) || !_storage.DirectoryExists(fullPath3) || _storage.IsDirectoryReparsePoint(fullPath3) || !hashSet.Add(fullPath3))
					{
						failureCode = "source-directory-tree-invalid";
						return false;
					}
					if (list.Count >= 4096)
					{
						outcome = MigrationBackupOutcome.FileLimitExceeded;
						failureCode = "source-directory-count-limit";
						return false;
					}
					list.Add(fullPath3);
				}
			}
			list2.Sort(RootPathComparer);
			members = list2.AsReadOnly();
			failureCode = string.Empty;
			return true;
		}

		private static bool SamePaths(IReadOnlyList<string> expected, IReadOnlyList<string> actual)
		{
			if (expected == null || actual == null || expected.Count != actual.Count)
			{
				return false;
			}
			for (int i = 0; i < expected.Count; i++)
			{
				if (!RootPathComparer.Equals(expected[i], actual[i]))
				{
					return false;
				}
			}
			return true;
		}

		private static bool PathsOverlap(string left, string right)
		{
			if (!SameRoot(left, right) && !IsNested(left, right))
			{
				return IsNested(right, left);
			}
			return true;
		}

		private static bool IsSameOrNested(string parent, string candidate)
		{
			if (!SameRoot(parent, candidate))
			{
				return IsNested(parent, candidate);
			}
			return true;
		}

		private static bool IsNested(string parent, string candidate)
		{
			string value = EnsureTrailingSeparator(Path.GetFullPath(parent));
			return Path.GetFullPath(candidate).StartsWith(value, RootPathComparison);
		}

		private bool TryAcquireRootLease(MigrationBackupRequest request, string correlation, out string canonicalRoot, out IDisposable lease, out MigrationBackupResult failure)
		{
			canonicalRoot = string.Empty;
			lease = null;
			failure = null;
			List<MigrationBackupFile> files = new List<MigrationBackupFile>();
			try
			{
				if (!TryValidateRequest(request, out canonicalRoot, out var outcome, out var failureCode))
				{
					failure = Failure(outcome, correlation, failureCode, files);
					return false;
				}
			}
			catch (Exception)
			{
				failure = Failure(MigrationBackupOutcome.IoFailure, correlation, "backup-root-canonicalization-failed", files);
				return false;
			}
			lock (RootGateSync)
			{
				if (ActiveBackupRoots.Contains(canonicalRoot))
				{
					failure = Failure(MigrationBackupOutcome.IoFailure, correlation, "backup-root-busy", files);
					return false;
				}
				if (ActiveBackupRoots.Count >= 128)
				{
					failure = Failure(MigrationBackupOutcome.IoFailure, correlation, "backup-root-gate-capacity", files);
					return false;
				}
				ActiveBackupRoots.Add(canonicalRoot);
			}
			lease = new RootLease(canonicalRoot);
			return true;
		}

		private static bool SameRoot(string left, string right)
		{
			if (!string.IsNullOrEmpty(left) && !string.IsNullOrEmpty(right))
			{
				return RootPathComparer.Equals(Path.GetFullPath(left), Path.GetFullPath(right));
			}
			return false;
		}

		private static void ReleaseRoot(string root)
		{
			if (string.IsNullOrEmpty(root))
			{
				return;
			}
			lock (RootGateSync)
			{
				ActiveBackupRoots.Remove(root);
			}
		}

		private string CopyAndHash(string source, string destination, long expectedLength, CancellationToken cancellationToken)
		{
			using Stream stream = _storage.OpenRead(source);
			using Stream stream2 = _storage.CreateNew(destination);
			using SHA256 sHA = SHA256.Create();
			byte[] array = new byte[81920];
			long num = 0L;
			int num2;
			while ((num2 = stream.Read(array, 0, array.Length)) != 0)
			{
				cancellationToken.ThrowIfCancellationRequested();
				num = checked(num + num2);
				if (num > expectedLength)
				{
					throw new BackupSourceChangedException();
				}
				stream2.Write(array, 0, num2);
				sHA.TransformBlock(array, 0, num2, null, 0);
			}
			sHA.TransformFinalBlock(Array.Empty<byte>(), 0, 0);
			if (num != expectedLength)
			{
				throw new BackupSourceChangedException();
			}
			stream2.Flush();
			if (stream2 is FileStream fileStream)
			{
				fileStream.Flush(flushToDisk: true);
			}
			return ToHex(sHA.Hash);
		}

		private string HashFile(string path, long expectedLength, CancellationToken cancellationToken)
		{
			using Stream stream = _storage.OpenRead(path);
			using SHA256 sHA = SHA256.Create();
			byte[] array = new byte[81920];
			long num = 0L;
			int num2;
			while ((num2 = stream.Read(array, 0, array.Length)) != 0)
			{
				cancellationToken.ThrowIfCancellationRequested();
				num = checked(num + num2);
				if (num > expectedLength)
				{
					throw new InvalidDataException("Backup file grew during validation.");
				}
				sHA.TransformBlock(array, 0, num2, null, 0);
			}
			sHA.TransformFinalBlock(Array.Empty<byte>(), 0, 0);
			if (num != expectedLength)
			{
				throw new InvalidDataException("Backup file shrank during validation.");
			}
			return ToHex(sHA.Hash);
		}

		private void ProtectFile(ICollection<Stream> protection, string path)
		{
			Stream stream = _storage.OpenRead(path);
			if (stream == null || !stream.CanRead)
			{
				stream?.Dispose();
				throw new IOException("A committed backup file could not be protected.");
			}
			protection.Add(stream);
		}

		private string ReadBoundedUtf8(string path, long maximumBytes)
		{
			if (maximumBytes < 0 || maximumBytes > int.MaxValue)
			{
				throw new ArgumentOutOfRangeException("maximumBytes");
			}
			using Stream stream = _storage.OpenRead(path);
			using MemoryStream memoryStream = new MemoryStream((int)Math.Min(maximumBytes, 81920L));
			checked
			{
				byte[] array = new byte[(maximumBytes >= 81920) ? 81920 : ((int)maximumBytes + 1)];
				long num = 0L;
				int num2;
				while ((num2 = stream.Read(array, 0, array.Length)) != 0)
				{
					num += num2;
					if (num > maximumBytes)
					{
						throw new InvalidDataException("Bounded UTF-8 file exceeded its limit.");
					}
					memoryStream.Write(array, 0, num2);
				}
				return StrictUtf8.GetString(memoryStream.ToArray());
			}
		}

		private string[] ReadBoundedUtf8Lines(string path, long maximumBytes, int maximumLines)
		{
			string s = ReadBoundedUtf8(path, maximumBytes);
			List<string> list = new List<string>(Math.Min(maximumLines, 64));
			using (StringReader stringReader = new StringReader(s))
			{
				string item;
				while ((item = stringReader.ReadLine()) != null)
				{
					if (list.Count >= maximumLines)
					{
						throw new InvalidDataException("Bounded manifest exceeded its line limit.");
					}
					list.Add(item);
				}
			}
			return list.ToArray();
		}

		private void WriteManifest(string partial, string migrationId, string correlation, IReadOnlyList<MigrationBackupFile> files)
		{
			StringBuilder stringBuilder = new StringBuilder();
			stringBuilder.AppendLine("RUNIC_SAFETY_BACKUP_V1");
			stringBuilder.Append(RunicText.Get("text_591ed5228a15")).AppendLine(Encode(migrationId));
			stringBuilder.Append(RunicText.Get("text_9e4ee92280d6")).AppendLine(Encode(correlation));
			stringBuilder.Append(RunicText.Get("text_3194eac00899")).AppendLine(_clock().ToUniversalTime().ToString("O", CultureInfo.InvariantCulture));
			foreach (MigrationBackupFile file in files)
			{
				stringBuilder.Append("FILE\t").Append(Encode(file.LogicalName)).Append('\t')
					.Append(Encode(file.OriginalPath))
					.Append('\t')
					.Append(Encode(file.BackupFileName))
					.Append('\t')
					.Append(file.Length.ToString(CultureInfo.InvariantCulture))
					.Append('\t')
					.Append(file.Sha256)
					.AppendLine();
			}
			_storage.WriteAllTextNew(_storage.Combine(partial, "manifest.runic"), stringBuilder.ToString());
		}

		private void WriteRestoreInstructions(string partial)
		{
			_storage.WriteAllTextNew(_storage.Combine(partial, "RESTORE.txt"), "Runic Safety migration backup\n\n1. Stop Valheim and the dedicated server.\n2. Validate this backup with Runic Safety before restoring.\n3. Read manifest.runic; each FILE row contains Base64 UTF-8 logical name, original path, backup filename, byte length, and SHA-256.\n4. Copy each backup file to its decoded original path only after preserving the current file.\n5. Keep world and character file families from the same timestamp together.\nRunic Safety intentionally does not auto-restore multi-file saves because that is not atomic.\n");
		}

		private void ApplyRetention(string root, string current, int retentionCount, IReadOnlyList<SourceState> sources)
		{
			List<BackupDirectory> list = new List<BackupDirectory>();
			string[] directories = _storage.GetDirectories(root, "backup-*");
			foreach (string path in directories)
			{
				string fullPath = _storage.GetFullPath(path);
				string path2 = _storage.Combine(fullPath, ".runicsafety-backup");
				if (IsDirectChild(root, fullPath) && !_storage.IsDirectoryReparsePoint(fullPath) && _storage.FileExists(path2) && _storage.GetFileMetadata(path2).Length <= 128 && string.Equals(ReadBoundedUtf8(path2, 128L).Trim(), "RUNIC_SAFETY_BACKUP_V1", StringComparison.Ordinal))
				{
					list.Add(new BackupDirectory(fullPath, _storage.GetDirectoryCreationUtc(fullPath)));
				}
			}
			list.Sort(delegate(BackupDirectory left, BackupDirectory right)
			{
				int num2 = right.CreatedUtc.CompareTo(left.CreatedUtc);
				return (num2 == 0) ? RootPathComparer.Compare(right.Path, left.Path) : num2;
			});
			for (int num = retentionCount; num < list.Count; num++)
			{
				if (!string.Equals(list[num].Path, current, RootPathComparison) && !ContainsSource(list[num].Path, sources))
				{
					_storage.DeleteDirectory(list[num].Path, recursive: true);
				}
			}
		}

		private static bool ContainsSource(string directory, IReadOnlyList<SourceState> sources)
		{
			string value = EnsureTrailingSeparator(Path.GetFullPath(directory));
			for (int i = 0; i < sources.Count; i++)
			{
				if (Path.GetFullPath(sources[i].Path).StartsWith(value, RootPathComparison))
				{
					return true;
				}
			}
			return false;
		}

		private MigrationBackupResult FailureWithCleanup(MigrationBackupOutcome outcome, string correlation, string code, List<MigrationBackupFile> files, string root, string partial)
		{
			CleanupPartial(root, partial);
			return Failure(outcome, correlation, code, files);
		}

		private MigrationBackupResult Failure(MigrationBackupOutcome outcome, string correlation, string code, List<MigrationBackupFile> files)
		{
			_diagnostics.Record(correlation, "migration-backup", code, SafetyDiagnosticSeverity.Warning);
			return new MigrationBackupResult(outcome, string.Empty, correlation, code, files.AsReadOnly());
		}

		private MigrationBackupResult CommittedFailure(MigrationBackupResult backup, MigrationBackupOutcome outcome, string code)
		{
			_diagnostics.Record(backup.CorrelationId, "migration-backup", code, SafetyDiagnosticSeverity.Error);
			return new MigrationBackupResult(outcome, backup.BackupDirectory, backup.CorrelationId, code, backup.Files);
		}

		private void CleanupPartial(string root, string partial)
		{
			if (string.IsNullOrWhiteSpace(root) || string.IsNullOrWhiteSpace(partial))
			{
				return;
			}
			try
			{
				string fullPath = _storage.GetFullPath(root);
				string fullPath2 = _storage.GetFullPath(partial);
				string fileName = _storage.GetFileName(fullPath2);
				if (IsDirectChild(fullPath, fullPath2) && fileName.StartsWith(".partial-", StringComparison.Ordinal) && !_storage.IsDirectoryReparsePoint(fullPath2) && _storage.DirectoryExists(fullPath2))
				{
					_storage.DeleteDirectory(fullPath2, recursive: true);
				}
			}
			catch (Exception)
			{
			}
		}

		private static bool IsDirectChild(string parent, string child)
		{
			string text = EnsureTrailingSeparator(Path.GetFullPath(parent));
			string fullPath = Path.GetFullPath(child);
			if (!fullPath.StartsWith(text, RootPathComparison))
			{
				return false;
			}
			string text2 = fullPath.Substring(text.Length);
			if (text2.Length != 0 && text2.IndexOf(Path.DirectorySeparatorChar) < 0)
			{
				return text2.IndexOf(Path.AltDirectorySeparatorChar) < 0;
			}
			return false;
		}

		private static string EnsureTrailingSeparator(string value)
		{
			if (!value.EndsWith(Path.DirectorySeparatorChar.ToString(), StringComparison.Ordinal) && !value.EndsWith(Path.AltDirectorySeparatorChar.ToString(), StringComparison.Ordinal))
			{
				return value + Path.DirectorySeparatorChar;
			}
			return value;
		}

		private static string CreateBackupName(int index, string path)
		{
			string text = Path.GetExtension(path);
			if (text.Length > 16 || text.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0)
			{
				text = ".bin";
			}
			return index.ToString("D4", CultureInfo.InvariantCulture) + text.ToLowerInvariant();
		}

		private static string BoundLogicalName(string value, int index)
		{
			string result = "source-" + index.ToString(CultureInfo.InvariantCulture);
			if (string.IsNullOrWhiteSpace(value))
			{
				return result;
			}
			string text = value.Trim();
			if (text.Length > 96)
			{
				return text.Substring(0, 96);
			}
			return text;
		}

		private static string BoundCode(string value)
		{
			if (string.IsNullOrWhiteSpace(value))
			{
				return "unknown";
			}
			string text = value.Trim();
			StringBuilder stringBuilder = new StringBuilder(Math.Min(text.Length, 48));
			for (int i = 0; i < text.Length; i++)
			{
				if (stringBuilder.Length >= 48)
				{
					break;
				}
				char c = text[i];
				stringBuilder.Append((char.IsLetterOrDigit(c) || c == '-' || c == '_') ? c : '_');
			}
			return stringBuilder.ToString();
		}

		private static string ShortCorrelation(string correlation)
		{
			string text = correlation ?? string.Empty;
			int num = text.LastIndexOf('-');
			if (num >= 0 && num + 1 < text.Length)
			{
				text = text.Substring(num + 1);
			}
			if (text.Length > 16)
			{
				return text.Substring(text.Length - 16);
			}
			return text;
		}

		private static string Encode(string value)
		{
			return Convert.ToBase64String(Encoding.UTF8.GetBytes(value ?? string.Empty));
		}

		private static string Decode(string value)
		{
			return StrictUtf8.GetString(Convert.FromBase64String(value));
		}

		private static string ToHex(byte[] bytes)
		{
			StringBuilder stringBuilder = new StringBuilder(bytes.Length * 2);
			foreach (byte b in bytes)
			{
				stringBuilder.Append(b.ToString("x2", CultureInfo.InvariantCulture));
			}
			return stringBuilder.ToString();
		}

		private static bool IsSimpleFileName(string value)
		{
			if (!string.IsNullOrWhiteSpace(value) && string.Equals(Path.GetFileName(value), value, StringComparison.Ordinal))
			{
				return value.IndexOfAny(Path.GetInvalidFileNameChars()) < 0;
			}
			return false;
		}

		private 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') && (c < 'A' || c > 'F'))
				{
					return false;
				}
			}
			return true;
		}
	}
	internal readonly struct BackupFileMetadata
	{
		internal long Length { get; }

		internal DateTime LastWriteUtc { get; }

		internal bool IsReparsePoint { get; }

		internal BackupFileMetadata(long length, DateTime lastWriteUtc, bool isReparsePoint)
		{
			Length = length;
			LastWriteUtc = lastWriteUtc;
			IsReparsePoint = isReparsePoint;
		}

		internal bool StableEquals(BackupFileMetadata other)
		{
			if (Length == other.Length && LastWriteUtc == other.LastWriteUtc)
			{
				return IsReparsePoint == other.IsReparsePoint;
			}
			return false;
		}
	}
	internal readonly struct MigrationBackupDefaults
	{
		internal string DestinationRoot { get; }

		internal int RetentionCount { get; }

		internal int MaximumFiles { get; }

		internal long MaximumTotalBytes { get; }

		internal MigrationBackupDefaults(string destinationRoot, int retentionCount, int maximumFiles, long maximumTotalBytes)
		{
			DestinationRoot = destinationRoot;
			RetentionCount = retentionCount;
			MaximumFiles = maximumFiles;
			MaximumTotalBytes = maximumTotalBytes;
		}
	}
	internal interface IBackupStorage
	{
		string GetFullPath(string path);

		string Combine(string left, string right);

		string GetFileName(string path);

		bool FileExists(string path);

		bool DirectoryExists(string path);

		BackupFileMetadata GetFileMetadata(string path);

		Stream OpenRead(string path);

		Stream CreateNew(string path);

		void CreateDirectory(string path);

		void MoveDirectory(string source, string destination);

		void DeleteDirectory(string path, bool recursive);

		string[] GetDirectories(string path, string pattern);

		string[] GetFiles(string path, string pattern);

		DateTime GetDirectoryCreationUtc(string path);

		bool IsDirectoryReparsePoint(string path);

		void WriteAllTextNew(string path, string contents);

		string ReadAllText(string path);

		string[] ReadAllLines(string path);
	}
	internal sealed class PhysicalBackupStorage : IBackupStorage
	{
		public string GetFullPath(string path)
		{
			return Path.GetFullPath(path);
		}

		public string Combine(string left, string right)
		{
			return Path.Combine(left, right);
		}

		public string GetFileName(string path)
		{
			return Path.GetFileName(path);
		}

		public bool FileExists(string path)
		{
			return File.Exists(path);
		}

		public bool DirectoryExists(string path)
		{
			return Directory.Exists(path);
		}

		public BackupFileMetadata GetFileMetadata(string path)
		{
			FileInfo fileInfo = new FileInfo(path);
			return new BackupFileMetadata(fileInfo.Length, fileInfo.LastWriteTimeUtc, (fileInfo.Attributes & FileAttributes.ReparsePoint) != 0);
		}

		public Stream OpenRead(string path)
		{
			return new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 81920, FileOptions.SequentialScan);
		}

		public Stream CreateNew(string path)
		{
			return new FileStream(path, FileMode.CreateNew, FileAccess.Write, FileShare.None, 81920, FileOptions.WriteThrough);
		}

		public void CreateDirectory(string path)
		{
			Directory.CreateDirectory(path);
		}

		public void MoveDirectory(string source, string destination)
		{
			Directory.Move(source, destination);
		}

		public void DeleteDirectory(string path, bool recursive)
		{
			Directory.Delete(path, recursive);
		}

		public string[] GetDirectories(string path, string pattern)
		{
			return Directory.GetDirectories(path, pattern);
		}

		public string[] GetFiles(string path, string pattern)
		{
			return Directory.GetFiles(path, pattern);
		}

		public DateTime GetDirectoryCreationUtc(string path)
		{
			return Directory.GetCreationTimeUtc(path);
		}

		public bool IsDirectoryReparsePoint(string path)
		{
			return (new DirectoryInfo(path).Attributes & FileAttributes.ReparsePoint) != 0;
		}

		public void WriteAllTextNew(string path, string contents)
		{
			byte[] bytes = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false).GetBytes(contents ?? string.Empty);
			using FileStream fileStream = new FileStream(path, FileMode.CreateNew, FileAccess.Write, FileShare.None, 4096, FileOptions.WriteThrough);
			fileStream.Write(bytes, 0, bytes.Length);
			fileStream.Flush(flushToDisk: true);
		}

		public string ReadAllText(string path)
		{
			return File.ReadAllText(path, Encoding.UTF8);
		}

		public string[] ReadAllLines(string path)
		{
			return File.ReadAllLines(path, Encoding.UTF8);
		}
	}
	public sealed class ProtectedItemPolicy : IProtectedItemPolicy
	{
		private readonly struct ProviderEntry
		{
			internal IItemProtectionProvider Provider { get; }

			internal int Priority { get; }

			internal long Token { get; }

			internal ProviderEntry(IItemProtectionProvider provider, int priority, long token)
			{
				Provider = provider;
				Priority = priority;
				Token = token;
			}
		}

		private sealed class Registration : IDisposable
		{
			private ProtectedItemPolicy _owner;

			private readonly long _token;

			internal Registration(ProtectedItemPolicy owner, long token)
			{
				_owner = owner;
				_token = token;
			}

			public void Dispose()
			{
				ProtectedItemPolicy owner = _owner;
				_owner = null;
				owner?.Unregister(_token);
			}
		}

		private const int MaximumProviders = 16;

		private readonly object _sync = new object();

		private readonly List<ProviderEntry> _providers = new List<ProviderEntry>();

		private readonly ISafetyDiagnosticService _diagnostics;

		private readonly Func<bool> _rareConfirmationEnabled;

		private readonly Func<bool> _administratorBypassEnabled;

		private long _token;

		public bool HasExternalProvider
		{
			get
			{
				lock (_sync)
				{
					return _providers.Count != 0;
				}
			}
		}

		public int ProviderCount
		{
			get
			{
				lock (_sync)
				{
					return _providers.Count;
				}
			}
		}

		public ProtectedItemPolicy(ISafetyDiagnosticService diagnostics, Func<bool> rareConfirmationEnabled, Func<bool> administratorBypassEnabled)
		{
			_diagnostics = diagnostics ?? throw new ArgumentNullException("diagnostics");
			_rareConfirmationEnabled = rareConfirmationEnabled ?? throw new ArgumentNullException("rareConfirmationEnabled");
			_administratorBypassEnabled = administratorBypassEnabled ?? throw new ArgumentNullException("administratorBypassEnabled");
		}

		public IDisposable RegisterProvider(IItemProtectionProvider provider, int priority = 0)
		{
			if (provider == null)
			{
				throw new ArgumentNullException("provider");
			}
			if (!IsIdentifier(provider.ProviderId))
			{
				throw new ArgumentException("ProviderId must be a lowercase dotted identifier.", "provider");
			}
			lock (_sync)
			{
				if (_providers.Count >= 16)
				{
					throw new InvalidOperationException("The protected-item provider limit is 16.");
				}
				foreach (ProviderEntry provider2 in _providers)
				{
					if (string.Equals(provider2.Provider.ProviderId, provider.ProviderId, StringComparison.Ordinal))
					{
						throw new InvalidOperationException("Provider already registered: " + provider.ProviderId);
					}
				}
				long token = ++_token;
				_providers.Add(new ProviderEntry(provider, priority, token));
				_providers.Sort(CompareProviders);
				return new Registration(this, token);
			}
		}

		public ItemProtectionDecision Evaluate(ItemProtectionRequest request)
		{
			string text = request?.CorrelationId;
			if (string.IsNullOrWhiteSpace(text))
			{
				text = _diagnostics.NewCorrelationId("protect");
			}
			if (request?.Item == null || string.IsNullOrWhiteSpace(request.Item.StableItemId))
			{
				return Record(text, new ItemProtectionDecision(ProtectionOutcome.Deny, ProtectionReason.InvalidRequest));
			}
			if (request.Administrator && _administratorBypassEnabled())
			{
				return Record(text, new ItemProtectionDecision(ProtectionOutcome.Allow, ProtectionReason.AdministratorBypass));
			}
			if (request.Item.Equipped)
			{
				return Record(text, new ItemProtectionDecision(ProtectionOutcome.Deny, ProtectionReason.Equipped));
			}
			if (request.Item.QuestItem)
			{
				return Record(text, new ItemProtectionDecision(ProtectionOutcome.Deny, ProtectionReason.QuestItem));
			}
			if (request.Item.LockState == ItemLockState.Locked)
			{
				return Record(text, new ItemProtectionDecision(ProtectionOutcome.Deny, ProtectionReason.Locked));
			}
			if (request.ExternalInventoryCapabilityAdvertised && request.Item.LockState == ItemLockState.Unknown)
			{
				return Record(text, new ItemProtectionDecision(ProtectionOutcome.Deny, ProtectionReason.ProviderUnavailable));
			}
			ProviderEntry[] array;
			lock (_sync)
			{
				array = _providers.ToArray();
			}
			ItemProtectionDecision itemProtectionDecision = null;
			ProviderEntry[] array2 = array;
			for (int i = 0; i < array2.Length; i++)
			{
				ProviderEntry providerEntry = array2[i];
				ItemProtectionDecision itemProtectionDecision2;
				try
				{
					itemProtectionDecision2 = providerEntry.Provider.Evaluate(request);
				}
				catch (Exception)
				{
					return Record(text, new ItemProtectionDecision(ProtectionOutcome.Deny, ProtectionReason.ProviderFailure, providerEntry.Provider.ProviderId));
				}
				if (itemProtectionDecision2 == null)
				{
					return Record(text, new ItemProtectionDecision(ProtectionOutcome.Deny, ProtectionReason.ProviderFailure, providerEntry.Provider.ProviderId));
				}
				if (itemProtectionDecision2.Outcome == ProtectionOutcome.Deny)
				{
					return Record(text, itemProtectionDecision2);
				}
				if (itemProtectionDecision2.Outcome == ProtectionOutcome.RequireConfirmation && itemProtectionDecision == null)
				{
					itemProtectionDecision = itemProtectionDecision2;
				}
			}
			if (itemProtectionDecision != null)
			{
				return Record(text, itemProtectionDecision);
			}
			if (request.Item.ConfiguredRare && _rareConfirmationEnabled())
			{
				return Record(text, new ItemProtectionDecision(ProtectionOutcome.RequireConfirmation, ProtectionReason.ConfiguredRareItem));
			}
			return Record(text, new ItemProtectionDecision(ProtectionOutcome.Allow, ProtectionReason.None));
		}

		private ItemProtectionDecision Record(string correlation, ItemProtectionDecision decision)
		{
			SafetyDiagnosticSeverity severity = ((decision.Outcome == ProtectionOutcome.Deny) ? SafetyDiagnosticSeverity.Warning : SafetyDiagnosticSeverity.Information);
			_diagnostics.Record(correlation, "protected-item", decision.Outcome.ToString().ToLowerInvariant() + "-" + decision.Reason.ToString().ToLowerInvariant(), severity);
			return decision;
		}

		private void Unregister(long token)
		{
			lock (_sync)
			{
				_providers.RemoveAll((ProviderEntry entry) => entry.Token == token);
			}
		}

		private static int CompareProviders(ProviderEntry left, ProviderEntry right)
		{
			int num = right.Priority.CompareTo(left.Priority);
			if (num != 0)
			{
				return num;
			}
			int num2 = StringComparer.Ordinal.Compare(left.Provider.ProviderId, right.Provider.ProviderId);
			if (num2 == 0)
			{
				return left.Token.CompareTo(right.Token);
			}
			return num2;
		}

		private static bool IsIdentifier(string value)
		{
			if (string.IsNullOrWhiteSpace(value) || value.Length > 96)
			{
				return false;
			}
			bool flag = true;
			foreach (char c in value)
			{
				switch (c)
				{
				case '.':
					if (flag)
					{
						return false;
					}
					flag = true;
					continue;
				default:
					if ((c < '0' || c > '9') && c != '-')
					{
						return false;
					}
					break;
				case 'a':
				case 'b':
				case 'c':
				case 'd':
				case 'e':
				case 'f':
				case 'g':
				case 'h':
				case 'i':
				case 'j':
				case 'k':
				case 'l':
				case 'm':
				case 'n':
				case 'o':
				case 'p':
				case 'q':
				case 'r':
				case 's':
				case 't':
				case 'u':
				case 'v':
				case 'w':
				case 'x':
				case 'y':
				case 'z':
					break;
				}
				flag = false;
			}
			return !flag;
		}
	}
	public sealed class RecoveryPlanningService : IRecoveryPlanningService
	{
		private readonly struct ProviderEntry
		{
			internal IInventoryTopologyProvider Provider { get; }

			internal long Token { get; }

			internal ProviderEntry(IInventoryTopologyProvider provider, long token)
			{
				Provider = provider;
				Token = token;
			}
		}

		private sealed class Registration : IDisposable
		{
			private RecoveryPlanningService _owner;

			private readonly long _token;

			internal Registration(RecoveryPlanningService owner, long token)
			{
				_owner = owner;
				_token = token;
			}

			public void Dispose()
			{
				RecoveryPlanningService owner = _owner;
				_owner = null;
				owner?.Unregister(_token);
			}
		}

		private const int MaximumProviders = 8;

		private readonly object _sync = new object();

		private readonly List<ProviderEntry> _providers = new List<ProviderEntry>();

		private readonly ISafetyDiagnosticService _diagnostics;

		private long _token;

		public bool HasTopologyProvider
		{
			get
			{
				lock (_sync)
				{
					return _providers.Count != 0;
				}
			}
		}

		public RecoveryPlanningService(ISafetyDiagnosticService diagnostics)
		{
			_diagnostics = diagnostics ?? throw new ArgumentNullException("diagnostics");
		}

		public IDisposable RegisterTopologyProvider(IInventoryTopologyProvider provider)
		{
			if (provider == null)
			{
				throw new ArgumentNullException("provider");
			}
			if (string.IsNullOrWhiteSpace(provider.ProviderId) || provider.ProviderId.Length > 96)
			{
				throw new ArgumentException("A bounded topology provider ID is required.", "provider");
			}
			lock (_sync)
			{
				if (_providers.Count >= 8)
				{
					throw new InvalidOperationException("The topology provider limit is 8.");
				}
				foreach (ProviderEntry provider2 in _providers)
				{
					if (string.Equals(provider2.Provider.ProviderId, provider.ProviderId, StringComparison.Ordinal))
					{
						throw new InvalidOperationException("Topology provider already registered: " + provider.ProviderId);
					}
				}
				long token = ++_token;
				_providers.Add(new ProviderEntry(provider, token));
				_providers.Sort((ProviderEntry left, ProviderEntry right) => StringComparer.Ordinal.Compare(left.Provider.ProviderId, right.Provider.ProviderId));
				return new Registration(this, token);
			}
		}

		public bool TryCaptureTopology(long playerId, out InventoryTopologySnapshot snapshot, out string failureCode)
		{
			ProviderEntry[] array;
			lock (_sync)
			{
				array = _providers.ToArray();
			}
			snapshot = null;
			failureCode = ((array.Length == 0) ? "no-topology-provider" : string.Empty);
			ProviderEntry[] array2 = array;
			for (int i = 0; i < array2.Length; i++)
			{
				ProviderEntry providerEntry = array2[i];
				try
				{
					if (!providerEntry.Provider.TryCapture(playerId, out var snapshot2, out var failureCode2))
					{
						failureCode = Bound(failureCode2, "provider-declined");
						continue;
					}
					if (snapshot2 == null)
					{
						failureCode = "provider-returned-null";
						continue;
					}
					snapshot = snapshot2;
					failureCode = string.Empty;
					return true;
				}
				catch (Exception)
				{
					failureCode = "provider-threw";
				}
			}
			return false;
		}

		public RecoveryPlan Plan(RecoveryPlanningRequest request)
		{
			string correlation = _diagnostics.NewCorrelationId("recovery");
			if (request == null || request.VanillaWidth <= 0 || request.VanillaHeight <= 0 || request.VanillaOccupiedSlots < 0)
			{
				return Result(RecoveryPlanOutcome.Invalid, 0, 0, correlation, "invalid-request");
			}
			int num;
			try
			{
				num = checked(request.VanillaWidth * request.VanillaHeight);
			}
			catch (OverflowException)
			{
				return Result(RecoveryPlanOutcome.Invalid, 0, 0, correlation, "capacity-overflow");
			}
			if (request.VanillaOccupiedSlots > num)
			{
				return Result(RecoveryPlanOutcome.BlockedUnsupportedExternalTopology, request.VanillaOccupiedSlots, num, correlation, "vanilla-capacity-insufficient");
			}
			if (!request.VanillaSerializationVerified)
			{
				return Result(RecoveryPlanOutcome.BlockedSerializationFailure, request.VanillaOccupiedSlots, num, correlation, "verify-vanilla-serialization");
			}
			InventoryTopologySnapshot topology = request.Topology;
			if (topology == null)
			{
				return Result(RecoveryPlanOutcome.SafeVanillaTombstone, request.VanillaOccupiedSlots, num, correlation, "none");
			}
			if (!ValidTopology(topology) || !CompatibleProtocol(topology.ProtocolVersion))
			{
				return Result(RecoveryPlanOutcome.BlockedIncompatibleTopology, Math.Max(request.VanillaOccupiedSlots, topology.OccupiedSlots), Math.Max(num, topology.RecoveryCapacitySlots), correlation, "update-inventory-topology-provider");
			}
			if (!topology.SerializationVerified)
			{
				return Result(RecoveryPlanOutcome.BlockedSerializationFailure, Math.Max(request.VanillaOccupiedSlots, topology.OccupiedSlots), topology.RecoveryCapacitySlots, correlation, "verify-peer-topology-serialization");
			}
			int num2 = Math.Max(request.VanillaOccupiedSlots, topology.OccupiedSlots);
			int num3 = Math.Max(num, topology.RecoveryCapacitySlots);
			if (num3 < num2)
			{
				return Result(RecoveryPlanOutcome.BlockedUnsupportedExternalTopology, num2, num3, correlation, "unsupported-external-inventory-topology");
			}
			return Result(RecoveryPlanOutcome.SafeExpandedTombstone, num2, num3, correlation, "none");
		}

		private RecoveryPlan Result(RecoveryPlanOutcome outcome, int required, int planned, string correlation, string remediation)
		{
			_diagnostics.Record(correlation, "recovery-plan", outcome.ToString().ToLowerInvariant(), (outcome != RecoveryPlanOutcome.SafeVanillaTombstone && outcome != RecoveryPlanOutcome.SafeExpandedTombstone) ? SafetyDiagnosticSeverity.Warning : SafetyDiagnosticSeverity.Information);
			return new RecoveryPlan(outcome, required, planned, correlation, remediation);
		}

		private static bool ValidTopology(InventoryTopologySnapshot snapshot)
		{
			if (!string.IsNullOrWhiteSpace(snapshot.ProviderId) && !string.IsNullOrWhiteSpace(snapshot.TopologyHash) && snapshot.TotalSlots >= 0 && snapshot.OccupiedSlots >= 0 && snapshot.OccupiedSlots <= snapshot.TotalSlots)
			{
				return snapshot.RecoveryCapacitySlots >= 0;
			}
			return false;
		}

		private static bool CompatibleProtocol(string value)
		{
			if (TryReadMajor(value, out var major) && TryReadMajor("1.0", out var major2))
			{
				return major == major2;
			}
			return false;
		}

		private static bool TryReadMajor(string value, out int major)
		{
			major = 0;
			if (string.IsNullOrWhiteSpace(value))
			{
				return false;
			}
			string[] array = value.Trim().Split('.');
			if (array.Length >= 1 && int.TryParse(array[0], out major))
			{
				return major >= 0;
			}
			return false;
		}

		private void Unregister(long token)
		{
			lock (_sync)
			{
				_providers.RemoveAll((ProviderEntry entry) => entry.Token == token);
			}
		}

		private static string Bound(string value, string fallback)
		{
			if (string.IsNullOrWhiteSpace(value))
			{
				return fallback;
			}
			string text = value.Trim();
			if (text.Length > 64)
			{
				return text.Substring(0, 64);
			}
			return text;
		}
	}
}
namespace RunicSafety.Integration
{
	internal static class LocalizationBridge
	{
		private static readonly MethodInfo AddWord = typeof(Localization).GetMethod("AddWord", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[2]
		{
			typeof(string),
			typeof(string)
		}, null);

		private static readonly IReadOnlyDictionary<string, string> English = new Dictionary<string, string>(StringComparer.Ordinal)
		{
			["runicsafety_confirm_container"] = "Runic Safety: repeat the same removal to destroy this occupied container.",
			["runicsafety_confirm_vehicle"] = "Runic Safety: repeat the same removal to destroy this ship or cart.",
			["runicsafety_confirm_portal"] = "Runic Safety: submit the same tag again to overwrite this portal.",
			["runicsafety_confirm_rare"] = "Runic Safety: repeat the same action to use the protected rare item.",
			["runicsafety_protected_denied"] = "Runic Safety blocked a protected item from this destination.",
			["runicsafety_provider_missing"] = "Runic Safety blocked the action because the installed inventory protection provider is unavailable.",
			["runicsafety_recovery_unsafe"] = "Runic Safety could not verify expanded death recovery; see the correlated log before migrating topology."
		};

		internal static bool Validate(out string problem)
		{
			if (AddWord == null)
			{
				problem = "Localization.AddWord(string,string) is missing.";
				return false;
			}
			problem = string.Empty;
			return true;
		}

		internal static void Install(Localization localization)
		{
			if (localization == null || AddWord == null)
			{
				return;
			}
			foreach (KeyValuePair<string, string> item in English)
			{
				AddWord.Invoke(localization, new object[2] { item.Key, item.Value });
			}
		}
	}
	[HarmonyPatch(typeof(Localization), "SetupLanguage", new Type[] { typeof(string) })]
	internal static class LocalizationSetupLanguagePatch
	{
		[HarmonyPostfix]
		private static void Postfix(Localization __instance)
		{
			LocalizationBridge.Install(__instance);
		}
	}
	[HarmonyPatch(typeof(Player), "RemovePiece", new Type[] { })]
	internal static class PlayerRemovePiecePatch
	{
		[HarmonyPrefix]
		[HarmonyPriority(800)]
		private static bool Prefix(Player __instance)
		{
			if (Plugin.RuntimeReady)
			{
				return Plugin.CurrentRuntime.AuthorizePieceRemoval(__instance);
			}
			return true;
		}
	}
	[HarmonyPatch(typeof(TeleportWorld), "SetText", new Type[] { typeof(string) })]
	[HarmonyAfter(new string[] { "chazman.RunicInteraction" })]
	internal static class TeleportWorldSetTextPatch
	{
		[HarmonyPrefix]
		private static bool Prefix(TeleportWorld __instance, string text)
		{
			if (Plugin.RuntimeReady)
			{
				return Plugin.CurrentRuntime.AuthorizePortalOverwrite(__instance, text);
			}
			return true;
		}
	}
	[HarmonyPatch(typeof(Incinerator), "OnIncinerate", new Type[]
	{
		typeof(Switch),
		typeof(Humanoid),
		typeof(ItemData)
	})]
	internal static class IncineratorOnIncineratePatch
	{
		[HarmonyPrefix]
		[HarmonyPriority(800)]
		[HarmonyBefore(new string[] { "chazman.RunicInventory" })]
		private static bool Prefix(Incinerator __instance, Humanoid user)
		{
			if (Plugin.RuntimeReady)
			{
				return Plugin.CurrentRuntime.AuthorizeIncineratorClient(__instance, user);
			}
			return true;
		}
	}
	[HarmonyPatch(typeof(Incinerator), "RPC_RequestIncinerate", new Type[]
	{
		typeof(long),
		typeof(long)
	})]
	internal static class IncineratorRequestPatch
	{
		[HarmonyPrefix]
		[HarmonyPriority(800)]
		private static bool Prefix(Incinerator __instance, long uid)
		{
			if (!Plugin.RuntimeReady || Plugin.CurrentRuntime.AuthorizeIncineratorOwner(__instance, uid))
			{
				return true;
			}
			ValheimContracts.SendIncineratorFailure(__instance, uid);
			return false;
		}
	}
	[HarmonyPatch(typeof(Smelter), "OnAddOre", new Type[]
	{
		typeof(Switch),
		typeof(Humanoid),
		typeof(ItemData)
	})]
	internal static class SmelterAddOrePatch
	{
		[HarmonyPrefix]
		[HarmonyPriority(800)]
		[HarmonyBefore(new string[] { "chazman.RunicInventory" })]
		private static bool Prefix(Smelter __instance, Humanoid user, ItemData item)
		{
			if (Plugin.RuntimeReady)
			{
				return Plugin.CurrentRuntime.AuthorizeSmelterOre(__instance, user, item);
			}
			return true;
		}
	}
	[HarmonyPatch(typeof(Smelter), "OnAddFuel", new Type[]
	{
		typeof(Switch),
		typeof(Humanoid),
		typeof(ItemData)
	})]
	internal static class SmelterAddFuelPatch
	{
		[HarmonyPrefix]
		[HarmonyPriority(800)]
		[HarmonyBefore(new string[] { "chazman.RunicInventory" })]
		private static bool Prefix(Smelter __instance, Humanoid user, ItemData item)
		{
			if (Plugin.RuntimeReady)
			{
				return Plugin.CurrentRuntime.AuthorizeSmelterFuel(__instance, user, item);
			}
			return true;
		}
	}
	[HarmonyPatch(typeof(CookingStation), "OnAddFuelSwitch", new Type[]
	{
		typeof(Switch),
		typeof(Humanoid),
		typeof(ItemData)
	})]
	internal static class CookingStationAddFuelPatch
	{
		[HarmonyPrefix]
		[HarmonyPriority(800)]
		[HarmonyBefore(new string[] { "chazman.RunicInventory", "chazman.RunicProduction" })]
		private static bool Prefix(CookingStation __instance, Humanoid user, ItemData item)
		{
			if (Plugin.RuntimeReady)
			{
				return Plugin.CurrentRuntime.AuthorizeCookingFuel(__instance, user, item);
			}
			return true;
		}
	}
	[HarmonyPatch(ty