diff --git a/src/LibHac/Fs/Impl/SaveDataMetaPolicy.cs b/src/LibHac/Fs/Impl/SaveDataMetaPolicy.cs
index 6492747a..50cfdcd4 100644
--- a/src/LibHac/Fs/Impl/SaveDataMetaPolicy.cs
+++ b/src/LibHac/Fs/Impl/SaveDataMetaPolicy.cs
@@ -40,4 +40,24 @@ internal readonly struct SaveDataMetaPolicy
GenerateMetaInfo(out SaveDataMetaInfo metaInfo);
return metaInfo.Type;
}
+}
+
+internal readonly struct SaveDataMetaPolicyForSaveDataTransfer
+{
+ private readonly SaveDataMetaPolicy _base;
+
+ public SaveDataMetaPolicyForSaveDataTransfer(SaveDataType saveType) => _base = new SaveDataMetaPolicy(saveType);
+ public void GenerateMetaInfo(out SaveDataMetaInfo metaInfo) => _base.GenerateMetaInfo(out metaInfo);
+ public long GetSaveDataMetaSize() => _base.GetSaveDataMetaSize();
+ public SaveDataMetaType GetSaveDataMetaType() => _base.GetSaveDataMetaType();
+}
+
+internal readonly struct SaveDataMetaPolicyForSaveDataTransferVersion2
+{
+ private readonly SaveDataMetaPolicy _base;
+
+ public SaveDataMetaPolicyForSaveDataTransferVersion2(SaveDataType saveType) => _base = new SaveDataMetaPolicy(saveType);
+ public void GenerateMetaInfo(out SaveDataMetaInfo metaInfo) => _base.GenerateMetaInfo(out metaInfo);
+ public long GetSaveDataMetaSize() => _base.GetSaveDataMetaSize();
+ public SaveDataMetaType GetSaveDataMetaType() => _base.GetSaveDataMetaType();
}
\ No newline at end of file
diff --git a/src/LibHac/FsSrv/Impl/SaveDataMover.cs b/src/LibHac/FsSrv/Impl/SaveDataMover.cs
index dcdd47d6..9a8dd216 100644
--- a/src/LibHac/FsSrv/Impl/SaveDataMover.cs
+++ b/src/LibHac/FsSrv/Impl/SaveDataMover.cs
@@ -1,16 +1,25 @@
-// ReSharper disable UnusedMember.Local UnusedType.Local
-#pragma warning disable CS0169 // Field is never used
-using System;
+using System;
+using System.Runtime.CompilerServices;
using LibHac.Common;
using LibHac.Common.FixedArrays;
+using LibHac.Diag;
using LibHac.Fs;
+using LibHac.Fs.Impl;
using LibHac.FsSrv.Sf;
using LibHac.Os;
using LibHac.Sf;
using LibHac.Util;
+using IFileSystem = LibHac.Fs.Fsa.IFileSystem;
+using IStorage = LibHac.Fs.IStorage;
namespace LibHac.FsSrv.Impl;
+///
+/// Bulk moves save data from one save data space to another.
+///
+/// To use this class, call for each save data to be moved. After all save data
+/// have been registered, repeatedly call until it returns 0 for the remaining size.
+/// Based on nnSdk 18.3.0 (FS 18.0.0)
public class SaveDataMover : ISaveDataMover
{
private enum State
@@ -25,13 +34,13 @@ public class SaveDataMover : ISaveDataMover
private SharedRef _transferInterface;
private Optional _duplicator;
- private SaveDataSpaceId _sourceSpaceId;
- private Array128 _sourceSaveIds;
- private SaveDataSpaceId _destinationSpaceId;
- private Array128 _destinationSaveIds;
+ private SaveDataSpaceId _srcSpaceId;
+ private Array128 _srcSaveIds;
+ private SaveDataSpaceId _dstSpaceId;
+ private Array128 _dstSaveIds;
// private TransferMemory _transferMemory;
private Memory _workBuffer;
- // private ulong _transferMemorySize;
+ private ulong _transferMemorySize;
private int _saveCount;
private int _currentSaveIndex;
private long _remainingSize;
@@ -42,46 +51,377 @@ public class SaveDataMover : ISaveDataMover
SaveDataSpaceId sourceSpaceId, SaveDataSpaceId destinationSpaceId, NativeHandle transferMemoryHandle,
ulong transferMemorySize)
{
- throw new NotImplementedException();
+ _transferInterface = SharedRef.CreateCopy(in transferInterface);
+ _duplicator = new Optional();
+
+ _srcSpaceId = sourceSpaceId;
+ _srcSaveIds = default;
+ _dstSpaceId = destinationSpaceId;
+ _dstSaveIds = default;
+
+ // Missing: Attach transfer memory
+
+ _workBuffer = default;
+ _transferMemorySize = transferMemorySize;
+ _saveCount = 0;
+ _currentSaveIndex = 0;
+ _remainingSize = 0;
+ _state = State.Initial;
+ _mutex = new SdkMutex();
+ transferMemoryHandle.Detach();
}
public void Dispose()
{
- throw new NotImplementedException();
+ // Missing: Destroy transfer memory
+
+ if (_duplicator.HasValue)
+ {
+ _duplicator.Value.Dispose();
+ _duplicator.Clear();
+ }
+
+ _transferInterface.Destroy();
}
private Result OpenDuplicator()
{
- throw new NotImplementedException();
+ Result res;
+
+ using var srcFileStorage = new SharedRef();
+ using (var srcInternalStorageAccessor = new SharedRef())
+ {
+ res = SaveDataTransferUtilityGlobalMethods.OpenSaveDataInternalStorageAccessor(null,
+ ref srcInternalStorageAccessor.Ref, _srcSpaceId, _srcSaveIds[_currentSaveIndex]);
+ if (res.IsFailure()) return res.Miss();
+
+ res = srcInternalStorageAccessor.Get.Initialize(coreInterface: _transferInterface.Get,
+ isTemporaryTransferSave: false, hashSalt: default);
+ if (res.IsFailure()) return res.Miss();
+
+ res = srcInternalStorageAccessor.Get.OpenConcatenationStorage(ref srcFileStorage.Ref);
+ if (res.IsFailure()) return res.Miss();
+ }
+
+ using var dstFileStorage = new SharedRef();
+ using var dstFs = new SharedRef();
+ using (var dstInternalStorageAccessor = new SharedRef())
+ {
+ res = SaveDataTransferUtilityGlobalMethods.OpenSaveDataInternalStorageAccessor(null,
+ ref dstInternalStorageAccessor.Ref, _dstSpaceId, _dstSaveIds[_currentSaveIndex]);
+ if (res.IsFailure()) return res.Miss();
+
+ res = dstInternalStorageAccessor.Get.Initialize(coreInterface: _transferInterface.Get,
+ isTemporaryTransferSave: false, hashSalt: default);
+ if (res.IsFailure()) return res.Miss();
+
+ res = dstInternalStorageAccessor.Get.OpenConcatenationStorage(ref dstFileStorage.Ref);
+ if (res.IsFailure()) return res.Miss();
+
+ using SharedRef fs = dstInternalStorageAccessor.Get.GetSaveDataInternalFileSystem();
+ dstFs.SetByMove(ref fs.Ref);
+ }
+
+ _duplicator.Set(new StorageDuplicator(in srcFileStorage, in dstFileStorage, in dstFs));
+ res = _duplicator.Value.Initialize();
+ if (res.IsFailure()) return res.Miss();
+
+ return Result.Success;
}
private Result Initialize()
{
- throw new NotImplementedException();
+ // Missing: Map transfer memory
+
+ _remainingSize = 0;
+ for (int i = 0; i < _saveCount; i++)
+ {
+ Result res = _transferInterface.Get.ReadSaveDataFileSystemExtraDataCore(out SaveDataExtraData srcExtraData,
+ _srcSpaceId, _srcSaveIds[i], isTemporarySaveData: false);
+ if (res.IsFailure()) return res.Miss();
+
+ if (!SaveDataProperties.IsValidSpaceIdForSaveDataMover(srcExtraData.Attribute.Type, _dstSpaceId))
+ return ResultFs.InvalidArgument.Log();
+
+ if (_srcSpaceId == _dstSpaceId)
+ return ResultFs.InvalidArgument.Log();
+
+ Unsafe.SkipInit(out IntegrityParam integrityParam);
+
+ using (var srcInternalStorageAccessor = new SharedRef())
+ {
+ res = SaveDataTransferUtilityGlobalMethods.OpenSaveDataInternalStorageAccessor(null,
+ ref srcInternalStorageAccessor.Ref, _srcSpaceId, _srcSaveIds[_currentSaveIndex]);
+ if (res.IsFailure()) return res.Miss();
+
+ res = srcInternalStorageAccessor.Get.Initialize(coreInterface: _transferInterface.Get,
+ isTemporaryTransferSave: false, hashSalt: default);
+ if (res.IsFailure()) return res.Miss();
+
+ res = srcInternalStorageAccessor.Get.GetIntegrityParam(ref integrityParam);
+ if (res.IsFailure()) return res.Miss();
+
+ using var srcFileStorage = new SharedRef();
+ res = srcInternalStorageAccessor.Get.OpenConcatenationStorage(ref srcFileStorage.Ref);
+ if (res.IsFailure()) return res.Miss();
+
+ res = srcFileStorage.Get.GetSize(out long size);
+ if (res.IsFailure()) return res.Miss();
+
+ _remainingSize += size;
+ }
+
+ SaveDataAttribute attribute = srcExtraData.Attribute;
+ res = SaveDataCreationInfo.Make(out SaveDataCreationInfo creationInfo, srcExtraData.DataSize,
+ srcExtraData.JournalSize, srcExtraData.OwnerId, srcExtraData.Flags, _dstSpaceId);
+ if (res.IsFailure()) return res.Miss();
+
+ new SaveDataMetaPolicyForSaveDataTransferVersion2(srcExtraData.Attribute.Type).GenerateMetaInfo(out SaveDataMetaInfo metaInfo);
+
+ res = _transferInterface.Get.CreateSaveDataFileSystemCore(in attribute, in creationInfo, in metaInfo,
+ new Optional(in integrityParam.IntegritySeed), leaveUnfinalized: true);
+ if (res.IsFailure()) return res.Miss();
+
+ res = _transferInterface.Get.GetSaveDataInfo(out SaveDataInfo info, _dstSpaceId, attribute);
+ if (res.IsFailure()) return res.Miss();
+
+ _dstSaveIds[i] = info.SaveDataId;
+ }
+
+ return OpenDuplicator().Ret();
}
public Result Register(ulong saveDataId)
{
- throw new NotImplementedException();
+ bool isSuccess = false;
+
+ try
+ {
+ using var scopedLock = new ScopedLock(ref _mutex);
+
+ if (_saveCount >= _srcSaveIds.Length)
+ return ResultFs.PreconditionViolation.Log();
+
+ switch (_state)
+ {
+ case State.Initial:
+ case State.Registered:
+ _srcSaveIds[_saveCount] = saveDataId;
+ _saveCount++;
+ _state = State.Registered;
+
+ isSuccess = true;
+ return Result.Success;
+ case State.Copying:
+ case State.Finished:
+ case State.Fatal:
+ case State.Canceled:
+ return ResultFs.PreconditionViolation.Log();
+ default:
+ Abort.UnexpectedDefault();
+ return Result.Success;
+ }
+ }
+ finally
+ {
+ if (!isSuccess)
+ ChangeStateToFatal();
+ }
}
- public Result Process(out long remainingSize, long sizeToProcess)
+ public Result Process(out long outRemainingSize, long sizeToProcess)
{
- throw new NotImplementedException();
+ UnsafeHelpers.SkipParamInit(out outRemainingSize);
+ bool isSuccess = false;
+
+ try
+ {
+ using var scopedLock = new ScopedLock(ref _mutex);
+
+ if ((long)_transferMemorySize < sizeToProcess)
+ return ResultFs.InvalidSize.Log();
+
+ Result res;
+ switch (_state)
+ {
+ case State.Initial:
+ return ResultFs.PreconditionViolation.Log();
+ case State.Registered:
+ res = Initialize();
+ if (res.IsFailure()) return res.Miss();
+
+ _state = State.Copying;
+ goto case State.Copying;
+ case State.Copying:
+ Assert.SdkAssert(!_workBuffer.IsEmpty);
+ Span workBuffer = _workBuffer.Span.Slice(0, (int)_transferMemorySize);
+
+ // Call the current duplicator with a size of zero to get the remaining size for this save before processing
+ res = _duplicator.Value.ProcessDuplication(out long remainingSizeBefore, workBuffer, 0);
+ if (res.IsFailure()) return res.Miss();
+
+ res = _duplicator.Value.ProcessDuplication(out long remainingSize, workBuffer, sizeToProcess);
+ if (res.IsFailure()) return res.Miss();
+
+ // Update the total remaining size with how much data was processed in this iteration
+ _remainingSize -= remainingSizeBefore - remainingSize;
+
+ if (remainingSize == 0)
+ {
+ // When finished copying this save, finalize and close the duplicator
+ res = _duplicator.Value.FinalizeObject();
+ if (res.IsFailure()) return res.Miss();
+
+ _duplicator.Value.Dispose();
+ _duplicator.Clear();
+
+ // Copy the extra data from the old save to the new save
+ res = _transferInterface.Get.ReadSaveDataFileSystemExtraDataCore(out SaveDataExtraData extraData,
+ _srcSpaceId, _srcSaveIds[_currentSaveIndex], isTemporarySaveData: false);
+ if (res.IsFailure()) return res.Miss();
+
+ res = _transferInterface.Get.WriteSaveDataFileSystemExtraDataCore(_dstSpaceId,
+ _dstSaveIds[_currentSaveIndex], in extraData, extraData.Attribute.Type,
+ updateTimeStamp: false);
+ if (res.IsFailure()) return res.Miss();
+
+ if (_currentSaveIndex == _saveCount - 1)
+ {
+ // If this was the last save to copy, finalize the entire move operation
+ FinalizeObject().IgnoreResult();
+ _state = State.Finished;
+ }
+ else
+ {
+ // If there are still saves to copy, open the duplicator for the next one
+ _currentSaveIndex++;
+
+ res = OpenDuplicator();
+ if (res.IsFailure()) return res.Miss();
+ }
+ }
+
+ outRemainingSize = _remainingSize;
+ isSuccess = true;
+
+ return Result.Success;
+ case State.Finished:
+ outRemainingSize = 0;
+ isSuccess = true;
+
+ return Result.Success;
+ case State.Fatal:
+ return ResultFs.PreconditionViolation.Log();
+ case State.Canceled:
+ return ResultFs.PreconditionViolation.Log();
+ default:
+ Abort.UnexpectedDefault();
+ return Result.Success;
+ }
+ }
+ finally
+ {
+ if (!isSuccess)
+ ChangeStateToFatal();
+ }
}
private Result FinalizeObject()
{
- throw new NotImplementedException();
+ Assert.SdkAssert(_mutex.IsLockedByCurrentThread());
+
+ Result res = SetStates(_dstSpaceId, _dstSaveIds, SaveDataState.Normal);
+ if (res.IsFailure()) return res.Miss();
+
+ res = SetStates(_srcSpaceId, _srcSaveIds, SaveDataState.Processing);
+ if (res.IsFailure()) return res.Miss();
+
+ for (int i = 0; i < _saveCount; i++)
+ {
+ res = _transferInterface.Get.DeleteSaveDataFileSystemBySaveDataSpaceId(_srcSpaceId, _srcSaveIds[i]);
+ if (res.IsFailure()) return res.Miss();
+ }
+
+ _state = State.Finished;
+ return Result.Success;
+
+ Result SetStates(SaveDataSpaceId spaceId, ReadOnlySpan saveDataIds, SaveDataState state)
+ {
+ using var accessor = new UniqueRef();
+ Result r = _transferInterface.Get.OpenSaveDataIndexerAccessor(ref accessor.Ref, spaceId);
+ if (r.IsFailure()) return r.Miss();
+
+ ReadOnlySpan ids = saveDataIds.Slice(0, _saveCount);
+ for (int i = 0; i < ids.Length; i++)
+ {
+ r = accessor.Get.GetInterface().SetState(ids[i], state);
+ if (r.IsFailure()) return r.Miss();
+ }
+
+ r = accessor.Get.GetInterface().Commit();
+ if (r.IsFailure()) return r.Miss();
+
+ return Result.Success;
+ }
}
public Result Cancel()
{
- throw new NotImplementedException();
+ Result DoCancel()
+ {
+ _duplicator.Value.Dispose();
+ _duplicator.Clear();
+
+ Result result = Result.Success;
+
+ for (int i = 0; i < _saveCount; i++)
+ {
+ Result res = _transferInterface.Get.DeleteSaveDataFileSystemBySaveDataSpaceId(_dstSpaceId, _dstSaveIds[_saveCount]);
+ if (!res.IsSuccess() && !ResultFs.TargetNotFound.Includes(res))
+ {
+ if (result.IsSuccess())
+ result = res;
+ }
+ }
+
+ _state = State.Canceled;
+ return result.Ret();
+ }
+
+ using var scopedLock = new ScopedLock(ref _mutex);
+
+ switch (_state)
+ {
+ case State.Initial:
+ case State.Registered:
+ case State.Copying:
+ case State.Fatal:
+ return DoCancel().Ret();
+ case State.Finished:
+ case State.Canceled:
+ return ResultFs.PreconditionViolation.Log();
+ default:
+ Abort.UnexpectedDefault();
+ return Result.Success;
+ }
}
private void ChangeStateToFatal()
{
- throw new NotImplementedException();
+ switch (_state)
+ {
+ case State.Initial:
+ case State.Registered:
+ case State.Copying:
+ _state = State.Fatal;
+ break;
+ case State.Finished:
+ case State.Fatal:
+ case State.Canceled:
+ break;
+ default:
+ Abort.UnexpectedDefault();
+ break;
+ }
}
}
\ No newline at end of file
diff --git a/src/LibHac/FsSrv/Impl/SaveDataTransferUtility.cs b/src/LibHac/FsSrv/Impl/SaveDataTransferUtility.cs
index 91763492..6f56580c 100644
--- a/src/LibHac/FsSrv/Impl/SaveDataTransferUtility.cs
+++ b/src/LibHac/FsSrv/Impl/SaveDataTransferUtility.cs
@@ -329,7 +329,8 @@ public static partial class SaveDataTransferUtilityGlobalMethods
public static Result OpenSaveDataInternalStorageAccessor(this FileSystemServer fsSrv,
ref SharedRef outAccessor, SaveDataSpaceId spaceId, ulong saveDataId)
{
- throw new NotImplementedException();
+ outAccessor.Reset(new SaveDataInternalStorageAccessor(spaceId, saveDataId));
+ return Result.Success;
}
public static Result CommitConcatenatedSaveDataStorage(this FileSystemServer fsSrv, IFileSystem fileSystem,
diff --git a/src/LibHac/FsSrv/Sf/ISaveDataMover.cs b/src/LibHac/FsSrv/Sf/ISaveDataMover.cs
index 445c8084..8c5c7077 100644
--- a/src/LibHac/FsSrv/Sf/ISaveDataMover.cs
+++ b/src/LibHac/FsSrv/Sf/ISaveDataMover.cs
@@ -2,9 +2,15 @@
namespace LibHac.FsSrv.Sf;
+///
+/// Bulk moves save data from one save data space to another.
+///
+/// To use this class, call for each save data to be moved. After all save data
+/// have been registered, repeatedly call until it returns 0 for the remaining size.
+/// Based on nnSdk 18.3.0 (FS 18.0.0)
public interface ISaveDataMover : IDisposable
{
Result Register(ulong saveDataId);
- Result Process(out long remainingSize, long sizeToProcess);
+ Result Process(out long outRemainingSize, long sizeToProcess);
Result Cancel();
}
\ No newline at end of file