From 3056c5c29643eb1e3c17aecb89639c685ab16833 Mon Sep 17 00:00:00 2001 From: Alex Barney Date: Thu, 27 May 2021 16:38:29 -0700 Subject: [PATCH] Implement extra data functions in SaveDataFileSystemService --- .../FsSrv/FileSystemServerInitializer.cs | 1 - src/LibHac/FsSrv/Impl/Utility.cs | 11 + src/LibHac/FsSrv/SaveDataFileSystemService.cs | 373 +++++++++++++++++- .../FsSrv/SaveDataFileSystemServiceImpl.cs | 2 +- .../Fs/FileSystemClientTests/ShimTests/Bis.cs | 1 + .../ShimTests/SaveDataManagement.cs | 143 +++++++ 6 files changed, 511 insertions(+), 20 deletions(-) diff --git a/src/LibHac/FsSrv/FileSystemServerInitializer.cs b/src/LibHac/FsSrv/FileSystemServerInitializer.cs index fb1895e7..9ad512a2 100644 --- a/src/LibHac/FsSrv/FileSystemServerInitializer.cs +++ b/src/LibHac/FsSrv/FileSystemServerInitializer.cs @@ -1,5 +1,4 @@ using System; -using LibHac.Common.Keys; using LibHac.Fs.Impl; using LibHac.Fs.Shim; using LibHac.FsSrv.FsCreator; diff --git a/src/LibHac/FsSrv/Impl/Utility.cs b/src/LibHac/FsSrv/Impl/Utility.cs index 35e26bd0..2526629d 100644 --- a/src/LibHac/FsSrv/Impl/Utility.cs +++ b/src/LibHac/FsSrv/Impl/Utility.cs @@ -162,6 +162,17 @@ namespace LibHac.FsSrv.Impl return CreateSubDirectoryFileSystem(out fileSystem, ref baseFileSystem, path); } + public static long ConvertZeroCommitId(in SaveDataExtraData extraData) + { + if (extraData.CommitId != 0) + return extraData.CommitId; + + Span hash = stackalloc byte[Crypto.Sha256.DigestSize]; + + Crypto.Sha256.GenerateSha256Hash(SpanHelpers.AsReadOnlyByteSpan(in extraData), hash); + return BitConverter.ToInt64(hash); + } + private static ReadOnlySpan FileSystemRootPath => // / new[] { diff --git a/src/LibHac/FsSrv/SaveDataFileSystemService.cs b/src/LibHac/FsSrv/SaveDataFileSystemService.cs index 1662a649..6ed7f041 100644 --- a/src/LibHac/FsSrv/SaveDataFileSystemService.cs +++ b/src/LibHac/FsSrv/SaveDataFileSystemService.cs @@ -301,6 +301,75 @@ namespace LibHac.FsSrv return Result.Success; } + public static Result CheckWriteExtraData(in SaveDataAttribute attribute, in SaveDataExtraData mask, + ProgramInfo programInfo, ExtraDataGetter extraDataGetter) + { + AccessControl accessControl = programInfo.AccessControl; + + if (mask.Flags != SaveDataFlags.None) + { + bool canAccess = accessControl.CanCall(OperationType.WriteSaveDataFileSystemExtraDataAll) || + accessControl.CanCall(OperationType.WriteSaveDataFileSystemExtraDataFlags); + + if (SaveDataProperties.IsSystemSaveData(attribute.Type)) + { + Result rc = GetAccessibilityForSaveData(out Accessibility accessibility, programInfo, + extraDataGetter); + if (rc.IsFailure()) return rc; + + canAccess |= accessibility.CanWrite; + } + + if ((mask.Flags & ~SaveDataFlags.Restore) == 0) + { + Result rc = GetAccessibilityForSaveData(out Accessibility accessibility, programInfo, + extraDataGetter); + if (rc.IsFailure()) return rc; + + canAccess |= accessibility.CanWrite; + } + + if (!canAccess) + return ResultFs.PermissionDenied.Log(); + } + + if (mask.TimeStamp != 0) + { + bool canAccess = accessControl.CanCall(OperationType.WriteSaveDataFileSystemExtraDataAll) || + accessControl.CanCall(OperationType.WriteSaveDataFileSystemExtraDataTimeStamp); + + if (!canAccess) + return ResultFs.PermissionDenied.Log(); + } + + if (mask.CommitId != 0) + { + bool canAccess = accessControl.CanCall(OperationType.WriteSaveDataFileSystemExtraDataAll) || + accessControl.CanCall(OperationType.WriteSaveDataFileSystemExtraDataCommitId); + + if (!canAccess) + return ResultFs.PermissionDenied.Log(); + } + + SaveDataExtraData emptyMask = default; + SaveDataExtraData maskWithoutFlags = mask; + maskWithoutFlags.Flags = SaveDataFlags.None; + maskWithoutFlags.TimeStamp = 0; + maskWithoutFlags.CommitId = 0; + + // Full write access is needed for writing anything other than flags, timestamp or commit ID + if (SpanHelpers.AsReadOnlyByteSpan(in emptyMask) + .SequenceEqual(SpanHelpers.AsReadOnlyByteSpan(in maskWithoutFlags))) + { + bool canAccess = accessControl.CanCall(OperationType.WriteSaveDataFileSystemExtraDataAll); + + if (!canAccess) + return ResultFs.PermissionDenied.Log(); + } + + return Result.Success; + } + public static Result CheckFind(in SaveDataFilter filter, ProgramInfo programInfo) { bool canAccess; @@ -1199,68 +1268,297 @@ namespace LibHac.FsSrv } } + // ReSharper disable once UnusedParameter.Local + // Nintendo used this parameter in older FS versions, but never removed it. private Result ReadSaveDataFileSystemExtraDataCore(out SaveDataExtraData extraData, SaveDataSpaceId spaceId, ulong saveDataId, bool isTemporarySaveData) { - throw new NotImplementedException(); + UnsafeHelpers.SkipParamInit(out extraData); + + using var scopedLayoutType = new ScopedStorageLayoutTypeSetter(StorageType.NonGameCard); + + SaveDataIndexerAccessor accessor = null; + try + { + Result rc = OpenSaveDataIndexerAccessor(out accessor, spaceId); + if (rc.IsFailure()) return rc; + + rc = accessor.Indexer.GetKey(out SaveDataAttribute key, saveDataId); + if (rc.IsFailure()) return rc; + + return ServiceImpl.ReadSaveDataFileSystemExtraData(out extraData, spaceId, saveDataId, key.Type, + SaveDataRootPath); + } + finally + { + accessor?.Dispose(); + } } private Result ReadSaveDataFileSystemExtraDataCore(out SaveDataExtraData extraData, SaveDataSpaceId spaceId, ulong saveDataId, in SaveDataExtraData extraDataMask) { - throw new NotImplementedException(); + UnsafeHelpers.SkipParamInit(out extraData); + + using var scopedLayoutType = new ScopedStorageLayoutTypeSetter(StorageType.NonGameCard); + + Result rc = GetProgramInfo(out ProgramInfo programInfo); + if (rc.IsFailure()) return rc; + + SaveDataSpaceId resolvedSpaceId; + SaveDataAttribute key; + + if (spaceId == SaveDataSpaceId.BisAuto) + { + SaveDataIndexerAccessor accessor = null; + try + { + if (IsStaticSaveDataIdValueRange(saveDataId)) + { + rc = OpenSaveDataIndexerAccessor(out accessor, SaveDataSpaceId.System); + if (rc.IsFailure()) return rc; + } + else + { + rc = OpenSaveDataIndexerAccessor(out accessor, SaveDataSpaceId.User); + if (rc.IsFailure()) return rc; + } + + rc = accessor.Indexer.GetValue(out SaveDataIndexerValue value, saveDataId); + if (rc.IsFailure()) return rc; + + resolvedSpaceId = value.SpaceId; + + rc = accessor.Indexer.GetKey(out key, saveDataId); + if (rc.IsFailure()) return rc; + } + finally + { + accessor?.Dispose(); + } + } + else + { + SaveDataIndexerAccessor accessor = null; + try + { + rc = OpenSaveDataIndexerAccessor(out accessor, spaceId); + if (rc.IsFailure()) return rc; + + rc = accessor.Indexer.GetValue(out SaveDataIndexerValue value, saveDataId); + if (rc.IsFailure()) return rc; + + resolvedSpaceId = value.SpaceId; + + rc = accessor.Indexer.GetKey(out key, saveDataId); + if (rc.IsFailure()) return rc; + } + finally + { + accessor?.Dispose(); + } + } + + Result ReadExtraData(out SaveDataExtraData data) => ServiceImpl.ReadSaveDataFileSystemExtraData(out data, + resolvedSpaceId, saveDataId, key.Type, new U8Span(SaveDataRootPath.Str)); + + rc = SaveDataAccessibilityChecker.CheckReadExtraData(in key, in extraDataMask, programInfo, + ReadExtraData); + if (rc.IsFailure()) return rc; + + rc = ServiceImpl.ReadSaveDataFileSystemExtraData(out SaveDataExtraData tempExtraData, resolvedSpaceId, + saveDataId, key.Type, new U8Span(SaveDataRootPath.Str)); + if (rc.IsFailure()) return rc; + + MaskExtraData(ref tempExtraData, in extraDataMask); + extraData = tempExtraData; + + return Result.Success; } public Result ReadSaveDataFileSystemExtraData(OutBuffer extraData, ulong saveDataId) { - throw new NotImplementedException(); + if (extraData.Size != Unsafe.SizeOf()) + return ResultFs.InvalidArgument.Log(); + + // Make a mask for reading the entire extra data + Unsafe.SkipInit(out SaveDataExtraData extraDataMask); + SpanHelpers.AsByteSpan(ref extraDataMask).Fill(0xFF); + + return ReadSaveDataFileSystemExtraDataCore(out SpanHelpers.AsStruct(extraData.Buffer), + SaveDataSpaceId.BisAuto, saveDataId, in extraDataMask); } public Result ReadSaveDataFileSystemExtraDataBySaveDataAttribute(OutBuffer extraData, SaveDataSpaceId spaceId, in SaveDataAttribute attribute) { - throw new NotImplementedException(); + if (extraData.Size != Unsafe.SizeOf()) + return ResultFs.InvalidArgument.Log(); + + ref SaveDataExtraData extraDataRef = ref SpanHelpers.AsStruct(extraData.Buffer); + + Result rc = GetProgramInfo(out ProgramInfo programInfo); + if (rc.IsFailure()) return rc; + + SaveDataAttribute tempAttribute = attribute; + + if (tempAttribute.ProgramId == SaveData.AutoResolveCallerProgramId) + { + tempAttribute.ProgramId = ResolveDefaultSaveDataReferenceProgramId(programInfo.ProgramId); + } + + rc = GetSaveDataInfo(out SaveDataInfo info, spaceId, in tempAttribute); + if (rc.IsFailure()) return rc; + + // Make a mask for reading the entire extra data + Unsafe.SkipInit(out SaveDataExtraData extraDataMask); + SpanHelpers.AsByteSpan(ref extraDataMask).Fill(0xFF); + + return ReadSaveDataFileSystemExtraDataCore(out extraDataRef, spaceId, info.SaveDataId, in extraDataMask); } public Result ReadSaveDataFileSystemExtraDataBySaveDataSpaceId(OutBuffer extraData, SaveDataSpaceId spaceId, ulong saveDataId) { - throw new NotImplementedException(); + if (extraData.Size != Unsafe.SizeOf()) + return ResultFs.InvalidArgument.Log(); + + ref SaveDataExtraData extraDataRef = ref SpanHelpers.AsStruct(extraData.Buffer); + + // Make a mask for reading the entire extra data + Unsafe.SkipInit(out SaveDataExtraData extraDataMask); + SpanHelpers.AsByteSpan(ref extraDataMask).Fill(0xFF); + + return ReadSaveDataFileSystemExtraDataCore(out extraDataRef, spaceId, saveDataId, in extraDataMask); } public Result ReadSaveDataFileSystemExtraDataWithMaskBySaveDataAttribute(OutBuffer extraData, SaveDataSpaceId spaceId, in SaveDataAttribute attribute, InBuffer extraDataMask) { - throw new NotImplementedException(); - } + if (extraDataMask.Size != Unsafe.SizeOf()) + return ResultFs.InvalidArgument.Log(); - public Result WriteSaveDataFileSystemExtraDataWithMaskBySaveDataAttribute(in SaveDataAttribute attribute, - SaveDataSpaceId spaceId, InBuffer extraData, InBuffer extraDataMask) - { - throw new NotImplementedException(); + if (extraData.Size != Unsafe.SizeOf()) + return ResultFs.InvalidArgument.Log(); + + ref readonly SaveDataExtraData maskRef = + ref SpanHelpers.AsReadOnlyStruct(extraDataMask.Buffer); + + ref SaveDataExtraData extraDataRef = ref SpanHelpers.AsStruct(extraData.Buffer); + + Result rc = GetProgramInfo(out ProgramInfo programInfo); + if (rc.IsFailure()) return rc; + + SaveDataAttribute tempAttribute = attribute; + + if (tempAttribute.ProgramId == SaveData.AutoResolveCallerProgramId) + { + tempAttribute.ProgramId = ResolveDefaultSaveDataReferenceProgramId(programInfo.ProgramId); + } + + rc = GetSaveDataInfo(out SaveDataInfo info, spaceId, in tempAttribute); + if (rc.IsFailure()) return rc; + + return ReadSaveDataFileSystemExtraDataCore(out extraDataRef, spaceId, info.SaveDataId, in maskRef); } private Result WriteSaveDataFileSystemExtraDataCore(SaveDataSpaceId spaceId, ulong saveDataId, in SaveDataExtraData extraData, SaveDataType saveType, bool updateTimeStamp) { - throw new NotImplementedException(); + using var scopedLayoutType = new ScopedStorageLayoutTypeSetter(StorageType.NonGameCard); + + return ServiceImpl.WriteSaveDataFileSystemExtraData(spaceId, saveDataId, in extraData, SaveDataRootPath, + saveType, updateTimeStamp); } private Result WriteSaveDataFileSystemExtraDataWithMaskCore(ulong saveDataId, SaveDataSpaceId spaceId, in SaveDataExtraData extraData, in SaveDataExtraData extraDataMask) { - throw new NotImplementedException(); + using var scopedLayoutType = new ScopedStorageLayoutTypeSetter(StorageType.NonGameCard); + + Result rc = GetProgramInfo(out ProgramInfo programInfo); + if (rc.IsFailure()) return rc; + + SaveDataIndexerAccessor accessor = null; + try + { + rc = OpenSaveDataIndexerAccessor(out accessor, spaceId); + if (rc.IsFailure()) return rc; + + rc = accessor.Indexer.GetKey(out SaveDataAttribute key, saveDataId); + if (rc.IsFailure()) return rc; + + Result ReadExtraData(out SaveDataExtraData data) => ServiceImpl.ReadSaveDataFileSystemExtraData(out data, + spaceId, saveDataId, key.Type, new U8Span(SaveDataRootPath.Str)); + + rc = SaveDataAccessibilityChecker.CheckWriteExtraData(in key, in extraDataMask, programInfo, + ReadExtraData); + if (rc.IsFailure()) return rc; + + rc = ServiceImpl.ReadSaveDataFileSystemExtraData(out SaveDataExtraData extraDataModify, spaceId, + saveDataId, key.Type, SaveDataRootPath); + if (rc.IsFailure()) return rc; + + ModifySaveDataExtraData(ref extraDataModify, in extraData, in extraDataMask); + + return ServiceImpl.WriteSaveDataFileSystemExtraData(spaceId, saveDataId, in extraDataModify, + SaveDataRootPath, key.Type, false); + } + finally + { + accessor?.Dispose(); + } } public Result WriteSaveDataFileSystemExtraData(ulong saveDataId, SaveDataSpaceId spaceId, InBuffer extraData) { - throw new NotImplementedException(); + if (extraData.Size != Unsafe.SizeOf()) + return ResultFs.InvalidArgument.Log(); + + ref readonly SaveDataExtraData extraDataRef = + ref SpanHelpers.AsReadOnlyStruct(extraData.Buffer); + + var extraDataMask = new SaveDataExtraData(); + extraDataMask.Flags = unchecked((SaveDataFlags)0xFFFFFFFF); + + return WriteSaveDataFileSystemExtraDataWithMaskCore(saveDataId, spaceId, in extraDataRef, in extraDataMask); + } + + public Result WriteSaveDataFileSystemExtraDataWithMaskBySaveDataAttribute(in SaveDataAttribute attribute, + SaveDataSpaceId spaceId, InBuffer extraData, InBuffer extraDataMask) + { + Result rc = GetProgramInfo(out ProgramInfo programInfo); + if (rc.IsFailure()) return rc; + + SaveDataAttribute tempAttribute = attribute; + + if (tempAttribute.ProgramId == SaveData.AutoResolveCallerProgramId) + { + tempAttribute.ProgramId = ResolveDefaultSaveDataReferenceProgramId(programInfo.ProgramId); + } + + rc = GetSaveDataInfo(out SaveDataInfo info, spaceId, in tempAttribute); + if (rc.IsFailure()) return rc; + + return WriteSaveDataFileSystemExtraDataWithMask(info.SaveDataId, spaceId, extraData, extraDataMask); } public Result WriteSaveDataFileSystemExtraDataWithMask(ulong saveDataId, SaveDataSpaceId spaceId, InBuffer extraData, InBuffer extraDataMask) { - throw new NotImplementedException(); + if (extraDataMask.Size != Unsafe.SizeOf()) + return ResultFs.InvalidArgument.Log(); + + if (extraData.Size != Unsafe.SizeOf()) + return ResultFs.InvalidArgument.Log(); + + ref readonly SaveDataExtraData maskRef = + ref SpanHelpers.AsReadOnlyStruct(extraDataMask.Buffer); + + ref readonly SaveDataExtraData extraDataRef = + ref SpanHelpers.AsReadOnlyStruct(extraData.Buffer); + + return WriteSaveDataFileSystemExtraDataWithMaskCore(saveDataId, spaceId, in extraDataRef, in maskRef); } public Result OpenSaveDataInfoReader(out ReferenceCountedDisposable infoReader) @@ -1462,7 +1760,21 @@ namespace LibHac.FsSrv public Result GetSaveDataCommitId(out long commitId, SaveDataSpaceId spaceId, ulong saveDataId) { - throw new NotImplementedException(); + UnsafeHelpers.SkipParamInit(out commitId); + + Result rc = GetProgramInfo(out ProgramInfo programInfo); + if (rc.IsFailure()) return rc; + + if (!programInfo.AccessControl.CanCall(OperationType.GetSaveDataCommitId)) + return ResultFs.PermissionDenied.Log(); + + Unsafe.SkipInit(out SaveDataExtraData extraData); + rc = ReadSaveDataFileSystemExtraDataBySaveDataSpaceId(OutBuffer.FromStruct(ref extraData), spaceId, + saveDataId); + if (rc.IsFailure()) return rc; + + commitId = Impl.Utility.ConvertZeroCommitId(in extraData); + return Result.Success; } public Result OpenSaveDataInfoReaderOnlyCacheStorage( @@ -1700,9 +2012,9 @@ namespace LibHac.FsSrv throw new NotImplementedException(); } - private ProgramId ResolveDefaultSaveDataReferenceProgramId(in ProgramId programId) + private ProgramId ResolveDefaultSaveDataReferenceProgramId(ProgramId programId) { - return ServiceImpl.ResolveDefaultSaveDataReferenceProgramId(in programId); + return ServiceImpl.ResolveDefaultSaveDataReferenceProgramId(programId); } public Result VerifySaveDataFileSystemBySaveDataSpaceId(SaveDataSpaceId spaceId, ulong saveDataId, @@ -1995,6 +2307,31 @@ namespace LibHac.FsSrv return (long)id < 0; } + private void ModifySaveDataExtraData(ref SaveDataExtraData currentExtraData, in SaveDataExtraData extraData, + in SaveDataExtraData extraDataMask) + { + Span currentExtraDataBytes = SpanHelpers.AsByteSpan(ref currentExtraData); + ReadOnlySpan extraDataBytes = SpanHelpers.AsReadOnlyByteSpan(in extraData); + ReadOnlySpan extraDataMaskBytes = SpanHelpers.AsReadOnlyByteSpan(in extraDataMask); + + for (int i = 0; i < Unsafe.SizeOf(); i++) + { + currentExtraDataBytes[i] = (byte)(extraDataBytes[i] & extraDataMaskBytes[i] | + currentExtraDataBytes[i] & ~extraDataMaskBytes[i]); + } + } + + private void MaskExtraData(ref SaveDataExtraData extraData, in SaveDataExtraData extraDataMask) + { + Span extraDataBytes = SpanHelpers.AsByteSpan(ref extraData); + ReadOnlySpan extraDataMaskBytes = SpanHelpers.AsReadOnlyByteSpan(in extraDataMask); + + for (int i = 0; i < Unsafe.SizeOf(); i++) + { + extraDataBytes[i] &= extraDataMaskBytes[i]; + } + } + private StorageType DecidePossibleStorageFlag(SaveDataType type, SaveDataSpaceId spaceId) { if (type == SaveDataType.Cache || type == SaveDataType.Bcat) diff --git a/src/LibHac/FsSrv/SaveDataFileSystemServiceImpl.cs b/src/LibHac/FsSrv/SaveDataFileSystemServiceImpl.cs index eb743f38..0378549f 100644 --- a/src/LibHac/FsSrv/SaveDataFileSystemServiceImpl.cs +++ b/src/LibHac/FsSrv/SaveDataFileSystemServiceImpl.cs @@ -774,7 +774,7 @@ namespace LibHac.FsSrv /// for their save data. The main program always has a program index of 0. /// The program ID to get the save data program ID for. /// The program ID of the save data. - public ProgramId ResolveDefaultSaveDataReferenceProgramId(in ProgramId programId) + public ProgramId ResolveDefaultSaveDataReferenceProgramId(ProgramId programId) { // First check if there's an entry in the program index map with the program ID and program index 0 ProgramId mainProgramId = _config.ProgramRegistryService.GetProgramIdByIndex(programId, 0); diff --git a/tests/LibHac.Tests/Fs/FileSystemClientTests/ShimTests/Bis.cs b/tests/LibHac.Tests/Fs/FileSystemClientTests/ShimTests/Bis.cs index e253afac..c5f17ebc 100644 --- a/tests/LibHac.Tests/Fs/FileSystemClientTests/ShimTests/Bis.cs +++ b/tests/LibHac.Tests/Fs/FileSystemClientTests/ShimTests/Bis.cs @@ -22,6 +22,7 @@ namespace LibHac.Tests.Fs.FileSystemClientTests.ShimTests Assert.Success(rootFs.GetEntryType(out DirectoryEntryType type, "/bis/cal/file".ToU8Span())); Assert.Equal(DirectoryEntryType.File, type); } + [Fact] public void MountBis_MountSafePartition_OpensCorrectDirectory() { diff --git a/tests/LibHac.Tests/Fs/FileSystemClientTests/ShimTests/SaveDataManagement.cs b/tests/LibHac.Tests/Fs/FileSystemClientTests/ShimTests/SaveDataManagement.cs index a39bec47..3b52b6be 100644 --- a/tests/LibHac.Tests/Fs/FileSystemClientTests/ShimTests/SaveDataManagement.cs +++ b/tests/LibHac.Tests/Fs/FileSystemClientTests/ShimTests/SaveDataManagement.cs @@ -3,6 +3,7 @@ using System.Linq; using LibHac.Common; using LibHac.Fs; using LibHac.Fs.Shim; +using LibHac.Time; using Xunit; namespace LibHac.Tests.Fs.FileSystemClientTests.ShimTests @@ -159,6 +160,90 @@ namespace LibHac.Tests.Fs.FileSystemClientTests.ShimTests Assert.Equal(userId, info[0].UserId); } + [Fact] + public void CreateSaveData_DoesNotExist_HasCorrectOwnerId() + { + uint ownerId = 1; + + var applicationId = new Ncm.ApplicationId(ownerId); + var userId = new UserId(5, 4); + + FileSystemClient fs = FileSystemServerFactory.CreateClient(true); + + // Create the save + Assert.Success(fs.CreateSaveData(applicationId, userId, ownerId, 0x1000, 0x1000, SaveDataFlags.None)); + + // Get the created save data's ID + Assert.Success(fs.OpenSaveDataIterator(out SaveDataIterator iterator, SaveDataSpaceId.User)); + + var info = new SaveDataInfo[2]; + iterator.ReadSaveDataInfo(out long entriesRead, info); + + Assert.Equal(1, entriesRead); + + // Get the created save data's owner ID + Assert.Success(fs.GetSaveDataOwnerId(out ulong actualOwnerId, info[0].SaveDataId)); + + Assert.Equal(ownerId, actualOwnerId); + } + + [Fact] + public void CreateSaveData_DoesNotExist_HasCorrectFlags() + { + SaveDataFlags flags = SaveDataFlags.KeepAfterRefurbishment | SaveDataFlags.NeedsSecureDelete; + + var applicationId = new Ncm.ApplicationId(1); + var userId = new UserId(5, 4); + + FileSystemClient fs = FileSystemServerFactory.CreateClient(true); + + // Create the save + Assert.Success(fs.CreateSaveData(applicationId, userId, 0, 0x1000, 0x1000, flags)); + + // Get the created save data's ID + Assert.Success(fs.OpenSaveDataIterator(out SaveDataIterator iterator, SaveDataSpaceId.User)); + + var info = new SaveDataInfo[2]; + iterator.ReadSaveDataInfo(out long entriesRead, info); + + Assert.Equal(1, entriesRead); + + // Get the created save data's flags + Assert.Success(fs.GetSaveDataFlags(out SaveDataFlags actualFlags, info[0].SaveDataId)); + + Assert.Equal(flags, actualFlags); + } + + [Fact] + public void CreateSaveData_DoesNotExist_HasCorrectSizes() + { + long availableSize = 0x220000; + long journalSize = 0x120000; + + var applicationId = new Ncm.ApplicationId(1); + var userId = new UserId(5, 4); + + FileSystemClient fs = FileSystemServerFactory.CreateClient(true); + + // Create the save + Assert.Success(fs.CreateSaveData(applicationId, userId, 0, availableSize, journalSize, SaveDataFlags.None)); + + // Get the created save data's ID + Assert.Success(fs.OpenSaveDataIterator(out SaveDataIterator iterator, SaveDataSpaceId.User)); + + var info = new SaveDataInfo[2]; + iterator.ReadSaveDataInfo(out long entriesRead, info); + + Assert.Equal(1, entriesRead); + + // Get the created save data's sizes + Assert.Success(fs.GetSaveDataAvailableSize(out long actualAvailableSize, info[0].SaveDataId)); + Assert.Success(fs.GetSaveDataJournalSize(out long actualJournalSize, info[0].SaveDataId)); + + Assert.Equal(availableSize, actualAvailableSize); + Assert.Equal(journalSize, actualJournalSize); + } + [Fact] public void DeleteSaveData_DoesNotExist_ReturnsTargetNotFound() { @@ -287,6 +372,64 @@ namespace LibHac.Tests.Fs.FileSystemClientTests.ShimTests } } + [Fact] + public void GetSaveDataCommitId_AfterSetSaveDataCommitIdIsCalled_ReturnsSetCommitId() + { + long commitId = 46506854; + + var applicationId = new Ncm.ApplicationId(1); + var userId = new UserId(5, 4); + + FileSystemClient fs = FileSystemServerFactory.CreateClient(true); + + // Create the save + Assert.Success(fs.CreateSaveData(applicationId, userId, 0, 0x1000, 0x1000, SaveDataFlags.None)); + + // Get the created save data's ID + Assert.Success(fs.OpenSaveDataIterator(out SaveDataIterator iterator, SaveDataSpaceId.User)); + + var info = new SaveDataInfo[2]; + iterator.ReadSaveDataInfo(out long entriesRead, info); + + Assert.Equal(1, entriesRead); + + // Set the new commit ID + Assert.Success(fs.SetSaveDataCommitId(info[0].SpaceId, info[0].SaveDataId, commitId)); + + Assert.Success(fs.GetSaveDataCommitId(out long actualCommitId, info[0].SpaceId, info[0].SaveDataId)); + + Assert.Equal(commitId, actualCommitId); + } + + [Fact] + public void GetSaveDataTimeStamp_AfterSetSaveDataTimeStampIsCalled_ReturnsSetTimeStamp() + { + var timeStamp = new PosixTime(12345678); + + var applicationId = new Ncm.ApplicationId(1); + var userId = new UserId(5, 4); + + FileSystemClient fs = FileSystemServerFactory.CreateClient(true); + + // Create the save + Assert.Success(fs.CreateSaveData(applicationId, userId, 0, 0x1000, 0x1000, SaveDataFlags.None)); + + // Get the created save data's ID + Assert.Success(fs.OpenSaveDataIterator(out SaveDataIterator iterator, SaveDataSpaceId.User)); + + var info = new SaveDataInfo[2]; + iterator.ReadSaveDataInfo(out long entriesRead, info); + + Assert.Equal(1, entriesRead); + + // Set the new timestamp + Assert.Success(fs.SetSaveDataTimeStamp(info[0].SpaceId, info[0].SaveDataId, timeStamp)); + + Assert.Success(fs.GetSaveDataTimeStamp(out PosixTime actualTimeStamp, info[0].SpaceId, info[0].SaveDataId)); + + Assert.Equal(timeStamp, actualTimeStamp); + } + private static Result PopulateSaveData(FileSystemClient fs, int count, int seed = -1) { if (seed == -1)