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;