using System;
using System.Collections.Generic;
using System.IO;
#if NET452_OR_GREATER || NETCOREAPP
using System.IO.Compression;
#endif
using System.Linq;
using System.Text.RegularExpressions;
using SabreTools.RedumpLib.Data;
namespace MPF.Processors
{
public abstract class BaseProcessor
{
///
/// All found volume labels and their corresponding file systems
///
public Dictionary>? VolumeLabels;
#region Metadata
///
/// Currently represented system
///
public RedumpSystem? System { get; private set; }
///
/// Currently represented media type
///
public MediaType? Type { get; private set; }
#endregion
///
/// Generate processor for a system and media type combination
///
/// RedumpSystem value to use
/// MediaType value to use
public BaseProcessor(RedumpSystem? system, MediaType? type)
{
System = system;
Type = type;
}
#region Abstract Methods
///
/// Validate if all required output files exist
///
/// Base filename and path to use for checking
/// True if this is a check done before a dump, false if done after
/// Tuple of true if all required files exist, false otherwise and a list representing missing files
public abstract (bool, List) CheckAllOutputFilesExist(string basePath, bool preCheck);
///
/// Generate artifacts and add to the SubmissionInfo
///
/// Base submission info to fill in specifics for
/// Base filename and path to use for checking
public abstract void GenerateArtifacts(SubmissionInfo submissionInfo, string basePath);
///
/// Generate a SubmissionInfo for the output files
///
/// Base submission info to fill in specifics for
/// Base filename and path to use for checking
/// Determines if outputs are processed according to Redump specifications
public abstract void GenerateSubmissionInfo(SubmissionInfo submissionInfo, string basePath, bool redumpCompat);
///
/// Generate a list of all log files generated
///
/// Base filename and path to use for checking
/// List of all log file paths, empty otherwise
public abstract List GetLogFilePaths(string basePath);
#endregion
#region Virtual Methods
///
/// Generate a list of all deleteable files generated
///
/// Base filename and path to use for checking
/// List of all deleteable file paths, empty otherwise
public virtual List GetDeleteableFilePaths(string basePath) => [];
#endregion
#region Shared Methods
///
/// Compress log files to save space
///
/// Output folder to write to
/// Output filename to use as the base path
/// Output filename to use as the base path
/// Processor object representing how to process the outputs
/// True if the process succeeded, false otherwise
public (bool, string) CompressLogFiles(string? outputDirectory, string? filenameSuffix, string outputFilename)
{
#if NET20 || NET35 || NET40
return (false, "Log compression is not available for this framework version");
#else
// Prepare the necessary paths
outputFilename = Path.GetFileNameWithoutExtension(outputFilename);
string combinedBase;
if (string.IsNullOrEmpty(outputDirectory))
combinedBase = outputFilename;
else
combinedBase = Path.Combine(outputDirectory, outputFilename);
string archiveName = combinedBase + "_logs.zip";
// Get the list of log files from the parameters object
var files = GetLogFilePaths(combinedBase);
// Add on generated log files if they exist
var mpfFiles = GetGeneratedFilePaths(outputDirectory, filenameSuffix);
files.AddRange(mpfFiles);
if (!files.Any())
return (true, "No files to compress!");
// If the file already exists, we want to delete the old one
try
{
if (File.Exists(archiveName))
File.Delete(archiveName);
}
catch
{
return (false, "Could not delete old archive!");
}
// Add the log files to the archive and delete the uncompressed file after
ZipArchive? zf = null;
try
{
zf = ZipFile.Open(archiveName, ZipArchiveMode.Create);
foreach (string file in files)
{
if (string.IsNullOrEmpty(outputDirectory))
{
zf.CreateEntryFromFile(file, file, CompressionLevel.Optimal);
}
else
{
string entryName = file[outputDirectory!.Length..].TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
#if NETFRAMEWORK || NETCOREAPP3_1 || NET5_0
zf.CreateEntryFromFile(file, entryName, CompressionLevel.Optimal);
#else
zf.CreateEntryFromFile(file, entryName, CompressionLevel.SmallestSize);
#endif
}
// If the file is MPF-specific, don't delete
if (mpfFiles.Contains(file))
continue;
try
{
File.Delete(file);
}
catch { }
}
return (true, "Compression complete!");
}
catch (Exception ex)
{
return (false, $"Compression could not complete: {ex}");
}
finally
{
zf?.Dispose();
}
#endif
}
///
/// Compress log files to save space
///
/// Output folder to write to
/// Output filename to use as the base path
/// Processor object representing how to process the outputs
/// True if the process succeeded, false otherwise
public (bool, string) DeleteUnnecessaryFiles(string? outputDirectory, string outputFilename)
{
// Prepare the necessary paths
outputFilename = Path.GetFileNameWithoutExtension(outputFilename);
string combinedBase;
if (string.IsNullOrEmpty(outputDirectory))
combinedBase = outputFilename;
else
combinedBase = Path.Combine(outputDirectory, outputFilename);
// Get the list of deleteable files from the parameters object
var files = GetDeleteableFilePaths(combinedBase);
if (!files.Any())
return (true, "No files to delete!");
// Attempt to delete all of the files
try
{
foreach (string file in files)
{
try
{
File.Delete(file);
}
catch { }
}
return (true, "Deletion complete!");
}
catch (Exception ex)
{
return (false, $"Deletion could not complete: {ex}");
}
}
///
/// Ensures that all required output files have been created
///
/// Output folder to write to
/// Output filename to use as the base path
/// Processor object representing how to process the outputs
/// True if this is a check done before a dump, false if done after
/// Tuple of true if all required files exist, false otherwise and a list representing missing files
public (bool, List) FoundAllFiles(string? outputDirectory, string outputFilename, bool preCheck)
{
// First, sanitized the output filename to strip off any potential extension
outputFilename = Path.GetFileNameWithoutExtension(outputFilename);
// Then get the base path for all checking
string basePath;
if (string.IsNullOrEmpty(outputDirectory))
basePath = outputFilename;
else
basePath = Path.Combine(outputDirectory, outputFilename);
// Finally, let the parameters say if all files exist
return CheckAllOutputFilesExist(basePath, preCheck);
}
///
/// Get the hex contents of the PIC file
///
/// Path to the PIC.bin file associated with the dump
/// Number of characters to trim the PIC to, if -1, ignored
/// PIC data as a hex string if possible, null on error
/// https://stackoverflow.com/questions/9932096/add-separator-to-string-at-every-n-characters
protected static string? GetPIC(string picPath, int trimLength = -1)
{
// If the file doesn't exist, we can't get the info
if (!File.Exists(picPath))
return null;
try
{
var hex = ProcessingTool.GetFullFile(picPath, true);
if (hex == null)
return null;
if (trimLength > -1)
hex = hex.Substring(0, trimLength);
// TODO: Check for non-zero values in discarded PIC
return Regex.Replace(hex, ".{32}", "$0\n", RegexOptions.Compiled);
}
catch
{
// We don't care what the error was right now
return null;
}
}
///
/// Get a isobuster-formatted PVD from a 2048 byte-per-sector image, if possible
///
/// Path to ISO file
/// Formatted PVD string, otherwise null
/// True if PVD was successfully parsed, otherwise false
protected static bool GetPVD(string isoPath, out string? pvd)
{
pvd = null;
try
{
// Get PVD bytes from ISO file
var buf = new byte[96];
using (FileStream iso = File.OpenRead(isoPath))
{
// TODO: Don't hardcode 0x8320
iso.Seek(0x8320, SeekOrigin.Begin);
int offset = 0;
while (offset < 96)
{
int read = iso.Read(buf, offset, buf.Length - offset);
if (read == 0)
throw new EndOfStreamException();
offset += read;
}
}
// Format PVD to isobuster standard
char[] pvdCharArray = new char[96];
for (int i = 0; i < 96; i++)
{
if (buf[i] >= 0x20 && buf[i] <= 0x7E)
pvdCharArray[i] = (char)buf[i];
else
pvdCharArray[i] = '.';
}
string pvdASCII = new string(pvdCharArray, 0, 96);
pvd = string.Empty;
for (int i = 0; i < 96; i += 16)
{
pvd += $"{(0x0320 + i):X4} : {buf[i]:X2} {buf[i + 1]:X2} {buf[i + 2]:X2} {buf[i + 3]:X2} {buf[i + 4]:X2} {buf[i + 5]:X2} {buf[i + 6]:X2} {buf[i + 7]:X2} " +
$"{buf[i + 8]:X2} {buf[i + 9]:X2} {buf[i + 10]:X2} {buf[i + 11]:X2} {buf[i + 12]:X2} {buf[i + 13]:X2} {buf[i + 14]:X2} {buf[i + 15]:X2} {pvdASCII.Substring(i, 16)}\n";
}
return true;
}
catch
{
// We don't care what the error is
return false;
}
}
///
/// Generate a list of all MPF-specific log files generated
///
/// Output folder to write to
/// Optional suffix to append to the filename
/// List of all log file paths, empty otherwise
private static List GetGeneratedFilePaths(string? outputDirectory, string? filenameSuffix)
{
var files = new List();
if (string.IsNullOrEmpty(outputDirectory) && string.IsNullOrEmpty(filenameSuffix))
{
if (File.Exists("!submissionInfo.txt"))
files.Add("!submissionInfo.txt");
if (File.Exists("!submissionInfo.json"))
files.Add("!submissionInfo.json");
if (File.Exists("!submissionInfo.json.gz"))
files.Add("!submissionInfo.json.gz");
if (File.Exists("!protectionInfo.txt"))
files.Add("!protectionInfo.txt");
}
else if (string.IsNullOrEmpty(outputDirectory) && !string.IsNullOrEmpty(filenameSuffix))
{
if (File.Exists($"!submissionInfo_{filenameSuffix}.txt"))
files.Add($"!submissionInfo_{filenameSuffix}.txt");
if (File.Exists($"!submissionInfo_{filenameSuffix}.json"))
files.Add($"!submissionInfo_{filenameSuffix}.json");
if (File.Exists($"!submissionInfo_{filenameSuffix}.json.gz"))
files.Add($"!submissionInfo_{filenameSuffix}.json.gz");
if (File.Exists($"!protectionInfo_{filenameSuffix}.txt"))
files.Add($"!protectionInfo_{filenameSuffix}.txt");
}
else if (!string.IsNullOrEmpty(outputDirectory) && string.IsNullOrEmpty(filenameSuffix))
{
if (File.Exists(Path.Combine(outputDirectory, "!submissionInfo.txt")))
files.Add(Path.Combine(outputDirectory, "!submissionInfo.txt"));
if (File.Exists(Path.Combine(outputDirectory, "!submissionInfo.json")))
files.Add(Path.Combine(outputDirectory, "!submissionInfo.json"));
if (File.Exists(Path.Combine(outputDirectory, "!submissionInfo.json.gz")))
files.Add(Path.Combine(outputDirectory, "!submissionInfo.json.gz"));
if (File.Exists(Path.Combine(outputDirectory, "!protectionInfo.txt")))
files.Add(Path.Combine(outputDirectory, "!protectionInfo.txt"));
}
else if (!string.IsNullOrEmpty(outputDirectory) && !string.IsNullOrEmpty(filenameSuffix))
{
if (File.Exists(Path.Combine(outputDirectory, $"!submissionInfo_{filenameSuffix}.txt")))
files.Add(Path.Combine(outputDirectory, $"!submissionInfo_{filenameSuffix}.txt"));
if (File.Exists(Path.Combine(outputDirectory, $"!submissionInfo_{filenameSuffix}.json")))
files.Add(Path.Combine(outputDirectory, $"!submissionInfo_{filenameSuffix}.json"));
if (File.Exists(Path.Combine(outputDirectory, $"!submissionInfo_{filenameSuffix}.json.gz")))
files.Add(Path.Combine(outputDirectory, $"!submissionInfo_{filenameSuffix}.json.gz"));
if (File.Exists(Path.Combine(outputDirectory, $"!protectionInfo_{filenameSuffix}.txt")))
files.Add(Path.Combine(outputDirectory, $"!protectionInfo_{filenameSuffix}.txt"));
}
return files;
}
#endregion
}
}