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 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 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); } } }