diff --git a/SabreTools.Serialization/Readers/MicrosoftCabinet.cs b/SabreTools.Serialization/Readers/MicrosoftCabinet.cs index 875f35a7..25d2fdc2 100644 --- a/SabreTools.Serialization/Readers/MicrosoftCabinet.cs +++ b/SabreTools.Serialization/Readers/MicrosoftCabinet.cs @@ -203,13 +203,13 @@ namespace SabreTools.Serialization.Readers dataBlock.Checksum = data.ReadUInt32LittleEndian(); dataBlock.CompressedSize = data.ReadUInt16LittleEndian(); dataBlock.UncompressedSize = data.ReadUInt16LittleEndian(); - + if (dataReservedSize > 0) - dataBlock.ReservedData = data.ReadBytes(dataReservedSize); + data.SeekIfPossible(dataReservedSize, SeekOrigin.Current); if (dataBlock.CompressedSize > 0) - dataBlock.CompressedData = data.ReadBytes(dataBlock.CompressedSize); - + data.SeekIfPossible(dataBlock.CompressedSize, SeekOrigin.Current); + return dataBlock; } diff --git a/SabreTools.Serialization/Wrappers/MicrosoftCabinet.Extraction.cs b/SabreTools.Serialization/Wrappers/MicrosoftCabinet.Extraction.cs index 9b754325..8831b0d8 100644 --- a/SabreTools.Serialization/Wrappers/MicrosoftCabinet.Extraction.cs +++ b/SabreTools.Serialization/Wrappers/MicrosoftCabinet.Extraction.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using SabreTools.Data.Models.MicrosoftCabinet; using SabreTools.IO.Extensions; @@ -174,12 +175,22 @@ namespace SabreTools.Serialization.Wrappers // Loop through the current folders for (int f = 0; f < cabinet.Folders.Length; f++) { + if (f == 0 && (cabinet.Files[0].FolderIndex == FolderIndex.CONTINUED_PREV_AND_NEXT + || cabinet.Files[0].FolderIndex == FolderIndex.CONTINUED_FROM_PREV)) + continue; + var folder = cabinet.Folders[f]; allExtracted &= cabinet.ExtractFolder(Filename, outputDirectory, folder, f, ignorePrev, includeDebug); } // Move to the next cabinet, if possible + Array.ForEach(cabinet.Folders, folder => folder.DataBlocks = []); + cabinet = cabinet.Next; + cabinet?.Prev = null; + + // TODO: already-extracted data isn't being cleared from memory, at least not nearly enough. + if (cabinet?.Folders == null || cabinet.Folders.Length == 0) break; } @@ -192,7 +203,7 @@ namespace SabreTools.Serialization.Wrappers return false; } } - + /// /// Extract the contents of a single folder /// @@ -217,15 +228,77 @@ namespace SabreTools.Serialization.Wrappers // Loop through the files bool allExtracted = true; - var files = GetFiles(folderIndex, ignorePrev); + var filterFiles = GetSpannedFiles(filename, folderIndex, ignorePrev); + List fileList = []; + + // Filtering, add debug output eventually + for (int i = 0; i < filterFiles.Length; i++) + { + var file = filterFiles[i]; + + if (file.FolderIndex == FolderIndex.CONTINUED_PREV_AND_NEXT || + file.FolderIndex == FolderIndex.CONTINUED_FROM_PREV) + { + // debug output for inconsistencies would go here + continue; + } + + fileList.Add(file); + } + + CFFILE[] files = fileList.ToArray(); + blockStream.SeekIfPossible(0, SeekOrigin.Begin); for (int i = 0; i < files.Length; i++) { var file = files[i]; - allExtracted &= ExtractFile(outputDirectory, blockStream, file, includeDebug); + + allExtracted &= ExtractFiles(outputDirectory, blockStream, file, includeDebug); } return allExtracted; } + + // TODO: this will apparently improve memory usage/performance, but it's not clear if this implementation is enough for that to happen + /// + /// Extract the contents of a single file, intended to be used with all files in a straight shot + /// + /// Path to the output directory + /// Stream representing the uncompressed block data + /// File information + /// True to include debug data, false otherwise + /// True if the file extracted, false otherwise + private static bool ExtractFiles(string outputDirectory, Stream blockStream, CFFILE file, bool includeDebug) + { + try + { + byte[] fileData = blockStream.ReadBytes((int)file.FileSize); + + // Ensure directory separators are consistent + string filename = file.Name; + if (Path.DirectorySeparatorChar == '\\') + filename = filename.Replace('/', '\\'); + else if (Path.DirectorySeparatorChar == '/') + filename = filename.Replace('\\', '/'); + + // Ensure the full output directory exists + filename = Path.Combine(outputDirectory, filename); + var directoryName = Path.GetDirectoryName(filename); + if (directoryName != null && !Directory.Exists(directoryName)) + Directory.CreateDirectory(directoryName); + + // Open the output file for writing + using var fs = File.Open(filename, FileMode.Create, FileAccess.Write, FileShare.None); + fs.Write(fileData, 0, fileData.Length); + fs.Flush(); + } + catch (Exception ex) + { + if (includeDebug) Console.Error.WriteLine(ex); + return false; + } + + return true; + } /// /// Extract the contents of a single file diff --git a/SabreTools.Serialization/Wrappers/MicrosoftCabinet.cs b/SabreTools.Serialization/Wrappers/MicrosoftCabinet.cs index 49ab1983..78743500 100644 --- a/SabreTools.Serialization/Wrappers/MicrosoftCabinet.cs +++ b/SabreTools.Serialization/Wrappers/MicrosoftCabinet.cs @@ -2,6 +2,7 @@ using System.IO; using SabreTools.Data.Models.MicrosoftCabinet; using SabreTools.IO.Compression.MSZIP; +using SabreTools.IO.Extensions; namespace SabreTools.Serialization.Wrappers { @@ -315,6 +316,8 @@ namespace SabreTools.Serialization.Wrappers if (folder?.DataBlocks == null || folder.DataBlocks.Length == 0) return null; + GetData(folder); + // Get all files for the folder var files = GetFiles(folderIndex); if (files.Length == 0) @@ -335,7 +338,7 @@ namespace SabreTools.Serialization.Wrappers // Get all blocks from Prev if (Prev?.Header != null && Prev.Folders != null) { - int prevFolderIndex = Prev.FolderCount; + int prevFolderIndex = Prev.FolderCount - 1; var prevFolder = Prev.Folders[prevFolderIndex - 1]; prevBlocks = Prev.GetDataBlocks(filename, prevFolder, prevFolderIndex, skipNext: true) ?? []; } @@ -360,6 +363,106 @@ namespace SabreTools.Serialization.Wrappers // Return all found blocks in order return [.. prevBlocks, .. folder.DataBlocks, .. nextBlocks]; } + + /// + /// Loads in all the datablocks for the current folder. + /// + /// The folder to have the datablocks loaded for + public void GetData(CFFOLDER folder) + { + if (folder.CabStartOffset <= 0) + return; + + uint offset = folder.CabStartOffset; + for (int i = 0; i < folder.DataCount; i++) + { + offset += 8; + + if (Header.DataReservedSize > 0) + { + folder.DataBlocks[i].ReservedData = ReadRangeFromSource(offset, Header.DataReservedSize); + offset += Header.DataReservedSize; + } + + if (folder.DataBlocks[i].CompressedSize > 0) + { + folder.DataBlocks[i].CompressedData = ReadRangeFromSource(offset, folder.DataBlocks[i].CompressedSize); + offset += folder.DataBlocks[i].CompressedSize; + } + } + + } + + /// + /// Get all files for the current folder, plus connected spanned folders. + /// + /// Index of the folder in the cabinet + /// True to ignore previous links, false otherwise + /// Array of all files for the folder + private CFFILE[] GetSpannedFiles(string? filename, int folderIndex, bool ignorePrev = false, bool skipPrev = false, bool skipNext = false) + { + // Ignore invalid archives + if (Files.IsNullOrEmpty()) + return []; + + // Get all files with a name and matching index + var files = Array.FindAll(Files, f => + { + if (string.IsNullOrEmpty(f.Name)) + return false; + + // Ignore links to previous cabinets, if required + if (ignorePrev) + { + if (f.FolderIndex == FolderIndex.CONTINUED_FROM_PREV) + return false; + else if (f.FolderIndex == FolderIndex.CONTINUED_PREV_AND_NEXT) + return false; + } + + int fileFolder = GetFolderIndex(f); + return fileFolder == folderIndex; + }); + + // Check if the folder spans in either direction + bool spanPrev = Array.Exists(files, f => f.FolderIndex == FolderIndex.CONTINUED_FROM_PREV || f.FolderIndex == FolderIndex.CONTINUED_PREV_AND_NEXT); + bool spanNext = Array.Exists(files, f => f.FolderIndex == FolderIndex.CONTINUED_TO_NEXT || f.FolderIndex == FolderIndex.CONTINUED_PREV_AND_NEXT); + + // If the folder spans backward and Prev is not being skipped + CFFILE[] prevFiles = []; + if (!skipPrev && spanPrev) + { + // Try to get Prev if it doesn't exist + if (Prev?.Header == null) + Prev = OpenPrevious(filename); + + // Get all files from Prev + if (Prev?.Header != null && Prev.Folders != null) + { + int prevFolderIndex = Prev.FolderCount - 1; + prevFiles = Prev.GetSpannedFiles(filename, prevFolderIndex, skipNext: true) ?? []; + } + } + + // If the folder spans forward and Next is not being skipped + CFFILE[] nextFiles = []; + if (!skipNext && spanNext) + { + // Try to get Next if it doesn't exist + if (Next?.Header == null) + Next = OpenNext(filename); + + // Get all files from Prev + if (Next?.Header != null && Next.Folders != null) + { + var nextFolder = Next.Folders[0]; + nextFiles = Next.GetSpannedFiles(filename, 0, skipPrev: true) ?? []; + } + } + + // Return all found files in order + return [.. prevFiles, .. files, .. nextFiles]; + } /// /// Get all files for the current folder index