Files
MPF/DICUI.Library/Utilities/DumpEnvironment.cs
2019-05-20 00:12:06 -07:00

2129 lines
88 KiB
C#

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using DICUI.Data;
using DICUI.Web;
using Newtonsoft.Json;
namespace DICUI.Utilities
{
/// <summary>
/// Represents information for a single drive
/// </summary>
public class Drive
{
public char Letter { get; private set; }
public bool IsFloppy { get; private set; }
public string VolumeLabel { get; private set; }
public bool MarkedActive { get; private set; }
private Drive(char letter, string volumeLabel, bool isFloppy, bool markedActive)
{
this.Letter = letter;
this.IsFloppy = isFloppy;
this.VolumeLabel = volumeLabel;
this.MarkedActive = markedActive;
}
public static Drive Floppy(char letter) => new Drive(letter, null, true, true);
public static Drive Optical(char letter, string volumeLabel, bool active) => new Drive(letter, volumeLabel, false, active);
}
/// <summary>
/// Represents the state of all settings to be used during dumping
/// </summary>
public class DumpEnvironment
{
// Tool paths
public string DICPath;
public string SubdumpPath;
// Output paths
public string OutputDirectory;
public string OutputFilename;
// UI information
public Drive Drive;
public KnownSystem? System;
public MediaType? Type;
public bool IsFloppy { get => Drive.IsFloppy; }
public Parameters DICParameters;
// extra DIC arguments
public bool QuietMode;
public bool ParanoidMode;
public bool ScanForProtection;
public int RereadAmountC2;
// Redump login information
public string Username;
public string Password;
public bool HasRedumpLogin { get => !string.IsNullOrWhiteSpace(Username) && !string.IsNullOrWhiteSpace(Password); }
// External process information
private Process dicProcess;
#region Public Functionality
/// <summary>
/// Cancel an in-progress dumping process
/// </summary>
public void CancelDumping()
{
try
{
if (dicProcess != null && !dicProcess.HasExited)
dicProcess.Kill();
}
catch
{ }
}
/// <summary>
/// Eject the disc using DIC
/// </summary>
public async void EjectDisc()
{
// Validate that the required program exists
if (!File.Exists(DICPath))
return;
CancelDumping();
// Validate we're not trying to eject a floppy disk
if (IsFloppy)
return;
Process childProcess;
await Task.Run(() =>
{
childProcess = new Process()
{
StartInfo = new ProcessStartInfo()
{
FileName = DICPath,
Arguments = DICCommandStrings.Eject + " " + Drive.Letter,
CreateNoWindow = true,
UseShellExecute = false,
RedirectStandardInput = true,
RedirectStandardOutput = true,
},
};
childProcess.Start();
childProcess.WaitForExit(1000);
// Just in case, we want to push a button 5 times to clear any errors
for (int i = 0; i < 5; i++)
childProcess.StandardInput.WriteLine("Y");
childProcess.Dispose();
});
}
/// <summary>
/// Gets if the current drive has the latest firmware
/// </summary>
/// <returns></returns>
public async Task<bool> DriveHasLatestFimrware()
{
// Validate that the required program exists
if (!File.Exists(DICPath))
return false;
// Use the drive speed command as a quick test
Process childProcess;
string output = await Task.Run(() =>
{
childProcess = new Process()
{
StartInfo = new ProcessStartInfo()
{
FileName = DICPath,
Arguments = DICCommandStrings.DriveSpeed + " " + Drive.Letter,
CreateNoWindow = true,
UseShellExecute = false,
RedirectStandardInput = true,
RedirectStandardOutput = true,
},
};
childProcess.Start();
childProcess.WaitForExit(1000);
// Just in case, we want to push a button 5 times to clear any errors
for (int i = 0; i < 5; i++)
childProcess.StandardInput.WriteLine("Y");
string stdout = childProcess.StandardOutput.ReadToEnd();
childProcess.Dispose();
return stdout;
});
// If we get the firmware message
if (output.Contains("[ERROR] This drive isn't latest firmware. Please update."))
return false;
// Otherwise, we know the firmware's good
return true;
}
/// <summary>
/// Get the full parameter string for DIC
/// </summary>
/// <param name="driveSpeed">Nullable int representing the drive speed</param>
/// <returns>String representing the params, null on error</returns>
public string GetFullParameters(int? driveSpeed)
{
// Populate with the correct params for inputs (if we're not on the default option)
if (System != KnownSystem.NONE && Type != MediaType.NONE)
{
// If drive letter is invalid, skip this
if (Drive == null)
return null;
FixOutputPaths();
// Set the proper parameters
DICParameters = new Parameters(System, Type, Drive.Letter, Path.Combine(OutputDirectory, OutputFilename), driveSpeed, ParanoidMode, RereadAmountC2);
if (QuietMode)
DICParameters[DICFlag.DisableBeep] = true;
// Generate and return the param string
return DICParameters.GenerateParameters();
}
return null;
}
/// <summary>
/// Execute a complete dump workflow
/// </summary>
public async Task<Result> StartDumping(IProgress<Result> progress)
{
Result result = IsValidForDump();
// Execute DIC and external tools, if needed
if (Validators.GetSupportStatus(System, Type)
&& !result.Message.Contains("not supported") // Completely unsupported media
&& !result.Message.Contains("submission info")) // Submission info-only media
{
// If the environment is invalid, return
if (!result)
return result;
progress?.Report(Result.Success("Executing DiscImageCreator... please wait!"));
await Task.Run(() => ExecuteDiskImageCreator());
progress?.Report(Result.Success("DiscImageCreator has finished!"));
// Execute additional tools
progress?.Report(Result.Success("Running any additional tools... please wait!"));
result = await Task.Run(() => ExecuteAdditionalToolsAfterDIC());
progress?.Report(result);
}
// Verify dump output and save it
progress?.Report(Result.Success("Gathering submission information... please wait!"));
result = await Task.Run(() => VerifyAndSaveDumpOutput(progress));
progress?.Report(Result.Success("All submission information gathered!"));
return result;
}
#endregion
#region Public for Testing Purposes
/// <summary>
/// Fix output paths to strip out any invalid characters
/// </summary>
public void FixOutputPaths()
{
try
{
// Cache if we had a directory separator or not
bool endedWithDirectorySeparator = OutputDirectory.EndsWith(Path.DirectorySeparatorChar.ToString());
// Normalize the path so the below filters work better
string combinedPath = Path.Combine(OutputDirectory, OutputFilename);
OutputDirectory = Path.GetDirectoryName(combinedPath);
OutputFilename = Path.GetFileName(combinedPath);
// If we have either a blank filename or path, just exit
if (String.IsNullOrWhiteSpace(OutputFilename) || String.IsNullOrWhiteSpace(OutputDirectory))
return;
// Take care of extra path characters
OutputDirectory = new StringBuilder(OutputDirectory.Replace('.', '_').Replace('&', '_'))
.Replace(':', '_', 0, OutputDirectory.LastIndexOf(':') == -1 ? 0 : OutputDirectory.LastIndexOf(':')).ToString();
OutputFilename = new StringBuilder(OutputFilename.Replace('&', '_'))
.Replace('.', '_', 0, OutputFilename.LastIndexOf('.') == -1 ? 0 : OutputFilename.LastIndexOf('.')).ToString();
// Sanitize everything else
foreach (char c in Path.GetInvalidPathChars())
OutputDirectory = OutputDirectory.Replace(c, '_');
foreach (char c in Path.GetInvalidFileNameChars())
OutputFilename = OutputFilename.Replace(c, '_');
// If we had a directory separator at the end before, add it again
if (endedWithDirectorySeparator)
OutputDirectory += Path.DirectorySeparatorChar;
}
catch
{
// We don't care what the error was
return;
}
}
/// <summary>
/// Checks if the parameters are valid
/// </summary>
/// <returns>True if the configuration is valid, false otherwise</returns>
public bool ParametersValid()
{
return DICParameters.IsValid() && !(IsFloppy ^ Type == MediaType.FloppyDisk);
}
/// <summary>
/// Ensures that all required output files have been created
/// </summary>
/// <returns></returns>
public bool FoundAllFiles()
{
// First, sanitized the output filename to strip off any potential extension
string outputFilename = Path.GetFileNameWithoutExtension(OutputFilename);
// Some disc types are audio-only
bool audioOnly = (System == KnownSystem.AtariJaguarCD) || (System == KnownSystem.AudioCD);
// Now ensure that all required files exist
string combinedBase = Path.Combine(OutputDirectory, outputFilename);
switch (Type)
{
case MediaType.CDROM:
case MediaType.GDROM: // TODO: Verify GD-ROM outputs this
// return File.Exists(combinedBase + ".c2") // Doesn't output on Linux
return File.Exists(combinedBase + ".ccd")
&& File.Exists(combinedBase + ".cue")
&& File.Exists(combinedBase + ".dat")
&& File.Exists(combinedBase + ".img")
&& (audioOnly || File.Exists(combinedBase + ".img_EdcEcc.txt") || File.Exists(combinedBase + ".img_EccEdc.txt"))
&& (audioOnly || File.Exists(combinedBase + ".scm"))
&& File.Exists(combinedBase + ".sub")
// && File.Exists(combinedBase + "_c2Error.txt") // Doesn't output on Linux
&& File.Exists(combinedBase + "_cmd.txt")
&& File.Exists(combinedBase + "_disc.txt")
&& File.Exists(combinedBase + "_drive.txt")
&& File.Exists(combinedBase + "_img.cue")
&& File.Exists(combinedBase + "_mainError.txt")
&& File.Exists(combinedBase + "_mainInfo.txt")
&& File.Exists(combinedBase + "_subError.txt")
&& File.Exists(combinedBase + "_subInfo.txt")
// && File.Exists(combinedBase + "_subIntention.txt") // Not guaranteed output
&& (File.Exists(combinedBase + "_subReadable.txt") || File.Exists(combinedBase + "_sub.txt"))
&& File.Exists(combinedBase + "_volDesc.txt");
case MediaType.DVD:
case MediaType.HDDVD:
case MediaType.BluRay:
case MediaType.NintendoGameCubeGameDisc:
case MediaType.NintendoWiiOpticalDisc:
return File.Exists(combinedBase + ".dat")
&& File.Exists(combinedBase + "_cmd.txt")
&& File.Exists(combinedBase + "_disc.txt")
&& File.Exists(combinedBase + "_drive.txt")
&& File.Exists(combinedBase + "_mainError.txt")
&& File.Exists(combinedBase + "_mainInfo.txt")
&& File.Exists(combinedBase + "_volDesc.txt");
case MediaType.FloppyDisk:
return File.Exists(combinedBase + ".dat")
&& File.Exists(combinedBase + "_cmd.txt")
&& File.Exists(combinedBase + "_disc.txt");
case MediaType.UMD:
return File.Exists(combinedBase + "_disc.txt")
|| File.Exists(combinedBase + "_mainError.txt")
|| File.Exists(combinedBase + "_mainInfo.txt")
|| File.Exists(combinedBase + "_volDesc.txt");
default:
// Non-dumping commands will usually produce no output, so this is irrelevant
return true;
}
}
#endregion
#region Private Helpers
/// <summary>
/// Run any additional tools given a DumpEnvironment
/// </summary>
/// <returns>Result instance with the outcome</returns>
private Result ExecuteAdditionalToolsAfterDIC()
{
// Special cases
switch (System)
{
case KnownSystem.SegaSaturn:
if (!File.Exists(SubdumpPath))
return Result.Failure("Error! Could not find subdump!");
ExecuteSubdump();
return Result.Success("subdump has finished!");
}
return Result.Success("No external tools needed!");
}
/// <summary>
/// Run DiscImageCreator
/// </summary>
private void ExecuteDiskImageCreator()
{
dicProcess = new Process()
{
StartInfo = new ProcessStartInfo()
{
FileName = DICPath,
Arguments = DICParameters.GenerateParameters() ?? "",
},
};
dicProcess.Start();
dicProcess.WaitForExit();
}
/// <summary>
/// Execute subdump for a (potential) Sega Saturn dump
/// </summary>
private async void ExecuteSubdump()
{
await Task.Run(() =>
{
Process childProcess = new Process()
{
StartInfo = new ProcessStartInfo()
{
FileName = SubdumpPath,
Arguments = "-i " + Drive.Letter + ": -f " + Path.Combine(OutputDirectory, Path.GetFileNameWithoutExtension(OutputFilename) + "_subdump.sub") + "-mode 6 -rereadnum 25 -fix 2",
},
};
childProcess.Start();
childProcess.WaitForExit();
});
}
/// <summary>
/// Extract all of the possible information from a given input combination
/// </summary>
/// <param name="driveLetter">Drive letter to check</param>
/// <returns>SubmissionInfo populated based on outputs, null on error</returns>
private SubmissionInfo ExtractOutputInformation(IProgress<Result> progress)
{
// Ensure the current disc combination should exist
if (!Validators.GetValidMediaTypes(System).Contains(Type))
return null;
// Sanitize the output filename to strip off any potential extension
string outputFilename = Path.GetFileNameWithoutExtension(OutputFilename);
// Check that all of the relevant files are there
if (!FoundAllFiles())
return null;
// Create the SubmissionInfo object with all user-inputted values by default
string combinedBase = Path.Combine(OutputDirectory, outputFilename);
SubmissionInfo info = new SubmissionInfo()
{
System = this.System,
Media = this.Type,
Title = Template.RequiredValue,
ForeignTitleNonLatin = Template.OptionalValue,
DiscNumberLetter = Template.OptionalValue,
DiscTitle = Template.OptionalValue,
Category = Category.Games,
Region = null,
Languages = null,
Serial = Template.RequiredIfExistsValue,
Barcode = Template.OptionalValue,
Contents = Template.OptionalValue,
Version = Template.RequiredIfExistsValue,
OtherEditions = "Original (VERIFY THIS)",
ClrMameProData = GetDatfile(combinedBase + ".dat"),
};
// First and foremost, we want to get a list of matching IDs for each line in the DAT
if (!string.IsNullOrEmpty(info.ClrMameProData) && HasRedumpLogin)
{
// Set the current dumper based on username
info.Dumpers = new string[] { this.Username };
info.MatchedIDs = new List<int>();
using (CookieAwareWebClient wc = new CookieAwareWebClient())
{
// Login to Redump
RedumpAccess access = new RedumpAccess();
if (access.RedumpLogin(wc, this.Username, this.Password))
{
// Loop through all of the hashdata to find matching IDs
progress?.Report(Result.Success("Finding disc matches on Redump..."));
string[] splitData = info.ClrMameProData.Split('\n');
foreach (string hashData in splitData)
{
if (GetISOHashValues(hashData, out long size, out string crc32, out string md5, out string sha1))
{
List<int> newIds = access.ProcessSearch(wc, sha1);
if (info.MatchedIDs.Any())
info.MatchedIDs = info.MatchedIDs.Intersect(newIds).ToList();
else
info.MatchedIDs = newIds;
}
}
progress?.Report(Result.Success("Match finding complete! " + (info.MatchedIDs.Count > 0 ? "Matched IDs: " + string.Join(",", info.MatchedIDs) : "No matches found")));
// If we have exactly 1 ID, we can grab a bunch of info from it
if (info.MatchedIDs.Count == 1)
{
progress?.Report(Result.Success($"Filling fields from existing ID {info.MatchedIDs[0]}..."));
string discData = access.DownloadSingleSiteID(wc, info.MatchedIDs[0]);
info.FillFromDiscPage(discData);
progress?.Report(Result.Success("Information filling complete!"));
}
}
}
}
// Extract info based generically on MediaType
switch (Type)
{
case MediaType.CDROM:
case MediaType.GDROM: // TODO: Verify GD-ROM outputs this
info.MasteringRingFirstLayerDataSide = Template.RequiredIfExistsValue;
info.MasteringSIDCodeFirstLayerDataSide = Template.RequiredIfExistsValue;
info.ToolstampMasteringCodeFirstLayerDataSide = Template.RequiredIfExistsValue;
info.MouldSIDCodeFirstLayerDataSide = Template.RequiredIfExistsValue;
info.MouldSIDCodeSecondLayerLabelSide = Template.RequiredIfExistsValue;
info.AdditionalMouldFirstLayerDataSide = Template.RequiredIfExistsValue;
info.PVD = GetPVD(combinedBase + "_mainInfo.txt") ?? "Disc has no PVD"; ;
long errorCount = -1;
if (File.Exists(combinedBase + ".img_EdcEcc.txt"))
errorCount = GetErrorCount(combinedBase + ".img_EdcEcc.txt");
else if (File.Exists(combinedBase + ".img_EccEdc.txt"))
errorCount = GetErrorCount(combinedBase + ".img_EccEdc.txt");
info.ErrorsCount = (errorCount == -1 ? "Error retrieving error count" : errorCount.ToString());
info.Cuesheet = GetFullFile(combinedBase + ".cue") ?? ""; ;
string cdWriteOffset = GetWriteOffset(combinedBase + "_disc.txt") ?? "";
info.RingWriteOffset = cdWriteOffset;
info.OtherWriteOffsets = cdWriteOffset;
// GD-ROM-specfic options
if (Type == MediaType.GDROM)
info.Header = GetSegaHeader(combinedBase + "_mainInfo.txt") ?? "";
break;
case MediaType.DVD:
case MediaType.HDDVD:
case MediaType.BluRay:
bool isXbox = (System == KnownSystem.MicrosoftXBOX || System == KnownSystem.MicrosoftXBOX360);
string layerbreak = GetLayerbreak(combinedBase + "_disc.txt", isXbox) ?? "";
// Get the individual hash data, as per internal
if (GetISOHashValues(info.ClrMameProData, out long size, out string crc32, out string md5, out string sha1))
{
info.Size = size;
info.CRC32 = crc32;
info.MD5 = md5;
info.SHA1 = sha1;
info.ClrMameProData = null;
}
// If we have a single-layer disc
if (String.IsNullOrWhiteSpace(layerbreak))
{
info.MasteringRingFirstLayerDataSide = Template.RequiredIfExistsValue;
info.MasteringSIDCodeFirstLayerDataSide = Template.RequiredIfExistsValue;
info.ToolstampMasteringCodeFirstLayerDataSide = Template.RequiredIfExistsValue;
info.MouldSIDCodeFirstLayerDataSide = Template.RequiredIfExistsValue;
info.MouldSIDCodeSecondLayerLabelSide = Template.RequiredIfExistsValue;
info.AdditionalMouldFirstLayerDataSide = Template.RequiredIfExistsValue;
info.PVD = GetPVD(combinedBase + "_mainInfo.txt") ?? "";
}
// If we have a dual-layer disc
else
{
info.MasteringRingFirstLayerDataSide = Template.RequiredIfExistsValue;
info.MasteringSIDCodeFirstLayerDataSide = Template.RequiredIfExistsValue;
info.ToolstampMasteringCodeFirstLayerDataSide = Template.RequiredIfExistsValue;
info.MouldSIDCodeFirstLayerDataSide = Template.RequiredIfExistsValue;
info.AdditionalMouldFirstLayerDataSide = Template.RequiredIfExistsValue;
info.MasteringRingSecondLayerLabelSide = Template.RequiredIfExistsValue;
info.MasteringSIDCodeSecondLayerLabelSide = Template.RequiredIfExistsValue;
info.ToolstampMasteringCodeSecondLayerLabelSide = Template.RequiredIfExistsValue;
info.MouldSIDCodeSecondLayerLabelSide = Template.RequiredIfExistsValue;
info.PVD = GetPVD(combinedBase + "_mainInfo.txt") ?? "";
info.Layerbreak = Int64.Parse(layerbreak);
}
// Bluray-specific options
if (Type == MediaType.BluRay)
info.PIC = GetPIC(Path.Combine(OutputDirectory, "PIC.bin")) ?? "";
break;
case MediaType.NintendoGameCubeGameDisc:
info.BCA = Template.RequiredValue;
break;
case MediaType.NintendoWiiOpticalDisc:
info.DiscKey = Template.RequiredValue;
info.BCA = Template.RequiredValue;
break;
case MediaType.UMD:
info.PVD = GetPVD(combinedBase + "_mainInfo.txt") ?? "";
info.Size = -1;
info.CRC32 = Template.RequiredValue + " [Not automatically generated for UMD]";
info.MD5 = Template.RequiredValue + " [Not automatically generated for UMD]";
info.SHA1 = Template.RequiredValue + " [Not automatically generated for UMD]";
info.ClrMameProData = null;
if (GetUMDAuxInfo(combinedBase + "_disc.txt", out string title, out string umdversion, out string umdlayer))
{
info.Title = title ?? "";
info.Version = umdversion ?? "";
if (!String.IsNullOrWhiteSpace(umdlayer))
info.Layerbreak = Int64.Parse(umdlayer ?? "-1");
}
break;
}
// Extract info based specifically on KnownSystem
switch (System)
{
case KnownSystem.AppleMacintosh:
case KnownSystem.EnhancedCD:
case KnownSystem.IBMPCCompatible:
case KnownSystem.RainbowDisc:
if (string.IsNullOrWhiteSpace(info.Comments))
info.Comments += $"[T:ISBN] {Template.OptionalValue}";
progress?.Report(Result.Success("Running copy protection scan... this might take a while!"));
info.Protection = GetCopyProtection();
progress?.Report(Result.Success("Copy protection scan complete!"));
if (File.Exists(combinedBase + "_subIntention.txt"))
{
FileInfo fi = new FileInfo(combinedBase + "_subIntention.txt");
if (fi.Length > 0)
info.SecuROMData = GetFullFile(combinedBase + "_subIntention.txt") ?? "";
}
break;
case KnownSystem.BandaiPlaydiaQuickInteractiveSystem:
info.EXEDateBuildDate = Template.RequiredValue;
break;
case KnownSystem.BDVideo:
info.Protection = Template.RequiredIfExistsValue;
break;
case KnownSystem.CommodoreAmiga:
info.EXEDateBuildDate = Template.RequiredValue;
break;
case KnownSystem.CommodoreAmigaCD32:
info.EXEDateBuildDate = Template.RequiredValue;
break;
case KnownSystem.CommodoreAmigaCDTV:
info.EXEDateBuildDate = Template.RequiredValue;
break;
case KnownSystem.DVDVideo:
info.Protection = GetDVDProtection(combinedBase + "_CSSKey.txt", combinedBase + "_disc.txt") ?? "";
break;
case KnownSystem.FujitsuFMTowns:
info.EXEDateBuildDate = Template.RequiredValue;
break;
case KnownSystem.IncredibleTechnologiesEagle:
info.EXEDateBuildDate = Template.RequiredValue;
break;
case KnownSystem.KonamieAmusement:
info.EXEDateBuildDate = Template.RequiredValue;
break;
case KnownSystem.KonamiFirebeat:
info.EXEDateBuildDate = Template.RequiredValue;
break;
case KnownSystem.KonamiGVSystem:
info.EXEDateBuildDate = Template.RequiredValue;
break;
case KnownSystem.KonamiSystem573:
info.EXEDateBuildDate = Template.RequiredValue;
break;
case KnownSystem.KonamiTwinkle:
info.EXEDateBuildDate = Template.RequiredValue;
break;
case KnownSystem.MattelHyperscan:
info.EXEDateBuildDate = Template.RequiredValue;
break;
case KnownSystem.MicrosoftXBOX:
if (GetXBOXAuxInfo(combinedBase + "_disc.txt", out string dmihash, out string pfihash, out string sshash, out string ss, out string ssver))
{
info.Comments += $"{Template.XBOXDMIHash}: {dmihash ?? ""}\n" +
$"{Template.XBOXPFIHash}: {pfihash ?? ""}\n" +
$"{Template.XBOXSSHash}: {sshash ?? ""}\n" +
$"{Template.XBOXSSVersion}: {ssver ?? ""}\n";
info.SecuritySectorRanges = ss ?? "";
}
if (GetXBOXDMIInfo(Path.Combine(OutputDirectory, "DMI.bin"), out string serial, out string version, out Region? region))
{
info.Serial = serial ?? Template.RequiredValue;
info.Version = version ?? Template.RequiredValue;
info.Region = region;
}
break;
case KnownSystem.MicrosoftXBOX360:
if (GetXBOXAuxInfo(combinedBase + "_disc.txt", out string dmi360hash, out string pfi360hash, out string ss360hash, out string ss360, out string ssver360))
{
info.Comments += $"{Template.XBOXDMIHash}: {dmi360hash ?? ""}\n" +
$"{Template.XBOXPFIHash}: {pfi360hash ?? ""}\n" +
$"{Template.XBOXSSHash}: {ss360hash ?? ""}\n" +
$"{Template.XBOXSSVersion}: {ssver360 ?? ""}\n";
info.SecuritySectorRanges = ss360 ?? "";
}
if (GetXBOX360DMIInfo(Path.Combine(OutputDirectory, "DMI.bin"), out string serial360, out string version360, out Region? region360))
{
info.Serial = serial360 ?? Template.RequiredValue;
info.Version = version360 ?? Template.RequiredValue;
info.Region = region360;
}
break;
case KnownSystem.NamcoSegaNintendoTriforce:
info.EXEDateBuildDate = Template.RequiredValue;
break;
case KnownSystem.NavisoftNaviken21:
info.EXEDateBuildDate = Template.RequiredValue;
break;
case KnownSystem.NECPC98:
info.EXEDateBuildDate = Template.RequiredValue;
break;
case KnownSystem.SegaCDMegaCD:
info.Header = GetSegaHeader(combinedBase + "_mainInfo.txt") ?? "";
// Take only the last 16 lines for Sega CD
if (!string.IsNullOrEmpty(info.Header))
info.Header = string.Join("\n", info.Header.Split('\n').Skip(16));
if (GetSegaCDBuildInfo(info.Header, out string scdSerial, out string fixedDate))
{
info.Serial = scdSerial ?? "";
info.EXEDateBuildDate = fixedDate ?? "";
}
break;
case KnownSystem.SegaChihiro:
info.EXEDateBuildDate = Template.RequiredValue;
break;
case KnownSystem.SegaDreamcast:
info.EXEDateBuildDate = Template.RequiredValue;
break;
case KnownSystem.SegaNaomi:
info.EXEDateBuildDate = Template.RequiredValue;
break;
case KnownSystem.SegaNaomi2:
info.EXEDateBuildDate = Template.RequiredValue;
break;
case KnownSystem.SegaSaturn:
info.Header = GetSegaHeader(combinedBase + "_mainInfo.txt") ?? "";
// Take only the first 16 lines for Saturn
if (!string.IsNullOrEmpty(info.Header))
info.Header = string.Join("\n", info.Header.Split('\n').Take(16));
if (GetSaturnBuildInfo(info.Header, out string saturnSerial, out string saturnVersion, out string buildDate))
{
info.Serial = saturnSerial ?? "";
info.Version = saturnVersion ?? "";
info.EXEDateBuildDate = buildDate ?? "";
}
break;
case KnownSystem.SegaTitanVideo:
info.EXEDateBuildDate = Template.RequiredValue;
break;
case KnownSystem.SNKNeoGeoCD:
info.EXEDateBuildDate = Template.RequiredValue;
break;
case KnownSystem.SonyPlayStation:
info.EXEDateBuildDate = GetPlayStationEXEDate(Drive?.Letter) ?? "";
info.EDC = GetMissingEDCCount(combinedBase + ".img_EdcEcc.txt") > 0 ? YesNo.No : YesNo.Yes;
info.AntiModchip = GetAntiModchipDetected(combinedBase + "_disc.txt") ? YesNo.Yes : YesNo.No;
info.LibCrypt = YesNo.No;
if (File.Exists(combinedBase + "_subIntention.txt"))
{
FileInfo fi = new FileInfo(combinedBase + "_subIntention.txt");
if (fi.Length > 0)
{
info.LibCrypt = YesNo.Yes;
info.LibCryptData = GetFullFile(combinedBase + "_subIntention.txt") ?? "";
}
}
break;
case KnownSystem.SonyPlayStation2:
info.LanguageSelection = new string[] { "Bios settings", "Language selector", "Options menu" };
info.EXEDateBuildDate = GetPlayStationEXEDate(Drive?.Letter) ?? "";
info.Version = GetPlayStation2Version(Drive?.Letter) ?? "";
break;
case KnownSystem.SonyPlayStation3:
info.DiscKey = Template.RequiredValue;
info.DiscID = Template.RequiredValue;
break;
case KnownSystem.SonyPlayStation4:
info.Version = GetPlayStation4Version(Drive?.Letter) ?? "";
break;
case KnownSystem.ZAPiTGamesGameWaveFamilyEntertainmentSystem:
info.Protection = Template.RequiredIfExistsValue;
break;
}
// Comments is one of the few fields with odd handling
if (string.IsNullOrEmpty(info.Comments))
info.Comments = Template.OptionalValue;
return info;
}
/// <summary>
/// Format the output data in a human readable way, separating each printed line into a new item in the list
/// </summary>
/// <param name="info">Information object that should contain normalized values</param>
/// <returns>List of strings representing each line of an output file, null on error</returns>
private List<string> FormatOutputData(SubmissionInfo info)
{
// Check to see if the inputs are valid
if (info == null)
return null;
try
{
// Common Disc Info section
List<string> output = new List<string> { "Common Disc Info:" };
AddIfExists(output, Template.TitleField, info.Title, 1);
AddIfExists(output, Template.ForeignTitleField, info.ForeignTitleNonLatin, 1);
AddIfExists(output, Template.DiscNumberField, info.DiscNumberLetter, 1);
AddIfExists(output, Template.DiscTitleField, info.DiscTitle, 1);
AddIfExists(output, Template.SystemField, info.System.Name(), 1);
AddIfExists(output, Template.MediaTypeField, GetFixedMediaType(info.Media, info.Layerbreak), 1);
AddIfExists(output, Template.CategoryField, info.Category.Name(), 1);
AddIfExists(output, Template.MatchingIDsField, info.MatchedIDs, 1);
AddIfExists(output, Template.RegionField, info.Region.Name(), 1);
AddIfExists(output, Template.LanguagesField, (info.Languages ?? new Language?[] { null }).Select(l => l.Name()).ToArray(), 1);
AddIfExists(output, Template.PlaystationLanguageSelectionViaField, info.LanguageSelection, 1);
AddIfExists(output, Template.DiscSerialField, info.Serial, 1);
// All ringcode information goes in an indented area
output.Add(""); output.Add("\tRingcode Information:");
// If we have a dual-layer disc
if (info.Layerbreak != default(long))
{
AddIfExists(output, "Inner " + Template.MasteringRingField, info.MasteringRingFirstLayerDataSide, 2);
AddIfExists(output, "Inner " + Template.MasteringSIDField, info.MasteringSIDCodeFirstLayerDataSide, 2);
AddIfExists(output, "Inner " + Template.ToolstampField, info.ToolstampMasteringCodeFirstLayerDataSide, 2);
AddIfExists(output, "Outer " + Template.MasteringRingField, info.MasteringRingSecondLayerLabelSide, 2);
AddIfExists(output, "Outer " + Template.MasteringSIDField, info.MasteringSIDCodeSecondLayerLabelSide, 2);
AddIfExists(output, "Outer " + Template.ToolstampField, info.ToolstampMasteringCodeSecondLayerLabelSide, 2);
AddIfExists(output, "Data-Side " + Template.MouldSIDField, info.MouldSIDCodeFirstLayerDataSide, 2);
AddIfExists(output, "Label-Side " + Template.MouldSIDField, info.MouldSIDCodeSecondLayerLabelSide, 2);
AddIfExists(output, Template.AdditionalMouldField, info.AdditionalMouldFirstLayerDataSide, 2);
}
// If we have a single-layer disc
else
{
AddIfExists(output, Template.MasteringRingField, info.MasteringRingFirstLayerDataSide, 2);
AddIfExists(output, Template.MasteringSIDField, info.MasteringSIDCodeFirstLayerDataSide, 2);
AddIfExists(output, "Data-Side " + Template.MouldSIDField, info.MouldSIDCodeFirstLayerDataSide, 2);
AddIfExists(output, "Label-Side " + Template.MouldSIDField, info.MouldSIDCodeSecondLayerLabelSide, 2);
AddIfExists(output, Template.AdditionalMouldField, info.AdditionalMouldFirstLayerDataSide, 2);
AddIfExists(output, Template.ToolstampField, info.ToolstampMasteringCodeFirstLayerDataSide, 2);
}
AddIfExists(output, Template.BarcodeField, info.Barcode, 1);
AddIfExists(output, Template.EXEDateBuildDate, info.EXEDateBuildDate, 1);
AddIfExists(output, Template.ErrorCountField, info.ErrorsCount, 1);
AddIfExists(output, Template.CommentsField, info.Comments.Trim(), 1);
AddIfExists(output, Template.ContentsField, info.Contents.Trim(), 1);
// Version and Editions section
output.Add(""); output.Add("Version and Editions:");
AddIfExists(output, Template.VersionField, info.Version, 1);
AddIfExists(output, Template.EditionField, info.OtherEditions, 1);
// EDC section
if (info.EDC != YesNo.NULL)
{
output.Add("EDC:");
AddIfExists(output, Template.PlayStationEDCField, info.EDC.Name(), 1);
}
// Parent/Clone Relationship section
// output.Add(""); output.Add("Parent/Clone Relationship:");
// AddIfExists(output, Template.ParentIDField, info.ParentID);
// AddIfExists(output, Template.RegionalParentField, info.RegionalParent.ToString());
// Extras section
if (info.PVD != null || info.PIC != null || info.BCA != null)
{
output.Add(""); output.Add("Extras:");
AddIfExists(output, Template.PVDField, info.PVD.Trim(), 1);
AddIfExists(output, Template.PlayStation3WiiDiscKeyField, info.DiscKey, 1);
AddIfExists(output, Template.PlayStation3DiscIDField, info.DiscID, 1);
AddIfExists(output, Template.PICField, info.PIC, 1);
AddIfExists(output, Template.HeaderField, info.Header, 1);
AddIfExists(output, Template.GameCubeWiiBCAField, info.BCA, 1);
AddIfExists(output, Template.XBOXSSRanges, info.SecuritySectorRanges, 1);
}
// Copy Protection section
if (info.Protection != null || info.EDC != YesNo.NULL)
{
output.Add(""); output.Add("Copy Protection:");
if (info.EDC != YesNo.NULL)
{
AddIfExists(output, Template.PlayStationAntiModchipField, info.AntiModchip.Name(), 1);
AddIfExists(output, Template.PlayStationLibCryptField, info.LibCrypt.Name(), 1);
AddIfExists(output, Template.SubIntentionField, info.LibCryptData, 1);
}
AddIfExists(output, Template.CopyProtectionField, info.Protection, 1);
AddIfExists(output, Template.SubIntentionField, info.SecuROMData, 1);
}
// Dumpers and Status section
// output.Add(""); output.Add("Dumpers and Status");
// AddIfExists(output, Template.StatusField, info.Status.Name());
// AddIfExists(output, Template.OtherDumpersField, info.OtherDumpers);
// Tracks and Write Offsets section
if (!string.IsNullOrWhiteSpace(info.ClrMameProData))
{
output.Add(""); output.Add("Tracks and Write Offsets:");
AddIfExists(output, Template.DATField, info.ClrMameProData, 1);
AddIfExists(output, Template.CuesheetField, info.Cuesheet, 1);
AddIfExists(output, Template.WriteOffsetField, info.OtherWriteOffsets, 1);
}
// Size & Checksum section
else
{
output.Add(""); output.Add("Size & Checksum:");
AddIfExists(output, Template.LayerbreakField, info.Layerbreak.ToString(), 1);
AddIfExists(output, Template.SizeField, info.Size.ToString(), 1);
AddIfExists(output, Template.CRC32Field, info.CRC32, 1);
AddIfExists(output, Template.MD5Field, info.MD5, 1);
AddIfExists(output, Template.SHA1Field, info.SHA1, 1);
}
// Make sure there aren't any instances of two blank lines in a row
string last = null;
for (int i = 0; i < output.Count; )
{
if (output[i] == last)
{
output.RemoveAt(i);
}
else
{
last = output[i];
i++;
}
}
return output;
}
catch
{
// We don't care what the error is
return null;
}
}
/// <summary>
/// Add the properly formatted key and value, if possible
/// </summary>
/// <param name="output">Output list</param>
/// <param name="key">Name of the output key to write</param>
/// <param name="value">Name of the output value to write</param>
/// <param name="indent">Number of tabs to indent the line</param>
private void AddIfExists(List<string> output, string key, string value, int indent)
{
// If there's no valid value to write
if (string.IsNullOrWhiteSpace(value))
return;
string prefix = "";
for (int i = 0; i < indent; i++)
prefix += "\t";
// If the value contains a newline
if (value.Contains("\n"))
{
output.Add(prefix + key + ":"); output.Add("");
string[] values = value.Split('\n');
foreach (string val in values)
output.Add(prefix + val);
output.Add("");
}
// For all regular values
else
{
output.Add(prefix + key + ": " + value);
}
}
/// <summary>
/// Add the properly formatted key and value, if possible
/// </summary>
/// <param name="output">Output list</param>
/// <param name="key">Name of the output key to write</param>
/// <param name="value">Name of the output value to write</param>
/// <param name="indent">Number of tabs to indent the line</param>
private void AddIfExists(List<string> output, string key, string[] value, int indent)
{
// If there's no valid value to write
if (value == null || value.Length == 0)
return;
AddIfExists(output, key, string.Join(", ", value), indent);
}
/// <summary>
/// Add the properly formatted key and value, if possible
/// </summary>
/// <param name="output">Output list</param>
/// <param name="key">Name of the output key to write</param>
/// <param name="value">Name of the output value to write</param>
/// <param name="indent">Number of tabs to indent the line</param>
private void AddIfExists(List<string> output, string key, List<int> value, int indent)
{
// If there's no valid value to write
if (value == null || value.Count() == 0)
return;
AddIfExists(output, key, string.Join(", ", value.Select(o => o.ToString())), indent);
}
/// <summary>
/// Get the adjusted name of the media baed on layers, if applicable
/// </summary>
/// <param name="mediaType">MediaType to get the proper name for</param>
/// <param name="layerbreak">Layerbreak value, as applicable</param>
/// <returns>String representation of the media, including layer specification</returns>
private string GetFixedMediaType(MediaType? mediaType, long layerbreak)
{
switch (mediaType)
{
case MediaType.DVD:
if (layerbreak != default(long))
return $"{mediaType.Name()}-9";
else
return $"{mediaType.Name()}-5";
case MediaType.BluRay:
if (layerbreak != default(long))
return $"{mediaType.Name()}-50";
else
return $"{mediaType.Name()}-25";
case MediaType.UMD:
if (layerbreak != default(long))
return $"{mediaType.Name()}-DL";
else
return $"{mediaType.Name()}-SL";
default:
return mediaType.Name();
}
}
/// <summary>
/// Validate the current environment is ready for a dump
/// </summary>
/// <returns>Result instance with the outcome</returns>
private Result IsValidForDump()
{
// Validate that everything is good
if (!ParametersValid())
return Result.Failure("Error! Current configuration is not supported!");
FixOutputPaths();
// Validate that the required program exists
if (!File.Exists(DICPath))
return Result.Failure("Error! Could not find DiscImageCreator!");
return Result.Success();
}
/// <summary>
/// Verify that the current environment has a complete dump and create submission info is possible
/// </summary>
/// <returns>Result instance with the outcome</returns>
public Result VerifyAndSaveDumpOutput(IProgress<Result> progress)
{
// Check to make sure that the output had all the correct files
if (!FoundAllFiles())
return Result.Failure("Error! Please check output directory as dump may be incomplete!");
progress?.Report(Result.Success("Extracting output information from output files..."));
SubmissionInfo submissionInfo = ExtractOutputInformation(progress);
progress?.Report(Result.Success("Extracting information complete!"));
// TODO: Add UI step here (possibly) to get user info on the disc
progress?.Report(Result.Success("Formatting extracted information..."));
List<string> formattedValues = FormatOutputData(submissionInfo);
progress?.Report(Result.Success("Formatting complete!"));
progress?.Report(Result.Success("Writing information to !submissionInfo.txt..."));
bool success = WriteOutputData(formattedValues);
success &= WriteOutputData(submissionInfo);
if (success)
progress?.Report(Result.Success("Writing complete!"));
else
progress?.Report(Result.Failure("Writing could not complete!"));
return Result.Success();
}
/// <summary>
/// Write the data to the output folder
/// </summary>
/// <param name="lines">Preformatted list of lines to write out to the file</param>
/// <returns>True on success, false on error</returns>
private bool WriteOutputData(List<string> lines)
{
// Check to see if the inputs are valid
if (lines == null)
return false;
// Now write out to a generic file
try
{
using (StreamWriter sw = new StreamWriter(File.Open(Path.Combine(OutputDirectory, "!submissionInfo.txt"), FileMode.Create, FileAccess.Write)))
{
foreach (string line in lines)
sw.WriteLine(line);
}
}
catch
{
// We don't care what the error is right now
return false;
}
return true;
}
/// <summary>
/// Write the data to the output folder
/// </summary>
/// <param name="info">SubmissionInfo object representign the JSON to write out to the file</param>
/// <returns>True on success, false on error</returns>
private bool WriteOutputData(SubmissionInfo info)
{
// Check to see if the input is valid
if (info == null)
return false;
// Now write out to a generic file
try
{
using (StreamWriter sw = new StreamWriter(File.Open(Path.Combine(OutputDirectory, "!submissionInfo.json"), FileMode.Create, FileAccess.Write)))
{
string json = JsonConvert.SerializeObject(info, Formatting.Indented);
sw.WriteLine(json);
}
}
catch
{
// We don't care what the error is right now
return false;
}
return true;
}
#endregion
#region Information Extraction Methods
/// <summary>
/// Get the existance of an anti-modchip string from the input file, if possible
/// </summary>
/// <param name="disc">_disc.txt file location</param>
/// <returns>Antimodchip existance if possible, false on error</returns>
private bool GetAntiModchipDetected(string disc)
{
// If the file doesn't exist, we can't get info from it
if (!File.Exists(disc))
return false;
using (StreamReader sr = File.OpenText(disc))
{
try
{
// Check for either antimod string
string line = sr.ReadLine().Trim();
while (!sr.EndOfStream)
{
if (line.StartsWith("Detected anti-mod string"))
return true;
else if (line.StartsWith("No anti-mod string"))
return false;
line = sr.ReadLine().Trim();
}
return false;
}
catch
{
// We don't care what the exception is right now
return false;
}
}
}
/// <summary>
/// Get the current copy protection scheme, if possible
/// </summary>
/// <returns>Copy protection scheme if possible, null on error</returns>
private string GetCopyProtection()
{
if (ScanForProtection)
return Task.Run(() => Validators.RunProtectionScanOnPath(Drive.Letter + ":\\")).GetAwaiter().GetResult();
return "(CHECK WITH PROTECTIONID)";
}
/// <summary>
/// Get the proper datfile from the input file, if possible
/// </summary>
/// <param name="dat">.dat file location</param>
/// <returns>Relevant pieces of the datfile, null on error</returns>
private string GetDatfile(string dat)
{
// If the file doesn't exist, we can't get info from it
if (!File.Exists(dat))
return null;
using (StreamReader sr = File.OpenText(dat))
{
try
{
// Make sure this file is a .dat
if (sr.ReadLine() != "<?xml version=\"1.0\" encoding=\"UTF-8\"?>")
return null;
if (sr.ReadLine() != "<!DOCTYPE datafile PUBLIC \"-//Logiqx//DTD ROM Management Datafile//EN\" \"http://www.logiqx.com/Dats/datafile.dtd\">")
return null;
// Fast forward to the rom lines
while (!sr.ReadLine().TrimStart().StartsWith("<game")) ;
sr.ReadLine(); // <category>Games</category>
sr.ReadLine(); // <description>Plextor</description>
// Now that we're at the relevant entries, read each line in and concatenate
string pvd = "", line = sr.ReadLine().Trim();
while (line.StartsWith("<rom"))
{
pvd += line + "\n";
line = sr.ReadLine().Trim();
}
return pvd.TrimEnd('\n');
}
catch
{
// We don't care what the exception is right now
return null;
}
}
}
/// <summary>
/// Get the DVD protection information, if possible
/// </summary>
/// <param name="cssKey">_CSSKey.txt file location</param>
/// <param name="disc">_disc.txt file location</param>
/// <returns>Formatted string representing the DVD protection, null on error</returns>
private string GetDVDProtection(string cssKey, string disc)
{
// If one of the files doesn't exist, we can't get info from them
if (!File.Exists(disc))
return null;
// Setup all of the individual pieces
string region = null, rceProtection = null, copyrightProtectionSystemType = null, encryptedDiscKey = null, playerKey = null, decryptedDiscKey = null;
// Get everything from _disc.txt first
using (StreamReader sr = File.OpenText(disc))
{
try
{
// Fast forward to the copyright information
while (!sr.ReadLine().Trim().StartsWith("========== CopyrightInformation ==========")) ;
// Now read until we hit the manufacturing information
string line = sr.ReadLine().Trim();
while (!line.StartsWith("========== ManufacturingInformation =========="))
{
if (line.StartsWith("CopyrightProtectionType"))
copyrightProtectionSystemType = line.Substring("CopyrightProtectionType: ".Length);
else if (line.StartsWith("RegionManagementInformation"))
region = line.Substring("RegionManagementInformation: ".Length);
line = sr.ReadLine().Trim();
}
}
catch { }
}
// Get everything from _CSSKey.txt next, if it exists
if (File.Exists(cssKey))
{
using (StreamReader sr = File.OpenText(cssKey))
{
try
{
// Read until the end
while (!sr.EndOfStream)
{
string line = sr.ReadLine().Trim();
if (line.StartsWith("[001]"))
encryptedDiscKey = line.Substring("[001]: ".Length);
else if (line.StartsWith("PlayerKey"))
playerKey = line.Substring("PlayerKey[1]: ".Length);
else if (line.StartsWith("DecryptedDiscKey"))
decryptedDiscKey = line.Substring("DecryptedDiscKey[020]: ".Length);
}
}
catch { }
}
}
// Now we format everything we can
string protection = "";
if (!String.IsNullOrEmpty(region))
protection += $"Region: {region}\n";
if (!String.IsNullOrEmpty(rceProtection))
protection += $"RCE Protection: {rceProtection}\n";
if (!String.IsNullOrEmpty(copyrightProtectionSystemType))
protection += $"Copyright Protection System Type: {copyrightProtectionSystemType}\n";
if (!String.IsNullOrEmpty(encryptedDiscKey))
protection += $"Encrypted Disc Key: {encryptedDiscKey}\n";
if (!String.IsNullOrEmpty(playerKey))
protection += $"Player Key: {playerKey}\n";
if (!String.IsNullOrEmpty(decryptedDiscKey))
protection += $"Decrypted Disc Key: {decryptedDiscKey}\n";
return protection;
}
/// <summary>
/// Get the detected error count from the input files, if possible
/// </summary>
/// <param name="edcecc">.img_EdcEcc.txt/.img_EccEdc.txt file location</param>
/// <returns>Error count if possible, -1 on error</returns>
private long GetErrorCount(string edcecc)
{
// TODO: Better usage of _mainInfo and _c2Error for uncorrectable errors
// If the file doesn't exist, we can't get info from it
if (!File.Exists(edcecc))
return -1;
// First line of defense is the EdcEcc error file
using (StreamReader sr = File.OpenText(edcecc))
{
try
{
// Read in the error count whenever we find it
while (!sr.EndOfStream)
{
string line = sr.ReadLine().Trim();
if (line.StartsWith("[NO ERROR]"))
{
return 0;
}
else if (line.StartsWith("Total errors"))
{
if (Int64.TryParse(line.Substring("Total errors: ".Length).Trim(), out long te))
return te;
else
return Int64.MinValue;
}
}
// If we haven't found anything, return -1
return -1;
}
catch
{
// We don't care what the exception is right now
return Int64.MaxValue;
}
}
}
/// <summary>
/// Get the full lines from the input file, if possible
/// </summary>
/// <param name="filename">file location</param>
/// <returns>Full text of the file, null on error</returns>
private string GetFullFile(string filename)
{
// If the file doesn't exist, we can't get info from it
if (!File.Exists(filename))
return null;
return string.Join("\n", File.ReadAllLines(filename));
}
/// <summary>
/// Get the split values for ISO-based media
/// </summary>
/// <param name="hashData">String representing the combined hash data</param>
/// <returns>True if extraction was successful, false otherwise</returns>
private 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.IsNullOrWhiteSpace(hashData))
return false;
// <rom name="DMI.bin" size="2048" crc="49e4f2fe" md5="843d1e6252d128bed8eff88a8855aff0" sha1="494f54aeeb1c72524440267c783a13ed2d424fea"/>
Regex hashreg = new Regex(@"<rom name="".*?"" size=""(.*?)"" crc=""(.*?)"" md5=""(.*?)"" sha1=""(.*?)""");
Match m = hashreg.Match(hashData);
if (m.Success)
{
Int64.TryParse(m.Groups[1].Value, out size);
crc32 = m.Groups[2].Value;
md5 = m.Groups[3].Value;
sha1 = m.Groups[4].Value;
return true;
}
else
{
return false;
}
}
/// <summary>
/// Get the layerbreak from the input file, if possible
/// </summary>
/// <param name="disc">_disc.txt file location</param>
/// <param name="ignoreFirst">True if the first sector length is to be ignored, false otherwise</param>
/// <returns>Layerbreak if possible, null on error</returns>
private string GetLayerbreak(string disc, bool ignoreFirst)
{
// If the file doesn't exist, we can't get info from it
if (!File.Exists(disc))
return null;
using (StreamReader sr = File.OpenText(disc))
{
try
{
// Fast forward to the layerbreak
string line = sr.ReadLine();
while (line != null)
{
// We definitely found a single-layer disc
if (line.Contains("NumberOfLayers: Single Layer"))
{
return null;
}
else if (line.Trim().StartsWith("========== SectorLength =========="))
{
// Skip the first one and unset the flag
if (ignoreFirst)
ignoreFirst = false;
else
break;
}
line = sr.ReadLine();
}
// Now that we're at the layerbreak line, attempt to get the decimal version
return sr.ReadLine().Trim().Split(' ')[1];
}
catch
{
// We don't care what the exception is right now
return null;
}
}
}
/// <summary>
/// Get the detected missing EDC count from the input files, if possible
/// </summary>
/// <param name="edcecc">.img_EdcEcc.txt file location</param>
/// <returns>Missing EDC count if possible, -1 on error</returns>
private long GetMissingEDCCount(string edcecc)
{
// If one of the files doesn't exist, we can't get info from them
if (!File.Exists(edcecc))
return -1;
// First line of defense is the EdcEcc error file
using (StreamReader sr = File.OpenText(edcecc))
{
try
{
// Fast forward to the PVD
string line = sr.ReadLine();
while (!line.StartsWith("[INFO] Number of sector(s) where EDC doesn't exist: "))
line = sr.ReadLine();
return Int64.Parse(line.Remove(0, "[INFO] Number of sector(s) where EDC doesn't exist: ".Length).Trim());
}
catch
{
// We don't care what the exception is right now
return -1;
}
}
}
/// <summary>
/// Get the PVD from the input file, if possible
/// </summary>
/// <param name="mainInfo">_mainInfo.txt file location</param>
/// <returns>Newline-deliminated PVD if possible, null on error</returns>
private string GetPVD(string mainInfo)
{
// If the file doesn't exist, we can't get info from it
if (!File.Exists(mainInfo))
return null;
using (StreamReader sr = File.OpenText(mainInfo))
{
try
{
// Make sure we're in the right sector
while (!sr.ReadLine().StartsWith("========== LBA[000016, 0x00010]: Main Channel ==========")) ;
// Fast forward to the PVD
while (!sr.ReadLine().StartsWith("0310")) ;
// Now that we're at the PVD, read each line in and concatenate
string pvd = "";
for (int i = 0; i < 6; i++)
pvd += sr.ReadLine() + "\n"; // 320-370
return pvd;
}
catch
{
// We don't care what the exception is right now
return null;
}
}
}
/// <summary>
/// Get the EXE date from a PlayStation disc, if possible
/// </summary>
/// <param name="driveLetter">Drive letter to use to check</param>
/// <returns>EXE date in "yyyy-mm-dd" format if possible, null on error</returns>
private string GetPlayStationEXEDate(char? driveLetter)
{
// If there's no drive letter, we can't do this part
if (driveLetter == null)
return null;
// If the folder no longer exists, we can't do this part
string drivePath = driveLetter + ":\\";
if (!Directory.Exists(drivePath))
return null;
// If we can't find SYSTEM.CNF, we don't have a PlayStation disc
string systemCnfPath = Path.Combine(drivePath, "SYSTEM.CNF");
if (!File.Exists(systemCnfPath))
return null;
// Let's try reading SYSTEM.CNF to find the "BOOT" value
string exeName = null;
try
{
using (StreamReader sr = File.OpenText(systemCnfPath))
{
// Not assuming proper ordering, just in case
string line = sr.ReadLine();
while (!line.StartsWith("BOOT"))
line = sr.ReadLine();
// Once it finds the "BOOT" line, extract the name
exeName = Regex.Match(line, @"BOOT.*?=\s*cdrom.?:\\(.*?);.*").Groups[1].Value;
}
}
catch
{
// We don't care what the error was
return null;
}
// Now that we have the EXE name, try to get the fileinfo for it
string exePath = Path.Combine(drivePath, exeName);
if (!File.Exists(exePath))
return null;
FileInfo fi = new FileInfo(exePath);
return fi.LastWriteTimeUtc.ToString("yyyy-MM-dd");
}
/// <summary>
/// Get the version from a PlayStation 2 disc, if possible
/// </summary>
/// <param name="driveLetter">Drive letter to use to check</param>
/// <returns>Game version if possible, null on error</returns>
private string GetPlayStation2Version(char? driveLetter)
{
// If there's no drive letter, we can't do this part
if (driveLetter == null)
return null;
// If the folder no longer exists, we can't do this part
string drivePath = driveLetter + ":\\";
if (!Directory.Exists(drivePath))
return null;
// If we can't find SYSTEM.CNF, we don't have a PlayStation 2 disc
string systemCnfPath = Path.Combine(drivePath, "SYSTEM.CNF");
if (!File.Exists(systemCnfPath))
return null;
// Let's try reading SYSTEM.CNF to find the "VER" value
try
{
using (StreamReader sr = File.OpenText(systemCnfPath))
{
// Not assuming proper ordering, just in case
string line = sr.ReadLine();
while (!line.StartsWith("VER"))
line = sr.ReadLine();
// Once it finds the "VER" line, extract the version
return Regex.Match(line, @"VER\s*=\s*(.*)").Groups[1].Value;
}
}
catch
{
// We don't care what the error was
return null;
}
}
/// <summary>
/// Get the hex contents of the PIC file
/// </summary>
/// <param name="picPath">Path to the PIC.bin file associated with the dump</param>
/// <returns>PIC data as a hex string if possible, null on error</returns>
/// <remarks>https://stackoverflow.com/questions/9932096/add-separator-to-string-at-every-n-characters</remarks>
private string GetPIC(string picPath)
{
// If the file doesn't exist, we can't get the info
if (!File.Exists(picPath))
return null;
try
{
using (BinaryReader br = new BinaryReader(File.OpenRead(picPath)))
{
string hex = BitConverter.ToString(br.ReadBytes(140)).Replace("-", string.Empty);
return Regex.Replace(hex, ".{32}", "$0\n");
}
}
catch
{
// We don't care what the error was right now
return null;
}
}
/// <summary>
/// Get the version from a PlayStation 4 disc, if possible
/// </summary>
/// <param name="driveLetter">Drive letter to use to check</param>
/// <returns>Game version if possible, null on error</returns>
private string GetPlayStation4Version(char? driveLetter)
{
// If there's no drive letter, we can't do this part
if (driveLetter == null)
return null;
// If the folder no longer exists, we can't do this part
string drivePath = driveLetter + ":\\";
if (!Directory.Exists(drivePath))
return null;
// If we can't find param.sfo, we don't have a PlayStation 4 disc
string paramSfoPath = Path.Combine(drivePath, "bd", "param.sfo");
if (!File.Exists(paramSfoPath))
return null;
// Let's try reading param.sfo to find the version at the end of the file
try
{
using (BinaryReader br = new BinaryReader(File.OpenRead(paramSfoPath)))
{
br.BaseStream.Seek(0x9A4, SeekOrigin.Begin);
return new string(br.ReadChars(5));
}
}
catch
{
// We don't care what the error was
return null;
}
}
/// <summary>
/// Get the header from a Sega Saturn or Sega CD / Mega CD disc, if possible
/// </summary>
/// <param name="mainInfo">_mainInfo.txt file location</param>
/// <returns>Header as a byte array if possible, null on error</returns>
private string GetSegaHeader(string mainInfo)
{
// If the file doesn't exist, we can't get info from it
if (!File.Exists(mainInfo))
return null;
using (StreamReader sr = File.OpenText(mainInfo))
{
try
{
// Make sure we're in the right sector
while (!sr.ReadLine().StartsWith("========== LBA[000000, 0000000]: Main Channel ==========")) ;
// Fast forward to the header
while (!sr.ReadLine().Trim().StartsWith("+0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +A +B +C +D +E +F")) ;
// Now that we're at the Header, read each line in and concatenate
string header = "";
for (int i = 0; i < 32; i++)
header += sr.ReadLine() + "\n"; // 0000-01F0
return header;
}
catch
{
// We don't care what the exception is right now
return null;
}
}
}
/// <summary>
/// Get the build info from a Sega CD disc, if possible
/// </summary>
/// <<param name="segaHeader">String representing a formatter variant of the Sega CD header</param>
/// <returns>True on successful extraction of info, false otherwise</returns>
/// <remarks>Note that this works for MOST headers, except ones where the copyright stretches > 1 line</remarks>
private bool GetSegaCDBuildInfo(string segaHeader, out string serial, out string date)
{
serial = null; date = null;
// If the input header is null, we can't do a thing
if (String.IsNullOrWhiteSpace(segaHeader))
return false;
// Now read it in cutting it into lines for easier parsing
try
{
string[] header = segaHeader.Split('\n');
string serialVersionLine = header[8].Substring(58);
string dateLine = header[1].Substring(58);
serial = serialVersionLine.Substring(3, 7);
date = dateLine.Substring(8).Trim();
// Properly format the date string, if possible
string[] dateSplit = date.Split('.');
if (dateSplit.Length == 1)
dateSplit = new string[] { date.Substring(0, 4), date.Substring(4) };
string month = dateSplit[1];
switch (month)
{
case "JAN":
dateSplit[1] = "01";
break;
case "FEB":
dateSplit[1] = "02";
break;
case "MAR":
dateSplit[1] = "03";
break;
case "APR":
dateSplit[1] = "04";
break;
case "MAY":
dateSplit[1] = "05";
break;
case "JUN":
dateSplit[1] = "06";
break;
case "JUL":
dateSplit[1] = "07";
break;
case "AUG":
dateSplit[1] = "08";
break;
case "SEP":
dateSplit[1] = "09";
break;
case "OCT":
dateSplit[1] = "10";
break;
case "NOV":
dateSplit[1] = "11";
break;
case "DEC":
dateSplit[1] = "12";
break;
default:
dateSplit[1] = "00";
break;
}
date = string.Join("-", dateSplit);
return true;
}
catch
{
// We don't care what the error is
return false;
}
}
/// <summary>
/// Get the build info from a Saturn disc, if possible
/// </summary>
/// <<param name="segaHeader">String representing a formatter variant of the Saturn header</param>
/// <returns>True on successful extraction of info, false otherwise</returns>
private bool GetSaturnBuildInfo(string segaHeader, out string serial, out string version, out string date)
{
serial = null; version = null; date = null;
// If the input header is null, we can't do a thing
if (String.IsNullOrWhiteSpace(segaHeader))
return false;
// Now read it in cutting it into lines for easier parsing
try
{
string[] header = segaHeader.Split('\n');
string serialVersionLine = header[2].Substring(58);
string dateLine = header[3].Substring(58);
serial = serialVersionLine.Substring(0, 8);
version = serialVersionLine.Substring(10, 6);
date = dateLine.Substring(0, 8);
return true;
}
catch
{
// We don't care what the error is
return false;
}
}
/// <summary>
/// Get the UMD auxiliary info from the outputted files, if possible
/// </summary>
/// <param name="disc">_disc.txt file location</param>
/// <returns>True on successful extraction of info, false otherwise</returns>
private bool GetUMDAuxInfo(string disc, out string title, out string umdversion, out string umdlayer)
{
title = null; umdversion = null; umdlayer = null;
// If the file doesn't exist, we can't get info from it
if (!File.Exists(disc))
return false;
using (StreamReader sr = File.OpenText(disc))
{
try
{
// Loop through everything to get the first instance of each required field
string line = string.Empty;
while (!sr.EndOfStream)
{
line = sr.ReadLine().Trim();
if (line.StartsWith("TITLE") && title == null)
title = line.Substring("TITLE: ".Length);
else if (line.StartsWith("version") && umdversion == null)
umdversion = line.Substring("version: ".Length);
else if (line.StartsWith("L0 length"))
umdlayer = line.Split(' ')[2];
}
return true;
}
catch
{
// We don't care what the exception is right now
return false;
}
}
}
/// <summary>
/// Get the XBOX/360 auxiliary info from the outputted files, if possible
/// </summary>
/// <param name="disc">_disc.txt file location</param>
/// <returns>True on successful extraction of info, false otherwise</returns>
private bool GetXBOXAuxInfo(string disc, out string dmihash, out string pfihash, out string sshash, out string ss, out string ssver)
{
dmihash = null; pfihash = null; sshash = null; ss = null; ssver = null;
// If the file doesn't exist, we can't get info from it
if (!File.Exists(disc))
return false;
using (StreamReader sr = File.OpenText(disc))
{
try
{
// Fast forward to the Security Sector version and read it
while (!sr.ReadLine().Trim().StartsWith("CPR_MAI Key")) ;
ssver = sr.ReadLine().Trim().Split(' ')[4]; // "Version of challenge table: <VER>"
// Fast forward to the Security Sector Ranges
while (!sr.ReadLine().Trim().StartsWith("Number of security sector ranges:")) ;
// Now that we're at the ranges, read each line in and concatenate
Regex layerRegex = new Regex(@"Layer [01].*, startLBA-endLBA:\s*(\d+)-\s*(\d+)");
string line = sr.ReadLine().Trim();
while (!line.StartsWith("========== Unlock 2 state(wxripper) =========="))
{
// If we have a recognized line format, parse it
if (line.StartsWith("Layer "))
{
var match = layerRegex.Match(line);
ss += $"{match.Groups[1]}-{match.Groups[2]}\n";
}
line = sr.ReadLine().Trim();
}
// Fast forward to the aux hashes
while (!line.StartsWith("<rom"))
line = sr.ReadLine().Trim();
// Read in the hashes to the proper parts
while (line.StartsWith("<rom"))
{
if (line.Contains("SS.bin"))
sshash = line;
else if (line.Contains("PFI.bin"))
pfihash = line;
else if (line.Contains("DMI.bin"))
dmihash = line;
if (sr.EndOfStream)
break;
line = sr.ReadLine().Trim();
}
return true;
}
catch
{
// We don't care what the exception is right now
return false;
}
}
}
/// <summary>
/// Get the XOX serial info from the DMI.bin file, if possible
/// </summary>
/// <param name="dmi">DMI.bin file location</param>
/// <returns>True on successful extraction of info, false otherwise</returns>
private bool GetXBOXDMIInfo(string dmi, out string serial, out string version, out Region? region)
{
serial = null; version = null; region = Region.World;
if (!File.Exists(dmi))
return false;
using (BinaryReader br = new BinaryReader(File.OpenRead(dmi)))
{
try
{
br.BaseStream.Seek(8, SeekOrigin.Begin);
char[] str = br.ReadChars(8);
serial = $"{str[0]}{str[1]}-{str[2]}{str[3]}{str[4]}";
version = $"1.{str[5]}{str[6]}";
region = GetXBOXRegion(str[7]);
return true;
}
catch
{
return false;
}
}
}
/// <summary>
/// Get the XBOX 360 serial info from the DMI.bin file, if possible
/// </summary>
/// <param name="dmi">DMI.bin file location</param>
/// <returns>True on successful extraction of info, false otherwise</returns>
private bool GetXBOX360DMIInfo(string dmi, out string serial, out string version, out Region? region)
{
serial = null; version = null; region = null;
if (!File.Exists(dmi))
return false;
using (BinaryReader br = new BinaryReader(File.OpenRead(dmi)))
{
try
{
br.BaseStream.Seek(64, SeekOrigin.Begin);
char[] str = br.ReadChars(14);
serial = $"{str[0]}{str[1]}-{str[2]}{str[3]}{str[4]}{str[5]}";
version = $"1.{str[6]}{str[7]}";
region = GetXBOXRegion(str[8]);
// str[9], str[10], str[11] - unknown purpose
// str[12], str[13] - disc <12> of <13>
return true;
}
catch
{
return false;
}
}
}
/// <summary>
/// Determine the region based on the XBOX serial character
/// </summary>
/// <param name="region">Character denoting the region</param>
/// <returns>Region, if possible</returns>
private Region? GetXBOXRegion(char region)
{
switch (region)
{
case 'W':
return Region.World;
case 'A':
return Region.USA;
case 'J':
return Region.JapanAsia;
case 'E':
return Region.Europe;
case 'K':
return Region.USAJapan;
case 'L':
return Region.USAEurope;
case 'H':
return Region.JapanEurope;
default:
return null;
}
}
/// <summary>
/// Get the write offset from the input file, if possible
/// </summary>
/// <param name="disc">_disc.txt file location</param>
/// <returns>Sample write offset if possible, null on error</returns>
private string GetWriteOffset(string disc)
{
// If the file doesn't exist, we can't get info from it
if (!File.Exists(disc))
{
return null;
}
using (StreamReader sr = File.OpenText(disc))
{
try
{
// Fast forward to the offsets
while (!sr.ReadLine().Trim().StartsWith("========== Offset")) ;
sr.ReadLine(); // Combined Offset
sr.ReadLine(); // Drive Offset
sr.ReadLine(); // Separator line
// Now that we're at the offsets, attempt to get the sample offset
return sr.ReadLine().Split(' ').LastOrDefault();
}
catch
{
// We don't care what the exception is right now
return null;
}
}
}
#endregion
}
}