Files
MPF/MPF.Frontend/Tools/PhysicalTool.cs
Deterous ca59d71e7d Print PS4/PS5 app.pkg info (#785)
* Print PS4/PS5 app.pkg info

* namespace

* Use a filestream

* Use appPkgHeaderDeserializer obj

* null check
2024-12-19 10:23:50 -05:00

703 lines
26 KiB
C#

using System;
using System.IO;
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
/// <summary>s
/// Get the last modified date for a file from a physical disc, if possible
/// </summary>
/// <param name="drive">Drive to extract information from</param>
/// <param name="filePath">Relative file path</param>
/// <returns>Output last modified date in "yyyy-mm-dd" format if possible, null on error</returns>
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
/// <summary>
/// Get if the Bus Encryption Enabled (BEE) flag is set in a path
/// </summary>
/// <param name="drive">Drive to extract information from</param>
/// <returns>Bus encryption enabled status if possible, false otherwise</returns>
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
/// <summary>
/// Get the EXE name from a PlayStation disc, if possible
/// </summary>
/// <param name="drive">Drive to extract information from</param>
/// <returns>Executable name on success, null otherwise</returns>
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;
}
/// <summary>
/// Get the serial from a PlayStation disc, if possible
/// </summary>
/// <param name="drive">Drive to extract information from</param>
/// <returns>Serial on success, null otherwise</returns>
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);
}
/// <summary>
/// Get the version from a PlayStation 2 disc, if possible
/// </summary>
/// <param name="drive">Drive to extract information from</param>
/// <returns>Game version if possible, null on error</returns>
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;
}
/// <summary>
/// Get the internal serial from a PlayStation 3 disc, if possible
/// </summary>
/// <param name="drive">Drive to extract information from</param>
/// <returns>Internal disc serial if possible, null on error</returns>
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;
}
/// <summary>
/// Get the version from a PlayStation 3 disc, if possible
/// </summary>
/// <param name="drive">Drive to extract information from</param>
/// <returns>Game version if possible, null on error</returns>
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;
}
/// <summary>
/// Get the firmware version from a PlayStation 3 disc, if possible
/// </summary>
/// <param name="drive">Drive to extract information from</param>
/// <returns>Firmware version if possible, null on error</returns>
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;
}
}
/// <summary>
/// Get the internal serial from a PlayStation 4 disc, if possible
/// </summary>
/// <param name="drive">Drive to extract information from</param>
/// <returns>Internal disc serial if possible, null on error</returns>
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;
}
}
/// <summary>
/// Get the version from a PlayStation 4 disc, if possible
/// </summary>
/// <param name="drive">Drive to extract information from</param>
/// <returns>Game version if possible, null on error</returns>
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;
}
}
/// <summary>
/// Get the app.pkg info from a PlayStation 4 disc, if possible
/// </summary>
/// <param name="drive">Drive to extract information from</param>
/// <returns>PKG info if possible, null on error</returns>
public static string? GetPlayStation4PkgInfo(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;
// Try parse the app.pkg (multiple if they exist)
try
{
string? pkgInfo = "";
string[] appDirs = Directory.GetDirectories(Path.Combine(drive.Name, "app"), "?????????", SearchOption.TopDirectoryOnly);
foreach (string dir in appDirs)
{
string appPkgPath = Path.Combine(dir, "app.pkg");
if (!File.Exists(appPkgPath))
continue;
// Read the app.pkg header
using var fileStream = new FileStream(appPkgPath, FileMode.Open, FileAccess.Read);
var appPkgHeaderDeserializer = new SabreTools.Serialization.Deserializers.AppPkgHeader();
var appPkgHeader = appPkgHeaderDeserializer.Deserialize(fileStream);
if (appPkgHeader != null)
{
byte[] date = BitConverter.GetBytes(appPkgHeader.VersionDate);
if (BitConverter.IsLittleEndian)
Array.Reverse(date);
pkgInfo = $"app.pkg ID: {appPkgHeader.ContentID}" + Environment.NewLine + $"app.pkg Date: {date[0]:X2}{date[1]:X2}-{date[2]:X2}-{date[3]:X2}";
}
}
if (pkgInfo == "")
return null;
else
return pkgInfo;
}
catch
{
// We don't care what the error was
return null;
}
}
/// <summary>
/// Get the internal serial from a PlayStation 5 disc, if possible
/// </summary>
/// <param name="drive">Drive to extract information from</param>
/// <returns>Internal disc serial if possible, null on error</returns>
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<string>()?.Insert(4, "-");
}
catch
{
// We don't care what the error was
return null;
}
}
// <summary>
/// Get the version from a PlayStation 5 disc, if possible
/// </summary>
/// <param name="drive">Drive to extract information from</param>
/// <returns>Game version if possible, null on error</returns>
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<string>();
}
catch
{
// We don't care what the error was
return null;
}
}
/// <summary>
/// Get the params.json file from a drive path, if possible
/// </summary>
/// <param name="drive">Drive to extract information from</param>
/// <returns>JObject representing the JSON on success, null on error</returns>
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);
}
/// <summary>
/// Get the params.json file from a filename, if possible
/// </summary>
/// <param name="filename">Filename to check</param>
/// <returns>JObject representing the JSON on success, null on error</returns>
private static JObject? GetPlayStation5ParamsJsonFromFile(string? filename)
{
// If the file doesn't exist
if (string.IsNullOrEmpty(filename))
return null;
if (!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;
}
}
/// <summary>
/// Get the app.pkg info from a PlayStation 5 disc, if possible
/// </summary>
/// <param name="drive">Drive to extract information from</param>
/// <returns>PKG info if possible, null on error</returns>
public static string? GetPlayStation5PkgInfo(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;
// Try parse the app_sc.pkg (multiple if they exist)
try
{
string? pkgInfo = "";
string[] appDirs = Directory.GetDirectories(Path.Combine(drive.Name, "app"), "?????????", SearchOption.TopDirectoryOnly);
foreach (string dir in appDirs)
{
string appPkgPath = Path.Combine(dir, "app_sc.pkg");
if (!File.Exists(appPkgPath))
continue;
// Read the app_sc.pkg header
using var fileStream = new FileStream(appPkgPath, FileMode.Open, FileAccess.Read);
var appPkgHeaderDeserializer = new SabreTools.Serialization.Deserializers.AppPkgHeader();
var appPkgHeader = appPkgHeaderDeserializer.Deserialize(fileStream);
if (appPkgHeader != null)
{
byte[] date = BitConverter.GetBytes(appPkgHeader.VersionDate);
if (BitConverter.IsLittleEndian)
Array.Reverse(date);
string pkgDate = $"{date[0]:X2}{date[1]:X2}-{date[2]:X2}-{date[3]:X2}";
pkgInfo = $"app_sc.pkg ID: {appPkgHeader.ContentID}" + Environment.NewLine + $"app_sc.pkg Date: {pkgDate}";
}
}
if (pkgInfo == "")
return null;
else
return pkgInfo;
}
catch
{
// We don't care what the error was
return null;
}
}
#endregion
#region Xbox
/// <summary>
/// Get all filenames for Xbox One and Xbox Series X
/// </summary>
/// <param name="drive">Drive to extract information from</param>
/// <returns>Filenames if possible, null on error</returns>
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 = Array.ConvertAll(files, Path.GetFileName);
return string.Join("\n", filenames);
}
catch
{
// We don't care what the error is right now
return null;
}
}
#endregion
}
}