Compare commits

...

16 Commits
1.1.4 ... 1.1.7

Author SHA1 Message Date
Matt Nadareski
0790fc93b6 Bump version 2023-10-25 15:34:38 -04:00
Matt Nadareski
7f6c128521 Update README 2023-10-25 15:04:22 -04:00
Matt Nadareski
87df6b3ebd Add IRD wrapper 2023-10-25 14:51:07 -04:00
Matt Nadareski
2ca7326074 Implement IRD serializers 2023-10-25 14:43:43 -04:00
Matt Nadareski
3b90af7b3a Split XgdInfo into two proper wrappers 2023-10-25 13:29:59 -04:00
Matt Nadareski
6eda2f2541 Update models, add XgdInfo extensions 2023-10-25 13:17:36 -04:00
Matt Nadareski
defe1c53aa Port a version of XgdInfo from MPF 2023-10-24 23:33:53 -04:00
Matt Nadareski
e5fe0a71ef Add Xbox string serialization 2023-10-24 23:24:17 -04:00
Matt Nadareski
83450f693f Make Strings namespace and move Xbox 2023-10-24 23:20:27 -04:00
Matt Nadareski
1f70e1f544 Remove unnecessary validation / methods from Xbox 2023-10-24 23:05:16 -04:00
Matt Nadareski
4bfc83d5d4 Make XMID consistent with XeMID 2023-10-24 21:50:26 -04:00
Matt Nadareski
7364661900 Add two X360 publishers 2023-10-24 21:43:21 -04:00
Matt Nadareski
1cafc4079d Make XeMID logic a bit clearer 2023-10-24 16:56:41 -04:00
Matt Nadareski
0d62cbd1e9 Add XGD4 PIC reading 2023-09-28 23:25:25 -04:00
Matt Nadareski
bf753262a5 Update Models version 2023-09-22 16:05:20 -04:00
Matt Nadareski
5510b5d19d Fix inconsistent access issue 2023-09-16 00:55:31 -04:00
22 changed files with 904 additions and 526 deletions

28
Bytes/IRD.Deserializer.cs Normal file
View File

@@ -0,0 +1,28 @@
using System.IO;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Bytes
{
public partial class IRD : IByteSerializer<Models.IRD.IRD>
{
/// <inheritdoc/>
#if NET48
public Models.IRD.IRD Deserialize(byte[] data, int offset)
#else
public Models.IRD.IRD? Deserialize(byte[]? data, int offset)
#endif
{
// If the data is invalid
if (data == null)
return null;
// If the offset is out of bounds
if (offset < 0 || offset >= data.Length)
return null;
// Create a memory stream and parse that
MemoryStream dataStream = new MemoryStream(data, offset, data.Length - offset);
return new Streams.IRD().Deserialize(dataStream);
}
}
}

20
Files/IRD.Deserializer.cs Normal file
View File

@@ -0,0 +1,20 @@
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Files
{
public partial class IRD : IFileSerializer<Models.IRD.IRD>
{
/// <inheritdoc/>
#if NET48
public Models.IRD.IRD Deserialize(string path)
#else
public Models.IRD.IRD? Deserialize(string? path)
#endif
{
using (var stream = PathProcessor.OpenStream(path))
{
return new Streams.IRD().Deserialize(stream);
}
}
}
}

30
Files/IRD.Serializer.cs Normal file
View File

@@ -0,0 +1,30 @@
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Files
{
public partial class IRD : IFileSerializer<Models.IRD.IRD>
{
/// <inheritdoc/>
#if NET48
public bool Serialize(Models.IRD.IRD obj, string path)
#else
public bool Serialize(Models.IRD.IRD? obj, string? path)
#endif
{
if (string.IsNullOrWhiteSpace(path))
return false;
using (var stream = new Streams.IRD().Serialize(obj))
{
if (stream == null)
return false;
using (var fs = System.IO.File.OpenWrite(path))
{
stream.CopyTo(fs);
return true;
}
}
}
}
}

View File

