From 8e843647bf4d1bc42421f246883db3dfcf46d2ab Mon Sep 17 00:00:00 2001 From: Matt Nadareski Date: Tue, 21 Dec 2021 10:54:57 -0800 Subject: [PATCH] Overhaul XeMID handling (fixes #334) --- CHANGELIST.md | 1 + MPF.Core/Data/Constants.cs | 1 + MPF.Core/Data/XgdInfo.cs | 423 +++++++++++++++++++++ MPF.Modules/BaseParameters.cs | 61 +-- MPF.Modules/DiscImageCreator/Parameters.cs | 94 ++--- MPF.Test/Core/Data/XgdInfoTests.cs | 115 ++++++ 6 files changed, 588 insertions(+), 107 deletions(-) create mode 100644 MPF.Core/Data/XgdInfo.cs create mode 100644 MPF.Test/Core/Data/XgdInfoTests.cs diff --git a/CHANGELIST.md b/CHANGELIST.md index be179fc8..cb2879c7 100644 --- a/CHANGELIST.md +++ b/CHANGELIST.md @@ -34,6 +34,7 @@ - Trim filenames for DVD protection from DIC - Fill out internal tests around Redump library - Refine the "missing disc" text +- Overhaul XeMID handling ### 2.1 (2021-07-22) - Enum, no more diff --git a/MPF.Core/Data/Constants.cs b/MPF.Core/Data/Constants.cs index d9fe3be6..50fb9caa 100644 --- a/MPF.Core/Data/Constants.cs +++ b/MPF.Core/Data/Constants.cs @@ -105,6 +105,7 @@ namespace MPF.Core.Data public const string XBOXSSHash = "SS"; public const string XBOXSSRanges = "Security Sector Ranges"; public const string XBOXSSVersion = "Security Sector Version"; + public const string XBOXXeMID = "XeMID"; // Default values diff --git a/MPF.Core/Data/XgdInfo.cs b/MPF.Core/Data/XgdInfo.cs new file mode 100644 index 00000000..32b545be --- /dev/null +++ b/MPF.Core/Data/XgdInfo.cs @@ -0,0 +1,423 @@ +using RedumpLib.Data; + +namespace MPF.Core.Data +{ + /// + /// Contains information specific to an XGD disc + /// + /// + /// XGD1 XeMID Format Information: + /// + /// AABBBCCD + /// - AA => The two-ASCII-character publisher identifier (see GetPublisher for details) + /// - BBB => Game ID + /// - CC => Version number + /// - D => Region identifier (see GetRegion for details) + /// + /// XGD2/3 XeMID Format Information: + /// + /// AABCCCDDEFFGHH(IIIIIIII) + /// - AA => The two-ASCII-character publisher identifier (see GetPublisher for details) + /// - B => Platform identifier; 2 indicates Xbox 360. + /// - CCC => Game ID + /// - DD => SKU number (unique per SKU of a title) + /// - E => Region identifier (see GetRegion for details) + /// - FF => Base version; usually starts at 01 (can be 1 or 2 characters) + /// - G => Media type identifier (see GetMediaSubtype for details) + /// - HH => Disc number stored in [disc number][total discs] format + /// - IIIIIIII => 8-hex-digit certification submission identifier; usually on test discs only + /// + public class XgdInfo + { + #region Fields + + /// + /// Indicates whether the information in this object is fully instantiated or not + /// + public bool Initialized { get; private set; } + + /// + /// Raw XeMID string that all other information is derived from + /// + public string XeMID { get; private set; } + + /// + /// 2-character publisher identifier + /// + public string PublisherIdentifier { get; private set; } + + /// + /// Platform disc is made for, 2 indicates Xbox 360 + /// + public char? PlatformIdentifier { get; private set; } + + /// + /// Game ID + /// + public string GameID { get; private set; } + + /// + /// For XGD1: Internal version number + /// For XGD2/3: Title-specific SKU + /// + public string SKU { get; private set; } + + /// + /// Region identifier character + /// + public char RegionIdentifier { get; private set; } + + /// + /// Base version of executables, usually starts at 01 + /// + /// + /// TODO: Check if this is always 2 characters for XGD2/3 + /// + public string BaseVersion { get; private set; } + + /// + /// Media subtype identifier + /// + public char MediaSubtypeIdentifier { get; private set; } + + /// + /// Disc number stored in [disc number][total discs] format + /// + public string DiscNumberIdentifier { get; private set; } + + /// + /// 8-hex-digit certification submission identifier; usually on test discs only + /// + public string CertificationSubmissionIdentifier { get; private set; } + + #endregion + + #region Auto-Generated Information + + /// + /// Human-readable name derived from the publisher identifier + /// + public string PublisherName => GetPublisher(this.PublisherIdentifier); + + /// + /// Internally represented region + /// + public Region? InternalRegion => GetRegion(this.RegionIdentifier); + + /// + /// Human-readable subtype derived from the media identifier + /// + public string MediaSubtype => GetMediaSubtype(this.MediaSubtypeIdentifier); + + #endregion + + /// + /// Populate a set of XGD information from a Master ID (XeMID) string + /// + /// XeMID string representing the DMI information + /// True if value validation should be performed, false otherwise + public XgdInfo(string xemid, bool validate = false) + { + this.Initialized = false; + if (string.IsNullOrWhiteSpace(xemid)) + return; + + this.XeMID = xemid.TrimEnd('\0'); + if (string.IsNullOrWhiteSpace(this.XeMID)) + return; + + // XGD1 information is 8 characters + if (this.XeMID.Length == 8) + this.Initialized = ParseXGD1XeMID(this.XeMID, validate); + + // XGD2/3 information is semi-variable length + else if (this.XeMID.Length == 13 || this.XeMID.Length == 14 || this.XeMID.Length == 21 || this.XeMID.Length == 22) + this.Initialized = ParseXGD23XeMID(this.XeMID, validate); + } + + /// + /// Get the human-readable serial string + /// + /// Formatted serial string, null on error + public string GetSerial() + { + if (!this.Initialized) + return null; + + try + { + // XGD1 doesn't use PlatformIdentifier + if (this.PlatformIdentifier == null) + return $"{this.PublisherIdentifier}-{this.GameID}"; + + // XGD2/3 uses a specific identifier + else if (this.PlatformIdentifier == '2') + return $"{this.PublisherIdentifier}-{this.PlatformIdentifier}{this.GameID}"; + + return null; + } + catch + { + return null; + } + } + + /// + /// Get the human-readable version string + /// + /// Formatted version string, null on error + /// This may differ for XGD2/3 in the future + public string GetVersion() + { + if (!this.Initialized) + return null; + + try + { + // XGD1 doesn't use PlatformIdentifier + if (this.PlatformIdentifier == null) + return $"1.{this.SKU}"; + + // XGD2/3 uses a specific identifier + else if (this.PlatformIdentifier == '2') + return $"1.{this.SKU}"; + + return null; + } + catch + { + return null; + } + } + + /// + /// Parse an XGD1 XeMID string + /// + /// XeMID string to attempt to parse + /// True if value validation should be performed, false otherwise + /// True if the XeMID could be parsed, false otherwise + private bool ParseXGD1XeMID(string xemid, bool validate) + { + if (xemid == null || xemid.Length != 8) + return false; + + this.PublisherIdentifier = xemid.Substring(0, 2); + if (validate && string.IsNullOrEmpty(this.PublisherName)) + return false; + + this.GameID = xemid.Substring(2, 3); + this.SKU = xemid.Substring(5, 2); + this.RegionIdentifier = xemid[7]; + if (validate && this.InternalRegion == null) + return false; + + return true; + } + + /// + /// Parse an XGD2/3 XeMID string + /// + /// XeMID string to attempt to parse + /// True if value validation should be performed, false otherwise + /// True if the XeMID could be parsed, false otherwise + private bool ParseXGD23XeMID(string xemid, bool validate) + { + if (xemid == null + || (xemid.Length != 13 && xemid.Length != 14 + && xemid.Length != 21 && xemid.Length != 22)) + return false; + + this.PublisherIdentifier = xemid.Substring(0, 2); + if (validate && string.IsNullOrEmpty(this.PublisherName)) + return false; + + this.PlatformIdentifier = xemid[2]; + if (validate && this.PlatformIdentifier != '2') + return false; + + this.GameID = xemid.Substring(3, 3); + this.SKU = xemid.Substring(6, 2); + this.RegionIdentifier = xemid[8]; + if (validate && this.InternalRegion == null) + return false; + + if (xemid.Length == 13 || xemid.Length == 21) + { + this.BaseVersion = xemid.Substring(9, 1); + this.MediaSubtypeIdentifier = xemid[10]; + if (validate && string.IsNullOrEmpty(this.MediaSubtype)) + return false; + + this.DiscNumberIdentifier = xemid.Substring(11, 2); + } + else if (xemid.Length == 14 || xemid.Length == 22) + { + this.BaseVersion = xemid.Substring(9, 2); + this.MediaSubtypeIdentifier = xemid[11]; + if (validate && string.IsNullOrEmpty(this.MediaSubtype)) + return false; + + this.DiscNumberIdentifier = xemid.Substring(12, 2); + } + + if (xemid.Length == 21) + this.CertificationSubmissionIdentifier = xemid.Substring(13); + else if (xemid.Length == 22) + this.CertificationSubmissionIdentifier = xemid.Substring(14); + + return true; + } + + #region Helpers + + /// + /// Determine the XGD type based on the XGD2/3 media type identifier character + /// + /// Character denoting the media type + /// Media subtype as a string, if possible + private static string GetMediaSubtype(char mediaTypeIdentifier) + { + switch (mediaTypeIdentifier) + { + case 'F': return "XGD3"; + case 'X': return "XGD2"; + case 'Z': return "Games on Demand / Marketplace Demo"; + default: return null; + } + } + + /// + /// Get the full name of the publisher from the 2-character identifier + /// + /// Case-sensitive 2-character identifier + /// Publisher name, if possible + /// + private static string GetPublisher(string publisherIdentifier) + { + 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; + } + } + + /// + /// Determine the region based on the XGD serial character + /// + /// Character denoting the region + /// Region, if possible + private static Region? GetRegion(char region) + { + switch (region) + { + case 'W': return Region.World; + case 'A': return Region.USA; + case 'J': return Region.JapanAsia; + case 'E': return Region.Europe; + case 'K': return Region.USAJapan; + case 'L': return Region.USAEurope; + case 'H': return Region.JapanEurope; + default: return null; + } + } + + #endregion + } +} diff --git a/MPF.Modules/BaseParameters.cs b/MPF.Modules/BaseParameters.cs index 1e135c81..30218917 100644 --- a/MPF.Modules/BaseParameters.cs +++ b/MPF.Modules/BaseParameters.cs @@ -1324,14 +1324,10 @@ namespace MPF.Modules { switch (category) { - case "GAME": - return DiscCategory.Games; - case "VIDEO": - return DiscCategory.Video; - case "AUDIO": - return DiscCategory.Audio; - default: - return null; + case "GAME": return DiscCategory.Games; + case "VIDEO": return DiscCategory.Video; + case "AUDIO": return DiscCategory.Audio; + default: return null; } } @@ -1353,20 +1349,13 @@ namespace MPF.Modules // char secondRegion = serial[3]; switch (serial[2]) { - case 'A': - return Region.Asia; - case 'C': - return Region.China; - case 'E': - return Region.Europe; - case 'J': - return Region.JapanKorea; - case 'K': - return Region.Korea; - case 'P': - return Region.Japan; - case 'U': - return Region.USA; + case 'A': return Region.Asia; + case 'C': return Region.China; + case 'E': return Region.Europe; + case 'J': return Region.JapanKorea; + case 'K': return Region.Korea; + case 'P': return Region.Japan; + case 'U': return Region.USA; } } @@ -1393,34 +1382,6 @@ namespace MPF.Modules return null; } - /// - /// Determine the region based on the XGD serial character - /// - /// Character denoting the region - /// Region, if possible - protected static Region? GetXgdRegion(char region) - { - switch (region) - { - case 'W': - return Region.World; - case 'A': - return Region.USA; - case 'J': - return Region.JapanAsia; - case 'E': - return Region.Europe; - case 'K': - return Region.USAJapan; - case 'L': - return Region.USAEurope; - case 'H': - return Region.JapanEurope; - default: - return null; - } - } - #endregion } } \ No newline at end of file diff --git a/MPF.Modules/DiscImageCreator/Parameters.cs b/MPF.Modules/DiscImageCreator/Parameters.cs index 29a0342b..3928b342 100644 --- a/MPF.Modules/DiscImageCreator/Parameters.cs +++ b/MPF.Modules/DiscImageCreator/Parameters.cs @@ -472,40 +472,31 @@ namespace MPF.Modules.DiscImageCreator break; case RedumpSystem.MicrosoftXbox: - if (GetXgdAuxInfo(basePath + "_disc.txt", out string dmihash, out string pfihash, out string sshash, out string ss, out string ssver)) - { - info.CommonDiscInfo.Comments += $"{Template.XBOXDMIHash}: {dmihash ?? ""}\n" + - $"{Template.XBOXPFIHash}: {pfihash ?? ""}\n" + - $"{Template.XBOXSSHash}: {sshash ?? ""}\n" + - $"{Template.XBOXSSVersion}: {ssver ?? ""}\n"; - info.Extras.SecuritySectorRanges = ss ?? ""; - } - - if (GetXboxDMIInfo(Path.Combine(outputDirectory, "DMI.bin"), out string serial, out string version, out Region? region)) - { - info.CommonDiscInfo.Serial = serial ?? ""; - info.VersionAndEditions.Version = version ?? ""; - info.CommonDiscInfo.Region = region; - } - - break; - case RedumpSystem.MicrosoftXbox360: - if (GetXgdAuxInfo(basePath + "_disc.txt", out string dmi360hash, out string pfi360hash, out string ss360hash, out string ss360, out string ssver360)) + string xgdXeMID = string.Empty; + if (this.System == RedumpSystem.MicrosoftXbox) + xgdXeMID = GetXGD1XeMID(Path.Combine(outputDirectory, "DMI.bin")); + else if (this.System == RedumpSystem.MicrosoftXbox360) + xgdXeMID = GetXGD23XeMID(Path.Combine(outputDirectory, "DMI.bin")); + + XgdInfo xgdInfo = new XgdInfo(xgdXeMID); + if (xgdInfo?.Initialized == true) { - info.CommonDiscInfo.Comments += $"{Template.XBOXDMIHash}: {dmi360hash ?? ""}\n" + - $"{Template.XBOXPFIHash}: {pfi360hash ?? ""}\n" + - $"{Template.XBOXSSHash}: {ss360hash ?? ""}\n" + - $"{Template.XBOXSSVersion}: {ssver360 ?? ""}\n"; - info.Extras.SecuritySectorRanges = ss360 ?? ""; + info.CommonDiscInfo.Comments += $"{Template.XBOXXeMID}: {xgdInfo.XeMID ?? ""}\n"; + info.CommonDiscInfo.Serial = xgdInfo.GetSerial() ?? ""; + info.VersionAndEditions.Version = xgdInfo.GetVersion() ?? ""; + info.CommonDiscInfo.Region = xgdInfo.InternalRegion; } - if (GetXbox360DMIInfo(Path.Combine(outputDirectory, "DMI.bin"), out string serial360, out string version360, out Region? region360)) + if (GetXGDAuxInfo(basePath + "_disc.txt", out string xgdDMIHash, out string xgdPFIHash, out string xgdSSHash, out string xgdSS, out string xgdSSVer)) { - info.CommonDiscInfo.Serial = serial360 ?? ""; - info.VersionAndEditions.Version = version360 ?? ""; - info.CommonDiscInfo.Region = region360; + info.CommonDiscInfo.Comments += $"{Template.XBOXDMIHash}: {xgdDMIHash ?? ""}\n" + + $"{Template.XBOXPFIHash}: {xgdPFIHash ?? ""}\n" + + $"{Template.XBOXSSHash}: {xgdSSHash ?? ""}\n" + + $"{Template.XBOXSSVersion}: {xgdSSVer ?? ""}\n"; + info.Extras.SecuritySectorRanges = xgdSS ?? ""; } + break; case RedumpSystem.NamcoSegaNintendoTriforce: @@ -3094,8 +3085,13 @@ namespace MPF.Modules.DiscImageCreator /// Get the XGD auxiliary info from the outputted files, if possible /// /// _disc.txt file location + /// Extracted DMI.bin CRC32 hash (upper-cased) + /// Extracted PFI.bin CRC32 hash (upper-cased) + /// Extracted SS.bin CRC32 hash (upper-cased) + /// Extracted security sector data + /// Extracted security sector version /// True on successful extraction of info, false otherwise - private static bool GetXgdAuxInfo(string disc, out string dmihash, out string pfihash, out string sshash, out string ss, out string ssver) + private static bool GetXGDAuxInfo(string disc, out string dmihash, out string pfihash, out string sshash, out string ss, out string ssver) { dmihash = null; pfihash = null; sshash = null; ss = null; ssver = null; @@ -3169,65 +3165,49 @@ namespace MPF.Modules.DiscImageCreator } /// - /// Get the Xbox serial info from the DMI.bin file, if possible + /// Get the XGD1 Master ID (XeMID) information /// /// DMI.bin file location - /// True on successful extraction of info, false otherwise - private static bool GetXboxDMIInfo(string dmi, out string serial, out string version, out Region? region) + /// String representation of the XGD1 DMI information, empty string on error + private static string GetXGD1XeMID(string dmi) { - serial = null; version = null; region = Region.World; - if (!File.Exists(dmi)) - return false; + return string.Empty; using (BinaryReader br = new BinaryReader(File.OpenRead(dmi))) { try { br.BaseStream.Seek(8, SeekOrigin.Begin); - char[] str = br.ReadChars(8); - - serial = $"{str[0]}{str[1]}-{str[2]}{str[3]}{str[4]}"; - version = $"1.{str[5]}{str[6]}"; - region = GetXgdRegion(str[7]); - return true; + return new string(br.ReadChars(8)); } catch { - return false; + return string.Empty; } } } /// - /// Get the Xbox 360 serial info from the DMI.bin file, if possible + /// Get the XGD2/3 Master ID (XeMID) information /// /// DMI.bin file location - /// True on successful extraction of info, false otherwise - private static bool GetXbox360DMIInfo(string dmi, out string serial, out string version, out Region? region) + /// String representation of the XGD2/3 DMI information, empty string on error + private static string GetXGD23XeMID(string dmi) { - serial = null; version = null; region = Region.World; - if (!File.Exists(dmi)) - return false; + return string.Empty; using (BinaryReader br = new BinaryReader(File.OpenRead(dmi))) { try { br.BaseStream.Seek(64, SeekOrigin.Begin); - char[] str = br.ReadChars(14); - - serial = $"{str[0]}{str[1]}-{str[2]}{str[3]}{str[4]}{str[5]}"; - version = $"1.{str[6]}{str[7]}"; - region = GetXgdRegion(str[8]); - // str[9], str[10], str[11] - unknown purpose - // str[12], str[13] - disc <12> of <13> - return true; + return new string(br.ReadChars(14)); } catch { - return false; + return string.Empty; } } } diff --git a/MPF.Test/Core/Data/XgdInfoTests.cs b/MPF.Test/Core/Data/XgdInfoTests.cs new file mode 100644 index 00000000..335407ab --- /dev/null +++ b/MPF.Test/Core/Data/XgdInfoTests.cs @@ -0,0 +1,115 @@ +using MPF.Core.Data; +using Xunit; + +namespace MPF.Test.Core.Data +{ + public class XgdInfoTests + { + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData("1234567")] + [InlineData("1234567\0")] + [InlineData("123456789")] + public void UnmatchedStringTests(string invalidString) + { + XgdInfo xgdInfo = new XgdInfo(invalidString); + Assert.False(xgdInfo.Initialized); + } + + [Theory] + [InlineData("AV00100W", "AV", "001", "00", 'W')] + [InlineData("AV00100W\0", "AV", "001", "00", 'W')] + public void XGD1ValidTests(string validString, string publisher, string gameId, string version, char regionIdentifier) + { + XgdInfo xgdInfo = new XgdInfo(validString, validate: true); + + Assert.True(xgdInfo.Initialized); + Assert.Equal(publisher, xgdInfo.PublisherIdentifier); + Assert.Equal(gameId, xgdInfo.GameID); + Assert.Equal(version, xgdInfo.SKU); + Assert.Equal(regionIdentifier, xgdInfo.RegionIdentifier); + } + + [Theory] + // Invalid publisher identifier + [InlineData("ZZ000000")] + [InlineData("ZZ000000\0")] + // Invalid region identifier + [InlineData("AV00000Z")] + [InlineData("AV00000Z\0")] + public void XGD1InvalidTests(string invalidString) + { + XgdInfo xgdInfo = new XgdInfo(invalidString, validate: true); + Assert.False(xgdInfo.Initialized); + } + + [Theory] + [InlineData("AV200100W0F11", "AV", "001", "00", 'W', "0", 'F', "11", null)] + [InlineData("AV200100W0F11\0", "AV", "001", "00", 'W', "0", 'F', "11", null)] + [InlineData("AV200100W01F11", "AV", "001", "00", 'W', "01", 'F', "11", null)] + [InlineData("AV200100W01F11\0", "AV", "001", "00", 'W', "01", 'F', "11", null)] + [InlineData("AV200100W0F11DEADBEEF", "AV", "001", "00", 'W', "0", 'F', "11", "DEADBEEF")] + [InlineData("AV200100W0F11DEADBEEF\0", "AV", "001", "00", 'W', "0", 'F', "11", "DEADBEEF")] + [InlineData("AV200100W01F11DEADBEEF", "AV", "001", "00", 'W', "01", 'F', "11", "DEADBEEF")] + [InlineData("AV200100W01F11DEADBEEF\0", "AV", "001", "00", 'W', "01", 'F', "11", "DEADBEEF")] + public void XGD23ValidTests(string validString, string publisher, string gameId, string sku, char regionIdentifier, string baseVersion, char mediaSubtype, string discNumber, string cert) + { + XgdInfo xgdInfo = new XgdInfo(validString, validate: true); + + Assert.True(xgdInfo.Initialized); + Assert.Equal(publisher, xgdInfo.PublisherIdentifier); + Assert.Equal('2', xgdInfo.PlatformIdentifier); + Assert.Equal(gameId, xgdInfo.GameID); + Assert.Equal(sku, xgdInfo.SKU); + Assert.Equal(regionIdentifier, xgdInfo.RegionIdentifier); + Assert.Equal(baseVersion, xgdInfo.BaseVersion); + Assert.Equal(mediaSubtype, xgdInfo.MediaSubtypeIdentifier); + Assert.Equal(discNumber, xgdInfo.DiscNumberIdentifier); + Assert.Equal(cert, xgdInfo.CertificationSubmissionIdentifier); + } + + [Theory] + // Invalid publisher identifier + [InlineData("ZZ00000000000")] + [InlineData("ZZ00000000000\0")] + [InlineData("ZZ000000000000")] + [InlineData("ZZ000000000000\0")] + [InlineData("ZZ0000000000000000000")] + [InlineData("ZZ0000000000000000000\0")] + [InlineData("ZZ00000000000000000000")] + [InlineData("ZZ00000000000000000000\0")] + // Invalid platform identifier + [InlineData("AV90000000000")] + [InlineData("AV90000000000\0")] + [InlineData("AV900000000000")] + [InlineData("AV900000000000\0")] + [InlineData("AV9000000000000000000")] + [InlineData("AV9000000000000000000\0")] + [InlineData("AV90000000000000000000")] + [InlineData("AV90000000000000000000\0")] + // Invalid region identifier + [InlineData("AV200000Z0000")] + [InlineData("AV200000Z0000\0")] + [InlineData("AV200000Z00000")] + [InlineData("AV200000Z00000\0")] + [InlineData("AV200000Z000000000000")] + [InlineData("AV200000Z000000000000\0")] + [InlineData("AV200000Z0000000000000")] + [InlineData("AV200000Z0000000000000\0")] + // Invalid media subtype identifier + [InlineData("AV200000W0A00")] + [InlineData("AV200000W0A00\0")] + [InlineData("AV200000W00A00")] + [InlineData("AV200000W00A00\0")] + [InlineData("AV200000W00A000000000")] + [InlineData("AV200000W00A000000000\0")] + [InlineData("AV200000W00A0000000000")] + [InlineData("AV200000W00A0000000000\0")] + public void XGD23InvalidTests(string invalidString) + { + XgdInfo xgdInfo = new XgdInfo(invalidString, validate: true); + Assert.False(xgdInfo.Initialized); + } + } +}