Support optional header in STFS, fix endianness in Descriptor (#80)

* Fix endianness in STFS Descriptor

* Support optional header for installer packages

* Fix field types

* Fix syntax

* Fix build
This commit is contained in:
Deterous
2026-04-09 22:57:26 +09:00
committed by GitHub
parent 45956e9128
commit c88ea9ce30
10 changed files with 213 additions and 13 deletions

View File

@@ -34,8 +34,33 @@ namespace SabreTools.Data.Models.STFS
public const string MagicStringCON = "CON ";
/// <summary>
/// Standard length of an STFS header
/// Standard length of all fixed STFS header fields
/// </summary>
public const uint StandardHeaderSize = 0xB000;
public const uint MinimumHeaderSize = 0x971A;
/// <summary>
/// System Update installer type magic string
/// </summary>
public const string InstallerTypeSystemUpdate = "SUPD";
/// <summary>
/// Title Update installer type magic string
/// </summary>
public const string InstallerTypeTitleUpdate = "TUPD";
/// <summary>
/// System Update Cache installer type magic string
/// </summary>
public const string InstallerTypeSystemUpdateCache = "P$SU";
/// <summary>
/// Title Update Cache installer type magic string
/// </summary>
public const string InstallerTypeTitleUpdateCache = "P$TU";
/// <summary>
/// Title Content Cache installer type magic string
/// </summary>
public const string InstallerTypeTitleContentCache = "P$TC";
}
}

View File

@@ -58,4 +58,17 @@ namespace SabreTools.Data.Models.XenonExecutable
DEVICE_ID_TRANSFER = 0x00000040,
PROFILE_ID_TRANSFER = 0x00000080,
}
/// <summary>
/// Installer cache package resume state
/// </summary>
public enum ResumeState : uint
{
FILE_HEADERS_NOT_READY = 0x46494C48,
NEW_FOLDER = 0x666F6C64,
NEW_FOLDER_RESUME_ATTEMPT_1 = 0x666F6C31,
NEW_FOLDER_RESUME_ATTEMPT_2 = 0x666F6C32,
NEW_FOLDER_RESUME_ATTEMPT_UNKNOWN = 0x666F6C3F,
NEW_FOLDER_RESUME_ATTEMPT_SPECIFIC = 0x666F6C40,
}
}

View File

@@ -34,8 +34,8 @@ namespace SabreTools.Data.Models.STFS
public byte[] HeaderHash { get; set; } = new byte[20];
/// <summary>
/// Size of the header, in bytes (from ??? to ???)
/// The actual end of header is padded and zeroed up until next multiple of 4096 bytes
/// Size of the header, in bytes (from start of file)
/// The actual end of header is padded and zeroed up until next block (multiple of 4096 bytes)
/// </summary>
/// <remarks>Big-endian</remarks>
public uint HeaderSize { get; set; }
@@ -274,5 +274,10 @@ namespace SabreTools.Data.Models.STFS
/// </summary>
/// <remarks>If present, 768 bytes, UTF-8 string</remarks>
public byte[]? AdditionalDisplayDescriptions { get; set; }
/// <summary>
/// Optional field present on installer update/cache packages
/// </summary>
public InstallerHeader? InstallerHeader { get; set; }
}
}

View File

@@ -0,0 +1,41 @@
using SabreTools.Numerics;
namespace SabreTools.Data.Models.STFS
{
/// <summary>
/// STFS Volume Descriptor, for System or Title Cache Installer STFS packages
/// </summary>
public class InstallerCacheHeader : InstallerHeader
{
/// <summary>
/// Resume state enum
/// See Enums.ResumeState
/// </summary>
/// <remarks>If present, 4 bytes</remarks>
public uint ResumeState { get; set; }
/// <summary>
/// Current file index
/// </summary>
/// <remarks>Big-endian</remarks>
public ulong CurrentFileIndex { get; set; }
/// <summary>
/// Number of bytes processed
/// </summary>
/// <remarks>Big-endian</remarks>
public ulong BytesProcessed { get; set; }
/// <summary>
/// Datetime for last modified
/// </summary>
/// <remarks>Microsoft FILETIME, Big-endian, 8 bytes</remarks>
public long LastModifiedDateTime { get; set; }
/// <summary>
/// Cache resume data
/// </summary>
/// <remarks>5584 bytes</remarks>
public byte[] ResumeData { get; set; } = new byte[5584];
}
}

View File

