Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of ModsmithCycle v1.1.0
ModsmithCycle.dll
Decompiled a day ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; 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.Versioning; using System.Security.Cryptography; using System.Text; using System.Threading; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using Steamworks; using TMPro; using UnityEngine; using UnityEngine.Events; using UnityEngine.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("ModsmithCycle")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.1.0.0")] [assembly: AssemblyInformationalVersion("1.1.0+51dd64e63e3ff3f81ddf60d59666c64cadea44df")] [assembly: AssemblyProduct("ModsmithCycle")] [assembly: AssemblyTitle("ModsmithCycle")] [assembly: AssemblyVersion("1.1.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace ModsmithCycle { internal static class BackupIdentityDirectoryService { private const int LabelLimit = 28; private const int HashSuffixLength = 12; private const string MarkerFilename = ".ModsmithCycle.identity"; private const string RotationLockFilename = ".ModsmithCycle.rotation.lock"; internal static bool TryResolve(BackupSnapshotKind kind, string stableIdentity, string saveFilename, out string identityDirectory, out string failureReason) { identityDirectory = null; failureReason = null; if (string.IsNullOrWhiteSpace(stableIdentity) || string.IsNullOrWhiteSpace(saveFilename) || !SaveBackupCoordinator.TryGetBackupRoot(out var backupRoot, out failureReason)) { if (failureReason == null) { failureReason = "Backup identity is incomplete."; } return false; } try { string text = ComputeIdentityHash(stableIdentity); string fullPath = Path.GetFullPath(Path.Combine(backupRoot, (kind == BackupSnapshotKind.Character) ? "Characters" : "Worlds")); string text2 = Path.Combine(fullPath, text); string text3 = Path.Combine(fullPath, SafeLabel(saveFilename) + "-" + text.Substring(0, 12)); string text4 = MarkerContents(text, stableIdentity); if (!HasNoReparseExistingAncestors(fullPath)) { failureReason = "Backup destination contains a filesystem link."; return false; } Directory.CreateDirectory(ToIoPath(fullPath)); if (!HasNoReparseAncestors(fullPath)) { failureReason = "Backup destination contains a filesystem link."; return false; } if (File.Exists(ToIoPath(text2)) || File.Exists(ToIoPath(text3))) { failureReason = "A backup identity path is occupied by a file."; return false; } bool flag = Directory.Exists(ToIoPath(text2)); bool flag2 = Directory.Exists(ToIoPath(text3)); if (flag && flag2) { failureReason = "Both the former identity folder and the readable folder exist. Neither was changed."; return false; } if (flag2) { if (!HasNoReparseAncestors(text3)) { failureReason = "Readable backup folder is a filesystem link."; return false; } if (!TryValidateOrInitializeNewFolder(text3, text4, out failureReason)) { return false; } identityDirectory = text3; return true; } if (flag) { if (!HasNoReparseAncestors(text2)) { failureReason = "Former backup folder is a filesystem link."; return false; } if (ExistsEither(Path.Combine(text2, "Tx")) || ExistsEither(Path.Combine(text2, ".ModsmithCycleTransaction"))) { identityDirectory = text2; Squirrel.Info(SquirrelChannel.Backup, "[ExitBackup] Readable backup folder migration deferred: existing transaction must be handled first."); return true; } string path = Path.Combine(text2, ".ModsmithCycle.rotation.lock"); if (File.Exists(ToIoPath(path)) && (File.GetAttributes(ToIoPath(path)) & FileAttributes.ReparsePoint) != FileAttributes.None) { failureReason = "Former backup lock is a filesystem link."; return false; } using (new FileStream(ToIoPath(path), FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None)) { if (ExistsEither(Path.Combine(text2, "Tx")) || ExistsEither(Path.Combine(text2, ".ModsmithCycleTransaction"))) { identityDirectory = text2; return true; } if (!TryValidateOrCreateMarker(text2, text4, out failureReason)) { return false; } } if (ExistsEither(text3)) { failureReason = "Readable backup folder appeared during migration. Former backups were left untouched."; return false; } Directory.Move(ToIoPath(text2), ToIoPath(text3)); identityDirectory = text3; Squirrel.Info(SquirrelChannel.Backup, "[ExitBackup] Backup folder migrated to readable name: " + Path.GetFileName(text3) + "."); return true; } Directory.CreateDirectory(ToIoPath(text3)); if (!HasNoReparseAncestors(text3) || !TryValidateOrInitializeNewFolder(text3, text4, out failureReason)) { if (failureReason == null) { failureReason = "New backup folder is a filesystem link."; } return false; } identityDirectory = text3; return true; } catch (IOException ex) { failureReason = "Backup folder is busy or unavailable: " + ex.Message; } catch (UnauthorizedAccessException) { failureReason = "Backup folder access was denied."; } catch (ArgumentException) { failureReason = "Backup folder path is invalid."; } catch (NotSupportedException) { failureReason = "Backup folder path is unsupported."; } return false; } private static bool TryValidateOrInitializeNewFolder(string directory, string expectedMarker, out string reason) { reason = null; if (File.Exists(ToIoPath(Path.Combine(directory, ".ModsmithCycle.identity")))) { return TryValidateOrCreateMarker(directory, expectedMarker, out reason); } using (IEnumerator<string> enumerator = Directory.EnumerateFileSystemEntries(ToIoPath(directory)).GetEnumerator()) { if (enumerator.MoveNext()) { _ = enumerator.Current; reason = "Readable backup folder has no identity marker and contains unknown data. Nothing was changed."; return false; } } return TryValidateOrCreateMarker(directory, expectedMarker, out reason); } private static bool TryValidateOrCreateMarker(string directory, string expected, out string reason) { reason = null; string path = Path.Combine(directory, ".ModsmithCycle.identity"); if (File.Exists(ToIoPath(path))) { if ((File.GetAttributes(ToIoPath(path)) & FileAttributes.ReparsePoint) != FileAttributes.None || new FileInfo(ToIoPath(path)).Length > 4096 || !string.Equals(File.ReadAllText(ToIoPath(path), Encoding.UTF8), expected, StringComparison.Ordinal)) { reason = "Backup identity marker does not match this save. The folder was left untouched."; return false; } return true; } string path2 = Path.Combine(Path.GetDirectoryName(directory), ".ModsmithCycle-identity-" + Guid.NewGuid().ToString("N") + ".tmp"); using (FileStream fileStream = new FileStream(ToIoPath(path2), FileMode.CreateNew, FileAccess.Write, FileShare.None)) { byte[] bytes = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false).GetBytes(expected); fileStream.Write(bytes, 0, bytes.Length); fileStream.Flush(flushToDisk: true); } File.Move(ToIoPath(path2), ToIoPath(path)); return true; } private static bool ExistsEither(string path) { if (!Directory.Exists(ToIoPath(path))) { return File.Exists(ToIoPath(path)); } return true; } private static string ComputeIdentityHash(string identity) { using SHA256 sHA = SHA256.Create(); return BitConverter.ToString(sHA.ComputeHash(Encoding.UTF8.GetBytes(identity))).Replace("-", string.Empty); } private static string MarkerContents(string hash, string identity) { return "ModsmithCycleIdentityV1\n" + hash + "\n" + Convert.ToBase64String(Encoding.UTF8.GetBytes(identity)) + "\n"; } private static string SafeLabel(string name) { StringBuilder stringBuilder = new StringBuilder(28); foreach (char c in name) { if (stringBuilder.Length >= 28) { break; } if (!char.IsLetterOrDigit(c)) { switch (c) { case '-': case '.': case '_': break; case ' ': stringBuilder.Append('-'); continue; default: stringBuilder.Append('_'); continue; } } stringBuilder.Append(c); } string text = stringBuilder.ToString().Trim(' ', '-', '_', '.'); if (text.Length != 0) { return text; } return "Save"; } private static string ToIoPath(string path) { string fullPath = Path.GetFullPath(path); if (Path.DirectorySeparatorChar != '\\' || fullPath.Length < 240) { return fullPath; } if (!fullPath.StartsWith("\\\\", StringComparison.Ordinal)) { return "\\\\?\\" + fullPath; } return "\\\\?\\UNC\\" + fullPath.Substring(2); } private static bool HasNoReparseAncestors(string directory) { string text = Path.GetFullPath(directory); while (!string.IsNullOrEmpty(text)) { if (!Directory.Exists(ToIoPath(text)) || (File.GetAttributes(ToIoPath(text)) & FileAttributes.ReparsePoint) != FileAttributes.None) { return false; } string directoryName = Path.GetDirectoryName(text); if (string.IsNullOrEmpty(directoryName) || string.Equals(directoryName, text, StringComparison.OrdinalIgnoreCase)) { break; } text = directoryName; } return true; } private static bool HasNoReparseExistingAncestors(string directory) { string text = Path.GetFullPath(directory); while (!string.IsNullOrEmpty(text)) { if (ExistsEither(text) && (File.GetAttributes(ToIoPath(text)) & FileAttributes.ReparsePoint) != FileAttributes.None) { return false; } string directoryName = Path.GetDirectoryName(text); if (string.IsNullOrEmpty(directoryName) || string.Equals(directoryName, text, StringComparison.OrdinalIgnoreCase)) { break; } text = directoryName; } return true; } } internal static class BackupRotationCleanupService { private const string StagingFolder = ".ModsmithCycleStaging"; private const string CompletedPrefix = ".ModsmithCycleCommitted-"; internal static bool TryCleanupCommittedRotation(BackupRotationLease lease, StagedBackupSnapshot snapshot, Guid operationId, string stableIdentity, string completionArchive, out string failureReason) { failureReason = null; if (lease == null || snapshot == null || operationId == Guid.Empty || string.IsNullOrWhiteSpace(stableIdentity) || string.IsNullOrWhiteSpace(completionArchive)) { failureReason = "Completed cleanup information is incomplete."; return false; } if (!SaveBackupCoordinator.TryGetBackupRoot(out var backupRoot, out failureReason)) { return false; } string step = "resolving completed backup paths"; string path = lease.IdentityDirectory; try { string fullPath = Path.GetFullPath(lease.IdentityDirectory); string fullPath2 = Path.GetFullPath(completionArchive); string fullPath3 = Path.GetFullPath(Path.Combine(fullPath, ".ModsmithCycleCommitted-" + operationId.ToString("N"))); step = "verifying committed archive"; path = fullPath2; if (!SamePath(fullPath2, fullPath3) || !Directory.Exists(ToIoPath(fullPath2)) || !HasNoReparseAncestors(fullPath) || !IsVerifiedCompletedArchive(fullPath2, operationId, stableIdentity)) { failureReason = "The newly committed archive could not be verified."; return false; } string fullPath4 = Path.GetFullPath(Path.Combine(backupRoot, ".ModsmithCycleStaging")); string fullPath5 = Path.GetFullPath(snapshot.StagingDirectory); string text = Path.Combine(fullPath5, "Files"); string fileName = Path.GetFileName(fullPath5); step = "validating completed staging directory"; path = fullPath5; if (!SamePath(Path.GetDirectoryName(fullPath5), fullPath4) || !SamePath(snapshot.ContentDirectory, text) || !HasNoReparseAncestors(fullPath4) || fileName.Length != 65 || !fileName.StartsWith(operationId.ToString("N") + "-", StringComparison.OrdinalIgnoreCase) || !Guid.TryParseExact(fileName.Substring(33), "N", out var _) || !Directory.Exists(ToIoPath(fullPath5)) || !Directory.Exists(ToIoPath(text))) { failureReason = "Completed staging path did not match its operation."; return false; } if (!HasNoReparsePoints(fullPath5)) { failureReason = "Completed staging contains an unsafe filesystem link."; return false; } List<string> list = new List<string>(); step = "enumerating older committed archives"; path = fullPath; string[] directories = Directory.GetDirectories(ToIoPath(fullPath), ".ModsmithCycleCommitted-*", SearchOption.TopDirectoryOnly); for (int i = 0; i < directories.Length; i++) { string text2 = FromIoPath(directories[i]); if (SamePath(text2, fullPath2)) { continue; } string fileName2 = Path.GetFileName(text2); if (fileName2.StartsWith(".ModsmithCycleCommitted-", StringComparison.Ordinal) && Guid.TryParseExact(fileName2.Substring(".ModsmithCycleCommitted-".Length), "N", out var result2)) { step = "verifying older committed archive"; path = text2; if (IsVerifiedCompletedArchive(text2, result2, stableIdentity)) { list.Add(text2); } } } step = "deleting completed staging directory"; path = fullPath5; Directory.Delete(ToIoPath(fullPath5, recursive: true), recursive: true); foreach (string item in list) { step = "deleting older committed archive"; path = item; Directory.Delete(ToIoPath(item, recursive: true), recursive: true); } return true; } catch (IOException exception) { failureReason = DescribeCleanupException(step, path, exception); } catch (UnauthorizedAccessException exception2) { failureReason = DescribeCleanupException(step, path, exception2); } catch (ArgumentException exception3) { failureReason = DescribeCleanupException(step, path, exception3); } catch (NotSupportedException exception4) { failureReason = DescribeCleanupException(step, path, exception4); } return false; } private static string DescribeCleanupException(string step, string path, Exception exception) { return "Temporary backup cleanup failed while " + step + "; target=" + path + "; exception=" + exception.GetType().Name + "; HRESULT=0x" + exception.HResult.ToString("X8") + "; detail=" + exception.Message; } private static bool IsVerifiedCompletedArchive(string archive, Guid operationId, string stableIdentity) { if (!Directory.Exists(ToIoPath(archive)) || !HasNoReparsePoints(archive)) { return false; } string text = Path.Combine(archive, "prepared.journal"); string text2 = Path.Combine(archive, "committed.marker"); if (!File.Exists(ToIoPath(text)) || !File.Exists(ToIoPath(text2)) || File.Exists(ToIoPath(Path.Combine(archive, "rollback.completed"))) || new FileInfo(ToIoPath(text)).Length > 4096 || new FileInfo(ToIoPath(text2)).Length > 256) { return false; } string expected = "OperationId=" + operationId.ToString("N"); string expected2 = "StableIdentity=" + Convert.ToBase64String(Encoding.UTF8.GetBytes(stableIdentity)); if (!ContainsExactLine(text, "JournalVersion=1") || !ContainsExactLine(text, expected) || !ContainsExactLine(text, expected2) || !ContainsExactLine(text2, expected)) { return false; } string[] fileSystemEntries = Directory.GetFileSystemEntries(ToIoPath(archive)); for (int i = 0; i < fileSystemEntries.Length; i++) { string path = FromIoPath(fileSystemEntries[i]); string fileName = Path.GetFileName(path); if (File.Exists(ToIoPath(path))) { if (fileName != "prepared.journal" && fileName != "committed.marker") { return false; } continue; } bool flag = false; for (int j = 1; j <= 3; j++) { if (fileName == "Old-" + j || fileName == "New-" + j) { flag = true; break; } } if (!flag) { return false; } } return true; } private static bool ContainsExactLine(string file, string expected) { string[] array = File.ReadAllLines(ToIoPath(file)); for (int i = 0; i < array.Length; i++) { if (string.Equals(array[i], expected, StringComparison.Ordinal)) { return true; } } return false; } private static bool SamePath(string first, string second) { if (string.IsNullOrWhiteSpace(first) || string.IsNullOrWhiteSpace(second)) { return false; } return string.Equals(Path.GetFullPath(first).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar), Path.GetFullPath(second).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar), StringComparison.OrdinalIgnoreCase); } private static string ToIoPath(string path, bool recursive = false) { string fullPath = Path.GetFullPath(FromIoPath(path)); if (Path.DirectorySeparatorChar != '\\' || (!recursive && fullPath.Length < 240)) { return fullPath; } if (fullPath.StartsWith("\\\\", StringComparison.Ordinal)) { return "\\\\?\\UNC\\" + fullPath.Substring(2); } return "\\\\?\\" + fullPath; } private static string FromIoPath(string path) { if (Path.DirectorySeparatorChar != '\\') { return path; } if (path.StartsWith("\\\\?\\UNC\\", StringComparison.OrdinalIgnoreCase)) { return "\\\\" + path.Substring(8); } if (path.StartsWith("\\\\?\\", StringComparison.Ordinal)) { return path.Substring(4); } return path; } private static bool HasNoReparsePoints(string root) { Stack<string> stack = new Stack<string>(); stack.Push(root); while (stack.Count > 0) { string path = stack.Pop(); FileAttributes attributes = File.GetAttributes(ToIoPath(path)); if ((attributes & FileAttributes.ReparsePoint) != FileAttributes.None || (attributes & FileAttributes.Directory) == 0) { return false; } string[] fileSystemEntries = Directory.GetFileSystemEntries(ToIoPath(path)); for (int i = 0; i < fileSystemEntries.Length; i++) { string text = FromIoPath(fileSystemEntries[i]); FileAttributes attributes2 = File.GetAttributes(ToIoPath(text)); if ((attributes2 & FileAttributes.ReparsePoint) != FileAttributes.None) { return false; } if ((attributes2 & FileAttributes.Directory) != FileAttributes.None) { stack.Push(text); } } } return true; } private static bool HasNoReparseAncestors(string directory) { string text = Path.GetFullPath(directory); while (!string.IsNullOrEmpty(text)) { if (!Directory.Exists(ToIoPath(text)) || (File.GetAttributes(ToIoPath(text)) & FileAttributes.ReparsePoint) != FileAttributes.None) { return false; } string directoryName = Path.GetDirectoryName(text); if (string.IsNullOrEmpty(directoryName) || SamePath(text, directoryName)) { break; } text = directoryName; } return true; } } internal sealed class BackupRotationLease : IDisposable { private readonly FileStream _lockStream; internal string IdentityDirectory { get; } internal BackupRotationLease(string identityDirectory, FileStream lockStream) { IdentityDirectory = identityDirectory; _lockStream = lockStream; } public void Dispose() { _lockStream?.Dispose(); } } internal sealed class BackupRotationJournalState { internal Guid OperationId { get; } internal string StableIdentity { get; } internal bool HadBackup1 { get; } internal bool HadBackup2 { get; } internal bool HadBackup3 { get; } internal bool IsCommitted { get; } internal BackupRotationJournalState(Guid operationId, string stableIdentity, bool hadBackup1, bool hadBackup2, bool hadBackup3, bool isCommitted) { OperationId = operationId; StableIdentity = stableIdentity; HadBackup1 = hadBackup1; HadBackup2 = hadBackup2; HadBackup3 = hadBackup3; IsCommitted = isCommitted; } } internal static class BackupRotationJournal { internal const string TransactionFolderName = "Tx"; private const string LegacyTransactionFolderName = ".ModsmithCycleTransaction"; internal const string OldBackupPrefix = "Old-"; internal const string NewBackupPrefix = "New-"; private const string LockFilename = ".ModsmithCycle.rotation.lock"; private const string JournalFilename = "prepared.journal"; private const string CommitFilename = "committed.marker"; internal static bool TryAcquireIdentityLock(string identityDirectory, out BackupRotationLease lease, out string failureReason) { lease = null; failureReason = null; if (string.IsNullOrWhiteSpace(identityDirectory)) { failureReason = "Backup identity directory is missing."; return false; } if (!SaveBackupCoordinator.TryGetBackupRoot(out var backupRoot, out failureReason)) { return false; } try { if (!Path.IsPathFullyQualified(identityDirectory)) { failureReason = "Backup identity directory must be absolute."; return false; } string fullPath = Path.GetFullPath(backupRoot); string fullPath2 = Path.GetFullPath(identityDirectory); string[] array = Path.GetRelativePath(fullPath, fullPath2).Split(new char[2] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries); if (array.Length != 2 || (array[0] != "Characters" && array[0] != "Worlds") || array[1] == "." || array[1] == "..") { failureReason = "Backup identity directory is outside the expected backup structure."; return false; } CheckDirectoryAncestry(fullPath2); Directory.CreateDirectory(fullPath2); CheckDirectoryAncestry(fullPath2); string path = Path.Combine(fullPath2, ".ModsmithCycle.rotation.lock"); if (File.Exists(path) && (File.GetAttributes(path) & FileAttributes.ReparsePoint) != FileAttributes.None) { failureReason = "Backup lock path is unsafe."; return false; } FileStream fileStream = new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None); string path2 = Path.Combine(fullPath2, ".ModsmithCycleTransaction"); if (Directory.Exists(path2) || File.Exists(path2)) { fileStream.Dispose(); failureReason = "An interrupted transaction from an older ModsmithCycle build still exists. The legacy transaction must be recovered before a new rotation; its data was left untouched."; return false; } lease = new BackupRotationLease(fullPath2, fileStream); return true; } catch (IOException) { failureReason = "Backup directory is busy or unavailable."; } catch (UnauthorizedAccessException) { failureReason = "Backup directory access was denied."; } catch (ArgumentException) { failureReason = "Backup identity path is invalid."; } catch (NotSupportedException) { failureReason = "Backup identity path is unsupported."; } return false; } internal static bool TryPrepareTransaction(BackupRotationLease lease, Guid operationId, string stableIdentity, out BackupRotationJournalState state, out string failureReason) { state = null; failureReason = null; if (lease == null || operationId == Guid.Empty || string.IsNullOrWhiteSpace(stableIdentity)) { failureReason = "Backup transaction information is incomplete."; return false; } try { string identityDirectory = lease.IdentityDirectory; string text = Path.Combine(identityDirectory, "Tx"); if (Directory.Exists(text) || File.Exists(text)) { failureReason = "An earlier backup transaction requires inspection or recovery."; return false; } string path = Path.Combine(identityDirectory, "Backup-1"); string path2 = Path.Combine(identityDirectory, "Backup-2"); string path3 = Path.Combine(identityDirectory, "Backup-3"); if (File.Exists(path) || File.Exists(path2) || File.Exists(path3)) { failureReason = "A backup slot contains an unexpected file."; return false; } bool flag = Directory.Exists(path); bool flag2 = Directory.Exists(path2); bool flag3 = Directory.Exists(path3); if ((flag2 && !flag) || (flag3 && !flag2)) { failureReason = "Existing backup slots are inconsistent."; return false; } Directory.CreateDirectory(text); string text2 = Path.Combine(text, "prepared.journal.tmp"); string destFileName = Path.Combine(text, "prepared.journal"); using (FileStream fileStream = new FileStream(text2, FileMode.CreateNew, FileAccess.Write, FileShare.None)) { using (StreamWriter streamWriter = new StreamWriter(fileStream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false), 4096, leaveOpen: true)) { streamWriter.WriteLine("JournalVersion=1"); streamWriter.WriteLine("OperationId=" + operationId.ToString("N")); streamWriter.WriteLine("StableIdentity=" + Convert.ToBase64String(Encoding.UTF8.GetBytes(stableIdentity))); streamWriter.WriteLine("HadBackup1=" + (flag ? "1" : "0")); streamWriter.WriteLine("HadBackup2=" + (flag2 ? "1" : "0")); streamWriter.WriteLine("HadBackup3=" + (flag3 ? "1" : "0")); streamWriter.Flush(); } fileStream.Flush(flushToDisk: true); } File.Move(text2, destFileName); state = new BackupRotationJournalState(operationId, stableIdentity, flag, flag2, flag3, isCommitted: false); return true; } catch (IOException) { failureReason = "Could not prepare backup transaction journal."; } catch (UnauthorizedAccessException) { failureReason = "Access to the transaction directory was denied."; } catch (ArgumentException) { failureReason = "Backup transaction path is invalid."; } catch (NotSupportedException) { failureReason = "Backup transaction storage is unsupported."; } return false; } internal static bool TryReadTransaction(BackupRotationLease lease, out BackupRotationJournalState state, out string failureReason) { state = null; failureReason = null; if (lease == null) { failureReason = "Backup identity lock is unavailable."; return false; } try { string text = Path.Combine(lease.IdentityDirectory, "Tx"); if (!Directory.Exists(text)) { return true; } CheckDirectoryAncestry(text); string path = Path.Combine(text, "prepared.journal"); if (!File.Exists(path)) { failureReason = "Backup transaction journal is missing."; return false; } string[] array = File.ReadAllLines(path); if (array.Length != 6 || array[0] != "JournalVersion=1") { failureReason = "Backup transaction journal is malformed."; return false; } if (!Guid.TryParseExact(ReadValue(array[1], "OperationId="), "N", out var result) || result == Guid.Empty) { failureReason = "Backup transaction operation ID is invalid."; return false; } string s = ReadValue(array[2], "StableIdentity="); string text2 = Encoding.UTF8.GetString(Convert.FromBase64String(s)); if (string.IsNullOrWhiteSpace(text2)) { failureReason = "Backup transaction identity is invalid."; return false; } bool flag = ReadValue(array[3], "HadBackup1=") == "1"; bool flag2 = ReadValue(array[4], "HadBackup2=") == "1"; bool flag3 = ReadValue(array[5], "HadBackup3=") == "1"; if (!IsValidFlag(array[3], "HadBackup1=") || !IsValidFlag(array[4], "HadBackup2=") || !IsValidFlag(array[5], "HadBackup3=")) { failureReason = "Backup transaction slot flags are invalid."; return false; } if ((flag2 && !flag) || (flag3 && !flag2)) { failureReason = "Backup transaction slot history is invalid."; return false; } string path2 = Path.Combine(text, "committed.marker"); bool flag4 = File.Exists(path2); if (flag4) { string[] array2 = File.ReadAllLines(path2); if (array2.Length != 1 || array2[0] != "OperationId=" + result.ToString("N")) { failureReason = "Backup commit marker is inconsistent."; return false; } } state = new BackupRotationJournalState(result, text2, flag, flag2, flag3, flag4); return true; } catch (IOException) { failureReason = "Could not read backup transaction journal."; } catch (UnauthorizedAccessException) { failureReason = "Access to backup transaction was denied."; } catch (ArgumentException) { failureReason = "Backup transaction contains invalid data."; } catch (FormatException) { failureReason = "Backup transaction identity encoding is invalid."; } catch (NotSupportedException) { failureReason = "Backup transaction storage is unsupported."; } return false; } internal static bool TryMarkCommitted(BackupRotationLease lease, Guid expectedOperationId, string expectedStableIdentity, out string failureReason) { failureReason = null; if (lease == null || expectedOperationId == Guid.Empty || string.IsNullOrWhiteSpace(expectedStableIdentity)) { failureReason = "Backup commit information is incomplete."; return false; } if (!TryReadTransaction(lease, out var state, out failureReason)) { return false; } if (state == null) { failureReason = "No prepared backup transaction exists."; return false; } if (state.IsCommitted) { failureReason = "Backup transaction is already committed."; return false; } if (state.OperationId != expectedOperationId || !string.Equals(state.StableIdentity, expectedStableIdentity, StringComparison.Ordinal)) { failureReason = "Backup transaction identity does not match."; return false; } try { string identityDirectory = lease.IdentityDirectory; if (!TryResolveKindFromLease(lease, out var kind)) { failureReason = "Cannot determine the backup save kind."; return false; } string text = Path.Combine(identityDirectory, "Tx"); CheckDirectoryAncestry(text); bool[] array = new bool[3] { state.HadBackup1, state.HadBackup2, state.HadBackup3 }; bool[] array2 = new bool[3] { true, state.HadBackup1, state.HadBackup2 }; for (int i = 1; i <= 3; i++) { string text2 = Path.Combine(text, "Old-" + i); string text3 = Path.Combine(text, "New-" + i); string text4 = Path.Combine(identityDirectory, "Backup-" + i); if (Directory.Exists(text2) != array[i - 1]) { failureReason = "Previous backup preservation is incomplete."; return false; } if (array[i - 1] && !BackupRotationService.TryVerifyBackupFolder(text2, kind, expectedStableIdentity, null, out failureReason)) { failureReason = "Preserved Old-" + i + " failed commit verification: " + failureReason; return false; } bool flag = Directory.Exists(text3); if (flag != array2[i - 1]) { failureReason = "Prepared replacement backup is unavailable."; return false; } if (flag && (!File.Exists(Path.Combine(text3, "backup-manifest.txt")) || !Directory.Exists(Path.Combine(text3, "Files")))) { failureReason = "Prepared replacement backup is structurally incomplete."; return false; } if (flag) { Guid? expectedOperationId2 = ((i == 1) ? new Guid?(expectedOperationId) : ((Guid?)null)); if (!BackupRotationService.TryVerifyBackupFolder(text3, kind, expectedStableIdentity, expectedOperationId2, out failureReason)) { failureReason = "Prepared New-" + i + " failed commit verification: " + failureReason; return false; } } bool flag2 = Directory.Exists(text4); if (flag2 != array2[i - 1]) { failureReason = "Installed backup slots are incomplete."; return false; } if (flag2) { string path = Path.Combine(text4, "backup-manifest.txt"); string path2 = Path.Combine(text4, "Files"); if (!File.Exists(path) || !Directory.Exists(path2)) { failureReason = "Installed backup structure is incomplete."; return false; } CheckDirectoryAncestry(text4); CheckDirectoryAncestry(path2); } if (flag2) { Guid? expectedOperationId3 = ((i == 1) ? new Guid?(expectedOperationId) : ((Guid?)null)); if (!BackupRotationService.TryVerifyBackupFolder(text4, kind, expectedStableIdentity, expectedOperationId3, out failureReason)) { failureReason = "Installed Backup-" + i + " failed commit verification: " + failureReason; return false; } } } string text5 = Path.Combine(text, "committed.marker.tmp"); string text6 = Path.Combine(text, "committed.marker"); if (File.Exists(text5) || File.Exists(text6)) { failureReason = "Backup commit marker already exists or requires recovery."; return false; } using (FileStream fileStream = new FileStream(text5, FileMode.CreateNew, FileAccess.Write, FileShare.None)) { using (StreamWriter streamWriter = new StreamWriter(fileStream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false), 4096, leaveOpen: true)) { streamWriter.WriteLine("OperationId=" + expectedOperationId.ToString("N")); streamWriter.Flush(); } fileStream.Flush(flushToDisk: true); } File.Move(text5, text6); return true; } catch (IOException) { failureReason = "Backup commit marker could not be written."; } catch (UnauthorizedAccessException) { failureReason = "Backup commit marker access was denied."; } catch (ArgumentException) { failureReason = "Backup commit path is invalid."; } catch (NotSupportedException) { failureReason = "Backup commit storage is unsupported."; } return false; } internal static bool TryRollbackPreparedTransaction(BackupRotationLease lease, string expectedStableIdentity, out string recoveryArchive, out string failureReason) { recoveryArchive = null; failureReason = null; if (lease == null || string.IsNullOrWhiteSpace(expectedStableIdentity)) { failureReason = "Backup recovery identity is unavailable."; return false; } if (!TryReadTransaction(lease, out var state, out failureReason)) { return false; } if (state == null) { return true; } if (state.IsCommitted) { failureReason = "Committed transactions require forward recovery, not rollback."; return false; } if (!string.Equals(state.StableIdentity, expectedStableIdentity, StringComparison.Ordinal)) { failureReason = "Recovery identity does not match the recorded transaction."; return false; } try { string identityDirectory = lease.IdentityDirectory; if (!TryResolveKindFromLease(lease, out var kind)) { failureReason = "Rollback cannot determine the backup save kind."; return false; } string text = Path.Combine(identityDirectory, "Tx"); string text2 = Path.Combine(text, "rollback.completed"); string text3 = text2 + ".tmp"; string text4 = Path.Combine(identityDirectory, ".ModsmithCycleRecovered-" + state.OperationId.ToString("N")); if (Directory.Exists(text4) || File.Exists(text4)) { failureReason = "Recovery archive destination already exists."; return false; } bool[] array = new bool[3] { state.HadBackup1, state.HadBackup2, state.HadBackup3 }; string[] array2 = new string[3]; string[] array3 = new string[3]; string[] array4 = new string[3]; bool flag = File.Exists(text2); if (flag) { string[] array5 = File.ReadAllLines(text2); if (array5.Length != 1 || array5[0] != "OperationId=" + state.OperationId.ToString("N")) { failureReason = "Rollback completion marker is invalid."; return false; } } for (int i = 1; i <= 3; i++) { array2[i - 1] = Path.Combine(text, "Old-" + i); array3[i - 1] = Path.Combine(identityDirectory, "Backup-" + i); array4[i - 1] = Path.Combine(text, "Aborted-" + i); string text5 = array2[i - 1]; string text6 = array3[i - 1]; string path = array4[i - 1]; CheckDirectoryAncestry(text5); CheckDirectoryAncestry(text6); CheckDirectoryAncestry(path); if (File.Exists(text5) || File.Exists(text6) || File.Exists(path)) { failureReason = "Recovery encountered an unexpected file."; return false; } bool flag2 = Directory.Exists(text5); bool flag3 = Directory.Exists(text6); bool flag4 = Directory.Exists(path); bool flag5 = array[i - 1]; if (flag2 && !BackupRotationService.TryVerifyBackupFolder(text5, kind, expectedStableIdentity, null, out failureReason)) { failureReason = "Rollback Old-" + i + " failed verification: " + failureReason; return false; } if (flag5 && !flag2 && flag3 && !BackupRotationService.TryVerifyBackupFolder(text6, kind, expectedStableIdentity, null, out failureReason)) { failureReason = "Rollback visible Backup-" + i + " failed verification: " + failureReason; return false; } if (flag) { if (flag2 || flag3 != flag5) { failureReason = "Previously completed rollback has an inconsistent folder state."; return false; } continue; } if (flag5 && !flag2 && !flag3) { failureReason = "An original backup cannot be located."; return false; } if (!flag5 && flag2) { failureReason = "Recovery found an unexpected original backup."; return false; } if (flag4 && flag3 && (flag2 || !flag5)) { failureReason = "Recovery archive slot is occupied."; return false; } } if (!flag) { for (int j = 0; j < 3; j++) { string text7 = array2[j]; string text8 = array3[j]; string destDirName = array4[j]; bool flag6 = array[j]; bool num = Directory.Exists(text7); bool flag7 = Directory.Exists(text8); if (num) { if (flag7) { Directory.Move(text8, destDirName); } Directory.Move(text7, text8); } else if (!flag6 && flag7) { Directory.Move(text8, destDirName); } } for (int k = 0; k < 3; k++) { if (Directory.Exists(array2[k]) || Directory.Exists(array3[k]) != array[k]) { failureReason = "Previous backup restoration could not be confirmed."; return false; } if (array[k] && !BackupRotationService.TryVerifyBackupFolder(array3[k], kind, expectedStableIdentity, null, out failureReason)) { failureReason = "Restored Backup-" + (k + 1) + " failed verification: " + failureReason; return false; } } if (File.Exists(text3)) { failureReason = "Incomplete rollback marker requires inspection."; return false; } using (FileStream fileStream = new FileStream(text3, FileMode.CreateNew, FileAccess.Write, FileShare.None)) { using (StreamWriter streamWriter = new StreamWriter(fileStream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false), 4096, leaveOpen: true)) { streamWriter.WriteLine("OperationId=" + state.OperationId.ToString("N")); streamWriter.Flush(); } fileStream.Flush(flushToDisk: true); } File.Move(text3, text2); } Directory.Move(text, text4); recoveryArchive = text4; return true; } catch (IOException) { failureReason = "Backup rollback encountered a file error."; } catch (UnauthorizedAccessException) { failureReason = "Backup rollback was denied filesystem access."; } catch (ArgumentException) { failureReason = "Backup rollback encountered an invalid path."; } catch (NotSupportedException) { failureReason = "Backup rollback storage is unsupported."; } return false; } internal static bool TryFinalizeCommittedTransaction(BackupRotationLease lease, string expectedStableIdentity, out string completionArchive, out string failureReason) { completionArchive = null; failureReason = null; if (lease == null || string.IsNullOrWhiteSpace(expectedStableIdentity)) { failureReason = "Committed recovery identity is unavailable."; return false; } if (!TryReadTransaction(lease, out var state, out failureReason)) { return false; } if (state == null) { return true; } if (!state.IsCommitted) { failureReason = "Transaction is not committed. Rollback recovery is required."; return false; } if (!string.Equals(state.StableIdentity, expectedStableIdentity, StringComparison.Ordinal)) { failureReason = "Committed recovery identity does not match."; return false; } try { string identityDirectory = lease.IdentityDirectory; if (!TryResolveKindFromLease(lease, out var kind)) { failureReason = "Committed recovery cannot determine the backup save kind."; return false; } string text = Path.Combine(identityDirectory, "Tx"); string text2 = Path.Combine(identityDirectory, ".ModsmithCycleCommitted-" + state.OperationId.ToString("N")); if (Directory.Exists(text2) || File.Exists(text2)) { failureReason = "Committed recovery archive already exists."; return false; } if (File.Exists(Path.Combine(text, "rollback.completed"))) { failureReason = "Transaction contains conflicting recovery markers."; return false; } bool[] array = new bool[3] { state.HadBackup1, state.HadBackup2, state.HadBackup3 }; bool[] array2 = new bool[3] { true, state.HadBackup1, state.HadBackup2 }; string[] array3 = new string[3]; string[] array4 = new string[3]; for (int i = 1; i <= 3; i++) { string text3 = Path.Combine(text, "Old-" + i); string text4 = Path.Combine(text, "New-" + i); string text5 = (array3[i - 1] = Path.Combine(identityDirectory, "Backup-" + i)); array4[i - 1] = text4; CheckDirectoryAncestry(text3); CheckDirectoryAncestry(text4); CheckDirectoryAncestry(text5); if (File.Exists(text3) || File.Exists(text4) || File.Exists(text5)) { failureReason = "Committed recovery encountered an unexpected file."; return false; } if (Directory.Exists(text3) != array[i - 1]) { failureReason = "Previous backup preservation is inconsistent."; return false; } if (array[i - 1] && !BackupRotationService.TryVerifyBackupFolder(text3, kind, expectedStableIdentity, null, out failureReason)) { failureReason = "Committed Old-" + i + " failed verification: " + failureReason; return false; } bool flag = Directory.Exists(text5); bool flag2 = Directory.Exists(text4); if (!array2[i - 1]) { if (flag || flag2) { failureReason = "Unexpected committed backup slot."; return false; } continue; } if (!flag && !flag2) { failureReason = "Committed backup and its recovery copy are both missing."; return false; } if (flag) { if (!File.Exists(Path.Combine(text5, "backup-manifest.txt")) || !Directory.Exists(Path.Combine(text5, "Files"))) { failureReason = "Installed committed backup is structurally incomplete."; return false; } Guid? expectedOperationId = ((i == 1) ? new Guid?(state.OperationId) : ((Guid?)null)); if (flag && !BackupRotationService.TryVerifyBackupFolder(text5, kind, expectedStableIdentity, expectedOperationId, out failureReason)) { failureReason = "Committed visible Backup-" + i + " failed verification: " + failureReason; return false; } if (flag2 && !BackupRotationService.TryVerifyBackupFolder(text4, kind, expectedStableIdentity, expectedOperationId, out failureReason)) { failureReason = "Committed prepared New-" + i + " failed verification: " + failureReason; return false; } } if (flag2) { if (!File.Exists(Path.Combine(text4, "backup-manifest.txt")) || !Directory.Exists(Path.Combine(text4, "Files"))) { failureReason = "Retained replacement backup is structurally incomplete."; return false; } Guid? expectedOperationId2 = ((i == 1) ? new Guid?(state.OperationId) : ((Guid?)null)); if (flag && !BackupRotationService.TryVerifyBackupFolder(text5, kind, expectedStableIdentity, expectedOperationId2, out failureReason)) { failureReason = "Committed visible Backup-" + i + " failed verification: " + failureReason; return false; } if (flag2 && !BackupRotationService.TryVerifyBackupFolder(text4, kind, expectedStableIdentity, expectedOperationId2, out failureReason)) { failureReason = "Committed prepared New-" + i + " failed verification: " + failureReason; return false; } } } for (int j = 0; j < 3; j++) { if (array2[j] && !Directory.Exists(array3[j])) { Directory.Move(array4[j], array3[j]); } } for (int k = 0; k < 3; k++) { if (Directory.Exists(array3[k]) != array2[k]) { failureReason = "Committed backup layout could not be restored."; return false; } } for (int l = 0; l < 3; l++) { if (array2[l]) { Guid? expectedOperationId3 = ((l == 0) ? new Guid?(state.OperationId) : ((Guid?)null)); if (!BackupRotationService.TryVerifyBackupFolder(array3[l], kind, expectedStableIdentity, expectedOperationId3, out failureReason)) { failureReason = "Recovered Backup-" + (l + 1) + " failed final verification: " + failureReason; return false; } } } Directory.Move(text, text2); completionArchive = text2; return true; } catch (IOException) { failureReason = "Committed recovery encountered a file error."; } catch (UnauthorizedAccessException) { failureReason = "Committed recovery was denied access."; } catch (ArgumentException) { failureReason = "Committed recovery encountered an invalid path."; } catch (NotSupportedException) { failureReason = "Committed recovery storage is unsupported."; } return false; } private static bool TryResolveKindFromLease(BackupRotationLease lease, out BackupSnapshotKind kind) { kind = BackupSnapshotKind.Character; if (lease == null || string.IsNullOrWhiteSpace(lease.IdentityDirectory)) { return false; } string fileName = Path.GetFileName(Path.GetDirectoryName(lease.IdentityDirectory)); if (string.Equals(fileName, "Characters", StringComparison.OrdinalIgnoreCase)) { kind = BackupSnapshotKind.Character; return true; } if (string.Equals(fileName, "Worlds", StringComparison.OrdinalIgnoreCase)) { kind = BackupSnapshotKind.World; return true; } return false; } private static string ReadValue(string line, string prefix) { if (line == null || !line.StartsWith(prefix, StringComparison.Ordinal)) { throw new FormatException("Invalid backup journal entry."); } return line.Substring(prefix.Length); } private static bool IsValidFlag(string line, string prefix) { string text = ReadValue(line, prefix); if (!(text == "0")) { return text == "1"; } return true; } private static void CheckDirectoryAncestry(string path) { for (DirectoryInfo directoryInfo = new DirectoryInfo(Path.GetFullPath(path)); directoryInfo != null; directoryInfo = directoryInfo.Parent) { if (directoryInfo.Exists && (directoryInfo.Attributes & FileAttributes.ReparsePoint) != FileAttributes.None) { throw new IOException("Backup path contains a reparse point."); } } } } internal enum BackupSnapshotKind { Character, World } internal sealed class StagedBackupSnapshot { internal string StagingDirectory { get; } internal string ContentDirectory { get; } internal int FileCount { get; } internal long TotalBytes { get; } internal StagedBackupSnapshot(string stagingDirectory, string contentDirectory, int fileCount, long totalBytes) { StagingDirectory = stagingDirectory; ContentDirectory = contentDirectory; FileCount = fileCount; TotalBytes = totalBytes; } } internal static class BackupRotationService { private sealed class BackupFileRecord { internal string RelativePath; internal long Length; internal byte[] Sha256; } private const int CopyBufferSize = 131072; internal static bool TryStageLocalSnapshot(string sourcePath, BackupSnapshotKind kind, string stableIdentity, Guid operationId, out StagedBackupSnapshot snapshot, out string failureReason) { snapshot = null; failureReason = null; if (operationId == Guid.Empty) { failureReason = "Backup operation identifier is missing."; return false; } if (string.IsNullOrWhiteSpace(stableIdentity)) { failureReason = "Backup save identity is missing."; return false; } if (string.IsNullOrWhiteSpace(sourcePath)) { failureReason = "Backup source path is missing."; return false; } if (!SaveBackupCoordinator.TryGetBackupRoot(out var backupRoot, out failureReason)) { return false; } try { if (!Path.IsPathFullyQualified(sourcePath)) { failureReason = "Backup source must be an absolute path."; return false; } string fullPath = Path.GetFullPath(sourcePath); string fullPath2 = Path.GetFullPath(backupRoot); bool flag = kind == BackupSnapshotKind.World; if (PathsOverlap(flag ? fullPath : Path.GetDirectoryName(fullPath), fullPath2)) { failureReason = "Backup destination overlaps native saves."; return false; } CheckPathForReparsePoints(fullPath); CheckPathForReparsePoints(fullPath2); if (flag) { if (!Directory.Exists(fullPath)) { failureReason = "World source directory is missing."; return false; } } else if (!File.Exists(fullPath)) { failureReason = "Character source file is missing."; return false; } string text = Path.Combine(fullPath2, ".ModsmithCycleStaging"); Directory.CreateDirectory(text); string text2 = Path.Combine(text, operationId.ToString("N") + "-" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(text2); string text3 = Path.Combine(text2, "Files"); Directory.CreateDirectory(text3); List<BackupFileRecord> list = new List<BackupFileRecord>(); if (flag) { CopyDirectoryContents(fullPath, text3, string.Empty, list); } else { string fileName = Path.GetFileName(fullPath); CopyVerifiedFile(fullPath, Path.Combine(text3, fileName), fileName, list); } if (list.Count == 0) { failureReason = "No backup files were staged."; return false; } WriteManifest(text2, kind, stableIdentity, operationId, list); long num = 0L; foreach (BackupFileRecord item in list) { num += item.Length; } snapshot = new StagedBackupSnapshot(text2, text3, list.Count, num); return true; } catch (IOException) { failureReason = "Backup staging encountered a file error."; } catch (UnauthorizedAccessException) { failureReason = "Backup staging encountered an access error."; } catch (ArgumentException) { failureReason = "Backup source or destination path is invalid."; } catch (NotSupportedException) { failureReason = "Backup storage location is unsupported."; } catch (CryptographicException) { failureReason = "Backup file verification failed."; } return false; } private static void CopyDirectoryContents(string sourceDirectory, string destinationDirectory, string relativePrefix, List<BackupFileRecord> records) { foreach (string item in Directory.EnumerateFileSystemEntries(sourceDirectory)) { FileAttributes attributes = File.GetAttributes(item); if ((attributes & FileAttributes.ReparsePoint) != FileAttributes.None) { throw new IOException("Backup source contains a reparse point."); } string fileName = Path.GetFileName(item); string text = ((relativePrefix.Length == 0) ? fileName : (relativePrefix + "/" + fileName)); string text2 = Path.Combine(destinationDirectory, fileName); if ((attributes & FileAttributes.Directory) != FileAttributes.None) { Directory.CreateDirectory(text2); CopyDirectoryContents(item, text2, text, records); } else { CopyVerifiedFile(item, text2, text, records); } } TryPreserveDirectoryWriteTime(sourceDirectory, destinationDirectory); } private static void CopyVerifiedFile(string sourcePath, string destinationPath, string relativePath, List<BackupFileRecord> records) { FileInfo fileInfo = new FileInfo(sourcePath); long length = fileInfo.Length; DateTime lastWriteTimeUtc = fileInfo.LastWriteTimeUtc; long num = 0L; byte[] array = new byte[131072]; using (FileStream fileStream = new FileStream(sourcePath, FileMode.Open, FileAccess.Read, FileShare.Read)) { using FileStream fileStream2 = new FileStream(destinationPath, FileMode.CreateNew, FileAccess.Write, FileShare.None); int num2; while ((num2 = fileStream.Read(array, 0, array.Length)) > 0) { fileStream2.Write(array, 0, num2); num += num2; } fileStream2.Flush(flushToDisk: true); } FileInfo fileInfo2 = new FileInfo(sourcePath); if (num != length || fileInfo2.Length != length || fileInfo2.LastWriteTimeUtc != lastWriteTimeUtc) { throw new IOException("Backup source changed during copying."); } byte[] array2 = ComputeFileHash(sourcePath); byte[] array3 = ComputeFileHash(destinationPath); if (!CryptographicOperations.FixedTimeEquals(array2, array3)) { throw new IOException("Staged backup differs from its source."); } TryPreserveFileWriteTime(destinationPath, lastWriteTimeUtc); records.Add(new BackupFileRecord { RelativePath = relativePath, Length = num, Sha256 = array3 }); } private static void TryPreserveFileWriteTime(string destinationPath, DateTime originalWriteTime) { try { File.SetLastWriteTimeUtc(destinationPath, originalWriteTime); } catch (IOException) { } catch (UnauthorizedAccessException) { } catch (ArgumentException) { } catch (NotSupportedException) { } } private static void TryPreserveDirectoryWriteTime(string sourceDirectory, string destinationDirectory) { try { Directory.SetLastWriteTimeUtc(destinationDirectory, Directory.GetLastWriteTimeUtc(sourceDirectory)); } catch (IOException) { } catch (UnauthorizedAccessException) { } catch (ArgumentException) { } catch (NotSupportedException) { } } private static byte[] ComputeFileHash(string path) { using FileStream inputStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read); using SHA256 sHA = SHA256.Create(); return sHA.ComputeHash(inputStream); } private static void WriteManifest(string stagingDirectory, BackupSnapshotKind kind, string stableIdentity, Guid operationId, List<BackupFileRecord> records) { using FileStream fileStream = new FileStream(Path.Combine(stagingDirectory, "backup-manifest.txt"), FileMode.CreateNew, FileAccess.Write, FileShare.None); using (StreamWriter streamWriter = new StreamWriter(fileStream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false), 4096, leaveOpen: true)) { streamWriter.WriteLine("ManifestVersion=1"); streamWriter.WriteLine("OperationId=" + operationId.ToString("N")); streamWriter.WriteLine("Kind=" + kind); streamWriter.WriteLine("StableIdentity=" + Convert.ToBase64String(Encoding.UTF8.GetBytes(stableIdentity))); streamWriter.WriteLine("FileCount=" + records.Count.ToString(CultureInfo.InvariantCulture)); foreach (BackupFileRecord record in records) { string text = Convert.ToBase64String(Encoding.UTF8.GetBytes(record.RelativePath)); string text2 = BitConverter.ToString(record.Sha256).Replace("-", string.Empty); streamWriter.WriteLine(text + "|" + record.Length.ToString(CultureInfo.InvariantCulture) + "|" + text2); } streamWriter.Flush(); } fileStream.Flush(flushToDisk: true); } private static bool PathsOverlap(string firstPath, string secondPath) { if (!IsWithin(firstPath, secondPath)) { return IsWithin(secondPath, firstPath); } return true; } private static bool IsWithin(string candidate, string directory) { string text = Path.GetFullPath(candidate).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); string text2 = Path.GetFullPath(directory).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); if (string.Equals(text, text2, StringComparison.OrdinalIgnoreCase)) { return true; } return text.StartsWith(text2 + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase); } internal static bool TryPrepareReplacementCopies(StagedBackupSnapshot snapshot, BackupSnapshotKind kind, string stableIdentity, Guid operationId, BackupRotationLease lease, out string failureReason) { failureReason = null; if (snapshot == null || lease == null || operationId == Guid.Empty || string.IsNullOrWhiteSpace(stableIdentity)) { failureReason = "Replacement preparation information is incomplete."; return false; } if (!TryVerifyStagedSnapshot(snapshot, kind, stableIdentity, operationId, out failureReason)) { return false; } if (!BackupRotationJournal.TryReadTransaction(lease, out var state, out failureReason)) { return false; } if (state == null || state.IsCommitted || state.OperationId != operationId || !string.Equals(state.StableIdentity, stableIdentity, StringComparison.Ordinal)) { failureReason = "Prepared transaction does not match the requested backup operation."; return false; } try { string identityDirectory = lease.IdentityDirectory; string path = Path.Combine(identityDirectory, "Tx"); string[] array = new string[3] { snapshot.StagingDirectory, Path.Combine(identityDirectory, "Backup-1"), Path.Combine(identityDirectory, "Backup-2") }; bool[] array2 = new bool[3] { true, state.HadBackup1, state.HadBackup2 }; for (int i = 0; i < 3; i++) { string path2 = Path.Combine(path, "New-" + (i + 1)); if (Directory.Exists(path2) || File.Exists(path2)) { failureReason = "A prepared replacement folder already exists."; return false; } if (array2[i]) { string text = array[i]; if (!Directory.Exists(text)) { failureReason = "Required replacement source directory is missing."; return false; } CheckPathForReparsePoints(text); if (!File.Exists(Path.Combine(text, "backup-manifest.txt")) || !Directory.Exists(Path.Combine(text, "Files"))) { failureReason = "Replacement source is missing its backup structure."; return false; } if (i > 0 && !TryVerifyBackupFolder(text, kind, stableIdentity, null, out failureReason)) { failureReason = "Existing Backup-" + i + " failed verification: " + failureReason; return false; } } } for (int j = 0; j < 3; j++) { if (array2[j]) { string sourceDirectory = array[j]; string text2 = Path.Combine(path, "New-" + (j + 1)); Directory.CreateDirectory(text2); List<BackupFileRecord> list = new List<BackupFileRecord>(); CopyDirectoryContents(sourceDirectory, text2, string.Empty, list); if (list.Count == 0) { failureReason = "Prepared replacement contains no files."; return false; } if (!File.Exists(Path.Combine(text2, "backup-manifest.txt")) || !Directory.Exists(Path.Combine(text2, "Files"))) { failureReason = "Prepared replacement structure is incomplete."; return false; } Guid? expectedOperationId = ((j == 0) ? new Guid?(operationId) : ((Guid?)null)); if (!TryVerifyBackupFolder(text2, kind, stableIdentity, expectedOperationId, out failureReason)) { failureReason = "Prepared New-" + (j + 1) + " failed verification: " + failureReason; return false; } } } return true; } catch (PathTooLongException ex) { failureReason = "Replacement preparation exceeded the supported Windows path length: " + ex.Message; } catch (IOException ex2) { failureReason = "Replacement preparation I/O failure (" + ex2.GetType().Name + ", HResult=0x" + ex2.HResult.ToString("X8") + "): " + ex2.Message; } catch (UnauthorizedAccessException) { failureReason = "Replacement preparation was denied access."; } catch (ArgumentException) { failureReason = "Replacement preparation path is invalid."; } catch (NotSupportedException) { failureReason = "Replacement storage location is unsupported."; } catch (CryptographicException) { failureReason = "Replacement file verification failed."; } return false; } internal static bool TryVerifyBackupFolder(string folderPath, BackupSnapshotKind expectedKind, string expectedStableIdentity, Guid? expectedOperationId, out string failureReason) { failureReason = null; if (string.IsNullOrWhiteSpace(folderPath) || string.IsNullOrWhiteSpace(expectedStableIdentity)) { failureReason = "Backup folder verification information is incomplete."; return false; } try { if (!SaveBackupCoordinator.TryGetBackupRoot(out var backupRoot, out failureReason)) { return false; } string fullPath = Path.GetFullPath(backupRoot); string fullPath2 = Path.GetFullPath(folderPath); if (!IsWithin(fullPath2, fullPath) || string.Equals(fullPath2, fullPath, StringComparison.OrdinalIgnoreCase)) { failureReason = "Backup folder is outside the configured backup area."; return false; } if (!Directory.Exists(fullPath2)) { failureReason = "Backup folder does not exist."; return false; } CheckPathForReparsePoints(fullPath2); string path = Path.Combine(fullPath2, "backup-manifest.txt"); string text = Path.Combine(fullPath2, "Files"); if (!File.Exists(path) || !Directory.Exists(text)) { failureReason = "Backup folder is missing its manifest or Files directory."; return false; } CheckPathForReparsePoints(path); CheckPathForReparsePoints(text); foreach (string item in Directory.EnumerateFileSystemEntries(fullPath2)) { string fileName = Path.GetFileName(item); if (!string.Equals(fileName, "backup-manifest.txt", StringComparison.OrdinalIgnoreCase) && !string.Equals(fileName, "Files", StringComparison.OrdinalIgnoreCase)) { failureReason = "Backup folder contains an unexpected root entry."; return false; } } string[] array = File.ReadAllLines(path); if (array.Length < 6 || array[0] != "ManifestVersion=1") { failureReason = "Backup manifest is incomplete or unsupported."; return false; } if (!array[1].StartsWith("OperationId=", StringComparison.Ordinal) || !Guid.TryParseExact(array[1].Substring("OperationId=".Length), "N", out var result)) { failureReason = "Backup manifest operation ID is invalid."; return false; } if (expectedOperationId.HasValue && result != expectedOperationId.Value) { failureReason = "Backup manifest operation ID does not match."; return false; } if (array[2] != "Kind=" + expectedKind) { failureReason = "Backup manifest save kind does not match."; return false; } string text2 = Convert.ToBase64String(Encoding.UTF8.GetBytes(expectedStableIdentity)); if (array[3] != "StableIdentity=" + text2) { failureReason = "Backup manifest identity does not match."; return false; } if (!array[4].StartsWith("FileCount=", StringComparison.Ordinal) || !int.TryParse(array[4].Substring("FileCount=".Length), NumberStyles.None, CultureInfo.InvariantCulture, out var result2) || result2 <= 0 || result2 > 1000000 || array.Length != result2 + 5) { failureReason = "Backup manifest file count is invalid."; return false; } HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase); for (int i = 5; i < array.Length; i++) { string[] array2 = array[i].Split('|'); if (array2.Length != 3) { failureReason = "Backup manifest contains a malformed file record."; return false; } string text3 = Encoding.UTF8.GetString(Convert.FromBase64String(array2[0])); if (!IsSafeRelativeBackupPath(text3) || !hashSet.Add(text3)) { failureReason = "Backup manifest contains an unsafe or duplicate path."; return false; } if (!long.TryParse(array2[1], NumberStyles.None, CultureInfo.InvariantCulture, out var result3) || result3 < 0) { failureReason = "Backup manifest contains an invalid file size."; return false; } string fullPath3 = Path.GetFullPath(Path.Combine(text, text3.Replace('/', Path.DirectorySeparatorChar))); if (!IsWithin(fullPath3, text)) { failureReason = "Backup file escapes its content directory."; return false; } CheckPathForReparsePoints(fullPath3); FileInfo fileInfo = new FileInfo(fullPath3); if (!fileInfo.Exists || fileInfo.Length != result3) { failureReason = "Backup file is missing or has an incorrect size."; return false; } if (!string.Equals(BitConverter.ToString(ComputeFileHash(fullPath3)).Replace("-", string.Empty), array2[2], StringComparison.OrdinalIgnoreCase)) { failureReason = "Backup file SHA-256 fingerprint does not match."; return false; } } HashSet<string> hashSet2 = new HashSet<string>(StringComparer.OrdinalIgnoreCase); CollectStagedFileNames(text, string.Empty, hashSet2); if (!hashSet.SetEquals(hashSet2)) { failureReason = "Backup contents do not match the manifest inventory."; return false; } return true; } catch (IOException) { failureReason = "Backup verification encountered a filesystem error."; } catch (UnauthorizedAccessException) { failureReason = "Backup verification was denied access."; } catch (ArgumentException) { failureReason = "Backup verification encountered an invalid path."; } catch (FormatException) { failureReason = "Backup manifest encoding is invalid."; } catch (NotSupportedException) { failureReason = "Backup verification encountered an unsupported path."; } catch (CryptographicException) { failureReason = "Backup fingerprint verification failed."; } return false; } internal static bool TryPreflightPreparedInstallation(BackupRotationLease lease, BackupSnapshotKind kind, string stableIdentity, Guid operationId, out string failureReason) { failureReason = null; if (lease == null || operationId == Guid.Empty || string.IsNullOrWhiteSpace(stableIdentity)) { failureReason = "Installation preflight information is incomplete."; return false; } if (!BackupRotationJournal.TryReadTransaction(lease, out var state, out failureReason)) { return false; } if (state == null || state.IsCommitted || state.OperationId != operationId || !string.Equals(state.StableIdentity, stableIdentity, StringComparison.Ordinal)) { failureReason = "Installation journal does not match this operation."; return false; } try { string identityDirectory = lease.IdentityDirectory; string b = ((kind == BackupSnapshotKind.Character) ? "Characters" : "Worlds"); if (!string.Equals(Path.GetFileName(Path.GetDirectoryName(identityDirectory)), b, StringComparison.OrdinalIgnoreCase)) { failureReason = "Backup directory does not match the save kind."; return false; } string path = Path.Combine(identityDirectory, "Tx"); if (File.Exists(Path.Combine(path, "committed.marker.tmp")) || File.Exists(Path.Combine(path, "rollback.completed")) || File.Exists(Path.Combine(path, "rollback.completed.tmp"))) { failureReason = "Transaction contains recovery or incomplete markers."; return false; } bool[] array = new bool[3] { state.HadBackup1, state.HadBackup2, state.HadBackup3 }; bool[] array2 = new bool[3] { true, state.HadBackup1, state.HadBackup2 }; for (int i = 1; i <= 3; i++) { string text = Path.Combine(identityDirectory, "Backup-" + i); string path2 = Path.Combine(path, "Old-" + i); string text2 = Path.Combine(path, "New-" + i); CheckPathForReparsePoints(text); CheckPathForReparsePoints(path2); CheckPathForReparsePoints(text2); if (File.Exists(text) || File.Exists(path2) || File.Exists(text2)) { failureReason = "Installation encountered a file where a backup directory was expected."; return false; } if (Directory.Exists(path2)) { failureReason = "An original backup was already moved. Recovery is required."; return false; } if (Directory.Exists(text) != array[i - 1]) { failureReason = "Visible backup slots changed after the transaction was prepared."; return false; } if (Directory.Exists(text2) != array2[i - 1]) { failureReason = "Prepared replacement slots are incomplete."; return false; } if (array[i - 1] && !TryVerifyBackupFolder(text, kind, stableIdentity, null, out failureReason)) { failureReason = "Visible Backup-" + i + " failed verification: " + failureReason; return false; } if (array2[i - 1]) { Guid? expectedOperationId = ((i == 1) ? new Guid?(operationId) : ((Guid?)null)); if (!TryVerifyBackupFolder(text2, kind, stableIdentity, expectedOperationId, out failureReason)) { failureReason = "Prepared New-" + i + " failed verification: " + failureReason; return false; } } } for (int j = 1; j <= 2; j++) { if (array[j - 1]) { string path3 = Path.Combine(identityDirectory, "Backup-" + j, "backup-manifest.txt"); string path4 = Path.Combine(path, "New-" + (j + 1), "backup-manifest.txt"); byte[] array3 = ComputeFileHash(path3); if (!CryptographicOperations.FixedTimeEquals(right: ComputeFileHash(path4), left: array3)) { failureReason = "Existing backup history changed after replacement preparation."; return false; } } } return true; } catch (IOException) { failureReason = "Installation preflight encountered a filesystem error."; } catch (UnauthorizedAccessException) { failureReason = "Installation preflight was denied access."; } catch (ArgumentException) { failureReason = "Installation preflight encountered an invalid path."; } catch (NotSupportedException) { failureReason = "Installation preflight encountered an unsupported path."; } catch (CryptographicException) { failureReason = "Installation preflight fingerprint verification failed."; } return false; } internal static bool TryInstallPreparedReplacementCopies(BackupRotationLease lease, BackupSnapshotKind kind, string stableIdentity, Guid operationId, out string failureReason) { failureReason = null; if (!TryPreflightPreparedInstallation(lease, kind, stableIdentity, operationId, out failureReason)) { return false; } if (!BackupRotationJournal.TryReadTransaction(lease, out var state, out failureReason)) { return false; } if (state == null || state.IsCommitted || state.OperationId != operationId || !string.Equals(state.StableIdentity, stableIdentity, StringComparison.Ordinal)) { failureReason = "Installation journal changed after preflight."; return false; } try { string identityDirectory = lease.IdentityDirectory; string path = Path.Combine(identityDirectory, "Tx"); bool[] array = new bool[3] { state.HadBackup1, state.HadBackup2, state.HadBackup3 }; bool[] array2 = new bool[3] { true, state.HadBackup1, state.HadBackup2 }; long num = 0L; for (int i = 1; i <= 3; i++) { if (!array2[i - 1]) { continue; } foreach (string item in Directory.EnumerateFiles(Path.Combine(path, "New-" + i), "*", SearchOption.AllDirectories)) { num = checked(num + new FileInfo(item).Length); } } string pathRoot = Path.GetPathRoot(identityDirectory); if (string.IsNullOrWhiteSpace(pathRoot)) { failureReason = "Installation storage volume cannot be identified."; return false; } DriveInfo driveInfo = new DriveInfo(pathRoot); if (!driveInfo.IsReady) { failureReason = "Installation storage volume is unavailable."; return false; } long num2 = Math.Max(67108864L, num / 20); long num3 = checked(num + num2); if (driveInfo.AvailableFreeSpace < num3) { failureReason = "Insufficient free space to install the prepared backup copies safely."; return false; } for (int j = 1; j <= 3; j++) { if (array[j - 1]) { string sourceDirName = Path.Combine(identityDirectory, "Backup-" + j); string text = Path.Combine(path, "Old-" + j); Directory.Move(sourceDirName, text); if (!TryVerifyBackupFolder(text, kind, stableIdentity, null, out failureReason)) { failureReason = "Preserved Old-" + j + " failed verification: " + failureReason; return false; } } } for (int k = 1; k <= 3; k++) { if (array2[k - 1]) { string sourceDirectory = Path.Combine(path, "New-" + k); string text2 = Path.Combine(identityDirectory, "Backup-" + k); Directory.CreateDirectory(text2); List<BackupFileRecord> list = new List<BackupFileRecord>(); CopyDirectoryContents(sourceDirectory, text2, string.Empty, list); if (list.Count == 0) { failureReason = "Installed Backup-" + k + " contains no copied files."; return false; } Guid? expectedOperationId = ((k == 1) ? new Guid?(operationId) : ((Guid?)null)); if (!TryVerifyBackupFolder(text2, kind, stableIdentity, expectedOperationId, out failureReason)) { failureReason = "Installed Backup-" + k + " failed verification: " + failureReason; return false; } } } for (int l = 1; l <= 3; l++) { if (Directory.Exists(Path.Combine(identityDirectory, "Backup-" + l)) != array2[l - 1]) { failureReason = "Installed backup layout does not match the expected rotation."; return false; } } return true; } catch (IOException) { failureReason = "Backup installation encountered a filesystem error."; } catch (UnauthorizedAccessException) { failureReason = "Backup installation was denied access."; } catch (ArgumentException) { failureReason = "Backup installation encountered an invalid path."; } catch (NotSupportedException) { failureReason = "Backup installation storage is unsupported."; } catch (OverflowException) { failureReason = "Backup installation size calculation overflowed."; } catch (CryptographicException) { failureReason = "Backup installation fingerprint verification failed."; } return false; } internal static bool TryVerifyStagedSnapshot(StagedBackupSnapshot snapshot, BackupSnapshotKind expectedKind, string expectedStableIdentity, Guid expectedOperationId, out string failureReason) { failureReason = null; if (snapshot == null) { failureReason = "No staged backup was supplied."; return false; } if (expectedOperationId == Guid.Empty || string.IsNullOrWhiteSpace(expectedStableIdentity)) { failureReason = "Expected backup identity is incomplete."; return false; } try { if (!SaveBackupCoordinator.TryGetBackupRoot(out var backupRoot, out failureReason)) { return false; } string fullPath = Path.GetFullPath(Path.Combine(backupRoot, ".ModsmithCycleStaging")); string fullPath2 = Path.GetFullPath(snapshot.StagingDirectory); string fullPath3 = Path.GetFullPath(snapshot.ContentDirectory); if (!IsWithin(fullPath2, fullPath) || string.Equals(fullPath2, fullPath, StringComparison.OrdinalIgnoreCase) || !IsWithin(fullPath3, fullPath2)) { failureReason = "Staging location is outside the expected backup area."; return false; } CheckPathForReparsePoints(fullPath2); CheckPathForReparsePoints(fullPath3); string[] array = File.ReadAllLines(Path.Combine(fullPath2, "backup-manifest.txt")); if (array.Length < 6) { failureReason = "Staging manifest is incomplete."; return false; } if (array[0] != "ManifestVersion=1") { failureReason = "Staging manifest version is unsupported."; return false; } string text = "OperationId=" + expectedOperationId.ToString("N"); if (array[1] != text) { failureReason = "Staging operation ID does not match."; return false; } if (array[2] != "Kind=" + expectedKind) { failureReason = "Staging save kind does not match."; return false; } string text2 = "StableIdentity=" + Convert.ToBase64String(Encoding.UTF8.GetBytes(expectedStableIdentity)); if (array[3] != text2) { failureReason = "Staging save identity does not match."; return false; } if (!array[4].StartsWith("FileCount=", StringComparison.Ordinal)) { failureReason = "Staging file count is missing."; return false; } if (!int.TryParse(array[4].Substring("FileCount=".Length), NumberStyles.None, CultureInfo.InvariantCulture, out var result)) { failureReason = "Staging file count is invalid."; return false; } if (result <= 0 || result > 1000000 || result != snapshot.FileCount || array.Length != result + 5) { failureReason = "Staging manifest file count does not match."; return false; } HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase); long num = 0L; for (int i = 5; i < array.Length; i++) { string[] array2 = array[i].Split('|'); if (array2.Length != 3) { failureReason = "Staging manifest contains a malformed file record."; return false; } string text3 = Encoding.UTF8.GetString(Convert.FromBase64String(array2[0])); if (!IsSafeRelativeBackupPath(text3)) { failureReason = "Staging manifest contains an unsafe relative path."; return false; } if (!hashSet.Add(text3)) { failureReason = "Staging manifest contains duplicate files."; return false; } if (!long.TryParse(array2[1], NumberStyles.None, CultureInfo.InvariantCulture, out var result2) || result2 < 0) { failureReason = "Staging file size is invalid."; return false; } string fullPath4 = Path.GetFullPath(Path.Combine(fullPath3, text3.Replace('/', Path.DirectorySeparatorChar))); if (!IsWithin(fullPath4, fullPath3)) { failureReason = "Staging file escapes its directory."; return false; } CheckPathForReparsePoints(fullPath4); FileInfo fileInfo = new FileInfo(fullPath4); if (!fileInfo.Exists || fileInfo.Length != result2) { failureReason = "Staging file is missing or incomplete."; return false; } if (!string.Equals(BitConverter.ToString(ComputeFileHash(fullPath4)).Replace("-", string.Empty), array2[2], StringComparison.OrdinalIgnoreCase)) { failureReason = "Staging file fingerprint does not match."; return false; } num = checked(num + fileInfo.Length); } HashSet<string> hashSet2 = new HashSet<string>(StringComparer.OrdinalIgnoreCase); CollectStagedFileNames(fullPath3, string.Empty, hashSet2); if (!hashSet.SetEquals(hashSet2)) { failureReason = "Staging directory contains unlisted or missing files."; return false; } if (num != snapshot.TotalBytes) { failureReason = "Staging total byte count does not match."; return false; } return true; } catch (InvalidDataException) { failureReason = "Staging data failed validation."; } catch (IOException) { failureReason = "Staging verification encountered a file error."; } catch (UnauthorizedAccessException) { failureReason = "Staging verification was denied access."; } catch (ArgumentException) { failureReason = "Staging manifest contains invalid data."; } catch (FormatException) { failureReason = "Staging manifest encoding is invalid."; } catch (NotSupportedException) { failureReason = "Staging verification encountered an unsupported path."; } catch (OverflowException) { failureReason = "Staging manifest contains an invalid size."; } catch (CryptographicException) { failureReason = "Staging fingerprint calculation failed."; } return false; } private static bool IsSafeRelativeBackupPath(string relativePath) { if (string.IsNullOrWhiteSpace(relativePath) || relativePath.Contains("\\") || Path.IsPathRooted(relativePath)) { return false; } string[] array = relativePath.Split('/'); foreach (string text in array) { if (text.Length == 0 || text == "." || text == ".." || text.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0) { return false; } } return true; } private static void CollectStagedFileNames(string directory, string relativePrefix, HashSet<string> results) { foreach (string item in Directory.EnumerateFileSystemEntries(directory)) { FileAttributes attributes = File.GetAttributes(item); if ((attributes & FileAttributes.ReparsePoint) != FileAttributes.None) { throw new IOException("Staging directory contains a reparse point."); } string fileName = Path.GetFileName(item); string text = ((relativePrefix.Length == 0) ? fileName : (relativePrefix + "/" + fileName)); if ((attributes & FileAttributes.Directory) != FileAttributes.None) { CollectStagedFileNames(item, text, results); } else if (!results.Add(text)) { throw new InvalidDataException("Duplicate staged file."); } } } private static void CheckPathForReparsePoints(string path) { string fullPath = Path.GetFullPath(path); if (File.Exists(fullPath) && (File.GetAttributes(fullPath) & FileAttributes.ReparsePoint) != FileAttributes.None) { throw new IOException("Backup path contains a reparse point."); } for (DirectoryInfo directoryInfo = new DirectoryInfo(Directory.Exists(fullPath) ? fullPath : Path.GetDirectoryName(fullPath)); directoryInfo != null; directoryInfo = directoryInfo.Parent) { if (directoryInfo.Exists && (directoryInfo.Attributes & FileAttributes.ReparsePoint) != FileAttributes.None) { throw new IOException("Backup path contains a reparse point."); } } } } internal sealed class CharacterBackupIdentity { internal long PlayerId { get; } internal string DisplayName { get; } internal string SaveFilename { get; } internal CharacterBackupIdentity(long playerId, string displayName, string saveFilename) { PlayerId = playerId; DisplayName = displayName; SaveFilename = saveFilename; } } internal static class CharacterSaveBackupService { private const int ExpectedHashLength = 64; private const int CopyBufferSize = 65536; internal static bool TryCaptureIdentity(PlayerProfile profile, out CharacterBackupIdentity identity, out string failureReason) { identity = null; failureReason = null; if (profile == null) { failureReason = "No active character profile was supplied."; return false; } try { long playerID = profile.GetPlayerID(); string name = profile.GetName(); string filename = profile.GetFilename(); if (playerID == 0L) { failureReason = "Character has no valid player ID."; return false; } if (string.IsNullOrWhiteSpace(name)) { failureReason = "Character display name is missing."; return false; } if (string.IsNullOrWhiteSpace(filename)) { failureReason = "Character save filename is missing."; return false; } if (filename == "." || filename == ".." || filename.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0) { failureReason = "Character save filename is invalid."; return false; } identity = new CharacterBackupIdentity(playerID, name, filename); return true; } catch (Exception ex) { failureReason = "Could not capture character identity: " + ex.GetType().Name; return false; } } internal static bool TryVerifyLocalEnvelope(string sourcePath, byte[] expectedPayloadHash, out string failureReason) { failureReason = null; if (string.IsNullOrWhiteSpace(sourcePath)) { failureReason = "Character source path is missing."; return false; } if (expectedPayloadHash == null || expectedPayloadHash.Length != 64) { failureReason = "Expected character save hash is unavailable."; return false; } try { if (!Path.IsPathFullyQualified(sourcePath)) { failureReason = "Character source is not a local absolute path."; return false; } using (FileStream fileStream = new FileStream(sourcePath, FileMode.Open, FileAccess.Read, FileShare.Read)) { using BinaryReader binaryReader = new BinaryReader(fileStream); long length = fileStream.Length; if (length < 72) { failureReason = "Character file is incomplete."; return false; } int num = binaryReader.ReadInt32(); if (num <= 0) { failureReason = "Character payload length is invalid."; return false; } long num2 = 4L + (long)num + 4 + 64; if (length != num2) { failureReason = "Character file length does not match its declared contents."; return false; } byte[] hashAndReset; using (IncrementalHash incrementalHash = IncrementalHash.CreateHash(HashAlgorithmName.SHA512)) { byte[] array = new byte[65536]; int num3 = num; while (num3 > 0) { int count = Math.Min(num3, array.Length); int num4 = binaryReader.Read(array, 0, count); if (num4 <= 0) { failureReason = "Character payload ended early."; return false; } incrementalHash.AppendData(array, 0, num4); num3 -= num4; } hashAndReset = incrementalHash.GetHashAndReset(); } if (binaryReader.ReadInt32() != 64) { failureReason = "Character hash length is invalid."; return false; } byte[] array2 = binaryReader.ReadBytes(64); if (array2.Length != 64) { failureReason = "Character hash is incomplete."; return false; } if (fileStream.Position != length) { failureReason = "Unexpected trailing character data."; return false; } if (!CryptographicOperations.FixedTimeEquals(hashAndReset, array2)) { failureReason = "Character file failed its integrity check."; return false; } if (!CryptographicOperations.FixedTimeEquals(hashAndReset, expectedPayloadHash)) { failureReason = "Character file does not match the expected save operation."; return false; } } return true; } catch (IOException) { failureReason = "Character save could not be read consistently."; } catch (UnauthorizedAccessException) { failureReason = "Access to the character save was denied."; } catch (ArgumentException) { failureReason = "Character source path is invalid."; } catch (NotSupportedException) { failureReason = "Character source path is unsupported."; } catch (CryptographicException) { failureReason = "Character hash verification failed."; } return false; } } internal static class CloudExitSnapshotService { private const int MaxFileBytes = 268435456; private const long MaxTotalBytes = 3221225472L; private const int MaxFiles = 100000; private const string PrivateFolder = ".ModsmithCycleCloudCapture"; private static string Describe(Exception ex) { string text = (ex.Message ?? string.Empty).Replace('\r', ' ').Replace('\n', ' '); if (text.Length > 240) { text = text.Substring(0, 240); } return ex.GetType().Name + ": " + text; } private static bool TryReadExact(string key, out byte[] data, out string reason) { data = null; reason = null; try { int fileSize = SteamRemoteStorage.GetFileSize(key); if (fileSize <= 0 || fileSize > 268435456) { reason = "Cloud key has invalid/unsupported size: " + key; return false; } byte[] array = new byte[fileSize]; int actualBytesRead = SteamRemoteStorage.FileRead(key, array, fileSize); if (!CloudSaveService.TryVerifyCloudRead(key, (ulong)fileSize, array, actualBytesRead, out var _, out reason)) { return false; } data = array; return true; } catch (Exception ex) { reason = "Cloud read could not complete (" + Describe(ex) + ")"; return false; } } private static string CreatePrivateCapture(Guid id) { if (!SaveBackupCoordinator.TryGetDataRoot(out var dataRoot, out var failureReason)) { throw new IOException("ModsmithCycle data directory is unavailable: " + failureReason); } string text = Path.Combine(dataRoot, ".ModsmithCycleCloudCapture"); Directory.CreateDirectory(text); string text2 = Path.Combine(text, id.ToString("N") + "-" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(text2); return text2; } private static void WriteExact(string path, byte[] data) { using FileStream fileStream = new FileStream(path, FileMode.CreateNew, FileAccess.Write, FileShare.None); fileStream.Write(data, 0, data.Length); fileStream.Flush(flushToDisk: true); } private static bool SameBytes(byte[] first, byte[] second) { if (first == null || second == null || first.Length != second.Length) { return false; } using SHA256 sHA = SHA256.Create(); byte[] first2 = sHA.ComputeHash(first); byte[] second2 = sHA.ComputeHash(second); return CryptographicOperationsUnavailableSafeEquals(first2, second2); } private static bool CryptographicOperationsUnavailableSafeEquals(byte[] first, byte[] second) { if (first.Length != second.Length) { return false; } int num = 0; for (int i = 0; i < first.Length; i++) { num |= first[i] ^ second[i]; } return num == 0; } internal static bool TryCaptureCharacter(LiveSaveBackupService.CharacterCandidate character, Guid operationId, out string savedPath, out string captureDirectory, out string reason) { savedPath = null; captureDirectory = null; reason = null; string characterPath = SaveSystem.GetCharacterPath((FileSource)4, character.Identity.SaveFilename); if (!string.Equals(character.Path, characterPath, StringComparison.Ordinal)) { reason = "Character Cloud publication did not match its native key."; return false; } if (!TryReadExact(characterPath, out var data, out reason) || !TryReadExact(characterPath, out var data2, out reason)) { return false; } if (!SameBytes(data, data2)) { reason = "Character Cloud content changed between complete reads."; return false; } try { captureDirectory = CreatePrivateCapture(operationId); savedPath = Path.Combine(captureDirectory, character.Identity.SaveFilename + ".fch"); WriteExact(savedPath, data); return true; } catch (Exception ex) { reason = "Character Cloud staging failed (" + Describe(ex) + ")"; return false; } } private static bool TryEnumerate(out string[] all, out string reason) { all = null; reason = null; try { int fileCount = SteamRemoteStorage.GetFileCount(); if (fileCount <= 0 || fileCount > 100000) { reason = "Cloud enumeration reported invalid file count."; return false; } string[] array = new string[fileCount]; int num = default(int); for (int i = 0; i < fileCount; i++) { array[i] = SteamRemoteStorage.GetFileNameAndSize(i, ref num); if (string.IsNullOrWhiteSpace(array[i]) || num < 0) { reason = "Cloud enumeration returned an invalid entry."; return false; } } all = array; return true; } catch (Exception ex) { reason = "Cloud enumeration failed (" + Describe(ex) + ")"; return false; } } internal static bool TryCaptureWorld(WorldBackupIdentity world, uint generation, Guid operationId, out string savedDirectory, out string captureDirectory, out string reason) { savedDirectory = null; captureDirectory = null; reason = null; string text = World.GetSaveDirectory((FileSource)4, world.SaveName).Replace('\\', '/').TrimStart('/'); string text2 = text.TrimEnd('/') + "/"; if (!TryEnumerate(out var all, out reason)) { return false; } List<string> list = new List<string>(); HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase); string[] array = all; foreach (string text3 in array) { string text4 = text3.Replace('\\', '/'); if (text4.StartsWith(text2, StringComparison.OrdinalIgnoreCase)) { string text5 = text4.Substring(text2.Length); if (text5.Length == 0 || text5.IndexOf('/') >= 0 || text5 == "." || text5 == ".." || text5.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0 || !hashSet.Add(text4)) { reason = "Cloud world contains an invalid/duplicate key."; return false; } list.Add(text3); } } if (list.Count < 4) { string text6 = "worlds/" + world.SaveName + "/"; int num = 0; int num2 = 0; string text7 = null; array = all; for (int i = 0; i < array.Length; i++) { string text8 = array[i].Replace('\\', '/'); if (!text8.StartsWith(text6, StringComparison.OrdinalIgnoreCase)) { continue; } num++; if (text8.Substring(text6.Length).StartsWith("_main.", StringComparison.OrdinalIgnoreCase)) { num2++; if (text7 == null) { text7 = text8; } } } Squirrel.Warning(SquirrelChannel.Backup, "[ExitBackup] Cloud world listing diagnostic: expectedPrefix=" + text2 + "; matched=" + list.Count + "; totalKeys=" + all.Length + "; directWorldMatches=" + num + "; directMainFiles=" + num2 + "; exampleMainKey=" + (text7 ?? "<none>") + "."); reason = "Cloud world does not have its four generation files."; return false; } try { captureDirectory = CreatePrivateCapture(operationId); Dictionary<string, byte[]> dictionary = new Dictionary<string, byte[]>(StringComparer.OrdinalIgnoreCase); long num3 = 0L; foreach (string item in list) { if (!TryReadExact(item, out var data, out reason)) { return false; } num3 = checked(num3 + data.LongLength); if (num3 > 3221225472u) { reason = "Cloud world exceeds the configured safety size limit."; return false; } string path = item.Replace('\\', '/').Substring(text2.Length); WriteExact(Path.Combine(captureDirectory, path), data); using SHA256 sHA = SHA256.Create(); dictionary[item] = sHA.ComputeHash(data); } if (!TryEnumerate(out var all2, out reason)) { return false; } if (!CloudSaveService.TryConfirmWorldFileListing(text, generation, all, all2, out var _, out reason)) { return false; } foreach (string item2 in list) { if (!TryReadExact(item2, out var data2, out reason)) { return false; } using SHA256 sHA2 = SHA256.Create(); byte[] second = sHA2.ComputeHash(data2); if (!CryptographicOperationsUnavailableSafeEquals(dictionary[item2], second)) { reason = "Cloud world content changed during capture."; return false; } } if (!TryEnumerate(out var all3, out reason)) { return false; } if (!CloudSaveService.TryConfirmWorldFileListing(text, generation, all, all3, out var _, out reason)) { return false; } savedDirectory = captureDirectory; return true; } catch (Exception ex) { reason = "Cloud world capture failed (" + Describe(ex) + ")"; return false; } } internal static void TryCleanupPrivateCapture(string path) { if (string.IsNullOrWhiteSpace(path)) { return; } try { if (!SaveBackupCoordinator.TryGetDataRoot(out var dataRoot, out var _)) { return; } string fullPath = Path.GetFullPath(Path.Combine(dataRoot, ".ModsmithCycleCloudCapture")); string fullPath2 = Path.GetFullPath(path); string fileName = Path.GetFileName(fullPath2); if (!string.Equals(Path.GetDirectoryName(fullPath2), fullPath, StringComparison.OrdinalIgnoreCase) || fileName.Length != 65 || fileName[32] != '-' || !Guid.TryParseExact(fileName.Substring(0, 32), "N", out var _) || !Guid.TryParseExact(fileName.Substring(33), "N", out var _) || !Directory.Exists(fullPath2) || (File.GetAttributes(fullPath2) & FileAttributes.ReparsePoint) != FileAttributes.None) { return; } foreach (string item in Directory.EnumerateFileSystemEntries(fullPath2)) { if ((File.GetAttributes(item) & FileAttributes.ReparsePoint) != FileAttributes.None) { return; } } Directory.Delete(fullPath2, recursive: true); } catch (Exception ex) { Squirrel.Warning(SquirrelChannel.Backup, "[ExitBackup] Private Cloud capture cleanup failed: " + Describe(ex)); } } } internal sealed class CloudReadReceipt { private readonly byte[] _sha256; internal string LogicalKey { get; } internal long ByteCount { get; } internal byte[] GetSha256() { return (byte[])_sha256.Clone(); } internal CloudReadReceipt(string logicalKey, long byteCount, byte[] sha256) { LogicalKey = logicalKey; ByteCount = byteCount; _sha256 = (byte[])sha256.Clone(); } } internal static class CloudSaveService { private const int Sha256Length = 32; internal static bool TryVerifyCloudRead(string logicalKey, ulong providerReportedSize, byte[] data, int actualBytesRead, out CloudReadReceipt receipt, out string failureReason) { receipt = null; failureReason = null; if (string.IsNullOrWhiteSpace(logicalKey)) { failureReason = "Cloud file identity is missing."; return false; } if (providerReportedSize == 0L || providerReportedSize > int.MaxValue) { failureReason = "Cloud file size is invalid or unsupported."; return false; } if (data == null) { failureReason = "Cloud provider returned no data."; return false; } if (actualBytesRead <= 0) { failureReason = "Cloud provider did not report a successful full read."; return false; } if (data.LongLength != (long)providerReportedSize) { failureReason = "Cloud read buffer size does not match the provider's reported file size."; return false; } if ((ulong)actualBytesRead != providerReportedSize) { failureReason = "Cloud read was incomplete."; return false; } try { byte[] array; using (SHA256 sHA = SHA256.Create()) { array = sHA.ComputeHash(data, 0, actualBytesRead); } if (array.Length != 32) { failureReason = "Cloud file fingerprint is invalid."; return false; } receipt = new CloudReadReceipt(logicalKey, actualBytesRead, array); return true; } catch (CryptographicException) { failureReason = "Cloud file fingerprint calculation failed."; return false; } } internal static bool TryConfirmWorldFileListing(string cloudWorldDirectory, uint expectedGeneration, string[] beforeRead, string[] afterRead, out string[] confirmedFiles, out string failureReason) { confirmedFiles = null; failureReason = null; if (string.IsNullOrWhiteSpace(cloudWorldDirectory)) { failureReason = "Cloud world directory is missing."; return false; } if (expectedGeneration == 0) { failureReason = "Expected Cloud world generation is missing."; return false; } if (beforeRead == null || afterRead == null) { failureReason = "Cloud file enumeration is incomplete."; return false; } string text = cloudWorldDirectory.Replace('\\', '/').TrimEnd('/') + "/"; HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase); HashSet<string> hashSet2 = new HashSet<string>(StringComparer.OrdinalIgnoreCase); if (!CollectWorldFiles(beforeRead,
ModsmithCycle.Restarter.exe
Decompiled a day agousing 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.Versioning; using System.Text; using System.Threading; using Microsoft.CodeAnalysis; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] [assembly: AssemblyCompany("ModsmithCycle.Restarter")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("0.1.3.0")] [assembly: AssemblyInformationalVersion("0.1.3+51dd64e63e3ff3f81ddf60d59666c64cadea44df")] [assembly: AssemblyProduct("ModsmithCycle.Restarter")] [assembly: AssemblyTitle("ModsmithCycle.Restarter")] [assembly: AssemblyVersion("0.1.3.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 ModsmithCycle.Restarter { internal static class Program { private sealed class LaunchRequest { public int ParentProcessId; public string SteamExecutablePath; public string[] ProfileArguments; public bool HelperDiagnostics; public string HelperLogDirectory; public static LaunchRequest Load(string path) { string[] array = File.ReadAllLines(path); if (array.Length == 0 || !string.Equals(array[0], "MODSMITH_CYCLE_LAUNCH_V2", StringComparison.Ordinal)) { throw new InvalidDataException("Unsupported restart request."); } LaunchRequest launchRequest = new LaunchRequest(); List<string> list = new List<string>(); bool flag = false; bool flag2 = false; for (int i = 1; i < array.Length; i++) { string text = array[i]; int num = text.IndexOf('='); if (num <= 0) { continue; } string a = text.Substring(0, num); string text2 = text.Substring(num + 1); if (string.Equals(a, "ParentProcessId", StringComparison.Ordinal)) { if (!int.TryParse(text2, NumberStyles.Integer, CultureInfo.InvariantCulture, out launchRequest.ParentProcessId)) { throw new InvalidDataException("Invalid parent process id."); } flag = true; } else if (string.Equals(a, "SteamExecutablePath", StringComparison.Ordinal)) { launchRequest.SteamExecutablePath = Decode(text2); flag2 = true; } else if (string.Equals(a, "HelperDiagnostics", StringComparison.Ordinal)) { launchRequest.HelperDiagnostics = string.Equals(text2, "1", StringComparison.Ordinal); } else if (string.Equals(a, "HelperLogDirectory", StringComparison.Ordinal)) { launchRequest.HelperLogDirectory = Decode(text2); } else if (string.Equals(a, "Argument", StringComparison.Ordinal)) { list.Add(Decode(text2)); } } if (!flag) { throw new InvalidDataException("Missing parent process id."); } if (!flag2 || string.IsNullOrEmpty(launchRequest.SteamExecutablePath)) { throw new InvalidDataException("Missing Steam executable path."); } launchRequest.ProfileArguments = list.ToArray(); return launchRequest; } private static string Decode(string value) { if (string.IsNullOrEmpty(value)) { return string.Empty; } return Encoding.UTF8.GetString(Convert.FromBase64String(value)); } } private const int SteamAppId = 892970; private const int ParentExitTimeoutMilliseconds = 120000; [STAThread] private static int Main(string[] args) { string text = ((args != null && args.Length != 0) ? args[0] : string.Empty); try { if (string.IsNullOrEmpty(text) || !File.Exists(text)) { throw new FileNotFoundException("Restart request file was not found.", text); } LaunchRequest launchRequest = LaunchRequest.Load(text); RestarterSquirrel.Initialize(text, launchRequest.HelperDiagnostics, launchRequest.HelperLogDirectory); RestarterSquirrel.Info("Restart request loaded successfully."); if (!File.Exists(launchRequest.SteamExecutablePath)) { throw new FileNotFoundException("Steam.exe was not found.", launchRequest.SteamExecutablePath); } WaitForParent(launchRequest.ParentProcessId); RestarterSquirrel.Info("Previous Valheim process has exited."); Process.Start(new ProcessStartInfo { FileName = launchRequest.SteamExecutablePath, Arguments = BuildSteamCommandLine(launchRequest.ProfileArguments), WorkingDirectory = Path.GetDirectoryName(launchRequest.SteamExecutablePath), UseShellExecute = false }); RestarterSquirrel.Info("Steam launch request submitted."); DeleteIfPresent(text); DeleteIfPresent(text + ".error"); return 0; } catch (Exception ex) { RestarterSquirrel.Error("Restart failed: " + ex.GetType().Name); TryWriteError(text, ex); return 1; } } private static void WaitForParent(int processId) { try { using Process process = Process.GetProcessById(processId); if (!process.WaitForExit(120000)) { throw new TimeoutException("Valheim did not exit before the restart timeout."); } } catch (ArgumentException) { } Thread.Sleep(1500); } private static string BuildSteamCommandLine(string[] profileArguments) { IEnumerable<string> source = profileArguments ?? new string[0]; string text = string.Join(" ", source.Select(QuoteArgument)); if (string.IsNullOrEmpty(text)) { return "-applaunch " + 892970.ToString(CultureInfo.InvariantCulture); } return "-applaunch " + 892970.ToString(CultureInfo.InvariantCulture) + " " + text; } private static string QuoteArgument(string value) { if (string.IsNullOrEmpty(value)) { return "\"\""; } if (value.All((char c2) => !char.IsWhiteSpace(c2) && c2 != '"')) { return value; } StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append('"'); int num = 0; foreach (char c in value) { switch (c) { case '\\': num++; break; case '"': stringBuilder.Append('\\', num * 2 + 1); stringBuilder.Append('"'); num = 0; break; default: stringBuilder.Append('\\', num); num = 0; stringBuilder.Append(c); break; } } stringBuilder.Append('\\', num * 2); stringBuilder.Append('"'); return stringBuilder.ToString(); } private static void TryWriteError(string requestPath, Exception exception) { if (string.IsNullOrEmpty(requestPath)) { return; } try { File.WriteAllText(requestPath + ".error", exception.ToString(), new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); } catch { } } private static void DeleteIfPresent(string path) { try { if (File.Exists(path)) { File.Delete(path); } } catch { } } } internal static class RestarterSquirrel { private static readonly object SyncRoot = new object(); private static bool _enabled; private static string _logPath; internal static void Initialize(string requestPath, bool enabled, string dataDirectory) { _enabled = false; _logPath = null; if (!enabled || string.IsNullOrEmpty(requestPath)) { return; } try { string directoryName = Path.GetDirectoryName(Path.GetFullPath(requestPath)); if (string.IsNullOrEmpty(directoryName) || !string.Equals(Path.GetFileName(directoryName), "config", StringComparison.OrdinalIgnoreCase)) { return; } DirectoryInfo parent = Directory.GetParent(directoryName); if (parent == null) { return; } string directory = Path.Combine(parent.FullName, "ModsmithCycle"); bool flag = !TryUseDirectory(dataDirectory); if (!flag || TryUseDirectory(directory)) { _enabled = true; if (flag && !string.IsNullOrWhiteSpace(dataDirectory)) { Warning("Configured helper log directory was unavailable; using BepInEx/ModsmithCycle."); } Info("Helper diagnostic session initialized."); } } catch { _enabled = false; _logPath = null; } } private static bool TryUseDirectory(string directory) { if (string.IsNullOrWhiteSpace(directory)) { return false; } try { if (!Path.IsPathRooted(directory)) { return false; } string pathRoot = Path.GetPathRoot(directory); if (!string.IsNullOrEmpty(pathRoot)) { char directorySeparatorChar = Path.DirectorySeparatorChar; if (pathRoot.EndsWith(directorySeparatorChar.ToString(), StringComparison.Ordinal)) { Directory.CreateDirectory(directory); string text = Path.Combine(directory, "ModsmithCycleHelper.log"); File.AppendAllText(text, string.Empty, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); _logPath = text; return true; } } return false; } catch { _logPath = null; return false; } } internal static void Info(string message) { Write("INFO", message); } internal static void Warning(string message) { Write("WARNING", message); } internal static void Error(string message) { Write("ERROR", message); } private static void Write(string level, string message) { if (!_enabled || string.IsNullOrEmpty(_logPath)) { return; } try { string contents = DateTime.UtcNow.ToString("O", CultureInfo.InvariantCulture) + " [RestarterSquirrel/" + level + "] " + message + Environment.NewLine; lock (SyncRoot) { File.AppendAllText(_logPath, contents, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); } } catch { } } } }