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();
+ }
}
}