2018-06-22 21:05:29 +02:00
|
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
2018-06-30 21:15:55 +02:00
|
|
|
|
using System.Diagnostics;
|
2018-06-21 18:16:51 +02:00
|
|
|
|
using System.IO;
|
|
|
|
|
using System.Linq;
|
2018-07-01 22:12:59 +02:00
|
|
|
|
using System.Text;
|
2018-06-21 18:16:51 +02:00
|
|
|
|
|
|
|
|
|
namespace libhac
|
|
|
|
|
{
|
2018-06-26 00:26:47 +02:00
|
|
|
|
public class SdFs : IDisposable
|
2018-06-21 18:16:51 +02:00
|
|
|
|
{
|
|
|
|
|
public Keyset Keyset { get; }
|
|
|
|
|
public string RootDir { get; }
|
|
|
|
|
public string ContentsDir { get; }
|
2018-06-27 02:10:21 +02:00
|
|
|
|
|
|
|
|
|
public Dictionary<string, Nca> Ncas { get; } = new Dictionary<string, Nca>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
|
public Dictionary<ulong, Title> Titles { get; } = new Dictionary<ulong, Title>();
|
2018-07-02 20:16:38 +02:00
|
|
|
|
public Dictionary<ulong, Application> Applications { get; } = new Dictionary<ulong, Application>();
|
2018-06-27 02:10:21 +02:00
|
|
|
|
|
2018-06-26 00:26:47 +02:00
|
|
|
|
private List<Nax0> Nax0s { get; } = new List<Nax0>();
|
2018-06-21 18:16:51 +02:00
|
|
|
|
|
2018-07-01 22:12:59 +02:00
|
|
|
|
public SdFs(Keyset keyset, string rootDir)
|
2018-06-21 18:16:51 +02:00
|
|
|
|
{
|
2018-07-01 22:12:59 +02:00
|
|
|
|
RootDir = rootDir;
|
|
|
|
|
Keyset = keyset;
|
|
|
|
|
|
|
|
|
|
if (Directory.Exists(Path.Combine(rootDir, "Nintendo")))
|
|
|
|
|
{
|
|
|
|
|
ContentsDir = Path.Combine(rootDir, "Nintendo", "Contents");
|
|
|
|
|
}
|
|
|
|
|
else if (Directory.Exists(Path.Combine(rootDir, "Contents")))
|
2018-06-21 18:16:51 +02:00
|
|
|
|
{
|
2018-07-01 22:12:59 +02:00
|
|
|
|
ContentsDir = Path.Combine(rootDir, "Contents");
|
2018-06-21 18:16:51 +02:00
|
|
|
|
}
|
|
|
|
|
|
2018-06-27 02:10:21 +02:00
|
|
|
|
OpenAllNcas();
|
|
|
|
|
ReadTitles();
|
2018-06-30 02:44:12 +02:00
|
|
|
|
ReadControls();
|
2018-07-02 20:16:38 +02:00
|
|
|
|
CreateApplications();
|
2018-06-21 18:16:51 +02:00
|
|
|
|
}
|
|
|
|
|
|
2018-06-27 02:42:01 +02:00
|
|
|
|
private void OpenAllNcas()
|
2018-06-21 18:16:51 +02:00
|
|
|
|
{
|
2018-07-01 22:12:59 +02:00
|
|
|
|
string[] files = Directory.GetFileSystemEntries(ContentsDir, "*.nca", SearchOption.AllDirectories).ToArray();
|
|
|
|
|
|
|
|
|
|
foreach (var file in files)
|
2018-06-21 18:16:51 +02:00
|
|
|
|
{
|
2018-06-22 21:05:29 +02:00
|
|
|
|
Nca nca = null;
|
|
|
|
|
try
|
|
|
|
|
{
|
2018-07-01 22:12:59 +02:00
|
|
|
|
bool isNax0;
|
|
|
|
|
Stream stream = OpenSplitNcaStream(file);
|
|
|
|
|
if (stream == null) continue;
|
|
|
|
|
|
|
|
|
|
using (var reader = new BinaryReader(stream, Encoding.Default, true))
|
|
|
|
|
{
|
|
|
|
|
stream.Position = 0x20;
|
|
|
|
|
isNax0 = reader.ReadUInt32() == 0x3058414E; // NAX0
|
|
|
|
|
stream.Position = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (isNax0)
|
|
|
|
|
{
|
|
|
|
|
var sdPath = "/" + Util.GetRelativePath(file, ContentsDir).Replace('\\', '/');
|
|
|
|
|
var nax0 = new Nax0(Keyset, stream, sdPath, false);
|
|
|
|
|
Nax0s.Add(nax0);
|
|
|
|
|
nca = new Nca(Keyset, nax0.Stream, false);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
nca = new Nca(Keyset, stream, false);
|
|
|
|
|
}
|
2018-07-02 20:16:38 +02:00
|
|
|
|
|
2018-06-27 02:10:21 +02:00
|
|
|
|
nca.NcaId = Path.GetFileNameWithoutExtension(file);
|
2018-06-27 02:42:01 +02:00
|
|
|
|
var extention = nca.Header.ContentType == ContentType.Meta ? ".cnmt.nca" : ".nca";
|
|
|
|
|
nca.Filename = nca.NcaId + extention;
|
2018-06-22 21:05:29 +02:00
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
Console.WriteLine($"{ex.Message} {file}");
|
|
|
|
|
}
|
|
|
|
|
|
2018-06-27 02:10:21 +02:00
|
|
|
|
if (nca != null) Ncas.Add(nca.NcaId, nca);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-06-27 02:42:01 +02:00
|
|
|
|
private void ReadTitles()
|
2018-06-27 02:10:21 +02:00
|
|
|
|
{
|
|
|
|
|
foreach (var nca in Ncas.Values.Where(x => x.Header.ContentType == ContentType.Meta))
|
|
|
|
|
{
|
|
|
|
|
var title = new Title();
|
|
|
|
|
|
|
|
|
|
// Meta contents always have 1 Partition FS section with 1 file in it
|
2018-06-28 22:02:23 +02:00
|
|
|
|
Stream sect = nca.OpenSection(0, false);
|
2018-06-27 02:10:21 +02:00
|
|
|
|
var pfs0 = new Pfs0(sect);
|
|
|
|
|
var file = pfs0.GetFile(0);
|
|
|
|
|
|
|
|
|
|
var metadata = new Cnmt(new MemoryStream(file));
|
|
|
|
|
title.Id = metadata.TitleId;
|
2018-07-01 22:12:59 +02:00
|
|
|
|
title.Version = metadata.TitleVersion;
|
2018-06-27 02:10:21 +02:00
|
|
|
|
title.Metadata = metadata;
|
2018-06-29 21:53:51 +02:00
|
|
|
|
title.MetaNca = nca;
|
2018-06-27 02:42:01 +02:00
|
|
|
|
title.Ncas.Add(nca);
|
|
|
|
|
|
|
|
|
|
foreach (var content in metadata.ContentEntries)
|
|
|
|
|
{
|
|
|
|
|
var ncaId = content.NcaId.ToHexString();
|
|
|
|
|
|
|
|
|
|
if (Ncas.TryGetValue(ncaId, out Nca contentNca))
|
|
|
|
|
{
|
|
|
|
|
title.Ncas.Add(contentNca);
|
|
|
|
|
}
|
2018-06-29 21:53:51 +02:00
|
|
|
|
|
|
|
|
|
switch (content.Type)
|
|
|
|
|
{
|
|
|
|
|
case CnmtContentType.Program:
|
|
|
|
|
title.ProgramNca = contentNca;
|
|
|
|
|
break;
|
|
|
|
|
case CnmtContentType.Control:
|
|
|
|
|
title.ControlNca = contentNca;
|
|
|
|
|
break;
|
|
|
|
|
}
|
2018-06-27 02:42:01 +02:00
|
|
|
|
}
|
|
|
|
|
|
2018-06-27 02:10:21 +02:00
|
|
|
|
Titles.Add(title.Id, title);
|
2018-06-21 18:16:51 +02:00
|
|
|
|
}
|
2018-06-26 00:26:47 +02:00
|
|
|
|
}
|
|
|
|
|
|
2018-06-30 02:44:12 +02:00
|
|
|
|
private void ReadControls()
|
|
|
|
|
{
|
|
|
|
|
foreach (var title in Titles.Values.Where(x => x.ControlNca != null))
|
|
|
|
|
{
|
|
|
|
|
var romfs = new Romfs(title.ControlNca.OpenSection(0, false));
|
|
|
|
|
var control = romfs.GetFile("/control.nacp");
|
|
|
|
|
Directory.CreateDirectory("control");
|
|
|
|
|
File.WriteAllBytes($"control/{title.Id:X16}.nacp", control);
|
|
|
|
|
|
|
|
|
|
var reader = new BinaryReader(new MemoryStream(control));
|
2018-06-30 21:15:55 +02:00
|
|
|
|
title.Control = new Nacp(reader);
|
|
|
|
|
|
|
|
|
|
foreach (var lang in title.Control.Languages)
|
|
|
|
|
{
|
|
|
|
|
if (!string.IsNullOrWhiteSpace(lang.Title))
|
|
|
|
|
{
|
|
|
|
|
title.Name = lang.Title;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-06-30 02:44:12 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-07-02 20:16:38 +02:00
|
|
|
|
private void CreateApplications()
|
|
|
|
|
{
|
|
|
|
|
foreach (var title in Titles.Values.Where(x => x.Metadata.Type >= TitleType.Application))
|
|
|
|
|
{
|
|
|
|
|
var meta = title.Metadata;
|
|
|
|
|
ulong appId = meta.ApplicationTitleId;
|
|
|
|
|
|
|
|
|
|
if (!Applications.TryGetValue(appId, out var app))
|
|
|
|
|
{
|
|
|
|
|
app = new Application();
|
|
|
|
|
Applications.Add(appId, app);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
app.AddTitle(title);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-07-01 22:12:59 +02:00
|
|
|
|
internal static Stream OpenSplitNcaStream(string path)
|
|
|
|
|
{
|
|
|
|
|
List<string> files = new List<string>();
|
|
|
|
|
List<Stream> streams = new List<Stream>();
|
|
|
|
|
|
|
|
|
|
if (Directory.Exists(path))
|
|
|
|
|
{
|
|
|
|
|
while (true)
|
|
|
|
|
{
|
|
|
|
|
var partName = Path.Combine(path, $"{files.Count:D2}");
|
|
|
|
|
if (!File.Exists(partName)) break;
|
|
|
|
|
|
|
|
|
|
files.Add(partName);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else if (File.Exists(path))
|
|
|
|
|
{
|
|
|
|
|
if (Path.GetFileName(path) != "00")
|
|
|
|
|
{
|
|
|
|
|
return new FileStream(path, FileMode.Open, FileAccess.Read);
|
|
|
|
|
}
|
|
|
|
|
files.Add(path);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
throw new FileNotFoundException("Could not find the input file or directory");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach (var file in files)
|
|
|
|
|
{
|
|
|
|
|
streams.Add(new FileStream(file, FileMode.Open, FileAccess.Read));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (streams.Count == 0) return null;
|
|
|
|
|
|
|
|
|
|
var stream = new CombinationStream(streams);
|
|
|
|
|
return stream;
|
|
|
|
|
}
|
|
|
|
|
|
2018-06-26 00:26:47 +02:00
|
|
|
|
private void DisposeNcas()
|
|
|
|
|
{
|
2018-06-27 02:10:21 +02:00
|
|
|
|
foreach (Nca nca in Ncas.Values)
|
2018-06-26 00:26:47 +02:00
|
|
|
|
{
|
|
|
|
|
nca.Dispose();
|
|
|
|
|
}
|
|
|
|
|
Ncas.Clear();
|
2018-06-21 18:16:51 +02:00
|
|
|
|
|
2018-06-27 02:10:21 +02:00
|
|
|
|
// Disposing the Nca disposes the Nax0 as well
|
2018-06-26 00:26:47 +02:00
|
|
|
|
Nax0s.Clear();
|
2018-06-27 02:10:21 +02:00
|
|
|
|
Titles.Clear();
|
2018-06-26 00:26:47 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void Dispose()
|
|
|
|
|
{
|
|
|
|
|
DisposeNcas();
|
2018-06-21 18:16:51 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
2018-06-27 02:10:21 +02:00
|
|
|
|
|
2018-06-30 21:15:55 +02:00
|
|
|
|
[DebuggerDisplay("{" + nameof(Name) + "}")]
|
2018-06-27 02:10:21 +02:00
|
|
|
|
public class Title
|
|
|
|
|
{
|
|
|
|
|
public ulong Id { get; internal set; }
|
|
|
|
|
public TitleVersion Version { get; internal set; }
|
|
|
|
|
public List<Nca> Ncas { get; } = new List<Nca>();
|
|
|
|
|
public Cnmt Metadata { get; internal set; }
|
2018-06-29 21:53:51 +02:00
|
|
|
|
|
2018-06-30 02:44:12 +02:00
|
|
|
|
public string Name { get; internal set; }
|
2018-06-30 21:15:55 +02:00
|
|
|
|
public Nacp Control { get; internal set; }
|
2018-06-29 21:53:51 +02:00
|
|
|
|
public Nca MetaNca { get; internal set; }
|
|
|
|
|
public Nca ProgramNca { get; internal set; }
|
|
|
|
|
public Nca ControlNca { get; internal set; }
|
2018-07-02 20:16:38 +02:00
|
|
|
|
|
|
|
|
|
public long GetSize()
|
|
|
|
|
{
|
|
|
|
|
return Metadata.ContentEntries
|
|
|
|
|
.Where(x => x.Type < CnmtContentType.UpdatePatch)
|
|
|
|
|
.Sum(x => x.Size);
|
|
|
|
|
}
|
2018-06-27 02:10:21 +02:00
|
|
|
|
}
|
2018-07-01 22:12:59 +02:00
|
|
|
|
|
|
|
|
|
public class Application
|
|
|
|
|
{
|
|
|
|
|
public Title Main { get; private set; }
|
|
|
|
|
public Title Patch { get; private set; }
|
2018-07-02 20:16:38 +02:00
|
|
|
|
public List<Title> AddOnContent { get; } = new List<Title>();
|
|
|
|
|
|
|
|
|
|
public ulong TitleId { get; private set; }
|
|
|
|
|
public TitleVersion Version { get; private set; }
|
|
|
|
|
public Nacp Nacp { get; private set; }
|
2018-07-01 22:12:59 +02:00
|
|
|
|
|
|
|
|
|
public string Name { get; private set; }
|
2018-07-02 20:16:38 +02:00
|
|
|
|
public string DisplayVersion { get; private set; }
|
2018-07-01 22:12:59 +02:00
|
|
|
|
|
2018-07-02 20:16:38 +02:00
|
|
|
|
public void AddTitle(Title title)
|
2018-07-01 22:12:59 +02:00
|
|
|
|
{
|
2018-07-02 20:16:38 +02:00
|
|
|
|
if (TitleId != 0 && title.Metadata.ApplicationTitleId != TitleId)
|
|
|
|
|
throw new InvalidDataException("Title IDs do not match");
|
|
|
|
|
TitleId = title.Metadata.ApplicationTitleId;
|
2018-07-01 22:12:59 +02:00
|
|
|
|
|
2018-07-02 20:16:38 +02:00
|
|
|
|
switch (title.Metadata.Type)
|
|
|
|
|
{
|
|
|
|
|
case TitleType.Application:
|
|
|
|
|
Main = title;
|
|
|
|
|
break;
|
|
|
|
|
case TitleType.Patch:
|
|
|
|
|
Patch = title;
|
|
|
|
|
break;
|
|
|
|
|
case TitleType.AddOnContent:
|
|
|
|
|
AddOnContent.Add(title);
|
|
|
|
|
break;
|
|
|
|
|
case TitleType.DeltaTitle:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
UpdateInfo();
|
2018-07-01 22:12:59 +02:00
|
|
|
|
}
|
|
|
|
|
|
2018-07-02 20:16:38 +02:00
|
|
|
|
private void UpdateInfo()
|
2018-07-01 22:12:59 +02:00
|
|
|
|
{
|
|
|
|
|
if (Patch != null)
|
|
|
|
|
{
|
|
|
|
|
Name = Patch.Name;
|
2018-07-02 20:16:38 +02:00
|
|
|
|
Version = Patch.Version;
|
|
|
|
|
DisplayVersion = Patch.Control?.Version ?? "";
|
|
|
|
|
Nacp = Patch.Control;
|
2018-07-01 22:12:59 +02:00
|
|
|
|
}
|
|
|
|
|
else if (Main != null)
|
|
|
|
|
{
|
|
|
|
|
Name = Main.Name;
|
2018-07-02 20:16:38 +02:00
|
|
|
|
Version = Main.Version;
|
|
|
|
|
DisplayVersion = Main.Control?.Version ?? "";
|
|
|
|
|
Nacp = Main.Control;
|
2018-07-01 22:12:59 +02:00
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
Name = "";
|
2018-07-02 20:16:38 +02:00
|
|
|
|
DisplayVersion = "";
|
2018-07-01 22:12:59 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-06-21 18:16:51 +02:00
|
|
|
|
}
|