@@ -1,216 +0,0 @@
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Files
{
public partial class XMID : IFileSerializer<Models.Xbox.XMID>
{
/// <inheritdoc/>
/// <remarks>This treats the input path like a parseable string</remarks>
#if NET48
public Models.Xbox.XMID Deserialize(string path)
#else
public Models.Xbox.XMID? Deserialize(string? path)
#endif
{
if (string.IsNullOrWhiteSpace(path))
return null;
string xmid = path.TrimEnd('\0');
if (string.IsNullOrWhiteSpace(xmid))
return null;
return ParseXMID(xmid);
}
/// <summary>
/// Parse an XGD2/3 XMID string
/// </summary>
/// <param name="xmidString">XMID string to attempt to parse</param>
/// <returns>Filled XMID on success, null on error</returns>
#if NET48
private static Models.Xbox.XMID ParseXMID(string xmidString)
#else
private static Models.Xbox.XMID? ParseXMID(string? xmidString)
#endif
{
if (xmidString == null || xmidString.Length != 8)
return null;
var xmid = new Models.Xbox.XMID();
xmid.PublisherIdentifier = xmidString.Substring(0, 2);
if (string.IsNullOrEmpty(PublisherName(xmid)))
return null;
xmid.GameID = xmidString.Substring(2, 3);
xmid.VersionNumber = xmidString.Substring(5, 2);
xmid.RegionIdentifier = xmidString[7];
if (InternalRegion(xmid) == null)
return null;
return xmid;
}
#region Helpers
/// <summary>
/// Human-readable name derived from the publisher identifier
/// </summary>
#if NET48
public static string PublisherName(Models.Xbox.XMID xmid) => GetPublisher(xmid.PublisherIdentifier);
#else
public static string? PublisherName(Models.Xbox.XMID xmid) => GetPublisher(xmid.PublisherIdentifier);
#endif
/// <summary>
/// Internally represented region
/// </summary>
#if NET48
public static string InternalRegion(Models.Xbox.XMID xmid) => GetRegion(xmid.RegionIdentifier);
#else
public static string? InternalRegion(Models.Xbox.XMID xmid) => GetRegion(xmid.RegionIdentifier);
#endif
/// <summary>
/// Get the full name of the publisher from the 2-character identifier
/// </summary>
/// <param name="publisherIdentifier">Case-sensitive 2-character identifier</param>
/// <returns>Publisher name, if possible</returns>
/// <see cref="https://xboxdevwiki.net/Xbe#Title_ID"/>
#if NET48
private static string GetPublisher(string publisherIdentifier)
#else
private static string? GetPublisher(string? publisherIdentifier)
#endif
{
switch (publisherIdentifier)
{
case "AC": return "Acclaim Entertainment";
case "AH": return "ARUSH Entertainment";
case "AQ": return "Aqua System";
case "AS": return "ASK";
case "AT": return "Atlus";
case "AV": return "Activision";
case "AY": return "Aspyr Media";
case "BA": return "Bandai";
case "BL": return "Black Box";
case "BM": return "BAM! Entertainment";
case "BR": return "Broccoli Co.";
case "BS": return "Bethesda Softworks";
case "BU": return "Bunkasha Co.";
case "BV": return "Buena Vista Games";
case "BW": return "BBC Multimedia";
case "BZ": return "Blizzard";
case "CC": return "Capcom";
case "CK": return "Kemco Corporation"; // TODO: Confirm
case "CM": return "Codemasters";
case "CV": return "Crave Entertainment";
case "DC": return "DreamCatcher Interactive";
case "DX": return "Davilex";
case "EA": return "Electronic Arts (EA)";
case "EC": return "Encore inc";
case "EL": return "Enlight Software";
case "EM": return "Empire Interactive";
case "ES": return "Eidos Interactive";
case "FI": return "Fox Interactive";
case "FS": return "From Software";
case "GE": return "Genki Co.";
case "GV": return "Groove Games";
case "HE": return "Tru Blu (Entertainment division of Home Entertainment Suppliers)";
case "HP": return "Hip games";
case "HU": return "Hudson Soft";
case "HW": return "Highwaystar";
case "IA": return "Mad Catz Interactive";
case "IF": return "Idea Factory";
case "IG": return "Infogrames";
case "IL": return "Interlex Corporation";
case "IM": return "Imagine Media";
case "IO": return "Ignition Entertainment";
case "IP": return "Interplay Entertainment";
case "IX": return "InXile Entertainment"; // TODO: Confirm
case "JA": return "Jaleco";
case "JW": return "JoWooD";
case "KB": return "Kemco"; // TODO: Confirm
case "KI": return "Kids Station Inc."; // TODO: Confirm
case "KN": return "Konami";
case "KO": return "KOEI";
case "KU": return "Kobi and / or GAE (formerly Global A Entertainment)"; // TODO: Confirm
case "LA": return "LucasArts";
case "LS": return "Black Bean Games (publishing arm of Leader S.p.A.)";
case "MD": return "Metro3D";
case "ME": return "Medix";
case "MI": return "Microïds";
case "MJ": return "Majesco Entertainment";
case "MM": return "Myelin Media";
case "MP": return "MediaQuest"; // TODO: Confirm
case "MS": return "Microsoft Game Studios";
case "MW": return "Midway Games";
case "MX": return "Empire Interactive"; // TODO: Confirm
case "NK": return "NewKidCo";
case "NL": return "NovaLogic";
case "NM": return "Namco";
case "OX": return "Oxygen Interactive";
case "PC": return "Playlogic Entertainment";
case "PL": return "Phantagram Co., Ltd.";
case "RA": return "Rage";
case "SA": return "Sammy";
case "SC": return "SCi Games";
case "SE": return "SEGA";
case "SN": return "SNK";
case "SS": return "Simon & Schuster";
case "SU": return "Success Corporation";
case "SW": return "Swing! Deutschland";
case "TA": return "Takara";
case "TC": return "Tecmo";
case "TD": return "The 3DO Company (or just 3DO)";
case "TK": return "Takuyo";
case "TM": return "TDK Mediactive";
case "TQ": return "THQ";
case "TS": return "Titus Interactive";
case "TT": return "Take-Two Interactive Software";
case "US": return "Ubisoft";
case "VC": return "Victor Interactive Software";
case "VN": return "Vivendi Universal (just took Interplays publishing rights)"; // TODO: Confirm
case "VU": return "Vivendi Universal Games";
case "VV": return "Vivendi Universal Games"; // TODO: Confirm
case "WE": return "Wanadoo Edition";
case "WR": return "Warner Bros. Interactive Entertainment"; // TODO: Confirm
case "XI": return "XPEC Entertainment and Idea Factory";
case "XK": return "Xbox kiosk disk?"; // TODO: Confirm
case "XL": return "Xbox special bundled or live demo disk?"; // TODO: Confirm
case "XM": return "Evolved Games"; // TODO: Confirm
case "XP": return "XPEC Entertainment";
case "XR": return "Panorama";
case "YB": return "YBM Sisa (South-Korea)";
case "ZD": return "Zushi Games (formerly Zoo Digital Publishing)";
default: return null;
}
}
/// <summary>
/// Determine the region based on the XGD serial character
/// </summary>
/// <param name="region">Character denoting the region</param>
/// <returns>Region, if possible</returns>
#if NET48
private static string GetRegion(char region)
#else
private static string? GetRegion(char region)
#endif
{
switch (region)
{
case 'W': return "World";
case 'A': return "USA";
case 'J': return "Japan / Asia";
case 'E': return "Europe";
case 'K': return "USA / Japan";
case 'L': return "USA / Europe";
case 'H': return "Japan / Europe";
default: return null;
}
}
#endregion
}
}

View File

@@ -1,15 +0,0 @@
using System;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Files
{
public partial class XMID : IFileSerializer<Models.Xbox.XMID>
{
/// <inheritdoc/>
#if NET48
public bool Serialize(Models.Xbox.XMID obj, string path) => throw new NotImplementedException();
#else
public bool Serialize(Models.Xbox.XMID? obj, string? path) => throw new NotImplementedException();
#endif
}
}

