mirror of
https://github.com/SabreTools/MPF.git
synced 2026-02-16 05:44:56 +00:00
865 lines
31 KiB
C#
865 lines
31 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
#if NET462_OR_GREATER || NETCOREAPP
|
|
using Microsoft.Management.Infrastructure;
|
|
using Microsoft.Management.Infrastructure.Generic;
|
|
#endif
|
|
using Newtonsoft.Json;
|
|
using Newtonsoft.Json.Linq;
|
|
using SabreTools.IO;
|
|
using SabreTools.RedumpLib.Data;
|
|
|
|
namespace MPF.Frontend
|
|
{
|
|
/// <summary>
|
|
/// Represents information for a single drive
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// TODO: Can the Aaru models be used instead of the ones I've created here?
|
|
/// </remarks>
|
|
public class Drive
|
|
{
|
|
#region Fields
|
|
|
|
/// <summary>
|
|
/// Represents drive type
|
|
/// </summary>
|
|
public InternalDriveType? InternalDriveType { get; set; }
|
|
|
|
/// <summary>
|
|
/// Drive partition format
|
|
/// </summary>
|
|
public string? DriveFormat { get; private set; } = null;
|
|
|
|
/// <summary>
|
|
/// Windows drive path
|
|
/// </summary>
|
|
public string? Name { get; private set; } = null;
|
|
|
|
/// <summary>
|
|
/// Represents if Windows has marked the drive as active
|
|
/// </summary>
|
|
public bool MarkedActive { get; private set; } = false;
|
|
|
|
/// <summary>
|
|
/// Represents the total size of the drive
|
|
/// </summary>
|
|
public long TotalSize { get; private set; } = default;
|
|
|
|
/// <summary>
|
|
/// Media label as read by Windows
|
|
/// </summary>
|
|
/// <remarks>The try/catch is needed because Windows will throw an exception if the drive is not marked as active</remarks>
|
|
public string? VolumeLabel { get; private set; } = null;
|
|
|
|
#endregion
|
|
|
|
#region Derived Fields
|
|
|
|
/// <summary>
|
|
/// Read-only access to the drive letter
|
|
/// </summary>
|
|
/// <remarks>Should only be used in UI applications</remarks>
|
|
public char? Letter => Name?[0] ?? '\0';
|
|
|
|
#endregion
|
|
|
|
/// <summary>
|
|
/// Protected constructor
|
|
/// </summary>
|
|
protected Drive() { }
|
|
|
|
/// <summary>
|
|
/// Create a new Drive object from a drive type and device path
|
|
/// </summary>
|
|
/// <param name="driveType">InternalDriveType value representing the drive type</param>
|
|
/// <param name="devicePath">Path to the device according to the local machine</param>
|
|
public static Drive? Create(InternalDriveType? driveType, string devicePath)
|
|
{
|
|
// Create a new, empty drive object
|
|
var drive = new Drive()
|
|
{
|
|
InternalDriveType = driveType,
|
|
};
|
|
|
|
// If we have an invalid device path, return null
|
|
if (string.IsNullOrEmpty(devicePath))
|
|
return null;
|
|
|
|
// Sanitize a Windows-formatted long device path
|
|
if (devicePath.StartsWith("\\\\.\\"))
|
|
devicePath = devicePath.Substring("\\\\.\\".Length);
|
|
|
|
// Create and validate the drive info object
|
|
var driveInfo = new DriveInfo(devicePath);
|
|
if (driveInfo == null || driveInfo == default)
|
|
return null;
|
|
|
|
// Fill in the rest of the data
|
|
drive.PopulateFromDriveInfo(driveInfo);
|
|
|
|
return drive;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Populate all fields from a DriveInfo object
|
|
/// </summary>
|
|
/// <param name="driveInfo">DriveInfo object to populate from</param>
|
|
private void PopulateFromDriveInfo(DriveInfo? driveInfo)
|
|
{
|
|
// If we have an invalid DriveInfo, just return
|
|
if (driveInfo == null || driveInfo == default)
|
|
return;
|
|
|
|
// Populate the data fields
|
|
Name = driveInfo.Name;
|
|
MarkedActive = driveInfo.IsReady;
|
|
if (MarkedActive)
|
|
{
|
|
DriveFormat = driveInfo.DriveFormat;
|
|
TotalSize = driveInfo.TotalSize;
|
|
VolumeLabel = driveInfo.VolumeLabel;
|
|
}
|
|
else
|
|
{
|
|
DriveFormat = string.Empty;
|
|
TotalSize = default;
|
|
VolumeLabel = string.Empty;
|
|
}
|
|
}
|
|
|
|
#region Public Functionality
|
|
|
|
/// <summary>
|
|
/// Create a list of active drives matched to their volume labels
|
|
/// </summary>
|
|
/// <param name="ignoreFixedDrives">True to ignore fixed drives from population, false otherwise</param>
|
|
/// <returns>Active drives, matched to labels, if possible</returns>
|
|
public static List<Drive> CreateListOfDrives(bool ignoreFixedDrives)
|
|
{
|
|
var drives = GetDriveList(ignoreFixedDrives);
|
|
drives = [.. drives.OrderBy(i => i == null ? "\0" : i.Name)];
|
|
return drives;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the current media type from drive letter
|
|
/// </summary>
|
|
/// <param name="system"></param>
|
|
/// <returns></returns>
|
|
public (MediaType?, string?) GetMediaType(RedumpSystem? system)
|
|
{
|
|
// Take care of the non-optical stuff first
|
|
switch (InternalDriveType)
|
|
{
|
|
case Frontend.InternalDriveType.Floppy:
|
|
return (MediaType.FloppyDisk, null);
|
|
case Frontend.InternalDriveType.HardDisk:
|
|
return (MediaType.HardDisk, null);
|
|
case Frontend.InternalDriveType.Removable:
|
|
return (MediaType.FlashDrive, null);
|
|
}
|
|
|
|
// Some systems should default to certain media types
|
|
switch (system)
|
|
{
|
|
// CD
|
|
case RedumpSystem.Panasonic3DOInteractiveMultiplayer:
|
|
case RedumpSystem.PhilipsCDi:
|
|
case RedumpSystem.SegaDreamcast:
|
|
case RedumpSystem.SegaSaturn:
|
|
case RedumpSystem.SonyPlayStation:
|
|
case RedumpSystem.VideoCD:
|
|
return (MediaType.CDROM, null);
|
|
|
|
// DVD
|
|
case RedumpSystem.DVDAudio:
|
|
case RedumpSystem.DVDVideo:
|
|
case RedumpSystem.MicrosoftXbox:
|
|
case RedumpSystem.MicrosoftXbox360:
|
|
return (MediaType.DVD, null);
|
|
|
|
// HD-DVD
|
|
case RedumpSystem.HDDVDVideo:
|
|
return (MediaType.HDDVD, null);
|
|
|
|
// Blu-ray
|
|
case RedumpSystem.BDVideo:
|
|
case RedumpSystem.MicrosoftXboxOne:
|
|
case RedumpSystem.MicrosoftXboxSeriesXS:
|
|
case RedumpSystem.SonyPlayStation3:
|
|
case RedumpSystem.SonyPlayStation4:
|
|
case RedumpSystem.SonyPlayStation5:
|
|
return (MediaType.BluRay, null);
|
|
|
|
// GameCube
|
|
case RedumpSystem.NintendoGameCube:
|
|
return (MediaType.NintendoGameCubeGameDisc, null);
|
|
|
|
// Wii
|
|
case RedumpSystem.NintendoWii:
|
|
return (MediaType.NintendoWiiOpticalDisc, null);
|
|
|
|
// WiiU
|
|
case RedumpSystem.NintendoWiiU:
|
|
return (MediaType.NintendoWiiUOpticalDisc, null);
|
|
|
|
// PSP
|
|
case RedumpSystem.SonyPlayStationPortable:
|
|
return (MediaType.UMD, null);
|
|
}
|
|
|
|
// Handle optical media by size and filesystem
|
|
if (TotalSize >= 0 && TotalSize <= 800_000_000 && (DriveFormat == "CDFS" || DriveFormat == "UDF"))
|
|
return (MediaType.CDROM, null);
|
|
else if (TotalSize > 800_000_000 && TotalSize <= 8_540_000_000 && (DriveFormat == "CDFS" || DriveFormat == "UDF"))
|
|
return (MediaType.DVD, null);
|
|
else if (TotalSize > 8_540_000_000)
|
|
return (MediaType.BluRay, null);
|
|
|
|
return (null, "Could not determine media type!");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Refresh the current drive information based on path
|
|
/// </summary>
|
|
public void RefreshDrive()
|
|
{
|
|
var driveInfo = DriveInfo.GetDrives().FirstOrDefault(d => d?.Name == Name);
|
|
PopulateFromDriveInfo(driveInfo);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Information Extraction
|
|
|
|
/// <summary>
|
|
/// Get the EXE name from a PlayStation disc, if possible
|
|
/// </summary>
|
|
/// <returns>Executable name on success, null otherwise</returns>
|
|
public string? GetPlayStationExecutableName()
|
|
{
|
|
// If there's no drive path, we can't get exe name
|
|
if (string.IsNullOrEmpty(Name))
|
|
return null;
|
|
|
|
// If the folder no longer exists, we can't get exe name
|
|
if (!Directory.Exists(Name))
|
|
return null;
|
|
|
|
// Get the two paths that we will need to check
|
|
string psxExePath = Path.Combine(Name, "PSX.EXE");
|
|
string systemCnfPath = Path.Combine(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 EXE date from a PlayStation disc, if possible
|
|
/// </summary>
|
|
/// <param name="serial">Internal disc serial, if possible</param>
|
|
/// <param name="region">Output region, if possible</param>
|
|
/// <param name="date">Output EXE date in "yyyy-mm-dd" format if possible, null on error</param>
|
|
/// <returns>True if information could be determined, false otherwise</returns>
|
|
public bool GetPlayStationExecutableInfo(out string? serial, out Region? region, out string? date)
|
|
{
|
|
serial = null; region = null; date = null;
|
|
|
|
// If there's no drive path, we can't do this part
|
|
if (string.IsNullOrEmpty(Name))
|
|
return false;
|
|
|
|
// If the folder no longer exists, we can't do this part
|
|
if (!Directory.Exists(Name))
|
|
return false;
|
|
|
|
// Get the executable name
|
|
string? exeName = GetPlayStationExecutableName();
|
|
|
|
// If no executable found, we can't do this part
|
|
if (exeName == null)
|
|
return false;
|
|
|
|
// 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
|
|
serial = exeName
|
|
.Replace('_', '-')
|
|
.Replace(".", string.Empty);
|
|
|
|
// Get the region, if possible
|
|
region = GetPlayStationRegion(exeName);
|
|
|
|
// Now that we have the EXE name, try to get the fileinfo for it
|
|
string exePath = Path.Combine(Name, exeName);
|
|
if (!File.Exists(exePath))
|
|
return false;
|
|
|
|
// Fix the Y2K timestamp issue
|
|
var fi = new FileInfo(exePath);
|
|
var dt = new DateTime(fi.LastWriteTimeUtc.Year >= 1900 && fi.LastWriteTimeUtc.Year < 1920 ? 2000 + fi.LastWriteTimeUtc.Year % 100 : fi.LastWriteTimeUtc.Year,
|
|
fi.LastWriteTimeUtc.Month, fi.LastWriteTimeUtc.Day);
|
|
date = dt.ToString("yyyy-MM-dd");
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determine the region based on the PlayStation serial code
|
|
/// </summary>
|
|
/// <param name="serial">PlayStation serial code</param>
|
|
/// <returns>Region mapped from name, if possible</returns>
|
|
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];
|
|
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,
|
|
};
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the version from a PlayStation 2 disc, if possible
|
|
/// </summary>
|
|
/// <returns>Game version if possible, null on error</returns>
|
|
public string? GetPlayStation2Version()
|
|
{
|
|
// If there's no drive path, we can't do this part
|
|
if (string.IsNullOrEmpty(Name))
|
|
return null;
|
|
|
|
// If the folder no longer exists, we can't do this part
|
|
if (!Directory.Exists(Name))
|
|
return null;
|
|
|
|
// Get the SYSTEM.CNF path to check
|
|
string systemCnfPath = Path.Combine(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>
|
|
/// <returns>Internal disc serial if possible, null on error</returns>
|
|
public string? GetPlayStation3Serial()
|
|
{
|
|
// If there's no drive path, we can't do this part
|
|
if (string.IsNullOrEmpty(Name))
|
|
return null;
|
|
|
|
// If the folder no longer exists, we can't do this part
|
|
if (!Directory.Exists(Name))
|
|
return null;
|
|
|
|
// Attempt to use PS3_DISC.SFB
|
|
string sfbPath = Path.Combine(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(Name, "PS3_GAME"), "PARAM.SFO");
|
|
#else
|
|
string sfoPath = Path.Combine(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>
|
|
/// <returns>Game version if possible, null on error</returns>
|
|
public string? GetPlayStation3Version()
|
|
{
|
|
// If there's no drive path, we can't do this part
|
|
if (string.IsNullOrEmpty(Name))
|
|
return null;
|
|
|
|
// If the folder no longer exists, we can't do this part
|
|
if (!Directory.Exists(Name))
|
|
return null;
|
|
|
|
// Attempt to use PS3_DISC.SFB
|
|
string sfbPath = Path.Combine(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(Name, "PS3_GAME"), "PARAM.SFO");
|
|
#else
|
|
string sfoPath = Path.Combine(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>
|
|
/// <returns>Firmware version if possible, null on error</returns>
|
|
public string? GetPlayStation3FirmwareVersion()
|
|
{
|
|
// If there's no drive path, we can't do this part
|
|
if (string.IsNullOrEmpty(Name))
|
|
return null;
|
|
|
|
// If the folder no longer exists, we can't do this part
|
|
if (!Directory.Exists(Name))
|
|
return null;
|
|
|
|
// Attempt to read from /PS3_UPDATE/PS3UPDAT.PUP
|
|
#if NET20 || NET35
|
|
string pupPath = Path.Combine(Path.Combine(Name, "PS3_UPDATE"), "PS3UPDAT.PUP");
|
|
#else
|
|
string pupPath = Path.Combine(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>
|
|
/// <returns>Internal disc serial if possible, null on error</returns>
|
|
public string? GetPlayStation4Serial()
|
|
{
|
|
// If there's no drive path, we can't do this part
|
|
if (string.IsNullOrEmpty(Name))
|
|
return null;
|
|
|
|
// If the folder no longer exists, we can't do this part
|
|
if (!Directory.Exists(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(Name, "bd"), "param.sfo");
|
|
#else
|
|
string paramSfoPath = Path.Combine(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>
|
|
/// <returns>Game version if possible, null on error</returns>
|
|
public string? GetPlayStation4Version()
|
|
{
|
|
// If there's no drive path, we can't do this part
|
|
if (string.IsNullOrEmpty(Name))
|
|
return null;
|
|
|
|
// If the folder no longer exists, we can't do this part
|
|
if (!Directory.Exists(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(Name, "bd"), "param.sfo");
|
|
#else
|
|
string paramSfoPath = Path.Combine(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 internal serial from a PlayStation 5 disc, if possible
|
|
/// </summary>
|
|
/// <returns>Internal disc serial if possible, null on error</returns>
|
|
public string? GetPlayStation5Serial()
|
|
{
|
|
// Attempt to get the param.json file
|
|
var json = GetPlayStation5ParamsJsonFromDrive();
|
|
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>
|
|
/// <returns>Game version if possible, null on error</returns>
|
|
public string? GetPlayStation5Version()
|
|
{
|
|
// Attempt to get the param.json file
|
|
var json = GetPlayStation5ParamsJsonFromDrive();
|
|
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>
|
|
/// <returns>JObject representing the JSON on success, null on error</returns>
|
|
private JObject? GetPlayStation5ParamsJsonFromDrive()
|
|
{
|
|
// If there's no drive path, we can't do this part
|
|
if (string.IsNullOrEmpty(Name))
|
|
return null;
|
|
|
|
// If the folder no longer exists, we can't do this part
|
|
if (!Directory.Exists(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(Name, "bd"), "param.json");
|
|
#else
|
|
string paramJsonPath = Path.Combine(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) || !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 Helpers
|
|
|
|
/// <summary>
|
|
/// Get all current attached Drives
|
|
/// </summary>
|
|
/// <param name="ignoreFixedDrives">True to ignore fixed drives from population, false otherwise</param>
|
|
/// <returns>List of drives, null on error</returns>
|
|
/// <remarks>
|
|
/// https://stackoverflow.com/questions/3060796/how-to-distinguish-between-usb-and-floppy-devices?utm_medium=organic&utm_source=google_rich_qa&utm_campaign=google_rich_qa
|
|
/// https://msdn.microsoft.com/en-us/library/aa394173(v=vs.85).aspx
|
|
/// </remarks>
|
|
private static List<Drive> GetDriveList(bool ignoreFixedDrives)
|
|
{
|
|
var desiredDriveTypes = new List<DriveType>() { DriveType.CDRom };
|
|
if (!ignoreFixedDrives)
|
|
{
|
|
desiredDriveTypes.Add(DriveType.Fixed);
|
|
desiredDriveTypes.Add(DriveType.Removable);
|
|
}
|
|
|
|
// TODO: Reduce reliance on `DriveInfo`
|
|
// https://github.com/aaru-dps/Aaru/blob/5164a154e2145941472f2ee0aeb2eff3338ecbb3/Aaru.Devices/Windows/ListDevices.cs#L66
|
|
|
|
// Create an output drive list
|
|
var drives = new List<Drive>();
|
|
|
|
// Get all standard supported drive types
|
|
try
|
|
{
|
|
drives = DriveInfo.GetDrives()
|
|
.Where(d => desiredDriveTypes.Contains(d.DriveType))
|
|
.Select(d => Create(ToInternalDriveType(d.DriveType), d.Name) ?? new Drive())
|
|
.ToList();
|
|
}
|
|
catch
|
|
{
|
|
return drives;
|
|
}
|
|
|
|
// Find and update all floppy drives
|
|
#if NET462_OR_GREATER || NETCOREAPP
|
|
try
|
|
{
|
|
CimSession session = CimSession.Create(null);
|
|
var collection = session.QueryInstances("root\\CIMV2", "WQL", "SELECT * FROM Win32_LogicalDisk");
|
|
|
|
foreach (CimInstance instance in collection)
|
|
{
|
|
CimKeyedCollection<CimProperty> properties = instance.CimInstanceProperties;
|
|
uint? mediaType = properties["MediaType"]?.Value as uint?;
|
|
if (mediaType != null && ((mediaType > 0 && mediaType < 11) || (mediaType > 12 && mediaType < 22)))
|
|
{
|
|
char devId = (properties["Caption"].Value as string ?? string.Empty)[0];
|
|
drives.ForEach(d => { if (d?.Name != null && d.Name[0] == devId) { d.InternalDriveType = Frontend.InternalDriveType.Floppy; } });
|
|
}
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// No-op
|
|
}
|
|
#endif
|
|
|
|
return drives;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Convert drive type to internal version, if possible
|
|
/// </summary>
|
|
/// <param name="driveType">DriveType value to check</param>
|
|
/// <returns>InternalDriveType, if possible, null on error</returns>
|
|
internal static InternalDriveType? ToInternalDriveType(DriveType driveType)
|
|
{
|
|
return driveType switch
|
|
{
|
|
DriveType.CDRom => (InternalDriveType?)Frontend.InternalDriveType.Optical,
|
|
DriveType.Fixed => (InternalDriveType?)Frontend.InternalDriveType.HardDisk,
|
|
DriveType.Removable => (InternalDriveType?)Frontend.InternalDriveType.Removable,
|
|
_ => null,
|
|
};
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|