forked from MeloNX/MeloNX
* Implement NGC service * Use raw byte arrays instead of string for _wordSeparators * Silence IDE0230 for _wordSeparators * Try to silence warning about _rangeValuesCount not being read on SparseSet * Make AcType enum private * Add abstract methods and one TODO that I forgot * PR feedback * More PR feedback * More PR feedback
405 lines
12 KiB
C#
405 lines
12 KiB
C#
using Ryujinx.Horizon.Common;
|
|
using Ryujinx.Horizon.Sdk.Fs;
|
|
using System;
|
|
using System.IO;
|
|
using System.IO.Compression;
|
|
|
|
namespace Ryujinx.Horizon.Sdk.Ngc.Detail
|
|
{
|
|
class ContentsReader : IDisposable
|
|
{
|
|
private const string MountName = "NgWord";
|
|
private const string VersionFilePath = $"{MountName}:/version.dat";
|
|
private const ulong DataId = 0x100000000000823UL;
|
|
|
|
private enum AcType
|
|
{
|
|
AcNotB,
|
|
AcB1,
|
|
AcB2,
|
|
AcSimilarForm,
|
|
TableSimilarForm,
|
|
}
|
|
|
|
private readonly IFsClient _fsClient;
|
|
private readonly object _lock;
|
|
private bool _intialized;
|
|
private ulong _cacheSize;
|
|
|
|
public ContentsReader(IFsClient fsClient)
|
|
{
|
|
_lock = new();
|
|
_fsClient = fsClient;
|
|
}
|
|
|
|
private static void MakeMountPoint(out string path, AcType type, int regionIndex)
|
|
{
|
|
path = null;
|
|
|
|
switch (type)
|
|
{
|
|
case AcType.AcNotB:
|
|
if (regionIndex < 0)
|
|
{
|
|
path = $"{MountName}:/ac_common_not_b_nx";
|
|
}
|
|
else
|
|
{
|
|
path = $"{MountName}:/ac_{regionIndex}_not_b_nx";
|
|
}
|
|
break;
|
|
case AcType.AcB1:
|
|
if (regionIndex < 0)
|
|
{
|
|
path = $"{MountName}:/ac_common_b1_nx";
|
|
}
|
|
else
|
|
{
|
|
path = $"{MountName}:/ac_{regionIndex}_b1_nx";
|
|
}
|
|
break;
|
|
case AcType.AcB2:
|
|
if (regionIndex < 0)
|
|
{
|
|
path = $"{MountName}:/ac_common_b2_nx";
|
|
}
|
|
else
|
|
{
|
|
path = $"{MountName}:/ac_{regionIndex}_b2_nx";
|
|
}
|
|
break;
|
|
case AcType.AcSimilarForm:
|
|
path = $"{MountName}:/ac_similar_form_nx";
|
|
break;
|
|
case AcType.TableSimilarForm:
|
|
path = $"{MountName}:/table_similar_form_nx";
|
|
break;
|
|
}
|
|
}
|
|
|
|
public Result Initialize(ulong cacheSize)
|
|
{
|
|
lock (_lock)
|
|
{
|
|
if (_intialized)
|
|
{
|
|
return Result.Success;
|
|
}
|
|
|
|
Result result = _fsClient.QueryMountSystemDataCacheSize(out long dataCacheSize, DataId);
|
|
if (result.IsFailure)
|
|
{
|
|
return result;
|
|
}
|
|
|
|
if (cacheSize < (ulong)dataCacheSize)
|
|
{
|
|
return NgcResult.InvalidSize;
|
|
}
|
|
|
|
result = _fsClient.MountSystemData(MountName, DataId);
|
|
if (result.IsFailure)
|
|
{
|
|
// Official firmware would return the result here,
|
|
// we don't to support older firmware where the archive didn't exist yet.
|
|
return Result.Success;
|
|
}
|
|
|
|
_cacheSize = cacheSize;
|
|
_intialized = true;
|
|
|
|
return Result.Success;
|
|
}
|
|
}
|
|
|
|
public Result Reload()
|
|
{
|
|
lock (_lock)
|
|
{
|
|
if (!_intialized)
|
|
{
|
|
return Result.Success;
|
|
}
|
|
|
|
_fsClient.Unmount(MountName);
|
|
|
|
Result result = Result.Success;
|
|
|
|
try
|
|
{
|
|
result = _fsClient.QueryMountSystemDataCacheSize(out long cacheSize, DataId);
|
|
if (result.IsFailure)
|
|
{
|
|
return result;
|
|
}
|
|
|
|
if (_cacheSize < (ulong)cacheSize)
|
|
{
|
|
result = NgcResult.InvalidSize;
|
|
return NgcResult.InvalidSize;
|
|
}
|
|
|
|
result = _fsClient.MountSystemData(MountName, DataId);
|
|
if (result.IsFailure)
|
|
{
|
|
return result;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
if (result.IsFailure)
|
|
{
|
|
_intialized = false;
|
|
_cacheSize = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
return Result.Success;
|
|
}
|
|
|
|
private Result GetFileSize(out long size, string filePath)
|
|
{
|
|
size = 0;
|
|
|
|
lock (_lock)
|
|
{
|
|
Result result = _fsClient.OpenFile(out FileHandle handle, filePath, OpenMode.Read);
|
|
if (result.IsFailure)
|
|
{
|
|
return result;
|
|
}
|
|
|
|
try
|
|
{
|
|
result = _fsClient.GetFileSize(out size, handle);
|
|
if (result.IsFailure)
|
|
{
|
|
return result;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_fsClient.CloseFile(handle);
|
|
}
|
|
}
|
|
|
|
return Result.Success;
|
|
}
|
|
|
|
private Result GetFileContent(Span<byte> destination, string filePath)
|
|
{
|
|
lock (_lock)
|
|
{
|
|
Result result = _fsClient.OpenFile(out FileHandle handle, filePath, OpenMode.Read);
|
|
if (result.IsFailure)
|
|
{
|
|
return result;
|
|
}
|
|
|
|
try
|
|
{
|
|
result = _fsClient.ReadFile(handle, 0, destination);
|
|
if (result.IsFailure)
|
|
{
|
|
return result;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_fsClient.CloseFile(handle);
|
|
}
|
|
}
|
|
|
|
return Result.Success;
|
|
}
|
|
|
|
public Result GetVersionDataSize(out long size)
|
|
{
|
|
return GetFileSize(out size, VersionFilePath);
|
|
}
|
|
|
|
public Result GetVersionData(Span<byte> destination)
|
|
{
|
|
return GetFileContent(destination, VersionFilePath);
|
|
}
|
|
|
|
public Result ReadDictionaries(out AhoCorasick partialWordsTrie, out AhoCorasick completeWordsTrie, out AhoCorasick delimitedWordsTrie, int regionIndex)
|
|
{
|
|
completeWordsTrie = null;
|
|
delimitedWordsTrie = null;
|
|
|
|
MakeMountPoint(out string partialWordsTriePath, AcType.AcNotB, regionIndex);
|
|
MakeMountPoint(out string completeWordsTriePath, AcType.AcB1, regionIndex);
|
|
MakeMountPoint(out string delimitedWordsTriePath, AcType.AcB2, regionIndex);
|
|
|
|
Result result = ReadDictionary(out partialWordsTrie, partialWordsTriePath);
|
|
if (result.IsFailure)
|
|
{
|
|
return NgcResult.DataAccessError;
|
|
}
|
|
|
|
result = ReadDictionary(out completeWordsTrie, completeWordsTriePath);
|
|
if (result.IsFailure)
|
|
{
|
|
return NgcResult.DataAccessError;
|
|
}
|
|
|
|
return ReadDictionary(out delimitedWordsTrie, delimitedWordsTriePath);
|
|
}
|
|
|
|
public Result ReadSimilarFormDictionary(out AhoCorasick similarFormTrie)
|
|
{
|
|
MakeMountPoint(out string similarFormTriePath, AcType.AcSimilarForm, 0);
|
|
|
|
return ReadDictionary(out similarFormTrie, similarFormTriePath);
|
|
}
|
|
|
|
public Result ReadSimilarFormTable(out SimilarFormTable similarFormTable)
|
|
{
|
|
similarFormTable = null;
|
|
|
|
MakeMountPoint(out string similarFormTablePath, AcType.TableSimilarForm, 0);
|
|
|
|
Result result = ReadGZipCompressedArchive(out byte[] data, similarFormTablePath);
|
|
if (result.IsFailure)
|
|
{
|
|
return result;
|
|
}
|
|
|
|
BinaryReader reader = new(data);
|
|
SimilarFormTable table = new();
|
|
|
|
if (!table.Import(ref reader))
|
|
{
|
|
// Official firmware doesn't return an error here and just assumes the import was successful.
|
|
return NgcResult.DataAccessError;
|
|
}
|
|
|
|
similarFormTable = table;
|
|
|
|
return Result.Success;
|
|
}
|
|
|
|
public static Result ReadNotSeparatorDictionary(out AhoCorasick notSeparatorTrie)
|
|
{
|
|
notSeparatorTrie = null;
|
|
|
|
BinaryReader reader = new(EmbeddedTries.NotSeparatorTrie);
|
|
AhoCorasick ac = new();
|
|
|
|
if (!ac.Import(ref reader))
|
|
{
|
|
// Official firmware doesn't return an error here and just assumes the import was successful.
|
|
return NgcResult.DataAccessError;
|
|
}
|
|
|
|
notSeparatorTrie = ac;
|
|
|
|
return Result.Success;
|
|
}
|
|
|
|
private Result ReadDictionary(out AhoCorasick trie, string path)
|
|
{
|
|
trie = null;
|
|
|
|
Result result = ReadGZipCompressedArchive(out byte[] data, path);
|
|
if (result.IsFailure)
|
|
{
|
|
return result;
|
|
}
|
|
|
|
BinaryReader reader = new(data);
|
|
AhoCorasick ac = new();
|
|
|
|
if (!ac.Import(ref reader))
|
|
{
|
|
// Official firmware doesn't return an error here and just assumes the import was successful.
|
|
return NgcResult.DataAccessError;
|
|
}
|
|
|
|
trie = ac;
|
|
|
|
return Result.Success;
|
|
}
|
|
|
|
private Result ReadGZipCompressedArchive(out byte[] data, string filePath)
|
|
{
|
|
data = null;
|
|
|
|
Result result = _fsClient.OpenFile(out FileHandle handle, filePath, OpenMode.Read);
|
|
if (result.IsFailure)
|
|
{
|
|
return result;
|
|
}
|
|
|
|
try
|
|
{
|
|
result = _fsClient.GetFileSize(out long fileSize, handle);
|
|
if (result.IsFailure)
|
|
{
|
|
return result;
|
|
}
|
|
|
|
data = new byte[fileSize];
|
|
|
|
result = _fsClient.ReadFile(handle, 0, data.AsSpan());
|
|
if (result.IsFailure)
|
|
{
|
|
return result;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_fsClient.CloseFile(handle);
|
|
}
|
|
|
|
try
|
|
{
|
|
data = DecompressGZipCompressedStream(data);
|
|
}
|
|
catch (InvalidDataException)
|
|
{
|
|
// Official firmware returns a different error, but it is translated to this error on the caller.
|
|
return NgcResult.DataAccessError;
|
|
}
|
|
|
|
return Result.Success;
|
|
}
|
|
|
|
private static byte[] DecompressGZipCompressedStream(byte[] data)
|
|
{
|
|
using MemoryStream input = new(data);
|
|
using GZipStream gZipStream = new(input, CompressionMode.Decompress);
|
|
using MemoryStream output = new();
|
|
|
|
gZipStream.CopyTo(output);
|
|
|
|
return output.ToArray();
|
|
}
|
|
|
|
protected virtual void Dispose(bool disposing)
|
|
{
|
|
if (disposing)
|
|
{
|
|
lock (_lock)
|
|
{
|
|
if (!_intialized)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_fsClient.Unmount(MountName);
|
|
_intialized = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
Dispose(disposing: true);
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
}
|
|
}
|