View File

@@ -1,275 +0,0 @@
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Files
{
public partial class XeMID : IFileSerializer<Models.Xbox.XeMID>
{
/// <inheritdoc/>
/// <remarks>This treats the input path like a parseable string</remarks>
#if NET48
public Models.Xbox.XeMID Deserialize(string path)
#else
public Models.Xbox.XeMID? Deserialize(string? path)
#endif
{
if (string.IsNullOrWhiteSpace(path))
return null;
string xemid = path.TrimEnd('\0');
if (string.IsNullOrWhiteSpace(xemid))
return null;
return ParseXeMID(xemid);
}
/// <summary>
/// Parse an XGD2/3 XeMID string
/// </summary>
/// <param name="xemidString">XeMID string to attempt to parse</param>
/// <returns>Filled XeMID on success, null on error</returns>
#if NET48
private static Models.Xbox.XeMID ParseXeMID(string xemidString)
#else
private static Models.Xbox.XeMID? ParseXeMID(string? xemidString)
#endif
{
if (xemidString == null
|| (xemidString.Length != 13 && xemidString.Length != 14
&& xemidString.Length != 21 && xemidString.Length != 22))
return null;
var xemid = new Models.Xbox.XeMID();
xemid.PublisherIdentifier = xemidString.Substring(0, 2);
if (string.IsNullOrEmpty(PublisherName(xemid)))
return null;
xemid.PlatformIdentifier = xemidString[2];
if (xemid.PlatformIdentifier != '2')
return null;
xemid.GameID = xemidString.Substring(3, 3);
xemid.SKU = xemidString.Substring(6, 2);
xemid.RegionIdentifier = xemidString[8];
if (InternalRegion(xemid) == null)
return null;
if (xemidString.Length == 13 || xemidString.Length == 21)
{
xemid.BaseVersion = xemidString.Substring(9, 1);
xemid.MediaSubtypeIdentifier = xemidString[10];
if (string.IsNullOrEmpty(MediaSubtype(xemid)))
return null;
xemid.DiscNumberIdentifier = xemidString.Substring(11, 2);
}
else if (xemidString.Length == 14 || xemidString.Length == 22)
{
xemid.BaseVersion = xemidString.Substring(9, 2);
xemid.MediaSubtypeIdentifier = xemidString[11];
if (string.IsNullOrEmpty(MediaSubtype(xemid)))
return null;
xemid.DiscNumberIdentifier = xemidString.Substring(12, 2);
}
if (xemidString.Length == 21)
xemid.CertificationSubmissionIdentifier = xemidString.Substring(13);
else if (xemidString.Length == 22)
xemid.CertificationSubmissionIdentifier = xemidString.Substring(14);
return xemid;
}
#region Helpers
/// <summary>
/// Human-readable name derived from the publisher identifier
/// </summary>
#if NET48
public static string PublisherName(Models.Xbox.XeMID xemid) => GetPublisher(xemid.PublisherIdentifier);
#else
public static string? PublisherName(Models.Xbox.XeMID xemid) => GetPublisher(xemid.PublisherIdentifier);
#endif
/// <summary>
/// Internally represented region
/// </summary>
#if NET48
public static string InternalRegion(Models.Xbox.XeMID xemid) => GetRegion(xemid.RegionIdentifier);
#else
public static string? InternalRegion(Models.Xbox.XeMID xemid) => GetRegion(xemid.RegionIdentifier);
#endif
/// <summary>
/// Human-readable subtype derived from the media identifier
/// </summary>
#if NET48
public static string MediaSubtype(Models.Xbox.XeMID xemid) => GetMediaSubtype(xemid.MediaSubtypeIdentifier);
#else
public static string? MediaSubtype(Models.Xbox.XeMID xemid) => GetMediaSubtype(xemid.MediaSubtypeIdentifier);
#endif
/// <summary>
/// Determine the XGD type based on the XGD2/3 media type identifier character
/// </summary>
/// <param name="mediaTypeIdentifier">Character denoting the media type</param>
/// <returns>Media subtype as a string, if possible</returns>
#if NET48
private static string GetMediaSubtype(char mediaTypeIdentifier)
#else
private static string? GetMediaSubtype(char mediaTypeIdentifier)
#endif
{
switch (mediaTypeIdentifier)
{
case 'F': return "XGD3";
case 'X': return "XGD2";
case 'Z': return "Games on Demand / Marketplace Demo";
default: return null;
}
}
/// <summary>
/// Get the full name of the publisher from the 2-character identifier
/// </summary>
/// <param name="publisherIdentifier">Case-sensitive 2-character identifier</param>
/// <returns>Publisher name, if possible</returns>
/// <see cref="https://xboxdevwiki.net/Xbe#Title_ID"/>
#if NET48
private static string GetPublisher(string publisherIdentifier)
#else
private static string? GetPublisher(string? publisherIdentifier)
#endif
{
switch (publisherIdentifier)
{
case "AC": return "Acclaim Entertainment";
case "AH": return "ARUSH Entertainment";
case "AQ": return "Aqua System";
case "AS": return "ASK";
case "AT": return "Atlus";
case "AV": return "Activision";
case "AY": return "Aspyr Media";
case "BA": return "Bandai";
case "BL": return "Black Box";
case "BM": return "BAM! Entertainment";
case "BR": return "Broccoli Co.";
case "BS": return "Bethesda Softworks";
case "BU": return "Bunkasha Co.";
case "BV": return "Buena Vista Games";
case "BW": return "BBC Multimedia";
case "BZ": return "Blizzard";
case "CC": return "Capcom";
case "CK": return "Kemco Corporation"; // TODO: Confirm
case "CM": return "Codemasters";
case "CV": return "Crave Entertainment";
case "DC": return "DreamCatcher Interactive";
case "DX": return "Davilex";
case "EA": return "Electronic Arts (EA)";
case "EC": return "Encore inc";
case "EL": return "Enlight Software";
case "EM": return "Empire Interactive";
case "ES": return "Eidos Interactive";
case "FI": return "Fox Interactive";
case "FS": return "From Software";
case "GE": return "Genki Co.";
case "GV": return "Groove Games";
case "HE": return "Tru Blu (Entertainment division of Home Entertainment Suppliers)";
case "HP": return "Hip games";
case "HU": return "Hudson Soft";
case "HW": return "Highwaystar";
case "IA": return "Mad Catz Interactive";
case "IF": return "Idea Factory";
case "IG": return "Infogrames";
case "IL": return "Interlex Corporation";
case "IM": return "Imagine Media";
case "IO": return "Ignition Entertainment";
case "IP": return "Interplay Entertainment";
case "IX": return "InXile Entertainment"; // TODO: Confirm
case "JA": return "Jaleco";
case "JW": return "JoWooD";
case "KB": return "Kemco"; // TODO: Confirm
case "KI": return "Kids Station Inc."; // TODO: Confirm
case "KN": return "Konami";
case "KO": return "KOEI";
case "KU": return "Kobi and / or GAE (formerly Global A Entertainment)"; // TODO: Confirm
case "LA": return "LucasArts";
case "LS": return "Black Bean Games (publishing arm of Leader S.p.A.)";
case "MD": return "Metro3D";
case "ME": return "Medix";
case "MI": return "Microïds";
case "MJ": return "Majesco Entertainment";
case "MM": return "Myelin Media";
case "MP": return "MediaQuest"; // TODO: Confirm
case "MS": return "Microsoft Game Studios";
case "MW": return "Midway Games";
case "MX": return "Empire Interactive"; // TODO: Confirm
case "NK": return "NewKidCo";
case "NL": return "NovaLogic";
case "NM": return "Namco";
case "OX": return "Oxygen Interactive";
case "PC": return "Playlogic Entertainment";
case "PL": return "Phantagram Co., Ltd.";
case "RA": return "Rage";
case "SA": return "Sammy";
case "SC": return "SCi Games";
case "SE": return "SEGA";
case "SN": return "SNK";
case "SS": return "Simon & Schuster";
case "SU": return "Success Corporation";
case "SW": return "Swing! Deutschland";
case "TA": return "Takara";
case "TC": return "Tecmo";
case "TD": return "The 3DO Company (or just 3DO)";
case "TK": return "Takuyo";
case "TM": return "TDK Mediactive";
case "TQ": return "THQ";
case "TS": return "Titus Interactive";
case "TT": return "Take-Two Interactive Software";
case "US": return "Ubisoft";
case "VC": return "Victor Interactive Software";
case "VN": return "Vivendi Universal (just took Interplays publishing rights)"; // TODO: Confirm
case "VU": return "Vivendi Universal Games";
case "VV": return "Vivendi Universal Games"; // TODO: Confirm
case "WE": return "Wanadoo Edition";
case "WR": return "Warner Bros. Interactive Entertainment"; // TODO: Confirm
case "XI": return "XPEC Entertainment and Idea Factory";
case "XK": return "Xbox kiosk disk?"; // TODO: Confirm
case "XL": return "Xbox special bundled or live demo disk?"; // TODO: Confirm
case "XM": return "Evolved Games"; // TODO: Confirm
case "XP": return "XPEC Entertainment";
case "XR": return "Panorama";
case "YB": return "YBM Sisa (South-Korea)";
case "ZD": return "Zushi Games (formerly Zoo Digital Publishing)";
default: return null;
}
}
/// <summary>
/// Determine the region based on the XGD serial character
/// </summary>
/// <param name="region">Character denoting the region</param>
/// <returns>Region, if possible</returns>
#if NET48
private static string GetRegion(char region)
#else
private static string? GetRegion(char region)
#endif
{
switch (region)
{
case 'W': return "World";
case 'A': return "USA";
case 'J': return "Japan / Asia";
case 'E': return "Europe";
case 'K': return "USA / Japan";
case 'L': return "USA / Europe";
case 'H': return "Japan / Europe";
default: return null;
}
}
#endregion
}
}