@@ -0,0 +1,16 @@
namespace SabreTools.Data.Models.STFS
{
/// <summary>
/// STFS Optional header present in STFS packages for installers
/// Original research, field not mentioned on free60 wiki
/// </summary>
public class InstallerHeader
{
/// <summary>
/// String indicating type of installer
/// See Constants.InstallerType*
/// </summary>
/// <remarks>4 bytes, ASCII</remarks>
public byte[] InstallerType { get; set; } = new byte[4];
}
}

View File

@@ -0,0 +1,22 @@
using SabreTools.Numerics;
namespace SabreTools.Data.Models.STFS
{
/// <summary>
/// STFS Volume Descriptor, for System or Title Update Installer STFS packages
/// </summary>
public class InstallerUpdateHeader : InstallerHeader
{
/// <summary>
/// Field for base version number, major.minor.build.revision
/// </summary>
/// <remarks>4 bytes</remarks>
public uint InstallerBaseVersion { get; set; }
/// <summary>
/// Field for version number number, major.minor.build.revision
/// </summary>
/// <remarks>4 bytes</remarks>
public uint InstallerVersion { get; set; }
}
}

View File

@@ -26,13 +26,13 @@ namespace SabreTools.Data.Models.STFS
/// <summary>
/// File Table Block Count
/// </summary>
/// <remarks>Big-endian</remarks>
/// <remarks>Little-endian</remarks>
public short FileTableBlockCount { get; set; }
/// <summary>
/// File Table Block Number
/// </summary>
/// <remarks>Big-endian, 3-byte int24</remarks>
/// <remarks>Little-endian, 3-byte int24</remarks>
public Int24 FileTableBlockNumber { get; set; } = new();
/// <summary>

View File

@@ -37,13 +37,13 @@ namespace SabreTools.Data.Models.STFS
/// <summary>
/// Data Block Count
/// </summary>
/// <remarks>Big-endian, 3-byte uint24</remarks>
/// <remarks>Little-endian, 3-byte uint24</remarks>
public UInt24 DataBlockCount { get; set; } = new();
/// <summary>
/// Data Block Offset
/// </summary>
/// <remarks>Big-endian, 3-byte uint24</remarks>
/// <remarks>Little-endian, 3-byte uint24</remarks>
public UInt24 DataBlockOffset { get; set; } = new();
/// <summary>

View File

@@ -1,4 +1,5 @@
using System.IO;
using System.Text;
using SabreTools.Data.Models.STFS;
using SabreTools.IO.Extensions;
using SabreTools.Numerics.Extensions;
@@ -15,7 +16,7 @@ namespace SabreTools.Serialization.Readers
return null;
// Simple check for a valid stream length
if (Constants.StandardHeaderSize > data.Length - data.Position)
if (Constants.MinimumHeaderSize > data.Length - data.Position)
return null;
try
@@ -127,6 +128,38 @@ namespace SabreTools.Serialization.Readers
obj.TitleThumbnailImage = data.ReadBytes(0x4000);
}
// Parse optional header if header size (rounded up to nearest block) is sufficiently large
if (((obj.HeaderSize + 0xFFF) & 0xFFFFF000) - Constants.MinimumHeaderSize >= 0x15F4)
{
byte[] installerType = data.ReadBytes(4);
string type = Encoding.UTF8.GetString(installerType);
if (type.Equals(Constants.InstallerTypeSystemUpdate) || type.Equals(Constants.InstallerTypeTitleUpdate))
{
var updateHeader = new InstallerUpdateHeader();
updateHeader.InstallerType = installerType;
updateHeader.InstallerBaseVersion = data.ReadUInt32BigEndian();
updateHeader.InstallerVersion = data.ReadUInt32BigEndian();
obj.InstallerHeader = updateHeader;
}
else if (type.Equals(Constants.InstallerTypeSystemUpdateCache) || type.Equals(Constants.InstallerTypeTitleUpdateCache) || type.Equals(Constants.InstallerTypeTitleContentCache))
{
var cacheHeader = new InstallerCacheHeader();
cacheHeader.InstallerType = installerType;
cacheHeader.ResumeState = data.ReadUInt32BigEndian();
cacheHeader.CurrentFileIndex = data.ReadUInt64BigEndian();
cacheHeader.BytesProcessed = data.ReadUInt64BigEndian();
cacheHeader.LastModifiedDateTime = data.ReadInt64BigEndian();
cacheHeader.ResumeData = data.ReadBytes(5584);
obj.InstallerHeader = cacheHeader;
}
else
{
var installerHeader = new InstallerHeader();
installerHeader.InstallerType = installerType;
obj.InstallerHeader = installerHeader;
}
}
return obj;
}
@@ -200,8 +233,8 @@ namespace SabreTools.Serialization.Readers
obj.WorkerThreadProcessor = data.ReadByteValue();
obj.WorkerThreadPriority = data.ReadByteValue();
obj.Hash = data.ReadBytes(20);
obj.DataBlockCount = data.ReadUInt24BigEndian();
obj.DataBlockOffset = data.ReadUInt24BigEndian();
obj.DataBlockCount = data.ReadUInt24LittleEndian();
obj.DataBlockOffset = data.ReadUInt24LittleEndian();
obj.Hash = data.ReadBytes(5);
return obj;
@@ -213,8 +246,8 @@ namespace SabreTools.Serialization.Readers
obj.VolumeDescriptorSize = data.ReadByteValue();
obj.Reserved = data.ReadByteValue();
obj.BlockSeparation = data.ReadByteValue();
obj.FileTableBlockCount = data.ReadInt16BigEndian();
obj.FileTableBlockNumber = data.ReadInt24BigEndian();
obj.FileTableBlockCount = data.ReadInt16LittleEndian();
obj.FileTableBlockNumber = data.ReadInt24LittleEndian();
obj.TopHashTableHash = data.ReadBytes(20);
obj.TotalAllocatedBlockCount = data.ReadInt32BigEndian();
obj.TotalUnallocatedBlockCount = data.ReadInt32BigEndian();

