Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support reading the installed game off the SD card directly #1

Merged
merged 72 commits into from
Jan 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
645900e
Attempt at adding SD card reading
LordBubblesDev Dec 25, 2024
c156c04
I should probably give up
LordBubblesDev Dec 29, 2024
2e09419
I can't believe this worked
LordBubblesDev Dec 29, 2024
85eb01f
Small cleanup
LordBubblesDev Dec 29, 2024
0d1403b
one last cleanup before merge
LordBubblesDev Dec 29, 2024
41f1c81
Merge branch 'master' into partitioned-test
LordBubblesDev Dec 29, 2024
66ba933
oops
LordBubblesDev Dec 29, 2024
62b687b
Add SD card root path to DevTool settings
LordBubblesDev Dec 30, 2024
68c9130
Merge branch 'master' into partitioned-test
LordBubblesDev Dec 30, 2024
0acdf7b
Directly pass keys as arguments to SdCardTkRom
LordBubblesDev Dec 30, 2024
22062d4
Merge branch 'master' into partitioned-test
LordBubblesDev Dec 31, 2024
b18c3fd
Arch disappeared so I got bored and did this
LordBubblesDev Dec 31, 2024
1f9770c
Support reading split files and auto-detect XCI/NSP format
LordBubblesDev Dec 31, 2024
c744c48
Rework the logic and clean up the code
LordBubblesDev Jan 1, 2025
2bc9a1b
Clean up and optimize
LordBubblesDev Jan 1, 2025
3ba1f51
Update DevTools to use TkRomHelper
LordBubblesDev Jan 2, 2025
045a6ab
Forgot this now all should be done
LordBubblesDev Jan 2, 2025
eb21d9b
Avoid initializing the same file system twice
LordBubblesDev Jan 2, 2025
a81e048
Merge branch 'master' into partitioned-test
LordBubblesDev Jan 2, 2025
cbe4ab1
Simplify XCI header thingy
LordBubblesDev Jan 4, 2025
2777078
misc adjustments
LordBubblesDev Jan 4, 2025
db529ad
Rename TkRomHelper to LibHacRomProvider
LordBubblesDev Jan 4, 2025
2bea384
Construct KeySet before call
LordBubblesDev Jan 4, 2025
f77c24f
Do not return null on invalid configuration
LordBubblesDev Jan 4, 2025
2109406
Actually don't return null at all
LordBubblesDev Jan 4, 2025
ba2b997
Properly dispose of stuff (I hope?)
LordBubblesDev Jan 4, 2025
66449c8
Fix what I broke
LordBubblesDev Jan 4, 2025
e34abb3
Clean up implementation
LordBubblesDev Jan 4, 2025
f7929ff
Excuse my redundancy (x2)
LordBubblesDev Jan 4, 2025
2b16cdf
This is probably better
LordBubblesDev Jan 6, 2025
90f1727
This is probably better (x2)
LordBubblesDev Jan 6, 2025
fbd82bc
Finally, I think? :D
LordBubblesDev Jan 6, 2025
3376542
Remove debug output and fix DebugRomProvider
LordBubblesDev Jan 7, 2025
fdac42e
Merge branch 'master' into partitioned-test
LordBubblesDev Jan 7, 2025
24028cf
Disposing DebugRomProvider
LordBubblesDev Jan 7, 2025
6da27dc
Revert unimportant fix :D
LordBubblesDev Jan 7, 2025
67f28aa
Misc code cleanup
ArchLeaders Jan 7, 2025
15d0796
misc fixes
LordBubblesDev Jan 7, 2025
869f4b6
Attempt at adding SD card reading
LordBubblesDev Dec 25, 2024
ee41f74
I should probably give up
LordBubblesDev Dec 29, 2024
5db2d52
I can't believe this worked
LordBubblesDev Dec 29, 2024
440d564
Small cleanup
LordBubblesDev Dec 29, 2024
2012f5c
one last cleanup before merge
LordBubblesDev Dec 29, 2024
19cd1fa
Add SD card root path to DevTool settings
LordBubblesDev Dec 30, 2024
7771636
Directly pass keys as arguments to SdCardTkRom
LordBubblesDev Dec 30, 2024
30d896e
Arch disappeared so I got bored and did this
LordBubblesDev Dec 31, 2024
bfc152d
Support reading split files and auto-detect XCI/NSP format
LordBubblesDev Dec 31, 2024
de24ef0
Rework the logic and clean up the code
LordBubblesDev Jan 1, 2025
2240c46
Clean up and optimize
LordBubblesDev Jan 1, 2025
06fa96d
Update DevTools to use TkRomHelper
LordBubblesDev Jan 2, 2025
f8d5841
Forgot this now all should be done
LordBubblesDev Jan 2, 2025
192674e
Avoid initializing the same file system twice
LordBubblesDev Jan 2, 2025
472e31f
Simplify XCI header thingy
LordBubblesDev Jan 4, 2025
f16d6b2
misc adjustments
LordBubblesDev Jan 4, 2025
8d6936c
Rename TkRomHelper to LibHacRomProvider
LordBubblesDev Jan 4, 2025
dbae691
Construct KeySet before call
LordBubblesDev Jan 4, 2025
6137044
Do not return null on invalid configuration
LordBubblesDev Jan 4, 2025
66851d1
Actually don't return null at all
LordBubblesDev Jan 4, 2025
d6fe192
Properly dispose of stuff (I hope?)
LordBubblesDev Jan 4, 2025
6114a2b
Fix what I broke
LordBubblesDev Jan 4, 2025
cef42c8
Clean up implementation
LordBubblesDev Jan 4, 2025
b324709
Excuse my redundancy (x2)
LordBubblesDev Jan 4, 2025
0923f6a
This is probably better
LordBubblesDev Jan 6, 2025
718e7ea
This is probably better (x2)
LordBubblesDev Jan 6, 2025
2ab2318
Finally, I think? :D
LordBubblesDev Jan 6, 2025
49fa335
Remove debug output and fix DebugRomProvider
LordBubblesDev Jan 7, 2025
08b4e4a
Disposing DebugRomProvider
LordBubblesDev Jan 7, 2025
ca94019
Revert unimportant fix :D
LordBubblesDev Jan 7, 2025
21e98b5
Misc code cleanup
ArchLeaders Jan 7, 2025
e92a705
Merge branch 'master' into partitioned-test
LordBubblesDev Jan 7, 2025
b14441e
Merge branch 'master' into partitioned-test
LordBubblesDev Jan 7, 2025
5ae14bb
Merge remote-tracking branch 'origin/partitioned-test' into partition…
ArchLeaders Jan 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ public static class FileSystemExtensions
{
public static SwitchFs GetSwitchFs(this IStorage storage, string filePath, KeySet keys)
{
if (storage is ConcatenationStorage) {
return IsXci(storage) ? OpenXci(keys, storage) : OpenNsp(keys, storage);
}

ReadOnlySpan<char> extension = Path.GetExtension(filePath.AsSpan());

return extension switch {
Expand All @@ -25,7 +29,14 @@ public static SwitchFs GetSwitchFs(this IStorage storage, string filePath, KeySe
_ => throw new ArgumentException($"Unsupported file extension: '{extension}'", nameof(filePath)),
};
}


private static bool IsXci(IStorage storage)
{
Span<byte> buffer = stackalloc byte[4];
storage.Read(0x100, buffer).ThrowIfFailure();
return buffer.SequenceEqual("HEAD"u8);
}

private static SwitchFs OpenNsp(KeySet keys, IStorage storage)
{
SharedRef<IStorage> storageShared = new(storage);
Expand Down
25 changes: 25 additions & 0 deletions Extensions/TkSharp.Extensions.LibHac/Helpers/FileRomHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using LibHac.Common.Keys;
using LibHac.Fs;
using LibHac.FsSystem;
using LibHac.Tools.Fs;
using TkSharp.Extensions.LibHac.Extensions;

namespace TkSharp.Extensions.LibHac.Helpers
{
public class FileRomHelper : ILibHacRomHelper
{
private IStorage? _storage;

public SwitchFs Initialize(string filePath, KeySet keys)
{
_storage = new LocalStorage(filePath, FileAccess.Read);
return _storage.GetSwitchFs(filePath, keys);
}

public void Dispose()
{
_storage?.Dispose();
GC.SuppressFinalize(this);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using LibHac.Common.Keys;
using LibHac.Tools.Fs;

namespace TkSharp.Extensions.LibHac.Helpers;

public interface ILibHacRomHelper : IDisposable
{
SwitchFs Initialize(string path, KeySet keys);
}
37 changes: 37 additions & 0 deletions Extensions/TkSharp.Extensions.LibHac/Helpers/SdRomHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using LibHac.Common;
using LibHac.Common.Keys;
using LibHac.Fs;
using LibHac.Fs.Fsa;
using LibHac.FsSystem;
using LibHac.Tools.Fs;
using LibHac.Tools.FsSystem;

namespace TkSharp.Extensions.LibHac.Helpers;

public class SdRomHelper : ILibHacRomHelper
{
private UniqueRef<IAttributeFileSystem> _localFsRef;

public SwitchFs Initialize(string sdCardPath, KeySet keys)
{
LocalFileSystem.Create(out LocalFileSystem? localFs, sdCardPath).ThrowIfFailure();
_localFsRef = new UniqueRef<IAttributeFileSystem>(localFs);

var concatFs = new ConcatenationFileSystem(ref _localFsRef);

using var contentDirPath = new global::LibHac.Fs.Path();
PathFunctions.SetUpFixedPath(ref contentDirPath.Ref(), "/Nintendo/Contents"u8).ThrowIfFailure();

var contentDirFs = new SubdirectoryFileSystem(concatFs);
contentDirFs.Initialize(in contentDirPath).ThrowIfFailure();

var encFs = new AesXtsFileSystem(contentDirFs, keys.SdCardEncryptionKeys[1].DataRo.ToArray(), 0x4000);
return new SwitchFs(keys, encFs, null);
}

public void Dispose()
{
_localFsRef.Destroy();
GC.SuppressFinalize(this);
}
}
30 changes: 30 additions & 0 deletions Extensions/TkSharp.Extensions.LibHac/Helpers/SplitRomHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using LibHac.Common.Keys;
using LibHac.Fs;
using LibHac.FsSystem;
using LibHac.Tools.FsSystem;
using LibHac.Tools.Fs;
using TkSharp.Extensions.LibHac.Extensions;

namespace TkSharp.Extensions.LibHac.Helpers
{
public class SplitRomHelper : ILibHacRomHelper
{
private ConcatenationStorage? _storage;

public SwitchFs Initialize(string splitDirectory, KeySet keys)
{
IList<IStorage> splitFiles = [.. Directory.EnumerateFiles(splitDirectory)
.OrderBy(f => f)
.Select(f => new LocalStorage(f, FileAccess.Read))];

_storage = new ConcatenationStorage(splitFiles, true);
return _storage.GetSwitchFs("rom", keys);
}

public void Dispose()
{
_storage?.Dispose();
GC.SuppressFinalize(this);
}
}
}
71 changes: 71 additions & 0 deletions Extensions/TkSharp.Extensions.LibHac/LibHacRomProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using LibHac.Common.Keys;
using LibHac.Fs.Fsa;
using LibHac.Tools.Fs;
using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils;
using TkSharp.Core;
using TkSharp.Extensions.LibHac.Helpers;

namespace TkSharp.Extensions.LibHac;

public class LibHacRomProvider : IDisposable
{
public const ulong EX_KING_APP_ID = 0x0100F2C0115B6000;

private SwitchFs? _baseFs;
private SwitchFs? _updateFs;
private IFileSystem? _fileSystem;
private ILibHacRomHelper? _helper;

public TkRom CreateRom(TkChecksums checksums, KeySet keys, LibHacRomSourceType baseSourceType, string basePath, LibHacRomSourceType updateSourceType, string updatePath)
{
if (baseSourceType is LibHacRomSourceType.SdCard && updateSourceType is LibHacRomSourceType.SdCard && basePath == updatePath) {
_helper = new SdRomHelper();
_baseFs = _helper.Initialize(basePath, keys);
_fileSystem = InitializeLayeredFs(_baseFs, _baseFs);
}
else {
_baseFs = CreateSwitchFs(baseSourceType, basePath, keys);
_updateFs = CreateSwitchFs(updateSourceType, updatePath, keys);
_fileSystem = InitializeLayeredFs(_baseFs, _updateFs);
}

return new TkRom(checksums, _fileSystem);
}

private SwitchFs CreateSwitchFs(LibHacRomSourceType sourceType, string path, KeySet keys)
{
_helper = sourceType switch {
LibHacRomSourceType.File => new FileRomHelper(),
LibHacRomSourceType.SdCard => new SdRomHelper(),
LibHacRomSourceType.SplitFiles => new SplitRomHelper(),
_ => throw new ArgumentException($"Invalid source: {sourceType}")
};

return _helper.Initialize(path, keys);
}

private static IFileSystem InitializeLayeredFs(SwitchFs baseFs, SwitchFs updateFs)
{
return baseFs.Applications[EX_KING_APP_ID].Main.MainNca.Nca
.OpenFileSystemWithPatch(updateFs.Applications[EX_KING_APP_ID].Patch.MainNca.Nca,
NcaSectionType.Data, IntegrityCheckLevel.ErrorOnInvalid);
}

public void Dispose()
{
_fileSystem?.Dispose();
_baseFs?.Dispose();
_updateFs?.Dispose();
_helper?.Dispose();

GC.SuppressFinalize(this);
}
}

public enum LibHacRomSourceType
{
File, // XCI or NSP file
SdCard, // From SD card
SplitFiles // Split files in a directory
}
Original file line number Diff line number Diff line change
@@ -1,26 +1,15 @@
using LibHac.Common;
using LibHac.Common.Keys;
using LibHac.Fs;
using LibHac.Fs.Fsa;
using LibHac.FsSystem;
using LibHac.Tools.Fs;
using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils;
using TkSharp.Core;
using TkSharp.Core.IO.Buffers;
using TkSharp.Core.IO.Parsers;
using TkSharp.Extensions.LibHac.Extensions;
using Path = System.IO.Path;

namespace TkSharp.Extensions.LibHac;

public sealed class PackedTkRom : ITkRom, IDisposable
public class TkRom : ITkRom, IDisposable
{
public const ulong EX_KING_APP_ID = 0x0100F2C0115B6000;

private readonly IStorage _baseStorage, _updateStorage;
private readonly SwitchFs _baseSwitchFs, _updateSwitchFs;

private readonly TkChecksums _checksums;
private readonly IFileSystem _fileSystem;

Expand All @@ -36,46 +25,33 @@ public sealed class PackedTkRom : ITkRom, IDisposable

public Dictionary<string, string>.AlternateLookup<ReadOnlySpan<char>> EffectVersions { get; }

public PackedTkRom(TkChecksums checksums, KeySet keys, string baseGameFilePath, string gameUpdateFilePath)
public TkRom(TkChecksums checksums, IFileSystem fileSystem)
{
_checksums = checksums;
_fileSystem = fileSystem;

_baseStorage = new LocalStorage(baseGameFilePath, FileAccess.Read);
_baseSwitchFs = _baseStorage.GetSwitchFs(baseGameFilePath, keys);

_updateStorage = new LocalStorage(gameUpdateFilePath, FileAccess.Read);
_updateSwitchFs = _updateStorage.GetSwitchFs(gameUpdateFilePath, keys);

_fileSystem = _baseSwitchFs.Applications[EX_KING_APP_ID].Main.MainNca.Nca
.OpenFileSystemWithPatch(_updateSwitchFs.Applications[EX_KING_APP_ID].Patch.MainNca.Nca, NcaSectionType.Data, IntegrityCheckLevel.ErrorOnInvalid);

{
using Stream regionLangMaskFs = _fileSystem.OpenFileStream("/System/RegionLangMask.txt");
using RentedBuffer<byte> regionLangMask = RentedBuffer<byte>.Allocate(regionLangMaskFs);
using (Stream regionLangMaskFs = _fileSystem.OpenFileStream("/System/RegionLangMask.txt"))
using (RentedBuffer<byte> regionLangMask = RentedBuffer<byte>.Allocate(regionLangMaskFs)) {
GameVersion = RegionLangMaskParser.ParseVersion(regionLangMask.Span, out string nsoBinaryId);
NsoBinaryId = nsoBinaryId;
}

{
using Stream zsDicFs = _fileSystem.OpenFileStream("/Pack/ZsDic.pack.zs");
using (Stream zsDicFs = _fileSystem.OpenFileStream("/Pack/ZsDic.pack.zs")) {
Zstd = new TkZstd(zsDicFs);
}

{
using Stream addressTableFs = _fileSystem.OpenFileStream($"/System/AddressTable/Product.{GameVersion}.Nin_NX_NVN.atbl.byml.zs");
using RentedBuffer<byte> addressTableBuffer = RentedBuffer<byte>.Allocate(addressTableFs);
using (Stream addressTableFs = _fileSystem.OpenFileStream($"/System/AddressTable/Product.{GameVersion}.Nin_NX_NVN.atbl.byml.zs"))
using (RentedBuffer<byte> addressTableBuffer = RentedBuffer<byte>.Allocate(addressTableFs)) {
AddressTable = AddressTableParser.ParseAddressTable(addressTableBuffer.Span, Zstd);
}

{
using Stream eventFlowFileEntryFs = _fileSystem.OpenFileStream($"/{AddressTable["Event/EventFlow/EventFlowFileEntry.Product.byml"]}.zs");
using RentedBuffer<byte> eventFlowFileEntryBuffer = RentedBuffer<byte>.Allocate(eventFlowFileEntryFs);
using (Stream eventFlowFileEntryFs = _fileSystem.OpenFileStream($"/{AddressTable["Event/EventFlow/EventFlowFileEntry.Product.byml"]}.zs"))
using (RentedBuffer<byte> eventFlowFileEntryBuffer = RentedBuffer<byte>.Allocate(eventFlowFileEntryFs)) {
EventFlowVersions = EventFlowFileEntryParser.ParseFileEntry(eventFlowFileEntryBuffer.Span, Zstd);
}

{
using Stream effectInfoFs = _fileSystem.OpenFileStream($"/{AddressTable["Effect/EffectFileInfo.Product.Nin_NX_NVN.byml"]}.zs");
using RentedBuffer<byte> effectInfoBuffer = RentedBuffer<byte>.Allocate(effectInfoFs);
using (Stream effectInfoFs = _fileSystem.OpenFileStream($"/{AddressTable["Effect/EffectFileInfo.Product.Nin_NX_NVN.byml"]}.zs"))
using (RentedBuffer<byte> effectInfoBuffer = RentedBuffer<byte>.Allocate(effectInfoFs)) {
EffectVersions = EffectInfoParser.ParseFileEntry(effectInfoBuffer.Span, Zstd);
}
}
Expand All @@ -87,7 +63,8 @@ public RentedBuffer<byte> GetVanilla(string relativeFilePath)
UniqueRef<IFile> file = new();
_fileSystem.OpenFile(ref file, relativeFilePath.ToU8Span(), OpenMode.Read);

if (!file.HasValue) {
if (!file.HasValue)
{
return default;
}

Expand All @@ -97,16 +74,19 @@ public RentedBuffer<byte> GetVanilla(string relativeFilePath)
file.Get.Read(out _, offset: 0, raw);
file.Destroy();

if (!TkZstd.IsCompressed(raw)) {
if (!TkZstd.IsCompressed(raw))
{
return rawBuffer;
}

try {
try
{
RentedBuffer<byte> decompressed = RentedBuffer<byte>.Allocate(TkZstd.GetDecompressedSize(raw));
Zstd.Decompress(raw, decompressed.Span);
return decompressed;
}
finally {
finally
{
rawBuffer.Dispose();
}
}
Expand All @@ -118,10 +98,7 @@ public bool IsVanilla(ReadOnlySpan<char> canonical, Span<byte> src, int fileVers

public void Dispose()
{
_baseStorage.Dispose();
_updateStorage.Dispose();
_baseSwitchFs.Dispose();
_updateSwitchFs.Dispose();
_fileSystem.Dispose();
GC.SuppressFinalize(this);
}
}
}
30 changes: 24 additions & 6 deletions TkSharp.Debug/DebugRomProvider.cs
Original file line number Diff line number Diff line change
@@ -1,22 +1,40 @@
using LibHac.Common.Keys;
using LibHac.FsSystem;
using LibHac.Tools.FsSystem;
using TkSharp.Core;
using TkSharp.Core.Common;
using TkSharp.Core.IO;
using TkSharp.Data.Embedded;
using TkSharp.Extensions.LibHac;

namespace TkSharp.Debug;

public sealed class DebugRomProvider : Singleton<DebugRomProvider>, ITkRomProvider
public sealed class DebugRomProvider : Singleton<DebugRomProvider>, ITkRomProvider, IDisposable
{
private LibHacRomProvider? _romProvider;

public ITkRom GetRom()
{
return new ExtractedTkRom(@"F:\Games\RomFS\Totk\1.2.1",
TkChecksums.FromStream(TkEmbeddedDataSource.GetChecksumsBin())
);
// return new ExtractedTkRom(@"F:\Games\RomFS\Totk\1.2.1",
// TkChecksums.FromStream(TkEmbeddedDataSource.GetChecksumsBin())
// );

// return new PackedTkRom(
// TkChecksums.FromStream(TkEmbeddedDataSource.GetChecksumsBin()),
// @"C:\Users\ArchLeaders\AppData\Roaming\Ryujinx\system",
// @"D:\Games\Emulation\Packaged\Tears-of-the-Kingdom\TotK-1.0.0.xci",
// @"D:\Games\Emulation\Packaged\Tears-of-the-Kingdom\TotK-1.2.1.nsp");

_romProvider = new LibHacRomProvider();
return _romProvider.CreateRom(
TkChecksums.FromStream(TkEmbeddedDataSource.GetChecksumsBin()),
ExternalKeyReader.ReadKeyFile(@"F:\TOTK\SDCardTest\switch\prod.keys", @"F:\TOTK\SDCardTest\switch\title.keys"),
LibHacRomSourceType.SplitFiles, @"F:\TOTK\SDCardTest\TOTKSPLIT",
LibHacRomSourceType.SdCard, @"F:\TOTK\SDCardTest");
}

public void Dispose()
{
_romProvider?.Dispose();
_romProvider = null;
}
}
}
Loading