mirror of
https://github.com/SabreTools/SabreTools.Serialization.git
synced 2026-09-22 06:45:11 +00:00
Secure Transacted File System (STFS) Support (#78)
* Initial STFS support * Fix build errors * Fix more build errors * Final fix * Cleanup printer * Fix indent
This commit is contained in:
@@ -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 |
|
||||
|
||||
64
SabreTools.Data.Models/STFS/ConsoleSignature.cs
Normal file
64
SabreTools.Data.Models/STFS/ConsoleSignature.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SabreTools.Data.Models.STFS
|
||||
{
|
||||
/// <summary>
|
||||
/// Signature signed by console, for "CON " format STFS files
|
||||
/// </summary>
|
||||
/// <see href="https://free60.org/System-Software/Formats/STFS/"/>
|
||||
public class ConsoleSignature : Signature
|
||||
{
|
||||
/// <summary>
|
||||
/// Public Key Certificate Size
|
||||
/// </summary>
|
||||
/// <remarks>Big-endian</remarks>
|
||||
public ushort CertificateSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Certificate Owner Console ID
|
||||
/// </summary>
|
||||
/// <remarks>5 bytes</remarks>
|
||||
public byte[] ConsoleID { get; set; } = new byte[5];
|
||||
|
||||
/// <summary>
|
||||
/// Certificate Owner Console Part Number
|
||||
/// </summary>
|
||||
/// <remarks>20 bytes, ASCII string</remarks>
|
||||
public byte[] PartNumber { get; set; } = new byte[20];
|
||||
|
||||
/// <summary>
|
||||
/// Certificate Owner Console Type (1 for devkit, 2 for retail)
|
||||
/// </summary>
|
||||
public byte ConsoleType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Certificate Date of Generation
|
||||
/// </summary>
|
||||
/// <remarks>8 bytes, ASCII string</remarks>
|
||||
public byte[] CertificateDate { get; set; } = new byte[8];
|
||||
|
||||
/// <summary>
|
||||
/// Public Exponent
|
||||
/// </summary>
|
||||
/// <remarks>4 bytes</remarks>
|
||||
public byte[] PublicExponent { get; set; } = new byte[4];
|
||||
|
||||
/// <summary>
|
||||
/// Public Modulus
|
||||
/// </summary>
|
||||
/// <remarks>128 bytes</remarks>
|
||||
public byte[] PublicModulus { get; set; } = new byte[128];
|
||||
|
||||
/// <summary>
|
||||
/// Certificate Signature
|
||||
/// </summary>
|
||||
/// <remarks>256 bytes</remarks>
|
||||
public byte[] CertificateSignature { get; set; } = new byte[256];
|
||||
|
||||
/// <summary>
|
||||
/// Signature
|
||||
/// </summary>
|
||||
/// <remarks>128 bytes</remarks>
|
||||
public byte[] Signature { get; set; } = new byte[128];
|
||||
}
|
||||
}
|
||||
43
SabreTools.Data.Models/STFS/Constants.cs
Normal file
43
SabreTools.Data.Models/STFS/Constants.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SabreTools.Data.Models.STFS
|
||||
{
|
||||
/// <see href="https://free60.org/System-Software/Formats/STFS/"/>
|
||||
public static class Constants
|
||||
{
|
||||
/// <summary>
|
||||
/// STFS LIVE magic number ("LIVE")
|
||||
/// </summary>
|
||||
public static readonly byte[] MagicBytesLIVE = [0x4C, 0x49, 0x56, 0x45];
|
||||
|
||||
/// <summary>
|
||||
/// STFS LIVE magic string ("LIVE")
|
||||
/// </summary>
|
||||
public const string MagicStringLIVE = "LIVE";
|
||||
|
||||
/// <summary>
|
||||
/// STFS PIRS magic number ("PIRS")
|
||||
/// </summary>
|
||||
public static readonly byte[] MagicBytesPIRS = [0x50, 0x49, 0x52, 0x53];
|
||||
|
||||
/// <summary>
|
||||
/// STFS PIRS magic string ("PIRS")
|
||||
/// </summary>
|
||||
public const string MagicStringPIRS = "PIRS";
|
||||
|
||||
/// <summary>
|
||||
/// STFS CON magic number ("CON ")
|
||||
/// </summary>
|
||||
public static readonly byte[] MagicBytesCON = [0x43, 0x4F, 0x4E, 0x20];
|
||||
|
||||
/// <summary>
|
||||
/// STFS CON magic string ("CON")
|
||||
/// </summary>
|
||||
public const string MagicStringCON = "CON ";
|
||||
|
||||
/// <summary>
|
||||
/// Standard length of an STFS header
|
||||
/// </summary>
|
||||
public const uint StandardHeaderSize = 0xB000;
|
||||
}
|
||||
}
|
||||
61
SabreTools.Data.Models/STFS/Enums.cs
Normal file
61
SabreTools.Data.Models/STFS/Enums.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using System;
|
||||
|
||||
namespace SabreTools.Data.Models.XenonExecutable
|
||||
{
|
||||
/// <summary>
|
||||
/// STFS Volume Content Type possible values
|
||||
/// </summary>
|
||||
/// <see href="https://free60.org/System-Software/Formats/STFS/"/>
|
||||
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,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// STFS Transfer Flags
|
||||
/// </summary>
|
||||
/// <see href="https://free60.org/System-Software/Formats/STFS/"/>
|
||||
[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,
|
||||
}
|
||||
}
|
||||
16
SabreTools.Data.Models/STFS/HashTable.cs
Normal file
16
SabreTools.Data.Models/STFS/HashTable.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SabreTools.Data.Models.STFS
|
||||
{
|
||||
/// <summary>
|
||||
/// STFS Hash Table in a Hash Table Block
|
||||
/// </summary>
|
||||
/// <see href="https://free60.org/System-Software/Formats/STFS/"/>
|
||||
public class HashTable
|
||||
{
|
||||
/// <summary>
|
||||
/// 170 hash table entries in a single table/block
|
||||
/// </summary>
|
||||
public HashTableEntry[] HashTableEntries { get; set; } = new HashTableEntry[170];
|
||||
}
|
||||
}
|
||||
33
SabreTools.Data.Models/STFS/HashTableEntry.cs
Normal file
33
SabreTools.Data.Models/STFS/HashTableEntry.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using System.Collections.Generic;
|
||||
using SabreTools.Numerics;
|
||||
|
||||
namespace SabreTools.Data.Models.STFS
|
||||
{
|
||||
/// <summary>
|
||||
/// STFS Hash Table Entry in a Hash Block's Hash Table
|
||||
/// </summary>
|
||||
/// <see href="https://free60.org/System-Software/Formats/STFS/"/>
|
||||
public class HashTableEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// SHA-1 hash of the block
|
||||
/// </summary>
|
||||
public byte[] Hash { get; set; } = new byte[20];
|
||||
|
||||
/// <summary>
|
||||
/// Status of the block
|
||||
/// 0x00 = Unused block
|
||||
/// 0x40 = Free block (previously used, now freed)
|
||||
/// 0x80 = Used block
|
||||
/// 0xC0 = Newly allocated block
|
||||
/// </summary>
|
||||
public byte Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Block number corresponding to the hash
|
||||
/// FFFFFF = Block 1 (starting 0xB000)
|
||||
/// </summary>
|
||||
/// <remarks>Big-endian, 3-byte uint24</remarks>
|
||||
public UInt24 BlockNumber { get; set; } = new();
|
||||
}
|
||||
}
|
||||
280
SabreTools.Data.Models/STFS/Header.cs
Normal file
280
SabreTools.Data.Models/STFS/Header.cs
Normal file
@@ -0,0 +1,280 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SabreTools.Data.Models.STFS
|
||||
{
|
||||
/// <summary>
|
||||
/// Secure Transacted File System, Header format
|
||||
/// </summary>
|
||||
/// <see href="https://free60.org/System-Software/Formats/STFS/"/>
|
||||
public class Header
|
||||
{
|
||||
/// <summary>
|
||||
/// Magic bytes indicating the format
|
||||
/// Possible values are "LIVE", "PIRS, and "CON "
|
||||
/// </summary>
|
||||
/// <remarks>4 bytes</remarks>
|
||||
public byte[] MagicBytes { get; set; } = new byte[4];
|
||||
|
||||
/// <summary>
|
||||
/// Signature Block
|
||||
/// Not optional, but abstract class
|
||||
/// </summary>
|
||||
/// <remarks>552 bytes</remarks>
|
||||
public Signature? Signature { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Used to check package owner
|
||||
/// 16 license entries, 16 bytes each
|
||||
/// </summary>
|
||||
/// <remarks>256 bytes</remarks>
|
||||
public LicenseEntry[] LicensingData { get; set; } = new LicenseEntry[16];
|
||||
|
||||
/// <summary>
|
||||
/// SHA-1 Integrity Hash of the header (from ContentType/0x344 to first hash table)
|
||||
/// </summary>
|
||||
/// <remarks>20 bytes</remarks>
|
||||
public byte[] HeaderHash { get; set; } = new byte[20];
|
||||
|
||||
/// <summary>
|
||||
/// Size of the header, in bytes (from ??? to ???)
|
||||
/// The actual end of header is padded and zeroed up until next multiple of 4096 bytes
|
||||
/// </summary>
|
||||
/// <remarks>Big-endian</remarks>
|
||||
public uint HeaderSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Indication of the content in the STFS
|
||||
/// See Enum.ContentType
|
||||
/// </summary>
|
||||
/// <remarks>Big-endian</remarks>
|
||||
public int ContentType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
/// <remarks>Big-endian</remarks>
|
||||
public int MetadataVersion { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Size of content in bytes
|
||||
/// </summary>
|
||||
/// <remarks>Big-endian</remarks>
|
||||
public long ContentSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Media ID
|
||||
/// </summary>
|
||||
/// <remarks>Big-endian</remarks>
|
||||
public uint MediaID { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Version of System/Title Updates
|
||||
/// </summary>
|
||||
/// <remarks>Big-endian</remarks>
|
||||
public int Version { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Base Version of System/Title Updates
|
||||
/// </summary>
|
||||
/// <remarks>Big-endian</remarks>
|
||||
public int BaseVersion { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Title ID
|
||||
/// </summary>
|
||||
/// <remarks>Big-endian</remarks>
|
||||
public uint TitleID { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Intended platform for content
|
||||
/// 0 = ???, 2 = Xbox 360, 4 = PC
|
||||
/// </summary>
|
||||
public byte Platform { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Intended platform for content
|
||||
/// Xbox 360 = 2, PC = 4
|
||||
/// </summary>
|
||||
public byte ExecutableType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Disc Number
|
||||
/// </summary>
|
||||
public byte DiscNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Disc In Set
|
||||
/// </summary>
|
||||
public byte DiscInSet { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Save Game ID
|
||||
/// </summary>
|
||||
/// <remarks>Big-endian</remarks>
|
||||
public uint SaveGameID { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Console ID
|
||||
/// </summary>
|
||||
/// <remarks>5 bytes</remarks>
|
||||
public byte[] ConsoleID { get; set; } = new byte[5];
|
||||
|
||||
/// <summary>
|
||||
/// Profile ID
|
||||
/// </summary>
|
||||
/// <remarks>8 bytes</remarks>
|
||||
public byte[] ProfileID { get; set; } = new byte[8];
|
||||
|
||||
/// <summary>
|
||||
/// STFS Volume Descriptor
|
||||
/// Not optional, but abstract class
|
||||
/// </summary>
|
||||
public VolumeDescriptor? VolumeDescriptor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Data File Count
|
||||
/// </summary>
|
||||
/// <remarks>Big-endian</remarks>
|
||||
public int DataFileCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Data File Combined Size, in bytes
|
||||
/// </summary>
|
||||
/// <remarks>Big-endian</remarks>
|
||||
public long DataFileCombinedSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Descriptor Type
|
||||
/// 0 = STFS, 1 = SVOD
|
||||
/// </summary>
|
||||
/// <remarks>Big-endian</remarks>
|
||||
public uint DescriptorType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Reserved bytes, should be all zeroed
|
||||
/// </summary>
|
||||
/// <remarks>Big-endian</remarks>
|
||||
public uint Reserved { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Series ID
|
||||
/// Zeroed for MetadataVersion = 1
|
||||
/// </summary>
|
||||
/// <remarks>16 bytes</remarks>
|
||||
public byte[]? SeriesID { get; set; } = new byte[16];
|
||||
|
||||
/// <summary>
|
||||
/// Season ID
|
||||
/// Zeroed for MetadataVersion = 1
|
||||
/// </summary>
|
||||
/// <remarks>16 bytes</remarks>
|
||||
public byte[]? SeasonID { get; set; } = new byte[16];
|
||||
|
||||
/// <summary>
|
||||
/// Season Number
|
||||
/// Zeroed for MetadataVersion = 1
|
||||
/// </summary>
|
||||
/// <remarks>Big-endian</remarks>
|
||||
public short? SeasonNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Season Number
|
||||
/// Zeroed for MetadataVersion = 1
|
||||
/// </summary>
|
||||
/// <remarks>Big-endian</remarks>
|
||||
public short? EpisodeNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Padding bytes
|
||||
/// If MetadataVersion is 2, there are 40 bytes
|
||||
/// Otherwise, there are 76 bytes
|
||||
/// </summary>
|
||||
/// <remarks>40 bytes</remarks>
|
||||
public byte[] Padding { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Device ID
|
||||
/// </summary>
|
||||
/// <remarks>20 bytes</remarks>
|
||||
public byte[] DeviceID { get; set; } = new byte[20];
|
||||
|
||||
/// <summary>
|
||||
/// Display Name, UTF-8 string
|
||||
/// 128 bytes per locale, 18 different locales
|
||||
/// </summary>
|
||||
/// <remarks>2304 bytes, UTF-8 string</remarks>
|
||||
public byte[] DisplayName { get; set; } = new byte[2304];
|
||||
|
||||
/// <summary>
|
||||
/// Display Description, UTF-8 string
|
||||
/// 128 bytes per locale, 18 different locales
|
||||
/// </summary>
|
||||
/// <remarks>2304 bytes, UTF-8 string</remarks>
|
||||
public byte[] DisplayDescription { get; set; } = new byte[2304];
|
||||
|
||||
/// <summary>
|
||||
/// Publisher Name, UTF-8 string
|
||||
/// </summary>
|
||||
/// <remarks>128 bytes, UTF-8 string</remarks>
|
||||
public byte[] PublisherName { get; set; } = new byte[128];
|
||||
|
||||
/// <summary>
|
||||
/// Title Name, UTF-8 string
|
||||
/// </summary>
|
||||
/// <remarks>128 bytes, UTF-8 string</remarks>
|
||||
public byte[] TitleName { get; set; } = new byte[128];
|
||||
|
||||
/// <summary>
|
||||
/// Transfer Flags, see Constants.TransferFlags
|
||||
/// </summary>
|
||||
public byte TransferFlags { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Size of the thumbnail image, in bytes
|
||||
/// </summary>
|
||||
/// <remarks>Big-endian</remarks>
|
||||
public int ThumbnailImageSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Size of the title thumbnail image, in bytes
|
||||
/// </summary>
|
||||
/// <remarks>Big-endian</remarks>
|
||||
public int TitleThumbnailImageSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Thumbnail image
|
||||
/// If Metadata version = 1, 0x4000 bytes allocated, padded with zeroes
|
||||
/// If Metadata version = 2, 0x3D00 bytes allocated, padded with zeroes
|
||||
/// </summary>
|
||||
public byte[] ThumbnailImage { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Additional Display Names, UTF-8 string
|
||||
/// 128 bytes per locale, 6 different locales
|
||||
/// Only present if MetadataVersion = 2
|
||||
/// </summary>
|
||||
/// <remarks>If present, 768 bytes, UTF-8 string</remarks>
|
||||
public byte[]? AdditionalDisplayNames { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Title thumbnail image
|
||||
/// If Metadata version = 1, 0x4000 bytes allocated, padded with zeroes
|
||||
/// If Metadata version = 2, 0x3D00 bytes allocated, padded with zeroes
|
||||
/// </summary>
|
||||
public byte[] TitleThumbnailImage { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Additional Display Descriptions, UTF-8 string
|
||||
/// 128 bytes per locale, 6 different locales
|
||||
/// Only present if MetadataVersion = 2
|
||||
/// </summary>
|
||||
/// <remarks>If present, 768 bytes, UTF-8 string</remarks>
|
||||
public byte[]? AdditionalDisplayDescriptions { get; set; }
|
||||
}
|
||||
}
|
||||
26
SabreTools.Data.Models/STFS/LicenseEntry.cs
Normal file
26
SabreTools.Data.Models/STFS/LicenseEntry.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SabreTools.Data.Models.STFS
|
||||
{
|
||||
/// <summary>
|
||||
/// Secure Transacted File System (STFS) License Entry format
|
||||
/// </summary>
|
||||
/// <see href="https://free60.org/System-Software/Formats/STFS/"/>
|
||||
public class LicenseEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// License ID
|
||||
/// </summary>
|
||||
public long LicenseID { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// License Bits
|
||||
/// </summary>
|
||||
public int LicenseBits { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// License Flags
|
||||
/// </summary>
|
||||
public int LicenseFlags { get; set; }
|
||||
}
|
||||
}
|
||||
23
SabreTools.Data.Models/STFS/MicrosoftSignature.cs
Normal file
23
SabreTools.Data.Models/STFS/MicrosoftSignature.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SabreTools.Data.Models.STFS
|
||||
{
|
||||
/// <summary>
|
||||
/// Signature block signed by Microsoft, for LIVE and PIRS format STFS files
|
||||
/// </summary>
|
||||
/// <see href="https://free60.org/System-Software/Formats/STFS/"/>
|
||||
public class MicrosoftSignature : Signature
|
||||
{
|
||||
/// <summary>
|
||||
/// Signature remotely signed by Microsoft
|
||||
/// </summary>
|
||||
/// <remarks>256 bytes</remarks>
|
||||
public byte[] PackageSignature { get; set; } = new byte[256];
|
||||
|
||||
/// <summary>
|
||||
/// Zeroed padding
|
||||
/// </summary>
|
||||
/// <remarks>296 bytes</remarks>
|
||||
public byte[] Padding { get; set; } = new byte[296];
|
||||
}
|
||||
}
|
||||
57
SabreTools.Data.Models/STFS/STFSDescriptor.cs
Normal file
57
SabreTools.Data.Models/STFS/STFSDescriptor.cs
Normal file
@@ -0,0 +1,57 @@
|
||||
using System.Collections.Generic;
|
||||
using SabreTools.Numerics;
|
||||
|
||||
namespace SabreTools.Data.Models.STFS
|
||||
{
|
||||
/// <summary>
|
||||
/// STFS Volume Descriptor, for STFS packages
|
||||
/// </summary>
|
||||
/// <see href="https://free60.org/System-Software/Formats/STFS/"/>
|
||||
public class STFSDescriptor : VolumeDescriptor
|
||||
{
|
||||
/// <summary>
|
||||
/// Volume descriptor size (Should be 0x24)
|
||||
/// </summary>
|
||||
public byte VolumeDescriptorSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Reserved (Should be 0x00)
|
||||
/// </summary>
|
||||
public byte Reserved { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Block Separation
|
||||
/// </summary>
|
||||
public byte BlockSeparation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// File Table Block Count
|
||||
/// </summary>
|
||||
/// <remarks>Big-endian</remarks>
|
||||
public short FileTableBlockCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// File Table Block Number
|
||||
/// </summary>
|
||||
/// <remarks>Big-endian, 3-byte int24</remarks>
|
||||
public Int24 FileTableBlockNumber { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Top Hash Table Hash
|
||||
/// </summary>
|
||||
/// <remarks>20 bytes</remarks>
|
||||
public byte[] TopHashTableHash { get; set; } = new byte[20];
|
||||
|
||||
/// <summary>
|
||||
/// Total Allocated Block Count
|
||||
/// </summary>
|
||||
/// <remarks>Big-endian</remarks>
|
||||
public int TotalAllocatedBlockCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Total Unallocated Block Count
|
||||
/// </summary>
|
||||
/// <remarks>Big-endian</remarks>
|
||||
public int TotalUnallocatedBlockCount { get; set; }
|
||||
}
|
||||
}
|
||||
56
SabreTools.Data.Models/STFS/SVODDescriptor.cs
Normal file
56
SabreTools.Data.Models/STFS/SVODDescriptor.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using System.Collections.Generic;
|
||||
using SabreTools.Numerics;
|
||||
|
||||
namespace SabreTools.Data.Models.STFS
|
||||
{
|
||||
/// <summary>
|
||||
/// STFS Volume Descriptor, for STFS packages
|
||||
/// </summary>
|
||||
/// <see href="https://free60.org/System-Software/Formats/STFS/"/>
|
||||
public class SVODDescriptor : VolumeDescriptor
|
||||
{
|
||||
/// <summary>
|
||||
/// Volume descriptor size (Should be 0x24)
|
||||
/// </summary>
|
||||
public byte VolumeDescriptorSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Block Cache Element Count
|
||||
/// </summary>
|
||||
public byte BlockCacheElementCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Worker Thread Processor
|
||||
/// </summary>
|
||||
public byte WorkerThreadProcessor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Worker Thread Priority
|
||||
/// </summary>
|
||||
public byte WorkerThreadPriority { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Hash
|
||||
/// </summary>
|
||||
/// <remarks>20 bytes</remarks>
|
||||
public byte[] Hash { get; set; } = new byte[20];
|
||||
|
||||
/// <summary>
|
||||
/// Data Block Count
|
||||
/// </summary>
|
||||
/// <remarks>Big-endian, 3-byte uint24</remarks>
|
||||
public UInt24 DataBlockCount { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Data Block Offset
|
||||
/// </summary>
|
||||
/// <remarks>Big-endian, 3-byte uint24</remarks>
|
||||
public UInt24 DataBlockOffset { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Padding, should be zeroed
|
||||
/// </summary>
|
||||
/// <remarks>5 bytes</remarks>
|
||||
public byte[] Padding { get; set; } = new byte[5];
|
||||
}
|
||||
}
|
||||
13
SabreTools.Data.Models/STFS/Signature.cs
Normal file
13
SabreTools.Data.Models/STFS/Signature.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SabreTools.Data.Models.STFS
|
||||
{
|
||||
/// <summary>
|
||||
/// STFS Signature, differs for remotely signed (PIRS/LIVE) and console signed ("CON ") formats
|
||||
/// </summary>
|
||||
/// <see href="https://free60.org/System-Software/Formats/STFS/"/>
|
||||
public abstract class Signature
|
||||
{
|
||||
// Filled in by child class, MicrosoftSignature or ConsoleSignature
|
||||
}
|
||||
}
|
||||
37
SabreTools.Data.Models/STFS/Volume.cs
Normal file
37
SabreTools.Data.Models/STFS/Volume.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SabreTools.Data.Models.STFS
|
||||
{
|
||||
/// <summary>
|
||||
/// 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)
|
||||
/// </summary>
|
||||
/// <see href="https://free60.org/System-Software/Formats/STFS/"/>
|
||||
public class Volume
|
||||
{
|
||||
/// <summary>
|
||||
/// STFS Header data
|
||||
/// Should be 0xA000 bytes (10 blocks)
|
||||
/// </summary>
|
||||
public Header Header { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
/// <remarks>Reader does not fill this in yet</remarks>
|
||||
public HashTable[]? HashTables { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
/// <remarks>Too large to read into memory, left in model for posterity</remarks>
|
||||
public byte[]? Data { get; set; }
|
||||
}
|
||||
}
|
||||
13
SabreTools.Data.Models/STFS/VolumeDescriptor.cs
Normal file
13
SabreTools.Data.Models/STFS/VolumeDescriptor.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SabreTools.Data.Models.STFS
|
||||
{
|
||||
/// <summary>
|
||||
/// STFS Volume Descriptor, differs for STFS and SVOD packages
|
||||
/// </summary>
|
||||
/// <see href="https://free60.org/System-Software/Formats/STFS/"/>
|
||||
public abstract class VolumeDescriptor
|
||||
{
|
||||
// Filled in by child class, SFTSDescriptor or SVODDescriptor
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
/// </summary>
|
||||
/// <see href="http://oskarsapps.mine.nu/xexdump"/>
|
||||
/// <see href="https://free60.org/System-Software/Formats/XEX/"/>
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace SabreTools.Data.Models.XenonExecutable
|
||||
{
|
||||
/// <summary>
|
||||
/// "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
|
||||
/// </summary>
|
||||
public byte[] MagicNumber { get; set; } = new byte[4];
|
||||
|
||||
|
||||
72
SabreTools.Serialization.Readers.Test/STFSTests.cs
Normal file
72
SabreTools.Serialization.Readers.Test/STFSTests.cs
Normal file
@@ -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<byte>(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<byte>(0xFF, 1024)]);
|
||||
var deserializer = new STFS();
|
||||
|
||||
var actual = deserializer.Deserialize(data);
|
||||
Assert.Null(actual);
|
||||
}
|
||||
}
|
||||
}
|
||||
228
SabreTools.Serialization.Readers/STFS.cs
Normal file
228
SabreTools.Serialization.Readers/STFS.cs
Normal file
@@ -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<Volume>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Header on success, null on error</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Signature
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Signature</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into an array of LicenseEntry
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled array of LicenseEntry</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a VolumeDescriptor
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled VolumeDescriptor</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
60
SabreTools.Wrappers.Test/STFSTests.cs
Normal file
60
SabreTools.Wrappers.Test/STFSTests.cs
Normal file
@@ -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<byte>(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<byte>(0xFF, 1024)]);
|
||||
var actual = STFS.Create(data);
|
||||
Assert.Null(actual);
|
||||
}
|
||||
}
|
||||
}
|
||||
270
SabreTools.Wrappers/STFS.Printing.cs
Normal file
270
SabreTools.Wrappers/STFS.Printing.cs
Normal file
@@ -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
|
||||
/// <inheritdoc/>
|
||||
public string ExportJSON() => System.Text.Json.JsonSerializer.Serialize(Model, _jsonSerializerOptions);
|
||||
#endif
|
||||
|
||||
/// <inheritdoc/>
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
98
SabreTools.Wrappers/STFS.cs
Normal file
98
SabreTools.Wrappers/STFS.cs
Normal file
@@ -0,0 +1,98 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using SabreTools.Data.Models.STFS;
|
||||
|
||||
namespace SabreTools.Wrappers
|
||||
{
|
||||
public partial class STFS : WrapperBase<Volume>
|
||||
{
|
||||
#region Descriptive Properties
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string DescriptionString => "Secure Transacted File System";
|
||||
|
||||
#endregion
|
||||
|
||||
#region Extension Properties
|
||||
|
||||
/// <inheritdoc cref="Volume.Header"/>
|
||||
public Header Header => Model.Header;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
|
||||
/// <inheritdoc/>
|
||||
public STFS(Volume model, byte[] data) : base(model, data) { }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public STFS(Volume model, byte[] data, int offset) : base(model, data, offset) { }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public STFS(Volume model, byte[] data, int offset, int length) : base(model, data, offset, length) { }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public STFS(Volume model, Stream data) : base(model, data) { }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public STFS(Volume model, Stream data, long offset) : base(model, data, offset) { }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public STFS(Volume model, Stream data, long offset, long length) : base(model, data, offset, length) { }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Static Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Create an STFS Volume from a byte array and offset
|
||||
/// </summary>
|
||||
/// <param name="data">Byte array representing the STFS Volume</param>
|
||||
/// <param name="offset">Offset within the array to parse</param>
|
||||
/// <returns>An STFS Volume wrapper on success, null on failure</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create an STFS Volume from a Stream
|
||||
/// </summary>
|
||||
/// <param name="data">Stream representing the STFS Volume</param>
|
||||
/// <returns>An STFS Volume wrapper on success, null on failure</returns>
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -247,6 +247,11 @@ namespace SabreTools.Wrappers
|
||||
/// </summary>
|
||||
SkuSis,
|
||||
|
||||
/// <summary>
|
||||
/// Secure Transacted File System
|
||||
/// </summary>
|
||||
STFS,
|
||||
|
||||
/// <summary>
|
||||
/// Tape archive
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user