mirror of
https://github.com/SabreTools/SabreTools.Serialization.git
synced 2026-09-22 23:05:12 +00:00
Extract Ms-cabs while reading instead of loading all datablocks into memory (#56)
* WIP * WIP 2 * Todo: you're missing a read somehow and getting misaligned by two bytes? maybe properly implementing the buffer will magically fix it * continued blocks my behated * Pre-major-testing * Forgot to add summaries * Attempt to properly roll back. The state i wanted to roll back to wasn't in a commit before. * Figured out the issue with the rolled back commit, this has to be a while loop because of 0 byte files. Reimplemented clean code. * Comment so I don't forget why it's like this * Skip unsupported compression types before opening filestream. * Reenable non-start cab skipping * TODO so i don't forget * TODO so I don't forget * iterate on continued block correctly. * Handle incomplete extraction better * Remove TODOs to ready for PR comments * Missed one * Some minor fixes before rewriting everything * Next round of fixes. * Next set of fixes * Fix debug output
This commit is contained in:
committed by
GitHub
parent
2a6e193756
commit
2e5c5b3c79
@@ -171,8 +171,8 @@ namespace SabreTools.Serialization.Readers
|
||||
|
||||
if (header.FolderReservedSize > 0)
|
||||
folder.ReservedData = data.ReadBytes(header.FolderReservedSize);
|
||||
|
||||
if (folder.CabStartOffset > 0)
|
||||
|
||||
/*if (folder.CabStartOffset > 0)
|
||||
{
|
||||
long currentPosition = data.Position;
|
||||
data.SeekIfPossible(folder.CabStartOffset, SeekOrigin.Begin);
|
||||
@@ -185,11 +185,11 @@ namespace SabreTools.Serialization.Readers
|
||||
}
|
||||
|
||||
data.SeekIfPossible(currentPosition, SeekOrigin.Begin);
|
||||
}
|
||||
}*/
|
||||
|
||||
return folder;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a data block
|
||||
/// </summary>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using SabreTools.Data.Models.MicrosoftCabinet;
|
||||
using SabreTools.IO.Compression.MSZIP;
|
||||
using SabreTools.IO.Extensions;
|
||||
|
||||
namespace SabreTools.Serialization.Wrappers
|
||||
@@ -30,8 +31,9 @@ namespace SabreTools.Serialization.Wrappers
|
||||
/// Open a cabinet set for reading, if possible
|
||||
/// </summary>
|
||||
/// <param name="filename">Filename for one cabinet in the set</param>
|
||||
/// <param name="includeDebug">True to include debug data, false otherwise</param>
|
||||
/// <returns>Wrapper representing the set, null on error</returns>
|
||||
private static MicrosoftCabinet? OpenSet(string? filename)
|
||||
private static MicrosoftCabinet? OpenSet(string? filename, bool includeDebug)
|
||||
{
|
||||
// If the file is invalid
|
||||
if (string.IsNullOrEmpty(filename))
|
||||
@@ -52,7 +54,7 @@ namespace SabreTools.Serialization.Wrappers
|
||||
while (current.CabinetPrev != null)
|
||||
{
|
||||
// Attempt to open the previous cabinet
|
||||
var prev = current.OpenPrevious(filename);
|
||||
var prev = current.OpenPrevious(filename, includeDebug);
|
||||
if (prev?.Header == null)
|
||||
break;
|
||||
|
||||
@@ -71,7 +73,7 @@ namespace SabreTools.Serialization.Wrappers
|
||||
break;
|
||||
|
||||
// Open the next cabinet and try to parse
|
||||
var next = current.OpenNext(filename);
|
||||
var next = current.OpenNext(filename, includeDebug);
|
||||
if (next?.Header == null)
|
||||
break;
|
||||
|
||||
@@ -89,7 +91,8 @@ namespace SabreTools.Serialization.Wrappers
|
||||
/// Open the next archive, if possible
|
||||
/// </summary>
|
||||
/// <param name="filename">Filename for one cabinet in the set</param>
|
||||
private MicrosoftCabinet? OpenNext(string? filename)
|
||||
/// <param name="includeDebug">True to include debug data, false otherwise</param>
|
||||
private MicrosoftCabinet? OpenNext(string? filename, bool includeDebug = false)
|
||||
{
|
||||
// Ignore invalid archives
|
||||
if (string.IsNullOrEmpty(filename))
|
||||
@@ -102,22 +105,32 @@ namespace SabreTools.Serialization.Wrappers
|
||||
string? next = CabinetNext;
|
||||
if (string.IsNullOrEmpty(next))
|
||||
return null;
|
||||
|
||||
|
||||
// Get the full next path
|
||||
string? folder = Path.GetDirectoryName(filename);
|
||||
if (folder != null)
|
||||
next = Path.Combine(folder, next);
|
||||
|
||||
// Open and return the next cabinet
|
||||
var fs = File.Open(next, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
||||
return Create(fs);
|
||||
// Catch exceptions due to file not existing, etc
|
||||
try
|
||||
{
|
||||
var fs = File.Open(next, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
||||
return Create(fs);
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (includeDebug) Console.WriteLine($"Error: Cabinet set part {next} could not be opened!");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Open the previous archive, if possible
|
||||
/// </summary>
|
||||
/// <param name="filename">Filename for one cabinet in the set</param>
|
||||
private MicrosoftCabinet? OpenPrevious(string? filename)
|
||||
/// <param name="includeDebug">True to include debug data, false otherwise</param>
|
||||
private MicrosoftCabinet? OpenPrevious(string? filename, bool includeDebug)
|
||||
{
|
||||
// Ignore invalid archives
|
||||
if (string.IsNullOrEmpty(filename))
|
||||
@@ -130,15 +143,24 @@ namespace SabreTools.Serialization.Wrappers
|
||||
string? prev = CabinetPrev;
|
||||
if (string.IsNullOrEmpty(prev))
|
||||
return null;
|
||||
|
||||
|
||||
// Get the full next path
|
||||
string? folder = Path.GetDirectoryName(filename);
|
||||
if (folder != null)
|
||||
prev = Path.Combine(folder, prev);
|
||||
|
||||
// Open and return the previous cabinet
|
||||
var fs = File.Open(prev, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
||||
return Create(fs);
|
||||
// Catch exceptions due to file not existing, etc
|
||||
try
|
||||
{
|
||||
var fs = File.Open(prev, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
||||
return Create(fs);
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (includeDebug) Console.WriteLine($"Error: Cabinet set part {prev} could not be opened!");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -151,83 +173,45 @@ namespace SabreTools.Serialization.Wrappers
|
||||
// Display warning in debug runs
|
||||
if (includeDebug) Console.WriteLine("WARNING: LZX and Quantum compression schemes are not supported so some files may be skipped!");
|
||||
|
||||
// Do not ignore previous links by default
|
||||
bool ignorePrev = false;
|
||||
|
||||
// Open the full set if possible
|
||||
var cabinet = this;
|
||||
if (Filename != null)
|
||||
if (Filename == null)
|
||||
{
|
||||
cabinet = OpenSet(Filename);
|
||||
ignorePrev = true;
|
||||
if (includeDebug) Console.WriteLine($"Cabinet set could not be opened!");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
cabinet = OpenSet(Filename, includeDebug);
|
||||
if (cabinet == null)
|
||||
return false;
|
||||
|
||||
// If we have anything but the first file, avoid extraction to avoid repeat extracts
|
||||
// TODO: if/when full msi support is added, somehow this is going to have to take that into account, while also still handling partial sets
|
||||
if (this.Filename != cabinet.Filename)
|
||||
{
|
||||
string firstCabName = Path.GetFileName(cabinet.Filename) ?? string.Empty;
|
||||
if (includeDebug) Console.WriteLine($"Only the first cabinet {firstCabName} will be extracted!");
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the archive is invalid
|
||||
if (cabinet?.Folders == null || cabinet.Folders.Length == 0)
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
// Loop through the folders
|
||||
bool allExtracted = true;
|
||||
while (true)
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
|
||||
return allExtracted;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (includeDebug) Console.Error.WriteLine(ex);
|
||||
return false;
|
||||
}
|
||||
|
||||
return cabinet.ExtractSet(Filename, outputDirectory, includeDebug);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extract the contents of a single folder
|
||||
/// Get filtered array of spanned files for a folder
|
||||
/// </summary>
|
||||
/// <param name="filename">Filename for one cabinet in the set, if available</param>
|
||||
/// <param name="outputDirectory">Path to the output directory</param>
|
||||
/// <param name="folder">Folder containing the blocks to decompress</param>
|
||||
/// <param name="folderIndex">Index of the folder in the cabinet</param>
|
||||
/// <param name="ignorePrev">True to ignore previous links, false otherwise</param>
|
||||
/// <param name="includeDebug">True to include debug data, false otherwise</param>
|
||||
/// <returns>True if all files extracted, false otherwise</returns>
|
||||
private bool ExtractFolder(string? filename,
|
||||
string outputDirectory,
|
||||
CFFOLDER? folder,
|
||||
int folderIndex,
|
||||
bool ignorePrev,
|
||||
bool includeDebug)
|
||||
/// <returns>Filtered array of files</returns>
|
||||
private CFFILE[] GetSpannedFilesArray(string? filename, int folderIndex, bool includeDebug)
|
||||
{
|
||||
// Decompress the blocks, if possible
|
||||
using var blockStream = DecompressBlocks(filename, folder, folderIndex, includeDebug);
|
||||
if (blockStream == null || blockStream.Length == 0)
|
||||
return false;
|
||||
|
||||
// Loop through the files
|
||||
bool allExtracted = true;
|
||||
var filterFiles = GetSpannedFiles(filename, folderIndex, ignorePrev);
|
||||
var filterFiles = GetSpannedFiles(filename, folderIndex, includeDebug);
|
||||
List<CFFILE> fileList = [];
|
||||
|
||||
// Filtering, add debug output eventually
|
||||
@@ -245,99 +229,302 @@ namespace SabreTools.Serialization.Wrappers
|
||||
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 &= ExtractFiles(outputDirectory, blockStream, file, includeDebug);
|
||||
}
|
||||
|
||||
return allExtracted;
|
||||
return fileList.ToArray();
|
||||
}
|
||||
|
||||
// TODO: this will apparently improve memory usage/performance, but it's not clear if this implementation is enough for that to happen
|
||||
/// <summary>
|
||||
/// Extract the contents of a single file, intended to be used with all files in a straight shot
|
||||
/// Get stream representing the output file
|
||||
/// </summary>
|
||||
/// <param name="filename">Filename for the file that will be extracted to</param>
|
||||
/// <param name="outputDirectory">Path to the output directory</param>
|
||||
/// <param name="blockStream">Stream representing the uncompressed block data</param>
|
||||
/// <param name="file">File information</param>
|
||||
/// <param name="includeDebug">True to include debug data, false otherwise</param>
|
||||
/// <returns>True if the file extracted, false otherwise</returns>
|
||||
private static bool ExtractFiles(string outputDirectory, Stream blockStream, CFFILE file, bool includeDebug)
|
||||
/// <returns>Filestream opened for the file</returns>
|
||||
private FileStream GetFileStream(string filename, string outputDirectory)
|
||||
{
|
||||
// Ensure directory separators are consistent
|
||||
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
|
||||
return File.Open(filename, FileMode.Create, FileAccess.Write, FileShare.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a datablock from a cabinet
|
||||
/// </summary>
|
||||
/// <param name="offset">Offset to be read from</param>
|
||||
/// <param name="includeDebug">True to include debug data, false otherwise</param>
|
||||
/// <returns>Read datablock</returns>
|
||||
private CFDATA? ReadBlock(ref long offset, bool includeDebug)
|
||||
{
|
||||
|
||||
// Should only ever occur if it tries to read more than the file, but good to catch in general
|
||||
try
|
||||
{
|
||||
lock (this._dataSourceLock)
|
||||
{
|
||||
this._dataSource.SeekIfPossible(offset, SeekOrigin.Begin);
|
||||
var dataBlock = new CFDATA();
|
||||
|
||||
var dataReservedSize = this.Header.DataReservedSize;
|
||||
|
||||
dataBlock.Checksum = this._dataSource.ReadUInt32LittleEndian();
|
||||
dataBlock.CompressedSize = this._dataSource.ReadUInt16LittleEndian();
|
||||
dataBlock.UncompressedSize = this._dataSource.ReadUInt16LittleEndian();
|
||||
|
||||
if (dataReservedSize > 0)
|
||||
dataBlock.ReservedData = this._dataSource.ReadBytes(dataReservedSize);
|
||||
|
||||
if (dataBlock.CompressedSize > 0)
|
||||
dataBlock.CompressedData = this._dataSource.ReadBytes(dataBlock.CompressedSize);
|
||||
|
||||
offset = _dataSource.Position;
|
||||
|
||||
return dataBlock;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (includeDebug) Console.Error.WriteLine(ex);
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extract the contents of a cabinet set
|
||||
/// </summary>
|
||||
/// <param name="cabFilename">Filename for one cabinet in the set, if available</param>
|
||||
/// <param name="outputDirectory">Path to the output directory</param>
|
||||
/// <param name="includeDebug">True to include debug data, false otherwise</param>
|
||||
/// <returns>True if all files extracted, false otherwise</returns>
|
||||
private bool ExtractSet(string? cabFilename, string outputDirectory, bool includeDebug)
|
||||
{
|
||||
var cabinet = this;
|
||||
var currentCabFilename = cabFilename;
|
||||
long offset = 0;
|
||||
try
|
||||
{
|
||||
byte[] fileData = blockStream.ReadBytes((int)file.FileSize);
|
||||
// Loop through the folders
|
||||
bool allExtracted = true;
|
||||
while (true)
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Ensure directory separators are consistent
|
||||
string filename = file.Name;
|
||||
if (Path.DirectorySeparatorChar == '\\')
|
||||
filename = filename.Replace('/', '\\');
|
||||
else if (Path.DirectorySeparatorChar == '/')
|
||||
filename = filename.Replace('\\', '/');
|
||||
var folder = cabinet.Folders[f];
|
||||
CFFILE[] files = cabinet.GetSpannedFilesArray(currentCabFilename, f, includeDebug);
|
||||
var file = files[0];
|
||||
int bytesLeft = (int)file.FileSize;
|
||||
int fileCounter = 0;
|
||||
|
||||
// Cache starting position
|
||||
offset = folder.CabStartOffset;
|
||||
|
||||
var mszip = Decompressor.Create();
|
||||
try
|
||||
{
|
||||
// Ensure folder contains data
|
||||
if (folder.DataCount == 0)
|
||||
return false;
|
||||
|
||||
// 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);
|
||||
// Skip unsupported compression types to avoid opening a blank filestream. This can be altered/removed if these types are ever supported.
|
||||
var compressionType = GetCompressionType(folder);
|
||||
if (compressionType == CompressionType.TYPE_QUANTUM || compressionType == CompressionType.TYPE_LZX)
|
||||
continue;
|
||||
|
||||
var fs = GetFileStream(file.Name, outputDirectory);
|
||||
|
||||
// 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();
|
||||
//uint quantumWindowBits = (uint)(((ushort)folder.CompressionType >> 8) & 0x1f);
|
||||
|
||||
if (folder.CabStartOffset <= 0)
|
||||
return false;
|
||||
|
||||
var tempCabinet = cabinet;
|
||||
int j = 0;
|
||||
|
||||
// Loop through the data blocks
|
||||
// Has to be a while loop instead of a for loop due to cab spanning continue blocks
|
||||
while (j < folder.DataCount)
|
||||
{
|
||||
var dataBlock = tempCabinet.ReadBlock(ref offset, includeDebug);
|
||||
if (dataBlock == null)
|
||||
{
|
||||
if (includeDebug) Console.Error.WriteLine($"Error extracting file {file.Name}");
|
||||
break;
|
||||
}
|
||||
|
||||
// Get the data to be processed
|
||||
byte[] blockData = dataBlock.CompressedData;
|
||||
|
||||
// If the block is continued, append
|
||||
bool continuedBlock = false;
|
||||
if (dataBlock.UncompressedSize == 0)
|
||||
{
|
||||
tempCabinet = tempCabinet.Next;
|
||||
if (tempCabinet == null)
|
||||
break; // Next cab is missing, continue
|
||||
|
||||
// CompressionType not updated because there's no way it's possible that it can swap on continued blocks
|
||||
folder = tempCabinet.Folders[0];
|
||||
offset = folder.CabStartOffset;
|
||||
var nextBlock = tempCabinet.ReadBlock(ref offset, includeDebug);
|
||||
if (nextBlock == null)
|
||||
{
|
||||
if (includeDebug) Console.Error.WriteLine($"Error extracting file {file.Name}");
|
||||
break;
|
||||
}
|
||||
|
||||
byte[] nextData = nextBlock.CompressedData;
|
||||
if (nextData.Length == 0)
|
||||
continue;
|
||||
|
||||
continuedBlock = true;
|
||||
blockData = [.. blockData, .. nextData];
|
||||
dataBlock.CompressedSize += nextBlock.CompressedSize;
|
||||
dataBlock.UncompressedSize = nextBlock.UncompressedSize;
|
||||
}
|
||||
|
||||
// Get the uncompressed data block
|
||||
byte[] data = compressionType switch
|
||||
{
|
||||
CompressionType.TYPE_NONE => blockData,
|
||||
CompressionType.TYPE_MSZIP => DecompressMSZIPBlock(f, mszip, j, dataBlock, blockData, includeDebug),
|
||||
|
||||
// TODO: Unsupported
|
||||
CompressionType.TYPE_QUANTUM => [],
|
||||
CompressionType.TYPE_LZX => [],
|
||||
|
||||
// Should be impossible
|
||||
_ => [],
|
||||
};
|
||||
|
||||
if (bytesLeft > 0 && bytesLeft >= data.Length)
|
||||
{
|
||||
fs.Write(data);
|
||||
bytesLeft -= data.Length;
|
||||
}
|
||||
else if (bytesLeft > 0 && bytesLeft < data.Length)
|
||||
{
|
||||
int tempBytesLeft = bytesLeft;
|
||||
fs.Write(data, 0, bytesLeft);
|
||||
fs.Close();
|
||||
|
||||
// reached end of folder
|
||||
if (fileCounter + 1 == files.Length)
|
||||
break;
|
||||
|
||||
file = files[++fileCounter];
|
||||
bytesLeft = (int)file.FileSize;
|
||||
fs = GetFileStream(file.Name, outputDirectory);
|
||||
while (bytesLeft < data.Length - tempBytesLeft)
|
||||
{
|
||||
fs.Write(data, tempBytesLeft, bytesLeft);
|
||||
tempBytesLeft += bytesLeft;
|
||||
fs.Close();
|
||||
|
||||
// reached end of folder
|
||||
if (fileCounter + 1 == files.Length)
|
||||
break;
|
||||
|
||||
file = files[++fileCounter];
|
||||
bytesLeft = (int)file.FileSize;
|
||||
fs = GetFileStream(file.Name, outputDirectory);
|
||||
}
|
||||
|
||||
fs.Write(data, tempBytesLeft, data.Length - tempBytesLeft);
|
||||
bytesLeft -= (data.Length - tempBytesLeft);
|
||||
}
|
||||
else
|
||||
{
|
||||
int tempBytesLeft = bytesLeft;
|
||||
fs.Close();
|
||||
|
||||
// reached end of folder
|
||||
if (fileCounter + 1 == files.Length)
|
||||
break;
|
||||
|
||||
file = files[++fileCounter];
|
||||
bytesLeft = (int)file.FileSize;
|
||||
fs = GetFileStream(file.Name, outputDirectory);
|
||||
while (bytesLeft < data.Length - tempBytesLeft)
|
||||
{
|
||||
fs.Write(data, tempBytesLeft, bytesLeft);
|
||||
tempBytesLeft += bytesLeft;
|
||||
fs.Close();
|
||||
|
||||
// reached end of folder
|
||||
if (fileCounter + 1 == files.Length)
|
||||
break;
|
||||
|
||||
file = files[++fileCounter];
|
||||
bytesLeft = (int)file.FileSize;
|
||||
fs = GetFileStream(file.Name, outputDirectory);
|
||||
}
|
||||
|
||||
fs.Write(data, tempBytesLeft, data.Length - tempBytesLeft);
|
||||
bytesLeft -= (data.Length - tempBytesLeft);
|
||||
}
|
||||
|
||||
// Top if block occurs on http://redump.org/disc/107833/ , middle on https://dbox.tools/titles/pc/57520FA0 , bottom still unobserved
|
||||
// While loop since this also handles 0 byte files. Example file seen in http://redump.org/disc/93312/ , cab Group17.cab, file TRACKSLOC6DYNTEX_BIN
|
||||
while (bytesLeft == 0)
|
||||
{
|
||||
fs.Close();
|
||||
|
||||
// reached end of folder
|
||||
if (fileCounter + 1 == files.Length)
|
||||
break;
|
||||
|
||||
file = files[++fileCounter];
|
||||
bytesLeft = (int)file.FileSize;
|
||||
fs = GetFileStream(file.Name, outputDirectory);
|
||||
}
|
||||
|
||||
if (continuedBlock)
|
||||
j = 0;
|
||||
|
||||
j++;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (includeDebug) Console.Error.WriteLine(ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Move to the next cabinet, if possible
|
||||
cabinet = cabinet.Next;
|
||||
if (cabinet == null) // If the next cabinet is missing, there's no better way to handle this
|
||||
return false;
|
||||
|
||||
currentCabFilename = cabinet.Filename;
|
||||
|
||||
if (cabinet.Folders.Length == 0)
|
||||
break;
|
||||
}
|
||||
|
||||
return allExtracted;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (includeDebug) Console.Error.WriteLine(ex);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extract the contents of a single file
|
||||
/// </summary>
|
||||
/// <param name="outputDirectory">Path to the output directory</param>
|
||||
/// <param name="blockStream">Stream representing the uncompressed block data</param>
|
||||
/// <param name="file">File information</param>
|
||||
/// <param name="includeDebug">True to include debug data, false otherwise</param>
|
||||
/// <returns>True if the file extracted, false otherwise</returns>
|
||||
private static bool ExtractFile(string outputDirectory, Stream blockStream, CFFILE file, bool includeDebug)
|
||||
{
|
||||
try
|
||||
{
|
||||
blockStream.SeekIfPossible(file.FolderStartOffset, SeekOrigin.Begin);
|
||||
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;
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -380,4 +567,4 @@ namespace SabreTools.Serialization.Wrappers
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -173,77 +173,6 @@ namespace SabreTools.Serialization.Wrappers
|
||||
|
||||
#region Folders
|
||||
|
||||
/// <summary>
|
||||
/// Decompress all blocks for a folder
|
||||
/// </summary>
|
||||
/// <param name="filename">Filename for one cabinet in the set, if available</param>
|
||||
/// <param name="folder">Folder containing the blocks to decompress</param>
|
||||
/// <param name="folderIndex">Index of the folder in the cabinet</param>
|
||||
/// <param name="includeDebug">True to include debug data, false otherwise</param>
|
||||
/// <returns>Stream representing the decompressed data on success, null otherwise</returns>
|
||||
public Stream? DecompressBlocks(string? filename, CFFOLDER? folder, int folderIndex, bool includeDebug)
|
||||
{
|
||||
// Ensure data blocks
|
||||
var dataBlocks = GetDataBlocks(filename, folder, folderIndex);
|
||||
if (dataBlocks == null || dataBlocks.Length == 0)
|
||||
return null;
|
||||
|
||||
// Get the compression type
|
||||
var compressionType = GetCompressionType(folder!);
|
||||
|
||||
// Setup decompressors
|
||||
var mszip = Decompressor.Create();
|
||||
//uint quantumWindowBits = (uint)(((ushort)folder.CompressionType >> 8) & 0x1f);
|
||||
|
||||
// Loop through the data blocks
|
||||
var ms = new MemoryStream();
|
||||
for (int i = 0; i < dataBlocks.Length; i++)
|
||||
{
|
||||
var db = dataBlocks[i];
|
||||
|
||||
// Get the data to be processed
|
||||
byte[] blockData = db.CompressedData;
|
||||
|
||||
// If the block is continued, append
|
||||
bool continuedBlock = false;
|
||||
if (db.UncompressedSize == 0)
|
||||
{
|
||||
var nextBlock = dataBlocks[i + 1];
|
||||
byte[]? nextData = nextBlock.CompressedData;
|
||||
if (nextData == null)
|
||||
continue;
|
||||
|
||||
continuedBlock = true;
|
||||
blockData = [.. blockData, .. nextData];
|
||||
db.CompressedSize += nextBlock.CompressedSize;
|
||||
db.UncompressedSize = nextBlock.UncompressedSize;
|
||||
}
|
||||
|
||||
// Get the uncompressed data block
|
||||
byte[] data = compressionType switch
|
||||
{
|
||||
CompressionType.TYPE_NONE => blockData,
|
||||
CompressionType.TYPE_MSZIP => DecompressMSZIPBlock(folderIndex, mszip, i, db, blockData, includeDebug),
|
||||
|
||||
// TODO: Unsupported
|
||||
CompressionType.TYPE_QUANTUM => [],
|
||||
CompressionType.TYPE_LZX => [],
|
||||
|
||||
// Should be impossible
|
||||
_ => [],
|
||||
};
|
||||
|
||||
// Write the uncompressed data block
|
||||
ms.Write(data, 0, data.Length);
|
||||
ms.Flush();
|
||||
|
||||
// Increment additionally if we had a continued block
|
||||
if (continuedBlock) i++;
|
||||
}
|
||||
|
||||
return ms;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decompress an MS-ZIP block using an existing decompressor
|
||||
/// </summary>
|
||||
@@ -301,104 +230,20 @@ namespace SabreTools.Serialization.Wrappers
|
||||
return (CompressionType)ushort.MaxValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the set of data blocks for a folder
|
||||
/// </summary>
|
||||
/// <param name="filename">Filename for one cabinet in the set, if available</param>
|
||||
/// <param name="folder">Folder containing the blocks to decompress</param>
|
||||
/// <param name="folderIndex">Index of the folder in the cabinet</param>
|
||||
/// <param name="skipPrev">Indicates if previous cabinets should be ignored</param>
|
||||
/// <param name="skipNext">Indicates if next cabinets should be ignored</param>
|
||||
/// <returns>Array of data blocks on success, null otherwise</returns>
|
||||
private CFDATA[]? GetDataBlocks(string? filename, CFFOLDER? folder, int folderIndex, bool skipPrev = false, bool skipNext = false)
|
||||
{
|
||||
// Skip invalid folders
|
||||
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)
|
||||
return folder.DataBlocks;
|
||||
|
||||
// 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
|
||||
CFDATA[] prevBlocks = [];
|
||||
if (!skipPrev && spanPrev)
|
||||
{
|
||||
// Try to get Prev if it doesn't exist
|
||||
if (Prev?.Header == null)
|
||||
Prev = OpenPrevious(filename);
|
||||
|
||||
// Get all blocks from Prev
|
||||
if (Prev?.Header != null && Prev.Folders != null)
|
||||
{
|
||||
int prevFolderIndex = Prev.FolderCount - 1;
|
||||
var prevFolder = Prev.Folders[prevFolderIndex - 1];
|
||||
prevBlocks = Prev.GetDataBlocks(filename, prevFolder, prevFolderIndex, skipNext: true) ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
// If the folder spans forward and Next is not being skipped
|
||||
CFDATA[] nextBlocks = [];
|
||||
if (!skipNext && spanNext)
|
||||
{
|
||||
// Try to get Next if it doesn't exist
|
||||
if (Next?.Header == null)
|
||||
Next = OpenNext(filename);
|
||||
|
||||
// Get all blocks from Prev
|
||||
if (Next?.Header != null && Next.Folders != null)
|
||||
{
|
||||
var nextFolder = Next.Folders[0];
|
||||
nextBlocks = Next.GetDataBlocks(filename, nextFolder, 0, skipPrev: true) ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
// Return all found blocks in order
|
||||
return [.. prevBlocks, .. folder.DataBlocks, .. nextBlocks];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads in all the datablocks for the current folder.
|
||||
/// </summary>
|
||||
/// <param name="folder">The folder to have the datablocks loaded for</param>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all files for the current folder, plus connected spanned folders.
|
||||
/// </summary>
|
||||
/// <param name="filename">Input filename of the cabinet to read from</param>
|
||||
/// <param name="folderIndex">Index of the folder in the cabinet</param>
|
||||
/// <param name="ignorePrev">True to ignore previous links, false otherwise</param>
|
||||
/// <param name="includeDebug">True to include debug data, false otherwise</param>
|
||||
/// <param name="skipPrev">True if previous cabinets should be skipped, false otherwise.</param>
|
||||
/// <param name="skipNext">True if next cabinets should be skipped, false otherwise.</param>
|
||||
/// <returns>Array of all files for the folder</returns>
|
||||
private CFFILE[] GetSpannedFiles(string? filename, int folderIndex, bool ignorePrev = false, bool skipPrev = false, bool skipNext = false)
|
||||
private CFFILE[] GetSpannedFiles(string? filename,
|
||||
int folderIndex,
|
||||
bool includeDebug,
|
||||
bool skipPrev = false,
|
||||
bool skipNext = false)
|
||||
{
|
||||
// Ignore invalid archives
|
||||
if (Files.IsNullOrEmpty())
|
||||
@@ -410,14 +255,12 @@ namespace SabreTools.Serialization.Wrappers
|
||||
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;
|
||||
}
|
||||
// Ignore links to previous cabinets
|
||||
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;
|
||||
@@ -433,13 +276,13 @@ namespace SabreTools.Serialization.Wrappers
|
||||
{
|
||||
// Try to get Prev if it doesn't exist
|
||||
if (Prev?.Header == null)
|
||||
Prev = OpenPrevious(filename);
|
||||
Prev = OpenPrevious(filename, includeDebug);
|
||||
|
||||
// Get all files from Prev
|
||||
if (Prev?.Header != null && Prev.Folders != null)
|
||||
{
|
||||
int prevFolderIndex = Prev.FolderCount - 1;
|
||||
prevFiles = Prev.GetSpannedFiles(filename, prevFolderIndex, skipNext: true) ?? [];
|
||||
prevFiles = Prev.GetSpannedFiles(filename, prevFolderIndex, includeDebug, skipNext: true) ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -449,13 +292,13 @@ namespace SabreTools.Serialization.Wrappers
|
||||
{
|
||||
// Try to get Next if it doesn't exist
|
||||
if (Next?.Header == null)
|
||||
Next = OpenNext(filename);
|
||||
|
||||
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) ?? [];
|
||||
nextFiles = Next.GetSpannedFiles(filename, 0, includeDebug, skipPrev: true) ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user