diff --git a/build/CodeGen/results.csv b/build/CodeGen/results.csv
index e390c181..bba44f13 100644
--- a/build/CodeGen/results.csv
+++ b/build/CodeGen/results.csv
@@ -1064,6 +1064,9 @@ Module,DescriptionStart,DescriptionEnd,Flags,Namespace,Name,Summary
2,6386,,,,UnsupportedSetSizeForZeroBitmapHashStorageFile,
2,6387,,,,UnsupportedWriteForCompressedStorage,
2,6388,,,,UnsupportedOperateRangeForCompressedStorage,
+2,6395,,,,UnsupportedRollbackOnlyModifiedForApplicationTemporaryFileSystem,
+2,6396,,,,UnsupportedRollbackOnlyModifiedForDirectorySaveDataFileSystem,
+2,6397,,,,UnsupportedOperateRangeForRegionSwitchStorage,
2,6400,6449,,,PermissionDenied,
2,6403,,,,PermissionDeniedForCreateHostFileSystem,Returned when opening a host FS on a retail device.
diff --git a/src/LibHac/Fs/ResultFs.cs b/src/LibHac/Fs/ResultFs.cs
index 50943f44..047d884b 100644
--- a/src/LibHac/Fs/ResultFs.cs
+++ b/src/LibHac/Fs/ResultFs.cs
@@ -1952,6 +1952,12 @@ public static class ResultFs
public static Result.Base UnsupportedWriteForCompressedStorage => new Result.Base(ModuleFs, 6387);
/// Error code: 2002-6388; Inner value: 0x31e802
public static Result.Base UnsupportedOperateRangeForCompressedStorage => new Result.Base(ModuleFs, 6388);
+ /// Error code: 2002-6395; Inner value: 0x31f602
+ public static Result.Base UnsupportedRollbackOnlyModifiedForApplicationTemporaryFileSystem => new Result.Base(ModuleFs, 6395);
+ /// Error code: 2002-6396; Inner value: 0x31f802
+ public static Result.Base UnsupportedRollbackOnlyModifiedForDirectorySaveDataFileSystem => new Result.Base(ModuleFs, 6396);
+ /// Error code: 2002-6397; Inner value: 0x31fa02
+ public static Result.Base UnsupportedOperateRangeForRegionSwitchStorage => new Result.Base(ModuleFs, 6397);
/// Error code: 2002-6400; Range: 6400-6449; Inner value: 0x320002
public static Result.Base PermissionDenied { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new Result.Base(ModuleFs, 6400, 6449); }
diff --git a/src/LibHac/FsSystem/SwitchStorage.cs b/src/LibHac/FsSystem/SwitchStorage.cs
new file mode 100644
index 00000000..342ee012
--- /dev/null
+++ b/src/LibHac/FsSystem/SwitchStorage.cs
@@ -0,0 +1,321 @@
+using System;
+using System.Runtime.CompilerServices;
+using LibHac.Common;
+using LibHac.Fs;
+
+namespace LibHac.FsSystem;
+
+///
+/// An that will switch between forwarding requests to one of two different base
+/// s. On each request the provided storage selection function will be called and the request
+/// will be forwarded to the appropriate based on the return value.
+///
+/// Based on FS 14.1.0 (nnSdk 14.3.0)
+public class SwitchStorage : IStorage
+{
+ private SharedRef _trueStorage;
+ private SharedRef _falseStorage;
+ private Func _storageSelectionFunction;
+
+ public SwitchStorage(in SharedRef trueStorage, in SharedRef falseStorage,
+ Func storageSelectionFunction)
+ {
+ _trueStorage = SharedRef.CreateCopy(in trueStorage);
+ _falseStorage = SharedRef.CreateCopy(in falseStorage);
+ _storageSelectionFunction = storageSelectionFunction;
+ }
+
+ public override void Dispose()
+ {
+ _trueStorage.Destroy();
+ _falseStorage.Destroy();
+
+ base.Dispose();
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private IStorage SelectStorage()
+ {
+ return (_storageSelectionFunction() ? _trueStorage : _falseStorage).Get;
+ }
+
+ public override Result Read(long offset, Span destination)
+ {
+ Result rc = SelectStorage().Read(offset, destination);
+ if (rc.IsFailure()) return rc.Miss();
+
+ return Result.Success;
+ }
+
+ public override Result Write(long offset, ReadOnlySpan source)
+ {
+ Result rc = SelectStorage().Write(offset, source);
+ if (rc.IsFailure()) return rc.Miss();
+
+ return Result.Success;
+ }
+
+ public override Result Flush()
+ {
+ Result rc = SelectStorage().Flush();
+ if (rc.IsFailure()) return rc.Miss();
+
+ return Result.Success;
+ }
+
+ public override Result GetSize(out long size)
+ {
+ Result rc = SelectStorage().GetSize(out size);
+ if (rc.IsFailure()) return rc.Miss();
+
+ return Result.Success;
+ }
+
+ public override Result SetSize(long size)
+ {
+ Result rc = SelectStorage().SetSize(size);
+ if (rc.IsFailure()) return rc.Miss();
+
+ return Result.Success;
+ }
+
+ public override Result OperateRange(Span outBuffer, OperationId operationId, long offset, long size,
+ ReadOnlySpan inBuffer)
+ {
+ switch (operationId)
+ {
+ case OperationId.InvalidateCache:
+ {
+ Result rc = _trueStorage.Get.OperateRange(outBuffer, operationId, offset, size, inBuffer);
+ if (rc.IsFailure()) return rc.Miss();
+
+ rc = _falseStorage.Get.OperateRange(outBuffer, operationId, offset, size, inBuffer);
+ if (rc.IsFailure()) return rc.Miss();
+
+ return Result.Success;
+ }
+ case OperationId.QueryRange:
+ {
+ Result rc = SelectStorage().OperateRange(outBuffer, operationId, offset, size, inBuffer);
+ if (rc.IsFailure()) return rc.Miss();
+
+ return Result.Success;
+ }
+ default:
+ return ResultFs.UnsupportedOperateRangeForSwitchStorage.Log();
+ }
+ }
+}
+
+///
+/// Takes a and two base s upon construction. Requests inside
+/// the provided will be forwarded to one , and requests outside
+/// will be forwarded to the other.
+///
+/// Based on FS 14.1.0 (nnSdk 14.3.0)
+public class RegionSwitchStorage : IStorage
+{
+ public struct Region
+ {
+ public long Offset;
+ public long Size;
+ }
+
+ private SharedRef _insideRegionStorage;
+ private SharedRef _outsideRegionStorage;
+ private Region _region;
+
+ public RegionSwitchStorage(in SharedRef insideRegionStorage,
+ in SharedRef outsideRegionStorage, Region region)
+ {
+ _insideRegionStorage = SharedRef.CreateCopy(in insideRegionStorage);
+ _outsideRegionStorage = SharedRef.CreateCopy(in outsideRegionStorage);
+ _region = region;
+ }
+
+ public override void Dispose()
+ {
+ _insideRegionStorage.Destroy();
+ _outsideRegionStorage.Destroy();
+
+ base.Dispose();
+ }
+
+ ///
+ /// Checks if the requested range is inside or outside the .
+ ///
+ /// The size past the start of the range until entering or exiting the .
+ /// The offset of the range to check.
+ /// The size of the range to check.
+ /// the start of the range is inside the ;
+ /// otherwise .
+ private bool CheckRegions(out long currentSize, long offset, long size)
+ {
+ if (_region.Offset > offset)
+ {
+ // The requested start offset is before the region's start offset.
+ // Check if the requested end offset is inside the region.
+ if (offset + size < _region.Offset)
+ {
+ // The request is completely outside the region.
+ currentSize = size;
+ }
+ else
+ {
+ // The request ends inside the region. Calculate the length of the request outside the region.
+ currentSize = _region.Offset - offset;
+ }
+
+ return false;
+ }
+
+ if (_region.Offset + _region.Size > offset)
+ {
+ // The requested start offset is inside the region.
+ // Check if the requested end offset is also inside the region.
+ if (offset + size < _region.Offset + _region.Size)
+ {
+ // The request is completely within the region.
+ currentSize = size;
+ }
+ else
+ {
+ // The request ends outside the region. Calculate the length of the request inside the region.
+ currentSize = _region.Offset + _region.Size - offset;
+ }
+
+ return true;
+ }
+
+ // The request starts after the end of the region.
+ currentSize = size;
+ return false;
+ }
+
+ public override Result Read(long offset, Span destination)
+ {
+ int bytesRead = 0;
+ while (bytesRead < destination.Length)
+ {
+ if (CheckRegions(out long currentSize, offset + bytesRead, destination.Length - bytesRead))
+ {
+ Result rc = _insideRegionStorage.Get.Read(offset + bytesRead,
+ destination.Slice(bytesRead, (int)currentSize));
+ if (rc.IsFailure()) return rc.Miss();
+ }
+ else
+ {
+ Result rc = _outsideRegionStorage.Get.Read(offset + bytesRead,
+ destination.Slice(bytesRead, (int)currentSize));
+ if (rc.IsFailure()) return rc.Miss();
+ }
+
+ bytesRead += (int)currentSize;
+ }
+
+ return Result.Success;
+ }
+
+ public override Result Write(long offset, ReadOnlySpan source)
+ {
+ int bytesWritten = 0;
+ while (bytesWritten < source.Length)
+ {
+ if (CheckRegions(out long currentSize, offset + bytesWritten, source.Length - bytesWritten))
+ {
+ Result rc = _insideRegionStorage.Get.Write(offset + bytesWritten,
+ source.Slice(bytesWritten, (int)currentSize));
+ if (rc.IsFailure()) return rc.Miss();
+ }
+ else
+ {
+ Result rc = _outsideRegionStorage.Get.Write(offset + bytesWritten,
+ source.Slice(bytesWritten, (int)currentSize));
+ if (rc.IsFailure()) return rc.Miss();
+ }
+
+ bytesWritten += (int)currentSize;
+ }
+
+ return Result.Success;
+ }
+
+ public override Result Flush()
+ {
+ Result rc = _insideRegionStorage.Get.Flush();
+ if (rc.IsFailure()) return rc.Miss();
+
+ rc = _outsideRegionStorage.Get.Flush();
+ if (rc.IsFailure()) return rc.Miss();
+
+ return Result.Success;
+ }
+
+ public override Result GetSize(out long size)
+ {
+ Result rc = _insideRegionStorage.Get.GetSize(out size);
+ if (rc.IsFailure()) return rc.Miss();
+
+ return Result.Success;
+ }
+
+ public override Result SetSize(long size)
+ {
+ Result rc = _insideRegionStorage.Get.SetSize(size);
+ if (rc.IsFailure()) return rc.Miss();
+
+ rc = _outsideRegionStorage.Get.SetSize(size);
+ if (rc.IsFailure()) return rc.Miss();
+
+ return Result.Success;
+ }
+
+ public override Result OperateRange(Span outBuffer, OperationId operationId, long offset, long size, ReadOnlySpan inBuffer)
+ {
+ switch (operationId)
+ {
+ case OperationId.InvalidateCache:
+ {
+ Result rc = _insideRegionStorage.Get.OperateRange(outBuffer, operationId, offset, size, inBuffer);
+ if (rc.IsFailure()) return rc.Miss();
+
+ rc = _outsideRegionStorage.Get.OperateRange(outBuffer, operationId, offset, size, inBuffer);
+ if (rc.IsFailure()) return rc.Miss();
+
+ return Result.Success;
+ }
+ case OperationId.QueryRange:
+ {
+ Unsafe.SkipInit(out QueryRangeInfo mergedInfo);
+ mergedInfo.Clear();
+
+ long bytesProcessed = 0;
+ while (bytesProcessed < size)
+ {
+ Unsafe.SkipInit(out QueryRangeInfo currentInfo);
+
+ if (CheckRegions(out long currentSize, offset + bytesProcessed, size - bytesProcessed))
+ {
+ Result rc = _insideRegionStorage.Get.OperateRange(SpanHelpers.AsByteSpan(ref currentInfo),
+ operationId, offset + bytesProcessed, currentSize, inBuffer);
+ if (rc.IsFailure()) return rc.Miss();
+ }
+ else
+ {
+ Result rc = _outsideRegionStorage.Get.OperateRange(SpanHelpers.AsByteSpan(ref currentInfo),
+ operationId, offset + bytesProcessed, currentSize, inBuffer);
+ if (rc.IsFailure()) return rc.Miss();
+ }
+
+ mergedInfo.Merge(in currentInfo);
+ bytesProcessed += currentSize;
+ }
+
+ SpanHelpers.AsByteSpan(ref mergedInfo).CopyTo(outBuffer);
+ return Result.Success;
+ }
+ default:
+ return ResultFs.UnsupportedOperateRangeForRegionSwitchStorage.Log();
+ }
+ }
+}
\ No newline at end of file