Files
TinfoilVibeServer/TinfoilVibeServer/Services/NSPExtractor.cs
T
2025-11-07 14:31:59 +10:30

296 lines
12 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 SHA256 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 firststream 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;
}
}
}