c2ed73e03f
Fix some memory leaks
296 lines
12 KiB
C#
296 lines
12 KiB
C#
using System.Security.Cryptography;
|
||
using LibHac.Common;
|
||
using LibHac.Fs;
|
||
using LibHac.Fs.Fsa;
|
||
using LibHac.FsSystem;
|
||
using LibHac.Tools.FsSystem;
|
||
using LibHac.Tools.FsSystem.NcaUtils;
|
||
using LibHac.Common.Keys;
|
||
using LibHac.Ncm;
|
||
using LibHac.Tools.Fs;
|
||
using LibHac.Tools.Ncm;
|
||
|
||
namespace TinfoilVibeServer.Services
|
||
{
|
||
public interface INSPExtractor
|
||
{
|
||
/// <summary>
|
||
/// Public convenience wrapper that opens the file on disk.
|
||
/// </summary>
|
||
NcaMetadataWithHash? ExtractFromFile(string filePath);
|
||
|
||
/// <summary>
|
||
/// Core implementation – works on any seekable stream that contains a full NSP/XCI container.
|
||
/// </summary>
|
||
NcaMetadataWithHash? ExtractFromStream(Stream stream);
|
||
|
||
string ExtractHashFromStream(Stream nspStream);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Extracts the TitleId, version, type *and* the SHA‑256 of the first NCA stream.
|
||
/// </summary>
|
||
public sealed class NSPExtractor : INSPExtractor
|
||
{
|
||
private readonly KeySet _keySet;
|
||
private readonly ILogger<INSPExtractor> _logger;
|
||
|
||
public NSPExtractor(KeySet keySet, ILogger<INSPExtractor> logger)
|
||
{
|
||
_keySet = keySet;
|
||
_logger = logger;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Public convenience wrapper that opens the file on disk.
|
||
/// </summary>
|
||
public NcaMetadataWithHash? ExtractFromFile(string filePath)
|
||
{
|
||
using var stream = File.OpenRead(filePath);
|
||
return ExtractFromStream(stream);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Core implementation – works on any seekable stream that contains a full NSP/XCI container.
|
||
/// </summary>
|
||
public NcaMetadataWithHash? ExtractFromStream(Stream stream)
|
||
{
|
||
if (!stream.CanSeek) return null;
|
||
stream.Seek(0, SeekOrigin.Begin);
|
||
|
||
_logger.LogInformation("Extracting NSP/XCI from stream (length={Length})", stream.Length);
|
||
using var storage = new StreamStorage(stream, false);
|
||
if (IsPfs0FileSystem(stream))
|
||
{
|
||
return ExtractNSPFromStream(storage);
|
||
}
|
||
|
||
if (IsXciFileSystem(stream))
|
||
{
|
||
var xci = new Xci(_keySet, storage);
|
||
List<DirectoryEntryEx> ncaEntries;
|
||
if (xci.HasPartition(XciPartitionType.Secure))
|
||
{
|
||
_logger.LogInformation("Processing as XCI");
|
||
var partition = xci.OpenPartition(XciPartitionType.Secure);
|
||
ncaEntries = partition
|
||
.EnumerateEntries("*.cnmt.nca", SearchOptions.RecurseSubdirectories)
|
||
.Where(e => e.Type == DirectoryEntryType.File)
|
||
.ToList();
|
||
byte[]? hash = null;
|
||
foreach (var dirEntry in ncaEntries)
|
||
{
|
||
using var fileRef = new UniqueRef<IFile>();
|
||
partition.OpenFile(ref fileRef.Ref, dirEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||
using var ncaFile = fileRef.Release();
|
||
using var ncaFileStorage = new FileStorage(ncaFile);
|
||
|
||
var nca = new Nca(_keySet, ncaFileStorage);
|
||
if (hash == null)
|
||
{
|
||
// Hash the *first* NCA stream – the stream we just opened
|
||
using var sha256 = SHA256.Create();
|
||
using var ncaStream = ncaFile.AsStream();
|
||
hash = sha256.ComputeHash(ncaStream);
|
||
}
|
||
|
||
if (nca.Header.ContentType != NcaContentType.Meta)
|
||
continue; // only the meta NCA contains title metadata
|
||
|
||
string titleId = nca.Header.TitleId.ToString("X16");
|
||
var (contentMetaType, applicationTitleId, titleVersion) = GetMetaData(nca);
|
||
|
||
_logger.LogInformation("Meta NCA found – TitleId={TitleId} Version={Version}", titleId, titleVersion);
|
||
// XCI can never be a patch?
|
||
return new NcaMetadataWithHash(titleId, applicationTitleId.ToString("X16"), titleVersion.Major, ContentMetaType.Application, BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant());
|
||
}
|
||
}
|
||
}
|
||
|
||
return null; // no meta NCA found
|
||
}
|
||
|
||
private NcaMetadataWithHash? ExtractNSPFromStream(StreamStorage storage)
|
||
{
|
||
List<DirectoryEntryEx> ncaEntries;
|
||
_logger.LogInformation("Processing as NSP");
|
||
var partition = new PartitionFileSystem();
|
||
partition.Initialize(storage).ThrowIfFailure();
|
||
// Find the first *.nca that contains the meta header
|
||
ncaEntries = partition
|
||
.EnumerateEntries("*.nca", SearchOptions.RecurseSubdirectories)
|
||
.Where(e => e.Type == DirectoryEntryType.File)
|
||
.ToList();
|
||
byte[]? hash = null;
|
||
foreach (var dirEntry in ncaEntries)
|
||
{
|
||
using var fileRef = new UniqueRef<IFile>();
|
||
partition.OpenFile(ref fileRef.Ref, dirEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||
using var ncaFile = fileRef.Release();
|
||
using var ncaFileStorage = new FileStorage(ncaFile);
|
||
|
||
var nca = new Nca(_keySet, ncaFileStorage);
|
||
if (hash == null)
|
||
{
|
||
// Hash the *first* NCA stream – the stream we just opened
|
||
using var sha256 = SHA256.Create();
|
||
using var ncaStream = ncaFile.AsStream();
|
||
hash = sha256.ComputeHash(ncaStream);
|
||
}
|
||
|
||
if (nca.Header.ContentType != NcaContentType.Meta)
|
||
continue; // only the meta NCA contains title metadata
|
||
|
||
_logger.LogInformation("Meta NCA found – TitleId={TitleId} Version={Version}", nca.Header.TitleId, nca.Header.Version);
|
||
string titleId = nca.Header.TitleId.ToString("X16");
|
||
|
||
var (contentMetaType,applicationTitle,titleVersion) = GetMetaData(nca);
|
||
if (contentMetaType != null)
|
||
return new NcaMetadataWithHash(titleId, applicationTitle.ToString("X16"), titleVersion.Minor, contentMetaType.Value, BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant());
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
private static (ContentMetaType?,ulong, TitleVersion) GetMetaData(Nca nca)
|
||
{
|
||
if (nca.Header.ContentType != NcaContentType.Meta) return (null,0, new TitleVersion(0, true));
|
||
using var openFileSystem = nca.OpenFileSystem(0, IntegrityCheckLevel.ErrorOnInvalid);
|
||
foreach (var entry in openFileSystem.EnumerateEntries("*.cnmt", SearchOptions.Default))
|
||
{
|
||
using var fileRef = new UniqueRef<IFile>();
|
||
|
||
var result = openFileSystem.OpenFile(ref fileRef.Ref, entry.FullPath.ToU8Span(), OpenMode.Read);
|
||
if (result.IsFailure()) continue;
|
||
using var nacpFile = fileRef.Release();
|
||
using var asStream = nacpFile.AsStream();
|
||
|
||
var cnmt = new Cnmt(asStream);
|
||
var applicationTitle = cnmt.ApplicationTitleId;
|
||
return (cnmt.Type,applicationTitle, cnmt.TitleVersion);
|
||
}
|
||
|
||
return (null,0, new TitleVersion(0, true));
|
||
}
|
||
|
||
/// <summary>
|
||
/// Quick sanity check that the stream looks like a PFS0 file system.
|
||
/// </summary>
|
||
private bool IsPfs0FileSystem(Stream stream)
|
||
{
|
||
try
|
||
{
|
||
if (!stream.CanSeek) return false;
|
||
stream.Seek(0, SeekOrigin.Begin);
|
||
|
||
var storage = new StreamStorage(stream, true);
|
||
var partition = new PartitionFileSystem();
|
||
partition.Initialize(storage).ThrowIfFailure();
|
||
_logger.LogInformation("PFS0 found");
|
||
return true;
|
||
}
|
||
catch
|
||
{
|
||
// ignored
|
||
}
|
||
|
||
return false;
|
||
}
|
||
private bool IsXciFileSystem(Stream stream)
|
||
{
|
||
try
|
||
{
|
||
if (!stream.CanSeek) return false;
|
||
stream.Seek(0, SeekOrigin.Begin);
|
||
|
||
var storage = new StreamStorage(stream, true);
|
||
try
|
||
{
|
||
var xciBlock = new Xci(_keySet, storage);
|
||
_logger.LogInformation("XCI found");
|
||
return xciBlock.HasPartition(XciPartitionType.Secure);
|
||
}
|
||
catch
|
||
{
|
||
// ignored
|
||
}
|
||
return false;
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
_logger.LogError("Failed to extract XCI: {Exception}", e.Message);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
public string ExtractHashFromStream(Stream nspStream)
|
||
{
|
||
if (!IsPfs0FileSystem(nspStream))
|
||
return string.Empty;
|
||
|
||
nspStream.Seek(0, SeekOrigin.Begin);
|
||
|
||
using var storage = new StreamStorage(nspStream, true);
|
||
var partition = new PartitionFileSystem();
|
||
partition.Initialize(storage).ThrowIfFailure();
|
||
|
||
// Find the first *.nca that contains the meta header
|
||
var ncaEntries = partition
|
||
.EnumerateEntries("*.nca", SearchOptions.RecurseSubdirectories)
|
||
.Where(e => e.Type == DirectoryEntryType.File)
|
||
.ToList();
|
||
|
||
foreach (var dirEntry in ncaEntries)
|
||
{
|
||
using var fileRef = new UniqueRef<IFile>();
|
||
partition.OpenFile(ref fileRef.Ref, dirEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||
using var ncaFile = fileRef.Release();
|
||
using var ncaFileStorage = new FileStorage(ncaFile);
|
||
|
||
try
|
||
{
|
||
var nca = new Nca(_keySet, ncaFileStorage);
|
||
if (nca.Header.ContentType != NcaContentType.Meta)
|
||
continue; // only the meta NCA contains title metadata
|
||
|
||
// Hash the *first* NCA stream – the stream we just opened
|
||
using var ncaStream = ncaFile.AsStream();
|
||
using var sha256 = SHA256.Create();
|
||
var hash = sha256.ComputeHash(ncaStream);
|
||
var extractHashFromStream = BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
|
||
_logger.LogInformation("Computed first‑stream hash {Hash} for {TitleId}", extractHashFromStream,
|
||
nca.Header.TitleId);
|
||
return extractHashFromStream;
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
_logger.LogError("Failed to extract NSP: {Exception}", e.Message);
|
||
}
|
||
}
|
||
return string.Empty;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// DTO returned by the extractor – contains all data the snapshot needs.
|
||
/// </summary>
|
||
public sealed class NcaMetadataWithHash
|
||
{
|
||
public string TitleId { get; }
|
||
public string ApplicationTitle { get; set; }
|
||
public int Version { get; }
|
||
|
||
public ContentMetaType ContentMetaType { get; set; }
|
||
public string Hash { get; }
|
||
|
||
public NcaMetadataWithHash(string titleId, string applicationTitle, int version,
|
||
ContentMetaType contentMetaType, string hash)
|
||
{
|
||
TitleId = titleId;
|
||
ApplicationTitle = applicationTitle;
|
||
Version = version;
|
||
ContentMetaType = contentMetaType;
|
||
Hash = hash;
|
||
}
|
||
}
|
||
} |