From c88ea9ce301af9237802ee5c3f93ad18c92a0df6 Mon Sep 17 00:00:00 2001 From: Deterous <138427222+Deterous@users.noreply.github.com> Date: Thu, 9 Apr 2026 22:57:26 +0900 Subject: [PATCH] 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 --- SabreTools.Data.Models/STFS/Constants.cs | 29 +++++++++++- SabreTools.Data.Models/STFS/Enums.cs | 13 ++++++ SabreTools.Data.Models/STFS/Header.cs | 9 +++- .../STFS/InstallerCacheHeader.cs | 41 +++++++++++++++++ .../STFS/InstallerHeader.cs | 16 +++++++ .../STFS/InstallerUpdateHeader.cs | 22 +++++++++ SabreTools.Data.Models/STFS/STFSDescriptor.cs | 4 +- SabreTools.Data.Models/STFS/SVODDescriptor.cs | 4 +- SabreTools.Serialization.Readers/STFS.cs | 43 +++++++++++++++--- SabreTools.Wrappers/STFS.Printing.cs | 45 +++++++++++++++++++ 10 files changed, 213 insertions(+), 13 deletions(-) create mode 100644 SabreTools.Data.Models/STFS/InstallerCacheHeader.cs create mode 100644 SabreTools.Data.Models/STFS/InstallerHeader.cs create mode 100644 SabreTools.Data.Models/STFS/InstallerUpdateHeader.cs diff --git a/SabreTools.Data.Models/STFS/Constants.cs b/SabreTools.Data.Models/STFS/Constants.cs index 2e719722..3e930aa6 100644 --- a/SabreTools.Data.Models/STFS/Constants.cs +++ b/SabreTools.Data.Models/STFS/Constants.cs @@ -34,8 +34,33 @@ namespace SabreTools.Data.Models.STFS public const string MagicStringCON = "CON "; /// - /// Standard length of an STFS header + /// Standard length of all fixed STFS header fields /// - public const uint StandardHeaderSize = 0xB000; + public const uint MinimumHeaderSize = 0x971A; + + /// + /// System Update installer type magic string + /// + public const string InstallerTypeSystemUpdate = "SUPD"; + + /// + /// Title Update installer type magic string + /// + public const string InstallerTypeTitleUpdate = "TUPD"; + + /// + /// System Update Cache installer type magic string + /// + public const string InstallerTypeSystemUpdateCache = "P$SU"; + + /// + /// Title Update Cache installer type magic string + /// + public const string InstallerTypeTitleUpdateCache = "P$TU"; + + /// + /// Title Content Cache installer type magic string + /// + public const string InstallerTypeTitleContentCache = "P$TC"; } } diff --git a/SabreTools.Data.Models/STFS/Enums.cs b/SabreTools.Data.Models/STFS/Enums.cs index 091da320..c61f56bf 100644 --- a/SabreTools.Data.Models/STFS/Enums.cs +++ b/SabreTools.Data.Models/STFS/Enums.cs @@ -58,4 +58,17 @@ namespace SabreTools.Data.Models.XenonExecutable DEVICE_ID_TRANSFER = 0x00000040, PROFILE_ID_TRANSFER = 0x00000080, } + + /// + /// Installer cache package resume state + /// + 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, + } } diff --git a/SabreTools.Data.Models/STFS/Header.cs b/SabreTools.Data.Models/STFS/Header.cs index 6ff6cc94..5db051f0 100644 --- a/SabreTools.Data.Models/STFS/Header.cs +++ b/SabreTools.Data.Models/STFS/Header.cs @@ -34,8 +34,8 @@ namespace SabreTools.Data.Models.STFS public byte[] HeaderHash { get; set; } = new byte[20]; /// - /// 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) /// /// Big-endian public uint HeaderSize { get; set; } @@ -274,5 +274,10 @@ namespace SabreTools.Data.Models.STFS /// /// If present, 768 bytes, UTF-8 string public byte[]? AdditionalDisplayDescriptions { get; set; } + + /// + /// Optional field present on installer update/cache packages + /// + public InstallerHeader? InstallerHeader { get; set; } } } diff --git a/SabreTools.Data.Models/STFS/InstallerCacheHeader.cs b/SabreTools.Data.Models/STFS/InstallerCacheHeader.cs new file mode 100644 index 00000000..5b575b51 --- /dev/null +++ b/SabreTools.Data.Models/STFS/InstallerCacheHeader.cs @@ -0,0 +1,41 @@ +using SabreTools.Numerics; + +namespace SabreTools.Data.Models.STFS +{ + /// + /// STFS Volume Descriptor, for System or Title Cache Installer STFS packages + /// + public class InstallerCacheHeader : InstallerHeader + { + /// + /// Resume state enum + /// See Enums.ResumeState + /// + /// If present, 4 bytes + public uint ResumeState { get; set; } + + /// + /// Current file index + /// + /// Big-endian + public ulong CurrentFileIndex { get; set; } + + /// + /// Number of bytes processed + /// + /// Big-endian + public ulong BytesProcessed { get; set; } + + /// + /// Datetime for last modified + /// + /// Microsoft FILETIME, Big-endian, 8 bytes + public long LastModifiedDateTime { get; set; } + + /// + /// Cache resume data + /// + /// 5584 bytes + public byte[] ResumeData { get; set; } = new byte[5584]; + } +} diff --git a/SabreTools.Data.Models/STFS/InstallerHeader.cs b/SabreTools.Data.Models/STFS/InstallerHeader.cs new file mode 100644 index 00000000..bbb30e52 --- /dev/null +++ b/SabreTools.Data.Models/STFS/InstallerHeader.cs @@ -0,0 +1,16 @@ +namespace SabreTools.Data.Models.STFS +{ + /// + /// STFS Optional header present in STFS packages for installers + /// Original research, field not mentioned on free60 wiki + /// + public class InstallerHeader + { + /// + /// String indicating type of installer + /// See Constants.InstallerType* + /// + /// 4 bytes, ASCII + public byte[] InstallerType { get; set; } = new byte[4]; + } +} diff --git a/SabreTools.Data.Models/STFS/InstallerUpdateHeader.cs b/SabreTools.Data.Models/STFS/InstallerUpdateHeader.cs new file mode 100644 index 00000000..b4f895ae --- /dev/null +++ b/SabreTools.Data.Models/STFS/InstallerUpdateHeader.cs @@ -0,0 +1,22 @@ +using SabreTools.Numerics; + +namespace SabreTools.Data.Models.STFS +{ + /// + /// STFS Volume Descriptor, for System or Title Update Installer STFS packages + /// + public class InstallerUpdateHeader : InstallerHeader + { + /// + /// Field for base version number, major.minor.build.revision + /// + /// 4 bytes + public uint InstallerBaseVersion { get; set; } + + /// + /// Field for version number number, major.minor.build.revision + /// + /// 4 bytes + public uint InstallerVersion { get; set; } + } +} diff --git a/SabreTools.Data.Models/STFS/STFSDescriptor.cs b/SabreTools.Data.Models/STFS/STFSDescriptor.cs index 8678d540..3da03d9e 100644 --- a/SabreTools.Data.Models/STFS/STFSDescriptor.cs +++ b/SabreTools.Data.Models/STFS/STFSDescriptor.cs @@ -26,13 +26,13 @@ namespace SabreTools.Data.Models.STFS /// /// File Table Block Count /// - /// Big-endian + /// Little-endian public short FileTableBlockCount { get; set; } /// /// File Table Block Number /// - /// Big-endian, 3-byte int24 + /// Little-endian, 3-byte int24 public Int24 FileTableBlockNumber { get; set; } = new(); /// diff --git a/SabreTools.Data.Models/STFS/SVODDescriptor.cs b/SabreTools.Data.Models/STFS/SVODDescriptor.cs index a6832b83..4b4e7ae3 100644 --- a/SabreTools.Data.Models/STFS/SVODDescriptor.cs +++ b/SabreTools.Data.Models/STFS/SVODDescriptor.cs @@ -37,13 +37,13 @@ namespace SabreTools.Data.Models.STFS /// /// Data Block Count /// - /// Big-endian, 3-byte uint24 + /// Little-endian, 3-byte uint24 public UInt24 DataBlockCount { get; set; } = new(); /// /// Data Block Offset /// - /// Big-endian, 3-byte uint24 + /// Little-endian, 3-byte uint24 public UInt24 DataBlockOffset { get; set; } = new(); /// diff --git a/SabreTools.Serialization.Readers/STFS.cs b/SabreTools.Serialization.Readers/STFS.cs index 9597e517..608a8631 100644 --- a/SabreTools.Serialization.Readers/STFS.cs +++ b/SabreTools.Serialization.Readers/STFS.cs @@ -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(); diff --git a/SabreTools.Wrappers/STFS.Printing.cs b/SabreTools.Wrappers/STFS.Printing.cs index ee6fd801..e81f93e4 100644 --- a/SabreTools.Wrappers/STFS.Printing.cs +++ b/SabreTools.Wrappers/STFS.Printing.cs @@ -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(); + } } }