From 7485a43364e57c38f9500238343e6f8e60be36dd Mon Sep 17 00:00:00 2001
From: Deterous <138427222+Deterous@users.noreply.github.com>
Date: Wed, 8 Apr 2026 11:14:39 +0900
Subject: [PATCH 1/2] Secure Transacted File System (STFS) Support (#78)
* Initial STFS support
* Fix build errors
* Fix more build errors
* Final fix
* Cleanup printer
* Fix indent
---
SabreTools.Data.Models/README.MD | 1 +
.../STFS/ConsoleSignature.cs | 64 ++++
SabreTools.Data.Models/STFS/Constants.cs | 43 +++
SabreTools.Data.Models/STFS/Enums.cs | 61 ++++
SabreTools.Data.Models/STFS/HashTable.cs | 16 +
SabreTools.Data.Models/STFS/HashTableEntry.cs | 33 +++
SabreTools.Data.Models/STFS/Header.cs | 280 ++++++++++++++++++
SabreTools.Data.Models/STFS/LicenseEntry.cs | 26 ++
.../STFS/MicrosoftSignature.cs | 23 ++
SabreTools.Data.Models/STFS/STFSDescriptor.cs | 57 ++++
SabreTools.Data.Models/STFS/SVODDescriptor.cs | 56 ++++
SabreTools.Data.Models/STFS/Signature.cs | 13 +
SabreTools.Data.Models/STFS/Volume.cs | 37 +++
.../STFS/VolumeDescriptor.cs | 13 +
.../XenonExecutable/Executable.cs | 2 +-
.../XenonExecutable/Header.cs | 2 +-
.../STFSTests.cs | 72 +++++
SabreTools.Serialization.Readers/STFS.cs | 228 ++++++++++++++
SabreTools.Wrappers.Test/STFSTests.cs | 60 ++++
SabreTools.Wrappers/STFS.Printing.cs | 270 +++++++++++++++++
SabreTools.Wrappers/STFS.cs | 98 ++++++
SabreTools.Wrappers/WrapperFactory.cs | 17 ++
SabreTools.Wrappers/WrapperType.cs | 5 +
23 files changed, 1475 insertions(+), 2 deletions(-)
create mode 100644 SabreTools.Data.Models/STFS/ConsoleSignature.cs
create mode 100644 SabreTools.Data.Models/STFS/Constants.cs
create mode 100644 SabreTools.Data.Models/STFS/Enums.cs
create mode 100644 SabreTools.Data.Models/STFS/HashTable.cs
create mode 100644 SabreTools.Data.Models/STFS/HashTableEntry.cs
create mode 100644 SabreTools.Data.Models/STFS/Header.cs
create mode 100644 SabreTools.Data.Models/STFS/LicenseEntry.cs
create mode 100644 SabreTools.Data.Models/STFS/MicrosoftSignature.cs
create mode 100644 SabreTools.Data.Models/STFS/STFSDescriptor.cs
create mode 100644 SabreTools.Data.Models/STFS/SVODDescriptor.cs
create mode 100644 SabreTools.Data.Models/STFS/Signature.cs
create mode 100644 SabreTools.Data.Models/STFS/Volume.cs
create mode 100644 SabreTools.Data.Models/STFS/VolumeDescriptor.cs
create mode 100644 SabreTools.Serialization.Readers.Test/STFSTests.cs
create mode 100644 SabreTools.Serialization.Readers/STFS.cs
create mode 100644 SabreTools.Wrappers.Test/STFSTests.cs
create mode 100644 SabreTools.Wrappers/STFS.Printing.cs
create mode 100644 SabreTools.Wrappers/STFS.cs
diff --git a/SabreTools.Data.Models/README.MD b/SabreTools.Data.Models/README.MD
index bcd9fb33..46f7e2fb 100644
--- a/SabreTools.Data.Models/README.MD
+++ b/SabreTools.Data.Models/README.MD
@@ -117,6 +117,7 @@ Not all of this information was able to be gathered directly from the files in q
| [ECMA-119](https://ecma-international.org/wp-content/uploads/ECMA-119_5th_edition_december_2024.pdf) | ISO9660 |
| [EDM/2](https://www.edm2.com/index.php/Main_Page) | LinearExecutable |
| [faydoc.tripod.com](https://faydoc.tripod.com/formats/) | LinearExecutable |
+| [free60.org](https://free60.org/) | STFS, XEX
| [GuitarGame_ChartFormats](https://github.com/TheNathannator/GuitarGame_ChartFormats/) | Charts |
| [HandWiki](https://handwiki.org/wiki/Start) | Quantum |
| [Handy SDL](https://github.com/mozzwald/handy-sdl) | AtariLynxCart |
diff --git a/SabreTools.Data.Models/STFS/ConsoleSignature.cs b/SabreTools.Data.Models/STFS/ConsoleSignature.cs
new file mode 100644
index 00000000..b590a8ab
--- /dev/null
+++ b/SabreTools.Data.Models/STFS/ConsoleSignature.cs
@@ -0,0 +1,64 @@
+using System.Collections.Generic;
+
+namespace SabreTools.Data.Models.STFS
+{
+ ///
+ /// Signature signed by console, for "CON " format STFS files
+ ///
+ ///
+ public class ConsoleSignature : Signature
+ {
+ ///
+ /// Public Key Certificate Size
+ ///
+ /// Big-endian
+ public ushort CertificateSize { get; set; }
+
+ ///
+ /// Certificate Owner Console ID
+ ///
+ /// 5 bytes
+ public byte[] ConsoleID { get; set; } = new byte[5];
+
+ ///
+ /// Certificate Owner Console Part Number
+ ///
+ /// 20 bytes, ASCII string
+ public byte[] PartNumber { get; set; } = new byte[20];
+
+ ///
+ /// Certificate Owner Console Type (1 for devkit, 2 for retail)
+ ///
+ public byte ConsoleType { get; set; }
+
+ ///
+ /// Certificate Date of Generation
+ ///
+ /// 8 bytes, ASCII string
+ public byte[] CertificateDate { get; set; } = new byte[8];
+
+ ///
+ /// Public Exponent
+ ///
+ /// 4 bytes
+ public byte[] PublicExponent { get; set; } = new byte[4];
+
+ ///
+ /// Public Modulus
+ ///
+ /// 128 bytes
+ public byte[] PublicModulus { get; set; } = new byte[128];
+
+ ///
+ /// Certificate Signature
+ ///
+ /// 256 bytes
+ public byte[] CertificateSignature { get; set; } = new byte[256];
+
+ ///
+ /// Signature
+ ///
+ /// 128 bytes
+ public byte[] Signature { get; set; } = new byte[128];
+ }
+}
diff --git a/SabreTools.Data.Models/STFS/Constants.cs b/SabreTools.Data.Models/STFS/Constants.cs
new file mode 100644
index 00000000..302bb93a
--- /dev/null
+++ b/SabreTools.Data.Models/STFS/Constants.cs
@@ -0,0 +1,43 @@
+using System.Collections.Generic;
+
+namespace SabreTools.Data.Models.STFS
+{
+ ///
+ public static class Constants
+ {
+ ///
+ /// STFS LIVE magic number ("LIVE")
+ ///
+ public static readonly byte[] MagicBytesLIVE = [0x4C, 0x49, 0x56, 0x45];
+
+ ///
+ /// STFS LIVE magic string ("LIVE")
+ ///
+ public const string MagicStringLIVE = "LIVE";
+
+ ///
+ /// STFS PIRS magic number ("PIRS")
+ ///
+ public static readonly byte[] MagicBytesPIRS = [0x50, 0x49, 0x52, 0x53];
+
+ ///
+ /// STFS PIRS magic string ("PIRS")
+ ///
+ public const string MagicStringPIRS = "PIRS";
+
+ ///
+ /// STFS CON magic number ("CON ")
+ ///
+ public static readonly byte[] MagicBytesCON = [0x43, 0x4F, 0x4E, 0x20];
+
+ ///
+ /// STFS CON magic string ("CON")
+ ///
+ public const string MagicStringCON = "CON ";
+
+ ///
+ /// Standard length of an STFS header
+ ///
+ public const uint StandardHeaderSize = 0xB000;
+ }
+}
diff --git a/SabreTools.Data.Models/STFS/Enums.cs b/SabreTools.Data.Models/STFS/Enums.cs
new file mode 100644
index 00000000..091da320
--- /dev/null
+++ b/SabreTools.Data.Models/STFS/Enums.cs
@@ -0,0 +1,61 @@
+using System;
+
+namespace SabreTools.Data.Models.XenonExecutable
+{
+ ///
+ /// STFS Volume Content Type possible values
+ ///
+ ///
+ public enum ContentType : int
+ {
+ SAVED_GAME = 0x00000001,
+ MARKETPLACE_CONTENT = 0x00000002,
+ PUBLISHER = 0x00000003,
+ XBOX_360_TITLE = 0x00001000,
+ IPTV_PAUSE_BUFFER = 0x00002000,
+ INSTALLED_GAME = 0x00004000,
+ XBOX_ORIGINAL_GAME = 0x00005000,
+ XBOX_TITLE = 0x00006000,
+ GAME_ON_DEMAND = 0x00007000,
+ AVATAR_ITEM = 0x00009000,
+ PROFILE = 0x00010000,
+ GAMER_PICTURE = 0x00020000,
+ THEME = 0x00030000,
+ CACHE_FILE = 0x00040000,
+ STORAGE_DOWNLOAD = 0x00050000,
+ XBOX_SAVED_GAME = 0x00060000,
+ XBOX_DOWNLOAD = 0x00070000,
+ GAME_DEMO = 0x00080000,
+ VIDEO = 0x00090000,
+ GAME_TITLE = 0x000A0000,
+ INSTALLER = 0x000B0000,
+ GAME_TRAILER = 0x000C0000,
+ ARCADE_TITLE = 0x000D0000,
+ XNA = 0x000E0000,
+ LICENSE_STORE = 0x000F0000,
+ MOVIE = 0x00100000,
+ TV = 0x00200000,
+ MUSIC_VIDEO = 0x00300000,
+ GAME_VIDEO = 0x00400000,
+ PODCAST_VIDEO = 0x00500000,
+ VIRAL_VIDEO = 0x00600000,
+ COMMUNITY_GAME = 0x02000000,
+ }
+
+ ///
+ /// STFS Transfer Flags
+ ///
+ ///
+ [Flags]
+ public enum TransferFlags : uint
+ {
+ NONE1 = 0x00000001,
+ NONE2 = 0x00000002,
+ DEEP_LINK_SUPPORTED = 0x00000004,
+ DISABLE_NETWORK_STORAGE = 0x00000008,
+ KINECT_ENABLED = 0x00000010,
+ MOVE_ONLY_TRANSFER = 0x00000020,
+ DEVICE_ID_TRANSFER = 0x00000040,
+ PROFILE_ID_TRANSFER = 0x00000080,
+ }
+}
diff --git a/SabreTools.Data.Models/STFS/HashTable.cs b/SabreTools.Data.Models/STFS/HashTable.cs
new file mode 100644
index 00000000..6acf27c5
--- /dev/null
+++ b/SabreTools.Data.Models/STFS/HashTable.cs
@@ -0,0 +1,16 @@
+using System.Collections.Generic;
+
+namespace SabreTools.Data.Models.STFS
+{
+ ///
+ /// STFS Hash Table in a Hash Table Block
+ ///
+ ///
+ public class HashTable
+ {
+ ///
+ /// 170 hash table entries in a single table/block
+ ///
+ public HashTableEntry[] HashTableEntries { get; set; } = new HashTableEntry[170];
+ }
+}
diff --git a/SabreTools.Data.Models/STFS/HashTableEntry.cs b/SabreTools.Data.Models/STFS/HashTableEntry.cs
new file mode 100644
index 00000000..b92888db
--- /dev/null
+++ b/SabreTools.Data.Models/STFS/HashTableEntry.cs
@@ -0,0 +1,33 @@
+using System.Collections.Generic;
+using SabreTools.Numerics;
+
+namespace SabreTools.Data.Models.STFS
+{
+ ///
+ /// STFS Hash Table Entry in a Hash Block's Hash Table
+ ///
+ ///
+ public class HashTableEntry
+ {
+ ///
+ /// SHA-1 hash of the block
+ ///
+ public byte[] Hash { get; set; } = new byte[20];
+
+ ///
+ /// Status of the block
+ /// 0x00 = Unused block
+ /// 0x40 = Free block (previously used, now freed)
+ /// 0x80 = Used block
+ /// 0xC0 = Newly allocated block
+ ///
+ public byte Status { get; set; }
+
+ ///
+ /// Block number corresponding to the hash
+ /// FFFFFF = Block 1 (starting 0xB000)
+ ///
+ /// Big-endian, 3-byte uint24
+ public UInt24 BlockNumber { get; set; } = new();
+ }
+}
diff --git a/SabreTools.Data.Models/STFS/Header.cs b/SabreTools.Data.Models/STFS/Header.cs
new file mode 100644
index 00000000..1d88fc2f
--- /dev/null
+++ b/SabreTools.Data.Models/STFS/Header.cs
@@ -0,0 +1,280 @@
+using System.Collections.Generic;
+
+namespace SabreTools.Data.Models.STFS
+{
+ ///
+ /// Secure Transacted File System, Header format
+ ///
+ ///
+ public class Header
+ {
+ ///
+ /// Magic bytes indicating the format
+ /// Possible values are "LIVE", "PIRS, and "CON "
+ ///
+ /// 4 bytes
+ public byte[] MagicBytes { get; set; } = new byte[4];
+
+ ///
+ /// Signature Block
+ /// Not optional, but abstract class
+ ///
+ /// 552 bytes
+ public Signature? Signature { get; set; }
+
+ ///
+ /// Used to check package owner
+ /// 16 license entries, 16 bytes each
+ ///
+ /// 256 bytes
+ public LicenseEntry[] LicensingData { get; set; } = new LicenseEntry[16];
+
+ ///
+ /// SHA-1 Integrity Hash of the header (from ContentType/0x344 to first hash table)
+ ///
+ /// 20 bytes
+ 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
+ ///
+ /// Big-endian
+ public uint HeaderSize { get; set; }
+
+ ///
+ /// Indication of the content in the STFS
+ /// See Enum.ContentType
+ ///
+ /// Big-endian
+ public int ContentType { get; set; }
+
+ ///
+ /// Intended meaning of some of the below fields
+ /// Known values are 0, 1 and 2
+ /// 0 = A bunch of fields will be zeroed (Seen in system updates)
+ /// 1 = All but the below fields are set
+ /// 2 = The following new fields are set:
+ /// SeriesID, SeasonID, SeasonNumber, EpisodeNumber,
+ /// AdditionalDisplayNames, AdditionalDisplayDescriptions
+ ///
+ /// Big-endian
+ public int MetadataVersion { get; set; }
+
+ ///
+ /// Size of content in bytes
+ ///
+ /// Big-endian
+ public long ContentSize { get; set; }
+
+ ///
+ /// Media ID
+ ///
+ /// Big-endian
+ public uint MediaID { get; set; }
+
+ ///
+ /// Version of System/Title Updates
+ ///
+ /// Big-endian
+ public int Version { get; set; }
+
+ ///
+ /// Base Version of System/Title Updates
+ ///
+ /// Big-endian
+ public int BaseVersion { get; set; }
+
+ ///
+ /// Title ID
+ ///
+ /// Big-endian
+ public uint TitleID { get; set; }
+
+ ///
+ /// Intended platform for content
+ /// 0 = ???, 2 = Xbox 360, 4 = PC
+ ///
+ public byte Platform { get; set; }
+
+ ///
+ /// Intended platform for content
+ /// Xbox 360 = 2, PC = 4
+ ///
+ public byte ExecutableType { get; set; }
+
+ ///
+ /// Disc Number
+ ///
+ public byte DiscNumber { get; set; }
+
+ ///
+ /// Disc In Set
+ ///
+ public byte DiscInSet { get; set; }
+
+ ///
+ /// Save Game ID
+ ///
+ /// Big-endian
+ public uint SaveGameID { get; set; }
+
+ ///
+ /// Console ID
+ ///
+ /// 5 bytes
+ public byte[] ConsoleID { get; set; } = new byte[5];
+
+ ///
+ /// Profile ID
+ ///
+ /// 8 bytes
+ public byte[] ProfileID { get; set; } = new byte[8];
+
+ ///
+ /// STFS Volume Descriptor
+ /// Not optional, but abstract class
+ ///
+ public VolumeDescriptor? VolumeDescriptor { get; set; }
+
+ ///
+ /// Data File Count
+ ///
+ /// Big-endian
+ public int DataFileCount { get; set; }
+
+ ///
+ /// Data File Combined Size, in bytes
+ ///
+ /// Big-endian
+ public long DataFileCombinedSize { get; set; }
+
+ ///
+ /// Descriptor Type
+ /// 0 = STFS, 1 = SVOD
+ ///
+ /// Big-endian
+ public uint DescriptorType { get; set; }
+
+ ///
+ /// Reserved bytes, should be all zeroed
+ ///
+ /// Big-endian
+ public uint Reserved { get; set; }
+
+ ///
+ /// Series ID
+ /// Zeroed for MetadataVersion = 1
+ ///
+ /// 16 bytes
+ public byte[]? SeriesID { get; set; } = new byte[16];
+
+ ///
+ /// Season ID
+ /// Zeroed for MetadataVersion = 1
+ ///
+ /// 16 bytes
+ public byte[]? SeasonID { get; set; } = new byte[16];
+
+ ///
+ /// Season Number
+ /// Zeroed for MetadataVersion = 1
+ ///
+ /// Big-endian
+ public short? SeasonNumber { get; set; }
+
+ ///
+ /// Season Number
+ /// Zeroed for MetadataVersion = 1
+ ///
+ /// Big-endian
+ public short? EpisodeNumber { get; set; }
+
+ ///
+ /// Padding bytes
+ /// If MetadataVersion is 2, there are 40 bytes
+ /// Otherwise, there are 76 bytes
+ ///
+ /// 40 bytes
+ public byte[] Padding { get; set; } = [];
+
+ ///
+ /// Device ID
+ ///
+ /// 20 bytes
+ public byte[] DeviceID { get; set; } = new byte[20];
+
+ ///
+ /// Display Name, UTF-8 string
+ /// 128 bytes per locale, 18 different locales
+ ///
+ /// 2304 bytes, UTF-8 string
+ public byte[] DisplayName { get; set; } = new byte[2304];
+
+ ///
+ /// Display Description, UTF-8 string
+ /// 128 bytes per locale, 18 different locales
+ ///
+ /// 2304 bytes, UTF-8 string
+ public byte[] DisplayDescription { get; set; } = new byte[2304];
+
+ ///
+ /// Publisher Name, UTF-8 string
+ ///
+ /// 128 bytes, UTF-8 string
+ public byte[] PublisherName { get; set; } = new byte[128];
+
+ ///
+ /// Title Name, UTF-8 string
+ ///
+ /// 128 bytes, UTF-8 string
+ public byte[] TitleName { get; set; } = new byte[128];
+
+ ///
+ /// Transfer Flags, see Constants.TransferFlags
+ ///
+ public byte TransferFlags { get; set; }
+
+ ///
+ /// Size of the thumbnail image, in bytes
+ ///
+ /// Big-endian
+ public int ThumbnailImageSize { get; set; }
+
+ ///
+ /// Size of the title thumbnail image, in bytes
+ ///
+ /// Big-endian
+ public int TitleThumbnailImageSize { get; set; }
+
+ ///
+ /// Thumbnail image
+ /// If Metadata version = 1, 0x4000 bytes allocated, padded with zeroes
+ /// If Metadata version = 2, 0x3D00 bytes allocated, padded with zeroes
+ ///
+ public byte[] ThumbnailImage { get; set; } = [];
+
+ ///
+ /// Additional Display Names, UTF-8 string
+ /// 128 bytes per locale, 6 different locales
+ /// Only present if MetadataVersion = 2
+ ///
+ /// If present, 768 bytes, UTF-8 string
+ public byte[]? AdditionalDisplayNames { get; set; }
+
+ ///
+ /// Title thumbnail image
+ /// If Metadata version = 1, 0x4000 bytes allocated, padded with zeroes
+ /// If Metadata version = 2, 0x3D00 bytes allocated, padded with zeroes
+ ///
+ public byte[] TitleThumbnailImage { get; set; } = [];
+
+ ///
+ /// Additional Display Descriptions, UTF-8 string
+ /// 128 bytes per locale, 6 different locales
+ /// Only present if MetadataVersion = 2
+ ///
+ /// If present, 768 bytes, UTF-8 string
+ public byte[]? AdditionalDisplayDescriptions { get; set; }
+ }
+}
diff --git a/SabreTools.Data.Models/STFS/LicenseEntry.cs b/SabreTools.Data.Models/STFS/LicenseEntry.cs
new file mode 100644
index 00000000..93f97ae7
--- /dev/null
+++ b/SabreTools.Data.Models/STFS/LicenseEntry.cs
@@ -0,0 +1,26 @@
+using System.Collections.Generic;
+
+namespace SabreTools.Data.Models.STFS
+{
+ ///
+ /// Secure Transacted File System (STFS) License Entry format
+ ///
+ ///
+ public class LicenseEntry
+ {
+ ///
+ /// License ID
+ ///
+ public long LicenseID { get; set; }
+
+ ///
+ /// License Bits
+ ///
+ public int LicenseBits { get; set; }
+
+ ///
+ /// License Flags
+ ///
+ public int LicenseFlags { get; set; }
+ }
+}
diff --git a/SabreTools.Data.Models/STFS/MicrosoftSignature.cs b/SabreTools.Data.Models/STFS/MicrosoftSignature.cs
new file mode 100644
index 00000000..9bdab7c7
--- /dev/null
+++ b/SabreTools.Data.Models/STFS/MicrosoftSignature.cs
@@ -0,0 +1,23 @@
+using System.Collections.Generic;
+
+namespace SabreTools.Data.Models.STFS
+{
+ ///
+ /// Signature block signed by Microsoft, for LIVE and PIRS format STFS files
+ ///
+ ///
+ public class MicrosoftSignature : Signature
+ {
+ ///
+ /// Signature remotely signed by Microsoft
+ ///
+ /// 256 bytes
+ public byte[] PackageSignature { get; set; } = new byte[256];
+
+ ///
+ /// Zeroed padding
+ ///
+ /// 296 bytes
+ public byte[] Padding { get; set; } = new byte[296];
+ }
+}
diff --git a/SabreTools.Data.Models/STFS/STFSDescriptor.cs b/SabreTools.Data.Models/STFS/STFSDescriptor.cs
new file mode 100644
index 00000000..8a79fcd0
--- /dev/null
+++ b/SabreTools.Data.Models/STFS/STFSDescriptor.cs
@@ -0,0 +1,57 @@
+using System.Collections.Generic;
+using SabreTools.Numerics;
+
+namespace SabreTools.Data.Models.STFS
+{
+ ///
+ /// STFS Volume Descriptor, for STFS packages
+ ///
+ ///
+ public class STFSDescriptor : VolumeDescriptor
+ {
+ ///
+ /// Volume descriptor size (Should be 0x24)
+ ///
+ public byte VolumeDescriptorSize { get; set; }
+
+ ///
+ /// Reserved (Should be 0x00)
+ ///
+ public byte Reserved { get; set; }
+
+ ///
+ /// Block Separation
+ ///
+ public byte BlockSeparation { get; set; }
+
+ ///
+ /// File Table Block Count
+ ///
+ /// Big-endian
+ public short FileTableBlockCount { get; set; }
+
+ ///
+ /// File Table Block Number
+ ///
+ /// Big-endian, 3-byte int24
+ public Int24 FileTableBlockNumber { get; set; } = new();
+
+ ///
+ /// Top Hash Table Hash
+ ///
+ /// 20 bytes
+ public byte[] TopHashTableHash { get; set; } = new byte[20];
+
+ ///
+ /// Total Allocated Block Count
+ ///
+ /// Big-endian
+ public int TotalAllocatedBlockCount { get; set; }
+
+ ///
+ /// Total Unallocated Block Count
+ ///
+ /// Big-endian
+ public int TotalUnallocatedBlockCount { get; set; }
+ }
+}
diff --git a/SabreTools.Data.Models/STFS/SVODDescriptor.cs b/SabreTools.Data.Models/STFS/SVODDescriptor.cs
new file mode 100644
index 00000000..f543d26f
--- /dev/null
+++ b/SabreTools.Data.Models/STFS/SVODDescriptor.cs
@@ -0,0 +1,56 @@
+using System.Collections.Generic;
+using SabreTools.Numerics;
+
+namespace SabreTools.Data.Models.STFS
+{
+ ///
+ /// STFS Volume Descriptor, for STFS packages
+ ///
+ ///
+ public class SVODDescriptor : VolumeDescriptor
+ {
+ ///
+ /// Volume descriptor size (Should be 0x24)
+ ///
+ public byte VolumeDescriptorSize { get; set; }
+
+ ///
+ /// Block Cache Element Count
+ ///
+ public byte BlockCacheElementCount { get; set; }
+
+ ///
+ /// Worker Thread Processor
+ ///
+ public byte WorkerThreadProcessor { get; set; }
+
+ ///
+ /// Worker Thread Priority
+ ///
+ public byte WorkerThreadPriority { get; set; }
+
+ ///
+ /// Hash
+ ///
+ /// 20 bytes
+ public byte[] Hash { get; set; } = new byte[20];
+
+ ///
+ /// Data Block Count
+ ///
+ /// Big-endian, 3-byte uint24
+ public UInt24 DataBlockCount { get; set; } = new();
+
+ ///
+ /// Data Block Offset
+ ///
+ /// Big-endian, 3-byte uint24
+ public UInt24 DataBlockOffset { get; set; } = new();
+
+ ///
+ /// Padding, should be zeroed
+ ///
+ /// 5 bytes
+ public byte[] Padding { get; set; } = new byte[5];
+ }
+}
diff --git a/SabreTools.Data.Models/STFS/Signature.cs b/SabreTools.Data.Models/STFS/Signature.cs
new file mode 100644
index 00000000..efd795f9
--- /dev/null
+++ b/SabreTools.Data.Models/STFS/Signature.cs
@@ -0,0 +1,13 @@
+using System.Collections.Generic;
+
+namespace SabreTools.Data.Models.STFS
+{
+ ///
+ /// STFS Signature, differs for remotely signed (PIRS/LIVE) and console signed ("CON ") formats
+ ///
+ ///
+ public abstract class Signature
+ {
+ // Filled in by child class, MicrosoftSignature or ConsoleSignature
+ }
+}
diff --git a/SabreTools.Data.Models/STFS/Volume.cs b/SabreTools.Data.Models/STFS/Volume.cs
new file mode 100644
index 00000000..968a32e7
--- /dev/null
+++ b/SabreTools.Data.Models/STFS/Volume.cs
@@ -0,0 +1,37 @@
+using System.Collections.Generic;
+
+namespace SabreTools.Data.Models.STFS
+{
+ ///
+ /// Secure Transacted File System, used by Xbox 360
+ /// There are three formats: "LIVE", "PIRS", "CON "
+ /// LIVE/PIRS are read-only signed by Microsoft, "CON " is read/write signed by console
+ /// LIVE files are only distributed via Xbox Live, PIRS can be found elsewhere (e.g. system updates)
+ ///
+ ///
+ public class Volume
+ {
+ ///
+ /// STFS Header data
+ /// Should be 0xA000 bytes (10 blocks)
+ ///
+ public Header Header { get; set; } = new();
+
+ ///
+ /// An STFS volume contains blocks dedicated to integrity hashes
+ /// Each 4096-byte block has 170 integrity hashes for other blocks
+ /// A new hash table block exists after every 170 data blocks
+ /// i.e. First hash block is at 0x0B000 then next is at 0xB7000
+ ///
+ /// Reader does not fill this in yet
+ public HashTable[]? HashTables { get; set; }
+
+ ///
+ /// Data in the STFS, arranged in blocks of 4096-bytes
+ /// The Hash Table Blocks are interleaved, and ignored when numbering
+ /// i.e. Block 170 is not adjacent to Block 171
+ ///
+ /// Too large to read into memory, left in model for posterity
+ public byte[]? Data { get; set; }
+ }
+}
diff --git a/SabreTools.Data.Models/STFS/VolumeDescriptor.cs b/SabreTools.Data.Models/STFS/VolumeDescriptor.cs
new file mode 100644
index 00000000..d3e95d24
--- /dev/null
+++ b/SabreTools.Data.Models/STFS/VolumeDescriptor.cs
@@ -0,0 +1,13 @@
+using System.Collections.Generic;
+
+namespace SabreTools.Data.Models.STFS
+{
+ ///
+ /// STFS Volume Descriptor, differs for STFS and SVOD packages
+ ///
+ ///
+ public abstract class VolumeDescriptor
+ {
+ // Filled in by child class, SFTSDescriptor or SVODDescriptor
+ }
+}
diff --git a/SabreTools.Data.Models/XenonExecutable/Executable.cs b/SabreTools.Data.Models/XenonExecutable/Executable.cs
index 9f08bb90..4b6b0f27 100644
--- a/SabreTools.Data.Models/XenonExecutable/Executable.cs
+++ b/SabreTools.Data.Models/XenonExecutable/Executable.cs
@@ -5,7 +5,7 @@ namespace SabreTools.Data.Models.XenonExecutable
/// It is based on PE format and is PPC architecutre (therefore Big-Endian)
/// During alpha stage, Xenon was a slightly modified Apple Power Mac G5
/// Early (Before March 2005) builds used pure PE-formatted executables, June 2005 XDK began requiring XEX-format
- /// Early (August 2005 and earlier) XEX-format images (XEX0, XEX?, XEX-, XEX1) are not supported.
+ /// Early (August 2005 and earlier) XEX-format images (XEX0, XEX?, XEX-, XEX%, XEX1) are not supported.
///
///
///
diff --git a/SabreTools.Data.Models/XenonExecutable/Header.cs b/SabreTools.Data.Models/XenonExecutable/Header.cs
index 92d0c345..9fb284ea 100644
--- a/SabreTools.Data.Models/XenonExecutable/Header.cs
+++ b/SabreTools.Data.Models/XenonExecutable/Header.cs
@@ -9,7 +9,7 @@ namespace SabreTools.Data.Models.XenonExecutable
{
///
/// "XEX2" is the only supported magic identifier string
- /// "XEX0", "XEX-", "XEX?", and "XEX1" files are not supported, and are only found in early pre-production builds
+ /// "XEX0", "XEX-", "XEX?", "XEX%", and "XEX1" files are not supported, and are only found in early pre-production builds
///
public byte[] MagicNumber { get; set; } = new byte[4];
diff --git a/SabreTools.Serialization.Readers.Test/STFSTests.cs b/SabreTools.Serialization.Readers.Test/STFSTests.cs
new file mode 100644
index 00000000..2f66e1ab
--- /dev/null
+++ b/SabreTools.Serialization.Readers.Test/STFSTests.cs
@@ -0,0 +1,72 @@
+using System.IO;
+using System.Linq;
+using Xunit;
+
+namespace SabreTools.Serialization.Readers.Test
+{
+ public class STFSTests
+ {
+ [Fact]
+ public void NullArray_Null()
+ {
+ byte[]? data = null;
+ int offset = 0;
+ var deserializer = new STFS();
+
+ var actual = deserializer.Deserialize(data, offset);
+ Assert.Null(actual);
+ }
+
+ [Fact]
+ public void EmptyArray_Null()
+ {
+ byte[]? data = [];
+ int offset = 0;
+ var deserializer = new STFS();
+
+ var actual = deserializer.Deserialize(data, offset);
+ Assert.Null(actual);
+ }
+
+ [Fact]
+ public void InvalidArray_Null()
+ {
+ byte[]? data = [.. Enumerable.Repeat(0xFF, 1024)];
+ int offset = 0;
+ var deserializer = new STFS();
+
+ var actual = deserializer.Deserialize(data, offset);
+ Assert.Null(actual);
+ }
+
+ [Fact]
+ public void NullStream_Null()
+ {
+ Stream? data = null;
+ var deserializer = new STFS();
+
+ var actual = deserializer.Deserialize(data);
+ Assert.Null(actual);
+ }
+
+ [Fact]
+ public void EmptyStream_Null()
+ {
+ Stream? data = new MemoryStream([]);
+ var deserializer = new STFS();
+
+ var actual = deserializer.Deserialize(data);
+ Assert.Null(actual);
+ }
+
+ [Fact]
+ public void InvalidStream_Null()
+ {
+ Stream? data = new MemoryStream([.. Enumerable.Repeat(0xFF, 1024)]);
+ var deserializer = new STFS();
+
+ var actual = deserializer.Deserialize(data);
+ Assert.Null(actual);
+ }
+ }
+}
diff --git a/SabreTools.Serialization.Readers/STFS.cs b/SabreTools.Serialization.Readers/STFS.cs
new file mode 100644
index 00000000..8e778874
--- /dev/null
+++ b/SabreTools.Serialization.Readers/STFS.cs
@@ -0,0 +1,228 @@
+using System.Collections.Generic;
+using System.IO;
+using SabreTools.Data.Models.STFS;
+using SabreTools.IO.Extensions;
+using SabreTools.Numerics;
+using SabreTools.Numerics.Extensions;
+
+namespace SabreTools.Serialization.Readers
+{
+ public class STFS : BaseBinaryReader
+ {
+ ///
+ public override Volume? Deserialize(Stream? data)
+ {
+ // If the data is invalid
+ if (data is null || !data.CanRead)
+ return null;
+
+ // Simple check for a valid stream length
+ if (Constants.StandardHeaderSize > data.Length - data.Position)
+ return null;
+
+ try
+ {
+ // Cache the current offset
+ long initialOffset = data.Position;
+
+ // Create a new Volume to fill
+ var volume = new Volume();
+
+ // Read and validate the header
+ var header = ParseHeader(data);
+ if (header is null)
+ return null;
+
+ volume.Header = header;
+
+ // TODO: Parse the hash table blocks
+
+ // Don't parse the data blocks into memory
+
+ return volume;
+ }
+ catch
+ {
+ // Ignore the actual error
+ return null;
+ }
+ }
+
+ ///
+ /// Parse a Stream into a Header
+ ///
+ /// Stream to parse
+ /// Filled Header on success, null on error
+ public static Header? ParseHeader(Stream data)
+ {
+ var obj = new Header();
+
+ obj.MagicBytes = data.ReadBytes(4);
+ var signature = System.Text.Encoding.ASCII.GetString(obj.MagicBytes);
+ bool remoteSigned = signature.Equals(Constants.MagicStringLIVE) | signature.Equals(Constants.MagicStringPIRS);
+ if (!remoteSigned && !signature.Equals(Constants.MagicStringCON))
+ return null;
+
+ obj.Signature = ParseSignature(data, remoteSigned);
+
+ obj.LicensingData = ParseLicensingData(data);
+
+ obj.HeaderHash = data.ReadBytes(20);
+ obj.HeaderSize = data.ReadUInt32BigEndian();
+ obj.ContentType = data.ReadInt32BigEndian();
+ obj.MetadataVersion = data.ReadInt32BigEndian();
+ obj.ContentSize = data.ReadInt64BigEndian();
+ obj.MediaID = data.ReadUInt32BigEndian();
+ obj.Version = data.ReadInt32BigEndian();
+ obj.BaseVersion = data.ReadInt32BigEndian();
+ obj.TitleID = data.ReadUInt32BigEndian();
+ obj.Platform = data.ReadByteValue();
+ obj.ExecutableType = data.ReadByteValue();
+ obj.DiscNumber = data.ReadByteValue();
+ obj.DiscInSet = data.ReadByteValue();
+ obj.SaveGameID = data.ReadUInt32BigEndian();
+ obj.ConsoleID = data.ReadBytes(5);
+ obj.ProfileID = data.ReadBytes(8);
+
+ // Peek forward to read whether VolumeDescriptor is SVOD or STFS
+ byte[] peeked = data.PeekBytes(52);
+ bool svod = peeked[51] == 0x01;
+ obj.VolumeDescriptor = ParseVolumeDescriptor(data, svod);
+
+ obj.DataFileCount = data.ReadInt32BigEndian();
+ obj.DataFileCombinedSize = data.ReadInt64BigEndian();
+ obj.DescriptorType = data.ReadUInt32BigEndian();
+ obj.Reserved = data.ReadUInt32BigEndian();
+
+ if (obj.MetadataVersion == 2)
+ {
+ obj.SeriesID = data.ReadBytes(16);
+ obj.SeasonID = data.ReadBytes(16);
+ obj.SeasonNumber = data.ReadInt16BigEndian();
+ obj.EpisodeNumber = data.ReadInt16BigEndian();
+ obj.Padding = data.ReadBytes(40);
+ }
+ else
+ {
+ obj.Padding = data.ReadBytes(76);
+ }
+
+ obj.DeviceID = data.ReadBytes(20);
+ obj.DisplayName = data.ReadBytes(2304);
+ obj.DisplayDescription = data.ReadBytes(2304);
+ obj.PublisherName = data.ReadBytes(128);
+ obj.TitleName = data.ReadBytes(128);
+ obj.TransferFlags = data.ReadByteValue();
+ obj.ThumbnailImageSize = data.ReadInt32BigEndian();
+ obj.TitleThumbnailImageSize = data.ReadInt32BigEndian();
+
+ if (obj.MetadataVersion == 2)
+ {
+ obj.ThumbnailImage = data.ReadBytes(0x3D00);
+ obj.AdditionalDisplayNames = data.ReadBytes(768);
+ obj.TitleThumbnailImage = data.ReadBytes(0x3D00);
+ obj.AdditionalDisplayDescriptions = data.ReadBytes(768);
+ }
+ else
+ {
+ obj.ThumbnailImage = data.ReadBytes(0x4000);
+ obj.TitleThumbnailImage = data.ReadBytes(0x4000);
+ }
+
+ return obj;
+ }
+
+ ///
+ /// Parse a Stream into a Signature
+ ///
+ /// Stream to parse
+ /// Filled Signature
+ public static Signature ParseSignature(Stream data, bool remoteSigned)
+ {
+ if (remoteSigned)
+ {
+ var obj = new MicrosoftSignature();
+
+ obj.PackageSignature = data.ReadBytes(256);
+ obj.Padding = data.ReadBytes(296);
+
+ return obj;
+ }
+ else
+ {
+ var obj = new ConsoleSignature();
+
+ obj.CertificateSize = data.ReadUInt16BigEndian();
+ obj.ConsoleID = data.ReadBytes(5);
+ obj.PartNumber = data.ReadBytes(20);
+ obj.ConsoleType = data.ReadByteValue();
+ obj.CertificateDate = data.ReadBytes(8);
+ obj.PublicExponent = data.ReadBytes(4);
+ obj.PublicModulus = data.ReadBytes(128);
+ obj.CertificateSignature = data.ReadBytes(256);
+ obj.Signature = data.ReadBytes(128);
+
+ return obj;
+ }
+ }
+
+ ///
+ /// Parse a Stream into an array of LicenseEntry
+ ///
+ /// Stream to parse
+ /// Filled array of LicenseEntry
+ public static LicenseEntry[] ParseLicensingData(Stream data)
+ {
+ var obj = new LicenseEntry[16];
+
+ for (int i = 0; i < 16; i++)
+ {
+ obj[i] = new LicenseEntry();
+ obj[i].LicenseID = data.ReadInt64BigEndian();
+ obj[i].LicenseBits = data.ReadInt32BigEndian();
+ obj[i].LicenseFlags = data.ReadInt32BigEndian();
+ }
+
+ return obj;
+ }
+
+ ///
+ /// Parse a Stream into a VolumeDescriptor
+ ///
+ /// Stream to parse
+ /// Filled VolumeDescriptor
+ public static VolumeDescriptor ParseVolumeDescriptor(Stream data, bool svod)
+ {
+ if (svod)
+ {
+ var obj = new SVODDescriptor();
+
+ obj.VolumeDescriptorSize = data.ReadByteValue();
+ obj.BlockCacheElementCount = data.ReadByteValue();
+ obj.WorkerThreadProcessor = data.ReadByteValue();
+ obj.WorkerThreadPriority = data.ReadByteValue();
+ obj.Hash = data.ReadBytes(20);
+ obj.DataBlockCount = data.ReadUInt24BigEndian();
+ obj.DataBlockOffset = data.ReadUInt24BigEndian();
+ obj.Hash = data.ReadBytes(5);
+
+ return obj;
+ }
+ else
+ {
+ var obj = new STFSDescriptor();
+
+ obj.VolumeDescriptorSize = data.ReadByteValue();
+ obj.Reserved = data.ReadByteValue();
+ obj.BlockSeparation = data.ReadByteValue();
+ obj.FileTableBlockCount = data.ReadInt16BigEndian();
+ obj.FileTableBlockNumber = data.ReadInt24BigEndian();
+ obj.TopHashTableHash = data.ReadBytes(20);
+ obj.TotalAllocatedBlockCount = data.ReadInt32BigEndian();
+ obj.TotalUnallocatedBlockCount = data.ReadInt32BigEndian();
+
+ return obj;
+ }
+ }
+ }
+}
diff --git a/SabreTools.Wrappers.Test/STFSTests.cs b/SabreTools.Wrappers.Test/STFSTests.cs
new file mode 100644
index 00000000..5159ec9c
--- /dev/null
+++ b/SabreTools.Wrappers.Test/STFSTests.cs
@@ -0,0 +1,60 @@
+using System.IO;
+using System.Linq;
+using Xunit;
+
+namespace SabreTools.Wrappers.Test
+{
+ public class STFSTests
+ {
+ [Fact]
+ public void NullArray_Null()
+ {
+ byte[]? data = null;
+ int offset = 0;
+ var actual = STFS.Create(data, offset);
+ Assert.Null(actual);
+ }
+
+ [Fact]
+ public void EmptyArray_Null()
+ {
+ byte[]? data = [];
+ int offset = 0;
+ var actual = STFS.Create(data, offset);
+ Assert.Null(actual);
+ }
+
+ [Fact]
+ public void InvalidArray_Null()
+ {
+ byte[]? data = [.. Enumerable.Repeat(0xFF, 1024)];
+ int offset = 0;
+ var actual = STFS.Create(data, offset);
+ Assert.Null(actual);
+ }
+
+ [Fact]
+ public void NullStream_Null()
+ {
+ Stream? data = null;
+ var actual = STFS.Create(data);
+ Assert.Null(actual);
+ }
+
+ [Fact]
+ public void EmptyStream_Null()
+ {
+ Stream? data = new MemoryStream([]);
+ var actual = STFS.Create(data);
+ Assert.Null(actual);
+ }
+
+ [Fact]
+ public void InvalidStream_Null()
+ {
+ Stream? data = new MemoryStream([.. Enumerable.Repeat(0xFF, 1024)]);
+ var actual = STFS.Create(data);
+ Assert.Null(actual);
+ }
+ }
+}
diff --git a/SabreTools.Wrappers/STFS.Printing.cs b/SabreTools.Wrappers/STFS.Printing.cs
new file mode 100644
index 00000000..0cacb82b
--- /dev/null
+++ b/SabreTools.Wrappers/STFS.Printing.cs
@@ -0,0 +1,270 @@
+using System;
+using System.Text;
+using SabreTools.Data.Models.STFS;
+using SabreTools.Numerics;
+using SabreTools.Text.Extensions;
+
+namespace SabreTools.Wrappers
+{
+ public partial class STFS : IPrintable
+ {
+#if NETCOREAPP
+ ///
+ public string ExportJSON() => System.Text.Json.JsonSerializer.Serialize(Model, _jsonSerializerOptions);
+#endif
+
+ ///
+ public void PrintInformation(StringBuilder builder)
+ {
+ builder.AppendLine("Secure Transacted File System Information:");
+ builder.AppendLine("-------------------------");
+ builder.AppendLine();
+
+ Print(builder, Model.Header);
+
+ // Do not print info about Hash Table Blocks or Data Blocks
+ }
+
+ protected static void Print(StringBuilder builder, Header header)
+ {
+ builder.AppendLine(header.MagicBytes, " Magic Bytes");
+
+ builder.AppendLine();
+
+ Print(builder, header.Signature);
+
+ Print(builder, header.LicensingData);
+
+ builder.AppendLine(header.HeaderHash, " Header Hash");
+ builder.AppendLine(header.HeaderSize, " Header Size");
+ builder.AppendLine(header.ContentType, " Content Type"); // See Enums.ContentType
+ builder.AppendLine(header.MetadataVersion, " Metadata Version");
+ builder.AppendLine(header.ContentSize, " Content Size");
+ builder.AppendLine(header.MediaID, " Media ID");
+ builder.AppendLine(header.Version, " Version");
+ builder.AppendLine(header.BaseVersion, " Base Version");
+ builder.AppendLine(header.TitleID, " Title ID");
+ builder.AppendLine(header.Platform, " Platform");
+ builder.AppendLine(header.ExecutableType, " Executable Type");
+ builder.AppendLine(header.DiscNumber, " Disc Number");
+ builder.AppendLine(header.DiscInSet, " Disc in Set");
+ builder.AppendLine(header.SaveGameID, " Save Game ID");
+ builder.AppendLine(header.ConsoleID, " Console ID");
+ builder.AppendLine(header.ProfileID, " Profile ID");
+
+ builder.AppendLine();
+
+ Print(builder, header.VolumeDescriptor);
+
+ builder.AppendLine(header.DataFileCount, " Data File Count");
+ builder.AppendLine(header.DataFileCombinedSize, " Data File Combined Size");
+ builder.AppendLine(header.DescriptorType, " Descriptor Type");
+ builder.AppendLine(header.Reserved, " Reserved");
+
+ if (header.MetadataVersion == 2)
+ {
+ builder.AppendLine(header.SeriesID, " Series ID");
+ builder.AppendLine(header.SeasonID, " Season ID");
+ builder.AppendLine(header.SeasonNumber, " Season Number");
+ builder.AppendLine(header.EpisodeNumber, " Episode Number");
+ }
+
+ if (Array.TrueForAll(header.Padding, b => b == 0))
+ builder.AppendLine("Zeroed", " Padding");
+ else
+ builder.AppendLine(header.Padding, " Padding");
+
+ builder.AppendLine(header.DeviceID, " Device ID");
+
+ if (Array.TrueForAll(header.DisplayName, b => b == 0))
+ {
+ builder.AppendLine("Zeroed", " Display Name");
+ }
+ else
+ {
+ for (int i = 0; i < 18; i++)
+ {
+ byte[] localeString = new byte[128];
+ Array.Copy(header.DisplayName, i * 128, localeString, 0, 128);
+ if (!Array.TrueForAll(localeString, b => b == 0))
+ {
+ builder.AppendLine(localeString, $" Display Name {i}");
+ builder.AppendLine(Encoding.BigEndianUnicode.GetString(localeString), $" Display Name {i} (Parsed)");
+ }
+ }
+ }
+
+ if (Array.TrueForAll(header.DisplayDescription, b => b == 0))
+ {
+ builder.AppendLine("Zeroed", " Display Description");
+ }
+ else
+ {
+ for (int i = 0; i < 18; i++)
+ {
+ byte[] localeString = new byte[128];
+ Array.Copy(header.DisplayDescription, i * 128, localeString, 0, 128);
+ if (!Array.TrueForAll(localeString, b => b == 0))
+ {
+ builder.AppendLine(localeString, $" Display Description {i}");
+ builder.AppendLine(Encoding.BigEndianUnicode.GetString(localeString), $" Display Description {i} (Parsed)");
+ }
+ }
+ }
+
+ if (Array.TrueForAll(header.PublisherName, b => b == 0))
+ {
+ builder.AppendLine("Zeroed", " Publisher Name");
+ }
+ else
+ {
+ builder.AppendLine(header.PublisherName, " Publisher Name");
+ builder.AppendLine(Encoding.BigEndianUnicode.GetString(header.PublisherName), " Publisher Name (Parsed)");
+ }
+
+ if (Array.TrueForAll(header.TitleName, b => b == 0))
+ {
+ builder.AppendLine("Zeroed", " Title Name");
+ }
+ else
+ {
+ builder.AppendLine(header.TitleName, " Title Name");
+ builder.AppendLine(Encoding.BigEndianUnicode.GetString(header.TitleName), " Title Name (Parsed)");
+ }
+
+ builder.AppendLine(header.TransferFlags, " Transfer Flags"); // See Enums.TransferFlags
+ builder.AppendLine(header.ThumbnailImageSize, " Thumbnail Image Size");
+ builder.AppendLine(header.TitleThumbnailImageSize, " Title Thumbnail Image Size");
+
+ if (header.AdditionalDisplayNames is not null)
+ {
+ for (int i = 0; i < 18; i++)
+ {
+ byte[] localeString = new byte[128];
+ Array.Copy(header.AdditionalDisplayNames, i * 128, localeString, 0, 128);
+ if (!Array.TrueForAll(localeString, b => b == 0))
+ {
+ builder.AppendLine(localeString, $" Additional Display Name {i}");
+ builder.AppendLine(Encoding.BigEndianUnicode.GetString(localeString), $" Additional Display Name {i} (Parsed)");
+ }
+ }
+ }
+ if (header.AdditionalDisplayDescriptions is not null)
+ {
+ for (int i = 0; i < 18; i++)
+ {
+ byte[] localeString = new byte[128];
+ Array.Copy(header.AdditionalDisplayDescriptions, i * 128, localeString, 0, 128);
+ if (!Array.TrueForAll(localeString, b => b == 0))
+ {
+ builder.AppendLine(localeString, $" Additional Display Description {i}");
+ builder.AppendLine(Encoding.BigEndianUnicode.GetString(localeString), $" Additional Display Description {i} (Parsed)");
+ }
+ }
+ }
+
+ builder.AppendLine();
+ }
+
+ protected static void Print(StringBuilder builder, Signature? signature)
+ {
+ builder.AppendLine(" Signature Information");
+ builder.AppendLine(" -------------------------");
+
+ if (signature is MicrosoftSignature ms)
+ {
+ builder.AppendLine(ms.PackageSignature, " Package Signature");
+ if (Array.TrueForAll(ms.Padding, b => b == 0))
+ builder.AppendLine("Zeroed", " Padding");
+ else
+ builder.AppendLine(ms.Padding, " Padding");
+ }
+ else if (signature is ConsoleSignature cs)
+ {
+ builder.AppendLine(cs.CertificateSize, " Certificate Size");
+ builder.AppendLine(cs.ConsoleID, " Console ID");
+ builder.AppendLine(cs.PartNumber, " Part Number");
+ builder.AppendLine(cs.ConsoleType, " Console Type");
+ builder.AppendLine(cs.CertificateDate, " Certificate Date");
+ builder.AppendLine(cs.PublicExponent, " Public Exponent");
+ builder.AppendLine(cs.PublicModulus, " Public Modulus");
+ builder.AppendLine(cs.CertificateSignature, " Certificate Signature");
+ builder.AppendLine(cs.Signature, " Signature");
+ }
+ else
+ {
+ builder.AppendLine(" Unknown Signature Type");
+ }
+
+ builder.AppendLine();
+ }
+
+ protected static void Print(StringBuilder builder, LicenseEntry[] ld)
+ {
+ builder.AppendLine(" Licensing Data Information");
+ builder.AppendLine(" -------------------------");
+
+ int lastLicenseData = 0;
+ for (int i = ld.Length - 1; i >= 0; i--)
+ {
+ if (ld[i].LicenseID != 0 || ld[i].LicenseBits != 0 || ld[i].LicenseFlags != 0)
+ {
+ lastLicenseData = i + 1;
+ break;
+ }
+ if (i == 0)
+ builder.AppendLine("Zeroed", " Licensing Data");
+ }
+
+ for (int i = 0; i < lastLicenseData; i++)
+ {
+ if (ld[i].LicenseID == 0 && ld[i].LicenseBits == 0 && ld[i].LicenseFlags == 0)
+ {
+ builder.AppendLine("Zeroed", $" License Entry {i}");
+ }
+ else
+ {
+ builder.AppendLine(ld[i].LicenseID, $" License Entry {i} ID");
+ builder.AppendLine(ld[i].LicenseBits, $" License Entry {i} Bits");
+ builder.AppendLine(ld[i].LicenseFlags, $" License Entry {i} Flags");
+ }
+ }
+
+ builder.AppendLine();
+ }
+
+ protected static void Print(StringBuilder builder, VolumeDescriptor? vd)
+ {
+ builder.AppendLine(" Volume Descriptor Information");
+ builder.AppendLine(" -------------------------");
+ if (vd is STFSDescriptor stfs)
+ {
+ builder.AppendLine(stfs.VolumeDescriptorSize, " Volume Descriptor Size");
+ builder.AppendLine(stfs.Reserved, " Reserved");
+ builder.AppendLine(stfs.BlockSeparation, " Block Separation");
+ builder.AppendLine(stfs.FileTableBlockCount, " File Table Block Count");
+ builder.AppendLine((uint)stfs.FileTableBlockNumber, " File Table Block Number");
+ builder.AppendLine(stfs.TopHashTableHash, " Top Hash Table Hash");
+ builder.AppendLine(stfs.TotalAllocatedBlockCount, " Total Allocated Block Count");
+ builder.AppendLine(stfs.TotalUnallocatedBlockCount, " Total Unallocated Block Count");
+ }
+ else if (vd is SVODDescriptor svod)
+ {
+ builder.AppendLine(svod.VolumeDescriptorSize, " Volume Descriptor Size");
+ builder.AppendLine(svod.BlockCacheElementCount, " Block Cache Element Count");
+ builder.AppendLine(svod.WorkerThreadProcessor, " Worker Thread Processor");
+ builder.AppendLine(svod.WorkerThreadPriority, " Worker Thread Priority");
+ builder.AppendLine(svod.Hash, " Hash");
+ builder.AppendLine((uint)svod.DataBlockCount, " Data Block Count");
+ builder.AppendLine((uint)svod.DataBlockOffset, " Data Block Offset");
+ builder.AppendLine(svod.Padding, " Padding");
+ }
+ else
+ {
+ builder.AppendLine(" Unknown Volume Descriptor Type");
+ }
+
+ builder.AppendLine();
+ }
+ }
+}
diff --git a/SabreTools.Wrappers/STFS.cs b/SabreTools.Wrappers/STFS.cs
new file mode 100644
index 00000000..7ac5b656
--- /dev/null
+++ b/SabreTools.Wrappers/STFS.cs
@@ -0,0 +1,98 @@
+using System.Collections.Generic;
+using System.IO;
+using SabreTools.Data.Models.STFS;
+
+namespace SabreTools.Wrappers
+{
+ public partial class STFS : WrapperBase
+ {
+ #region Descriptive Properties
+
+ ///
+ public override string DescriptionString => "Secure Transacted File System";
+
+ #endregion
+
+ #region Extension Properties
+
+ ///
+ public Header Header => Model.Header;
+
+ #endregion
+
+ #region Constructors
+
+ ///
+ public STFS(Volume model, byte[] data) : base(model, data) { }
+
+ ///
+ public STFS(Volume model, byte[] data, int offset) : base(model, data, offset) { }
+
+ ///
+ public STFS(Volume model, byte[] data, int offset, int length) : base(model, data, offset, length) { }
+
+ ///
+ public STFS(Volume model, Stream data) : base(model, data) { }
+
+ ///
+ public STFS(Volume model, Stream data, long offset) : base(model, data, offset) { }
+
+ ///
+ public STFS(Volume model, Stream data, long offset, long length) : base(model, data, offset, length) { }
+
+ #endregion
+
+ #region Static Constructors
+
+ ///
+ /// Create an STFS Volume from a byte array and offset
+ ///
+ /// Byte array representing the STFS Volume
+ /// Offset within the array to parse
+ /// An STFS Volume wrapper on success, null on failure
+ public static STFS? Create(byte[]? data, int offset)
+ {
+ // If the data is invalid
+ if (data is null || data.Length == 0)
+ return null;
+
+ // If the offset is out of bounds
+ if (offset < 0 || offset >= data.Length)
+ return null;
+
+ // Create a memory stream and use that
+ var dataStream = new MemoryStream(data, offset, data.Length - offset);
+ return Create(dataStream);
+ }
+
+ ///
+ /// Create an STFS Volume from a Stream
+ ///
+ /// Stream representing the STFS Volume
+ /// An STFS Volume wrapper on success, null on failure
+ public static STFS? Create(Stream? data)
+ {
+ // If the data is invalid
+ if (data is null || !data.CanRead)
+ return null;
+
+ try
+ {
+ // Cache the current offset
+ long currentOffset = data.Position;
+
+ var model = new Serialization.Readers.STFS().Deserialize(data);
+ if (model is null)
+ return null;
+
+ return new STFS(model, data, currentOffset);
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/SabreTools.Wrappers/WrapperFactory.cs b/SabreTools.Wrappers/WrapperFactory.cs
index 20d848d1..82e5358a 100644
--- a/SabreTools.Wrappers/WrapperFactory.cs
+++ b/SabreTools.Wrappers/WrapperFactory.cs
@@ -61,6 +61,7 @@ namespace SabreTools.Wrappers
WrapperType.SkuSis => SkuSis.Create(data),
WrapperType.SFFS => SFFS.Create(data),
WrapperType.SGA => SGA.Create(data),
+ WrapperType.STFS => STFS.Create(data),
WrapperType.TapeArchive => TapeArchive.Create(data),
WrapperType.VBSP => VBSP.Create(data),
WrapperType.VPK => VPK.Create(data),
@@ -781,6 +782,22 @@ namespace SabreTools.Wrappers
#endregion
+ #region STFS
+
+ // LIVE
+ if (magic.StartsWith(Data.Models.STFS.Constants.MagicBytesLIVE))
+ return WrapperType.STFS;
+
+ // PIRS
+ if (magic.StartsWith(Data.Models.STFS.Constants.MagicBytesPIRS))
+ return WrapperType.STFS;
+
+ // "CON "
+ if (magic.StartsWith(Data.Models.STFS.Constants.MagicBytesCON))
+ return WrapperType.STFS;
+
+ #endregion
+
// TODO: Use constants from Models here
#region TapeArchive
diff --git a/SabreTools.Wrappers/WrapperType.cs b/SabreTools.Wrappers/WrapperType.cs
index 05868981..cf44f759 100644
--- a/SabreTools.Wrappers/WrapperType.cs
+++ b/SabreTools.Wrappers/WrapperType.cs
@@ -247,6 +247,11 @@ namespace SabreTools.Wrappers
///
SkuSis,
+ ///
+ /// Secure Transacted File System
+ ///
+ STFS,
+
///
/// Tape archive
///
From 574b993985832a21fb90fef7c68778c83bc31fad Mon Sep 17 00:00:00 2001
From: Matt Nadareski
Date: Tue, 7 Apr 2026 22:20:14 -0400
Subject: [PATCH 2/2] Editorconfig cleanup of STFS
---
.../STFS/ConsoleSignature.cs | 2 -
SabreTools.Data.Models/STFS/Constants.cs | 2 -
SabreTools.Data.Models/STFS/HashTable.cs | 4 +-
SabreTools.Data.Models/STFS/HashTableEntry.cs | 1 -
SabreTools.Data.Models/STFS/Header.cs | 4 +-
SabreTools.Data.Models/STFS/LicenseEntry.cs | 2 -
.../STFS/MicrosoftSignature.cs | 2 -
SabreTools.Data.Models/STFS/STFSDescriptor.cs | 1 -
SabreTools.Data.Models/STFS/SVODDescriptor.cs | 1 -
SabreTools.Data.Models/STFS/Signature.cs | 2 -
SabreTools.Data.Models/STFS/Volume.cs | 2 -
.../STFS/VolumeDescriptor.cs | 2 -
SabreTools.Serialization.Readers/STFS.cs | 454 +++++++++---------
SabreTools.Wrappers/STFS.Printing.cs | 9 +-
SabreTools.Wrappers/STFS.cs | 1 -
15 files changed, 233 insertions(+), 256 deletions(-)
diff --git a/SabreTools.Data.Models/STFS/ConsoleSignature.cs b/SabreTools.Data.Models/STFS/ConsoleSignature.cs
index b590a8ab..ecb0bc40 100644
--- a/SabreTools.Data.Models/STFS/ConsoleSignature.cs
+++ b/SabreTools.Data.Models/STFS/ConsoleSignature.cs
@@ -1,5 +1,3 @@
-using System.Collections.Generic;
-
namespace SabreTools.Data.Models.STFS
{
///
diff --git a/SabreTools.Data.Models/STFS/Constants.cs b/SabreTools.Data.Models/STFS/Constants.cs
index 302bb93a..2e719722 100644
--- a/SabreTools.Data.Models/STFS/Constants.cs
+++ b/SabreTools.Data.Models/STFS/Constants.cs
@@ -1,5 +1,3 @@
-using System.Collections.Generic;
-
namespace SabreTools.Data.Models.STFS
{
///
diff --git a/SabreTools.Data.Models/STFS/HashTable.cs b/SabreTools.Data.Models/STFS/HashTable.cs
index 6acf27c5..ea6d1992 100644
--- a/SabreTools.Data.Models/STFS/HashTable.cs
+++ b/SabreTools.Data.Models/STFS/HashTable.cs
@@ -1,9 +1,7 @@
-using System.Collections.Generic;
-
namespace SabreTools.Data.Models.STFS
{
///
- /// STFS Hash Table in a Hash Table Block
+ /// STFS Hash Table in a Hash Table Block
///
///
public class HashTable
diff --git a/SabreTools.Data.Models/STFS/HashTableEntry.cs b/SabreTools.Data.Models/STFS/HashTableEntry.cs
index b92888db..065082b7 100644
--- a/SabreTools.Data.Models/STFS/HashTableEntry.cs
+++ b/SabreTools.Data.Models/STFS/HashTableEntry.cs
@@ -1,4 +1,3 @@
-using System.Collections.Generic;
using SabreTools.Numerics;
namespace SabreTools.Data.Models.STFS
diff --git a/SabreTools.Data.Models/STFS/Header.cs b/SabreTools.Data.Models/STFS/Header.cs
index 1d88fc2f..6ff6cc94 100644
--- a/SabreTools.Data.Models/STFS/Header.cs
+++ b/SabreTools.Data.Models/STFS/Header.cs
@@ -1,5 +1,3 @@
-using System.Collections.Generic;
-
namespace SabreTools.Data.Models.STFS
{
///
@@ -30,7 +28,7 @@ namespace SabreTools.Data.Models.STFS
public LicenseEntry[] LicensingData { get; set; } = new LicenseEntry[16];
///
- /// SHA-1 Integrity Hash of the header (from ContentType/0x344 to first hash table)
+ /// SHA-1 Integrity Hash of the header (from ContentType/0x344 to first hash table)
///
/// 20 bytes
public byte[] HeaderHash { get; set; } = new byte[20];
diff --git a/SabreTools.Data.Models/STFS/LicenseEntry.cs b/SabreTools.Data.Models/STFS/LicenseEntry.cs
index 93f97ae7..f6149219 100644
--- a/SabreTools.Data.Models/STFS/LicenseEntry.cs
+++ b/SabreTools.Data.Models/STFS/LicenseEntry.cs
@@ -1,5 +1,3 @@
-using System.Collections.Generic;
-
namespace SabreTools.Data.Models.STFS
{
///
diff --git a/SabreTools.Data.Models/STFS/MicrosoftSignature.cs b/SabreTools.Data.Models/STFS/MicrosoftSignature.cs
index 9bdab7c7..3fa9c61f 100644
--- a/SabreTools.Data.Models/STFS/MicrosoftSignature.cs
+++ b/SabreTools.Data.Models/STFS/MicrosoftSignature.cs
@@ -1,5 +1,3 @@
-using System.Collections.Generic;
-
namespace SabreTools.Data.Models.STFS
{
///
diff --git a/SabreTools.Data.Models/STFS/STFSDescriptor.cs b/SabreTools.Data.Models/STFS/STFSDescriptor.cs
index 8a79fcd0..8678d540 100644
--- a/SabreTools.Data.Models/STFS/STFSDescriptor.cs
+++ b/SabreTools.Data.Models/STFS/STFSDescriptor.cs
@@ -1,4 +1,3 @@
-using System.Collections.Generic;
using SabreTools.Numerics;
namespace SabreTools.Data.Models.STFS
diff --git a/SabreTools.Data.Models/STFS/SVODDescriptor.cs b/SabreTools.Data.Models/STFS/SVODDescriptor.cs
index f543d26f..a6832b83 100644
--- a/SabreTools.Data.Models/STFS/SVODDescriptor.cs
+++ b/SabreTools.Data.Models/STFS/SVODDescriptor.cs
@@ -1,4 +1,3 @@
-using System.Collections.Generic;
using SabreTools.Numerics;
namespace SabreTools.Data.Models.STFS
diff --git a/SabreTools.Data.Models/STFS/Signature.cs b/SabreTools.Data.Models/STFS/Signature.cs
index efd795f9..8ed435d6 100644
--- a/SabreTools.Data.Models/STFS/Signature.cs
+++ b/SabreTools.Data.Models/STFS/Signature.cs
@@ -1,5 +1,3 @@
-using System.Collections.Generic;
-
namespace SabreTools.Data.Models.STFS
{
///
diff --git a/SabreTools.Data.Models/STFS/Volume.cs b/SabreTools.Data.Models/STFS/Volume.cs
index 968a32e7..543602f5 100644
--- a/SabreTools.Data.Models/STFS/Volume.cs
+++ b/SabreTools.Data.Models/STFS/Volume.cs
@@ -1,5 +1,3 @@
-using System.Collections.Generic;
-
namespace SabreTools.Data.Models.STFS
{
///
diff --git a/SabreTools.Data.Models/STFS/VolumeDescriptor.cs b/SabreTools.Data.Models/STFS/VolumeDescriptor.cs
index d3e95d24..54fe928d 100644
--- a/SabreTools.Data.Models/STFS/VolumeDescriptor.cs
+++ b/SabreTools.Data.Models/STFS/VolumeDescriptor.cs
@@ -1,5 +1,3 @@
-using System.Collections.Generic;
-
namespace SabreTools.Data.Models.STFS
{
///
diff --git a/SabreTools.Serialization.Readers/STFS.cs b/SabreTools.Serialization.Readers/STFS.cs
index 8e778874..9597e517 100644
--- a/SabreTools.Serialization.Readers/STFS.cs
+++ b/SabreTools.Serialization.Readers/STFS.cs
@@ -1,228 +1,226 @@
-using System.Collections.Generic;
-using System.IO;
-using SabreTools.Data.Models.STFS;
-using SabreTools.IO.Extensions;
-using SabreTools.Numerics;
-using SabreTools.Numerics.Extensions;
-
-namespace SabreTools.Serialization.Readers
-{
- public class STFS : BaseBinaryReader
- {
- ///
- public override Volume? Deserialize(Stream? data)
- {
- // If the data is invalid
- if (data is null || !data.CanRead)
- return null;
-
- // Simple check for a valid stream length
- if (Constants.StandardHeaderSize > data.Length - data.Position)
- return null;
-
- try
- {
- // Cache the current offset
- long initialOffset = data.Position;
-
- // Create a new Volume to fill
- var volume = new Volume();
-
- // Read and validate the header
- var header = ParseHeader(data);
- if (header is null)
- return null;
-
- volume.Header = header;
-
- // TODO: Parse the hash table blocks
-
- // Don't parse the data blocks into memory
-
- return volume;
- }
- catch
- {
- // Ignore the actual error
- return null;
- }
- }
-
- ///
- /// Parse a Stream into a Header
- ///
- /// Stream to parse
- /// Filled Header on success, null on error
- public static Header? ParseHeader(Stream data)
- {
- var obj = new Header();
-
- obj.MagicBytes = data.ReadBytes(4);
- var signature = System.Text.Encoding.ASCII.GetString(obj.MagicBytes);
- bool remoteSigned = signature.Equals(Constants.MagicStringLIVE) | signature.Equals(Constants.MagicStringPIRS);
- if (!remoteSigned && !signature.Equals(Constants.MagicStringCON))
- return null;
-
- obj.Signature = ParseSignature(data, remoteSigned);
-
- obj.LicensingData = ParseLicensingData(data);
-
- obj.HeaderHash = data.ReadBytes(20);
- obj.HeaderSize = data.ReadUInt32BigEndian();
- obj.ContentType = data.ReadInt32BigEndian();
- obj.MetadataVersion = data.ReadInt32BigEndian();
- obj.ContentSize = data.ReadInt64BigEndian();
- obj.MediaID = data.ReadUInt32BigEndian();
- obj.Version = data.ReadInt32BigEndian();
- obj.BaseVersion = data.ReadInt32BigEndian();
- obj.TitleID = data.ReadUInt32BigEndian();
- obj.Platform = data.ReadByteValue();
- obj.ExecutableType = data.ReadByteValue();
- obj.DiscNumber = data.ReadByteValue();
- obj.DiscInSet = data.ReadByteValue();
- obj.SaveGameID = data.ReadUInt32BigEndian();
- obj.ConsoleID = data.ReadBytes(5);
- obj.ProfileID = data.ReadBytes(8);
-
- // Peek forward to read whether VolumeDescriptor is SVOD or STFS
- byte[] peeked = data.PeekBytes(52);
- bool svod = peeked[51] == 0x01;
- obj.VolumeDescriptor = ParseVolumeDescriptor(data, svod);
-
- obj.DataFileCount = data.ReadInt32BigEndian();
- obj.DataFileCombinedSize = data.ReadInt64BigEndian();
- obj.DescriptorType = data.ReadUInt32BigEndian();
- obj.Reserved = data.ReadUInt32BigEndian();
-
- if (obj.MetadataVersion == 2)
- {
- obj.SeriesID = data.ReadBytes(16);
- obj.SeasonID = data.ReadBytes(16);
- obj.SeasonNumber = data.ReadInt16BigEndian();
- obj.EpisodeNumber = data.ReadInt16BigEndian();
- obj.Padding = data.ReadBytes(40);
- }
- else
- {
- obj.Padding = data.ReadBytes(76);
- }
-
- obj.DeviceID = data.ReadBytes(20);
- obj.DisplayName = data.ReadBytes(2304);
- obj.DisplayDescription = data.ReadBytes(2304);
- obj.PublisherName = data.ReadBytes(128);
- obj.TitleName = data.ReadBytes(128);
- obj.TransferFlags = data.ReadByteValue();
- obj.ThumbnailImageSize = data.ReadInt32BigEndian();
- obj.TitleThumbnailImageSize = data.ReadInt32BigEndian();
-
- if (obj.MetadataVersion == 2)
- {
- obj.ThumbnailImage = data.ReadBytes(0x3D00);
- obj.AdditionalDisplayNames = data.ReadBytes(768);
- obj.TitleThumbnailImage = data.ReadBytes(0x3D00);
- obj.AdditionalDisplayDescriptions = data.ReadBytes(768);
- }
- else
- {
- obj.ThumbnailImage = data.ReadBytes(0x4000);
- obj.TitleThumbnailImage = data.ReadBytes(0x4000);
- }
-
- return obj;
- }
-
- ///
- /// Parse a Stream into a Signature
- ///
- /// Stream to parse
- /// Filled Signature
- public static Signature ParseSignature(Stream data, bool remoteSigned)
- {
- if (remoteSigned)
- {
- var obj = new MicrosoftSignature();
-
- obj.PackageSignature = data.ReadBytes(256);
- obj.Padding = data.ReadBytes(296);
-
- return obj;
- }
- else
- {
- var obj = new ConsoleSignature();
-
- obj.CertificateSize = data.ReadUInt16BigEndian();
- obj.ConsoleID = data.ReadBytes(5);
- obj.PartNumber = data.ReadBytes(20);
- obj.ConsoleType = data.ReadByteValue();
- obj.CertificateDate = data.ReadBytes(8);
- obj.PublicExponent = data.ReadBytes(4);
- obj.PublicModulus = data.ReadBytes(128);
- obj.CertificateSignature = data.ReadBytes(256);
- obj.Signature = data.ReadBytes(128);
-
- return obj;
- }
- }
-
- ///
- /// Parse a Stream into an array of LicenseEntry
- ///
- /// Stream to parse
- /// Filled array of LicenseEntry
- public static LicenseEntry[] ParseLicensingData(Stream data)
- {
- var obj = new LicenseEntry[16];
-
- for (int i = 0; i < 16; i++)
- {
- obj[i] = new LicenseEntry();
- obj[i].LicenseID = data.ReadInt64BigEndian();
- obj[i].LicenseBits = data.ReadInt32BigEndian();
- obj[i].LicenseFlags = data.ReadInt32BigEndian();
- }
-
- return obj;
- }
-
- ///
- /// Parse a Stream into a VolumeDescriptor
- ///
- /// Stream to parse
- /// Filled VolumeDescriptor
- public static VolumeDescriptor ParseVolumeDescriptor(Stream data, bool svod)
- {
- if (svod)
- {
- var obj = new SVODDescriptor();
-
- obj.VolumeDescriptorSize = data.ReadByteValue();
- obj.BlockCacheElementCount = data.ReadByteValue();
- obj.WorkerThreadProcessor = data.ReadByteValue();
- obj.WorkerThreadPriority = data.ReadByteValue();
- obj.Hash = data.ReadBytes(20);
- obj.DataBlockCount = data.ReadUInt24BigEndian();
- obj.DataBlockOffset = data.ReadUInt24BigEndian();
- obj.Hash = data.ReadBytes(5);
-
- return obj;
- }
- else
- {
- var obj = new STFSDescriptor();
-
- obj.VolumeDescriptorSize = data.ReadByteValue();
- obj.Reserved = data.ReadByteValue();
- obj.BlockSeparation = data.ReadByteValue();
- obj.FileTableBlockCount = data.ReadInt16BigEndian();
- obj.FileTableBlockNumber = data.ReadInt24BigEndian();
- obj.TopHashTableHash = data.ReadBytes(20);
- obj.TotalAllocatedBlockCount = data.ReadInt32BigEndian();
- obj.TotalUnallocatedBlockCount = data.ReadInt32BigEndian();
-
- return obj;
- }
- }
- }
-}
+using System.IO;
+using SabreTools.Data.Models.STFS;
+using SabreTools.IO.Extensions;
+using SabreTools.Numerics.Extensions;
+
+namespace SabreTools.Serialization.Readers
+{
+ public class STFS : BaseBinaryReader
+ {
+ ///
+ public override Volume? Deserialize(Stream? data)
+ {
+ // If the data is invalid
+ if (data is null || !data.CanRead)
+ return null;
+
+ // Simple check for a valid stream length
+ if (Constants.StandardHeaderSize > data.Length - data.Position)
+ return null;
+
+ try
+ {
+ // Cache the current offset
+ long initialOffset = data.Position;
+
+ // Create a new Volume to fill
+ var volume = new Volume();
+
+ // Read and validate the header
+ var header = ParseHeader(data);
+ if (header is null)
+ return null;
+
+ volume.Header = header;
+
+ // TODO: Parse the hash table blocks
+
+ // Don't parse the data blocks into memory
+
+ return volume;
+ }
+ catch
+ {
+ // Ignore the actual error
+ return null;
+ }
+ }
+
+ ///
+ /// Parse a Stream into a Header
+ ///
+ /// Stream to parse
+ /// Filled Header on success, null on error
+ public static Header? ParseHeader(Stream data)
+ {
+ var obj = new Header();
+
+ obj.MagicBytes = data.ReadBytes(4);
+ var signature = System.Text.Encoding.ASCII.GetString(obj.MagicBytes);
+ bool remoteSigned = signature.Equals(Constants.MagicStringLIVE) | signature.Equals(Constants.MagicStringPIRS);
+ if (!remoteSigned && !signature.Equals(Constants.MagicStringCON))
+ return null;
+
+ obj.Signature = ParseSignature(data, remoteSigned);
+
+ obj.LicensingData = ParseLicensingData(data);
+
+ obj.HeaderHash = data.ReadBytes(20);
+ obj.HeaderSize = data.ReadUInt32BigEndian();
+ obj.ContentType = data.ReadInt32BigEndian();
+ obj.MetadataVersion = data.ReadInt32BigEndian();
+ obj.ContentSize = data.ReadInt64BigEndian();
+ obj.MediaID = data.ReadUInt32BigEndian();
+ obj.Version = data.ReadInt32BigEndian();
+ obj.BaseVersion = data.ReadInt32BigEndian();
+ obj.TitleID = data.ReadUInt32BigEndian();
+ obj.Platform = data.ReadByteValue();
+ obj.ExecutableType = data.ReadByteValue();
+ obj.DiscNumber = data.ReadByteValue();
+ obj.DiscInSet = data.ReadByteValue();
+ obj.SaveGameID = data.ReadUInt32BigEndian();
+ obj.ConsoleID = data.ReadBytes(5);
+ obj.ProfileID = data.ReadBytes(8);
+
+ // Peek forward to read whether VolumeDescriptor is SVOD or STFS
+ byte[] peeked = data.PeekBytes(52);
+ bool svod = peeked[51] == 0x01;
+ obj.VolumeDescriptor = ParseVolumeDescriptor(data, svod);
+
+ obj.DataFileCount = data.ReadInt32BigEndian();
+ obj.DataFileCombinedSize = data.ReadInt64BigEndian();
+ obj.DescriptorType = data.ReadUInt32BigEndian();
+ obj.Reserved = data.ReadUInt32BigEndian();
+
+ if (obj.MetadataVersion == 2)
+ {
+ obj.SeriesID = data.ReadBytes(16);
+ obj.SeasonID = data.ReadBytes(16);
+ obj.SeasonNumber = data.ReadInt16BigEndian();
+ obj.EpisodeNumber = data.ReadInt16BigEndian();
+ obj.Padding = data.ReadBytes(40);
+ }
+ else
+ {
+ obj.Padding = data.ReadBytes(76);
+ }
+
+ obj.DeviceID = data.ReadBytes(20);
+ obj.DisplayName = data.ReadBytes(2304);
+ obj.DisplayDescription = data.ReadBytes(2304);
+ obj.PublisherName = data.ReadBytes(128);
+ obj.TitleName = data.ReadBytes(128);
+ obj.TransferFlags = data.ReadByteValue();
+ obj.ThumbnailImageSize = data.ReadInt32BigEndian();
+ obj.TitleThumbnailImageSize = data.ReadInt32BigEndian();
+
+ if (obj.MetadataVersion == 2)
+ {
+ obj.ThumbnailImage = data.ReadBytes(0x3D00);
+ obj.AdditionalDisplayNames = data.ReadBytes(768);
+ obj.TitleThumbnailImage = data.ReadBytes(0x3D00);
+ obj.AdditionalDisplayDescriptions = data.ReadBytes(768);
+ }
+ else
+ {
+ obj.ThumbnailImage = data.ReadBytes(0x4000);
+ obj.TitleThumbnailImage = data.ReadBytes(0x4000);
+ }
+
+ return obj;
+ }
+
+ ///
+ /// Parse a Stream into a Signature
+ ///
+ /// Stream to parse
+ /// Filled Signature
+ public static Signature ParseSignature(Stream data, bool remoteSigned)
+ {
+ if (remoteSigned)
+ {
+ var obj = new MicrosoftSignature();
+
+ obj.PackageSignature = data.ReadBytes(256);
+ obj.Padding = data.ReadBytes(296);
+
+ return obj;
+ }
+ else
+ {
+ var obj = new ConsoleSignature();
+
+ obj.CertificateSize = data.ReadUInt16BigEndian();
+ obj.ConsoleID = data.ReadBytes(5);
+ obj.PartNumber = data.ReadBytes(20);
+ obj.ConsoleType = data.ReadByteValue();
+ obj.CertificateDate = data.ReadBytes(8);
+ obj.PublicExponent = data.ReadBytes(4);
+ obj.PublicModulus = data.ReadBytes(128);
+ obj.CertificateSignature = data.ReadBytes(256);
+ obj.Signature = data.ReadBytes(128);
+
+ return obj;
+ }
+ }
+
+ ///
+ /// Parse a Stream into an array of LicenseEntry
+ ///
+ /// Stream to parse
+ /// Filled array of LicenseEntry
+ public static LicenseEntry[] ParseLicensingData(Stream data)
+ {
+ var obj = new LicenseEntry[16];
+
+ for (int i = 0; i < 16; i++)
+ {
+ obj[i] = new LicenseEntry();
+ obj[i].LicenseID = data.ReadInt64BigEndian();
+ obj[i].LicenseBits = data.ReadInt32BigEndian();
+ obj[i].LicenseFlags = data.ReadInt32BigEndian();
+ }
+
+ return obj;
+ }
+
+ ///
+ /// Parse a Stream into a VolumeDescriptor
+ ///
+ /// Stream to parse
+ /// Filled VolumeDescriptor
+ public static VolumeDescriptor ParseVolumeDescriptor(Stream data, bool svod)
+ {
+ if (svod)
+ {
+ var obj = new SVODDescriptor();
+
+ obj.VolumeDescriptorSize = data.ReadByteValue();
+ obj.BlockCacheElementCount = data.ReadByteValue();
+ obj.WorkerThreadProcessor = data.ReadByteValue();
+ obj.WorkerThreadPriority = data.ReadByteValue();
+ obj.Hash = data.ReadBytes(20);
+ obj.DataBlockCount = data.ReadUInt24BigEndian();
+ obj.DataBlockOffset = data.ReadUInt24BigEndian();
+ obj.Hash = data.ReadBytes(5);
+
+ return obj;
+ }
+ else
+ {
+ var obj = new STFSDescriptor();
+
+ obj.VolumeDescriptorSize = data.ReadByteValue();
+ obj.Reserved = data.ReadByteValue();
+ obj.BlockSeparation = data.ReadByteValue();
+ obj.FileTableBlockCount = data.ReadInt16BigEndian();
+ obj.FileTableBlockNumber = data.ReadInt24BigEndian();
+ obj.TopHashTableHash = data.ReadBytes(20);
+ obj.TotalAllocatedBlockCount = data.ReadInt32BigEndian();
+ obj.TotalUnallocatedBlockCount = data.ReadInt32BigEndian();
+
+ return obj;
+ }
+ }
+ }
+}
diff --git a/SabreTools.Wrappers/STFS.Printing.cs b/SabreTools.Wrappers/STFS.Printing.cs
index 0cacb82b..ee6fd801 100644
--- a/SabreTools.Wrappers/STFS.Printing.cs
+++ b/SabreTools.Wrappers/STFS.Printing.cs
@@ -1,7 +1,6 @@
using System;
using System.Text;
using SabreTools.Data.Models.STFS;
-using SabreTools.Numerics;
using SabreTools.Text.Extensions;
namespace SabreTools.Wrappers
@@ -60,7 +59,7 @@ namespace SabreTools.Wrappers
builder.AppendLine(header.DataFileCombinedSize, " Data File Combined Size");
builder.AppendLine(header.DescriptorType, " Descriptor Type");
builder.AppendLine(header.Reserved, " Reserved");
-
+
if (header.MetadataVersion == 2)
{
builder.AppendLine(header.SeriesID, " Series ID");
@@ -135,7 +134,7 @@ namespace SabreTools.Wrappers
builder.AppendLine(header.TransferFlags, " Transfer Flags"); // See Enums.TransferFlags
builder.AppendLine(header.ThumbnailImageSize, " Thumbnail Image Size");
builder.AppendLine(header.TitleThumbnailImageSize, " Title Thumbnail Image Size");
-
+
if (header.AdditionalDisplayNames is not null)
{
for (int i = 0; i < 18; i++)
@@ -149,6 +148,7 @@ namespace SabreTools.Wrappers
}
}
}
+
if (header.AdditionalDisplayDescriptions is not null)
{
for (int i = 0; i < 18; i++)
@@ -160,7 +160,7 @@ namespace SabreTools.Wrappers
builder.AppendLine(localeString, $" Additional Display Description {i}");
builder.AppendLine(Encoding.BigEndianUnicode.GetString(localeString), $" Additional Display Description {i} (Parsed)");
}
- }
+ }
}
builder.AppendLine();
@@ -212,6 +212,7 @@ namespace SabreTools.Wrappers
lastLicenseData = i + 1;
break;
}
+
if (i == 0)
builder.AppendLine("Zeroed", " Licensing Data");
}
diff --git a/SabreTools.Wrappers/STFS.cs b/SabreTools.Wrappers/STFS.cs
index 7ac5b656..391b1398 100644
--- a/SabreTools.Wrappers/STFS.cs
+++ b/SabreTools.Wrappers/STFS.cs
@@ -1,4 +1,3 @@
-using System.Collections.Generic;
using System.IO;
using SabreTools.Data.Models.STFS;