View File

@@ -1,15 +0,0 @@
using System;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Files
{
public partial class XeMID : IFileSerializer<Models.Xbox.XeMID>
{
/// <inheritdoc/>
#if NET48
public bool Serialize(Models.Xbox.XeMID obj, string path) => throw new NotImplementedException();
#else
public bool Serialize(Models.Xbox.XeMID? obj, string? path) => throw new NotImplementedException();
#endif
}
}

View File

@@ -18,7 +18,7 @@ namespace SabreTools.Serialization.Interfaces
#endif
/// <summary>
/// Sserialize a <typeparamref name="T"/> into a file
/// Serialize a <typeparamref name="T"/> into a file
/// </summary>
/// <typeparam name="T">Type of object to serialize from</typeparam>
/// <param name="obj">Data to serialize</param>

View File

@@ -0,0 +1,32 @@
namespace SabreTools.Serialization.Interfaces
{
/// <summary>
/// Defines how to serialize to and from strings
/// </summary>
public interface IStringSerializer<T>
{
/// <summary>
/// Deserialize a string into <typeparamref name="T"/>
/// </summary>
/// <typeparam name="T">Type of object to deserialize to</typeparam>
/// <param name="str">String to deserialize from</param>
/// <returns>Filled object on success, null on error</returns>
#if NET48
T Deserialize(string str);
#else
T? Deserialize(string? str);
#endif
/// <summary>
/// Serialize a <typeparamref name="T"/> into a string
/// </summary>
/// <typeparam name="T">Type of object to serialize from</typeparam>
/// <param name="obj">Data to serialize</param>
/// <returns>Filled string on successful serialization, null otherwise</returns>
#if NET48
string Serialize(T obj);
#else
string? Serialize(T? obj);
#endif
}
}

View File

