diff --git a/src/LibHac/FsSystem/DirectorySaveDataFileSystem.cs b/src/LibHac/FsSystem/DirectorySaveDataFileSystem.cs index 61cd6549..ac1aca8d 100644 --- a/src/LibHac/FsSystem/DirectorySaveDataFileSystem.cs +++ b/src/LibHac/FsSystem/DirectorySaveDataFileSystem.cs @@ -728,6 +728,24 @@ public class DirectorySaveDataFileSystem : IFileSystem, ISaveDataExtraDataAccess (byte)'t', (byte)'a', (byte)'_' }; + /// + /// Initializes the save data's extra data files. + /// + /// + /// There's no telling what state users might leave the extra data files in, so we want + /// to be able to handle or recover from all possible states based on which files exist: + /// This is the state a properly committed save should be in.
+ /// Committed, Modified -> Use committed
+ /// This is the state the save will be in after an interrupted commit.
+ /// Working, Synchronizing -> Use modified
+ /// These states shouldn't normally happen. Use the committed file, ignoring the others.
+ /// Committed, Synchronizing -> Use committed
+ /// Committed, Modified, Synchronizing -> Use committed
+ /// If only one file exists then use that file.
+ /// Committed -> Use committed
+ /// Modified -> Use modified
+ /// Synchronizing -> Use synchronizing
+ ///
private Result InitializeExtraData() { using var pathModifiedExtraData = new Path(); @@ -742,7 +760,8 @@ public class DirectorySaveDataFileSystem : IFileSystem, ISaveDataExtraDataAccess rc = PathFunctions.SetUpFixedPath(ref pathSynchronizingExtraData.Ref(), SynchronizingExtraDataName); if (rc.IsFailure()) return rc; - // Ensure the extra data files exist + // Ensure the extra data files exist. + // We don't currently handle the case where some of the extra data paths are directories instead of files. rc = _baseFs.GetEntryType(out _, in pathModifiedExtraData); if (rc.IsFailure()) @@ -750,14 +769,36 @@ public class DirectorySaveDataFileSystem : IFileSystem, ISaveDataExtraDataAccess if (!ResultFs.PathNotFound.Includes(rc)) return rc; + // The Modified file doesn't exist. Create it. rc = _baseFs.CreateFile(in pathModifiedExtraData, Unsafe.SizeOf()); if (rc.IsFailure()) return rc; if (_isJournalingSupported) { - rc = _baseFs.CreateFile(in pathCommittedExtraData, Unsafe.SizeOf()); - if (rc.IsFailure() && !ResultFs.PathAlreadyExists.Includes(rc)) - return rc; + rc = _baseFs.GetEntryType(out _, in pathCommittedExtraData); + + if (rc.IsFailure()) + { + if (!ResultFs.PathNotFound.Includes(rc)) + return rc; + + // Neither the modified or committed files existed. + // Check if the synchronizing file exists and use it if it does. + rc = _baseFs.GetEntryType(out _, in pathSynchronizingExtraData); + + if (rc.IsSuccess()) + { + rc = _baseFs.RenameFile(in pathSynchronizingExtraData, in pathCommittedExtraData); + if (rc.IsFailure()) return rc; + } + else + { + // The synchronizing file did not exist. Create an empty committed extra data file. + rc = _baseFs.CreateFile(in pathCommittedExtraData, Unsafe.SizeOf()); + if (rc.IsFailure() && !ResultFs.PathAlreadyExists.Includes(rc)) + return rc; + } + } } } else @@ -785,13 +826,31 @@ public class DirectorySaveDataFileSystem : IFileSystem, ISaveDataExtraDataAccess } else if (ResultFs.PathNotFound.Includes(rc)) { - // If a previous commit failed, the committed extra data may be missing. - // Finish that commit by copying the working extra data to the committed extra data - rc = SynchronizeExtraData(in pathSynchronizingExtraData, in pathModifiedExtraData); - if (rc.IsFailure()) return rc; + // The committed file doesn't exist. Try to recover from whatever invalid state we're in. - rc = _baseFs.RenameFile(in pathSynchronizingExtraData, in pathCommittedExtraData); - if (rc.IsFailure()) return rc; + // If the synchronizing file exists then the previous commit failed. + // Finish that commit by copying the working extra data to the committed extra data + if (_baseFs.GetEntryType(out _, in pathSynchronizingExtraData).IsSuccess()) + { + rc = SynchronizeExtraData(in pathSynchronizingExtraData, in pathModifiedExtraData); + if (rc.IsFailure()) return rc; + + rc = _baseFs.RenameFile(in pathSynchronizingExtraData, in pathCommittedExtraData); + if (rc.IsFailure()) return rc; + } + else + { + // The only existing file is the modified file. + // Copy the working extra data to the committed extra data. + rc = _baseFs.CreateFile(in pathSynchronizingExtraData, Unsafe.SizeOf()); + if (rc.IsFailure()) return rc; + + rc = SynchronizeExtraData(in pathSynchronizingExtraData, in pathModifiedExtraData); + if (rc.IsFailure()) return rc; + + rc = _baseFs.RenameFile(in pathSynchronizingExtraData, in pathCommittedExtraData); + if (rc.IsFailure()) return rc; + } } else { diff --git a/tests/LibHac.Tests/Fs/DirectorySaveDataFileSystemTests.cs b/tests/LibHac.Tests/FsSystem/DirectorySaveDataFileSystemTests.cs similarity index 79% rename from tests/LibHac.Tests/Fs/DirectorySaveDataFileSystemTests.cs rename to tests/LibHac.Tests/FsSystem/DirectorySaveDataFileSystemTests.cs index 9e2945f4..9f7f41fa 100644 --- a/tests/LibHac.Tests/Fs/DirectorySaveDataFileSystemTests.cs +++ b/tests/LibHac.Tests/FsSystem/DirectorySaveDataFileSystemTests.cs @@ -6,11 +6,12 @@ using LibHac.Fs; using LibHac.Fs.Fsa; using LibHac.FsSrv; using LibHac.FsSystem; +using LibHac.Tests.Fs; using LibHac.Tests.Fs.IFileSystemTestBase; using LibHac.Tools.Fs; using Xunit; -namespace LibHac.Tests.Fs; +namespace LibHac.Tests.FsSystem; public class DirectorySaveDataFileSystemTests : CommittableIFileSystemTests { @@ -79,7 +80,7 @@ public class DirectorySaveDataFileSystemTests : CommittableIFileSystemTests } [Fact] - public void CreateFile_CreatedInWorkingDirectory() + public void CreateFile_CreatedInModifiedDirectory() { (IFileSystem baseFs, IFileSystem saveFs) = CreateFileSystemInternal(); @@ -165,7 +166,7 @@ public class DirectorySaveDataFileSystemTests : CommittableIFileSystemTests } [Fact] - public void Initialize_InterruptedAfterCommitPart1_UsesWorkingData() + public void Initialize_InterruptedAfterCommitPart1_UsesModifiedData() { var baseFs = new InMemoryFileSystem(); @@ -183,7 +184,7 @@ public class DirectorySaveDataFileSystemTests : CommittableIFileSystemTests } [Fact] - public void Initialize_InterruptedDuringCommitPart2_UsesWorkingData() + public void Initialize_InterruptedDuringCommitPart2_UsesModifiedData() { var baseFs = new InMemoryFileSystem(); @@ -299,7 +300,7 @@ public class DirectorySaveDataFileSystemTests : CommittableIFileSystemTests } [Fact] - public void Initialize_InterruptedAfterCommitPart1_UsesWorkingExtraData() + public void Initialize_InterruptedAfterCommitPart1_UsesModifiedExtraData() { var baseFs = new InMemoryFileSystem(); @@ -313,6 +314,109 @@ public class DirectorySaveDataFileSystemTests : CommittableIFileSystemTests Assert.Equal(0x67890, extraData.DataSize); } + [Fact] + public void Initialize_OnlySynchronizingExtraDataExists_UsesSynchronizingExtraData() + { + var baseFs = new InMemoryFileSystem(); + + CreateExtraDataForTest(baseFs, "/ExtraData_", 0x67890).ThrowIfFailure(); + + CreateDirSaveFs(out DirectorySaveDataFileSystem saveFs, baseFs, true, true, true).ThrowIfFailure(); + + saveFs.ReadExtraData(out SaveDataExtraData extraData).ThrowIfFailure(); + + Assert.Equal(0x67890, extraData.DataSize); + } + + [Fact] + public void Initialize_OnlyCommittedExtraDataExists_UsesCommittedExtraData() + { + var baseFs = new InMemoryFileSystem(); + + CreateExtraDataForTest(baseFs, "/ExtraData0", 0x67890).ThrowIfFailure(); + + CreateDirSaveFs(out DirectorySaveDataFileSystem saveFs, baseFs, true, true, true).ThrowIfFailure(); + + saveFs.ReadExtraData(out SaveDataExtraData extraData).ThrowIfFailure(); + + Assert.Equal(0x67890, extraData.DataSize); + } + + [Fact] + public void Initialize_OnlyModifiedExtraDataExists_UsesModifiedExtraData() + { + var baseFs = new InMemoryFileSystem(); + + CreateExtraDataForTest(baseFs, "/ExtraData1", 0x67890).ThrowIfFailure(); + + CreateDirSaveFs(out DirectorySaveDataFileSystem saveFs, baseFs, true, true, true).ThrowIfFailure(); + + saveFs.ReadExtraData(out SaveDataExtraData extraData).ThrowIfFailure(); + + Assert.Equal(0x67890, extraData.DataSize); + } + + [Fact] + public void Initialize_SynchronizingAndCommittedExtraDataExist_UsesCommittedExtraData() + { + var baseFs = new InMemoryFileSystem(); + + CreateExtraDataForTest(baseFs, "/ExtraData_", 0x12345).ThrowIfFailure(); + CreateExtraDataForTest(baseFs, "/ExtraData0", 0x67890).ThrowIfFailure(); + + CreateDirSaveFs(out DirectorySaveDataFileSystem saveFs, baseFs, true, true, true).ThrowIfFailure(); + + saveFs.ReadExtraData(out SaveDataExtraData extraData).ThrowIfFailure(); + + Assert.Equal(0x67890, extraData.DataSize); + } + + [Fact] + public void Initialize_SynchronizingAndModifiedExtraDataExist_UsesModifiedExtraData() + { + var baseFs = new InMemoryFileSystem(); + + CreateExtraDataForTest(baseFs, "/ExtraData_", 0x12345).ThrowIfFailure(); + CreateExtraDataForTest(baseFs, "/ExtraData1", 0x67890).ThrowIfFailure(); + + CreateDirSaveFs(out DirectorySaveDataFileSystem saveFs, baseFs, true, true, true).ThrowIfFailure(); + + saveFs.ReadExtraData(out SaveDataExtraData extraData).ThrowIfFailure(); + + Assert.Equal(0x67890, extraData.DataSize); + } + + [Fact] + public void Initialize_CommittedAndModifiedExtraDataExist_UsesCommittedExtraData() + { + var baseFs = new InMemoryFileSystem(); + + CreateExtraDataForTest(baseFs, "/ExtraData0", 0x12345).ThrowIfFailure(); + CreateExtraDataForTest(baseFs, "/ExtraData1", 0x67890).ThrowIfFailure(); + + CreateDirSaveFs(out DirectorySaveDataFileSystem saveFs, baseFs, true, true, true).ThrowIfFailure(); + + saveFs.ReadExtraData(out SaveDataExtraData extraData).ThrowIfFailure(); + + Assert.Equal(0x12345, extraData.DataSize); + } + + [Fact] + public void Initialize_AllThreeExtraDataFilesExist_UsesCommittedExtraData() + { + var baseFs = new InMemoryFileSystem(); + + CreateExtraDataForTest(baseFs, "/ExtraData_", 0x12345).ThrowIfFailure(); + CreateExtraDataForTest(baseFs, "/ExtraData0", 0x54321).ThrowIfFailure(); + CreateExtraDataForTest(baseFs, "/ExtraData1", 0x67890).ThrowIfFailure(); + + CreateDirSaveFs(out DirectorySaveDataFileSystem saveFs, baseFs, true, true, true).ThrowIfFailure(); + + saveFs.ReadExtraData(out SaveDataExtraData extraData).ThrowIfFailure(); + + Assert.Equal(0x54321, extraData.DataSize); + } + [Fact] public void CommitSaveData_MultipleCommits_CommitIdIsUpdatedSkippingInvalidIds() {