using System; using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using SabreTools.IO; namespace MPF.Frontend.Tools { public static class PhysicalTool { #region Generic /// s /// Get the last modified date for a file from a physical disc, if possible /// /// Drive to extract information from /// Relative file path /// Output last modified date in "yyyy-mm-dd" format if possible, null on error public static string? GetFileDate(Drive? drive, string? filePath, bool fixTwoDigitYear = false) { // If there's no drive path, we can't do this part if (string.IsNullOrEmpty(drive?.Name)) return null; // If the folder no longer exists, we can't do this part if (!Directory.Exists(drive!.Name)) return null; // If the executable name is invalid, we can't do this part if (string.IsNullOrEmpty(filePath)) return null; // Now that we have the EXE name, try to get the fileinfo for it string exePath = Path.Combine(drive.Name, filePath); if (!File.Exists(exePath)) return null; // Get the last modified time var fi = new FileInfo(exePath); var lastModified = fi.LastWriteTimeUtc; int year = lastModified.Year; int month = lastModified.Month; int day = lastModified.Day; // Fix the Y2K timestamp issue, if required if (fixTwoDigitYear) year = year >= 1900 && year < 1920 ? 2000 + year % 100 : year; // Format and return the string var dt = new DateTime(year, month, day); return dt.ToString("yyyy-MM-dd"); } #endregion #region BD-Video /// /// Get if the Bus Encryption Enabled (BEE) flag is set in a path /// /// Drive to extract information from /// Bus encryption enabled status if possible, false otherwise public static bool GetBusEncryptionEnabled(Drive? drive) { // If there's no drive path, we can't get BEE flag if (string.IsNullOrEmpty(drive?.Name)) return false; // If the folder no longer exists, we can't get exe name if (!Directory.Exists(drive!.Name)) return false; // Get the two possible file paths #if NET20 || NET35 string content000 = Path.Combine(Path.Combine(drive.Name, "AACS"), "Content000.cer"); string content001 = Path.Combine(Path.Combine(drive.Name, "AACS"), "Content001.cer"); #else string content000 = Path.Combine(drive.Name, "AACS", "Content000.cer"); string content001 = Path.Combine(drive.Name, "AACS", "Content001.cer"); #endif try { // Check the required files if (File.Exists(content000) && new FileInfo(content000).Length > 1) { using var fs = File.OpenRead(content000); _ = fs.ReadByte(); // Skip the first byte return fs.ReadByte() > 127; } else if (File.Exists(content001) && new FileInfo(content001).Length > 1) { using var fs = File.OpenRead(content001); _ = fs.ReadByte(); // Skip the first byte return fs.ReadByte() > 127; } // False if neither file fits the criteria return false; } catch { // We don't care what the error is right now return false; } } #endregion #region PlayStation /// /// Get the EXE name from a PlayStation disc, if possible /// /// Drive to extract information from /// Executable name on success, null otherwise public static string? GetPlayStationExecutableName(Drive? drive) { // If there's no drive path, we can't get exe name if (string.IsNullOrEmpty(drive?.Name)) return null; // If the folder no longer exists, we can't get exe name if (!Directory.Exists(drive!.Name)) return null; // Get the two paths that we will need to check string psxExePath = Path.Combine(drive.Name, "PSX.EXE"); string systemCnfPath = Path.Combine(drive.Name, "SYSTEM.CNF"); // Read the CNF file as an INI file var systemCnf = new IniFile(systemCnfPath); string? bootValue = string.Empty; // PlayStation uses "BOOT" as the key if (systemCnf.ContainsKey("BOOT")) bootValue = systemCnf["BOOT"]; // PlayStation 2 uses "BOOT2" as the key if (systemCnf.ContainsKey("BOOT2")) bootValue = systemCnf["BOOT2"]; // If we had any boot value, parse it and get the executable name if (!string.IsNullOrEmpty(bootValue)) { var match = Regex.Match(bootValue, @"cdrom.?:\\?(.*)", RegexOptions.Compiled); if (match.Groups.Count > 1) { string? serial = match.Groups[1].Value; // Some games may have the EXE in a subfolder serial = Path.GetFileName(serial); return serial; } } // If the SYSTEM.CNF value can't be found, try PSX.EXE if (File.Exists(psxExePath)) return "PSX.EXE"; // If neither can be found, we return null return null; } /// /// Get the serial from a PlayStation disc, if possible /// /// Drive to extract information from /// Serial on success, null otherwise public static string? GetPlayStationSerial(Drive? drive) { // Try to get the executable name string? exeName = GetPlayStationExecutableName(drive); if (string.IsNullOrEmpty(exeName)) return null; // Handle generic PSX.EXE if (exeName == "PSX.EXE") return null; // EXE name may have a trailing `;` after // EXE name should always be in all caps exeName = exeName! .Split(';')[0] .ToUpperInvariant(); // Serial is most of the EXE name normalized return exeName .Replace('_', '-') .Replace(".", string.Empty); } /// /// Get the version from a PlayStation 2 disc, if possible /// /// Drive to extract information from /// Game version if possible, null on error public static string? GetPlayStation2Version(Drive? drive) { // If there's no drive path, we can't do this part if (string.IsNullOrEmpty(drive?.Name)) return null; // If the folder no longer exists, we can't do this part if (!Directory.Exists(drive!.Name)) return null; // Get the SYSTEM.CNF path to check string systemCnfPath = Path.Combine(drive.Name, "SYSTEM.CNF"); // Try to parse the SYSTEM.CNF file var systemCnf = new IniFile(systemCnfPath); if (systemCnf.ContainsKey("VER")) return systemCnf["VER"]; // If "VER" can't be found, we can't do much return null; } /// /// Get the internal serial from a PlayStation 3 disc, if possible /// /// Drive to extract information from /// Internal disc serial if possible, null on error public static string? GetPlayStation3Serial(Drive? drive) { // If there's no drive path, we can't do this part if (string.IsNullOrEmpty(drive?.Name)) return null; // If the folder no longer exists, we can't do this part if (!Directory.Exists(drive!.Name)) return null; // Attempt to use PS3_DISC.SFB string sfbPath = Path.Combine(drive.Name, "PS3_DISC.SFB"); if (File.Exists(sfbPath)) { try { using var br = new BinaryReader(File.OpenRead(sfbPath)); br.BaseStream.Seek(0x220, SeekOrigin.Begin); return new string(br.ReadChars(0x10)).TrimEnd('\0'); } catch { // We don't care what the error was return null; } } // Attempt to use PARAM.SFO #if NET20 || NET35 string sfoPath = Path.Combine(Path.Combine(drive.Name, "PS3_GAME"), "PARAM.SFO"); #else string sfoPath = Path.Combine(drive.Name, "PS3_GAME", "PARAM.SFO"); #endif if (File.Exists(sfoPath)) { try { using var br = new BinaryReader(File.OpenRead(sfoPath)); br.BaseStream.Seek(-0x18, SeekOrigin.End); return new string(br.ReadChars(9)).TrimEnd('\0').Insert(4, "-"); } catch { // We don't care what the error was return null; } } return null; } /// /// Get the version from a PlayStation 3 disc, if possible /// /// Drive to extract information from /// Game version if possible, null on error public static string? GetPlayStation3Version(Drive? drive) { // If there's no drive path, we can't do this part if (string.IsNullOrEmpty(drive?.Name)) return null; // If the folder no longer exists, we can't do this part if (!Directory.Exists(drive!.Name)) return null; // Attempt to use PS3_DISC.SFB string sfbPath = Path.Combine(drive.Name, "PS3_DISC.SFB"); if (File.Exists(sfbPath)) { try { using var br = new BinaryReader(File.OpenRead(sfbPath)); br.BaseStream.Seek(0x230, SeekOrigin.Begin); var discVersion = new string(br.ReadChars(0x10)).TrimEnd('\0'); if (!string.IsNullOrEmpty(discVersion)) return discVersion; } catch { // We don't care what the error was return null; } } // Attempt to use PARAM.SFO #if NET20 || NET35 string sfoPath = Path.Combine(Path.Combine(drive.Name, "PS3_GAME"), "PARAM.SFO"); #else string sfoPath = Path.Combine(drive.Name, "PS3_GAME", "PARAM.SFO"); #endif if (File.Exists(sfoPath)) { try { using var br = new BinaryReader(File.OpenRead(sfoPath)); br.BaseStream.Seek(-0x08, SeekOrigin.End); return new string(br.ReadChars(5)).TrimEnd('\0'); } catch { // We don't care what the error was return null; } } return null; } /// /// Get the firmware version from a PlayStation 3 disc, if possible /// /// Drive to extract information from /// Firmware version if possible, null on error public static string? GetPlayStation3FirmwareVersion(Drive? drive) { // If there's no drive path, we can't do this part if (string.IsNullOrEmpty(drive?.Name)) return null; // If the folder no longer exists, we can't do this part if (!Directory.Exists(drive!.Name)) return null; // Attempt to read from /PS3_UPDATE/PS3UPDAT.PUP #if NET20 || NET35 string pupPath = Path.Combine(Path.Combine(drive.Name, "PS3_UPDATE"), "PS3UPDAT.PUP"); #else string pupPath = Path.Combine(drive.Name, "PS3_UPDATE", "PS3UPDAT.PUP"); #endif if (!File.Exists(pupPath)) return null; try { using var br = new BinaryReader(File.OpenRead(pupPath)); br.BaseStream.Seek(0x3E, SeekOrigin.Begin); byte[] buf = new byte[2]; br.Read(buf, 0, 2); Array.Reverse(buf); short location = BitConverter.ToInt16(buf, 0); br.BaseStream.Seek(location, SeekOrigin.Begin); return new string(br.ReadChars(4)); } catch { // We don't care what the error was return null; } } /// /// Get the internal serial from a PlayStation 4 disc, if possible /// /// Drive to extract information from /// Internal disc serial if possible, null on error public static string? GetPlayStation4Serial(Drive? drive) { // If there's no drive path, we can't do this part if (string.IsNullOrEmpty(drive?.Name)) return null; // If the folder no longer exists, we can't do this part if (!Directory.Exists(drive!.Name)) return null; // If we can't find param.sfo, we don't have a PlayStation 4 disc #if NET20 || NET35 string paramSfoPath = Path.Combine(Path.Combine(drive.Name, "bd"), "param.sfo"); #else string paramSfoPath = Path.Combine(drive.Name, "bd", "param.sfo"); #endif if (!File.Exists(paramSfoPath)) return null; // Let's try reading param.sfo to find the serial at the end of the file try { using var br = new BinaryReader(File.OpenRead(paramSfoPath)); br.BaseStream.Seek(-0x14, SeekOrigin.End); return new string(br.ReadChars(9)).Insert(4, "-"); } catch { // We don't care what the error was return null; } } /// /// Get the version from a PlayStation 4 disc, if possible /// /// Drive to extract information from /// Game version if possible, null on error public static string? GetPlayStation4Version(Drive? drive) { // If there's no drive path, we can't do this part if (string.IsNullOrEmpty(drive?.Name)) return null; // If the folder no longer exists, we can't do this part if (!Directory.Exists(drive!.Name)) return null; // If we can't find param.sfo, we don't have a PlayStation 4 disc #if NET20 || NET35 string paramSfoPath = Path.Combine(Path.Combine(drive.Name, "bd"), "param.sfo"); #else string paramSfoPath = Path.Combine(drive.Name, "bd", "param.sfo"); #endif if (!File.Exists(paramSfoPath)) return null; // Let's try reading param.sfo to find the version at the end of the file try { using var br = new BinaryReader(File.OpenRead(paramSfoPath)); br.BaseStream.Seek(-0x08, SeekOrigin.End); return new string(br.ReadChars(5)); } catch { // We don't care what the error was return null; } } /// /// Get the internal serial from a PlayStation 5 disc, if possible /// /// Drive to extract information from /// Internal disc serial if possible, null on error public static string? GetPlayStation5Serial(Drive? drive) { // Attempt to get the param.json file var json = GetPlayStation5ParamsJsonFromDrive(drive); if (json == null) return null; try { return json["disc"]?[0]?["masterDataId"]?.Value()?.Insert(4, "-"); } catch { // We don't care what the error was return null; } } // /// Get the version from a PlayStation 5 disc, if possible /// /// Drive to extract information from /// Game version if possible, null on error public static string? GetPlayStation5Version(Drive? drive) { // Attempt to get the param.json file var json = GetPlayStation5ParamsJsonFromDrive(drive); if (json == null) return null; try { return json["masterVersion"]?.Value(); } catch { // We don't care what the error was return null; } } /// /// Get the params.json file from a drive path, if possible /// /// Drive to extract information from /// JObject representing the JSON on success, null on error private static JObject? GetPlayStation5ParamsJsonFromDrive(Drive? drive) { // If there's no drive path, we can't do this part if (string.IsNullOrEmpty(drive?.Name)) return null; // If the folder no longer exists, we can't do this part if (!Directory.Exists(drive!.Name)) return null; // If we can't find param.json, we don't have a PlayStation 5 disc #if NET20 || NET35 string paramJsonPath = Path.Combine(Path.Combine(drive.Name, "bd"), "param.json"); #else string paramJsonPath = Path.Combine(drive.Name, "bd", "param.json"); #endif return GetPlayStation5ParamsJsonFromFile(paramJsonPath); } /// /// Get the params.json file from a filename, if possible /// /// Filename to check /// JObject representing the JSON on success, null on error private static JObject? GetPlayStation5ParamsJsonFromFile(string? filename) { // If the file doesn't exist if (string.IsNullOrEmpty(filename) || !File.Exists(filename)) return null; // Let's try reading param.json to find the version in the unencrypted JSON try { using var br = new BinaryReader(File.OpenRead(filename)); br.BaseStream.Seek(0x800, SeekOrigin.Begin); byte[] jsonBytes = br.ReadBytes((int)(br.BaseStream.Length - 0x800)); return JsonConvert.DeserializeObject(Encoding.ASCII.GetString(jsonBytes)) as JObject; } catch { // We don't care what the error was return null; } } #endregion #region Xbox /// /// Get all filenames for Xbox One and Xbox Series X /// /// Drive to extract information from /// Filenames if possible, null on error public static string? GetXboxFilenames(Drive? drive) { // If there's no drive path, we can't get BEE flag if (string.IsNullOrEmpty(drive?.Name)) return null; // If the folder no longer exists, we can't get exe name if (!Directory.Exists(drive!.Name)) return null; // Get the MSXC directory path string msxc = Path.Combine(drive.Name, "MSXC"); if (!Directory.Exists(msxc)) return null; try { var files = Directory.GetFiles(msxc, "*", SearchOption.TopDirectoryOnly); var filenames = files.Select(Path.GetFileName).ToArray(); return string.Join("\n", filenames); } catch { // We don't care what the error is right now return null; } } #endregion } }