using System; using System.IO; using SabreTools.Data.Models.SecuROM; using SabreTools.Hashing; using SabreTools.Numerics.Extensions; namespace SabreTools.Wrappers { public partial class SecuROMMatroschkaPackage : IExtractable { /// public bool Extract(string outputDirectory, bool includeDebug) { // If we have no entries if (Entries.Length == 0) return false; // Loop through and extract all files to the output bool allExtracted = true; for (var i = 0; i < Entries.Length; i++) { allExtracted &= ExtractFile(i, outputDirectory, includeDebug); } return allExtracted; } /// /// Extract a file from the package to an output directory by index /// /// File index to extract /// Output directory to write to /// True to include debug data, false otherwise /// True if the file extracted, false otherwise public bool ExtractFile(int index, string outputDirectory, bool includeDebug) { // If we have no entries if (Entries.Length == 0) return false; // If the entry index is invalid if (index < 0 || index >= Entries.Length) return false; // Get the entry var entry = Entries[index]; if (entry.Path is null) return false; // Ensure directory separators are consistent string filename = entry.Path.TrimEnd('\0'); filename = filename.TrimStart(['\\', '/']); if (Path.DirectorySeparatorChar == '\\') filename = filename.Replace('/', '\\'); else if (Path.DirectorySeparatorChar == '/') filename = filename.Replace('\\', '/'); if (includeDebug) Console.WriteLine($"Attempting to extract {filename}"); // Read the file var data = ReadFileData(entry, includeDebug); if (data is null) return false; // Ensure the full output directory exists filename = Path.Combine(outputDirectory, filename); var directoryName = Path.GetDirectoryName(filename); if (directoryName is not null && !Directory.Exists(directoryName)) Directory.CreateDirectory(directoryName); // Try to write the data try { // Open the output file for writing using var fs = File.Open(filename, FileMode.Create, FileAccess.Write, FileShare.None); fs.Write(data); fs.Flush(); } catch (Exception ex) { if (includeDebug) Console.Error.WriteLine(ex); return false; } return true; } /// /// Read file and check bytes to be extracted against MD5 checksum /// /// Entry being extracted /// True to include debug data, false otherwise /// Byte array of the file data if successful, null otherwise /// Marked as public because it is needed outside of file extraction public byte[]? ReadFileData(MatroshkaEntry entry, bool includeDebug) { // Skip if the entry is incomplete if (entry.Path is null || entry.MD5 is null) return null; // Cache the expected MD5 string expectedMd5 = BitConverter.ToString(entry.MD5); expectedMd5 = expectedMd5.ToLowerInvariant().Replace("-", string.Empty); // Debug output if (includeDebug) Console.WriteLine($"Offset: {entry.Offset:X8}, Expected Size: {entry.Size}, Expected MD5: {expectedMd5}"); // Attempt to read from the offset var fileData = ReadRangeFromSource(entry.Offset, (int)entry.Size); if (fileData.Length == 0) { if (includeDebug) Console.Error.WriteLine($"Could not read {entry.Size} bytes from {entry.Offset:X8}"); return null; } // Get the actual MD5 of the data string actualMd5 = HashTool.GetByteArrayHash(fileData, HashType.MD5) ?? string.Empty; // Debug output if (includeDebug) Console.WriteLine($"Actual MD5: {actualMd5}"); // Do not return on a hash mismatch if (actualMd5 != expectedMd5) { string filename = entry.Path.TrimEnd('\0'); if (includeDebug) Console.Error.WriteLine($"MD5 checksum failure for file {filename})"); return null; } return fileData; } } }