using System; using System.IO; #if NET35_OR_GREATER || NETCOREAPP using System.Linq; #endif using System.Text; using System.Text.RegularExpressions; using System.Xml; using System.Xml.Schema; using System.Xml.Serialization; using SabreTools.Hashing; using SabreTools.IO.Extensions; using SabreTools.Data.Models.Logiqx; using SabreTools.Data.Models.PIC; using SabreTools.RedumpLib.Data; namespace MPF.Processors { /// /// Includes processing-specific utility functionality /// public static class ProcessingTool { #region Constants /// /// Shift-JIS encoding for detection and conversion /// private static readonly Encoding ShiftJIS = Encoding.GetEncoding(932); #endregion #region Information Extraction /// /// Generate the proper datfile from the input Datafile, if possible /// /// .dat file location /// Relevant pieces of the datfile, null on error public static string? GenerateDatfile(Datafile? datafile) { // If we don't have a valid datafile, we can't do anything if (datafile?.Game is null || datafile.Game.Length == 0) return null; var roms = datafile.Game[0].Rom; if (roms is null || roms.Length == 0) return null; // Otherwise, reconstruct the hash data with only the required info try { string datString = string.Empty; for (int i = 0; i < roms.Length; i++) { var rom = roms[i]; datString += $"\n"; } return datString.TrimEnd('\n'); } catch { // Absorb the exception return null; } } /// /// Get the Base64 representation of a string /// /// String content to encode /// Base64-encoded contents, if possible public static string? GetBase64(string? content) { if (string.IsNullOrEmpty(content)) return null; byte[] temp = Encoding.UTF8.GetBytes(content); return Convert.ToBase64String(temp); } /// /// Get Datafile from a standard DAT /// /// Path to the DAT file to parse /// Filled Datafile on success, null on error public static Datafile? GetDatafile(string? dat) { // If the file doesn't exist, we can't read it if (string.IsNullOrEmpty(dat)) return null; if (!File.Exists(dat)) return null; try { // Open and read in the XML file XmlReader xtr = XmlReader.Create(dat, new XmlReaderSettings { CheckCharacters = false, #if NET40_OR_GREATER || NETCOREAPP DtdProcessing = DtdProcessing.Ignore, #endif IgnoreComments = true, IgnoreWhitespace = true, ValidationFlags = XmlSchemaValidationFlags.None, ValidationType = ValidationType.None, }); // If the reader is null for some reason, we can't do anything if (xtr is null) return null; var serializer = new XmlSerializer(typeof(Datafile)); return serializer.Deserialize(xtr) as Datafile; } catch { // Absorb the exception return null; } } /// /// Gets disc information from a PIC file /// /// Path to a PIC.bin file /// Filled DiscInformation on success, null on error /// This omits the emergency brake information, if it exists public static DiscInformation? GetDiscInformation(string? pic) { try { return new SabreTools.Serialization.Readers.PIC().Deserialize(pic); } catch { // Absorb the exception return null; } } /// /// Get the last modified date from a file path, if possible /// /// Path to the input file /// Filled DateTime on success, null on failure public static DateTime? GetFileModifiedDate(string? filename, bool fallback = false) { if (string.IsNullOrEmpty(filename)) return fallback ? DateTime.UtcNow : null; if (!File.Exists(filename)) return fallback ? DateTime.UtcNow : null; var fi = new FileInfo(filename); return fi.LastWriteTimeUtc; } /// /// Get the full lines from the input file, if possible /// /// file location /// True if should read as binary, false otherwise (default) /// Full text of the file, null on error public static string? GetFullFile(string filename, bool binary = false) { // If the file doesn't exist, we can't get info from it if (string.IsNullOrEmpty(filename)) return null; if (!File.Exists(filename)) return null; // Read the entire file as bytes byte[] bytes = File.ReadAllBytes(filename); // If we're reading as binary if (binary) return BitConverter.ToString(bytes).Replace("-", string.Empty); // If we're reading as text return NormalizeShiftJIS(bytes); } /// /// Get the split values for ISO-based media /// /// Datafile represenging the hash data /// True if extraction was successful, false otherwise public static bool GetISOHashValues(Datafile? datafile, out long size, out string? crc32, out string? md5, out string? sha1) { size = -1; crc32 = null; md5 = null; sha1 = null; if (datafile?.Game is null || datafile.Game.Length == 0) return false; var roms = datafile.Game[0].Rom; if (roms is null || roms.Length == 0) return false; var rom = roms[0]; _ = long.TryParse(rom.Size, out size); crc32 = rom.CRC; md5 = rom.MD5; sha1 = rom.SHA1; return true; } /// /// Get the split values for ISO-based media /// /// String representing the combined hash data /// True if extraction was successful, false otherwise public static bool GetISOHashValues(string? hashData, out long size, out string? crc32, out string? md5, out string? sha1) { size = -1; crc32 = null; md5 = null; sha1 = null; if (string.IsNullOrEmpty(hashData)) return false; var hashreg = new Regex(@" /// Get the layerbreak info associated from the disc information /// /// Disc information containing unformatted data /// True if layerbreak info was set, false otherwise public static bool GetLayerbreaks(DiscInformation? di, out long? layerbreak1, out long? layerbreak2, out long? layerbreak3) { // Set the default values layerbreak1 = null; layerbreak2 = null; layerbreak3 = null; // If we don't have valid disc information, we can't do anything if (di?.Units is null || di.Units.Length <= 1) return false; // Wrap big-endian reading static int ReadFromArrayBigEndian(byte[]? bytes, int offset) { if (bytes is null || bytes.Length < offset + 4) return default; return bytes.ReadInt32BigEndian(ref offset); } // Layerbreak 1 (2+ layers) if (di.Units.Length >= 2) { long offset = ReadFromArrayBigEndian(di.Units[0]?.Body?.FormatDependentContents, 0x0C); long value = ReadFromArrayBigEndian(di.Units[0]?.Body?.FormatDependentContents, 0x10); layerbreak1 = value - offset + 2; } // Layerbreak 2 (3+ layers) if (di.Units.Length >= 3) { long offset = ReadFromArrayBigEndian(di.Units[1]?.Body?.FormatDependentContents, 0x0C); long value = ReadFromArrayBigEndian(di.Units[1]?.Body?.FormatDependentContents, 0x10); layerbreak2 = layerbreak1 + value - offset + 2; } // Layerbreak 3 (4 layers) if (di.Units.Length >= 4) { long offset = ReadFromArrayBigEndian(di.Units[2]?.Body?.FormatDependentContents, 0x0C); long value = ReadFromArrayBigEndian(di.Units[2]?.Body?.FormatDependentContents, 0x10); layerbreak3 = layerbreak2 + value - offset + 2; } return true; } /// /// Get the PIC identifier from the first disc information unit, if possible /// /// Disc information containing the data /// String representing the PIC identifier, null on error public static string? GetPICIdentifier(DiscInformation? di) { // If we don't have valid disc information, we can't do anything if (di?.Units is null || di.Units.Length < 1) return null; // Assume the identifier is consistent across all units return di.Units[0]?.Body?.DiscTypeIdentifier; } /// /// Normalize a byte array that may contain Shift-JIS characters /// /// String as a byte array to normalize /// Normalized version of a string public static string NormalizeShiftJIS(byte[]? contents) { // Invalid arrays are passed as-is if (contents is null || contents.Length == 0) return string.Empty; #if NET462_OR_GREATER || NETCOREAPP Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); #endif // If the line contains Shift-JIS characters if (BytesContainsShiftJIS(contents)) return ShiftJIS.GetString(contents); return Encoding.UTF8.GetString(contents); } /// /// Determine if a byte array contains Shift-JIS encoded characters /// /// Byte array to check for Shift-JIS encoding /// True if the byte array contains Shift-JIS characters, false otherwise /// internal static bool BytesContainsShiftJIS(byte[] bytes) { // Invalid arrays do not count if (bytes is null || bytes.Length == 0) return false; // Loop through and check each pair of bytes for (int i = 0; i < bytes.Length - 1; i++) { byte first = bytes[i]; byte second = bytes[i + 1]; if ((first >= 0x81 && first <= 0x84) || (first >= 0x87 && first <= 0x9F)) { if (second >= 0x40 && second <= 0x9E) return true; else if (second >= 0x9F && second <= 0xFC) return true; } else if (first >= 0xE0 && first <= 0xEF) { if (second >= 0x40 && second <= 0x9E) return true; else if (second >= 0x9F && second <= 0xFC) return true; } } return false; } #endregion #region Category Extraction /// /// Determine the category based on the UMDImageCreator string /// /// String representing the category /// Category, if possible public static DiscCategory? GetUMDCategory(string? category) { return category?.ToLowerInvariant() switch { "(game)" => DiscCategory.Games, "(video)" => DiscCategory.Video, "(audio)" => DiscCategory.Audio, _ => null, }; } #endregion #region Region Extraction /// /// Determine the region based on the PlayStation serial code /// /// PlayStation serial code /// Region mapped from name, if possible public static Region? GetPlayStationRegion(string? serial) { // If we have a fully invalid serial if (string.IsNullOrEmpty(serial)) return null; // Standardized "S" serials if (serial!.StartsWith("S")) { // string publisher = serial[0] + serial[1]; // char secondRegion = serial[3]; #pragma warning disable IDE0010 switch (serial[2]) { case 'A': return Region.Asia; case 'C': return Region.China; case 'E': return Region.Europe; case 'K': return Region.SouthKorea; case 'U': return Region.UnitedStatesOfAmerica; case 'P': // Region of S_P_ serials may be Japan, Asia, or SouthKorea return serial[3] switch { // Check first two digits of S_PS serial 'S' => (Region?)(serial.Substring(5, 2) switch { "46" => Region.SouthKorea, "51" => Region.Asia, "56" => Region.SouthKorea, "55" => Region.Asia, _ => Region.Japan, }), // Check first three digits of S_PM serial 'M' => (Region?)(serial.Substring(5, 3) switch { "645" => Region.SouthKorea, "675" => Region.SouthKorea, "885" => Region.SouthKorea, _ => Region.Japan, // Remaining S_PM serials may be Japan or Asia }), _ => (Region?)Region.Japan, }; } #pragma warning restore IDE0010 } // Japan-only special serial else if (serial.StartsWith("PAPX")) return Region.Japan; // Region appears entirely random else if (serial.StartsWith("PABX")) return null; // Region appears entirely random else if (serial.StartsWith("PBPX")) return null; // Japan-only special serial else if (serial.StartsWith("PCBX")) return Region.Japan; // Japan-only special serial else if (serial.StartsWith("PCXC")) return Region.Japan; // Single disc known, Japan else if (serial.StartsWith("PDBX")) return Region.Japan; // Single disc known, Europe else if (serial.StartsWith("PEBX")) return Region.Europe; // Single disc known, USA else if (serial.StartsWith("PUBX")) return Region.UnitedStatesOfAmerica; return null; } /// /// Determine the region based on the XGD serial character /// /// Character denoting the region /// Region, if possible public static Region? GetXGDRegion(char? region) { return region switch { 'W' => (Region?)Region.World, 'A' => (Region?)Region.UnitedStatesOfAmerica, 'J' => (Region?)Region.JapanAsia, 'E' => (Region?)Region.Europe, 'K' => (Region?)Region.USAJapan, 'L' => (Region?)Region.USAEurope, 'H' => (Region?)Region.JapanEurope, _ => null, }; } #endregion #region PlayStation specific tools /// /// Get if LibCrypt data is detected in the subchannel file, if possible /// /// Path to the subchannel file /// True if LibCrypt was detected, false otherwise public static bool DetectLibCrypt(string? subPath) { if (string.IsNullOrEmpty(subPath)) return false; // Create conversion delegates byte _btoi(byte b) => (byte)((b / 16 * 10) + (b % 16)); byte _itob(byte i) => (byte)((i / 10 * 16) + (i % 10)); try { if (!File.Exists(subPath)) return false; // Open the subfile var subFile = File.OpenRead(subPath); // Check the size long size = subFile.Length; if (size % 96 != 0) return false; // Setup loop variables byte[] expected = new byte[12]; uint time = 0; // Process sector data for (uint sector = 150; sector < ((size / 96) + 150); sector++) { // Read the sector header subFile.Seek(12, SeekOrigin.Current); byte[] actual = subFile.ReadBytes(12); // Skip the rest of the data for the sector subFile.Seek(72, SeekOrigin.Current); // New track if ((_btoi(actual[1]) == (_btoi(expected[1]) + 1)) && (actual[2] == 0 || actual[2] == 1)) { Array.Copy(actual, expected, 6); time = (uint)((_btoi((byte)(actual[3] * 60)) + _btoi(actual[4])) * 75) + _btoi(actual[5]); } // New index else if (_btoi(actual[2]) == (_btoi(expected[2]) + 1) && actual[1] == expected[1]) { Array.Copy(actual, 2, expected, 2, 4); time = (uint)((_btoi((byte)(actual[3] * 60)) + _btoi(actual[4])) * 75) + _btoi(actual[5]); } // MSF1 [3-5] else { if (expected[2] == 0) time--; else time++; expected[3] = _itob((byte)(time / 60 / 75)); expected[4] = _itob((byte)(time / 75 % 60)); expected[5] = _itob((byte)(time % 75)); } // Force skip [6] actual[6] = expected[6] = 0; // MSF2 [7-9] expected[7] = _itob((byte)(sector / 60 / 75)); expected[8] = _itob((byte)(sector / 75 % 60)); expected[9] = _itob((byte)(sector % 75)); // CRC-16 [10-11] -- TODO: Ensure byte order is correct var crcWrapper = new HashWrapper(HashType.CRC16); crcWrapper.Process(expected, 0, 10); byte[] crc = crcWrapper.CurrentHashBytes!; expected[10] = crc[0]; expected[11] = crc[1]; // If a subchannel mismatch is detected if (!actual.EqualsExactly(expected)) return true; } } catch { // Absorb the exception return false; } return false; } #endregion #region PlayStation 3 specific tools /// /// Validates a getkey log to check for presence of valid PS3 key /// /// Path to getkey log file /// Output key string, null if not valid /// Output disc ID string, null if not valid /// Output PIC string, null if not valid /// True if path to log file contains valid key, false otherwise public static bool ParseGetKeyLog(string? logPath, out string? key, out string? id, out string? pic) { key = id = pic = null; if (string.IsNullOrEmpty(logPath)) return false; try { if (!File.Exists(logPath)) return false; // Protect from attempting to read from really long files FileInfo logFile = new(logPath); if (logFile.Length > 65536) return false; // Read from .getkey.log file using StreamReader sr = File.OpenText(logPath); // Determine whether GetKey was successful string? line; while ((line = sr.ReadLine()) is not null && !line.Trim().StartsWith("get_dec_key succeeded!")) ; if (line is null) return false; // Look for Disc Key in log while ((line = sr.ReadLine()) is not null && !line.Trim().StartsWith("disc_key = ")) ; // If end of file reached, no key found if (line is null) return false; // Get Disc Key from log #if NETCOREAPP || NETSTANDARD2_1_OR_GREATER string discKeyStr = line["disc_key = ".Length..]; #else string discKeyStr = line.Substring("disc_key = ".Length); #endif // Validate Disc Key from log if (discKeyStr.Length != 32) return false; // Convert Disc Key to byte array key = discKeyStr; if (key is null) return false; // Read Disc ID while ((line = sr.ReadLine()) is not null && !line.Trim().StartsWith("disc_id = ")) ; // If end of file reached, no ID found if (line is null) return false; // Get Disc ID from log #if NETCOREAPP || NETSTANDARD2_1_OR_GREATER string discIDStr = line["disc_id = ".Length..]; #else string discIDStr = line.Substring("disc_id = ".Length); #endif // Validate Disc ID from log if (discIDStr.Length != 32) return false; // Replace X's in Disc ID with 00000001 #if NETCOREAPP || NETSTANDARD2_1_OR_GREATER discIDStr = $"{discIDStr[..24]}00000001"; #else discIDStr = discIDStr.Substring(0, 24) + "00000001"; #endif // Convert Disc ID to byte array id = discIDStr; if (id is null) return false; // Look for PIC in log while ((line = sr.ReadLine()) is not null && !line.Trim().StartsWith("PIC:")) ; // If end of file reached, no PIC found if (line is null) return false; // Get PIC from log string discPICStr = ""; for (int i = 0; i < 9; i++) discPICStr += sr.ReadLine(); if (discPICStr is null) return false; // Validate PIC from log if (discPICStr.Length != 264) return false; // Convert PIC to byte array pic = discPICStr; if (pic is null) return false; // Double check for warnings in .getkey.log while ((line = sr.ReadLine()) is not null) { string t = line.Trim(); if (t.StartsWith("WARNING")) return false; else if (t.StartsWith("SUCCESS")) return true; } } catch { // Absorb the exception return false; } return true; } /// /// Validates a getkey log to check for presence of valid PS3 key /// /// Path to getkey log file /// Output 16 byte disc key, null if not valid /// Output 16 byte disc ID, null if not valid /// Output 230 byte PIC, null if not valid /// True if path to log file contains valid key, false otherwise public static bool ParseGetKeyLog(string? logPath, out byte[]? key, out byte[]? id, out byte[]? pic) { key = id = pic = null; if (ParseGetKeyLog(logPath, out string? keyString, out string? idString, out string? picString)) { if (string.IsNullOrEmpty(keyString) || string.IsNullOrEmpty(idString) || string.IsNullOrEmpty(picString) || picString!.Length < 230) return false; key = keyString.FromHexString(); id = idString.FromHexString(); #if NETCOREAPP || NETSTANDARD2_1_OR_GREATER pic = picString[..230].FromHexString(); #else pic = picString.Substring(0, 230).FromHexString(); #endif return true; } else { return false; } } /// /// Validates a hexadecimal disc ID /// /// String representing hexadecimal disc ID /// True if string is a valid disc ID, false otherwise public static byte[]? ParseDiscID(string? discID) { if (string.IsNullOrEmpty(discID)) return null; string cleandiscID = discID!.Trim().Replace("\n", string.Empty); if (discID!.Length != 32) return null; // Censor last 4 bytes by replacing with 0x00000001 #if NETCOREAPP || NETSTANDARD2_1_OR_GREATER cleandiscID = $"{cleandiscID[..24]}00000001"; #else cleandiscID = cleandiscID.Substring(0, 24) + "00000001"; #endif // Convert to byte array, null if invalid hex string return cleandiscID.FromHexString(); } /// /// Validates a key file to check for presence of valid PS3 key /// /// Path to key file /// Output 16 byte key, null if not valid public static byte[]? ParseKeyFile(string? keyPath) { if (string.IsNullOrEmpty(keyPath)) return null; // Try read from key file try { if (!File.Exists(keyPath)) return null; // Key file must be exactly 16 bytes long FileInfo keyFile = new(keyPath); if (keyFile.Length != 16) return null; byte[] key = new byte[16]; // Read 16 bytes from Key file using FileStream fs = new(keyPath, FileMode.Open, FileAccess.Read); using BinaryReader reader = new(fs); int numBytes = reader.Read(key, 0, 16); if (numBytes != 16) return null; return key; } catch { // Not concerned with error return null; } } /// /// Validates a hexadecimal key /// /// String representing hexadecimal key /// Output 16 byte key, null if not valid public static byte[]? ParseHexKey(string? hexKey) { if (string.IsNullOrEmpty(hexKey)) return null; string cleanHexKey = hexKey!.Trim().Replace("\n", string.Empty); if (cleanHexKey.Length != 32) return null; // Convert to byte array, null if invalid hex string return cleanHexKey.FromHexString(); } /// /// Validates a PIC file path /// /// Path to PIC file /// Output PIC byte array, null if not valid public static byte[]? ParsePICFile(string? picPath) { if (string.IsNullOrEmpty(picPath)) return null; // Try read from PIC file try { if (!File.Exists(picPath)) return null; // PIC file must be at least 115 bytes long FileInfo picFile = new(picPath); if (picFile.Length < 115) return null; byte[] pic = new byte[115]; // Read 115 bytes from PIC file using FileStream fs = new(picPath, FileMode.Open, FileAccess.Read); using BinaryReader reader = new(fs); int numBytes = reader.Read(pic, 0, 115); if (numBytes != 115) return null; // Validate that a PIC was read by checking first 6 bytes if (pic[0] != 0x10 || pic[1] != 0x02 || pic[2] != 0x00 || pic[3] != 0x00 || pic[4] != 0x44 || pic[5] != 0x49) return null; return pic; } catch { // Not concerned with error return null; } } /// /// Validates a PIC /// /// String representing PIC /// Output PIC byte array, null if not valid public static byte[]? ParsePIC(string? inputPIC) { if (string.IsNullOrEmpty(inputPIC)) return null; string cleanPIC = inputPIC!.Trim().Replace("\n", string.Empty).Replace("\r", string.Empty); if (cleanPIC.Length < 230) return null; // Convert to byte array, null if invalid hex string #if NETCOREAPP || NETSTANDARD2_1_OR_GREATER return cleanPIC[..230].FromHexString(); #else return cleanPIC.Substring(0, 230).FromHexString(); #endif } /// /// Validates a string representing a layerbreak value (in sectors) /// /// String representing layerbreak value /// Output layerbreak value, null if not valid /// True if layerbreak is valid, false otherwise public static long? ParseLayerbreak(string? inputLayerbreak) { if (string.IsNullOrEmpty(inputLayerbreak)) return null; if (!long.TryParse(inputLayerbreak, out long layerbreak)) return null; return ParseLayerbreak(layerbreak); } /// /// Validates a layerbreak value (in sectors) /// /// Number representing layerbreak value /// Output layerbreak value, null if not valid /// True if layerbreak is valid, false otherwise public static long? ParseLayerbreak(long? layerbreak) { // Check that layerbreak is positive number and smaller than largest disc size (in sectors) if (layerbreak <= 0 || layerbreak > 24438784) return null; return layerbreak; } /// /// Converts a CRC32 hash hex string into uint32 representation /// /// Hex string representing CRC32 hash /// Output CRC32 value, null if not valid /// True if CRC32 hash string is valid, false otherwise public static uint? ParseCRC32(string? inputCRC32) { if (string.IsNullOrEmpty(inputCRC32)) return null; byte[]? crc32 = inputCRC32.FromHexString(); if (crc32 is null || crc32.Length != 4) return null; return (uint)((0x01000000 * crc32[0]) + (0x00010000 * crc32[1]) + (0x00000100 * crc32[2]) + (0x00000001 * crc32[3])); } #endregion #region Xbox and Xbox 360 /// /// Get the XGD1 Master ID (XMID) information /// /// DMI.bin file location /// String representation of the XGD1 DMI information, empty string on error public static string GetXMID(string dmi) { if (!File.Exists(dmi)) return string.Empty; try { using var br = new BinaryReader(File.OpenRead(dmi)); br.BaseStream.Seek(8, SeekOrigin.Begin); return new string(br.ReadChars(8)); } catch { return string.Empty; } } /// /// Get the XGD2/3 Master ID (XeMID) information /// /// DMI.bin file location /// String representation of the XGD2/3 DMI information, empty string on error public static string GetXeMID(string dmi) { if (!File.Exists(dmi)) return string.Empty; try { using var br = new BinaryReader(File.OpenRead(dmi)); br.BaseStream.Seek(64, SeekOrigin.Begin); return new string(br.ReadChars(14)); } catch { return string.Empty; } } /// /// Get XGD type from SS.bin file /// /// /// /// public static bool GetXGDType(string? ss, out int xgdType) { xgdType = 0; if (string.IsNullOrEmpty(ss)) return false; if (!File.Exists(ss)) return false; using FileStream fs = File.OpenRead(ss); byte[] buf = new byte[3]; int numBytes = fs.Read(buf, 13, 16); if (numBytes != 3) return false; return GetXGDType(buf, out xgdType); } /// /// Get XGD type from SS.bin sector /// /// Byte array of SS.bin sector /// XGD type /// True if successful, false otherwise public static bool GetXGDType(byte[] ss, out int xgdType) { xgdType = 0; // Concatenate the last three values long lastThree = (((ss[13] << 8) | ss[14]) << 8) | ss[15]; // Return XGD type based on value switch (lastThree) { case 0x2033AF: xgdType = 1; return true; case 0x20339F: xgdType = 2; return true; case 0x238E0F: xgdType = 3; return true; default: return false; } } /// /// Determine if a given SS.bin is valid (Known XGD type and SSv2 if XGD3) /// /// Path to the SS file to check /// True if valid, false otherwise public static bool IsValidSS(string ssPath) { if (!File.Exists(ssPath)) return false; byte[] ss = File.ReadAllBytes(ssPath); if (ss.Length != 2048) return false; return IsValidSS(ss); } /// /// Determine if a given SS is valid (2048 bytes, known XGD type, and SSv2 if XGD3) /// /// Byte array of SS sector /// True if SS is valid, false otherwise public static bool IsValidSS(byte[] ss) { // Check 1 sector long if (ss.Length != 2048) return false; // Must be a valid XGD type if (!GetXGDType(ss, out int xgdType)) return false; // Only continue to check SSv2 for XGD3 if (xgdType != 3) return true; // Determine if XGD3 SS.bin is SSv1 (Original Kreon) or SSv2 (0800 / Repaired Kreon) #if NET20 var checkArr = new byte[72]; Array.Copy(ss, 32, checkArr, 0, 72); return Array.Exists(checkArr, x => x != 0); #else return ss.Skip(32).Take(72).Any(x => x != 0); #endif } /// /// Determine if a given SS.bin is valid but contains zeroed challenge responses /// /// Path to the SS file to check /// True if valid but partial SS.bin, false otherwise public static bool IsValidPartialSS(string ssPath) { if (!File.Exists(ssPath)) return false; byte[] ss = File.ReadAllBytes(ssPath); if (ss.Length != 2048) return false; return IsValidPartialSS(ss); } /// /// Determine if a given SS is valid but contains zeroed challenge responses /// /// Byte array of SS sector /// True if SS is a valid but partial SS, false otherwise public static bool IsValidPartialSS(byte[] ss) { // Check 1 sector long if (ss.Length != 2048) return false; // Must be a valid XGD type if (!GetXGDType(ss, out int xgdType)) return false; // Determine challenge table offset, XGD1 is never partial int ccrt_offset = 0; if (xgdType == 1) return false; else if (xgdType == 2) ccrt_offset = 0x200; else if (xgdType == 3) ccrt_offset = 0x20; int[] entry_offsets = [0, 9, 18, 27, 36, 45, 54, 63]; int[] entry_lengths = [8, 8, 8, 8, 4, 4, 4, 4]; for (int i = 0; i < entry_offsets.Length; i++) { bool emptyResponse = true; for (int b = 0; b < entry_lengths[i]; b++) { if (ss[ccrt_offset + entry_offsets[i] + b] != 0x00) { emptyResponse = false; break; } } if (emptyResponse) return true; } return false; } /// /// Determine if a given SS has already been cleaned /// /// Byte array of SS sector /// True if SS is clean, false otherwise public static bool IsCleanSS(byte[] ss) { if (ss.Length != 2048) return false; if (!GetXGDType(ss, out int xgdType)) return false; #if NET20 var checkArr = new byte[72]; Array.Copy(ss, 32, checkArr, 0, 72); if (xgdType == 3 && Array.Exists(checkArr, x => x != 0)) #else if (xgdType == 3 && ss.Skip(32).Take(72).Any(x => x != 0)) #endif { // Check for a cleaned SSv2 int rtOffset = 0x24; if (ss[rtOffset + 36] != 0x01) return false; if (ss[rtOffset + 37] != 0x00) return false; if (ss[rtOffset + 39] != 0x01) return false; if (ss[rtOffset + 40] != 0x00) return false; if (ss[rtOffset + 45] != 0x5B) return false; if (ss[rtOffset + 46] != 0x00) return false; if (ss[rtOffset + 48] != 0x5B) return false; if (ss[rtOffset + 49] != 0x00) return false; if (ss[rtOffset + 54] != 0xB5) return false; if (ss[rtOffset + 55] != 0x00) return false; if (ss[rtOffset + 57] != 0xB5) return false; if (ss[rtOffset + 58] != 0x00) return false; if (ss[rtOffset + 63] != 0x0F) return false; if (ss[rtOffset + 64] != 0x01) return false; if (ss[rtOffset + 66] != 0x0F) return false; if (ss[rtOffset + 67] != 0x01) return false; } else { // Check for a cleaned SSv1 int rtOffset = 0x204; if (ss[rtOffset + 36] != 0x01) return false; if (ss[rtOffset + 37] != 0x00) return false; if (xgdType == 2 && ss[rtOffset + 39] != 0x00) return false; if (xgdType == 2 && ss[rtOffset + 40] != 0x00) return false; if (ss[rtOffset + 45] != 0x5B) return false; if (ss[rtOffset + 46] != 0x00) return false; if (xgdType == 2 && ss[rtOffset + 48] != 0x00) return false; if (xgdType == 2 && ss[rtOffset + 49] != 0x00) return false; if (ss[rtOffset + 54] != 0xB5) return false; if (ss[rtOffset + 55] != 0x00) return false; if (xgdType == 2 && ss[rtOffset + 57] != 0x00) return false; if (xgdType == 2 && ss[rtOffset + 58] != 0x00) return false; if (ss[rtOffset + 63] != 0x0F) return false; if (ss[rtOffset + 64] != 0x01) return false; if (xgdType == 2 && ss[rtOffset + 66] != 0x00) return false; if (xgdType == 2 && ss[rtOffset + 67] != 0x00) return false; } // All angles are as expected, it is clean return true; } /// /// Clean a rawSS.bin file and write it to a file /// /// Path to the raw SS file to read from /// Path to the clean SS file to write to /// True if successful, false otherwise public static bool CleanSS(string rawSS, string cleanSS) { if (!File.Exists(rawSS)) return false; byte[] ss = File.ReadAllBytes(rawSS); if (ss.Length != 2048) return false; if (!CleanSS(ss)) return false; File.WriteAllBytes(cleanSS, ss); return true; } /// /// Fix a SS sector to its predictable clean form. /// With help from ss_sector_range /// /// Byte array of raw SS sector /// True if successful, false otherwise public static bool CleanSS(byte[] ss) { // Must be entire sector if (ss.Length != 2048) return false; // Must be a valid SS file if (!IsValidSS(ss)) return false; // Determine XGD type if (!GetXGDType(ss, out int xgdType)) return false; // Determine if XGD3 SS.bin is SSv1 (Original Kreon) or SSv2 (0800 / Repaired Kreon) #if NET20 var checkArr = new byte[72]; Array.Copy(ss, 32, checkArr, 0, 72); bool ssv2 = Array.Exists(checkArr, x => x != 0); #else bool ssv2 = ss.Skip(32).Take(72).Any(x => x != 0); #endif // Do not produce an SS hash for bad SS (SSv1 XGD3 / Unrepaired Kreon SS) if (xgdType == 3 && !ssv2) return false; switch (xgdType) { case 1: // Leave Original Xbox SS.bin unchanged return true; case 2: // Fix standard SSv1 ss.bin ss[552] = 1; // 0x01 ss[553] = 0; // 0x00 ss[555] = 0; // 0x00 ss[556] = 0; // 0x00 ss[561] = 91; // 0x5B ss[562] = 0; // 0x00 ss[564] = 0; // 0x00 ss[565] = 0; // 0x00 ss[570] = 181; // 0xB5 ss[571] = 0; // 0x00 ss[573] = 0; // 0x00 ss[574] = 0; // 0x00 ss[579] = 15; // 0x0F ss[580] = 1; // 0x01 ss[582] = 0; // 0x00 ss[583] = 0; // 0x00 return true; case 3: if (ssv2) { ss[72] = 1; // 0x01 ss[73] = 0; // 0x00 ss[75] = 1; // 0x01 ss[76] = 0; // 0x00 ss[81] = 91; // 0x5B ss[82] = 0; // 0x00 ss[84] = 91; // 0x5B ss[85] = 0; // 0x00 ss[90] = 181; // 0xB5 ss[91] = 0; // 0x00 ss[93] = 181; // 0xB5 ss[94] = 0; // 0x00 ss[99] = 15; // 0x0F ss[100] = 1; // 0x01 ss[102] = 15; // 0x0F ss[103] = 1; // 0x01 } else { ss[552] = 1; // 0x01 ss[553] = 0; // 0x00 ss[561] = 91; // 0x5B ss[562] = 0; // 0x00 ss[570] = 181; // 0xB5 ss[571] = 0; // 0x00 ss[579] = 15; // 0x0F ss[580] = 1; // 0x01 } return true; default: // Unknown XGD type return false; } } /// /// Get Security Sector ranges from SS.bin /// /// Path to SS.bin file /// Sector ranges if found, null otherwise public static string? GetSSRanges(string ssBin) { if (!File.Exists(ssBin)) return null; byte[] ss = File.ReadAllBytes(ssBin); if (ss.Length != 2048) return null; return GetSSRanges(ss); } /// /// Get Security Sector ranges from SS sector. /// With help from ss_sector_range /// /// Byte array of SS sector /// Sector ranges if found, null otherwise public static string? GetSSRanges(byte[] ss) { if (ss.Length != 2048) return null; if (!GetXGDType(ss, out int xgdType)) return null; //uint numRanges = ss[1632]; uint numRanges; if (xgdType == 1) numRanges = 16; else numRanges = 4; uint[] startLBA = new uint[numRanges]; uint[] endLBA = new uint[numRanges]; for (uint i = 0; i < numRanges; i++) { // Determine range Physical Sector Number uint startPSN = (uint)((((ss[(i * 9) + 1636] << 8) | ss[(i * 9) + 1637]) << 8) | ss[(i * 9) + 1638]); uint endPSN = (uint)((((ss[(i * 9) + 1639] << 8) | ss[(i * 9) + 1640]) << 8) | ss[(i * 9) + 1641]); // Determine range Logical Sector Number if (xgdType == 1 && startPSN >= (1913776 + 0x030000)) { // Layer 1 of XGD1 startLBA[i] = ((1913776 + 0x030000) * 2) - (startPSN ^ 0xFFFFFF) - 0x030000 - 1; endLBA[i] = ((1913776 + 0x030000) * 2) - (endPSN ^ 0xFFFFFF) - 0x030000 - 1; } else if (xgdType == 2 && startPSN >= (1913760 + 0x030000)) { // Layer 1 of XGD2 startLBA[i] = ((1913760 + 0x030000) * 2) - (startPSN ^ 0xFFFFFF) - 0x030000 - 1; endLBA[i] = ((1913760 + 0x030000) * 2) - (endPSN ^ 0xFFFFFF) - 0x030000 - 1; } else if (xgdType == 3 && startPSN >= (2133520 + 0x030000)) { // Layer 1 of XGD3 startLBA[i] = ((2133520 + 0x030000) * 2) - (startPSN ^ 0xFFFFFF) - 0x030000 - 1; endLBA[i] = ((2133520 + 0x030000) * 2) - (endPSN ^ 0xFFFFFF) - 0x030000 - 1; } else { // Layer 0 startLBA[i] = startPSN - 0x030000; endLBA[i] = endPSN - 0x030000; } } // Sort ranges for XGD1 if (xgdType == 1) Array.Sort(startLBA, endLBA); // Represent ranges as string string? ranges = null; if (xgdType == 1) { for (int i = 0; i < 16; i++) { ranges += $"{startLBA[i]}-{endLBA[i]}"; if (i != numRanges - 1) ranges += "\r\n"; } } else { ranges = $"{startLBA[0]}-{endLBA[0]}\r\n{startLBA[3]}-{endLBA[3]}"; } return ranges; } #endregion } }