@@ -20,6 +20,10 @@ This namespace comprises of serializers and deserializers that can convert to an
This namespace comprises of serializers and deserializers that can convert to and from any type of stream. Most of the serializers are symmetric, but this is not guaranteed. Unimplemented methods will throw `NotImplementedException`.
## `SabreTools.Serialization.Strings`
This namespace comprises of serializers and deserializers that can convert to and from strings. Most of the serializers are symmetric, but this is not guaranteed. Unimplemented methods will throw `NotImplementedException`.
## `SabreTools.Serialization.Wrappers`
This namespace comrpises of wrapping classes that include keeping a reference to the source of each serializable model. Some of the wrappers may also include what are referred to as "extension properties", which are generated properties derived from either parts of the model or the underlying source.

View File

@@ -4,7 +4,7 @@
<!-- Assembly Properties -->
<TargetFrameworks>net48;net6.0;net7.0;net8.0</TargetFrameworks>
<RuntimeIdentifiers>win-x86;win-x64;linux-x64;osx-x64</RuntimeIdentifiers>
<Version>1.1.4</Version>
<Version>1.1.7</Version>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<!-- Package Properties -->
@@ -29,7 +29,7 @@
<ItemGroup>
<PackageReference Include="SabreTools.IO" Version="1.1.1" />
<PackageReference Include="SabreTools.Models" Version="1.1.2" />
<PackageReference Include="SabreTools.Models" Version="1.1.5" />
</ItemGroup>
</Project>

119
Streams/IRD.Deserializer.cs Normal file
View File

@@ -0,0 +1,119 @@
using System;
using System.IO;
using System.Text;
using SabreTools.IO;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Streams
{
public partial class IRD : IStreamSerializer<Models.IRD.IRD>
{
/// <inheritdoc/>
#if NET48
public Models.IRD.IRD Deserialize(Stream data)
#else
public Models.IRD.IRD? Deserialize(Stream? data)
#endif
{
// If the data is invalid
if (data == null || data.Length == 0 || !data.CanSeek || !data.CanRead)
return null;
// If the offset is out of bounds
if (data.Position < 0 || data.Position >= data.Length)
return null;
// Cache the current offset
int initialOffset = (int)data.Position;
// Create a new media key block to fill
var ird = new Models.IRD.IRD();
ird.Magic = data.ReadBytes(4);
if (ird.Magic == null)
return null;
string magic = Encoding.ASCII.GetString(ird.Magic);
if (magic != "3IRD")
return null;
ird.Version = data.ReadByteValue();
if (ird.Version < 6)
return null;
var titleId = data.ReadBytes(9);
if (titleId == null)
return null;
ird.TitleID = Encoding.ASCII.GetString(titleId);
ird.TitleLength = data.ReadByteValue();
var title = data.ReadBytes(ird.TitleLength);
if (title == null)
return null;
ird.Title = Encoding.ASCII.GetString(title);
var systemVersion = data.ReadBytes(4);
if (systemVersion == null)
return null;
ird.SystemVersion = Encoding.ASCII.GetString(systemVersion);
var gameVersion = data.ReadBytes(5);
if (gameVersion == null)
return null;
ird.GameVersion = Encoding.ASCII.GetString(gameVersion);
var appVersion = data.ReadBytes(5);
if (appVersion == null)
return null;
ird.AppVersion = Encoding.ASCII.GetString(appVersion);
if (ird.Version == 7)
ird.UID = data.ReadUInt32();
ird.HeaderLength = data.ReadByteValue();
ird.Header = data.ReadBytes((int)ird.HeaderLength);
ird.FooterLength = data.ReadByteValue();
ird.Footer = data.ReadBytes((int)ird.FooterLength);
ird.RegionCount = data.ReadByteValue();
ird.RegionHashes = new byte[ird.RegionCount][];
for (int i = 0; i < ird.RegionCount; i++)
{
ird.RegionHashes[i] = data.ReadBytes(16) ?? Array.Empty<byte>();
}
ird.FileCount = data.ReadByteValue();
ird.FileKeys = new ulong[ird.FileCount];
ird.FileHashes = new byte[ird.FileCount][];
for (int i = 0; i < ird.FileCount; i++)
{
ird.FileKeys[i] = data.ReadUInt64();
ird.FileHashes[i] = data.ReadBytes(16) ?? Array.Empty<byte>();
}
ird.ExtraConfig = data.ReadUInt16();
ird.Attachments = data.ReadUInt16();
if (ird.Version >= 9)
ird.PIC = data.ReadBytes(115);
ird.Data1Key = data.ReadBytes(16);
ird.Data2Key = data.ReadBytes(16);
if (ird.Version < 9)
ird.PIC = data.ReadBytes(115);
if (ird.Version > 7)
ird.UID = data.ReadUInt32();
ird.CRC = data.ReadUInt32();
return ird;
}
}
}

136
Streams/IRD.Serializer.cs Normal file
View File