View File

@@ -163,6 +163,9 @@ namespace SabreTools.Wrappers
}
}
if (header.InstallerHeader is not null)
Print(builder, header.InstallerHeader);
builder.AppendLine();
}
@@ -267,5 +270,47 @@ namespace SabreTools.Wrappers
builder.AppendLine();
}
protected static void Print(StringBuilder builder, InstallerHeader installerHeader)
{
builder.AppendLine(" Installer Information");
builder.AppendLine(" -------------------------");
builder.AppendLine(installerHeader.InstallerType, " Installer Type");
builder.AppendLine(Encoding.UTF8.GetString(installerHeader.InstallerType), " Installer Type (Parsed)");
if (installerHeader is InstallerUpdateHeader updateHeader)
{
builder.AppendLine(updateHeader.InstallerBaseVersion, " Installer Base Version");
uint bvMajor = updateHeader.InstallerBaseVersion >> 28; // Top 4 bits
uint bvMinor = (updateHeader.InstallerBaseVersion >> 24) & 0xF; // Next top 4 bits
uint bvBuild = (updateHeader.InstallerBaseVersion >> 8) & 0xFFFF; // Next 16 bits
uint bvRevision = updateHeader.InstallerBaseVersion & 0xFF; // Lowest 8 bits
builder.AppendLine($"{bvMajor}.{bvMinor}.{bvBuild}.{bvRevision}", " Installer Base Version (Parsed)");
builder.AppendLine(updateHeader.InstallerVersion, " Installer Version");
uint vMajor = updateHeader.InstallerVersion >> 28; // Top 4 bits
uint vMinor = (updateHeader.InstallerVersion >> 24) & 0xF; // Next top 4 bits
uint vBuild = (updateHeader.InstallerVersion >> 8) & 0xFFFF; // Next 8 bits
uint vRevision = updateHeader.InstallerVersion & 0xFF; // Lowest 8 bits
builder.AppendLine($"{vMajor}.{vMinor}.{vBuild}.{vRevision}", " Installer Version (Parsed)");
}
else if (installerHeader is InstallerCacheHeader cacheHeader)
{
builder.AppendLine(cacheHeader.ResumeState, " Resume State"); // See Enums.ResumeState
builder.AppendLine(cacheHeader.CurrentFileIndex, " Current File Index");
builder.AppendLine(cacheHeader.BytesProcessed, " Bytes Processed");
builder.AppendLine(cacheHeader.LastModifiedDateTime, " Last Modified Date Time");
DateTime datetime = DateTime.FromFileTime(cacheHeader.LastModifiedDateTime);
builder.AppendLine(datetime.ToString("yyyy-MM-dd HH:mm:ss"), " Last Modified Date Time (Parsed)");
builder.AppendLine(cacheHeader.ResumeData, " Resume Data");
}
builder.AppendLine();
}
}
}