using System; using System.IO; using System.Net; using System.Reflection; using MPF.Core.Data; using Newtonsoft.Json.Linq; using SabreTools.RedumpLib.Data; namespace MPF.Core.Utilities { public static class Tools { #region Byte Arrays /// /// Search for a byte array in another array /// public static bool Contains(this byte[] stack, byte[] needle, out int position, int start = 0, int end = -1) { // Initialize the found position to -1 position = -1; // If either array is null or empty, we can't do anything if (stack == null || stack.Length == 0 || needle == null || needle.Length == 0) return false; // If the needle array is larger than the stack array, it can't be contained within if (needle.Length > stack.Length) return false; // If start or end are not set properly, set them to defaults if (start < 0) start = 0; if (end < 0) end = stack.Length - needle.Length; for (int i = start; i < end; i++) { if (stack.EqualAt(needle, i)) { position = i; return true; } } return false; } /// /// See if a byte array starts with another /// public static bool StartsWith(this byte[] stack, byte[] needle) { return stack.Contains(needle, out int _, start: 0, end: 1); } /// /// Get if a stack at a certain index is equal to a needle /// private static bool EqualAt(this byte[] stack, byte[] needle, int index) { // If we're too close to the end of the stack, return false if (needle.Length >= stack.Length - index) return false; for (int i = 0; i < needle.Length; i++) { if (stack[i + index] != needle[i]) return false; } return true; } /// /// Converts a hex string into a byte array /// /// Hex string /// Converted byte array, or null if invalid hex string public static byte[]? HexStringToByteArray(string? hexString) { // Valid hex string must be an even number of characters if (string.IsNullOrEmpty(hexString) || hexString!.Length % 2 == 1) return null; // Convert ASCII to byte via lookup table int[] hexLookup = [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F]; byte[] byteArray = new byte[hexString.Length / 2]; for (int i = 0; i < hexString.Length; i += 2) { // Convert next two chars to ASCII value relative to '0' int a = Char.ToUpper(hexString[i]) - '0'; int b = Char.ToUpper(hexString[i + 1]) - '0'; // Ensure hex string only has '0' through '9' and 'A' through 'F' (case insensitive) if ((a < 0 || b < 0 || a > 22 || b > 22) || (a > 10 && a < 17) || (b > 10 && b < 17)) return null; byteArray[i / 2] = (byte)(hexLookup[a] << 4 | hexLookup[b]); } return byteArray; } #endregion #region Support /// /// Verify that, given a system and a media type, they are correct /// public static Result GetSupportStatus(RedumpSystem? system, MediaType? type) { // No system chosen, update status if (system == null) return Result.Failure("Please select a valid system"); // If we're on an unsupported type, update the status accordingly return type switch { // Fully supported types MediaType.BluRay or MediaType.CDROM or MediaType.DVD or MediaType.FloppyDisk or MediaType.HardDisk or MediaType.CompactFlash or MediaType.SDCard or MediaType.FlashDrive or MediaType.HDDVD => Result.Success($"{type.LongName()} ready to dump"), // Partially supported types MediaType.GDROM or MediaType.NintendoGameCubeGameDisc or MediaType.NintendoWiiOpticalDisc => Result.Success($"{type.LongName()} partially supported for dumping"), // Special case for other supported tools MediaType.UMD => Result.Failure($"{type.LongName()} supported for submission info parsing"), // Specifically unknown type MediaType.NONE => Result.Failure($"Please select a valid media type"), // Undumpable but recognized types _ => Result.Failure($"{type.LongName()} media are not supported for dumping"), }; } /// /// Returns false if a given InternalProgram does not support a given MediaType /// public static bool ProgramSupportsMedia(InternalProgram program, MediaType? type) { // If the media type is not set, return false if (type == null || type == MediaType.NONE) return false; return (program) switch { // Aaru InternalProgram.Aaru when type == MediaType.BluRay => true, InternalProgram.Aaru when type == MediaType.CDROM => true, InternalProgram.Aaru when type == MediaType.CompactFlash => true, InternalProgram.Aaru when type == MediaType.DVD => true, InternalProgram.Aaru when type == MediaType.GDROM => true, InternalProgram.Aaru when type == MediaType.FlashDrive => true, InternalProgram.Aaru when type == MediaType.FloppyDisk => true, InternalProgram.Aaru when type == MediaType.HardDisk => true, InternalProgram.Aaru when type == MediaType.HDDVD => true, InternalProgram.Aaru when type == MediaType.NintendoGameCubeGameDisc => true, InternalProgram.Aaru when type == MediaType.NintendoWiiOpticalDisc => true, InternalProgram.Aaru when type == MediaType.SDCard => true, // DiscImageCreator InternalProgram.DiscImageCreator when type == MediaType.BluRay => true, InternalProgram.DiscImageCreator when type == MediaType.CDROM => true, InternalProgram.DiscImageCreator when type == MediaType.CompactFlash => true, InternalProgram.DiscImageCreator when type == MediaType.DVD => true, InternalProgram.DiscImageCreator when type == MediaType.GDROM => true, InternalProgram.DiscImageCreator when type == MediaType.FlashDrive => true, InternalProgram.DiscImageCreator when type == MediaType.FloppyDisk => true, InternalProgram.DiscImageCreator when type == MediaType.HardDisk => true, InternalProgram.DiscImageCreator when type == MediaType.HDDVD => true, InternalProgram.DiscImageCreator when type == MediaType.NintendoGameCubeGameDisc => true, InternalProgram.DiscImageCreator when type == MediaType.NintendoWiiOpticalDisc => true, InternalProgram.DiscImageCreator when type == MediaType.SDCard => true, // Redumper InternalProgram.Redumper when type == MediaType.BluRay => true, InternalProgram.Redumper when type == MediaType.CDROM => true, InternalProgram.Redumper when type == MediaType.DVD => true, InternalProgram.Redumper when type == MediaType.GDROM => true, InternalProgram.Redumper when type == MediaType.HDDVD => true, // Default _ => false, }; } #endregion #region Versioning /// /// Check for a new MPF version /// /// /// Bool representing if the values are different. /// String representing the message to display the the user. /// String representing the new release URL. /// public static (bool different, string message, string? url) CheckForNewVersion() { try { // Get current assembly version var assemblyVersion = Assembly.GetEntryAssembly()?.GetName()?.Version; if (assemblyVersion == null) return (false, "Assembly version could not be determined", null); string version = $"{assemblyVersion.Major}.{assemblyVersion.Minor}.{assemblyVersion.Build}"; // Get the latest tag from GitHub var (tag, url) = GetRemoteVersionAndUrl(); bool different = version != tag && tag != null; string message = $"Local version: {version}" + $"{Environment.NewLine}Remote version: {tag}" + (different ? $"{Environment.NewLine}The update URL has been added copied to your clipboard" : $"{Environment.NewLine}You have the newest version!"); return (different, message, url); } catch (Exception ex) { return (false, ex.ToString(), null); } } /// /// Get the current informational version formatted as a string /// public static string? GetCurrentVersion() { try { var assembly = Assembly.GetEntryAssembly(); if (assembly == null) return null; var assemblyVersion = Attribute.GetCustomAttribute(assembly, typeof(AssemblyInformationalVersionAttribute)) as AssemblyInformationalVersionAttribute; return assemblyVersion?.InformationalVersion; } catch (Exception ex) { return ex.ToString(); } } /// /// Get the latest version of MPF from GitHub and the release URL /// private static (string? tag, string? url) GetRemoteVersionAndUrl() { #if NET20 || NET35 || NET40 // Not supported in .NET Frameworks 2.0, 3.5, or 4.0 return (null, null); #else using var hc = new System.Net.Http.HttpClient(); #if NET452 ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12; #endif // TODO: Figure out a better way than having this hardcoded... string url = "https://api.github.com/repos/SabreTools/MPF/releases/latest"; var message = new System.Net.Http.HttpRequestMessage(System.Net.Http.HttpMethod.Get, url); message.Headers.Add("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:64.0) Gecko/20100101 Firefox/64.0"); var latestReleaseJsonString = hc.SendAsync(message)?.ConfigureAwait(false).GetAwaiter().GetResult() .Content?.ReadAsStringAsync().ConfigureAwait(false).GetAwaiter().GetResult(); if (latestReleaseJsonString == null) return (null, null); var latestReleaseJson = JObject.Parse(latestReleaseJsonString); if (latestReleaseJson == null) return (null, null); var latestTag = latestReleaseJson["tag_name"]?.ToString(); var releaseUrl = latestReleaseJson["html_url"]?.ToString(); return (latestTag, releaseUrl); #endif } #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()) != null && line.Trim().StartsWith("get_dec_key succeeded!") == false) ; if (line == null) return false; // Look for Disc Key in log while ((line = sr.ReadLine()) != null && line.Trim().StartsWith("disc_key = ") == false) ; // If end of file reached, no key found if (line == null) return false; // Get Disc Key from log string discKeyStr = line.Substring("disc_key = ".Length); // Validate Disc Key from log if (discKeyStr.Length != 32) return false; // Convert Disc Key to byte array key = discKeyStr; if (key == null) return false; // Read Disc ID while ((line = sr.ReadLine()) != null && line.Trim().StartsWith("disc_id = ") == false) ; // If end of file reached, no ID found if (line == null) return false; // Get Disc ID from log string discIDStr = line.Substring("disc_id = ".Length); // Validate Disc ID from log if (discIDStr.Length != 32) return false; // Replace X's in Disc ID with 00000001 discIDStr = discIDStr.Substring(0, 24) + "00000001"; // Convert Disc ID to byte array id = discIDStr; if (id == null) return false; // Look for PIC in log while ((line = sr.ReadLine()) != null && line.Trim().StartsWith("PIC:") == false) ; // If end of file reached, no PIC found if (line == null) return false; // Get PIC from log string discPICStr = ""; for (int i = 0; i < 9; i++) discPICStr += sr.ReadLine(); if (discPICStr == null) return false; // Validate PIC from log if (discPICStr.Length != 264) return false; // Convert PIC to byte array pic = discPICStr; if (pic == null) return false; // Double check for warnings in .getkey.log while ((line = sr.ReadLine()) != null) { string t = line.Trim(); if (t.StartsWith("WARNING")) return false; else if (t.StartsWith("SUCCESS")) return true; } } catch { // We are not concerned with the error 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 = Tools.HexStringToByteArray(keyString); id = Tools.HexStringToByteArray(idString); pic = Tools.HexStringToByteArray(picString.Substring(0, 230)); 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 cleandiscID = cleandiscID.Substring(0, 24) + "00000001"; // Convert to byte array, null if invalid hex string byte[]? id = Tools.HexStringToByteArray(cleandiscID); return id; } /// /// 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 byte[]? key = Tools.HexStringToByteArray(cleanHexKey); return key; } /// /// 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); if (cleanPIC.Length < 230) return null; // Convert to byte array, null if invalid hex string byte[]? pic = Tools.HexStringToByteArray(cleanPIC.Substring(0, 230)); return pic; } /// /// 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 = Tools.HexStringToByteArray(inputCRC32); if (crc32 == null || crc32.Length != 4) return null; return (uint)(0x01000000 * crc32[0] + 0x00010000 * crc32[1] + 0x00000100 * crc32[2] + 0x00000001 * crc32[3]); } #endregion } }