@@ -0,0 +1,136 @@
using System;
using System.IO;
using System.Linq;
using System.Text;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Streams
{
public partial class IRD : IStreamSerializer<Models.IRD.IRD>
{
/// <inheritdoc/>
#if NET48
public Stream Serialize(Models.IRD.IRD obj)
#else
public Stream? Serialize(Models.IRD.IRD? obj)
#endif
{
// If the data is invalid
if (obj?.Magic == null)
return null;
// If the magic doesn't match
string magic = Encoding.ASCII.GetString(obj.Magic);
if (magic != "3IRD")
return null;
// If the version is less than the supported
if (obj.Version < 6)
return null;
// If any static-length fields aren't the correct length
if (obj.TitleID == null || obj.TitleID.Length != 9)
return null;
if (obj.Title == null || obj.Title.Length != obj.TitleLength)
return null;
if (obj.SystemVersion == null || obj.SystemVersion.Length != 4)
return null;
if (obj.GameVersion == null || obj.GameVersion.Length != 5)
return null;
if (obj.AppVersion == null || obj.AppVersion.Length != 5)
return null;
if (obj.Header == null || obj.Header.Length != obj.HeaderLength)
return null;
if (obj.Footer == null || obj.Footer.Length != obj.FooterLength)
return null;
if (obj.RegionHashes == null || obj.RegionHashes.Length != obj.RegionCount || obj.RegionHashes.Any(h => h == null || h.Length != 16))
return null;
if (obj.FileKeys == null || obj.FileKeys.Length != obj.FileCount)
return null;
if (obj.FileHashes == null || obj.FileHashes.Length != obj.FileCount || obj.FileHashes.Any(h => h == null || h.Length != 16))
return null;
if (obj.PIC == null || obj.PIC.Length != 115)
return null;
if (obj.Data1Key == null || obj.Data1Key.Length != 16)
return null;
if (obj.Data2Key == null || obj.Data2Key.Length != 16)
return null;
// Create the output stream
var stream = new MemoryStream();
stream.Write(obj.Magic, 0, obj.Magic.Length);
stream.WriteByte(obj.Version);
byte[] titleId = Encoding.ASCII.GetBytes(obj.TitleID);
stream.Write(titleId, 0, titleId.Length);
stream.WriteByte(obj.TitleLength);
byte[] title = Encoding.ASCII.GetBytes(obj.Title);
stream.Write(title, 0, title.Length);
byte[] systemVersion = Encoding.ASCII.GetBytes(obj.SystemVersion);
stream.Write(systemVersion, 0, systemVersion.Length);
byte[] gameVersion = Encoding.ASCII.GetBytes(obj.GameVersion);
stream.Write(gameVersion, 0, gameVersion.Length);
byte[] appVersion = Encoding.ASCII.GetBytes(obj.AppVersion);
stream.Write(appVersion, 0, appVersion.Length);
if (obj.Version == 7)
{
byte[] uid = BitConverter.GetBytes(obj.UID);
stream.Write(uid, 0, uid.Length);
}
byte[] headerLength = BitConverter.GetBytes(obj.HeaderLength);
stream.Write(headerLength, 0, headerLength.Length);
stream.Write(obj.Header, 0, obj.Header.Length);
byte[] footerLength = BitConverter.GetBytes(obj.FooterLength);
stream.Write(footerLength, 0, footerLength.Length);
stream.Write(obj.Footer, 0, obj.Footer.Length);
stream.WriteByte(obj.RegionCount);
for (int i = 0; i < obj.RegionCount; i++)
{
stream.Write(obj.RegionHashes[i], 0, obj.RegionHashes[i].Length);
}
byte[] fileCount = BitConverter.GetBytes(obj.FileCount);
stream.Write(fileCount, 0, fileCount.Length);
for (int i = 0; i < obj.FileCount; i++)
{
byte[] fileKey = BitConverter.GetBytes(obj.FileKeys[i]);
stream.Write(fileKey, 0, fileKey.Length);
stream.Write(obj.FileHashes[i], 0, obj.FileHashes[i].Length);
}
byte[] extraConfig = BitConverter.GetBytes(obj.ExtraConfig);
stream.Write(extraConfig, 0, extraConfig.Length);
byte[] attachments = BitConverter.GetBytes(obj.Attachments);
stream.Write(attachments, 0, attachments.Length);
if (obj.Version >= 9)
stream.Write(obj.PIC, 0, obj.PIC.Length);
stream.Write(obj.Data1Key, 0, obj.Data1Key.Length);
stream.Write(obj.Data2Key, 0, obj.Data2Key.Length);
if (obj.Version < 9)
stream.Write(obj.PIC, 0, obj.PIC.Length);
if (obj.Version > 7)
{
byte[] uid = BitConverter.GetBytes(obj.UID);
stream.Write(uid, 0, uid.Length);
}
byte[] crc = BitConverter.GetBytes(obj.CRC);
stream.Write(crc, 0, crc.Length);
return stream;
}
}
}

View File

