using System;
using System.Collections.Generic;
using System.IO;
#if NET452_OR_GREATER || NETCOREAPP
using System.IO.Compression;
#endif
using System.Text;
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
///
/// 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 output files generated
///
/// Base filename and path to use for checking
/// Base filename and path to use for checking
/// List of all output files, empty otherwise
internal abstract List GetOutputFiles(string? baseDirectory, string baseFilename);
#endregion
#region Output Files
///
/// 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 CompressLogFiles(string? outputDirectory, string? filenameSuffix, string outputFilename, out string status)
{
#if NET20 || NET35 || NET40
status = "Log compression is not available for this framework version";
return false;
#else
// Prepare the necessary paths
outputFilename = Path.GetFileNameWithoutExtension(outputFilename);
string combinedBase;
if (string.IsNullOrEmpty(outputDirectory))
combinedBase = outputFilename;
else
combinedBase = Path.Combine(outputDirectory, outputFilename);
// Generate the archive filename
string archiveName = $"{combinedBase}_logs.zip";
// Get the lists of zippable files
var zippableFiles = GetZippableFilePaths(combinedBase);
var generatedFiles = GetGeneratedFilePaths(outputDirectory, filenameSuffix);
// Don't create an archive if there are no paths
if (zippableFiles.Count == 0 && generatedFiles.Count == 0)
{
status = "No files to compress!";
return true;
}
// If the file already exists, we want to delete the old one
try
{
if (File.Exists(archiveName))
File.Delete(archiveName);
}
catch
{
status = "Could not delete old archive!";
return false;
}
// Add the log files to the archive and delete the uncompressed file after
ZipArchive? zf = null;
try
{
zf = ZipFile.Open(archiveName, ZipArchiveMode.Create);
_ = AddToArchive(zf, zippableFiles, outputDirectory, true);
_ = AddToArchive(zf, generatedFiles, outputDirectory, false);
status = "Compression complete!";
return true;
}
catch (Exception ex)
{
status = $"Compression could not complete: {ex}";
return false;
}
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 DeleteUnnecessaryFiles(string? outputDirectory, string outputFilename, out string status)
{
// 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.Count == 0)
{
status = "No files to delete!";
return true;
}
// Attempt to delete all of the files
foreach (string file in files)
{
try
{
File.Delete(file);
}
catch { }
}
status = "Deletion complete!";
return true;
}
///
/// 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
/// A list representing missing files, empty if none
public List FoundAllFiles(string? outputDirectory, string outputFilename)
{
// Sanitize the output filename to strip off any potential extension
outputFilename = Path.GetFileNameWithoutExtension(outputFilename);
// Finally, let the parameters say if all files exist
return CheckRequiredFiles(outputDirectory, outputFilename);
}
///
/// Generate artifacts and return them as a dictionary
///
/// Base filename and path to use for checking
/// Dictiionary of artifact keys to Base64-encoded values, if possible
public Dictionary GenerateArtifacts(string basePath)
{
// Handle invalid inputs
if (basePath.Length == 0)
return [];
// Split the base path for matching
string baseDirectory = Path.GetDirectoryName(basePath) ?? string.Empty;
string baseFilename = Path.GetFileNameWithoutExtension(basePath);
// Get the list of output files
var outputFiles = GetOutputFiles(baseDirectory, baseFilename);
if (outputFiles.Count == 0)
return [];
// Create the artifacts dictionary
var artifacts = new Dictionary();
// Only try to create artifacts for files that exist
foreach (var outputFile in outputFiles)
{
// Skip non-artifact files
if (!outputFile.IsArtifact || outputFile.ArtifactKey == null)
continue;
// Skip non-existent files
if (!outputFile.Exists(baseDirectory))
continue;
// Skip non-existent files
foreach (var filePath in outputFile.GetPaths(baseDirectory))
{
// Get binary artifacts as a byte array
if (outputFile.IsBinaryArtifact)
{
byte[] data = File.ReadAllBytes(filePath);
string str = Convert.ToBase64String(data);
artifacts[outputFile.ArtifactKey] = str;
}
else
{
string? data = ProcessingTool.GetFullFile(filePath);
string str = ProcessingTool.GetBase64(data) ?? string.Empty;
artifacts[outputFile.ArtifactKey] = str;
}
break;
}
}
return artifacts;
}
#if NET452_OR_GREATER || NETCOREAPP
///
/// Try to add a set of files to an existing archive
///
/// Archive to add the file to
/// Full path to a set of existing files
/// Directory that the existing files live in
/// Indicates if the files should be deleted after adding
/// True if all files were added successfully, false otherwise
private static bool AddToArchive(ZipArchive archive, List files, string? outputDirectory, bool delete)
{
// An empty list means success
if (files.Count == 0)
return true;
// Loop through and add all files
bool allAdded = true;
foreach (string file in files)
{
allAdded &= AddToArchive(archive, file, outputDirectory, delete);
}
return allAdded;
}
///
/// Try to add a file to an existing archive
///
/// Archive to add the file to
/// Full path to an existing file
/// Directory that the existing file lives in
/// Indicates if the file should be deleted after adding
/// True if the file was added successfully, false otherwise
private static bool AddToArchive(ZipArchive archive, string file, string? outputDirectory, bool delete)
{
// Check if the file exists
if (!File.Exists(file))
return false;
// Get the entry name from the file
string entryName = file;
if (!string.IsNullOrEmpty(outputDirectory))
entryName = entryName.Substring(outputDirectory!.Length);
// Ensure the entry is formatted correctly
entryName = entryName.TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
// Create and add the entry
try
{
#if NETFRAMEWORK || NETCOREAPP3_1 || NET5_0
archive.CreateEntryFromFile(file, entryName, CompressionLevel.Optimal);
#else
archive.CreateEntryFromFile(file, entryName, CompressionLevel.SmallestSize);
#endif
}
catch
{
return false;
}
// Try to delete the file if requested
if (delete)
{
try { File.Delete(file); } catch { }
}
return true;
}
#endif
///
/// Validate if all required output files exist
///
/// Base directory to check
/// Base filename template to use
/// A list representing missing files, empty if none
internal List CheckRequiredFiles(string? baseDirectory, string baseFilename)
{
// Assemble a base path
string basePath = baseFilename;
if (!string.IsNullOrEmpty(baseDirectory))
basePath = Path.Combine(baseDirectory, basePath);
// Get the list of output files
var outputFiles = GetOutputFiles(baseDirectory, baseFilename);
if (outputFiles.Count == 0)
return ["Media and system combination not supported"];
// Check for the log file
bool logArchiveExists = false;
#if NET452_OR_GREATER || NETCOREAPP
ZipArchive? logArchive = null;
#endif
if (File.Exists($"{basePath}_logs.zip"))
{
logArchiveExists = true;
#if NET452_OR_GREATER || NETCOREAPP
try
{
// Try to open the archive
logArchive = ZipFile.OpenRead($"{basePath}_logs.zip");
}
catch
{
logArchiveExists = false;
}
#endif
}
// Get a list of all missing required files
var missingFiles = new List();
foreach (var outputFile in outputFiles)
{
// Only check required files
if (!outputFile.IsRequired)
continue;
// Use the built-in existence function
if (outputFile.Exists(baseDirectory ?? string.Empty))
continue;
// If the log archive doesn't exist
if (!logArchiveExists)
{
missingFiles.Add(outputFile.Filenames[0]);
continue;
}
#if NET20 || NET35 || NET40
// Assume the zipfile has the file in it
continue;
#else
// Check the log archive
if (outputFile.Exists(logArchive))
continue;
// Add the file to the missing list
missingFiles.Add(outputFile.Filenames[0]);
#endif
}
return missingFiles;
}
///
/// Generate a list of all deleteable file paths
///
/// Base filename and path to use for checking
/// List of all deleteable file paths, empty otherwise
internal List GetDeleteableFilePaths(string basePath)
{
// Split the base path for matching
string baseDirectory = Path.GetDirectoryName(basePath) ?? string.Empty;
string baseFilename = Path.GetFileNameWithoutExtension(basePath);
// Get the list of output files
var outputFiles = GetOutputFiles(baseDirectory, baseFilename);
if (outputFiles.Count == 0)
return [];
// Filter down to deleteable files
var deleteable = outputFiles.FindAll(of => of.IsDeleteable);
// Get all paths that exist
var deleteablePaths = new List();
foreach (var file in deleteable)
{
var paths = file.GetPaths(baseDirectory);
paths = paths.FindAll(File.Exists);
deleteablePaths.AddRange(paths);
}
return deleteablePaths;
}
///
/// Generate a list of all MPF-generated filenames
///
/// Optional suffix to append to the filename
/// List of all MPF-generated filenames, empty otherwise
internal static List GetGeneratedFilenames(string? filenameSuffix)
{
// Set the base file path names
const string submissionInfoBase = "!submissionInfo";
const string protectionInfoBase = "!protectionInfo";
// Ensure the filename suffix is formatted correctly
filenameSuffix = string.IsNullOrEmpty(filenameSuffix) ? string.Empty : $"_{filenameSuffix}";
// Define the output filenames
return [
$"{protectionInfoBase}{filenameSuffix}.txt",
$"{submissionInfoBase}{filenameSuffix}.json",
$"{submissionInfoBase}{filenameSuffix}.json.gz",
$"{submissionInfoBase}{filenameSuffix}.txt",
];
}
///
/// Generate a list of all MPF-specific log files generated
///
/// Base directory to use for checking
/// Optional suffix to append to the filename
/// List of all log file paths, empty otherwise
internal static List GetGeneratedFilePaths(string? baseDirectory, string? filenameSuffix)
{
// Get the list of generated files
var generatedFilenames = GetGeneratedFilenames(filenameSuffix);
if (generatedFilenames.Count == 0)
return [];
// Ensure the output directory
baseDirectory ??= string.Empty;
// Return only files that exist
var generatedFiles = new List();
foreach (var filename in generatedFilenames)
{
// Skip non-existent files
string possiblePath = Path.Combine(baseDirectory, filename);
if (!File.Exists(possiblePath))
continue;
generatedFiles.Add(possiblePath);
}
return generatedFiles;
}
///
/// Generate a list of all zippable file paths
///
/// Base filename and path to use for checking
/// List of all zippable file paths, empty otherwise
internal List GetZippableFilePaths(string basePath)
{
// Split the base path for matching
string baseDirectory = Path.GetDirectoryName(basePath) ?? string.Empty;
string baseFilename = Path.GetFileNameWithoutExtension(basePath);
// Get the list of output files
var outputFiles = GetOutputFiles(baseDirectory, baseFilename);
if (outputFiles.Count == 0)
return [];
// Filter down to zippable files
var zippable = outputFiles.FindAll(of => of.IsZippable);
// Get all paths that exist
var zippablePaths = new List();
foreach (var file in zippable)
{
var paths = file.GetPaths(baseDirectory);
paths = paths.FindAll(File.Exists);
zippablePaths.AddRange(paths);
}
return zippablePaths;
}
#endregion
#region Shared Methods
///
/// 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
internal 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;
// If the trim length is 0, no data will be returned
if (trimLength == 0)
return string.Empty;
try
{
var hex = ProcessingTool.GetFullFile(picPath, true);
if (hex == null)
return null;
if (trimLength > -1 && trimLength < hex.Length)
hex = hex.Substring(0, trimLength);
// TODO: Check for non-zero values in discarded PIC
return SplitString(hex, 32);
}
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
internal 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;
}
}
///
/// Split a string with newlines every characters
///
internal static string SplitString(string? str, int count, bool trim = false)
{
// Ignore invalid inputs
if (str == null || str.Length == 0)
return string.Empty;
// Handle non-modifying counts
if (count < 1 || count > str.Length)
return $"{str}\n";
// Build the output string
var sb = new StringBuilder();
for (int i = 0; i < str.Length; i += count)
{
int lineSize = Math.Min(count, str.Length - i);
string line = str.Substring(i, lineSize);
if (trim)
line = line.Trim();
sb.Append(line);
sb.Append('\n');
}
return sb.ToString();
}
#endregion
}
}