diff --git a/src/LibHac/Fs/ApplicationSaveDataManagement.cs b/src/LibHac/Fs/ApplicationSaveDataManagement.cs index bce2ad33..f73cf2fd 100644 --- a/src/LibHac/Fs/ApplicationSaveDataManagement.cs +++ b/src/LibHac/Fs/ApplicationSaveDataManagement.cs @@ -1,4 +1,5 @@ -using System.Diagnostics.CodeAnalysis; +using System; +using System.Diagnostics.CodeAnalysis; using LibHac.Account; using LibHac.Fs.Shim; using LibHac.Ncm; @@ -220,6 +221,29 @@ namespace LibHac.Fs return Result.Success; } + private static Result CreateSaveData(FileSystemClient fs, Func createFunc, ref long requiredSize, long baseSize, + long dataSize, long journalSize) + { + Result rc = createFunc(); + + if (rc.IsSuccess()) + return Result.Success; + + if (ResultFs.InsufficientFreeSpace.Includes(rc)) + { + Result queryRc = fs.QuerySaveDataTotalSize(out long totalSize, dataSize, journalSize); + if (queryRc.IsFailure()) return queryRc; + + requiredSize += Util.AlignUp(totalSize, 0x4000) + baseSize; + } + else if (!ResultFs.PathAlreadyExists.Includes(rc)) + { + return rc; + } + + return Result.Success; + } + private static Result EnsureApplicationBcatDeliveryCacheStorageImpl(FileSystemClient fs, out long requiredSize, TitleId applicationId, ref ApplicationControlProperty nacp) { @@ -287,6 +311,200 @@ namespace LibHac.Fs return requiredSize > 0 ? ResultFs.InsufficientFreeSpace.Log() : Result.Success; } + private static Result EnsureApplicationCacheStorageImpl(this FileSystemClient fs, out long requiredSize, + out CacheStorageTargetMedia target, TitleId applicationId, TitleId saveDataOwnerId, short index, + long dataSize, long journalSize, bool allowExisting) + { + requiredSize = default; + target = CacheStorageTargetMedia.SdCard; + + Result rc = fs.GetCacheStorageTargetMediaImpl(out CacheStorageTargetMedia targetMedia, applicationId); + if (rc.IsFailure()) return rc; + + long requiredSizeLocal = 0; + + if (targetMedia == CacheStorageTargetMedia.Nand) + { + rc = TryCreateCacheStorage(fs, out requiredSizeLocal, SaveDataSpaceId.User, applicationId, + saveDataOwnerId, index, dataSize, journalSize, allowExisting); + if (rc.IsFailure()) return rc; + } + else if (targetMedia == CacheStorageTargetMedia.SdCard) + { + rc = TryCreateCacheStorage(fs, out requiredSizeLocal, SaveDataSpaceId.SdCache, applicationId, + saveDataOwnerId, index, dataSize, journalSize, allowExisting); + if (rc.IsFailure()) return rc; + } + // Savedata doesn't exist. Try to create a new one. + else + { + // Try to create the savedata on the SD card first + if (fs.IsSdCardAccessible()) + { + target = CacheStorageTargetMedia.SdCard; + + Result CreateFuncSdCard() => fs.CreateCacheStorage(applicationId, SaveDataSpaceId.SdCache, + saveDataOwnerId, index, dataSize, journalSize, SaveDataFlags.None); + + rc = CreateSaveData(fs, CreateFuncSdCard, ref requiredSizeLocal, 0x4000, dataSize, journalSize); + if (rc.IsFailure()) return rc; + + if (requiredSizeLocal == 0) + { + requiredSize = 0; + return Result.Success; + } + } + + // If the save can't be created on the SD card, try creating it on the User BIS partition + requiredSizeLocal = 0; + target = CacheStorageTargetMedia.Nand; + + Result CreateFuncNand() => fs.CreateCacheStorage(applicationId, SaveDataSpaceId.User, saveDataOwnerId, + index, dataSize, journalSize, SaveDataFlags.None); + + rc = CreateSaveData(fs, CreateFuncNand, ref requiredSizeLocal, 0x4000, dataSize, journalSize); + if (rc.IsFailure()) return rc; + + if (requiredSizeLocal != 0) + { + target = CacheStorageTargetMedia.None; + requiredSize = requiredSizeLocal; + return ResultFs.InsufficientFreeSpace.Log(); + } + } + + requiredSize = 0; + return Result.Success; + } + + public static Result EnsureApplicationCacheStorage(this FileSystemClient fs, out long requiredSize, + out CacheStorageTargetMedia target, TitleId applicationId, TitleId saveDataOwnerId, short index, + long dataSize, long journalSize, bool allowExisting) + { + return EnsureApplicationCacheStorageImpl(fs, out requiredSize, out target, applicationId, saveDataOwnerId, + index, dataSize, journalSize, allowExisting); + } + + public static Result EnsureApplicationCacheStorage(this FileSystemClient fs, out long requiredSize, + TitleId applicationId, ref ApplicationControlProperty nacp) + { + return EnsureApplicationCacheStorageImpl(fs, out requiredSize, out _, applicationId, nacp.SaveDataOwnerId, + 0, nacp.CacheStorageSize, nacp.CacheStorageJournalSize, true); + } + + public static Result EnsureApplicationCacheStorage(this FileSystemClient fs, out long requiredSize, + out CacheStorageTargetMedia target, TitleId applicationId, ref ApplicationControlProperty nacp) + { + if (nacp.CacheStorageSize <= 0) + { + requiredSize = default; + target = default; + return Result.Success; + } + + return EnsureApplicationCacheStorageImpl(fs, out requiredSize, out target, applicationId, + nacp.SaveDataOwnerId, 0, nacp.CacheStorageSize, nacp.CacheStorageJournalSize, true); + } + + public static Result TryCreateCacheStorage(this FileSystemClient fs, out long requiredSize, + SaveDataSpaceId spaceId, TitleId applicationId, TitleId saveDataOwnerId, short index, long dataSize, + long journalSize, bool allowExisting) + { + requiredSize = default; + long requiredSizeLocal = 0; + + var filter = new SaveDataFilter(); + filter.SetProgramId(applicationId); + filter.SetIndex(index); + filter.SetSaveDataType(SaveDataType.Cache); + + Result rc = fs.FindSaveDataWithFilter(out SaveDataInfo info, spaceId, ref filter); + + if (rc.IsFailure()) + { + if (!ResultFs.TargetNotFound.Includes(rc)) + return rc; + + Result CreateCacheFunc() => fs.CreateCacheStorage(applicationId, spaceId, saveDataOwnerId, index, + dataSize, journalSize, SaveDataFlags.None); + + rc = CreateSaveData(fs, CreateCacheFunc, ref requiredSizeLocal, 0x4000, dataSize, journalSize); + if (rc.IsFailure()) return rc; + + requiredSize = requiredSizeLocal; + return Result.Success; + } + + if (!allowExisting) + { + return ResultFs.SaveDataPathAlreadyExists.Log(); + } + + rc = ExtendSaveDataIfNeeded(fs, out requiredSizeLocal, spaceId, info.SaveDataId, dataSize, journalSize); + + if (rc.IsSuccess() || ResultFs.InsufficientFreeSpace.Includes(rc)) + { + requiredSize = requiredSizeLocal; + return Result.Success; + } + + if (ResultFs.SaveDataIsExtending.Includes(rc)) + { + return ResultFs.SaveDataCorrupted.LogConverted(rc); + } + + return rc; + } + + public static Result GetCacheStorageTargetMedia(this FileSystemClient fs, out CacheStorageTargetMedia target, TitleId applicationId) + { + return GetCacheStorageTargetMediaImpl(fs, out target, applicationId); + } + + private static Result GetCacheStorageTargetMediaImpl(this FileSystemClient fs, out CacheStorageTargetMedia target, TitleId applicationId) + { + target = default; + + if (fs.IsSdCardAccessible()) + { + var filter = new SaveDataFilter(); + filter.SetProgramId(applicationId); + filter.SetSaveDataType(SaveDataType.Cache); + + Result rc = fs.FindSaveDataWithFilter(out _, SaveDataSpaceId.SdCache, ref filter); + + if (rc.IsSuccess()) + { + target = CacheStorageTargetMedia.SdCard; + return Result.Success; + } + + if (!ResultFs.TargetNotFound.Includes(rc)) + return rc; + } + + { + var filter = new SaveDataFilter(); + filter.SetProgramId(applicationId); + filter.SetSaveDataType(SaveDataType.Cache); + + Result rc = fs.FindSaveDataWithFilter(out _, SaveDataSpaceId.User, ref filter); + + if (rc.IsSuccess()) + { + target = CacheStorageTargetMedia.Nand; + return Result.Success; + } + + if (!ResultFs.TargetNotFound.Includes(rc)) + return rc; + } + + target = CacheStorageTargetMedia.None; + return Result.Success; + } + public static Result CleanUpTemporaryStorage(FileSystemClient fs) { var filter = new SaveDataFilter(); diff --git a/src/LibHac/Fs/FsEnums.cs b/src/LibHac/Fs/FsEnums.cs index 7a877848..64534def 100644 --- a/src/LibHac/Fs/FsEnums.cs +++ b/src/LibHac/Fs/FsEnums.cs @@ -180,4 +180,11 @@ namespace LibHac.Fs SdCard = 1, GcAsic = 2 } + + public enum CacheStorageTargetMedia + { + None = 0, + Nand = 1, + SdCard = 2 + } } diff --git a/tests/LibHac.Tests/Fs/FileSystemClientTests/ApplicationSaveDataManagementTests.cs b/tests/LibHac.Tests/Fs/FileSystemClientTests/ApplicationSaveDataManagementTests.cs new file mode 100644 index 00000000..5a3fe24f --- /dev/null +++ b/tests/LibHac.Tests/Fs/FileSystemClientTests/ApplicationSaveDataManagementTests.cs @@ -0,0 +1,206 @@ +using LibHac.Account; +using LibHac.Fs; +using LibHac.Fs.Shim; +using LibHac.Ncm; +using LibHac.Ns; +using Xunit; + +using static LibHac.Fs.ApplicationSaveDataManagement; + +namespace LibHac.Tests.Fs.FileSystemClientTests +{ + public class ApplicationSaveDataManagementTests + { + [Fact] + public static void EnsureApplicationSaveData_CreatesAccountSaveData() + { + FileSystemClient fs = FileSystemServerFactory.CreateClient(true); + + var applicationId = new TitleId(11); + var userId = new Uid(2, 3); + + var nacp = new ApplicationControlProperty + { + UserAccountSaveDataSize = 0x1000, + UserAccountSaveDataJournalSize = 0x1000 + }; + + Assert.Success(EnsureApplicationSaveData(fs, out _, applicationId, ref nacp, ref userId)); + + fs.OpenSaveDataIterator(out SaveDataIterator iterator, SaveDataSpaceId.User); + + var info = new SaveDataInfo[2]; + Assert.Success(iterator.ReadSaveDataInfo(out long entriesRead, info)); + + Assert.Equal(1, entriesRead); + Assert.Equal(applicationId, info[0].TitleId); + Assert.Equal(ConvertAccountUidToFsUserId(userId), info[0].UserId); + Assert.Equal(SaveDataType.Account, info[0].Type); + } + + [Fact] + public static void EnsureApplicationSaveData_CreatesDeviceSaveData() + { + FileSystemClient fs = FileSystemServerFactory.CreateClient(true); + + var applicationId = new TitleId(11); + var userId = new Uid(2, 3); + + var nacp = new ApplicationControlProperty + { + DeviceSaveDataSize = 0x1000, + DeviceSaveDataJournalSize = 0x1000 + }; + + Assert.Success(EnsureApplicationSaveData(fs, out _, applicationId, ref nacp, ref userId)); + + fs.OpenSaveDataIterator(out SaveDataIterator iterator, SaveDataSpaceId.User); + + var info = new SaveDataInfo[2]; + Assert.Success(iterator.ReadSaveDataInfo(out long entriesRead, info)); + + Assert.Equal(1, entriesRead); + Assert.Equal(applicationId, info[0].TitleId); + Assert.Equal(UserId.Zero, info[0].UserId); + Assert.Equal(SaveDataType.Device, info[0].Type); + } + + [Fact] + public static void EnsureApplicationSaveData_CreatesBcatCacheStorage() + { + FileSystemClient fs = FileSystemServerFactory.CreateClient(true); + + var applicationId = new TitleId(11); + var userId = new Uid(2, 3); + + var nacp = new ApplicationControlProperty + { + BcatDeliveryCacheStorageSize = 0x1000 + }; + + Assert.Success(EnsureApplicationSaveData(fs, out _, applicationId, ref nacp, ref userId)); + + fs.OpenSaveDataIterator(out SaveDataIterator iterator, SaveDataSpaceId.User); + + var info = new SaveDataInfo[2]; + Assert.Success(iterator.ReadSaveDataInfo(out long entriesRead, info)); + + Assert.Equal(1, entriesRead); + Assert.Equal(applicationId, info[0].TitleId); + Assert.Equal(UserId.Zero, info[0].UserId); + Assert.Equal(SaveDataType.Bcat, info[0].Type); + } + + [Fact] + public static void EnsureApplicationSaveData_CreatesTemporaryStorage() + { + FileSystemClient fs = FileSystemServerFactory.CreateClient(true); + + var applicationId = new TitleId(11); + var userId = new Uid(2, 3); + + var nacp = new ApplicationControlProperty + { + TemporaryStorageSize = 0x1000 + }; + + Assert.Success(EnsureApplicationSaveData(fs, out _, applicationId, ref nacp, ref userId)); + + fs.OpenSaveDataIterator(out SaveDataIterator iterator, SaveDataSpaceId.Temporary); + + var info = new SaveDataInfo[2]; + Assert.Success(iterator.ReadSaveDataInfo(out long entriesRead, info)); + + Assert.Equal(1, entriesRead); + Assert.Equal(applicationId, info[0].TitleId); + Assert.Equal(UserId.Zero, info[0].UserId); + Assert.Equal(SaveDataType.Temporary, info[0].Type); + } + + [Fact] + public static void EnsureApplicationCacheStorage_SdCardAvailable_CreatesCacheStorageOnSd() + { + FileSystemClient fs = FileSystemServerFactory.CreateClient(true); + + var applicationId = new TitleId(11); + + var nacp = new ApplicationControlProperty + { + CacheStorageSize = 0x1000, + CacheStorageJournalSize = 0x1000 + }; + + Assert.Success(fs.EnsureApplicationCacheStorage(out _, out CacheStorageTargetMedia target, applicationId, + ref nacp)); + + Assert.Equal(CacheStorageTargetMedia.SdCard, target); + + fs.OpenSaveDataIterator(out SaveDataIterator iterator, SaveDataSpaceId.SdCache); + + var info = new SaveDataInfo[2]; + Assert.Success(iterator.ReadSaveDataInfo(out long entriesRead, info)); + + Assert.Equal(1, entriesRead); + Assert.Equal(applicationId, info[0].TitleId); + Assert.Equal(SaveDataType.Cache, info[0].Type); + } + + [Fact] + public static void EnsureApplicationCacheStorage_SdCardNotAvailable_CreatesCacheStorageOnBis() + { + FileSystemClient fs = FileSystemServerFactory.CreateClient(false); + + var applicationId = new TitleId(11); + + var nacp = new ApplicationControlProperty + { + CacheStorageSize = 0x1000, + CacheStorageJournalSize = 0x1000 + }; + + Assert.Success(fs.EnsureApplicationCacheStorage(out _, out CacheStorageTargetMedia target, applicationId, + ref nacp)); + + Assert.Equal(CacheStorageTargetMedia.Nand, target); + + fs.OpenSaveDataIterator(out SaveDataIterator iterator, SaveDataSpaceId.User); + + var info = new SaveDataInfo[2]; + Assert.Success(iterator.ReadSaveDataInfo(out long entriesRead, info)); + + Assert.Equal(1, entriesRead); + Assert.Equal(applicationId, info[0].TitleId); + Assert.Equal(SaveDataType.Cache, info[0].Type); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public static void GetCacheStorageTargetMedia_ReturnsTargetOfNewCacheStorage(bool isSdCardInserted) + { + FileSystemClient fs = FileSystemServerFactory.CreateClient(isSdCardInserted); + + var applicationId = new TitleId(11); + + var nacp = new ApplicationControlProperty + { + CacheStorageSize = 0x1000, + CacheStorageJournalSize = 0x1000 + }; + + fs.EnsureApplicationCacheStorage(out _, out CacheStorageTargetMedia targetFromCreation, applicationId, ref nacp); + + Assert.Success(fs.GetCacheStorageTargetMedia(out CacheStorageTargetMedia target, applicationId)); + Assert.Equal(targetFromCreation, target); + } + + [Fact] + public static void GetCacheStorageTargetMedia_CacheStorageDoesNotExist_ReturnsNone() + { + FileSystemClient fs = FileSystemServerFactory.CreateClient(true); + + Assert.Success(fs.GetCacheStorageTargetMedia(out CacheStorageTargetMedia target, new TitleId(11))); + Assert.Equal(CacheStorageTargetMedia.None, target); + } + } +}