@@ -171,6 +171,7 @@ namespace SabreTools.Serialization.Streams
{
case DiscTypeIdentifierROM:
case DiscTypeIdentifierROMUltra:
case DiscTypeIdentifierXGD4:
body.FormatDependentContents = data.ReadBytes(52);
break;
case DiscTypeIdentifierReWritable:

View File

@@ -0,0 +1,48 @@
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Strings
{
public partial class XMID : IStringSerializer<Models.Xbox.XMID>
{
/// <inheritdoc/>
#if NET48
public Models.Xbox.XMID Deserialize(string path)
#else
public Models.Xbox.XMID? Deserialize(string? path)
#endif
{
if (string.IsNullOrWhiteSpace(path))
return null;
string xmid = path.TrimEnd('\0');
if (string.IsNullOrWhiteSpace(xmid))
return null;
return ParseXMID(xmid);
}
/// <summary>
/// Parse an XGD2/3 XMID string
/// </summary>
/// <param name="xmidString">XMID string to attempt to parse</param>
/// <returns>Filled XMID on success, null on error</returns>
#if NET48
private static Models.Xbox.XMID ParseXMID(string xmidString)
#else
private static Models.Xbox.XMID? ParseXMID(string? xmidString)
#endif
{
if (xmidString == null || xmidString.Length != 8)
return null;
var xmid = new Models.Xbox.XMID();
xmid.PublisherIdentifier = xmidString.Substring(0, 2);
xmid.GameID = xmidString.Substring(2, 3);
xmid.VersionNumber = xmidString.Substring(5, 2);
xmid.RegionIdentifier = xmidString[7];
return xmid;
}
}
}

View File

@@ -0,0 +1,28 @@
using System.Text;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Strings
{
public partial class XMID : IStringSerializer<Models.Xbox.XMID>
{
/// <inheritdoc/>
#if NET48
public string Serialize(Models.Xbox.XMID obj)
#else
public string? Serialize(Models.Xbox.XMID? obj)
#endif
{
if (obj == null)
return null;
var sb = new StringBuilder();
sb.Append(obj.PublisherIdentifier);
sb.Append(obj.GameID);
sb.Append(obj.VersionNumber);
sb.Append(obj.RegionIdentifier);
return sb.ToString();
}
}
}

View File

@@ -0,0 +1,72 @@
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Strings
{
public partial class XeMID : IStringSerializer<Models.Xbox.XeMID>
{
/// <inheritdoc/>
#if NET48
public Models.Xbox.XeMID Deserialize(string path)
#else
public Models.Xbox.XeMID? Deserialize(string? path)
#endif
{
if (string.IsNullOrWhiteSpace(path))
return null;
string xemid = path.TrimEnd('\0');
if (string.IsNullOrWhiteSpace(xemid))
return null;
return ParseXeMID(xemid);
}
/// <summary>
/// Parse an XGD2/3 XeMID string
/// </summary>
/// <param name="xemidString">XeMID string to attempt to parse</param>
/// <returns>Filled XeMID on success, null on error</returns>
#if NET48
private static Models.Xbox.XeMID ParseXeMID(string xemidString)
#else
private static Models.Xbox.XeMID? ParseXeMID(string? xemidString)
#endif
{
if (xemidString == null)
return null;
if (!(xemidString.Length == 13 || xemidString.Length == 14 || xemidString.Length == 21 || xemidString.Length == 22))
return null;
var xemid = new Models.Xbox.XeMID();
xemid.PublisherIdentifier = xemidString.Substring(0, 2);
xemid.PlatformIdentifier = xemidString[2];
if (xemid.PlatformIdentifier != '2')
return null;
xemid.GameID = xemidString.Substring(3, 3);
xemid.SKU = xemidString.Substring(6, 2);
xemid.RegionIdentifier = xemidString[8];
if (xemidString.Length == 13 || xemidString.Length == 21)
{
xemid.BaseVersion = xemidString.Substring(9, 1);
xemid.MediaSubtypeIdentifier = xemidString[10];
xemid.DiscNumberIdentifier = xemidString.Substring(11, 2);
}
else if (xemidString.Length == 14 || xemidString.Length == 22)
{
xemid.BaseVersion = xemidString.Substring(9, 2);
xemid.MediaSubtypeIdentifier = xemidString[11];
xemid.DiscNumberIdentifier = xemidString.Substring(12, 2);
}
if (xemidString.Length == 21)
xemid.CertificationSubmissionIdentifier = xemidString.Substring(13);
else if (xemidString.Length == 22)
xemid.CertificationSubmissionIdentifier = xemidString.Substring(14);
return xemid;
}
}
}

View File

@@ -0,0 +1,34 @@
using System.Text;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Strings
{
public partial class XeMID : IStringSerializer<Models.Xbox.XeMID>
{
/// <inheritdoc/>
#if NET48
public string Serialize(Models.Xbox.XeMID obj)
#else
public string? Serialize(Models.Xbox.XeMID? obj)
#endif
{
if (obj == null)
return null;
var sb = new StringBuilder();
sb.Append(obj.PublisherIdentifier);
sb.Append(obj.PlatformIdentifier);
sb.Append(obj.GameID);
sb.Append(obj.SKU);
sb.Append(obj.RegionIdentifier);
sb.Append(obj.BaseVersion);
sb.Append(obj.MediaSubtypeIdentifier);
sb.Append(obj.DiscNumberIdentifier);
if (!string.IsNullOrWhiteSpace(obj.CertificationSubmissionIdentifier))
sb.Append(obj.CertificationSubmissionIdentifier);
return sb.ToString();
}
}
}

94
Wrappers/IRD.cs Normal file
View File

@@ -0,0 +1,94 @@
using System.IO;
namespace SabreTools.Serialization.Wrappers
{
public class IRD : WrapperBase<Models.IRD.IRD>
{
#region Descriptive Properties
/// <inheritdoc/>
public override string DescriptionString => "PS3 IRD file";
#endregion
#region Constructors
/// <inheritdoc/>
#if NET48
public IRD(Models.IRD.IRD model, byte[] data, int offset)
#else
public IRD(Models.IRD.IRD? model, byte[]? data, int offset)
#endif
: base(model, data, offset)
{
// All logic is handled by the base class
}
/// <inheritdoc/>
#if NET48
public IRD(Models.IRD.IRD model, Stream data)
#else
public IRD(Models.IRD.IRD? model, Stream? data)
#endif
: base(model, data)
{
// All logic is handled by the base class
}
/// <summary>
/// Create an IRD from a byte array and offset
/// </summary>
/// <param name="data">Byte array representing the archive</param>
/// <param name="offset">Offset within the array to parse</param>
/// <returns>An IRD wrapper on success, null on failure</returns>
#if NET48
public static IRD Create(byte[] data, int offset)
#else
public static IRD? Create(byte[]? data, int offset)
#endif
{
// If the data is invalid
if (data == null)
return null;
// If the offset is out of bounds
if (offset < 0 || offset >= data.Length)
return null;
// Create a memory stream and use that
MemoryStream dataStream = new MemoryStream(data, offset, data.Length - offset);
return Create(dataStream);
}
/// <summary>
/// Create an IRD from a Stream
/// </summary>
/// <param name="data">Stream representing the archive</param>
/// <returns>An IRD wrapper on success, null on failure</returns>
#if NET48
public static IRD Create(Stream data)
#else
public static IRD? Create(Stream? data)
#endif
{
// If the data is invalid
if (data == null || data.Length == 0 || !data.CanSeek || !data.CanRead)
return null;
var ird = new Streams.IRD().Deserialize(data);
if (ird == null)
return null;
try
{
return new IRD(ird, data);
}
catch
{
return null;
}
}
#endregion
}
}

View File

@@ -209,7 +209,7 @@ namespace SabreTools.Serialization.Wrappers
#if NET48
public byte[] ReadFromDataSource(int position, int length)
#else
protected byte[]? ReadFromDataSource(int position, int length)
public byte[]? ReadFromDataSource(int position, int length)
#endif
{
// Validate the data source
@@ -262,7 +262,7 @@ namespace SabreTools.Serialization.Wrappers
#if NET48
public List<string> ReadStringsFromDataSource(int position, int length, int charLimit = 5)
#else
protected List<string>? ReadStringsFromDataSource(int position, int length, int charLimit = 5)
public List<string>? ReadStringsFromDataSource(int position, int length, int charLimit = 5)
#endif
{
// Read the data as a byte array first

119
Wrappers/XMID.cs Normal file
View File

@@ -0,0 +1,119 @@
using System.IO;
using System.Text;
using static SabreTools.Models.Xbox.Constants;
namespace SabreTools.Serialization.Wrappers
{
public class XMID : WrapperBase<Models.Xbox.XMID>
{
#region Descriptive Properties
/// <inheritdoc/>
public override string DescriptionString => "Xbox Media Identifier";
#endregion
#region Extension Properties
/// <summary>
/// Get the human-readable publisher string
/// </summary>
public string Publisher
{
get
{
var publisherIdentifier = this.Model.PublisherIdentifier;
if (string.IsNullOrWhiteSpace(publisherIdentifier))
return "Unknown";
if (Publishers.ContainsKey(publisherIdentifier))
return Publishers[publisherIdentifier];
return $"Unknown ({publisherIdentifier})";
}
}
/// <summary>
/// Get the human-readable region string
/// </summary>
public string Region
{
get
{
var regionIdentifier = this.Model.RegionIdentifier;
if (Regions.ContainsKey(regionIdentifier))
return Regions[regionIdentifier];
return $"Unknown ({regionIdentifier})";
}
}
/// <summary>
/// Get the human-readable serial string
/// </summary>
public string Serial => $"{this.Model.PublisherIdentifier}-{this.Model.GameID}";
/// <summary>
/// Get the human-readable version string
/// </summary>
public string Version => $"1.{this.Model.VersionNumber}";
#endregion
#region Constructors
/// <inheritdoc/>
#if NET48
public XMID(Models.Xbox.XMID model, byte[] data, int offset)
#else
public XMID(Models.Xbox.XMID? model, byte[]? data, int offset)
#endif
: base(model, data, offset)
{
// All logic is handled by the base class
}
/// <inheritdoc/>
#if NET48
public XMID(Models.Xbox.XMID model, Stream data)
#else
public XMID(Models.Xbox.XMID? model, Stream? data)
#endif
: base(model, data)
{
// All logic is handled by the base class
}
/// <summary>
/// Create a XMID from a string
/// </summary>
/// <param name="data">String representing the data</param>
/// <returns>A XMID wrapper on success, null on failure</returns>
#if NET48
public static XMID Create(string data)
#else
public static XMID? Create(string? data)
#endif
{
// If the data is invalid
if (data == null || data.Length == 0)
return null;
var binary = new Strings.XMID().Deserialize(data);
if (binary == null)
return null;
try
{
var ms = new MemoryStream(Encoding.ASCII.GetBytes(data));
return new XMID(binary, ms);
}
catch
{
return null;
}
}
#endregion
}
}

134
Wrappers/XeMID.cs Normal file
View File

@@ -0,0 +1,134 @@
using System.IO;
using System.Text;
using static SabreTools.Models.Xbox.Constants;
namespace SabreTools.Serialization.Wrappers
{
public class XeMID : WrapperBase<Models.Xbox.XeMID>
{
#region Descriptive Properties
/// <inheritdoc/>
public override string DescriptionString => "Xbox 360 Media Identifier";
#endregion
#region Extension Properties
/// <summary>
/// Get the human-readable media subtype string
/// </summary>
public string MediaSubtype
{
get
{
char mediaSubtype = this.Model.MediaSubtypeIdentifier;
if (MediaSubtypes.ContainsKey(mediaSubtype))
return MediaSubtypes[mediaSubtype];
return $"Unknown ({mediaSubtype})";
}
}
/// <summary>
/// Get the human-readable publisher string
/// </summary>
public string Publisher
{
get
{
var publisherIdentifier = this.Model.PublisherIdentifier;
if (string.IsNullOrWhiteSpace(publisherIdentifier))
return "Unknown";
if (Publishers.ContainsKey(publisherIdentifier))
return Publishers[publisherIdentifier];
return $"Unknown ({publisherIdentifier})";
}
}
/// <summary>
/// Get the human-readable region string
/// </summary>
public string Region
{
get
{
var regionIdentifier = this.Model.RegionIdentifier;
if (Regions.ContainsKey(regionIdentifier))
return Regions[regionIdentifier];
return $"Unknown ({regionIdentifier})";
}
}
/// <summary>
/// Get the human-readable serial string
/// </summary>
public string Serial => $"{this.Model.PublisherIdentifier}-{this.Model.PlatformIdentifier}{this.Model.GameID}";
/// <summary>
/// Get the human-readable version string
/// </summary>
public string Version => $"1.{this.Model.SKU}";
#endregion
#region Constructors
/// <inheritdoc/>
#if NET48
public XeMID(Models.Xbox.XeMID model, byte[] data, int offset)
#else
public XeMID(Models.Xbox.XeMID? model, byte[]? data, int offset)
#endif
: base(model, data, offset)
{
// All logic is handled by the base class
}
/// <inheritdoc/>
#if NET48
public XeMID(Models.Xbox.XeMID model, Stream data)
#else
public XeMID(Models.Xbox.XeMID? model, Stream? data)
#endif
: base(model, data)
{
// All logic is handled by the base class
}
/// <summary>
/// Create a XeMID from a string
/// </summary>
/// <param name="data">String representing the data</param>
/// <returns>A XeMID wrapper on success, null on failure</returns>
#if NET48
public static XeMID Create(string data)
#else
public static XeMID? Create(string? data)
#endif
{
// If the data is invalid
if (data == null || data.Length == 0)
return null;
var binary = new Strings.XeMID().Deserialize(data);
if (binary == null)
return null;
try
{
var ms = new MemoryStream(Encoding.ASCII.GetBytes(data));
return new XeMID(binary, ms);
}
catch
{
return null;
}
}
#endregion
}
}