diff --git a/src/LibHac/Fs/Shim/DeviceSaveData.cs b/src/LibHac/Fs/Shim/DeviceSaveData.cs new file mode 100644 index 00000000..ab759bdd --- /dev/null +++ b/src/LibHac/Fs/Shim/DeviceSaveData.cs @@ -0,0 +1,179 @@ +using System; +using LibHac.Common; +using LibHac.Diag; +using LibHac.Fs.Fsa; +using LibHac.Fs.Impl; +using LibHac.FsSrv.Sf; +using LibHac.Ncm; +using LibHac.Os; +using IFileSystem = LibHac.Fs.Fsa.IFileSystem; +using IFileSystemSf = LibHac.FsSrv.Sf.IFileSystem; + +using static LibHac.Fs.Impl.AccessLogStrings; +using static LibHac.Fs.SaveData; + +namespace LibHac.Fs.Shim; + +public static class DeviceSaveData +{ + private class DeviceSaveDataAttributeGetter : ISaveDataAttributeGetter + { + private ProgramId _programId; + + public DeviceSaveDataAttributeGetter(ProgramId programId) + { + _programId = programId; + } + + public void Dispose() { } + + public Result GetSaveDataAttribute(out SaveDataAttribute attribute) + { + Result rc = SaveDataAttribute.Make(out attribute, _programId, SaveDataType.Device, InvalidUserId, + InvalidSystemSaveDataId); + if (rc.IsFailure()) return rc.Miss(); + + return Result.Success; + } + } + + private const SaveDataSpaceId DeviceSaveDataSpaceId = SaveDataSpaceId.User; + + private static Result MountDeviceSaveDataImpl(this FileSystemClientImpl fs, U8Span mountName, + in SaveDataAttribute attribute) + { + Result rc = fs.CheckMountName(mountName); + if (rc.IsFailure()) return rc; + + using SharedRef fileSystemProxy = fs.GetFileSystemProxyServiceObject(); + using var fileSystem = new SharedRef(); + + rc = fileSystemProxy.Get.OpenSaveDataFileSystem(ref fileSystem.Ref(), DeviceSaveDataSpaceId, in attribute); + if (rc.IsFailure()) return rc.Miss(); + + var fileSystemAdapterRaw = new FileSystemServiceObjectAdapter(ref fileSystem.Ref()); + using var fileSystemAdapter = new UniqueRef(fileSystemAdapterRaw); + + if (!fileSystemAdapter.HasValue) + return ResultFs.AllocationMemoryFailedInDeviceSaveDataA.Log(); + + using var saveDataAttributeGetter = + new UniqueRef(new DeviceSaveDataAttributeGetter(attribute.ProgramId)); + + using var mountNameGenerator = new UniqueRef(); + + rc = fs.Fs.Register(mountName, fileSystemAdapterRaw, ref fileSystemAdapter.Ref(), ref mountNameGenerator.Ref(), + ref saveDataAttributeGetter.Ref(), useDataCache: false, storageForPurgeFileDataCache: null, + usePathCache: true); + if (rc.IsFailure()) return rc.Miss(); + + return Result.Success; + } + + public static Result MountDeviceSaveData(this FileSystemClient fs, U8Span mountName) + { + Span logBuffer = stackalloc byte[0x30]; + + Result rc = SaveDataAttribute.Make(out SaveDataAttribute attribute, InvalidProgramId, SaveDataType.Device, + InvalidUserId, InvalidSystemSaveDataId); + + fs.Impl.AbortIfNeeded(rc); + if (rc.IsFailure()) return rc.Miss(); + + if (fs.Impl.IsEnabledAccessLog(AccessLogTarget.Application)) + { + Tick start = fs.Hos.Os.GetSystemTick(); + rc = MountDeviceSaveDataImpl(fs.Impl, mountName, in attribute); + Tick end = fs.Hos.Os.GetSystemTick(); + + var sb = new U8StringBuilder(logBuffer, true); + sb.Append(LogName).Append(mountName).Append(LogQuote); + + fs.Impl.OutputAccessLog(rc, start, end, null, new U8Span(sb.Buffer)); + } + else + { + rc = MountDeviceSaveDataImpl(fs.Impl, mountName, in attribute); + } + + fs.Impl.AbortIfNeeded(rc); + if (rc.IsFailure()) return rc.Miss(); + + if (fs.Impl.IsEnabledAccessLog(AccessLogTarget.Application)) + fs.Impl.EnableFileSystemAccessorAccessLog(mountName); + + return Result.Success; + } + + public static Result MountDeviceSaveData(this FileSystemClient fs, U8Span mountName, + Ncm.ApplicationId applicationId) + { + Span logBuffer = stackalloc byte[0x50]; + + Result rc = SaveDataAttribute.Make(out SaveDataAttribute attribute, applicationId, SaveDataType.Device, + InvalidUserId, InvalidSystemSaveDataId); + + fs.Impl.AbortIfNeeded(rc); + if (rc.IsFailure()) return rc.Miss(); + + if (fs.Impl.IsEnabledAccessLog(AccessLogTarget.Application)) + { + Tick start = fs.Hos.Os.GetSystemTick(); + rc = MountDeviceSaveDataImpl(fs.Impl, mountName, in attribute); + Tick end = fs.Hos.Os.GetSystemTick(); + + var sb = new U8StringBuilder(logBuffer, true); + sb.Append(LogName).Append(mountName).Append(LogQuote) + .Append(LogApplicationId).AppendFormat(applicationId.Value, 'X'); + + fs.Impl.OutputAccessLog(rc, start, end, null, new U8Span(sb.Buffer)); + } + else + { + rc = MountDeviceSaveDataImpl(fs.Impl, mountName, in attribute); + } + + fs.Impl.AbortIfNeeded(rc); + if (rc.IsFailure()) return rc.Miss(); + + if (fs.Impl.IsEnabledAccessLog(AccessLogTarget.Application)) + fs.Impl.EnableFileSystemAccessorAccessLog(mountName); + + return Result.Success; + } + + public static Result MountDeviceSaveData(this FileSystemClient fs, U8Span mountName, ApplicationId applicationId) + { + return MountDeviceSaveData(fs, mountName, new Ncm.ApplicationId(applicationId.Value)); + } + + public static bool IsDeviceSaveDataExisting(this FileSystemClient fs, ApplicationId applicationId) + { + Result rc; + Span logBuffer = stackalloc byte[0x30]; + + bool exists; + var appId = new Ncm.ApplicationId(applicationId.Value); + + if (fs.Impl.IsEnabledAccessLog() && fs.Impl.IsEnabledHandleAccessLog(null)) + { + Tick start = fs.Hos.Os.GetSystemTick(); + rc = fs.Impl.IsSaveDataExisting(out exists, appId, SaveDataType.Device, InvalidUserId); + Tick end = fs.Hos.Os.GetSystemTick(); + + var sb = new U8StringBuilder(logBuffer, true); + sb.Append(LogApplicationId).AppendFormat(applicationId.Value, 'X'); + + fs.Impl.OutputAccessLog(rc, start, end, null, new U8Span(sb.Buffer)); + } + else + { + rc = fs.Impl.IsSaveDataExisting(out exists, appId, SaveDataType.Device, InvalidUserId); + } + + fs.Impl.LogResultErrorMessage(rc); + Abort.DoAbortUnless(rc.IsSuccess()); + + return exists; + } +} \ No newline at end of file diff --git a/src/LibHac/FsSrv/SaveDataFileSystemService.cs b/src/LibHac/FsSrv/SaveDataFileSystemService.cs index b9bd2f43..3a6149fb 100644 --- a/src/LibHac/FsSrv/SaveDataFileSystemService.cs +++ b/src/LibHac/FsSrv/SaveDataFileSystemService.cs @@ -147,6 +147,7 @@ internal class SaveDataFileSystemService : ISaveDataTransferCoreInterface, ISave } else if (attribute.Type == SaveDataType.Account && attribute.UserId == InvalidUserId) { + // Trying to create a program's debug save. bool canAccess = accessControl.CanCall(OperationType.CreateSaveData) || accessControl.CanCall(OperationType.DebugSaveData); @@ -193,6 +194,7 @@ internal class SaveDataFileSystemService : ISaveDataTransferCoreInterface, ISave } else if (attribute.Type == SaveDataType.Account) { + // We need debug save data permissions to open a debug save. bool canAccess = attribute.UserId != InvalidUserId || accessControl.CanCall(OperationType.DebugSaveData); @@ -303,11 +305,11 @@ internal class SaveDataFileSystemService : ISaveDataTransferCoreInterface, ISave { canAccess |= accessControl.CanCall(OperationType.ExtendOwnSaveData); - bool hasDebugAccess = accessControl.CanCall(OperationType.DebugSaveData) - && attribute.Type == SaveDataType.Account - && attribute.UserId == UserId.InvalidId; + bool canAccessDebugSave = accessControl.CanCall(OperationType.DebugSaveData) + && attribute.Type == SaveDataType.Account + && attribute.UserId == UserId.InvalidId; - canAccess |= hasDebugAccess; + canAccess |= canAccessDebugSave; if (!canAccess) return ResultFs.PermissionDenied.Log(); @@ -351,11 +353,11 @@ internal class SaveDataFileSystemService : ISaveDataTransferCoreInterface, ISave { canAccess |= accessControl.CanCall(OperationType.ReadOwnSaveDataFileSystemExtraData); - bool hasDebugAccess = accessControl.CanCall(OperationType.DebugSaveData) - && attribute.Type == SaveDataType.Account - && attribute.UserId == UserId.InvalidId; + bool canAccessDebugSave = accessControl.CanCall(OperationType.DebugSaveData) + && attribute.Type == SaveDataType.Account + && attribute.UserId == UserId.InvalidId; - canAccess |= hasDebugAccess; + canAccess |= canAccessDebugSave; } if (!canAccess) @@ -442,11 +444,11 @@ internal class SaveDataFileSystemService : ISaveDataTransferCoreInterface, ISave AccessControl accessControl = programInfo.AccessControl; canAccess = accessControl.CanCall(OperationType.FindOwnSaveDataWithFilter); - bool hasDebugAccess = accessControl.CanCall(OperationType.DebugSaveData) - && filter.Attribute.Type == SaveDataType.Account - && filter.Attribute.UserId == UserId.InvalidId; + bool canAccessDebugSave = accessControl.CanCall(OperationType.DebugSaveData) + && filter.Attribute.Type == SaveDataType.Account + && filter.Attribute.UserId == UserId.InvalidId; - canAccess |= hasDebugAccess; + canAccess |= canAccessDebugSave; } else { diff --git a/tests/LibHac.Tests/Fs/FileSystemClientTests/FileSystemServerFactory.cs b/tests/LibHac.Tests/Fs/FileSystemClientTests/FileSystemServerFactory.cs index 185121da..00b9f5de 100644 --- a/tests/LibHac.Tests/Fs/FileSystemClientTests/FileSystemServerFactory.cs +++ b/tests/LibHac.Tests/Fs/FileSystemClientTests/FileSystemServerFactory.cs @@ -2,27 +2,45 @@ using LibHac.Fs; using LibHac.Fs.Fsa; using LibHac.FsSrv; +using LibHac.FsSrv.Impl; using LibHac.FsSystem; +using LibHac.Ncm; using LibHac.Tools.Fs; namespace LibHac.Tests.Fs.FileSystemClientTests; +public class HorizonServerSet +{ + public Horizon Server { get; set; } + public HorizonClient FsProcessClient { get; set; } + public HorizonClient InitialProcessClient { get; set; } + public HorizonClient Client { get; set; } + public FileSystemServer FsServer { get; set; } + public IFileSystem RootFileSystem { get; set; } +} + public static class FileSystemServerFactory { - private static Horizon CreateHorizonImpl(bool sdCardInserted, out IFileSystem rootFs) + private static HorizonServerSet CreateHorizonImpl(bool sdCardInserted = true, + AccessControlBits.Bits fsAcBits = AccessControlBits.Bits.Debug, ProgramLocation programLocation = default) { - rootFs = new InMemoryFileSystem(); + var hos = new HorizonServerSet(); + hos.RootFileSystem = new InMemoryFileSystem(); var keySet = new KeySet(); - var horizon = new Horizon(new HorizonConfiguration()); + hos.Server = new Horizon(new HorizonConfiguration()); - HorizonClient fsServerClient = horizon.CreatePrivilegedHorizonClient(); - var fsServer = new FileSystemServer(fsServerClient); + hos.FsProcessClient = hos.Server.CreatePrivilegedHorizonClient(); + hos.FsServer = new FileSystemServer(hos.FsProcessClient); + + hos.InitialProcessClient = hos.Server.CreatePrivilegedHorizonClient(); var random = new Random(12345); RandomDataGenerator randomGenerator = buffer => random.NextBytes(buffer); - var defaultObjects = DefaultFsServerObjects.GetDefaultEmulatedCreators(rootFs, keySet, fsServer, randomGenerator); + var defaultObjects = + DefaultFsServerObjects.GetDefaultEmulatedCreators(hos.RootFileSystem, keySet, hos.FsServer, + randomGenerator); defaultObjects.SdCard.SetSdCardInsertionStatus(sdCardInserted); @@ -32,31 +50,45 @@ public static class FileSystemServerFactory config.ExternalKeySet = new ExternalKeySet(); config.RandomGenerator = randomGenerator; - FileSystemServerInitializer.InitializeWithConfig(fsServerClient, fsServer, config); - return horizon; - } + FileSystemServerInitializer.InitializeWithConfig(hos.FsProcessClient, hos.FsServer, config); + hos.FsServer.SetDebugFlagEnabled(true); - private static FileSystemClient CreateClientImpl(bool sdCardInserted, out IFileSystem rootFs) - { - Horizon horizon = CreateHorizonImpl(sdCardInserted, out rootFs); + if (programLocation.ProgramId == ProgramId.InvalidId) + { + hos.Client = hos.Server.CreateHorizonClient(); + } + else + { + hos.Client = hos.Server.CreateHorizonClient(programLocation, fsAcBits); + } - HorizonClient horizonClient = horizon.CreatePrivilegedHorizonClient(); - - return horizonClient.Fs; + return hos; } public static FileSystemClient CreateClient(bool sdCardInserted) { - return CreateClientImpl(sdCardInserted, out _); + HorizonServerSet hos = CreateHorizonImpl(sdCardInserted: sdCardInserted); + + return hos.InitialProcessClient.Fs; } public static FileSystemClient CreateClient(out IFileSystem rootFs) { - return CreateClientImpl(false, out rootFs); + HorizonServerSet hos = CreateHorizonImpl(sdCardInserted: true); + rootFs = hos.RootFileSystem; + + return hos.InitialProcessClient.Fs; } public static Horizon CreateHorizonServer() { - return CreateHorizonImpl(true, out _); + return CreateHorizonImpl(sdCardInserted: true).Server; + } + + public static HorizonServerSet CreateHorizon(ProgramId programId = default, bool sdCardInserted = true, + AccessControlBits.Bits fsAcBits = AccessControlBits.Bits.Debug) + { + var programLocation = new ProgramLocation(programId, StorageId.BuiltInUser); + return CreateHorizonImpl(sdCardInserted, fsAcBits, programLocation); } } \ No newline at end of file diff --git a/tests/LibHac.Tests/Fs/FileSystemClientTests/ShimTests/DeviceSaveData.cs b/tests/LibHac.Tests/Fs/FileSystemClientTests/ShimTests/DeviceSaveData.cs new file mode 100644 index 00000000..ef205047 --- /dev/null +++ b/tests/LibHac.Tests/Fs/FileSystemClientTests/ShimTests/DeviceSaveData.cs @@ -0,0 +1,65 @@ +using LibHac.Common; +using LibHac.Fs; +using LibHac.Fs.Shim; +using LibHac.FsSrv.Impl; +using Xunit; + +namespace LibHac.Tests.Fs.FileSystemClientTests.ShimTests; + +public class DeviceSaveData +{ + [Fact] + public static void MountDeviceSaveData_SaveDoesNotExist_ReturnsTargetNotFound() + { + var applicationId = new Ncm.ApplicationId(1234); + HorizonServerSet hos = FileSystemServerFactory.CreateHorizon(applicationId, fsAcBits: AccessControlBits.Bits.FullPermission); + + Assert.Result(ResultFs.TargetNotFound, hos.Client.Fs.MountDeviceSaveData("device".ToU8Span(), applicationId)); + Assert.Result(ResultFs.TargetNotFound, hos.Client.Fs.MountDeviceSaveData("device2".ToU8Span())); + } + + [Fact] + public static void MountDeviceSaveData_OwnDeviceSaveExists_ReturnsSuccess() + { + var applicationId = new Ncm.ApplicationId(1234); + HorizonServerSet hos = FileSystemServerFactory.CreateHorizon(applicationId, fsAcBits: AccessControlBits.Bits.FullPermission); + + Assert.Success(hos.Client.Fs.CreateDeviceSaveData(applicationId, applicationId.Value, 0, 0, SaveDataFlags.None)); + Assert.Success(hos.Client.Fs.MountDeviceSaveData("device".ToU8Span())); + Assert.Success(hos.Client.Fs.MountDeviceSaveData("device2".ToU8Span(), applicationId)); + } + + [Fact] + public static void MountDeviceSaveData_OtherDeviceSaveExists_ReturnsSuccess() + { + var ownApplicationId = new Ncm.ApplicationId(1234); + var otherApplicationId = new Ncm.ApplicationId(12345); + HorizonServerSet hos = FileSystemServerFactory.CreateHorizon(ownApplicationId, fsAcBits: AccessControlBits.Bits.FullPermission); + + Assert.Success(hos.Client.Fs.CreateDeviceSaveData(otherApplicationId, otherApplicationId.Value, 0, 0, SaveDataFlags.None)); + Assert.Success(hos.Client.Fs.MountDeviceSaveData("device".ToU8Span(), otherApplicationId)); + + // Try to open missing own device save data + Assert.Result(ResultFs.TargetNotFound, hos.Client.Fs.MountDeviceSaveData("device2".ToU8Span())); + } + + [Fact] + public static void IsDeviceSaveDataExisting_ReturnsCorrectState() + { + var applicationId = new ApplicationId(1234); + var ncmApplicationId = new Ncm.ApplicationId(applicationId.Value); + HorizonServerSet hos = FileSystemServerFactory.CreateHorizon(ncmApplicationId); + FileSystemClient fs = hos.InitialProcessClient.Fs; + + // Should return false when there aren't any saves. + Assert.False(fs.IsDeviceSaveDataExisting(applicationId)); + + // Should return true after creating the save. + Assert.Success(fs.CreateDeviceSaveData(ncmApplicationId, applicationId.Value, 0, 0, SaveDataFlags.None)); + Assert.True(fs.IsDeviceSaveDataExisting(applicationId)); + + // Should return false after deleting the save. + Assert.Success(fs.DeleteDeviceSaveData(applicationId)); + Assert.False(fs.IsDeviceSaveDataExisting(applicationId)); + } +} \ No newline at end of file