Make partial classes for extraction

This commit is contained in:
Matt Nadareski
2025-09-12 09:02:03 -04:00
parent e9b1b2750f
commit 588ee5bfe4
58 changed files with 4876 additions and 4744 deletions

View File

@@ -0,0 +1,111 @@
using System;
using System.IO;
using SabreTools.IO.Compression.Deflate;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public partial class BFPK : IExtractable
{
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// If we have no files
if (Files == null || Files.Length == 0)
return false;
// Loop through and extract all files to the output
bool allExtracted = true;
for (int i = 0; i < Files.Length; i++)
{
allExtracted &= ExtractFile(i, outputDirectory, includeDebug);
}
return allExtracted;
}
/// <summary>
/// Extract a file from the BFPK to an output directory by index
/// </summary>
/// <param name="index">File index to extract</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the file extracted, false otherwise</returns>
public bool ExtractFile(int index, string outputDirectory, bool includeDebug)
{
// If we have no files
if (Files == null || Files.Length == 0)
return false;
// If we have an invalid index
if (index < 0 || index >= Files.Length)
return false;
// Get the file information
var file = Files[index];
if (file == null)
return false;
// Get the read index and length
int offset = file.Offset + 4;
int compressedSize = file.CompressedSize;
// Some files can lack the length prefix
if (compressedSize > Length)
{
offset -= 4;
compressedSize = file.UncompressedSize;
}
// If we have an invalid output directory
if (string.IsNullOrEmpty(outputDirectory))
return false;
// Ensure directory separators are consistent
string filename = file.Name ?? $"file{index}";
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);
// Try to write the data
try
{
// Open the output file for writing
using FileStream fs = File.OpenWrite(filename);
// Read the data block
var data = ReadRangeFromSource(offset, compressedSize);
if (data == null)
return false;
// If we have uncompressed data
if (compressedSize == file.UncompressedSize)
{
fs.Write(data, 0, compressedSize);
fs.Flush();
}
else
{
using MemoryStream ms = new MemoryStream(data);
using ZlibStream zs = new ZlibStream(ms, CompressionMode.Decompress);
zs.CopyTo(fs);
fs.Flush();
}
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
return true;
}
}
}

View File

@@ -1,13 +1,9 @@
using System;
using System.IO;
using SabreTools.IO.Compression.Deflate;
using SabreTools.IO.Extensions;
using System.IO;
using SabreTools.Models.BFPK;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public class BFPK : WrapperBase<Archive>, IExtractable
public partial class BFPK : WrapperBase<Archive>
{
#region Descriptive Properties
@@ -90,110 +86,5 @@ namespace SabreTools.Serialization.Wrappers
}
#endregion
#region Extraction
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// If we have no files
if (Files == null || Files.Length == 0)
return false;
// Loop through and extract all files to the output
bool allExtracted = true;
for (int i = 0; i < Files.Length; i++)
{
allExtracted &= ExtractFile(i, outputDirectory, includeDebug);
}
return allExtracted;
}
/// <summary>
/// Extract a file from the BFPK to an output directory by index
/// </summary>
/// <param name="index">File index to extract</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the file extracted, false otherwise</returns>
public bool ExtractFile(int index, string outputDirectory, bool includeDebug)
{
// If we have no files
if (Files == null || Files.Length == 0)
return false;
// If we have an invalid index
if (index < 0 || index >= Files.Length)
return false;
// Get the file information
var file = Files[index];
if (file == null)
return false;
// Get the read index and length
int offset = file.Offset + 4;
int compressedSize = file.CompressedSize;
// Some files can lack the length prefix
if (compressedSize > Length)
{
offset -= 4;
compressedSize = file.UncompressedSize;
}
// If we have an invalid output directory
if (string.IsNullOrEmpty(outputDirectory))
return false;
// Ensure directory separators are consistent
string filename = file.Name ?? $"file{index}";
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);
// Try to write the data
try
{
// Open the output file for writing
using FileStream fs = File.OpenWrite(filename);
// Read the data block
var data = ReadRangeFromSource(offset, compressedSize);
if (data == null)
return false;
// If we have uncompressed data
if (compressedSize == file.UncompressedSize)
{
fs.Write(data, 0, compressedSize);
fs.Flush();
}
else
{
using MemoryStream ms = new MemoryStream(data);
using ZlibStream zs = new ZlibStream(ms, CompressionMode.Decompress);
zs.CopyTo(fs);
fs.Flush();
}
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
return true;
}
#endregion
}
}

View File

@@ -0,0 +1,95 @@
using System;
using System.IO;
using SabreTools.Models.BSP;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public partial class BSP : IExtractable
{
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// If we have no lumps
if (Lumps == null || Lumps.Length == 0)
return false;
// Loop through and extract all lumps to the output
bool allExtracted = true;
for (int i = 0; i < Lumps.Length; i++)
{
allExtracted &= ExtractLump(i, outputDirectory, includeDebug);
}
return allExtracted;
}
/// <summary>
/// Extract a lump from the BSP to an output directory by index
/// </summary>
/// <param name="index">Lump index to extract</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the lump extracted, false otherwise</returns>
public bool ExtractLump(int index, string outputDirectory, bool includeDebug)
{
// If we have no lumps
if (Lumps == null || Lumps.Length == 0)
return false;
// If the lumps index is invalid
if (index < 0 || index >= Lumps.Length)
return false;
// Read the data
var lump = Lumps[index];
var data = ReadRangeFromSource(lump.Offset, lump.Length);
if (data == null)
return false;
// Create the filename
string filename = $"lump_{index}.bin";
switch ((LumpType)index)
{
case LumpType.LUMP_ENTITIES:
filename = "entities.ent";
break;
case LumpType.LUMP_TEXTURES:
filename = "texture_data.bin";
break;
}
// If we have an invalid output directory
if (string.IsNullOrEmpty(outputDirectory))
return false;
// 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);
// Try to write the data
try
{
// Open the output file for writing
using Stream fs = File.OpenWrite(filename);
fs.Write(data, 0, data.Length);
fs.Flush();
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
return true;
}
}
}

View File

@@ -1,12 +1,9 @@
using System;
using System.IO;
using SabreTools.IO.Extensions;
using SabreTools.Models.BSP;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public class BSP : WrapperBase<BspFile>, IExtractable
public partial class BSP : WrapperBase<BspFile>
{
#region Descriptive Properties
@@ -89,94 +86,5 @@ namespace SabreTools.Serialization.Wrappers
}
#endregion
#region Extraction
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// If we have no lumps
if (Lumps == null || Lumps.Length == 0)
return false;
// Loop through and extract all lumps to the output
bool allExtracted = true;
for (int i = 0; i < Lumps.Length; i++)
{
allExtracted &= ExtractLump(i, outputDirectory, includeDebug);
}
return allExtracted;
}
/// <summary>
/// Extract a lump from the BSP to an output directory by index
/// </summary>
/// <param name="index">Lump index to extract</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the lump extracted, false otherwise</returns>
public bool ExtractLump(int index, string outputDirectory, bool includeDebug)
{
// If we have no lumps
if (Lumps == null || Lumps.Length == 0)
return false;
// If the lumps index is invalid
if (index < 0 || index >= Lumps.Length)
return false;
// Read the data
var lump = Lumps[index];
var data = ReadRangeFromSource(lump.Offset, lump.Length);
if (data == null)
return false;
// Create the filename
string filename = $"lump_{index}.bin";
switch ((LumpType)index)
{
case LumpType.LUMP_ENTITIES:
filename = "entities.ent";
break;
case LumpType.LUMP_TEXTURES:
filename = "texture_data.bin";
break;
}
// If we have an invalid output directory
if (string.IsNullOrEmpty(outputDirectory))
return false;
// 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);
// Try to write the data
try
{
// Open the output file for writing
using Stream fs = File.OpenWrite(filename);
fs.Write(data, 0, data.Length);
fs.Flush();
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
return true;
}
#endregion
}
}

View File

@@ -0,0 +1,53 @@
using System;
using System.IO;
using SabreTools.IO.Compression.BZip2;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
/// <summary>
/// This is a shell wrapper; one that does not contain
/// any actual parsing. It is used as a placeholder for
/// types that typically do not have models.
/// </summary>
public partial class BZip2 : IExtractable
{
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
if (_dataSource == null || !_dataSource.CanRead)
return false;
try
{
// Try opening the stream
using var bz2File = new BZip2InputStream(_dataSource, true);
// Ensure directory separators are consistent
string filename = Guid.NewGuid().ToString();
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);
// Extract the file
using FileStream fs = File.OpenWrite(filename);
bz2File.CopyTo(fs);
fs.Flush();
return true;
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
}
}
}

View File

@@ -1,7 +1,4 @@
using System;
using System.IO;
using SabreTools.IO.Compression.BZip2;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
@@ -10,7 +7,7 @@ namespace SabreTools.Serialization.Wrappers
/// any actual parsing. It is used as a placeholder for
/// types that typically do not have models.
/// </summary>
public class BZip2 : WrapperBase, IExtractable
public partial class BZip2 : WrapperBase
{
#region Descriptive Properties
@@ -76,52 +73,10 @@ namespace SabreTools.Serialization.Wrappers
#if NETCOREAPP
/// <inheritdoc/>
public override string ExportJSON() => throw new NotImplementedException();
public override string ExportJSON() => throw new System.NotImplementedException();
#endif
#endregion
#region Extraction
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
if (_dataSource == null || !_dataSource.CanRead)
return false;
try
{
// Try opening the stream
using var bz2File = new BZip2InputStream(_dataSource, true);
// Ensure directory separators are consistent
string filename = Guid.NewGuid().ToString();
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);
// Extract the file
using FileStream fs = File.OpenWrite(filename);
bz2File.CopyTo(fs);
fs.Flush();
return true;
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
}
#endregion
}
}

View File

@@ -0,0 +1,135 @@
using System;
using System.IO;
using System.Text;
using SabreTools.IO.Extensions;
using SabreTools.Models.CFB;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public partial class CFB : IExtractable
{
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// If we have no files
if (DirectoryEntries == null || DirectoryEntries.Length == 0)
return false;
// Loop through and extract all directory entries to the output
bool allExtracted = true;
for (int i = 0; i < DirectoryEntries.Length; i++)
{
allExtracted &= ExtractEntry(i, outputDirectory, includeDebug);
}
return allExtracted;
}
/// <summary>
/// Extract a file from the CFB to an output directory by index
/// </summary>
/// <param name="index">Entry index to extract</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the file extracted, false otherwise</returns>
public bool ExtractEntry(int index, string outputDirectory, bool includeDebug)
{
// If we have no entries
if (DirectoryEntries == null || DirectoryEntries.Length == 0)
return false;
// If we have an invalid index
if (index < 0 || index >= DirectoryEntries.Length)
return false;
// Get the entry information
var entry = DirectoryEntries[index];
if (entry == null)
return false;
// Only try to extract stream objects
if (entry.ObjectType != ObjectType.StreamObject)
return true;
// Get the entry data
byte[]? data = GetDirectoryEntryData(entry);
if (data == null)
return false;
// If we have an invalid output directory
if (string.IsNullOrEmpty(outputDirectory))
return false;
// Ensure the output filename is trimmed
string filename = entry.Name ?? $"entry{index}";
byte[] nameBytes = Encoding.UTF8.GetBytes(filename);
if (nameBytes[0] == 0xe4 && nameBytes[1] == 0xa1 && nameBytes[2] == 0x80)
filename = Encoding.UTF8.GetString(nameBytes, 3, nameBytes.Length - 3);
foreach (char c in Path.GetInvalidFileNameChars())
{
filename = filename.Replace(c, '_');
}
// 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);
// Try to write the data
try
{
// Open the output file for writing
using FileStream fs = File.OpenWrite(filename);
fs.Write(data);
fs.Flush();
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
return true;
}
/// <summary>
/// Read the entry data for a single directory entry, if possible
/// </summary>
/// <param name="entry">Entry to try to retrieve data for</param>
/// <returns>Byte array representing the entry data on success, null otherwise</returns>
private byte[]? GetDirectoryEntryData(DirectoryEntry entry)
{
// If the CFB is invalid
if (Header == null)
return null;
// Only try to extract stream objects
if (entry.ObjectType != ObjectType.StreamObject)
return null;
// Determine which FAT is being used
bool miniFat = entry.StreamSize < Header.MiniStreamCutoffSize;
// Get the chain data
var chain = miniFat
? GetMiniFATSectorChainData((SectorNumber)entry.StartingSectorLocation)
: GetFATSectorChainData((SectorNumber)entry.StartingSectorLocation);
if (chain == null)
return null;
// Return only the proper amount of data
byte[] data = new byte[entry.StreamSize];
Array.Copy(chain, 0, data, 0, (int)Math.Min(chain.Length, (long)entry.StreamSize));
return data;
}
}
}

View File

@@ -1,14 +1,12 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using SabreTools.IO.Extensions;
using SabreTools.Models.CFB;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public class CFB : WrapperBase<Binary>, IExtractable
public partial class CFB : WrapperBase<Binary>
{
#region Descriptive Properties
@@ -136,133 +134,6 @@ namespace SabreTools.Serialization.Wrappers
#endregion
#region Extraction
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// If we have no files
if (DirectoryEntries == null || DirectoryEntries.Length == 0)
return false;
// Loop through and extract all directory entries to the output
bool allExtracted = true;
for (int i = 0; i < DirectoryEntries.Length; i++)
{
allExtracted &= ExtractEntry(i, outputDirectory, includeDebug);
}
return allExtracted;
}
/// <summary>
/// Extract a file from the CFB to an output directory by index
/// </summary>
/// <param name="index">Entry index to extract</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the file extracted, false otherwise</returns>
public bool ExtractEntry(int index, string outputDirectory, bool includeDebug)
{
// If we have no entries
if (DirectoryEntries == null || DirectoryEntries.Length == 0)
return false;
// If we have an invalid index
if (index < 0 || index >= DirectoryEntries.Length)
return false;
// Get the entry information
var entry = DirectoryEntries[index];
if (entry == null)
return false;
// Only try to extract stream objects
if (entry.ObjectType != ObjectType.StreamObject)
return true;
// Get the entry data
byte[]? data = GetDirectoryEntryData(entry);
if (data == null)
return false;
// If we have an invalid output directory
if (string.IsNullOrEmpty(outputDirectory))
return false;
// Ensure the output filename is trimmed
string filename = entry.Name ?? $"entry{index}";
byte[] nameBytes = Encoding.UTF8.GetBytes(filename);
if (nameBytes[0] == 0xe4 && nameBytes[1] == 0xa1 && nameBytes[2] == 0x80)
filename = Encoding.UTF8.GetString(nameBytes, 3, nameBytes.Length - 3);
foreach (char c in Path.GetInvalidFileNameChars())
{
filename = filename.Replace(c, '_');
}
// 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);
// Try to write the data
try
{
// Open the output file for writing
using FileStream fs = File.OpenWrite(filename);
fs.Write(data);
fs.Flush();
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
return true;
}
/// <summary>
/// Read the entry data for a single directory entry, if possible
/// </summary>
/// <param name="entry">Entry to try to retrieve data for</param>
/// <returns>Byte array representing the entry data on success, null otherwise</returns>
private byte[]? GetDirectoryEntryData(DirectoryEntry entry)
{
// If the CFB is invalid
if (Header == null)
return null;
// Only try to extract stream objects
if (entry.ObjectType != ObjectType.StreamObject)
return null;
// Determine which FAT is being used
bool miniFat = entry.StreamSize < Header.MiniStreamCutoffSize;
// Get the chain data
var chain = miniFat
? GetMiniFATSectorChainData((SectorNumber)entry.StartingSectorLocation)
: GetFATSectorChainData((SectorNumber)entry.StartingSectorLocation);
if (chain == null)
return null;
// Return only the proper amount of data
byte[] data = new byte[entry.StreamSize];
Array.Copy(chain, 0, data, 0, (int)Math.Min(chain.Length, (long)entry.StreamSize));
return data;
}
#endregion
#region FAT Sector Data
/// <summary>

View File

@@ -0,0 +1,114 @@
using System;
using System.Collections.Generic;
using System.IO;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public partial class GCF : IExtractable
{
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// If we have no files
if (Files == null || Files.Length == 0)
return false;
// Loop through and extract all files to the output
bool allExtracted = true;
for (int i = 0; i < Files.Length; i++)
{
allExtracted &= ExtractFile(i, outputDirectory, includeDebug);
}
return allExtracted;
}
/// <summary>
/// Extract a file from the GCF to an output directory by index
/// </summary>
/// <param name="index">File index to extract</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the file extracted, false otherwise</returns>
public bool ExtractFile(int index, string outputDirectory, bool includeDebug)
{
// If we have no files
if (Files == null || Files.Length == 0 || DataBlockOffsets == null)
return false;
// If the files index is invalid
if (index < 0 || index >= Files.Length)
return false;
// Get the file
var file = Files[index];
if (file?.BlockEntries == null || file.Size == 0)
return false;
// If the file is encrypted -- TODO: Revisit later
if (file.Encrypted)
return false;
// Get all data block offsets needed for extraction
var dataBlockOffsets = new List<long>();
for (int i = 0; i < file.BlockEntries.Length; i++)
{
var blockEntry = file.BlockEntries[i];
uint dataBlockIndex = blockEntry.FirstDataBlockIndex;
long blockEntrySize = blockEntry.FileDataSize;
while (blockEntrySize > 0)
{
long dataBlockOffset = DataBlockOffsets[dataBlockIndex++];
dataBlockOffsets.Add(dataBlockOffset);
blockEntrySize -= BlockSize;
}
}
// If we have an invalid output directory
if (string.IsNullOrEmpty(outputDirectory))
return false;
// Ensure directory separators are consistent
string filename = file.Path ?? $"file{index}";
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);
// Try to write the data
try
{
// Open the output file for writing
using Stream fs = File.OpenWrite(filename);
// Now read the data sequentially and write out while we have data left
long fileSize = file.Size;
for (int i = 0; i < dataBlockOffsets.Count; i++)
{
int readSize = (int)Math.Min(BlockSize, fileSize);
var data = ReadRangeFromSource((int)dataBlockOffsets[i], readSize);
if (data == null)
return false;
fs.Write(data, 0, data.Length);
fs.Flush();
}
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
return true;
}
}
}

View File

@@ -1,12 +1,9 @@
using System;
using System.Collections.Generic;
using System.IO;
using SabreTools.IO.Extensions;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public class GCF : WrapperBase<Models.GCF.File>, IExtractable
public partial class GCF : WrapperBase<Models.GCF.File>
{
#region Descriptive Properties
@@ -232,114 +229,6 @@ namespace SabreTools.Serialization.Wrappers
#endregion
#region Extraction
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// If we have no files
if (Files == null || Files.Length == 0)
return false;
// Loop through and extract all files to the output
bool allExtracted = true;
for (int i = 0; i < Files.Length; i++)
{
allExtracted &= ExtractFile(i, outputDirectory, includeDebug);
}
return allExtracted;
}
/// <summary>
/// Extract a file from the GCF to an output directory by index
/// </summary>
/// <param name="index">File index to extract</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the file extracted, false otherwise</returns>
public bool ExtractFile(int index, string outputDirectory, bool includeDebug)
{
// If we have no files
if (Files == null || Files.Length == 0 || DataBlockOffsets == null)
return false;
// If the files index is invalid
if (index < 0 || index >= Files.Length)
return false;
// Get the file
var file = Files[index];
if (file?.BlockEntries == null || file.Size == 0)
return false;
// If the file is encrypted -- TODO: Revisit later
if (file.Encrypted)
return false;
// Get all data block offsets needed for extraction
var dataBlockOffsets = new List<long>();
for (int i = 0; i < file.BlockEntries.Length; i++)
{
var blockEntry = file.BlockEntries[i];
uint dataBlockIndex = blockEntry.FirstDataBlockIndex;
long blockEntrySize = blockEntry.FileDataSize;
while (blockEntrySize > 0)
{
long dataBlockOffset = DataBlockOffsets[dataBlockIndex++];
dataBlockOffsets.Add(dataBlockOffset);
blockEntrySize -= BlockSize;
}
}
// If we have an invalid output directory
if (string.IsNullOrEmpty(outputDirectory))
return false;
// Ensure directory separators are consistent
string filename = file.Path ?? $"file{index}";
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);
// Try to write the data
try
{
// Open the output file for writing
using Stream fs = File.OpenWrite(filename);
// Now read the data sequentially and write out while we have data left
long fileSize = file.Size;
for (int i = 0; i < dataBlockOffsets.Count; i++)
{
int readSize = (int)Math.Min(BlockSize, fileSize);
var data = ReadRangeFromSource((int)dataBlockOffsets[i], readSize);
if (data == null)
return false;
fs.Write(data, 0, data.Length);
fs.Flush();
}
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
return true;
}
#endregion
#region Helper Classes
/// <summary>

View File

@@ -0,0 +1,71 @@
using System;
using System.IO;
using SabreTools.IO.Compression.Deflate;
using SabreTools.Models.GZIP;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public partial class GZip : IExtractable
{
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// Ensure there is data to extract
if (Header == null || DataOffset < 0)
{
if (includeDebug) Console.Error.WriteLine("Invalid archive detected, skipping...");
return false;
}
// Ensure that DEFLATE is being used
if (Header.CompressionMethod != CompressionMethod.Deflate)
{
if (includeDebug) Console.Error.WriteLine($"Invalid compression method {Header.CompressionMethod} detected, only DEFLATE is supported. Skipping...");
return false;
}
try
{
// Seek to the start of the compressed data
long offset = _dataSource.Seek(DataOffset, SeekOrigin.Begin);
if (offset != DataOffset)
{
if (includeDebug) Console.Error.WriteLine($"Could not seek to compressed data at {DataOffset}");
return false;
}
// Ensure directory separators are consistent
string filename = Header.OriginalFileName
?? (Filename != null ? Path.GetFileName(Filename).Replace(".gz", string.Empty) : null)
?? $"extracted_file";
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 source as a DEFLATE stream
var deflateStream = new DeflateStream(_dataSource, CompressionMode.Decompress, leaveOpen: true);
// Write the file
using var fs = File.Open(filename, FileMode.Create, FileAccess.Write, FileShare.None);
deflateStream.CopyTo(fs);
fs.Flush();
return true;
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
}
}
}

View File

@@ -1,12 +1,9 @@
using System;
using System.IO;
using SabreTools.IO.Compression.Deflate;
using SabreTools.Models.GZIP;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public class GZip : WrapperBase<Archive>, IExtractable
public partial class GZip : WrapperBase<Archive>
{
#region Descriptive Properties
@@ -138,69 +135,5 @@ namespace SabreTools.Serialization.Wrappers
}
#endregion
#region Extraction
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// Ensure there is data to extract
if (Header == null || DataOffset < 0)
{
if (includeDebug) Console.Error.WriteLine("Invalid archive detected, skipping...");
return false;
}
// Ensure that DEFLATE is being used
if (Header.CompressionMethod != CompressionMethod.Deflate)
{
if (includeDebug) Console.Error.WriteLine($"Invalid compression method {Header.CompressionMethod} detected, only DEFLATE is supported. Skipping...");
return false;
}
try
{
// Seek to the start of the compressed data
long offset = _dataSource.Seek(DataOffset, SeekOrigin.Begin);
if (offset != DataOffset)
{
if (includeDebug) Console.Error.WriteLine($"Could not seek to compressed data at {DataOffset}");
return false;
}
// Ensure directory separators are consistent
string filename = Header.OriginalFileName
?? (Filename != null ? Path.GetFileName(Filename).Replace(".gz", string.Empty) : null)
?? $"extracted_file";
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 source as a DEFLATE stream
var deflateStream = new DeflateStream(_dataSource, CompressionMode.Decompress, leaveOpen: true);
// Write the file
using var fs = File.Open(filename, FileMode.Create, FileAccess.Write, FileShare.None);
deflateStream.CopyTo(fs);
fs.Flush();
return true;
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
}
#endregion
}
}

View File

@@ -0,0 +1,127 @@
using System;
using System.IO;
using SabreTools.IO.Compression.Blast;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
/// <remarks>
/// Reference (de)compressor: https://www.sac.sk/download/pack/icomp95.zip
/// </remarks>
/// <see href="https://github.com/wfr/unshieldv3"/>
public partial class InstallShieldArchiveV3 : IExtractable
{
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// Get the file count
int fileCount = Files.Length;
if (fileCount == 0)
return false;
// Loop through and extract all files to the output
bool allExtracted = true;
for (int i = 0; i < fileCount; i++)
{
allExtracted &= ExtractFile(i, outputDirectory, includeDebug);
}
return allExtracted;
}
/// <summary>
/// Extract a file from the ISAv3 to an output directory by index
/// </summary>
/// <param name="index">File index to extract</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the file extracted, false otherwise</returns>
public bool ExtractFile(int index, string outputDirectory, bool includeDebug)
{
// If the files index is invalid
if (index < 0 || index >= FileCount)
return false;
// Get the file
var file = Files[index];
if (file == null)
return false;
// Create the filename
var filename = file.Name;
if (filename == null)
return false;
// Get the directory index
int dirIndex = FileDirMap[index];
if (dirIndex < 0 || dirIndex > DirCount)
return false;
// Get the directory name
var dirName = Directories[dirIndex].Name;
if (dirName != null)
filename = Path.Combine(dirName, filename);
// Get and adjust the file offset
long fileOffset = file.Offset + DataStart;
if (fileOffset < 0 || fileOffset >= Length)
return false;
// Get the file sizes
long fileSize = file.CompressedSize;
long outputFileSize = file.UncompressedSize;
// Read the compressed data directly
var compressedData = ReadRangeFromSource((int)fileOffset, (int)fileSize);
if (compressedData == null)
return false;
// If the compressed and uncompressed sizes match
byte[] data;
if (fileSize == outputFileSize)
{
data = compressedData;
}
else
{
// Decompress the data
var decomp = Decompressor.Create();
using var outData = new MemoryStream();
decomp.CopyTo(compressedData, outData);
data = outData.ToArray();
}
// If we have an invalid output directory
if (string.IsNullOrEmpty(outputDirectory))
return false;
// 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 && !System.IO.Directory.Exists(directoryName))
System.IO.Directory.CreateDirectory(directoryName);
// Try to write the data
try
{
// Open the output file for writing
using Stream fs = System.IO.File.OpenWrite(filename);
fs.Write(data, 0, data.Length);
fs.Flush();
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
return false;
}
}
}

View File

@@ -1,10 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using SabreTools.IO.Compression.Blast;
using SabreTools.IO.Extensions;
using SabreTools.Models.InstallShieldArchiveV3;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
@@ -12,7 +8,7 @@ namespace SabreTools.Serialization.Wrappers
/// Reference (de)compressor: https://www.sac.sk/download/pack/icomp95.zip
/// </remarks>
/// <see href="https://github.com/wfr/unshieldv3"/>
public partial class InstallShieldArchiveV3 : WrapperBase<Archive>, IExtractable
public partial class InstallShieldArchiveV3 : WrapperBase<Archive>
{
#region Descriptive Properties
@@ -176,122 +172,5 @@ namespace SabreTools.Serialization.Wrappers
}
#endregion
#region Extraction
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// Get the file count
int fileCount = Files.Length;
if (fileCount == 0)
return false;
// Loop through and extract all files to the output
bool allExtracted = true;
for (int i = 0; i < fileCount; i++)
{
allExtracted &= ExtractFile(i, outputDirectory, includeDebug);
}
return allExtracted;
}
/// <summary>
/// Extract a file from the ISAv3 to an output directory by index
/// </summary>
/// <param name="index">File index to extract</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the file extracted, false otherwise</returns>
public bool ExtractFile(int index, string outputDirectory, bool includeDebug)
{
// If the files index is invalid
if (index < 0 || index >= FileCount)
return false;
// Get the file
var file = Files[index];
if (file == null)
return false;
// Create the filename
var filename = file.Name;
if (filename == null)
return false;
// Get the directory index
int dirIndex = FileDirMap[index];
if (dirIndex < 0 || dirIndex > DirCount)
return false;
// Get the directory name
var dirName = Directories[dirIndex].Name;
if (dirName != null)
filename = Path.Combine(dirName, filename);
// Get and adjust the file offset
long fileOffset = file.Offset + DataStart;
if (fileOffset < 0 || fileOffset >= Length)
return false;
// Get the file sizes
long fileSize = file.CompressedSize;
long outputFileSize = file.UncompressedSize;
// Read the compressed data directly
var compressedData = ReadRangeFromSource((int)fileOffset, (int)fileSize);
if (compressedData == null)
return false;
// If the compressed and uncompressed sizes match
byte[] data;
if (fileSize == outputFileSize)
{
data = compressedData;
}
else
{
// Decompress the data
var decomp = Decompressor.Create();
using var outData = new MemoryStream();
decomp.CopyTo(compressedData, outData);
data = outData.ToArray();
}
// If we have an invalid output directory
if (string.IsNullOrEmpty(outputDirectory))
return false;
// 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 && !System.IO.Directory.Exists(directoryName))
System.IO.Directory.CreateDirectory(directoryName);
// Try to write the data
try
{
// Open the output file for writing
using Stream fs = System.IO.File.OpenWrite(filename);
fs.Write(data, 0, data.Length);
fs.Flush();
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
return false;
}
#endregion
}
}

View File

@@ -0,0 +1,838 @@
using System;
using System.IO;
using System.Text.RegularExpressions;
using SabreTools.Hashing;
using SabreTools.IO.Compression.zlib;
using SabreTools.Models.InstallShieldCabinet;
using SabreTools.Serialization.Interfaces;
using static SabreTools.Models.InstallShieldCabinet.Constants;
namespace SabreTools.Serialization.Wrappers
{
public partial class InstallShieldCabinet : WrapperBase<Cabinet>, IExtractable
{
#region Extension Properties
/// <summary>
/// Reference to the next cabinet header
/// </summary>
/// <remarks>Only used in multi-file</remarks>
public InstallShieldCabinet? Next { get; set; }
/// <summary>
/// Reference to the next previous header
/// </summary>
/// <remarks>Only used in multi-file</remarks>
public InstallShieldCabinet? Prev { get; set; }
/// <summary>
/// Volume index ID, 0 for headers
/// </summary>
/// <remarks>Only used in multi-file</remarks>
public ushort VolumeID { get; set; }
#endregion
#region Extraction State
/// <summary>
/// Base filename path for related CAB files
/// </summary>
internal string? FilenamePattern { get; set; }
#endregion
#region Constants
/// <summary>
/// Default buffer size
/// </summary>
private const int BUFFER_SIZE = 64 * 1024;
/// <summary>
/// Maximum size of the window in bits
/// </summary>
private const int MAX_WBITS = 15;
#endregion
#region Cabinet Set
/// <summary>
/// Open a cabinet set for reading, if possible
/// </summary>
/// <param name="pattern">Filename pattern for matching cabinet files</param>
/// <returns>Wrapper representing the set, null on error</returns>
public static InstallShieldCabinet? OpenSet(string? pattern)
{
// An invalid pattern means no cabinet files
if (string.IsNullOrEmpty(pattern))
return null;
// Create a placeholder wrapper for output
InstallShieldCabinet? set = null;
// Loop until there are no parts left
bool iterate = true;
InstallShieldCabinet? previous = null;
for (ushort i = 1; iterate; i++)
{
var file = OpenFileForReading(pattern, i, HEADER_SUFFIX);
if (file != null)
iterate = false;
else
file = OpenFileForReading(pattern, i, CABINET_SUFFIX);
if (file == null)
break;
var current = Create(file);
if (current == null)
break;
current.VolumeID = i;
if (previous != null)
{
previous.Next = current;
current.Prev = previous;
}
else
{
set = current;
previous = current;
}
}
// Set the pattern, if possible
if (set != null)
set.FilenamePattern = pattern;
return set;
}
/// <summary>
/// Open the numbered cabinet set volume
/// </summary>
/// <param name="volumeId">Volume ID, 1-indexed</param>
/// <returns>Wrapper representing the volume on success, null otherwise</returns>
public InstallShieldCabinet? OpenVolume(ushort volumeId, out Stream? volumeStream)
{
// Normalize the volume ID for odd cases
if (volumeId == ushort.MinValue || volumeId == ushort.MaxValue)
volumeId = 1;
// Try to open the file as a stream
volumeStream = OpenFileForReading(FilenamePattern, volumeId, CABINET_SUFFIX);
if (volumeStream == null)
{
Console.Error.WriteLine($"Failed to open input cabinet file {volumeId}");
return null;
}
// Try to parse the stream into a cabinet
var volume = Create(volumeStream);
if (volume == null)
{
Console.Error.WriteLine($"Failed to open input cabinet file {volumeId}");
return null;
}
// Set the volume ID and return
volume.VolumeID = volumeId;
return volume;
}
/// <summary>
/// Open a cabinet file for reading
/// </summary>
/// <param name="index">Cabinet part index to be opened</param>
/// <param name="suffix">Cabinet files suffix (e.g. `.cab`)</param>
/// <returns>A Stream representing the cabinet part, null on error</returns>
public Stream? OpenFileForReading(int index, string suffix)
=> OpenFileForReading(FilenamePattern, index, suffix);
/// <summary>
/// Create the generic filename pattern to look for from the input filename
/// </summary>
/// <returns>String representing the filename pattern for a cabinet set, null on error</returns>
private static string? CreateFilenamePattern(string filename)
{
string? pattern = null;
if (string.IsNullOrEmpty(filename))
return pattern;
string? directory = Path.GetDirectoryName(Path.GetFullPath(filename));
if (directory != null)
pattern = Path.Combine(directory, Path.GetFileNameWithoutExtension(filename));
else
pattern = Path.GetFileNameWithoutExtension(filename);
return new Regex(@"\d+$").Replace(pattern, string.Empty);
}
/// <summary>
/// Open a cabinet file for reading
/// </summary>
/// <param name="pattern">Filename pattern for matching cabinet files</param>
/// <param name="index">Cabinet part index to be opened</param>
/// <param name="suffix">Cabinet files suffix (e.g. `.cab`)</param>
/// <returns>A Stream representing the cabinet part, null on error</returns>
private static Stream? OpenFileForReading(string? pattern, int index, string suffix)
{
// An invalid pattern means no cabinet files
if (string.IsNullOrEmpty(pattern))
return null;
// Attempt lower-case extension
string filename = $"{pattern}{index}.{suffix}";
if (File.Exists(filename))
return File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
// Attempt upper-case extension
filename = $"{pattern}{index}.{suffix.ToUpperInvariant()}";
if (File.Exists(filename))
return File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
return null;
}
#endregion
#region Extraction
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// Open the full set if possible
var cabinet = this;
if (Filename != null)
{
// Get the name of the first cabinet file or header
string pattern = CreateFilenamePattern(Filename)!;
bool cabinetHeaderExists = File.Exists(pattern + "1.hdr");
bool shouldScanCabinet = cabinetHeaderExists
? Filename.Equals(pattern + "1.hdr", StringComparison.OrdinalIgnoreCase)
: Filename.Equals(pattern + "1.cab", StringComparison.OrdinalIgnoreCase);
// If we have anything but the first file
if (!shouldScanCabinet)
return false;
// Open the set from the pattern
cabinet = OpenSet(pattern);
}
// If the cabinet set could not be opened
if (cabinet == null)
return false;
try
{
for (int i = 0; i < cabinet.FileCount; i++)
{
try
{
// Check if the file is valid first
if (!cabinet.FileIsValid(i))
continue;
// Ensure directory separators are consistent
string filename = cabinet.GetFileName(i) ?? $"BAD_FILENAME{i}";
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);
cabinet.FileSave(i, filename);
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
}
}
return true;
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
}
/// <summary>
/// Save the file at the given index to the filename specified
/// </summary>
public bool FileSave(int index, string filename, bool useOld = false)
{
// Get the file descriptor
if (!TryGetFileDescriptor(index, out var fileDescriptor) || fileDescriptor == null)
return false;
// If the file is split
if (fileDescriptor.LinkFlags == LinkFlags.LINK_PREV)
return FileSave((int)fileDescriptor.LinkPrevious, filename, useOld);
// Get the reader at the index
var reader = Reader.Create(this, index, fileDescriptor);
if (reader == null)
return false;
// Create the output file and hasher
FileStream output = File.OpenWrite(filename);
var md5 = new HashWrapper(HashType.MD5);
long readBytesLeft = (long)GetReadableBytes(fileDescriptor);
long writeBytesLeft = (long)GetWritableBytes(fileDescriptor);
byte[] inputBuffer;
byte[] outputBuffer = new byte[BUFFER_SIZE];
long totalWritten = 0;
// Read while there are bytes remaining
while (readBytesLeft > 0 && writeBytesLeft > 0)
{
long bytesToWrite = BUFFER_SIZE;
int result;
// Handle compressed files
#if NET20 || NET35
if ((fileDescriptor.Flags & FileFlags.FILE_COMPRESSED) != 0)
#else
if (fileDescriptor.Flags.HasFlag(FileFlags.FILE_COMPRESSED))
#endif
{
// Attempt to read the length value
byte[] lengthArr = new byte[sizeof(ushort)];
if (!reader.Read(lengthArr, 0, lengthArr.Length))
{
Console.Error.WriteLine($"Failed to read {lengthArr.Length} bytes of file {index} ({GetFileName(index)}) from input cabinet file {fileDescriptor.Volume}");
reader.Dispose();
output?.Close();
return false;
}
// Attempt to read the specified number of bytes
ushort bytesToRead = BitConverter.ToUInt16(lengthArr, 0);
inputBuffer = new byte[BUFFER_SIZE + 1];
if (!reader.Read(inputBuffer, 0, bytesToRead))
{
Console.Error.WriteLine($"Failed to read {lengthArr.Length} bytes of file {index} ({GetFileName(index)}) from input cabinet file {fileDescriptor.Volume}");
reader.Dispose();
output?.Close();
return false;
}
// Add a null byte to make inflate happy
inputBuffer[bytesToRead] = 0;
ulong readBytes = (ulong)(bytesToRead + 1);
// Uncompress into a buffer
if (useOld)
result = UncompressOld(outputBuffer, ref bytesToWrite, inputBuffer, ref readBytes);
else
result = Uncompress(outputBuffer, ref bytesToWrite, inputBuffer, ref readBytes);
// If we didn't get a positive result that's not a data error (false positives)
if (result != zlibConst.Z_OK && result != zlibConst.Z_DATA_ERROR)
{
Console.Error.WriteLine($"Decompression failed with code {result.ToZlibConstName()}. bytes_to_read={bytesToRead}, volume={fileDescriptor.Volume}, read_bytes={readBytes}");
reader.Dispose();
output?.Close();
return false;
}
// Set remaining bytes
readBytesLeft -= 2;
readBytesLeft -= bytesToRead;
}
// Handle uncompressed files
else
{
bytesToWrite = Math.Min(readBytesLeft, BUFFER_SIZE);
if (!reader.Read(outputBuffer, 0, (int)bytesToWrite))
{
Console.Error.WriteLine($"Failed to write {bytesToWrite} bytes from input cabinet file {fileDescriptor.Volume}");
reader.Dispose();
output?.Close();
return false;
}
// Set remaining bytes
readBytesLeft -= (uint)bytesToWrite;
}
// Hash and write the next block
bytesToWrite = Math.Min(bytesToWrite, writeBytesLeft);
md5.Process(outputBuffer, 0, (int)bytesToWrite);
output?.Write(outputBuffer, 0, (int)bytesToWrite);
totalWritten += bytesToWrite;
writeBytesLeft -= bytesToWrite;
}
// Validate the number of bytes written
if ((long)fileDescriptor.ExpandedSize != totalWritten)
Console.WriteLine($"Expanded size of file {index} ({GetFileName(index)}) expected to be {fileDescriptor.ExpandedSize}, but was {totalWritten}");
// Finalize output values
md5.Terminate();
reader?.Dispose();
output?.Close();
// Validate the data written, if required
if (MajorVersion >= 6)
{
string expectedMd5 = BitConverter.ToString(fileDescriptor.MD5!);
expectedMd5 = expectedMd5.ToLowerInvariant().Replace("-", string.Empty);
string? actualMd5 = md5.CurrentHashString;
if (actualMd5 == null || actualMd5 != expectedMd5)
{
Console.Error.WriteLine($"MD5 checksum failure for file {index} ({GetFileName(index)})");
return false;
}
}
return true;
}
/// <summary>
/// Save the file at the given index to the filename specified as raw
/// </summary>
public bool FileSaveRaw(int index, string filename)
{
// Get the file descriptor
if (!TryGetFileDescriptor(index, out var fileDescriptor) || fileDescriptor == null)
return false;
// If the file is split
if (fileDescriptor.LinkFlags == LinkFlags.LINK_PREV)
return FileSaveRaw((int)fileDescriptor.LinkPrevious, filename);
// Get the reader at the index
var reader = Reader.Create(this, index, fileDescriptor);
if (reader == null)
return false;
// Create the output file
FileStream output = File.OpenWrite(filename);
ulong bytesLeft = GetReadableBytes(fileDescriptor);
byte[] outputBuffer = new byte[BUFFER_SIZE];
// Read while there are bytes remaining
while (bytesLeft > 0)
{
ulong bytesToWrite = Math.Min(bytesLeft, BUFFER_SIZE);
if (!reader.Read(outputBuffer, 0, (int)bytesToWrite))
{
Console.Error.WriteLine($"Failed to read {bytesToWrite} bytes from input cabinet file {fileDescriptor.Volume}");
reader.Dispose();
output?.Close();
return false;
}
// Set remaining bytes
bytesLeft -= (uint)bytesToWrite;
// Write the next block
output.Write(outputBuffer, 0, (int)bytesToWrite);
}
// Finalize output values
reader.Dispose();
output?.Close();
return true;
}
/// <summary>
/// Uncompress a source byte array to a destination
/// </summary>
private unsafe static int Uncompress(byte[] dest, ref long destLen, byte[] source, ref ulong sourceLen)
{
fixed (byte* sourcePtr = source)
fixed (byte* destPtr = dest)
{
var stream = new ZLib.z_stream_s
{
next_in = sourcePtr,
avail_in = (uint)sourceLen,
next_out = destPtr,
avail_out = (uint)destLen,
};
// make second parameter negative to disable checksum verification
int err = ZLib.inflateInit2_(stream, -MAX_WBITS, ZLib.zlibVersion(), source.Length);
if (err != zlibConst.Z_OK)
return err;
err = ZLib.inflate(stream, 1);
if (err != zlibConst.Z_OK && err != zlibConst.Z_STREAM_END)
{
ZLib.inflateEnd(stream);
return err;
}
destLen = stream.total_out;
sourceLen = stream.total_in;
return ZLib.inflateEnd(stream);
}
}
/// <summary>
/// Uncompress a source byte array to a destination (old version)
/// </summary>
private unsafe static int UncompressOld(byte[] dest, ref long destLen, byte[] source, ref ulong sourceLen)
{
fixed (byte* sourcePtr = source)
fixed (byte* destPtr = dest)
{
var stream = new ZLib.z_stream_s
{
next_in = sourcePtr,
avail_in = (uint)sourceLen,
next_out = destPtr,
avail_out = (uint)destLen,
};
destLen = 0;
sourceLen = 0;
// make second parameter negative to disable checksum verification
int err = ZLib.inflateInit2_(stream, -MAX_WBITS, ZLib.zlibVersion(), source.Length);
if (err != zlibConst.Z_OK)
return err;
while (stream.avail_in > 1)
{
err = ZLib.inflate(stream, 1);
if (err != zlibConst.Z_OK)
{
ZLib.inflateEnd(stream);
return err;
}
}
destLen = stream.total_out;
sourceLen = stream.total_in;
return ZLib.inflateEnd(stream);
}
}
#endregion
#region Obfuscation
/// <summary>
/// Deobfuscate a buffer
/// </summary>
public static void Deobfuscate(byte[] buffer, long size, ref uint offset)
{
offset = Deobfuscate(buffer, size, offset);
}
/// <summary>
/// Deobfuscate a buffer with a seed value
/// </summary>
/// <remarks>Seed is 0 at file start</remarks>
public static uint Deobfuscate(byte[] buffer, long size, uint seed)
{
for (int i = 0; size > 0; size--, i++, seed++)
{
buffer[i] = (byte)(ROR8(buffer[i] ^ 0xd5, 2) - (seed % 0x47));
}
return seed;
}
/// <summary>
/// Obfuscate a buffer
/// </summary>
public static void Obfuscate(byte[] buffer, long size, ref uint offset)
{
offset = Obfuscate(buffer, size, offset);
}
/// <summary>
/// Obfuscate a buffer with a seed value
/// </summary>
/// <remarks>Seed is 0 at file start</remarks>
public static uint Obfuscate(byte[] buffer, long size, uint seed)
{
for (int i = 0; size > 0; size--, i++, seed++)
{
buffer[i] = (byte)(ROL8(buffer[i] ^ 0xd5, 2) + (seed % 0x47));
}
return seed;
}
/// <summary>
/// Rotate Right 8
/// </summary>
private static int ROR8(int x, byte n) => (x >> n) | (x << (8 - n));
/// <summary>
/// Rotate Left 8
/// </summary>
private static int ROL8(int x, byte n) => (x << n) | (x >> (8 - n));
#endregion
#region Helper Classes
/// <summary>
/// Helper to read a single file from a cabinet set
/// </summary>
private class Reader : IDisposable
{
#region Private Instance Variables
/// <summary>
/// Cabinet file to read from
/// </summary>
private readonly InstallShieldCabinet _cabinet;
/// <summary>
/// Currently selected index
/// </summary>
private readonly uint _index;
/// <summary>
/// File descriptor defining the currently selected index
/// </summary>
private readonly FileDescriptor _fileDescriptor;
/// <summary>
/// Offset in the data where the file exists
/// </summary>
private ulong _dataOffset;
/// <summary>
/// Number of bytes left in the current volume
/// </summary>
private ulong _volumeBytesLeft;
/// <summary>
/// Handle to the current volume stream
/// </summary>
private Stream? _volumeFile;
/// <summary>
/// Current volume header
/// </summary>
private VolumeHeader? _volumeHeader;
/// <summary>
/// Current volume ID
/// </summary>
private ushort _volumeId;
/// <summary>
/// Offset for obfuscation seed
/// </summary>
private uint _obfuscationOffset;
#endregion
#region Constructors
private Reader(InstallShieldCabinet cabinet, uint index, FileDescriptor fileDescriptor)
{
_cabinet = cabinet;
_index = index;
_fileDescriptor = fileDescriptor;
}
#endregion
/// <summary>
/// Create a new <see cref="Reader"> from an existing cabinet, index, and file descriptor
/// </summary>
public static Reader? Create(InstallShieldCabinet cabinet, int index, FileDescriptor fileDescriptor)
{
var reader = new Reader(cabinet, (uint)index, fileDescriptor);
for (; ; )
{
// If the volume is invalid
if (!reader.OpenVolume(fileDescriptor.Volume))
{
Console.Error.WriteLine($"Failed to open volume {fileDescriptor.Volume}");
return null;
}
else if (reader._volumeFile == null || reader._volumeHeader == null)
{
Console.Error.WriteLine($"Volume {fileDescriptor.Volume} is invalid");
return null;
}
// Start with the correct volume for IS5 cabinets
if (reader._cabinet.MajorVersion <= 5 && index > (int)reader._volumeHeader.LastFileIndex)
{
// Normalize the volume ID for odd cases
if (fileDescriptor.Volume == ushort.MinValue || fileDescriptor.Volume == ushort.MaxValue)
fileDescriptor.Volume = 1;
fileDescriptor.Volume++;
continue;
}
break;
}
return reader;
}
/// <summary>
/// Dispose of the current object
/// </summary>
public void Dispose()
{
_volumeFile?.Close();
}
#region Reading
/// <summary>
/// Read a certain number of bytes from the current volume
/// </summary>
public bool Read(byte[] buffer, int start, long size)
{
long bytesLeft = size;
while (bytesLeft > 0)
{
// Open the next volume, if necessary
if (_volumeBytesLeft == 0)
{
if (!OpenNextVolume(out _))
return false;
}
// Get the number of bytes to read from this volume
int bytesToRead = (int)Math.Min(bytesLeft, (long)_volumeBytesLeft);
if (bytesToRead == 0)
break;
// Read as much as possible from this volume
if (bytesToRead != _volumeFile!.Read(buffer, start, bytesToRead))
return false;
// Set the number of bytes left
bytesLeft -= bytesToRead;
_volumeBytesLeft -= (uint)bytesToRead;
}
#if NET20 || NET35
if ((_fileDescriptor.Flags & FileFlags.FILE_OBFUSCATED) != 0)
#else
if (_fileDescriptor.Flags.HasFlag(FileFlags.FILE_OBFUSCATED))
#endif
Deobfuscate(buffer, size, ref _obfuscationOffset);
return true;
}
/// <summary>
/// Open the next volume based on the current index
/// </summary>
private bool OpenNextVolume(out ushort nextVolume)
{
nextVolume = (ushort)(_volumeId + 1);
return OpenVolume(nextVolume);
}
/// <summary>
/// Open the volume at the inputted index
/// </summary>
private bool OpenVolume(ushort volume)
{
// Read the volume from the cabinet set
var next = _cabinet.OpenVolume(volume, out var volumeStream);
if (next?.VolumeHeader == null || volumeStream == null)
{
Console.Error.WriteLine($"Failed to open input cabinet file {volume}");
return false;
}
// Assign the next items
_volumeFile?.Close();
_volumeFile = volumeStream;
_volumeHeader = next.VolumeHeader;
// Enable support for split archives for IS5
if (_cabinet.MajorVersion == 5)
{
if (_index < (_cabinet.FileCount - 1)
&& _index == _volumeHeader.LastFileIndex
&& _volumeHeader.LastFileSizeCompressed != _fileDescriptor.CompressedSize)
{
_fileDescriptor.Flags |= FileFlags.FILE_SPLIT;
}
else if (_index > 0
&& _index == _volumeHeader.FirstFileIndex
&& _volumeHeader.FirstFileSizeCompressed != _fileDescriptor.CompressedSize)
{
_fileDescriptor.Flags |= FileFlags.FILE_SPLIT;
}
}
ulong volumeBytesLeftCompressed, volumeBytesLeftExpanded;
#if NET20 || NET35
if ((_fileDescriptor.Flags & FileFlags.FILE_SPLIT) != 0)
#else
if (_fileDescriptor.Flags.HasFlag(FileFlags.FILE_SPLIT))
#endif
{
if (_index == _volumeHeader.LastFileIndex && _volumeHeader.LastFileOffset != 0x7FFFFFFF)
{
// can be first file too
_dataOffset = _volumeHeader.LastFileOffset;
volumeBytesLeftExpanded = _volumeHeader.LastFileSizeExpanded;
volumeBytesLeftCompressed = _volumeHeader.LastFileSizeCompressed;
}
else if (_index == _volumeHeader.FirstFileIndex)
{
_dataOffset = _volumeHeader.FirstFileOffset;
volumeBytesLeftExpanded = _volumeHeader.FirstFileSizeExpanded;
volumeBytesLeftCompressed = _volumeHeader.FirstFileSizeCompressed;
}
else
{
return true;
}
}
else
{
_dataOffset = _fileDescriptor.DataOffset;
volumeBytesLeftExpanded = _fileDescriptor.ExpandedSize;
volumeBytesLeftCompressed = _fileDescriptor.CompressedSize;
}
#if NET20 || NET35
if ((_fileDescriptor.Flags & FileFlags.FILE_COMPRESSED) != 0)
#else
if (_fileDescriptor.Flags.HasFlag(FileFlags.FILE_COMPRESSED))
#endif
_volumeBytesLeft = volumeBytesLeftCompressed;
else
_volumeBytesLeft = volumeBytesLeftExpanded;
_volumeFile.Seek((long)_dataOffset, SeekOrigin.Begin);
_volumeId = volume;
return true;
}
#endregion
}
#endregion
}
}

View File

@@ -1,15 +1,10 @@
using System;
using System.IO;
using System.Text.RegularExpressions;
using SabreTools.Hashing;
using SabreTools.IO.Compression.zlib;
using SabreTools.Models.InstallShieldCabinet;
using SabreTools.Serialization.Interfaces;
using static SabreTools.Models.InstallShieldCabinet.Constants;
namespace SabreTools.Serialization.Wrappers
{
public partial class InstallShieldCabinet : WrapperBase<Cabinet>, IExtractable
public partial class InstallShieldCabinet : WrapperBase<Cabinet>
{
#region Descriptive Properties
@@ -68,47 +63,6 @@ namespace SabreTools.Serialization.Wrappers
/// <inheritdoc cref="Cabinet.VolumeHeader"/>
public VolumeHeader? VolumeHeader => Model.VolumeHeader;
/// <summary>
/// Reference to the next cabinet header
/// </summary>
/// <remarks>Only used in multi-file</remarks>
public InstallShieldCabinet? Next { get; set; }
/// <summary>
/// Reference to the next previous header
/// </summary>
/// <remarks>Only used in multi-file</remarks>
public InstallShieldCabinet? Prev { get; set; }
/// <summary>
/// Volume index ID, 0 for headers
/// </summary>
/// <remarks>Only used in multi-file</remarks>
public ushort VolumeID { get; set; }
#endregion
#region Extraction State
/// <summary>
/// Base filename path for related CAB files
/// </summary>
internal string? FilenamePattern { get; set; }
#endregion
#region Constants
/// <summary>
/// Default buffer size
/// </summary>
private const int BUFFER_SIZE = 64 * 1024;
/// <summary>
/// Maximum size of the window in bits
/// </summary>
private const int MAX_WBITS = 15;
#endregion
#region Constructors
@@ -179,148 +133,6 @@ namespace SabreTools.Serialization.Wrappers
#endregion
#region Cabinet Set
/// <summary>
/// Open a cabinet set for reading, if possible
/// </summary>
/// <param name="pattern">Filename pattern for matching cabinet files</param>
/// <returns>Wrapper representing the set, null on error</returns>
public static InstallShieldCabinet? OpenSet(string? pattern)
{
// An invalid pattern means no cabinet files
if (string.IsNullOrEmpty(pattern))
return null;
// Create a placeholder wrapper for output
InstallShieldCabinet? set = null;
// Loop until there are no parts left
bool iterate = true;
InstallShieldCabinet? previous = null;
for (ushort i = 1; iterate; i++)
{
var file = OpenFileForReading(pattern, i, HEADER_SUFFIX);
if (file != null)
iterate = false;
else
file = OpenFileForReading(pattern, i, CABINET_SUFFIX);
if (file == null)
break;
var current = Create(file);
if (current == null)
break;
current.VolumeID = i;
if (previous != null)
{
previous.Next = current;
current.Prev = previous;
}
else
{
set = current;
previous = current;
}
}
// Set the pattern, if possible
if (set != null)
set.FilenamePattern = pattern;
return set;
}
/// <summary>
/// Open the numbered cabinet set volume
/// </summary>
/// <param name="volumeId">Volume ID, 1-indexed</param>
/// <returns>Wrapper representing the volume on success, null otherwise</returns>
public InstallShieldCabinet? OpenVolume(ushort volumeId, out Stream? volumeStream)
{
// Normalize the volume ID for odd cases
if (volumeId == ushort.MinValue || volumeId == ushort.MaxValue)
volumeId = 1;
// Try to open the file as a stream
volumeStream = OpenFileForReading(FilenamePattern, volumeId, CABINET_SUFFIX);
if (volumeStream == null)
{
Console.Error.WriteLine($"Failed to open input cabinet file {volumeId}");
return null;
}
// Try to parse the stream into a cabinet
var volume = Create(volumeStream);
if (volume == null)
{
Console.Error.WriteLine($"Failed to open input cabinet file {volumeId}");
return null;
}
// Set the volume ID and return
volume.VolumeID = volumeId;
return volume;
}
/// <summary>
/// Open a cabinet file for reading
/// </summary>
/// <param name="index">Cabinet part index to be opened</param>
/// <param name="suffix">Cabinet files suffix (e.g. `.cab`)</param>
/// <returns>A Stream representing the cabinet part, null on error</returns>
public Stream? OpenFileForReading(int index, string suffix)
=> OpenFileForReading(FilenamePattern, index, suffix);
/// <summary>
/// Create the generic filename pattern to look for from the input filename
/// </summary>
/// <returns>String representing the filename pattern for a cabinet set, null on error</returns>
private static string? CreateFilenamePattern(string filename)
{
string? pattern = null;
if (string.IsNullOrEmpty(filename))
return pattern;
string? directory = Path.GetDirectoryName(Path.GetFullPath(filename));
if (directory != null)
pattern = Path.Combine(directory, Path.GetFileNameWithoutExtension(filename));
else
pattern = Path.GetFileNameWithoutExtension(filename);
return new Regex(@"\d+$").Replace(pattern, string.Empty);
}
/// <summary>
/// Open a cabinet file for reading
/// </summary>
/// <param name="pattern">Filename pattern for matching cabinet files</param>
/// <param name="index">Cabinet part index to be opened</param>
/// <param name="suffix">Cabinet files suffix (e.g. `.cab`)</param>
/// <returns>A Stream representing the cabinet part, null on error</returns>
private static Stream? OpenFileForReading(string? pattern, int index, string suffix)
{
// An invalid pattern means no cabinet files
if (string.IsNullOrEmpty(pattern))
return null;
// Attempt lower-case extension
string filename = $"{pattern}{index}.{suffix}";
if (File.Exists(filename))
return File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
// Attempt upper-case extension
filename = $"{pattern}{index}.{suffix.ToUpperInvariant()}";
if (File.Exists(filename))
return File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
return null;
}
#endregion
#region Component
/// <summary>
@@ -374,336 +186,6 @@ namespace SabreTools.Serialization.Wrappers
#endregion
#region Extraction
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// Open the full set if possible
var cabinet = this;
if (Filename != null)
{
// Get the name of the first cabinet file or header
string pattern = CreateFilenamePattern(Filename)!;
bool cabinetHeaderExists = File.Exists(pattern + "1.hdr");
bool shouldScanCabinet = cabinetHeaderExists
? Filename.Equals(pattern + "1.hdr", StringComparison.OrdinalIgnoreCase)
: Filename.Equals(pattern + "1.cab", StringComparison.OrdinalIgnoreCase);
// If we have anything but the first file
if (!shouldScanCabinet)
return false;
// Open the set from the pattern
cabinet = OpenSet(pattern);
}
// If the cabinet set could not be opened
if (cabinet == null)
return false;
try
{
for (int i = 0; i < cabinet.FileCount; i++)
{
try
{
// Check if the file is valid first
if (!cabinet.FileIsValid(i))
continue;
// Ensure directory separators are consistent
string filename = cabinet.GetFileName(i) ?? $"BAD_FILENAME{i}";
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);
cabinet.FileSave(i, filename);
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
}
}
return true;
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
}
/// <summary>
/// Save the file at the given index to the filename specified
/// </summary>
public bool FileSave(int index, string filename, bool useOld = false)
{
// Get the file descriptor
if (!TryGetFileDescriptor(index, out var fileDescriptor) || fileDescriptor == null)
return false;
// If the file is split
if (fileDescriptor.LinkFlags == LinkFlags.LINK_PREV)
return FileSave((int)fileDescriptor.LinkPrevious, filename, useOld);
// Get the reader at the index
var reader = Reader.Create(this, index, fileDescriptor);
if (reader == null)
return false;
// Create the output file and hasher
FileStream output = File.OpenWrite(filename);
var md5 = new HashWrapper(HashType.MD5);
long readBytesLeft = (long)GetReadableBytes(fileDescriptor);
long writeBytesLeft = (long)GetWritableBytes(fileDescriptor);
byte[] inputBuffer;
byte[] outputBuffer = new byte[BUFFER_SIZE];
long totalWritten = 0;
// Read while there are bytes remaining
while (readBytesLeft > 0 && writeBytesLeft > 0)
{
long bytesToWrite = BUFFER_SIZE;
int result;
// Handle compressed files
#if NET20 || NET35
if ((fileDescriptor.Flags & FileFlags.FILE_COMPRESSED) != 0)
#else
if (fileDescriptor.Flags.HasFlag(FileFlags.FILE_COMPRESSED))
#endif
{
// Attempt to read the length value
byte[] lengthArr = new byte[sizeof(ushort)];
if (!reader.Read(lengthArr, 0, lengthArr.Length))
{
Console.Error.WriteLine($"Failed to read {lengthArr.Length} bytes of file {index} ({GetFileName(index)}) from input cabinet file {fileDescriptor.Volume}");
reader.Dispose();
output?.Close();
return false;
}
// Attempt to read the specified number of bytes
ushort bytesToRead = BitConverter.ToUInt16(lengthArr, 0);
inputBuffer = new byte[BUFFER_SIZE + 1];
if (!reader.Read(inputBuffer, 0, bytesToRead))
{
Console.Error.WriteLine($"Failed to read {lengthArr.Length} bytes of file {index} ({GetFileName(index)}) from input cabinet file {fileDescriptor.Volume}");
reader.Dispose();
output?.Close();
return false;
}
// Add a null byte to make inflate happy
inputBuffer[bytesToRead] = 0;
ulong readBytes = (ulong)(bytesToRead + 1);
// Uncompress into a buffer
if (useOld)
result = UncompressOld(outputBuffer, ref bytesToWrite, inputBuffer, ref readBytes);
else
result = Uncompress(outputBuffer, ref bytesToWrite, inputBuffer, ref readBytes);
// If we didn't get a positive result that's not a data error (false positives)
if (result != zlibConst.Z_OK && result != zlibConst.Z_DATA_ERROR)
{
Console.Error.WriteLine($"Decompression failed with code {result.ToZlibConstName()}. bytes_to_read={bytesToRead}, volume={fileDescriptor.Volume}, read_bytes={readBytes}");
reader.Dispose();
output?.Close();
return false;
}
// Set remaining bytes
readBytesLeft -= 2;
readBytesLeft -= bytesToRead;
}
// Handle uncompressed files
else
{
bytesToWrite = Math.Min(readBytesLeft, BUFFER_SIZE);
if (!reader.Read(outputBuffer, 0, (int)bytesToWrite))
{
Console.Error.WriteLine($"Failed to write {bytesToWrite} bytes from input cabinet file {fileDescriptor.Volume}");
reader.Dispose();
output?.Close();
return false;
}
// Set remaining bytes
readBytesLeft -= (uint)bytesToWrite;
}
// Hash and write the next block
bytesToWrite = Math.Min(bytesToWrite, writeBytesLeft);
md5.Process(outputBuffer, 0, (int)bytesToWrite);
output?.Write(outputBuffer, 0, (int)bytesToWrite);
totalWritten += bytesToWrite;
writeBytesLeft -= bytesToWrite;
}
// Validate the number of bytes written
if ((long)fileDescriptor.ExpandedSize != totalWritten)
Console.WriteLine($"Expanded size of file {index} ({GetFileName(index)}) expected to be {fileDescriptor.ExpandedSize}, but was {totalWritten}");
// Finalize output values
md5.Terminate();
reader?.Dispose();
output?.Close();
// Validate the data written, if required
if (MajorVersion >= 6)
{
string expectedMd5 = BitConverter.ToString(fileDescriptor.MD5!);
expectedMd5 = expectedMd5.ToLowerInvariant().Replace("-", string.Empty);
string? actualMd5 = md5.CurrentHashString;
if (actualMd5 == null || actualMd5 != expectedMd5)
{
Console.Error.WriteLine($"MD5 checksum failure for file {index} ({GetFileName(index)})");
return false;
}
}
return true;
}
/// <summary>
/// Save the file at the given index to the filename specified as raw
/// </summary>
public bool FileSaveRaw(int index, string filename)
{
// Get the file descriptor
if (!TryGetFileDescriptor(index, out var fileDescriptor) || fileDescriptor == null)
return false;
// If the file is split
if (fileDescriptor.LinkFlags == LinkFlags.LINK_PREV)
return FileSaveRaw((int)fileDescriptor.LinkPrevious, filename);
// Get the reader at the index
var reader = Reader.Create(this, index, fileDescriptor);
if (reader == null)
return false;
// Create the output file
FileStream output = File.OpenWrite(filename);
ulong bytesLeft = GetReadableBytes(fileDescriptor);
byte[] outputBuffer = new byte[BUFFER_SIZE];
// Read while there are bytes remaining
while (bytesLeft > 0)
{
ulong bytesToWrite = Math.Min(bytesLeft, BUFFER_SIZE);
if (!reader.Read(outputBuffer, 0, (int)bytesToWrite))
{
Console.Error.WriteLine($"Failed to read {bytesToWrite} bytes from input cabinet file {fileDescriptor.Volume}");
reader.Dispose();
output?.Close();
return false;
}
// Set remaining bytes
bytesLeft -= (uint)bytesToWrite;
// Write the next block
output.Write(outputBuffer, 0, (int)bytesToWrite);
}
// Finalize output values
reader.Dispose();
output?.Close();
return true;
}
/// <summary>
/// Uncompress a source byte array to a destination
/// </summary>
private unsafe static int Uncompress(byte[] dest, ref long destLen, byte[] source, ref ulong sourceLen)
{
fixed (byte* sourcePtr = source)
fixed (byte* destPtr = dest)
{
var stream = new ZLib.z_stream_s
{
next_in = sourcePtr,
avail_in = (uint)sourceLen,
next_out = destPtr,
avail_out = (uint)destLen,
};
// make second parameter negative to disable checksum verification
int err = ZLib.inflateInit2_(stream, -MAX_WBITS, ZLib.zlibVersion(), source.Length);
if (err != zlibConst.Z_OK)
return err;
err = ZLib.inflate(stream, 1);
if (err != zlibConst.Z_OK && err != zlibConst.Z_STREAM_END)
{
ZLib.inflateEnd(stream);
return err;
}
destLen = stream.total_out;
sourceLen = stream.total_in;
return ZLib.inflateEnd(stream);
}
}
/// <summary>
/// Uncompress a source byte array to a destination (old version)
/// </summary>
private unsafe static int UncompressOld(byte[] dest, ref long destLen, byte[] source, ref ulong sourceLen)
{
fixed (byte* sourcePtr = source)
fixed (byte* destPtr = dest)
{
var stream = new ZLib.z_stream_s
{
next_in = sourcePtr,
avail_in = (uint)sourceLen,
next_out = destPtr,
avail_out = (uint)destLen,
};
destLen = 0;
sourceLen = 0;
// make second parameter negative to disable checksum verification
int err = ZLib.inflateInit2_(stream, -MAX_WBITS, ZLib.zlibVersion(), source.Length);
if (err != zlibConst.Z_OK)
return err;
while (stream.avail_in > 1)
{
err = ZLib.inflate(stream, 1);
if (err != zlibConst.Z_OK)
{
ZLib.inflateEnd(stream);
return err;
}
}
destLen = stream.total_out;
sourceLen = stream.total_in;
return ZLib.inflateEnd(stream);
}
}
#endregion
#region File
/// <summary>
@@ -882,312 +364,5 @@ namespace SabreTools.Serialization.Wrappers
=> GetFileGroupFromFile(index)?.Name;
#endregion
#region Obfuscation
/// <summary>
/// Deobfuscate a buffer
/// </summary>
public static void Deobfuscate(byte[] buffer, long size, ref uint offset)
{
offset = Deobfuscate(buffer, size, offset);
}
/// <summary>
/// Deobfuscate a buffer with a seed value
/// </summary>
/// <remarks>Seed is 0 at file start</remarks>
public static uint Deobfuscate(byte[] buffer, long size, uint seed)
{
for (int i = 0; size > 0; size--, i++, seed++)
{
buffer[i] = (byte)(ROR8(buffer[i] ^ 0xd5, 2) - (seed % 0x47));
}
return seed;
}
/// <summary>
/// Obfuscate a buffer
/// </summary>
public static void Obfuscate(byte[] buffer, long size, ref uint offset)
{
offset = Obfuscate(buffer, size, offset);
}
/// <summary>
/// Obfuscate a buffer with a seed value
/// </summary>
/// <remarks>Seed is 0 at file start</remarks>
public static uint Obfuscate(byte[] buffer, long size, uint seed)
{
for (int i = 0; size > 0; size--, i++, seed++)
{
buffer[i] = (byte)(ROL8(buffer[i] ^ 0xd5, 2) + (seed % 0x47));
}
return seed;
}
/// <summary>
/// Rotate Right 8
/// </summary>
private static int ROR8(int x, byte n) => (x >> n) | (x << (8 - n));
/// <summary>
/// Rotate Left 8
/// </summary>
private static int ROL8(int x, byte n) => (x << n) | (x >> (8 - n));
#endregion
#region Helper Classes
/// <summary>
/// Helper to read a single file from a cabinet set
/// </summary>
private class Reader : IDisposable
{
#region Private Instance Variables
/// <summary>
/// Cabinet file to read from
/// </summary>
private readonly InstallShieldCabinet _cabinet;
/// <summary>
/// Currently selected index
/// </summary>
private readonly uint _index;
/// <summary>
/// File descriptor defining the currently selected index
/// </summary>
private readonly FileDescriptor _fileDescriptor;
/// <summary>
/// Offset in the data where the file exists
/// </summary>
private ulong _dataOffset;
/// <summary>
/// Number of bytes left in the current volume
/// </summary>
private ulong _volumeBytesLeft;
/// <summary>
/// Handle to the current volume stream
/// </summary>
private Stream? _volumeFile;
/// <summary>
/// Current volume header
/// </summary>
private VolumeHeader? _volumeHeader;
/// <summary>
/// Current volume ID
/// </summary>
private ushort _volumeId;
/// <summary>
/// Offset for obfuscation seed
/// </summary>
private uint _obfuscationOffset;
#endregion
#region Constructors
private Reader(InstallShieldCabinet cabinet, uint index, FileDescriptor fileDescriptor)
{
_cabinet = cabinet;
_index = index;
_fileDescriptor = fileDescriptor;
}
#endregion
/// <summary>
/// Create a new <see cref="Reader"> from an existing cabinet, index, and file descriptor
/// </summary>
public static Reader? Create(InstallShieldCabinet cabinet, int index, FileDescriptor fileDescriptor)
{
var reader = new Reader(cabinet, (uint)index, fileDescriptor);
for (; ; )
{
// If the volume is invalid
if (!reader.OpenVolume(fileDescriptor.Volume))
{
Console.Error.WriteLine($"Failed to open volume {fileDescriptor.Volume}");
return null;
}
else if (reader._volumeFile == null || reader._volumeHeader == null)
{
Console.Error.WriteLine($"Volume {fileDescriptor.Volume} is invalid");
return null;
}
// Start with the correct volume for IS5 cabinets
if (reader._cabinet.MajorVersion <= 5 && index > (int)reader._volumeHeader.LastFileIndex)
{
// Normalize the volume ID for odd cases
if (fileDescriptor.Volume == ushort.MinValue || fileDescriptor.Volume == ushort.MaxValue)
fileDescriptor.Volume = 1;
fileDescriptor.Volume++;
continue;
}
break;
}
return reader;
}
/// <summary>
/// Dispose of the current object
/// </summary>
public void Dispose()
{
_volumeFile?.Close();
}
#region Reading
/// <summary>
/// Read a certain number of bytes from the current volume
/// </summary>
public bool Read(byte[] buffer, int start, long size)
{
long bytesLeft = size;
while (bytesLeft > 0)
{
// Open the next volume, if necessary
if (_volumeBytesLeft == 0)
{
if (!OpenNextVolume(out _))
return false;
}
// Get the number of bytes to read from this volume
int bytesToRead = (int)Math.Min(bytesLeft, (long)_volumeBytesLeft);
if (bytesToRead == 0)
break;
// Read as much as possible from this volume
if (bytesToRead != _volumeFile!.Read(buffer, start, bytesToRead))
return false;
// Set the number of bytes left
bytesLeft -= bytesToRead;
_volumeBytesLeft -= (uint)bytesToRead;
}
#if NET20 || NET35
if ((_fileDescriptor.Flags & FileFlags.FILE_OBFUSCATED) != 0)
#else
if (_fileDescriptor.Flags.HasFlag(FileFlags.FILE_OBFUSCATED))
#endif
Deobfuscate(buffer, size, ref _obfuscationOffset);
return true;
}
/// <summary>
/// Open the next volume based on the current index
/// </summary>
private bool OpenNextVolume(out ushort nextVolume)
{
nextVolume = (ushort)(_volumeId + 1);
return OpenVolume(nextVolume);
}
/// <summary>
/// Open the volume at the inputted index
/// </summary>
private bool OpenVolume(ushort volume)
{
// Read the volume from the cabinet set
var next = _cabinet.OpenVolume(volume, out var volumeStream);
if (next?.VolumeHeader == null || volumeStream == null)
{
Console.Error.WriteLine($"Failed to open input cabinet file {volume}");
return false;
}
// Assign the next items
_volumeFile?.Close();
_volumeFile = volumeStream;
_volumeHeader = next.VolumeHeader;
// Enable support for split archives for IS5
if (_cabinet.MajorVersion == 5)
{
if (_index < (_cabinet.FileCount - 1)
&& _index == _volumeHeader.LastFileIndex
&& _volumeHeader.LastFileSizeCompressed != _fileDescriptor.CompressedSize)
{
_fileDescriptor.Flags |= FileFlags.FILE_SPLIT;
}
else if (_index > 0
&& _index == _volumeHeader.FirstFileIndex
&& _volumeHeader.FirstFileSizeCompressed != _fileDescriptor.CompressedSize)
{
_fileDescriptor.Flags |= FileFlags.FILE_SPLIT;
}
}
ulong volumeBytesLeftCompressed, volumeBytesLeftExpanded;
#if NET20 || NET35
if ((_fileDescriptor.Flags & FileFlags.FILE_SPLIT) != 0)
#else
if (_fileDescriptor.Flags.HasFlag(FileFlags.FILE_SPLIT))
#endif
{
if (_index == _volumeHeader.LastFileIndex && _volumeHeader.LastFileOffset != 0x7FFFFFFF)
{
// can be first file too
_dataOffset = _volumeHeader.LastFileOffset;
volumeBytesLeftExpanded = _volumeHeader.LastFileSizeExpanded;
volumeBytesLeftCompressed = _volumeHeader.LastFileSizeCompressed;
}
else if (_index == _volumeHeader.FirstFileIndex)
{
_dataOffset = _volumeHeader.FirstFileOffset;
volumeBytesLeftExpanded = _volumeHeader.FirstFileSizeExpanded;
volumeBytesLeftCompressed = _volumeHeader.FirstFileSizeCompressed;
}
else
{
return true;
}
}
else
{
_dataOffset = _fileDescriptor.DataOffset;
volumeBytesLeftExpanded = _fileDescriptor.ExpandedSize;
volumeBytesLeftCompressed = _fileDescriptor.CompressedSize;
}
#if NET20 || NET35
if ((_fileDescriptor.Flags & FileFlags.FILE_COMPRESSED) != 0)
#else
if (_fileDescriptor.Flags.HasFlag(FileFlags.FILE_COMPRESSED))
#endif
_volumeBytesLeft = volumeBytesLeftCompressed;
else
_volumeBytesLeft = volumeBytesLeftExpanded;
_volumeFile.Seek((long)_dataOffset, SeekOrigin.Begin);
_volumeId = volume;
return true;
}
#endregion
}
#endregion
}
}

View File

@@ -0,0 +1,66 @@
using System;
using System.IO;
using SabreTools.IO.Compression.SZDD;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public partial class LZKWAJ : IExtractable
{
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// Get the length of the compressed data
long compressedSize = Length - DataOffset;
if (compressedSize < DataOffset)
return false;
// Read in the data as an array
byte[]? contents = ReadRangeFromSource(DataOffset, (int)compressedSize);
if (contents == null)
return false;
// Get the decompressor
var decompressor = Decompressor.CreateKWAJ(contents, CompressionType);
if (decompressor == null)
return false;
// If we have an invalid output directory
if (string.IsNullOrEmpty(outputDirectory))
return false;
// Create the full output path
string filename = FileName ?? "tempfile";
if (FileExtension != null)
filename += $".{FileExtension}";
// 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);
// Try to write the data
try
{
// Open the output file for writing
using Stream fs = File.OpenWrite(filename);
decompressor.CopyTo(fs);
fs.Flush();
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
return true;
}
}
}

View File

@@ -1,13 +1,9 @@
using System;
using System.IO;
using SabreTools.IO.Compression.SZDD;
using SabreTools.IO.Extensions;
using SabreTools.Models.LZ;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public class LZKWAJ : WrapperBase<KWAJFile>, IExtractable
public partial class LZKWAJ : WrapperBase<KWAJFile>
{
#region Descriptive Properties
@@ -99,65 +95,5 @@ namespace SabreTools.Serialization.Wrappers
}
#endregion
#region Extraction
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// Get the length of the compressed data
long compressedSize = Length - DataOffset;
if (compressedSize < DataOffset)
return false;
// Read in the data as an array
byte[]? contents = ReadRangeFromSource(DataOffset, (int)compressedSize);
if (contents == null)
return false;
// Get the decompressor
var decompressor = Decompressor.CreateKWAJ(contents, CompressionType);
if (decompressor == null)
return false;
// If we have an invalid output directory
if (string.IsNullOrEmpty(outputDirectory))
return false;
// Create the full output path
string filename = FileName ?? "tempfile";
if (FileExtension != null)
filename += $".{FileExtension}";
// 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);
// Try to write the data
try
{
// Open the output file for writing
using Stream fs = File.OpenWrite(filename);
decompressor.CopyTo(fs);
fs.Flush();
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
return true;
}
#endregion
}
}

View File

@@ -0,0 +1,62 @@
using System;
using System.IO;
using SabreTools.IO.Compression.SZDD;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public partial class LZQBasic : IExtractable
{
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// Get the length of the compressed data
long compressedSize = Length - 12;
if (compressedSize < 12)
return false;
// Read in the data as an array
byte[]? contents = ReadRangeFromSource(12, (int)compressedSize);
if (contents == null)
return false;
// Get the decompressor
var decompressor = Decompressor.CreateQBasic(contents);
if (decompressor == null)
return false;
// If we have an invalid output directory
if (string.IsNullOrEmpty(outputDirectory))
return false;
// Ensure directory separators are consistent
string filename = "tempfile.bin";
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);
// Try to write the data
try
{
// Open the output file for writing
using Stream fs = File.OpenWrite(filename);
decompressor.CopyTo(fs);
fs.Flush();
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
return true;
}
}
}

View File

@@ -1,13 +1,9 @@
using System;
using System.IO;
using SabreTools.IO.Compression.SZDD;
using SabreTools.IO.Extensions;
using SabreTools.Models.LZ;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public class LZQBasic : WrapperBase<QBasicFile>, IExtractable
public partial class LZQBasic : WrapperBase<QBasicFile>
{
#region Descriptive Properties
@@ -83,61 +79,5 @@ namespace SabreTools.Serialization.Wrappers
}
#endregion
#region Extraction
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// Get the length of the compressed data
long compressedSize = Length - 12;
if (compressedSize < 12)
return false;
// Read in the data as an array
byte[]? contents = ReadRangeFromSource(12, (int)compressedSize);
if (contents == null)
return false;
// Get the decompressor
var decompressor = Decompressor.CreateQBasic(contents);
if (decompressor == null)
return false;
// If we have an invalid output directory
if (string.IsNullOrEmpty(outputDirectory))
return false;
// Ensure directory separators are consistent
string filename = "tempfile.bin";
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);
// Try to write the data
try
{
// Open the output file for writing
using Stream fs = File.OpenWrite(filename);
decompressor.CopyTo(fs);
fs.Flush();
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
return true;
}
#endregion
}
}

View File

@@ -0,0 +1,102 @@
using System;
using System.IO;
using SabreTools.IO.Compression.SZDD;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public partial class LZSZDD : IExtractable
{
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
=> Extract(string.Empty, outputDirectory, includeDebug);
/// <inheritdoc cref="Extract(string, bool)"/>
/// <param name="filename">Original name of the file to convert to the output name</param>
public bool Extract(string filename, string outputDirectory, bool includeDebug)
{
// Ensure the filename
if (filename.Length == 0 && Filename != null)
filename = Filename;
// Get the length of the compressed data
long compressedSize = Length - 14;
if (compressedSize < 14)
return false;
// Read in the data as an array
byte[]? contents = ReadRangeFromSource(14, (int)compressedSize);
if (contents == null)
return false;
// Get the decompressor
var decompressor = Decompressor.CreateSZDD(contents);
if (decompressor == null)
return false;
// Create the output file
filename = GetExpandedName(filename).TrimEnd('\0');
// If we have an invalid output directory
if (string.IsNullOrEmpty(outputDirectory))
return false;
// 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);
// Try to write the data
try
{
// Open the output file for writing
using Stream fs = File.OpenWrite(filename);
decompressor.CopyTo(fs);
fs.Flush();
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
return true;
}
/// <summary>
/// Get the full name of the input file
/// </summary>
private string GetExpandedName(string input)
{
// If the extension is missing
string extension = Path.GetExtension(input).TrimStart('.');
if (string.IsNullOrEmpty(extension))
return Path.GetFileNameWithoutExtension(input);
// If the extension is a single character
if (extension.Length == 1)
{
if (extension == "_" || extension == "$")
return $"{Path.GetFileNameWithoutExtension(input)}.{char.ToLower(LastChar)}";
return Path.GetFileNameWithoutExtension(input);
}
// If the extension isn't formatted
if (!extension.EndsWith("_"))
return Path.GetFileNameWithoutExtension(input);
// Handle replacing characters
char c = (char.IsUpper(input[0]) ? char.ToLower(LastChar) : char.ToUpper(LastChar));
string text2 = extension.Substring(0, extension.Length - 1) + c;
return Path.GetFileNameWithoutExtension(input) + "." + text2;
}
}
}

View File

@@ -1,13 +1,9 @@
using System;
using System.IO;
using SabreTools.IO.Compression.SZDD;
using SabreTools.IO.Extensions;
using SabreTools.Models.LZ;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public class LZSZDD : WrapperBase<SZDDFile>, IExtractable
public partial class LZSZDD : WrapperBase<SZDDFile>
{
#region Descriptive Properties
@@ -90,101 +86,5 @@ namespace SabreTools.Serialization.Wrappers
}
#endregion
#region Extraction
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
=> Extract(string.Empty, outputDirectory, includeDebug);
/// <inheritdoc cref="Extract(string, bool)"/>
/// <param name="filename">Original name of the file to convert to the output name</param>
public bool Extract(string filename, string outputDirectory, bool includeDebug)
{
// Ensure the filename
if (filename.Length == 0 && Filename != null)
filename = Filename;
// Get the length of the compressed data
long compressedSize = Length - 14;
if (compressedSize < 14)
return false;
// Read in the data as an array
byte[]? contents = ReadRangeFromSource(14, (int)compressedSize);
if (contents == null)
return false;
// Get the decompressor
var decompressor = Decompressor.CreateSZDD(contents);
if (decompressor == null)
return false;
// Create the output file
filename = GetExpandedName(filename).TrimEnd('\0');
// If we have an invalid output directory
if (string.IsNullOrEmpty(outputDirectory))
return false;
// 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);
// Try to write the data
try
{
// Open the output file for writing
using Stream fs = File.OpenWrite(filename);
decompressor.CopyTo(fs);
fs.Flush();
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
return true;
}
/// <summary>
/// Get the full name of the input file
/// </summary>
private string GetExpandedName(string input)
{
// If the extension is missing
string extension = Path.GetExtension(input).TrimStart('.');
if (string.IsNullOrEmpty(extension))
return Path.GetFileNameWithoutExtension(input);
// If the extension is a single character
if (extension.Length == 1)
{
if (extension == "_" || extension == "$")
return $"{Path.GetFileNameWithoutExtension(input)}.{char.ToLower(LastChar)}";
return Path.GetFileNameWithoutExtension(input);
}
// If the extension isn't formatted
if (!extension.EndsWith("_"))
return Path.GetFileNameWithoutExtension(input);
// Handle replacing characters
char c = (char.IsUpper(input[0]) ? char.ToLower(LastChar) : char.ToUpper(LastChar));
string text2 = extension.Substring(0, extension.Length - 1) + c;
return Path.GetFileNameWithoutExtension(input) + "." + text2;
}
#endregion
}
}

View File

@@ -0,0 +1,292 @@
using System;
using System.IO;
using SabreTools.IO.Extensions;
using SabreTools.Models.MicrosoftCabinet;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public partial class MicrosoftCabinet : IExtractable
{
#region Extension Properties
/// <summary>
/// Reference to the next cabinet header
/// </summary>
/// <remarks>Only used in multi-file</remarks>
public MicrosoftCabinet? Next { get; set; }
/// <summary>
/// Reference to the next previous header
/// </summary>
/// <remarks>Only used in multi-file</remarks>
public MicrosoftCabinet? Prev { get; set; }
#endregion
#region Cabinet Set
/// <summary>
/// Open a cabinet set for reading, if possible
/// </summary>
/// <param name="filename">Filename for one cabinet in the set</param>
/// <returns>Wrapper representing the set, null on error</returns>
private static MicrosoftCabinet? OpenSet(string? filename)
{
// If the file is invalid
if (string.IsNullOrEmpty(filename))
return null;
else if (!File.Exists(filename!))
return null;
// Get the full file path and directory
filename = Path.GetFullPath(filename);
// Read in the current file and try to parse
var stream = File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
var current = Create(stream);
if (current?.Header == null)
return null;
// Seek to the first part of the cabinet set
while (current.CabinetPrev != null)
{
// Attempt to open the previous cabinet
var prev = current.OpenPrevious(filename);
if (prev?.Header == null)
break;
// Assign previous as new current
current = prev;
}
// Cache the current start of the cabinet set
var start = current;
// Read in the cabinet parts sequentially
while (current.CabinetNext != null)
{
// Open the next cabinet and try to parse
var next = current.OpenNext(filename);
if (next?.Header == null)
break;
// Add the next and previous links, resetting current
next.Prev = current;
current.Next = next;
current = next;
}
// Return the start of the set
return start;
}
/// <summary>
/// Open the next archive, if possible
/// </summary>
/// <param name="filename">Filename for one cabinet in the set</param>
private MicrosoftCabinet? OpenNext(string? filename)
{
// Ignore invalid archives
if (Header == null || string.IsNullOrEmpty(filename))
return null;
// Normalize the filename
filename = Path.GetFullPath(filename);
// Get if the cabinet has a next part
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);
}
/// <summary>
/// Open the previous archive, if possible
/// </summary>
/// <param name="filename">Filename for one cabinet in the set</param>
private MicrosoftCabinet? OpenPrevious(string? filename)
{
// Ignore invalid archives
if (Header == null || string.IsNullOrEmpty(filename))
return null;
// Normalize the filename
filename = Path.GetFullPath(filename);
// Get if the cabinet has a previous part
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);
}
#endregion
#region Extraction
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// Display warning
Console.WriteLine("WARNING: LZX and Quantum compression schemes are not supported so some files may be skipped!");
// Open the full set if possible
var cabinet = this;
if (Filename != null)
cabinet = OpenSet(Filename);
// If the archive is invalid
if (cabinet?.Folders == null || cabinet.Folders.Length == 0)
return false;
try
{
// Loop through the folders
bool allExtracted = true;
for (int f = 0; f < cabinet.Folders.Length; f++)
{
var folder = cabinet.Folders[f];
allExtracted &= cabinet.ExtractFolder(Filename, outputDirectory, folder, f, includeDebug);
}
return allExtracted;
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
}
/// <summary>
/// Extract the contents of a single 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="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 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 files = GetFiles(folderIndex);
for (int i = 0; i < files.Length; i++)
{
var file = files[i];
allExtracted &= ExtractFile(outputDirectory, blockStream, file, includeDebug);
}
return allExtracted;
}
/// <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.Seek(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.OpenWrite(filename);
fs.Write(fileData, 0, fileData.Length);
fs.Flush();
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
return true;
}
#endregion
#region Checksumming
/// <summary>
/// The computation and verification of checksums found in CFDATA structure entries cabinet files is
/// done by using a function described by the following mathematical notation. When checksums are
/// not supplied by the cabinet file creating application, the checksum field is set to 0 (zero). Cabinet
/// extracting applications do not compute or verify the checksum if the field is set to 0 (zero).
/// </summary>
private static uint ChecksumData(byte[] data)
{
uint[] C =
[
S(data, 1, data.Length),
S(data, 2, data.Length),
S(data, 3, data.Length),
S(data, 4, data.Length),
];
return C[0] ^ C[1] ^ C[2] ^ C[3];
}
/// <summary>
/// Individual algorithmic step
/// </summary>
private static uint S(byte[] a, int b, int x)
{
int n = a.Length;
if (x < 4 && b > n % 4)
return 0;
else if (x < 4 && b <= n % 4)
return a[n - b + 1];
else // if (x >= 4)
return a[n - x + b] ^ S(a, b, x - 4);
}
#endregion
}
}

View File

@@ -1,13 +1,11 @@
using System;
using System.IO;
using SabreTools.IO.Compression.MSZIP;
using SabreTools.IO.Extensions;
using SabreTools.Models.MicrosoftCabinet;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public partial class MicrosoftCabinet : WrapperBase<Cabinet>, IExtractable
public partial class MicrosoftCabinet : WrapperBase<Cabinet>
{
#region Descriptive Properties
@@ -36,21 +34,9 @@ namespace SabreTools.Serialization.Wrappers
/// <inheritdoc cref="CFHEADER.CabinetNext"/>
public string? CabinetNext => Header?.CabinetNext;
/// <summary>
/// Reference to the next cabinet header
/// </summary>
/// <remarks>Only used in multi-file</remarks>
public MicrosoftCabinet? Next { get; set; }
/// <inheritdoc cref="CFHEADER.CabinetPrev"/>
public string? CabinetPrev => Header?.CabinetPrev;
/// <summary>
/// Reference to the next previous header
/// </summary>
/// <remarks>Only used in multi-file</remarks>
public MicrosoftCabinet? Prev { get; set; }
#endregion
#region Constructors
@@ -121,271 +107,6 @@ namespace SabreTools.Serialization.Wrappers
#endregion
#region Extraction
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// Display warning
Console.WriteLine("WARNING: LZX and Quantum compression schemes are not supported so some files may be skipped!");
// Open the full set if possible
var cabinet = this;
if (Filename != null)
cabinet = OpenSet(Filename);
// If the archive is invalid
if (cabinet?.Folders == null || cabinet.Folders.Length == 0)
return false;
try
{
// Loop through the folders
bool allExtracted = true;
for (int f = 0; f < cabinet.Folders.Length; f++)
{
var folder = cabinet.Folders[f];
allExtracted &= cabinet.ExtractFolder(Filename, outputDirectory, folder, f, includeDebug);
}
return allExtracted;
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
}
/// <summary>
/// Extract the contents of a single 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="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 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 files = GetFiles(folderIndex);
for (int i = 0; i < files.Length; i++)
{
var file = files[i];
allExtracted &= ExtractFile(outputDirectory, blockStream, file, includeDebug);
}
return allExtracted;
}
/// <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.Seek(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.OpenWrite(filename);
fs.Write(fileData, 0, fileData.Length);
fs.Flush();
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
return true;
}
#endregion
#region Cabinet Set
/// <summary>
/// Open a cabinet set for reading, if possible
/// </summary>
/// <param name="filename">Filename for one cabinet in the set</param>
/// <returns>Wrapper representing the set, null on error</returns>
private static MicrosoftCabinet? OpenSet(string? filename)
{
// If the file is invalid
if (string.IsNullOrEmpty(filename))
return null;
else if (!File.Exists(filename!))
return null;
// Get the full file path and directory
filename = Path.GetFullPath(filename);
// Read in the current file and try to parse
var stream = File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
var current = Create(stream);
if (current?.Header == null)
return null;
// Seek to the first part of the cabinet set
while (current.CabinetPrev != null)
{
// Attempt to open the previous cabinet
var prev = current.OpenPrevious(filename);
if (prev?.Header == null)
break;
// Assign previous as new current
current = prev;
}
// Cache the current start of the cabinet set
var start = current;
// Read in the cabinet parts sequentially
while (current.CabinetNext != null)
{
// Open the next cabinet and try to parse
var next = current.OpenNext(filename);
if (next?.Header == null)
break;
// Add the next and previous links, resetting current
next.Prev = current;
current.Next = next;
current = next;
}
// Return the start of the set
return start;
}
/// <summary>
/// Open the next archive, if possible
/// </summary>
/// <param name="filename">Filename for one cabinet in the set</param>
private MicrosoftCabinet? OpenNext(string? filename)
{
// Ignore invalid archives
if (Header == null || string.IsNullOrEmpty(filename))
return null;
// Normalize the filename
filename = Path.GetFullPath(filename);
// Get if the cabinet has a next part
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);
}
/// <summary>
/// Open the previous archive, if possible
/// </summary>
/// <param name="filename">Filename for one cabinet in the set</param>
private MicrosoftCabinet? OpenPrevious(string? filename)
{
// Ignore invalid archives
if (Header == null || string.IsNullOrEmpty(filename))
return null;
// Normalize the filename
filename = Path.GetFullPath(filename);
// Get if the cabinet has a previous part
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);
}
#endregion
#region Checksumming
/// <summary>
/// The computation and verification of checksums found in CFDATA structure entries cabinet files is
/// done by using a function described by the following mathematical notation. When checksums are
/// not supplied by the cabinet file creating application, the checksum field is set to 0 (zero). Cabinet
/// extracting applications do not compute or verify the checksum if the field is set to 0 (zero).
/// </summary>
private static uint ChecksumData(byte[] data)
{
uint[] C =
[
S(data, 1, data.Length),
S(data, 2, data.Length),
S(data, 3, data.Length),
S(data, 4, data.Length),
];
return C[0] ^ C[1] ^ C[2] ^ C[3];
}
/// <summary>
/// Individual algorithmic step
/// </summary>
private static uint S(byte[] a, int b, int x)
{
int n = a.Length;
if (x < 4 && b > n % 4)
return 0;
else if (x < 4 && b <= n % 4)
return a[n - b + 1];
else // if (x >= 4)
return a[n - x + b] ^ S(a, b, x - 4);
}
#endregion
#region Files
/// <summary>

View File

@@ -0,0 +1,79 @@
using System;
using SabreTools.Serialization.Interfaces;
#if (NET452_OR_GREATER || NETCOREAPP) && (WINX86 || WINX64)
using StormLibSharp;
#endif
namespace SabreTools.Serialization.Wrappers
{
public partial class MoPaQ : IExtractable
{
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
#if NET20 || NET35 || !(WINX86 || WINX64)
Console.WriteLine("Extraction is not supported for this framework!");
Console.WriteLine();
return false;
#else
try
{
if (Filename == null || !File.Exists(Filename))
return false;
// Try to open the archive and listfile
var mpqArchive = new MpqArchive(Filename, FileAccess.Read);
string? listfile = null;
MpqFileStream listStream = mpqArchive.OpenFile("(listfile)");
// If we can't read the listfile, we just return
if (!listStream.CanRead)
return false;
// Read the listfile in for processing
using (var sr = new StreamReader(listStream))
{
listfile = sr.ReadToEnd();
}
// Split the listfile by newlines
string[] listfileLines = listfile.Replace("\r\n", "\n").Split('\n');
// Loop over each entry
foreach (string sub in listfileLines)
{
// Ensure directory separators are consistent
string filename = sub;
if (Path.DirectorySeparatorChar == '\\')
filename = filename.Replace('/', '\\');
else if (Path.DirectorySeparatorChar == '/')
filename = filename.Replace('\\', '/');
// Ensure the full output directory exists
filename = Path.Combine(outDir, filename);
var directoryName = Path.GetDirectoryName(filename);
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
// Try to write the data
try
{
mpqArchive.ExtractFile(sub, filename);
}
catch (System.Exception ex)
{
if (includeDebug) System.Console.WriteLine(ex);
}
}
return true;
}
catch (System.Exception ex)
{
if (includeDebug) System.Console.WriteLine(ex);
return false;
}
#endif
}
}
}

View File

@@ -1,14 +1,9 @@
using System;
using System.IO;
using SabreTools.Serialization.Interfaces;
using System.IO;
using SabreTools.Models.MoPaQ;
#if (NET452_OR_GREATER || NETCOREAPP) && (WINX86 || WINX64)
using StormLibSharp;
#endif
namespace SabreTools.Serialization.Wrappers
{
public partial class MoPaQ : WrapperBase<Archive>, IExtractable
public partial class MoPaQ : WrapperBase<Archive>
{
#region Descriptive Properties
@@ -100,77 +95,5 @@ namespace SabreTools.Serialization.Wrappers
}
#endregion
#region Extraction
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
#if NET20 || NET35 || !(WINX86 || WINX64)
Console.WriteLine("Extraction is not supported for this framework!");
Console.WriteLine();
return false;
#else
try
{
if (Filename == null || !File.Exists(Filename))
return false;
// Try to open the archive and listfile
var mpqArchive = new MpqArchive(Filename, FileAccess.Read);
string? listfile = null;
MpqFileStream listStream = mpqArchive.OpenFile("(listfile)");
// If we can't read the listfile, we just return
if (!listStream.CanRead)
return false;
// Read the listfile in for processing
using (var sr = new StreamReader(listStream))
{
listfile = sr.ReadToEnd();
}
// Split the listfile by newlines
string[] listfileLines = listfile.Replace("\r\n", "\n").Split('\n');
// Loop over each entry
foreach (string sub in listfileLines)
{
// Ensure directory separators are consistent
string filename = sub;
if (Path.DirectorySeparatorChar == '\\')
filename = filename.Replace('/', '\\');
else if (Path.DirectorySeparatorChar == '/')
filename = filename.Replace('\\', '/');
// Ensure the full output directory exists
filename = Path.Combine(outDir, filename);
var directoryName = Path.GetDirectoryName(filename);
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
// Try to write the data
try
{
mpqArchive.ExtractFile(sub, filename);
}
catch (System.Exception ex)
{
if (includeDebug) System.Console.WriteLine(ex);
}
}
return true;
}
catch (System.Exception ex)
{
if (includeDebug) System.Console.WriteLine(ex);
return false;
}
#endif
}
#endregion
}
}

View File

@@ -0,0 +1,226 @@
using System;
using System.IO;
using SabreTools.IO.Extensions;
using SabreTools.Matching;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public partial class NewExecutable : IExtractable
{
/// <inheritdoc/>
/// <remarks>
/// This extracts the following data:
/// - Archives and executables in the overlay
/// - Wise installers
/// </remarks>
public bool Extract(string outputDirectory, bool includeDebug)
{
bool overlay = ExtractFromOverlay(outputDirectory, includeDebug);
bool wise = ExtractWise(outputDirectory, includeDebug);
return overlay | wise;
}
/// <summary>
/// Extract data from the overlay
/// </summary>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if extraction succeeded, false otherwise</returns>
public bool ExtractFromOverlay(string outputDirectory, bool includeDebug)
{
try
{
// Cache the overlay data for easier reading
var overlayData = OverlayData;
if (overlayData.Length == 0)
return false;
// Set the output variables
int overlayOffset = 0;
string extension = string.Empty;
// Only process the overlay if it is recognized
for (; overlayOffset < 0x400 && overlayOffset < overlayData.Length; overlayOffset++)
{
int temp = overlayOffset;
byte[] overlaySample = overlayData.ReadBytes(ref temp, 0x10);
if (overlaySample.StartsWith([0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C]))
{
extension = "7z";
break;
}
else if (overlaySample.StartsWith([0x3B, 0x21, 0x40, 0x49, 0x6E, 0x73, 0x74, 0x61, 0x6C, 0x6C]))
{
// 7-zip SFX script -- ";!@Install" to ";!@InstallEnd@!"
overlayOffset = overlayData.FirstPosition([0x3B, 0x21, 0x40, 0x49, 0x6E, 0x73, 0x74, 0x61, 0x6C, 0x6C, 0x45, 0x6E, 0x64, 0x40, 0x21]);
if (overlayOffset == -1)
return false;
overlayOffset += 15;
extension = "7z";
break;
}
else if (overlaySample.StartsWith([0x42, 0x5A, 0x68]))
{
extension = "bz2";
break;
}
else if (overlaySample.StartsWith([0x1F, 0x8B]))
{
extension = "gz";
break;
}
else if (overlaySample.StartsWith(Models.MicrosoftCabinet.Constants.SignatureBytes))
{
extension = "cab";
break;
}
else if (overlaySample.StartsWith(Models.PKZIP.Constants.LocalFileHeaderSignatureBytes))
{
extension = "zip";
break;
}
else if (overlaySample.StartsWith(Models.PKZIP.Constants.EndOfCentralDirectoryRecordSignatureBytes))
{
extension = "zip";
break;
}
else if (overlaySample.StartsWith(Models.PKZIP.Constants.EndOfCentralDirectoryRecord64SignatureBytes))
{
extension = "zip";
break;
}
else if (overlaySample.StartsWith(Models.PKZIP.Constants.DataDescriptorSignatureBytes))
{
extension = "zip";
break;
}
else if (overlaySample.StartsWith([0x52, 0x61, 0x72, 0x21, 0x1A, 0x07, 0x00]))
{
extension = "rar";
break;
}
else if (overlaySample.StartsWith([0x52, 0x61, 0x72, 0x21, 0x1A, 0x07, 0x01, 0x00]))
{
extension = "rar";
break;
}
else if (overlaySample.StartsWith([0x55, 0x48, 0x41, 0x06]))
{
extension = "uha";
break;
}
else if (overlaySample.StartsWith([0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00]))
{
extension = "xz";
break;
}
else if (overlaySample.StartsWith(Models.MSDOS.Constants.SignatureBytes))
{
extension = "bin"; // exe/dll
break;
}
}
// If the extension is unset
if (extension.Length == 0)
return false;
// Create the temp filename
string tempFile = $"embedded_overlay.{extension}";
if (Filename != null)
tempFile = $"{Path.GetFileName(Filename)}-{tempFile}";
tempFile = Path.Combine(outputDirectory, tempFile);
var directoryName = Path.GetDirectoryName(tempFile);
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
// Write the resource data to a temp file
using var tempStream = File.Open(tempFile, FileMode.Create, FileAccess.Write, FileShare.ReadWrite);
tempStream?.Write(overlayData, overlayOffset, overlayData.Length - overlayOffset);
return true;
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
}
/// <summary>
/// Extract data from a Wise installer
/// </summary>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if extraction succeeded, false otherwise</returns>
public bool ExtractWise(string outputDirectory, bool includeDebug)
{
// Get the source data for reading
Stream source = _dataSource;
if (Filename != null)
{
// Try to open a multipart file
if (WiseOverlayHeader.OpenFile(Filename, includeDebug, out var temp) && temp != null)
source = temp;
}
// Try to find the overlay header
long offset = FindWiseOverlayHeader();
if (offset > 0 && offset < Length)
return ExtractWiseOverlay(outputDirectory, includeDebug, source, offset);
// Everything else could not extract
return false;
}
/// <summary>
/// Extract using Wise overlay
/// </summary>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <param name="source">Potentially multi-part stream to read</param>
/// <param name="offset">Offset to the start of the overlay header</param>
/// <returns>True if extraction succeeded, false otherwise</returns>
private bool ExtractWiseOverlay(string outputDirectory, bool includeDebug, Stream source, long offset)
{
// Seek to the overlay and parse
source.Seek(offset, SeekOrigin.Begin);
var header = WiseOverlayHeader.Create(source);
if (header == null)
{
if (includeDebug) Console.Error.WriteLine("Could not parse the overlay header");
return false;
}
// Extract the header-defined files
bool extracted = header.ExtractHeaderDefinedFiles(outputDirectory, includeDebug);
if (!extracted)
{
if (includeDebug) Console.Error.WriteLine("Could not extract header-defined files");
return false;
}
// Open the script file from the output directory
var scriptStream = File.OpenRead(Path.Combine(outputDirectory, "WiseScript.bin"));
var script = WiseScript.Create(scriptStream);
if (script == null)
{
if (includeDebug) Console.Error.WriteLine("Could not parse WiseScript.bin");
return false;
}
// Get the source directory
string? sourceDirectory = null;
if (Filename != null)
sourceDirectory = Path.GetDirectoryName(Path.GetFullPath(Filename));
// Process the state machine
return script.ProcessStateMachine(header, sourceDirectory, outputDirectory, includeDebug);
}
}
}

View File

@@ -2,13 +2,11 @@
using System.Collections.Generic;
using System.IO;
using SabreTools.IO.Extensions;
using SabreTools.Matching;
using SabreTools.Models.NewExecutable;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public class NewExecutable : WrapperBase<Executable>, IExtractable
public partial class NewExecutable : WrapperBase<Executable>
{
#region Descriptive Properties
@@ -354,225 +352,6 @@ namespace SabreTools.Serialization.Wrappers
#endregion
#region Extraction
/// <inheritdoc/>
/// <remarks>
/// This extracts the following data:
/// - Archives and executables in the overlay
/// - Wise installers
/// </remarks>
public bool Extract(string outputDirectory, bool includeDebug)
{
bool overlay = ExtractFromOverlay(outputDirectory, includeDebug);
bool wise = ExtractWise(outputDirectory, includeDebug);
return overlay | wise;
}
/// <summary>
/// Extract data from the overlay
/// </summary>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if extraction succeeded, false otherwise</returns>
public bool ExtractFromOverlay(string outputDirectory, bool includeDebug)
{
try
{
// Cache the overlay data for easier reading
var overlayData = OverlayData;
if (overlayData.Length == 0)
return false;
// Set the output variables
int overlayOffset = 0;
string extension = string.Empty;
// Only process the overlay if it is recognized
for (; overlayOffset < 0x400 && overlayOffset < overlayData.Length; overlayOffset++)
{
int temp = overlayOffset;
byte[] overlaySample = overlayData.ReadBytes(ref temp, 0x10);
if (overlaySample.StartsWith([0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C]))
{
extension = "7z";
break;
}
else if (overlaySample.StartsWith([0x3B, 0x21, 0x40, 0x49, 0x6E, 0x73, 0x74, 0x61, 0x6C, 0x6C]))
{
// 7-zip SFX script -- ";!@Install" to ";!@InstallEnd@!"
overlayOffset = overlayData.FirstPosition([0x3B, 0x21, 0x40, 0x49, 0x6E, 0x73, 0x74, 0x61, 0x6C, 0x6C, 0x45, 0x6E, 0x64, 0x40, 0x21]);
if (overlayOffset == -1)
return false;
overlayOffset += 15;
extension = "7z";
break;
}
else if (overlaySample.StartsWith([0x42, 0x5A, 0x68]))
{
extension = "bz2";
break;
}
else if (overlaySample.StartsWith([0x1F, 0x8B]))
{
extension = "gz";
break;
}
else if (overlaySample.StartsWith(Models.MicrosoftCabinet.Constants.SignatureBytes))
{
extension = "cab";
break;
}
else if (overlaySample.StartsWith(Models.PKZIP.Constants.LocalFileHeaderSignatureBytes))
{
extension = "zip";
break;
}
else if (overlaySample.StartsWith(Models.PKZIP.Constants.EndOfCentralDirectoryRecordSignatureBytes))
{
extension = "zip";
break;
}
else if (overlaySample.StartsWith(Models.PKZIP.Constants.EndOfCentralDirectoryRecord64SignatureBytes))
{
extension = "zip";
break;
}
else if (overlaySample.StartsWith(Models.PKZIP.Constants.DataDescriptorSignatureBytes))
{
extension = "zip";
break;
}
else if (overlaySample.StartsWith([0x52, 0x61, 0x72, 0x21, 0x1A, 0x07, 0x00]))
{
extension = "rar";
break;
}
else if (overlaySample.StartsWith([0x52, 0x61, 0x72, 0x21, 0x1A, 0x07, 0x01, 0x00]))
{
extension = "rar";
break;
}
else if (overlaySample.StartsWith([0x55, 0x48, 0x41, 0x06]))
{
extension = "uha";
break;
}
else if (overlaySample.StartsWith([0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00]))
{
extension = "xz";
break;
}
else if (overlaySample.StartsWith(Models.MSDOS.Constants.SignatureBytes))
{
extension = "bin"; // exe/dll
break;
}
}
// If the extension is unset
if (extension.Length == 0)
return false;
// Create the temp filename
string tempFile = $"embedded_overlay.{extension}";
if (Filename != null)
tempFile = $"{Path.GetFileName(Filename)}-{tempFile}";
tempFile = Path.Combine(outputDirectory, tempFile);
var directoryName = Path.GetDirectoryName(tempFile);
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
// Write the resource data to a temp file
using var tempStream = File.Open(tempFile, FileMode.Create, FileAccess.Write, FileShare.ReadWrite);
tempStream?.Write(overlayData, overlayOffset, overlayData.Length - overlayOffset);
return true;
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
}
/// <summary>
/// Extract data from a Wise installer
/// </summary>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if extraction succeeded, false otherwise</returns>
public bool ExtractWise(string outputDirectory, bool includeDebug)
{
// Get the source data for reading
Stream source = _dataSource;
if (Filename != null)
{
// Try to open a multipart file
if (WiseOverlayHeader.OpenFile(Filename, includeDebug, out var temp) && temp != null)
source = temp;
}
// Try to find the overlay header
long offset = FindWiseOverlayHeader();
if (offset > 0 && offset < Length)
return ExtractWiseOverlay(outputDirectory, includeDebug, source, offset);
// Everything else could not extract
return false;
}
/// <summary>
/// Extract using Wise overlay
/// </summary>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <param name="source">Potentially multi-part stream to read</param>
/// <param name="offset">Offset to the start of the overlay header</param>
/// <returns>True if extraction succeeded, false otherwise</returns>
private bool ExtractWiseOverlay(string outputDirectory, bool includeDebug, Stream source, long offset)
{
// Seek to the overlay and parse
source.Seek(offset, SeekOrigin.Begin);
var header = WiseOverlayHeader.Create(source);
if (header == null)
{
if (includeDebug) Console.Error.WriteLine("Could not parse the overlay header");
return false;
}
// Extract the header-defined files
bool extracted = header.ExtractHeaderDefinedFiles(outputDirectory, includeDebug);
if (!extracted)
{
if (includeDebug) Console.Error.WriteLine("Could not extract header-defined files");
return false;
}
// Open the script file from the output directory
var scriptStream = File.OpenRead(Path.Combine(outputDirectory, "WiseScript.bin"));
var script = WiseScript.Create(scriptStream);
if (script == null)
{
if (includeDebug) Console.Error.WriteLine("Could not parse WiseScript.bin");
return false;
}
// Get the source directory
string? sourceDirectory = null;
if (Filename != null)
sourceDirectory = Path.GetDirectoryName(Path.GetFullPath(Filename));
// Process the state machine
return script.ProcessStateMachine(header, sourceDirectory, outputDirectory, includeDebug);
}
#endregion
#region Resources
/// <summary>

View File

@@ -0,0 +1,83 @@
using System;
using System.IO;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public partial class PAK : IExtractable
{
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// If we have no directory items
if (DirectoryItems == null || DirectoryItems.Length == 0)
return false;
// Loop through and extract all files to the output
bool allExtracted = true;
for (int i = 0; i < DirectoryItems.Length; i++)
{
allExtracted &= ExtractFile(i, outputDirectory, includeDebug);
}
return allExtracted;
}
/// <summary>
/// Extract a file from the PAK to an output directory by index
/// </summary>
/// <param name="index">File index to extract</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the file extracted, false otherwise</returns>
public bool ExtractFile(int index, string outputDirectory, bool includeDebug)
{
// If we have no directory items
if (DirectoryItems == null || DirectoryItems.Length == 0)
return false;
// If the directory item index is invalid
if (index < 0 || index >= DirectoryItems.Length)
return false;
// Read the item data
var directoryItem = DirectoryItems[index];
var data = ReadRangeFromSource((int)directoryItem.ItemOffset, (int)directoryItem.ItemLength);
if (data == null)
return false;
// If we have an invalid output directory
if (string.IsNullOrEmpty(outputDirectory))
return false;
// Ensure directory separators are consistent
string filename = directoryItem.ItemName ?? $"file{index}";
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);
// Try to write the data
try
{
// Open the output file for writing
using Stream fs = System.IO.File.OpenWrite(filename);
fs.Write(data, 0, data.Length);
fs.Flush();
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
return true;
}
}
}

View File

@@ -1,12 +1,9 @@
using System;
using System.IO;
using SabreTools.IO.Extensions;
using SabreTools.Models.PAK;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public class PAK : WrapperBase<Models.PAK.File>, IExtractable
public partial class PAK : WrapperBase<Models.PAK.File>
{
#region Descriptive Properties
@@ -89,83 +86,5 @@ namespace SabreTools.Serialization.Wrappers
}
#endregion
#region Extraction
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// If we have no directory items
if (DirectoryItems == null || DirectoryItems.Length == 0)
return false;
// Loop through and extract all files to the output
bool allExtracted = true;
for (int i = 0; i < DirectoryItems.Length; i++)
{
allExtracted &= ExtractFile(i, outputDirectory, includeDebug);
}
return allExtracted;
}
/// <summary>
/// Extract a file from the PAK to an output directory by index
/// </summary>
/// <param name="index">File index to extract</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the file extracted, false otherwise</returns>
public bool ExtractFile(int index, string outputDirectory, bool includeDebug)
{
// If we have no directory items
if (DirectoryItems == null || DirectoryItems.Length == 0)
return false;
// If the directory item index is invalid
if (index < 0 || index >= DirectoryItems.Length)
return false;
// Read the item data
var directoryItem = DirectoryItems[index];
var data = ReadRangeFromSource((int)directoryItem.ItemOffset, (int)directoryItem.ItemLength);
if (data == null)
return false;
// If we have an invalid output directory
if (string.IsNullOrEmpty(outputDirectory))
return false;
// Ensure directory separators are consistent
string filename = directoryItem.ItemName ?? $"file{index}";
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);
// Try to write the data
try
{
// Open the output file for writing
using Stream fs = System.IO.File.OpenWrite(filename);
fs.Write(data, 0, data.Length);
fs.Flush();
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
return true;
}
#endregion
}
}

View File

@@ -0,0 +1,88 @@
using System;
using System.IO;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public partial class PFF : IExtractable
{
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// If we have no segments
if (Segments == null || Segments.Length == 0)
return false;
// Loop through and extract all files to the output
bool allExtracted = true;
for (int i = 0; i < Segments.Length; i++)
{
allExtracted &= ExtractSegment(i, outputDirectory, includeDebug);
}
return allExtracted;
}
/// <summary>
/// Extract a segment from the PFF to an output directory by index
/// </summary>
/// <param name="index">Segment index to extract</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the segment extracted, false otherwise</returns>
public bool ExtractSegment(int index, string outputDirectory, bool includeDebug)
{
// If we have no files
if (FileCount == 0)
return false;
// If we have no segments
if (Segments == null || Segments.Length == 0)
return false;
// If we have an invalid index
if (index < 0 || index >= Segments.Length)
return false;
// Get the read index and length
var segment = Segments[index];
int offset = (int)segment.FileLocation;
int size = (int)segment.FileSize;
try
{
// Ensure directory separators are consistent
string filename = segment.FileName ?? $"file{index}";
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);
// Create the output file
using FileStream fs = File.OpenWrite(filename);
// Read the data block
var data = ReadRangeFromSource(offset, size);
if (data == null)
return false;
// Write the data -- TODO: Compressed data?
fs.Write(data, 0, size);
fs.Flush();
return true;
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
}
}
}

View File

@@ -1,12 +1,9 @@
using System;
using System.IO;
using SabreTools.IO.Extensions;
using SabreTools.Models.PFF;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public class PFF : WrapperBase<Archive>, IExtractable
public partial class PFF : WrapperBase<Archive>
{
#region Descriptive Properties
@@ -94,88 +91,5 @@ namespace SabreTools.Serialization.Wrappers
}
#endregion
#region Extraction
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// If we have no segments
if (Segments == null || Segments.Length == 0)
return false;
// Loop through and extract all files to the output
bool allExtracted = true;
for (int i = 0; i < Segments.Length; i++)
{
allExtracted &= ExtractSegment(i, outputDirectory, includeDebug);
}
return allExtracted;
}
/// <summary>
/// Extract a segment from the PFF to an output directory by index
/// </summary>
/// <param name="index">Segment index to extract</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the segment extracted, false otherwise</returns>
public bool ExtractSegment(int index, string outputDirectory, bool includeDebug)
{
// If we have no files
if (FileCount == 0)
return false;
// If we have no segments
if (Segments == null || Segments.Length == 0)
return false;
// If we have an invalid index
if (index < 0 || index >= Segments.Length)
return false;
// Get the read index and length
var segment = Segments[index];
int offset = (int)segment.FileLocation;
int size = (int)segment.FileSize;
try
{
// Ensure directory separators are consistent
string filename = segment.FileName ?? $"file{index}";
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);
// Create the output file
using FileStream fs = File.OpenWrite(filename);
// Read the data block
var data = ReadRangeFromSource(offset, size);
if (data == null)
return false;
// Write the data -- TODO: Compressed data?
fs.Write(data, 0, size);
fs.Flush();
return true;
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
}
#endregion
}
}

View File

@@ -0,0 +1,164 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using SabreTools.Serialization.Interfaces;
#if NET462_OR_GREATER || NETCOREAPP
using SharpCompress.Archives;
using SharpCompress.Archives.Zip;
using SharpCompress.Readers;
#endif
namespace SabreTools.Serialization.Wrappers
{
public partial class PKZIP : IExtractable
{
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
=> Extract(outputDirectory, lookForHeader: false, includeDebug);
/// <inheritdoc cref="Extract(string, bool)"/>
public bool Extract(string outputDirectory, bool lookForHeader, bool includeDebug)
{
if (_dataSource == null || !_dataSource.CanRead)
return false;
#if NET462_OR_GREATER || NETCOREAPP
try
{
var readerOptions = new ReaderOptions() { LookForHeader = lookForHeader };
var zipFile = ZipArchive.Open(_dataSource, readerOptions);
// If the file exists
if (!string.IsNullOrEmpty(Filename) && File.Exists(Filename!))
{
// Find all file parts
FileInfo[] parts = [.. ArchiveFactory.GetFileParts(new FileInfo(Filename))];
// If there are multiple parts
if (parts.Length > 1)
zipFile = ZipArchive.Open(parts, readerOptions);
// Try to read the file path if no entries are found
else if (zipFile.Entries.Count == 0)
zipFile = ZipArchive.Open(parts, readerOptions);
}
foreach (var entry in zipFile.Entries)
{
try
{
// If the entry is a directory
if (entry.IsDirectory)
continue;
// If the entry has an invalid key
if (entry.Key == null)
continue;
// If the entry is partial due to an incomplete multi-part archive, skip it
if (!entry.IsComplete)
continue;
// Ensure directory separators are consistent
string filename = entry.Key;
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);
entry.WriteToFile(filename);
}
catch (System.Exception ex)
{
if (includeDebug) System.Console.Error.WriteLine(ex);
}
}
return true;
}
catch (System.Exception ex)
{
if (includeDebug) System.Console.Error.WriteLine(ex);
return false;
}
#else
Console.WriteLine("Extraction is not supported for this framework!");
Console.WriteLine();
return false;
#endif
}
/// <summary>
/// Try to find all parts of the archive, if possible
/// </summary>
/// <param name="firstPart">Path of the first archive part</param>
/// <returns>List of all found parts, if possible</returns>
public static List<string> FindParts(string firstPart)
{
// Define the regex patterns
const string zipPattern = @"^(.*\.)(zipx?|zx?[0-9]+)$";
const string genericPattern = @"^(.*\.)([0-9]+)$";
// Ensure the full path is available
firstPart = Path.GetFullPath(firstPart);
string filename = Path.GetFileName(firstPart);
string? directory = Path.GetDirectoryName(firstPart);
// Make the output list
List<string> parts = [];
// Determine which pattern is being used
Match match;
Func<int, string> nextPartFunc;
if (Regex.IsMatch(filename, zipPattern, RegexOptions.IgnoreCase))
{
match = Regex.Match(filename, zipPattern, RegexOptions.IgnoreCase);
nextPartFunc = (i) =>
{
return string.Concat(
match.Groups[1].Value,
Regex.Replace(match.Groups[2].Value, @"[^xz]", ""),
$"{i:D2}");
};
}
else if (Regex.IsMatch(filename, genericPattern, RegexOptions.IgnoreCase))
{
match = Regex.Match(filename, genericPattern, RegexOptions.IgnoreCase);
nextPartFunc = (i) =>
{
return string.Concat(
match.Groups[1].Value,
$"{i + 1}".PadLeft(match.Groups[2].Value.Length, '0')
);
};
}
else
{
return [firstPart];
}
// Loop and add the files
parts.Add(firstPart);
for (int i = 1; ; i++)
{
string nextPart = nextPartFunc(i);
if (directory != null)
nextPart = Path.Combine(directory, nextPart);
if (!File.Exists(nextPart))
break;
parts.Add(nextPart);
}
return parts;
}
}
}

View File

@@ -1,18 +1,9 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using SabreTools.Models.PKZIP;
using SabreTools.Serialization.Interfaces;
#if NET462_OR_GREATER || NETCOREAPP
using SharpCompress.Archives;
using SharpCompress.Archives.Zip;
using SharpCompress.Readers;
#endif
namespace SabreTools.Serialization.Wrappers
{
public class PKZIP : WrapperBase<Archive>, IExtractable
public partial class PKZIP : WrapperBase<Archive>
{
#region Descriptive Properties
@@ -123,157 +114,5 @@ namespace SabreTools.Serialization.Wrappers
}
#endregion
#region Extraction
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
=> Extract(outputDirectory, lookForHeader: false, includeDebug);
/// <inheritdoc cref="Extract(string, bool)"/>
public bool Extract(string outputDirectory, bool lookForHeader, bool includeDebug)
{
if (_dataSource == null || !_dataSource.CanRead)
return false;
#if NET462_OR_GREATER || NETCOREAPP
try
{
var readerOptions = new ReaderOptions() { LookForHeader = lookForHeader };
var zipFile = ZipArchive.Open(_dataSource, readerOptions);
// If the file exists
if (!string.IsNullOrEmpty(Filename) && File.Exists(Filename!))
{
// Find all file parts
FileInfo[] parts = [.. ArchiveFactory.GetFileParts(new FileInfo(Filename))];
// If there are multiple parts
if (parts.Length > 1)
zipFile = ZipArchive.Open(parts, readerOptions);
// Try to read the file path if no entries are found
else if (zipFile.Entries.Count == 0)
zipFile = ZipArchive.Open(parts, readerOptions);
}
foreach (var entry in zipFile.Entries)
{
try
{
// If the entry is a directory
if (entry.IsDirectory)
continue;
// If the entry has an invalid key
if (entry.Key == null)
continue;
// If the entry is partial due to an incomplete multi-part archive, skip it
if (!entry.IsComplete)
continue;
// Ensure directory separators are consistent
string filename = entry.Key;
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);
entry.WriteToFile(filename);
}
catch (System.Exception ex)
{
if (includeDebug) System.Console.Error.WriteLine(ex);
}
}
return true;
}
catch (System.Exception ex)
{
if (includeDebug) System.Console.Error.WriteLine(ex);
return false;
}
#else
Console.WriteLine("Extraction is not supported for this framework!");
Console.WriteLine();
return false;
#endif
}
/// <summary>
/// Try to find all parts of the archive, if possible
/// </summary>
/// <param name="firstPart">Path of the first archive part</param>
/// <returns>List of all found parts, if possible</returns>
public static List<string> FindParts(string firstPart)
{
// Define the regex patterns
const string zipPattern = @"^(.*\.)(zipx?|zx?[0-9]+)$";
const string genericPattern = @"^(.*\.)([0-9]+)$";
// Ensure the full path is available
firstPart = Path.GetFullPath(firstPart);
string filename = Path.GetFileName(firstPart);
string? directory = Path.GetDirectoryName(firstPart);
// Make the output list
List<string> parts = [];
// Determine which pattern is being used
Match match;
Func<int, string> nextPartFunc;
if (Regex.IsMatch(filename, zipPattern, RegexOptions.IgnoreCase))
{
match = Regex.Match(filename, zipPattern, RegexOptions.IgnoreCase);
nextPartFunc = (i) =>
{
return string.Concat(
match.Groups[1].Value,
Regex.Replace(match.Groups[2].Value, @"[^xz]", ""),
$"{i:D2}");
};
}
else if (Regex.IsMatch(filename, genericPattern, RegexOptions.IgnoreCase))
{
match = Regex.Match(filename, genericPattern, RegexOptions.IgnoreCase);
nextPartFunc = (i) =>
{
return string.Concat(
match.Groups[1].Value,
$"{i + 1}".PadLeft(match.Groups[2].Value.Length, '0')
);
};
}
else
{
return [firstPart];
}
// Loop and add the files
parts.Add(firstPart);
for (int i = 1; ; i++)
{
string nextPart = nextPartFunc(i);
if (directory != null)
nextPart = Path.Combine(directory, nextPart);
if (!File.Exists(nextPart))
break;
parts.Add(nextPart);
}
return parts;
}
#endregion
}
}

View File

@@ -0,0 +1,557 @@
using System;
using System.IO;
using SabreTools.IO.Compression.zlib;
using SabreTools.IO.Extensions;
using SabreTools.Matching;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public partial class PortableExecutable : IExtractable
{
/// <inheritdoc/>
/// <remarks>
/// This extracts the following data:
/// - Archives and executables in the overlay
/// - Archives and executables in resource data
/// - CExe-compressed resource data
/// - SFX archives (7z, MS-CAB, PKZIP, RAR)
/// - Wise installers
/// </remarks>
public bool Extract(string outputDirectory, bool includeDebug)
{
bool cexe = ExtractCExe(outputDirectory, includeDebug);
bool overlay = ExtractFromOverlay(outputDirectory, includeDebug);
bool resources = ExtractFromResources(outputDirectory, includeDebug);
bool wise = ExtractWise(outputDirectory, includeDebug);
return cexe || overlay || resources | wise;
}
/// <summary>
/// Extract a CExe-compressed executable
/// </summary>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if extraction succeeded, false otherwise</returns>
public bool ExtractCExe(string outputDirectory, bool includeDebug)
{
try
{
// Get all resources of type 99 with index 2
var resources = FindResourceByNamedType("99, 2");
if (resources == null || resources.Count == 0)
return false;
// Get the first resource of type 99 with index 2
var resource = resources[0];
if (resource == null || resource.Length == 0)
return false;
// Create the output data buffer
byte[]? data = [];
// If we had the decompression DLL included, it's zlib
if (FindResourceByNamedType("99, 1").Count > 0)
data = DecompressCExeZlib(resource);
else
data = DecompressCExeLZ(resource);
// If we have no data
if (data == null)
return false;
// Create the temp filename
string tempFile = string.IsNullOrEmpty(Filename) ? "temp.sxe" : $"{Path.GetFileNameWithoutExtension(Filename)}.sxe";
tempFile = Path.Combine(outputDirectory, tempFile);
var directoryName = Path.GetDirectoryName(tempFile);
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
// Write the file data to a temp file
var tempStream = File.Open(tempFile, FileMode.Create, FileAccess.Write, FileShare.ReadWrite);
tempStream.Write(data, 0, data.Length);
return true;
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
}
/// <summary>
/// Extract data from the overlay
/// </summary>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if extraction succeeded, false otherwise</returns>
public bool ExtractFromOverlay(string outputDirectory, bool includeDebug)
{
try
{
// Cache the overlay data for easier reading
var overlayData = OverlayData;
if (overlayData.Length == 0)
return false;
// Set the output variables
int overlayOffset = 0;
string extension = string.Empty;
// Only process the overlay if it is recognized
for (; overlayOffset < 0x400 && overlayOffset < overlayData.Length - 0x10; overlayOffset++)
{
int temp = overlayOffset;
byte[] overlaySample = overlayData.ReadBytes(ref temp, 0x10);
if (overlaySample.StartsWith([0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C]))
{
extension = "7z";
break;
}
else if (overlaySample.StartsWith([0x3B, 0x21, 0x40, 0x49, 0x6E, 0x73, 0x74, 0x61, 0x6C, 0x6C]))
{
// 7-zip SFX script -- ";!@Install" to ";!@InstallEnd@!"
overlayOffset = overlayData.FirstPosition([0x3B, 0x21, 0x40, 0x49, 0x6E, 0x73, 0x74, 0x61, 0x6C, 0x6C, 0x45, 0x6E, 0x64, 0x40, 0x21]);
if (overlayOffset == -1)
return false;
overlayOffset += 15;
extension = "7z";
break;
}
else if (overlaySample.StartsWith([0x42, 0x5A, 0x68]))
{
extension = "bz2";
break;
}
else if (overlaySample.StartsWith([0x1F, 0x8B]))
{
extension = "gz";
break;
}
else if (overlaySample.StartsWith(Models.MicrosoftCabinet.Constants.SignatureBytes))
{
extension = "cab";
break;
}
else if (overlaySample.StartsWith(Models.PKZIP.Constants.LocalFileHeaderSignatureBytes))
{
extension = "zip";
break;
}
else if (overlaySample.StartsWith(Models.PKZIP.Constants.EndOfCentralDirectoryRecordSignatureBytes))
{
extension = "zip";
break;
}
else if (overlaySample.StartsWith(Models.PKZIP.Constants.EndOfCentralDirectoryRecord64SignatureBytes))
{
extension = "zip";
break;
}
else if (overlaySample.StartsWith(Models.PKZIP.Constants.DataDescriptorSignatureBytes))
{
extension = "zip";
break;
}
else if (overlaySample.StartsWith([0x52, 0x61, 0x72, 0x21, 0x1A, 0x07, 0x00]))
{
extension = "rar";
break;
}
else if (overlaySample.StartsWith([0x52, 0x61, 0x72, 0x21, 0x1A, 0x07, 0x01, 0x00]))
{
extension = "rar";
break;
}
else if (overlaySample.StartsWith([0x55, 0x48, 0x41, 0x06]))
{
extension = "uha";
break;
}
else if (overlaySample.StartsWith([0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00]))
{
extension = "xz";
break;
}
else if (overlaySample.StartsWith(Models.MSDOS.Constants.SignatureBytes))
{
extension = "bin"; // exe/dll
break;
}
}
// If the extension is unset
if (extension.Length == 0)
return false;
// Create the temp filename
string tempFile = $"embedded_overlay.{extension}";
if (Filename != null)
tempFile = $"{Path.GetFileName(Filename)}-{tempFile}";
tempFile = Path.Combine(outputDirectory, tempFile);
var directoryName = Path.GetDirectoryName(tempFile);
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
// Write the resource data to a temp file
using var tempStream = File.Open(tempFile, FileMode.Create, FileAccess.Write, FileShare.ReadWrite);
tempStream?.Write(overlayData, overlayOffset, overlayData.Length - overlayOffset);
return true;
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
}
/// <summary>
/// Extract data from the resources
/// </summary>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if extraction succeeded, false otherwise</returns>
public bool ExtractFromResources(string outputDirectory, bool includeDebug)
{
try
{
// Cache the resource data for easier reading
var resourceData = ResourceData;
if (resourceData.Count == 0)
return false;
// Get the resources that have an archive signature
int i = 0;
foreach (var kvp in resourceData)
{
// Get the key and value
string resourceKey = kvp.Key;
var value = kvp.Value;
if (value == null || value is not byte[] ba || ba.Length == 0)
continue;
// Set the output variables
int resourceOffset = 0;
string extension = string.Empty;
// Only process the resource if it a recognized signature
for (; resourceOffset < 0x400 && resourceOffset < ba.Length - 0x10; resourceOffset++)
{
int temp = resourceOffset;
byte[] resourceSample = ba.ReadBytes(ref temp, 0x10);
if (resourceSample.StartsWith([0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C]))
{
extension = "7z";
break;
}
else if (resourceSample.StartsWith([0x42, 0x4D]))
{
extension = "bmp";
break;
}
else if (resourceSample.StartsWith([0x42, 0x5A, 0x68]))
{
extension = "bz2";
break;
}
else if (resourceSample.StartsWith([0x47, 0x49, 0x46, 0x38]))
{
extension = "gif";
break;
}
else if (resourceSample.StartsWith([0x1F, 0x8B]))
{
extension = "gz";
break;
}
else if (resourceSample.StartsWith([0xFF, 0xD8, 0xFF, 0xE0]))
{
extension = "jpg";
break;
}
else if (resourceSample.StartsWith([0x3C, 0x68, 0x74, 0x6D, 0x6C]))
{
extension = "html";
break;
}
else if (resourceSample.StartsWith(Models.MicrosoftCabinet.Constants.SignatureBytes))
{
extension = "cab";
break;
}
else if (resourceSample.StartsWith(Models.PKZIP.Constants.LocalFileHeaderSignatureBytes))
{
extension = "zip";
break;
}
else if (resourceSample.StartsWith(Models.PKZIP.Constants.EndOfCentralDirectoryRecordSignatureBytes))
{
extension = "zip";
break;
}
else if (resourceSample.StartsWith(Models.PKZIP.Constants.EndOfCentralDirectoryRecord64SignatureBytes))
{
extension = "zip";
break;
}
else if (resourceSample.StartsWith(Models.PKZIP.Constants.DataDescriptorSignatureBytes))
{
extension = "zip";
break;
}
else if (resourceSample.StartsWith([0x89, 0x50, 0x4E, 0x47]))
{
extension = "png";
break;
}
else if (resourceSample.StartsWith([0x52, 0x61, 0x72, 0x21, 0x1A, 0x07, 0x00]))
{
extension = "rar";
break;
}
else if (resourceSample.StartsWith([0x52, 0x61, 0x72, 0x21, 0x1A, 0x07, 0x01, 0x00]))
{
extension = "rar";
break;
}
else if (resourceSample.StartsWith([0x55, 0x48, 0x41, 0x06]))
{
extension = "uha";
break;
}
else if (resourceSample.StartsWith([0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00]))
{
extension = "xz";
break;
}
else if (resourceSample.StartsWith(Models.MSDOS.Constants.SignatureBytes))
{
extension = "bin"; // exe/dll
break;
}
}
// If the extension is unset
if (extension.Length == 0)
continue;
try
{
// Create the temp filename
string tempFile = $"embedded_resource_{i++} ({resourceKey}).{extension}";
if (Filename != null)
tempFile = $"{Path.GetFileName(Filename)}-{tempFile}";
tempFile = Path.Combine(outputDirectory, tempFile);
var directoryName = Path.GetDirectoryName(tempFile);
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
// Write the resource data to a temp file
using var tempStream = File.Open(tempFile, FileMode.Create, FileAccess.Write, FileShare.ReadWrite);
tempStream?.Write(ba, resourceOffset, ba.Length - resourceOffset);
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
}
}
return true;
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
}
/// <summary>
/// Extract data from a Wise installer
/// </summary>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if extraction succeeded, false otherwise</returns>
public bool ExtractWise(string outputDirectory, bool includeDebug)
{
// Get the source data for reading
Stream source = _dataSource;
if (Filename != null)
{
// Try to open a multipart file
if (WiseOverlayHeader.OpenFile(Filename, includeDebug, out var temp) && temp != null)
source = temp;
}
// Try to find the overlay header
long offset = FindWiseOverlayHeader();
if (offset > 0 && offset < Length)
return ExtractWiseOverlay(outputDirectory, includeDebug, source, offset);
// Try to find the section header
var section = FindWiseSection();
if (section != null)
return ExtractWiseSection(outputDirectory, includeDebug, source, section);
// Everything else could not extract
return false;
}
/// <summary>
/// Decompress CExe data compressed with LZ
/// </summary>
/// <param name="resource">Resource data to inflate</param>
/// <returns>Inflated data on success, null otherwise</returns>
private static byte[]? DecompressCExeLZ(byte[] resource)
{
try
{
var decompressor = IO.Compression.SZDD.Decompressor.CreateSZDD(resource);
using var dataStream = new MemoryStream();
decompressor.CopyTo(dataStream);
return dataStream.ToArray();
}
catch
{
// Reset the data
return null;
}
}
/// <summary>
/// Decompress CExe data compressed with zlib
/// </summary>
/// <param name="resource">Resource data to inflate</param>
/// <returns>Inflated data on success, null otherwise</returns>
private static byte[]? DecompressCExeZlib(byte[] resource)
{
try
{
// Inflate the data into the buffer
var zstream = new ZLib.z_stream_s();
byte[] data = new byte[resource.Length * 4];
unsafe
{
fixed (byte* payloadPtr = resource)
fixed (byte* dataPtr = data)
{
zstream.next_in = payloadPtr;
zstream.avail_in = (uint)resource.Length;
zstream.total_in = (uint)resource.Length;
zstream.next_out = dataPtr;
zstream.avail_out = (uint)data.Length;
zstream.total_out = 0;
ZLib.inflateInit_(zstream, ZLib.zlibVersion(), resource.Length);
int zret = ZLib.inflate(zstream, 1);
ZLib.inflateEnd(zstream);
}
}
// Trim the buffer to the proper size
uint read = zstream.total_out;
#if NETFRAMEWORK
var temp = new byte[read];
Array.Copy(data, temp, read);
data = temp;
#else
data = new ReadOnlySpan<byte>(data, 0, (int)read).ToArray();
#endif
return data;
}
catch
{
// Reset the data
return null;
}
}
/// <summary>
/// Extract using Wise overlay
/// </summary>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <param name="source">Potentially multi-part stream to read</param>
/// <param name="offset">Offset to the start of the overlay header</param>
/// <returns>True if extraction succeeded, false otherwise</returns>
private bool ExtractWiseOverlay(string outputDirectory, bool includeDebug, Stream source, long offset)
{
// Seek to the overlay and parse
source.Seek(offset, SeekOrigin.Begin);
var header = WiseOverlayHeader.Create(source);
if (header == null)
{
if (includeDebug) Console.Error.WriteLine("Could not parse the overlay header");
return false;
}
// Extract the header-defined files
bool extracted = header.ExtractHeaderDefinedFiles(outputDirectory, includeDebug);
if (!extracted)
{
if (includeDebug) Console.Error.WriteLine("Could not extract header-defined files");
return false;
}
// Open the script file from the output directory
var scriptStream = File.OpenRead(Path.Combine(outputDirectory, "WiseScript.bin"));
var script = WiseScript.Create(scriptStream);
if (script == null)
{
if (includeDebug) Console.Error.WriteLine("Could not parse WiseScript.bin");
return false;
}
// Get the source directory
string? sourceDirectory = null;
if (Filename != null)
sourceDirectory = Path.GetDirectoryName(Path.GetFullPath(Filename));
// Process the state machine
return script.ProcessStateMachine(header, sourceDirectory, outputDirectory, includeDebug);
}
/// <summary>
/// Extract using Wise section
/// </summary>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <param name="source">Potentially multi-part stream to read</param>
/// <param name="section">Wise section information</param>
/// <returns>True if extraction succeeded, false otherwise</returns>
private bool ExtractWiseSection(string outputDirectory, bool includeDebug, Stream source, Models.PortableExecutable.SectionHeader section)
{
// Get the offset
long offset = section.VirtualAddress.ConvertVirtualAddress(SectionTable);
if (offset < 0 || offset >= source.Length)
return false;
// Read the section into a local array
int sectionLength = (int)section.VirtualSize;
byte[]? sectionData;
lock (source)
{
sectionData = source.ReadFrom(offset, sectionLength, retainPosition: true);
}
// Parse the section header
var header = WiseSectionHeader.Create(sectionData, 0);
if (header == null)
{
if (includeDebug) Console.Error.WriteLine("Could not parse the section header");
return false;
}
// Attempt to extract section
return header.Extract(outputDirectory, includeDebug);
}
}
}

View File

@@ -2,15 +2,13 @@
using System.Collections.Generic;
using System.IO;
using System.Text;
using SabreTools.IO.Compression.zlib;
using SabreTools.IO.Extensions;
using SabreTools.Matching;
using SabreTools.Models.PortableExecutable.ResourceEntries;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public class PortableExecutable : WrapperBase<Models.PortableExecutable.Executable>, IExtractable
public partial class PortableExecutable : WrapperBase<Models.PortableExecutable.Executable>
{
#region Descriptive Properties
@@ -1136,555 +1134,6 @@ namespace SabreTools.Serialization.Wrappers
#endregion
#region Extraction
/// <inheritdoc/>
/// <remarks>
/// This extracts the following data:
/// - Archives and executables in the overlay
/// - Archives and executables in resource data
/// - CExe-compressed resource data
/// - SFX archives (7z, MS-CAB, PKZIP, RAR)
/// - Wise installers
/// </remarks>
public bool Extract(string outputDirectory, bool includeDebug)
{
bool cexe = ExtractCExe(outputDirectory, includeDebug);
bool overlay = ExtractFromOverlay(outputDirectory, includeDebug);
bool resources = ExtractFromResources(outputDirectory, includeDebug);
bool wise = ExtractWise(outputDirectory, includeDebug);
return cexe || overlay || resources | wise;
}
/// <summary>
/// Extract a CExe-compressed executable
/// </summary>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if extraction succeeded, false otherwise</returns>
public bool ExtractCExe(string outputDirectory, bool includeDebug)
{
try
{
// Get all resources of type 99 with index 2
var resources = FindResourceByNamedType("99, 2");
if (resources == null || resources.Count == 0)
return false;
// Get the first resource of type 99 with index 2
var resource = resources[0];
if (resource == null || resource.Length == 0)
return false;
// Create the output data buffer
byte[]? data = [];
// If we had the decompression DLL included, it's zlib
if (FindResourceByNamedType("99, 1").Count > 0)
data = DecompressCExeZlib(resource);
else
data = DecompressCExeLZ(resource);
// If we have no data
if (data == null)
return false;
// Create the temp filename
string tempFile = string.IsNullOrEmpty(Filename) ? "temp.sxe" : $"{Path.GetFileNameWithoutExtension(Filename)}.sxe";
tempFile = Path.Combine(outputDirectory, tempFile);
var directoryName = Path.GetDirectoryName(tempFile);
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
// Write the file data to a temp file
var tempStream = File.Open(tempFile, FileMode.Create, FileAccess.Write, FileShare.ReadWrite);
tempStream.Write(data, 0, data.Length);
return true;
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
}
/// <summary>
/// Extract data from the overlay
/// </summary>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if extraction succeeded, false otherwise</returns>
public bool ExtractFromOverlay(string outputDirectory, bool includeDebug)
{
try
{
// Cache the overlay data for easier reading
var overlayData = OverlayData;
if (overlayData.Length == 0)
return false;
// Set the output variables
int overlayOffset = 0;
string extension = string.Empty;
// Only process the overlay if it is recognized
for (; overlayOffset < 0x400 && overlayOffset < overlayData.Length - 0x10; overlayOffset++)
{
int temp = overlayOffset;
byte[] overlaySample = overlayData.ReadBytes(ref temp, 0x10);
if (overlaySample.StartsWith([0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C]))
{
extension = "7z";
break;
}
else if (overlaySample.StartsWith([0x3B, 0x21, 0x40, 0x49, 0x6E, 0x73, 0x74, 0x61, 0x6C, 0x6C]))
{
// 7-zip SFX script -- ";!@Install" to ";!@InstallEnd@!"
overlayOffset = overlayData.FirstPosition([0x3B, 0x21, 0x40, 0x49, 0x6E, 0x73, 0x74, 0x61, 0x6C, 0x6C, 0x45, 0x6E, 0x64, 0x40, 0x21]);
if (overlayOffset == -1)
return false;
overlayOffset += 15;
extension = "7z";
break;
}
else if (overlaySample.StartsWith([0x42, 0x5A, 0x68]))
{
extension = "bz2";
break;
}
else if (overlaySample.StartsWith([0x1F, 0x8B]))
{
extension = "gz";
break;
}
else if (overlaySample.StartsWith(Models.MicrosoftCabinet.Constants.SignatureBytes))
{
extension = "cab";
break;
}
else if (overlaySample.StartsWith(Models.PKZIP.Constants.LocalFileHeaderSignatureBytes))
{
extension = "zip";
break;
}
else if (overlaySample.StartsWith(Models.PKZIP.Constants.EndOfCentralDirectoryRecordSignatureBytes))
{
extension = "zip";
break;
}
else if (overlaySample.StartsWith(Models.PKZIP.Constants.EndOfCentralDirectoryRecord64SignatureBytes))
{
extension = "zip";
break;
}
else if (overlaySample.StartsWith(Models.PKZIP.Constants.DataDescriptorSignatureBytes))
{
extension = "zip";
break;
}
else if (overlaySample.StartsWith([0x52, 0x61, 0x72, 0x21, 0x1A, 0x07, 0x00]))
{
extension = "rar";
break;
}
else if (overlaySample.StartsWith([0x52, 0x61, 0x72, 0x21, 0x1A, 0x07, 0x01, 0x00]))
{
extension = "rar";
break;
}
else if (overlaySample.StartsWith([0x55, 0x48, 0x41, 0x06]))
{
extension = "uha";
break;
}
else if (overlaySample.StartsWith([0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00]))
{
extension = "xz";
break;
}
else if (overlaySample.StartsWith(Models.MSDOS.Constants.SignatureBytes))
{
extension = "bin"; // exe/dll
break;
}
}
// If the extension is unset
if (extension.Length == 0)
return false;
// Create the temp filename
string tempFile = $"embedded_overlay.{extension}";
if (Filename != null)
tempFile = $"{Path.GetFileName(Filename)}-{tempFile}";
tempFile = Path.Combine(outputDirectory, tempFile);
var directoryName = Path.GetDirectoryName(tempFile);
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
// Write the resource data to a temp file
using var tempStream = File.Open(tempFile, FileMode.Create, FileAccess.Write, FileShare.ReadWrite);
tempStream?.Write(overlayData, overlayOffset, overlayData.Length - overlayOffset);
return true;
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
}
/// <summary>
/// Extract data from the resources
/// </summary>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if extraction succeeded, false otherwise</returns>
public bool ExtractFromResources(string outputDirectory, bool includeDebug)
{
try
{
// Cache the resource data for easier reading
var resourceData = ResourceData;
if (resourceData.Count == 0)
return false;
// Get the resources that have an archive signature
int i = 0;
foreach (var kvp in resourceData)
{
// Get the key and value
string resourceKey = kvp.Key;
var value = kvp.Value;
if (value == null || value is not byte[] ba || ba.Length == 0)
continue;
// Set the output variables
int resourceOffset = 0;
string extension = string.Empty;
// Only process the resource if it a recognized signature
for (; resourceOffset < 0x400 && resourceOffset < ba.Length - 0x10; resourceOffset++)
{
int temp = resourceOffset;
byte[] resourceSample = ba.ReadBytes(ref temp, 0x10);
if (resourceSample.StartsWith([0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C]))
{
extension = "7z";
break;
}
else if (resourceSample.StartsWith([0x42, 0x4D]))
{
extension = "bmp";
break;
}
else if (resourceSample.StartsWith([0x42, 0x5A, 0x68]))
{
extension = "bz2";
break;
}
else if (resourceSample.StartsWith([0x47, 0x49, 0x46, 0x38]))
{
extension = "gif";
break;
}
else if (resourceSample.StartsWith([0x1F, 0x8B]))
{
extension = "gz";
break;
}
else if (resourceSample.StartsWith([0xFF, 0xD8, 0xFF, 0xE0]))
{
extension = "jpg";
break;
}
else if (resourceSample.StartsWith([0x3C, 0x68, 0x74, 0x6D, 0x6C]))
{
extension = "html";
break;
}
else if (resourceSample.StartsWith(Models.MicrosoftCabinet.Constants.SignatureBytes))
{
extension = "cab";
break;
}
else if (resourceSample.StartsWith(Models.PKZIP.Constants.LocalFileHeaderSignatureBytes))
{
extension = "zip";
break;
}
else if (resourceSample.StartsWith(Models.PKZIP.Constants.EndOfCentralDirectoryRecordSignatureBytes))
{
extension = "zip";
break;
}
else if (resourceSample.StartsWith(Models.PKZIP.Constants.EndOfCentralDirectoryRecord64SignatureBytes))
{
extension = "zip";
break;
}
else if (resourceSample.StartsWith(Models.PKZIP.Constants.DataDescriptorSignatureBytes))
{
extension = "zip";
break;
}
else if (resourceSample.StartsWith([0x89, 0x50, 0x4E, 0x47]))
{
extension = "png";
break;
}
else if (resourceSample.StartsWith([0x52, 0x61, 0x72, 0x21, 0x1A, 0x07, 0x00]))
{
extension = "rar";
break;
}
else if (resourceSample.StartsWith([0x52, 0x61, 0x72, 0x21, 0x1A, 0x07, 0x01, 0x00]))
{
extension = "rar";
break;
}
else if (resourceSample.StartsWith([0x55, 0x48, 0x41, 0x06]))
{
extension = "uha";
break;
}
else if (resourceSample.StartsWith([0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00]))
{
extension = "xz";
break;
}
else if (resourceSample.StartsWith(Models.MSDOS.Constants.SignatureBytes))
{
extension = "bin"; // exe/dll
break;
}
}
// If the extension is unset
if (extension.Length == 0)
continue;
try
{
// Create the temp filename
string tempFile = $"embedded_resource_{i++} ({resourceKey}).{extension}";
if (Filename != null)
tempFile = $"{Path.GetFileName(Filename)}-{tempFile}";
tempFile = Path.Combine(outputDirectory, tempFile);
var directoryName = Path.GetDirectoryName(tempFile);
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
// Write the resource data to a temp file
using var tempStream = File.Open(tempFile, FileMode.Create, FileAccess.Write, FileShare.ReadWrite);
tempStream?.Write(ba, resourceOffset, ba.Length - resourceOffset);
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
}
}
return true;
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
}
/// <summary>
/// Extract data from a Wise installer
/// </summary>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if extraction succeeded, false otherwise</returns>
public bool ExtractWise(string outputDirectory, bool includeDebug)
{
// Get the source data for reading
Stream source = _dataSource;
if (Filename != null)
{
// Try to open a multipart file
if (WiseOverlayHeader.OpenFile(Filename, includeDebug, out var temp) && temp != null)
source = temp;
}
// Try to find the overlay header
long offset = FindWiseOverlayHeader();
if (offset > 0 && offset < Length)
return ExtractWiseOverlay(outputDirectory, includeDebug, source, offset);
// Try to find the section header
var section = FindWiseSection();
if (section != null)
return ExtractWiseSection(outputDirectory, includeDebug, source, section);
// Everything else could not extract
return false;
}
/// <summary>
/// Decompress CExe data compressed with LZ
/// </summary>
/// <param name="resource">Resource data to inflate</param>
/// <returns>Inflated data on success, null otherwise</returns>
private static byte[]? DecompressCExeLZ(byte[] resource)
{
try
{
var decompressor = IO.Compression.SZDD.Decompressor.CreateSZDD(resource);
using var dataStream = new MemoryStream();
decompressor.CopyTo(dataStream);
return dataStream.ToArray();
}
catch
{
// Reset the data
return null;
}
}
/// <summary>
/// Decompress CExe data compressed with zlib
/// </summary>
/// <param name="resource">Resource data to inflate</param>
/// <returns>Inflated data on success, null otherwise</returns>
private static byte[]? DecompressCExeZlib(byte[] resource)
{
try
{
// Inflate the data into the buffer
var zstream = new ZLib.z_stream_s();
byte[] data = new byte[resource.Length * 4];
unsafe
{
fixed (byte* payloadPtr = resource)
fixed (byte* dataPtr = data)
{
zstream.next_in = payloadPtr;
zstream.avail_in = (uint)resource.Length;
zstream.total_in = (uint)resource.Length;
zstream.next_out = dataPtr;
zstream.avail_out = (uint)data.Length;
zstream.total_out = 0;
ZLib.inflateInit_(zstream, ZLib.zlibVersion(), resource.Length);
int zret = ZLib.inflate(zstream, 1);
ZLib.inflateEnd(zstream);
}
}
// Trim the buffer to the proper size
uint read = zstream.total_out;
#if NETFRAMEWORK
var temp = new byte[read];
Array.Copy(data, temp, read);
data = temp;
#else
data = new ReadOnlySpan<byte>(data, 0, (int)read).ToArray();
#endif
return data;
}
catch
{
// Reset the data
return null;
}
}
/// <summary>
/// Extract using Wise overlay
/// </summary>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <param name="source">Potentially multi-part stream to read</param>
/// <param name="offset">Offset to the start of the overlay header</param>
/// <returns>True if extraction succeeded, false otherwise</returns>
private bool ExtractWiseOverlay(string outputDirectory, bool includeDebug, Stream source, long offset)
{
// Seek to the overlay and parse
source.Seek(offset, SeekOrigin.Begin);
var header = WiseOverlayHeader.Create(source);
if (header == null)
{
if (includeDebug) Console.Error.WriteLine("Could not parse the overlay header");
return false;
}
// Extract the header-defined files
bool extracted = header.ExtractHeaderDefinedFiles(outputDirectory, includeDebug);
if (!extracted)
{
if (includeDebug) Console.Error.WriteLine("Could not extract header-defined files");
return false;
}
// Open the script file from the output directory
var scriptStream = File.OpenRead(Path.Combine(outputDirectory, "WiseScript.bin"));
var script = WiseScript.Create(scriptStream);
if (script == null)
{
if (includeDebug) Console.Error.WriteLine("Could not parse WiseScript.bin");
return false;
}
// Get the source directory
string? sourceDirectory = null;
if (Filename != null)
sourceDirectory = Path.GetDirectoryName(Path.GetFullPath(Filename));
// Process the state machine
return script.ProcessStateMachine(header, sourceDirectory, outputDirectory, includeDebug);
}
/// <summary>
/// Extract using Wise section
/// </summary>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <param name="source">Potentially multi-part stream to read</param>
/// <param name="section">Wise section information</param>
/// <returns>True if extraction succeeded, false otherwise</returns>
private bool ExtractWiseSection(string outputDirectory, bool includeDebug, Stream source, Models.PortableExecutable.SectionHeader section)
{
// Get the offset
long offset = section.VirtualAddress.ConvertVirtualAddress(SectionTable);
if (offset < 0 || offset >= source.Length)
return false;
// Read the section into a local array
int sectionLength = (int)section.VirtualSize;
byte[]? sectionData;
lock (source)
{
sectionData = source.ReadFrom(offset, sectionLength, retainPosition: true);
}
// Parse the section header
var header = WiseSectionHeader.Create(sectionData, 0);
if (header == null)
{
if (includeDebug) Console.Error.WriteLine("Could not parse the section header");
return false;
}
// Attempt to extract section
return header.Extract(outputDirectory, includeDebug);
}
#endregion
#region Resource Data
/// <summary>

View File

@@ -0,0 +1,116 @@
using System;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public partial class Quantum : IExtractable
{
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// If we have no files
if (FileList == null || FileList.Length == 0)
return false;
// Loop through and extract all files to the output
bool allExtracted = true;
for (int i = 0; i < FileList.Length; i++)
{
allExtracted &= ExtractFile(i, outputDirectory, includeDebug);
}
return allExtracted;
}
/// <summary>
/// Extract a file from the Quantum archive to an output directory by index
/// </summary>
/// <param name="index">File index to extract</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the file extracted, false otherwise</returns>
public bool ExtractFile(int index, string outputDirectory, bool includeDebug)
{
// If we have no files
if (Header == null || FileCount == 0 || FileList == null || FileList.Length == 0)
return false;
// If we have an invalid index
if (index < 0 || index >= FileList.Length)
return false;
// Get the file information
var fileDescriptor = FileList[index];
// Read the entire compressed data
int compressedDataOffset = (int)CompressedDataOffset;
long compressedDataLength = Length - compressedDataOffset;
var compressedData = ReadRangeFromSource(compressedDataOffset, (int)compressedDataLength);
// Print a debug reminder
if (includeDebug) Console.WriteLine("Quantum archive extraction is unsupported");
// TODO: Figure out decompression
// - Single-file archives seem to work
// - Single-file archives with files that span a window boundary seem to work
// - The first files in each archive seem to work
return false;
// // Setup the decompression state
// State state = new State();
// Decompressor.InitState(state, TableSize, CompressionFlags);
// // Decompress the entire array
// int decompressedDataLength = (int)FileList.Sum(fd => fd.ExpandedFileSize);
// byte[] decompressedData = new byte[decompressedDataLength];
// Decompressor.Decompress(state, compressedData.Length, compressedData, decompressedData.Length, decompressedData);
// // Read the data
// int offset = (int)FileList.Take(index).Sum(fd => fd.ExpandedFileSize);
// byte[] data = new byte[fileDescriptor.ExpandedFileSize];
// Array.Copy(decompressedData, offset, data, 0, data.Length);
// // Loop through all files before the current
// for (int i = 0; i < index; i++)
// {
// // Decompress the next block of data
// byte[] tempData = new byte[FileList[i].ExpandedFileSize];
// int lastRead = Decompressor.Decompress(state, compressedData.Length, compressedData, tempData.Length, tempData);
// compressedData = new ReadOnlySpan<byte>(compressedData, (lastRead), compressedData.Length - (lastRead)).ToArray();
// }
// // Read the data
// byte[] data = new byte[fileDescriptor.ExpandedFileSize];
// _ = Decompressor.Decompress(state, compressedData.Length, compressedData, data.Length, data);
// // Create the filename
// string filename = fileDescriptor.FileName;
// // If we have an invalid output directory
// if (string.IsNullOrEmpty(outputDirectory))
// return false;
// // Create the full output path
// filename = Path.Combine(outputDirectory, filename);
// // Ensure the output directory is created
// Directory.CreateDirectory(Path.GetDirectoryName(filename));
// // Try to write the data
// try
// {
// // Open the output file for writing
// using (Stream fs = File.OpenWrite(filename))
// {
// fs.Write(data, 0, data.Length);
// }
// }
// catch
// {
// return false;
// }
// return true;
}
}
}

View File

@@ -1,12 +1,9 @@
using System;
using System.IO;
using SabreTools.IO.Extensions;
using SabreTools.Models.Quantum;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public class Quantum : WrapperBase<Archive>, IExtractable
public partial class Quantum : WrapperBase<Archive>
{
#region Descriptive Properties
@@ -98,117 +95,5 @@ namespace SabreTools.Serialization.Wrappers
}
#endregion
#region Extraction
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// If we have no files
if (FileList == null || FileList.Length == 0)
return false;
// Loop through and extract all files to the output
bool allExtracted = true;
for (int i = 0; i < FileList.Length; i++)
{
allExtracted &= ExtractFile(i, outputDirectory, includeDebug);
}
return allExtracted;
}
/// <summary>
/// Extract a file from the Quantum archive to an output directory by index
/// </summary>
/// <param name="index">File index to extract</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the file extracted, false otherwise</returns>
public bool ExtractFile(int index, string outputDirectory, bool includeDebug)
{
// If we have no files
if (Header == null || FileCount == 0 || FileList == null || FileList.Length == 0)
return false;
// If we have an invalid index
if (index < 0 || index >= FileList.Length)
return false;
// Get the file information
var fileDescriptor = FileList[index];
// Read the entire compressed data
int compressedDataOffset = (int)CompressedDataOffset;
long compressedDataLength = Length - compressedDataOffset;
var compressedData = ReadRangeFromSource(compressedDataOffset, (int)compressedDataLength);
// Print a debug reminder
if (includeDebug) Console.WriteLine("Quantum archive extraction is unsupported");
// TODO: Figure out decompression
// - Single-file archives seem to work
// - Single-file archives with files that span a window boundary seem to work
// - The first files in each archive seem to work
return false;
// // Setup the decompression state
// State state = new State();
// Decompressor.InitState(state, TableSize, CompressionFlags);
// // Decompress the entire array
// int decompressedDataLength = (int)FileList.Sum(fd => fd.ExpandedFileSize);
// byte[] decompressedData = new byte[decompressedDataLength];
// Decompressor.Decompress(state, compressedData.Length, compressedData, decompressedData.Length, decompressedData);
// // Read the data
// int offset = (int)FileList.Take(index).Sum(fd => fd.ExpandedFileSize);
// byte[] data = new byte[fileDescriptor.ExpandedFileSize];
// Array.Copy(decompressedData, offset, data, 0, data.Length);
// // Loop through all files before the current
// for (int i = 0; i < index; i++)
// {
// // Decompress the next block of data
// byte[] tempData = new byte[FileList[i].ExpandedFileSize];
// int lastRead = Decompressor.Decompress(state, compressedData.Length, compressedData, tempData.Length, tempData);
// compressedData = new ReadOnlySpan<byte>(compressedData, (lastRead), compressedData.Length - (lastRead)).ToArray();
// }
// // Read the data
// byte[] data = new byte[fileDescriptor.ExpandedFileSize];
// _ = Decompressor.Decompress(state, compressedData.Length, compressedData, data.Length, data);
// // Create the filename
// string filename = fileDescriptor.FileName;
// // If we have an invalid output directory
// if (string.IsNullOrEmpty(outputDirectory))
// return false;
// // Create the full output path
// filename = Path.Combine(outputDirectory, filename);
// // Ensure the output directory is created
// Directory.CreateDirectory(Path.GetDirectoryName(filename));
// // Try to write the data
// try
// {
// // Open the output file for writing
// using (Stream fs = File.OpenWrite(filename))
// {
// fs.Write(data, 0, data.Length);
// }
// }
// catch
// {
// return false;
// }
// return true;
}
#endregion
}
}

View File

@@ -0,0 +1,222 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using SabreTools.Serialization.Interfaces;
#if NET462_OR_GREATER || NETCOREAPP
using SharpCompress.Archives;
using SharpCompress.Archives.Rar;
using SharpCompress.Common;
using SharpCompress.Readers;
#endif
namespace SabreTools.Serialization.Wrappers
{
/// <summary>
/// This is a shell wrapper; one that does not contain
/// any actual parsing. It is used as a placeholder for
/// types that typically do not have models.
/// </summary>
public partial class RAR : IExtractable
{
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
=> Extract(outputDirectory, lookForHeader: false, includeDebug);
/// <inheritdoc cref="Extract(string, bool)"/>
public bool Extract(string outputDirectory, bool lookForHeader, bool includeDebug)
{
if (_dataSource == null || !_dataSource.CanRead)
return false;
#if NET462_OR_GREATER || NETCOREAPP
try
{
var readerOptions = new ReaderOptions() { LookForHeader = lookForHeader };
RarArchive rarFile = RarArchive.Open(_dataSource, readerOptions);
// If the file exists
if (!string.IsNullOrEmpty(Filename) && File.Exists(Filename!))
{
// Find all file parts
FileInfo[] parts = [.. ArchiveFactory.GetFileParts(new FileInfo(Filename))];
// If there are multiple parts
if (parts.Length > 1)
rarFile = RarArchive.Open(parts, readerOptions);
// Try to read the file path if no entries are found
else if (rarFile.Entries.Count == 0)
rarFile = RarArchive.Open(parts, readerOptions);
}
if (rarFile.IsSolid)
return ExtractSolid(rarFile, outputDirectory, includeDebug);
else
return ExtractNonSolid(rarFile, outputDirectory, includeDebug);
}
catch (System.Exception ex)
{
if (includeDebug) System.Console.Error.WriteLine(ex);
return false;
}
#else
Console.WriteLine("Extraction is not supported for this framework!");
Console.WriteLine();
return false;
#endif
}
/// <summary>
/// Try to find all parts of the archive, if possible
/// </summary>
/// <param name="firstPart">Path of the first archive part</param>
/// <returns>List of all found parts, if possible</returns>
public static List<string> FindParts(string firstPart)
{
// Define the regex patterns
const string rarNewPattern = @"^(.*\.part)([0-9]+)(\.rar)$";
const string rarOldPattern = @"^(.*\.)([r-z{])(ar|[0-9]+)$";
const string genericPattern = @"^(.*\.)([0-9]+)$";
// Ensure the full path is available
firstPart = Path.GetFullPath(firstPart);
string filename = Path.GetFileName(firstPart);
string? directory = Path.GetDirectoryName(firstPart);
// Make the output list
List<string> parts = [];
// Determine which pattern is being used
Match match;
Func<int, string> nextPartFunc;
if (Regex.IsMatch(filename, rarNewPattern, RegexOptions.IgnoreCase))
{
match = Regex.Match(filename, rarNewPattern, RegexOptions.IgnoreCase);
nextPartFunc = (i) =>
{
return string.Concat(
match.Groups[1].Value,
$"{i + 1}".PadLeft(match.Groups[2].Value.Length, '0'),
match.Groups[3].Value);
};
}
else if (Regex.IsMatch(filename, rarOldPattern, RegexOptions.IgnoreCase))
{
match = Regex.Match(filename, rarOldPattern, RegexOptions.IgnoreCase);
nextPartFunc = (i) =>
{
return string.Concat(
match.Groups[1].Value,
(char)(match.Groups[2].Value[0] + ((i - 1) / 100))
+ (i - 1).ToString("D4").Substring(2));
};
}
else if (Regex.IsMatch(filename, genericPattern, RegexOptions.IgnoreCase))
{
match = Regex.Match(filename, genericPattern, RegexOptions.IgnoreCase);
nextPartFunc = (i) =>
{
return string.Concat(
match.Groups[1].Value,
$"{i + 1}".PadLeft(match.Groups[2].Value.Length, '0')
);
};
}
else
{
return [firstPart];
}
// Loop and add the files
parts.Add(firstPart);
for (int i = 1; ; i++)
{
string nextPart = nextPartFunc(i);
if (directory != null)
nextPart = Path.Combine(directory, nextPart);
if (!File.Exists(nextPart))
break;
parts.Add(nextPart);
}
return parts;
}
#if NET462_OR_GREATER || NETCOREAPP
/// <summary>
/// Extraction method for non-solid archives. This iterates over each entry in the archive to extract every
/// file individually, in order to extract all valid files from the archive.
/// </summary>
private static bool ExtractNonSolid(RarArchive rarFile, string outDir, bool includeDebug)
{
foreach (var entry in rarFile.Entries)
{
try
{
// If the entry is a directory
if (entry.IsDirectory)
continue;
// If the entry has an invalid key
if (entry.Key == null)
continue;
// If we have a partial entry due to an incomplete multi-part archive, skip it
if (!entry.IsComplete)
continue;
// Ensure directory separators are consistent
string filename = entry.Key;
if (Path.DirectorySeparatorChar == '\\')
filename = filename.Replace('/', '\\');
else if (Path.DirectorySeparatorChar == '/')
filename = filename.Replace('\\', '/');
// Ensure the full output directory exists
filename = Path.Combine(outDir, filename);
var directoryName = Path.GetDirectoryName(filename);
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
entry.WriteToFile(filename);
}
catch (System.Exception ex)
{
if (includeDebug) System.Console.Error.WriteLine(ex);
}
}
return true;
}
/// <summary>
/// Extraction method for solid archives. Uses ExtractAllEntries because extraction for solid archives must be
/// done sequentially, and files beyond a corrupted point in a solid archive will be unreadable anyways.
/// </summary>
private static bool ExtractSolid(RarArchive rarFile, string outDir, bool includeDebug)
{
try
{
if (!Directory.Exists(outDir))
Directory.CreateDirectory(outDir);
rarFile.WriteToDirectory(outDir, new ExtractionOptions()
{
ExtractFullPath = true,
Overwrite = true,
});
}
catch (System.Exception ex)
{
if (includeDebug) System.Console.Error.WriteLine(ex);
}
return true;
}
#endif
}
}

View File

@@ -1,14 +1,4 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using SabreTools.Serialization.Interfaces;
#if NET462_OR_GREATER || NETCOREAPP
using SharpCompress.Archives;
using SharpCompress.Archives.Rar;
using SharpCompress.Common;
using SharpCompress.Readers;
#endif
namespace SabreTools.Serialization.Wrappers
{
@@ -17,7 +7,7 @@ namespace SabreTools.Serialization.Wrappers
/// any actual parsing. It is used as a placeholder for
/// types that typically do not have models.
/// </summary>
public class RAR : WrapperBase, IExtractable
public partial class RAR : WrapperBase
{
#region Descriptive Properties
@@ -87,210 +77,5 @@ namespace SabreTools.Serialization.Wrappers
#endif
#endregion
#region Extraction
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
=> Extract(outputDirectory, lookForHeader: false, includeDebug);
/// <inheritdoc cref="Extract(string, bool)"/>
public bool Extract(string outputDirectory, bool lookForHeader, bool includeDebug)
{
if (_dataSource == null || !_dataSource.CanRead)
return false;
#if NET462_OR_GREATER || NETCOREAPP
try
{
var readerOptions = new ReaderOptions() { LookForHeader = lookForHeader };
RarArchive rarFile = RarArchive.Open(_dataSource, readerOptions);
// If the file exists
if (!string.IsNullOrEmpty(Filename) && File.Exists(Filename!))
{
// Find all file parts
FileInfo[] parts = [.. ArchiveFactory.GetFileParts(new FileInfo(Filename))];
// If there are multiple parts
if (parts.Length > 1)
rarFile = RarArchive.Open(parts, readerOptions);
// Try to read the file path if no entries are found
else if (rarFile.Entries.Count == 0)
rarFile = RarArchive.Open(parts, readerOptions);
}
if (rarFile.IsSolid)
return ExtractSolid(rarFile, outputDirectory, includeDebug);
else
return ExtractNonSolid(rarFile, outputDirectory, includeDebug);
}
catch (System.Exception ex)
{
if (includeDebug) System.Console.Error.WriteLine(ex);
return false;
}
#else
Console.WriteLine("Extraction is not supported for this framework!");
Console.WriteLine();
return false;
#endif
}
/// <summary>
/// Try to find all parts of the archive, if possible
/// </summary>
/// <param name="firstPart">Path of the first archive part</param>
/// <returns>List of all found parts, if possible</returns>
public static List<string> FindParts(string firstPart)
{
// Define the regex patterns
const string rarNewPattern = @"^(.*\.part)([0-9]+)(\.rar)$";
const string rarOldPattern = @"^(.*\.)([r-z{])(ar|[0-9]+)$";
const string genericPattern = @"^(.*\.)([0-9]+)$";
// Ensure the full path is available
firstPart = Path.GetFullPath(firstPart);
string filename = Path.GetFileName(firstPart);
string? directory = Path.GetDirectoryName(firstPart);
// Make the output list
List<string> parts = [];
// Determine which pattern is being used
Match match;
Func<int, string> nextPartFunc;
if (Regex.IsMatch(filename, rarNewPattern, RegexOptions.IgnoreCase))
{
match = Regex.Match(filename, rarNewPattern, RegexOptions.IgnoreCase);
nextPartFunc = (i) =>
{
return string.Concat(
match.Groups[1].Value,
$"{i + 1}".PadLeft(match.Groups[2].Value.Length, '0'),
match.Groups[3].Value);
};
}
else if (Regex.IsMatch(filename, rarOldPattern, RegexOptions.IgnoreCase))
{
match = Regex.Match(filename, rarOldPattern, RegexOptions.IgnoreCase);
nextPartFunc = (i) =>
{
return string.Concat(
match.Groups[1].Value,
(char)(match.Groups[2].Value[0] + ((i - 1) / 100))
+ (i - 1).ToString("D4").Substring(2));
};
}
else if (Regex.IsMatch(filename, genericPattern, RegexOptions.IgnoreCase))
{
match = Regex.Match(filename, genericPattern, RegexOptions.IgnoreCase);
nextPartFunc = (i) =>
{
return string.Concat(
match.Groups[1].Value,
$"{i + 1}".PadLeft(match.Groups[2].Value.Length, '0')
);
};
}
else
{
return [firstPart];
}
// Loop and add the files
parts.Add(firstPart);
for (int i = 1; ; i++)
{
string nextPart = nextPartFunc(i);
if (directory != null)
nextPart = Path.Combine(directory, nextPart);
if (!File.Exists(nextPart))
break;
parts.Add(nextPart);
}
return parts;
}
#if NET462_OR_GREATER || NETCOREAPP
/// <summary>
/// Extraction method for non-solid archives. This iterates over each entry in the archive to extract every
/// file individually, in order to extract all valid files from the archive.
/// </summary>
private static bool ExtractNonSolid(RarArchive rarFile, string outDir, bool includeDebug)
{
foreach (var entry in rarFile.Entries)
{
try
{
// If the entry is a directory
if (entry.IsDirectory)
continue;
// If the entry has an invalid key
if (entry.Key == null)
continue;
// If we have a partial entry due to an incomplete multi-part archive, skip it
if (!entry.IsComplete)
continue;
// Ensure directory separators are consistent
string filename = entry.Key;
if (Path.DirectorySeparatorChar == '\\')
filename = filename.Replace('/', '\\');
else if (Path.DirectorySeparatorChar == '/')
filename = filename.Replace('\\', '/');
// Ensure the full output directory exists
filename = Path.Combine(outDir, filename);
var directoryName = Path.GetDirectoryName(filename);
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
entry.WriteToFile(filename);
}
catch (System.Exception ex)
{
if (includeDebug) System.Console.Error.WriteLine(ex);
}
}
return true;
}
/// <summary>
/// Extraction method for solid archives. Uses ExtractAllEntries because extraction for solid archives must be
/// done sequentially, and files beyond a corrupted point in a solid archive will be unreadable anyways.
/// </summary>
private static bool ExtractSolid(RarArchive rarFile, string outDir, bool includeDebug)
{
try
{
if (!Directory.Exists(outDir))
Directory.CreateDirectory(outDir);
rarFile.WriteToDirectory(outDir, new ExtractionOptions()
{
ExtractFullPath = true,
Overwrite = true,
});
}
catch (System.Exception ex)
{
if (includeDebug) System.Console.Error.WriteLine(ex);
}
return true;
}
#endif
#endregion
}
}

View File

@@ -0,0 +1,152 @@
using System;
using System.Collections.Generic;
using System.IO;
using SabreTools.IO.Compression.zlib;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public partial class SGA : IExtractable
{
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// Get the file count
int fileCount = FileCount;
if (fileCount == 0)
return false;
// Loop through and extract all files to the output
bool allExtracted = true;
for (int i = 0; i < fileCount; i++)
{
allExtracted &= ExtractFile(i, outputDirectory, includeDebug);
}
return allExtracted;
}
/// <summary>
/// Extract a file from the SGA to an output directory by index
/// </summary>
/// <param name="index">File index to extract</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the file extracted, false otherwise</returns>
public bool ExtractFile(int index, string outputDirectory, bool includeDebug)
{
// Get the file count
int fileCount = FileCount;
if (fileCount == 0)
return false;
// If the files index is invalid
if (index < 0 || index >= fileCount)
return false;
// Create the filename
var filename = GetFileName(index);
if (filename == null)
return false;
// Loop through and get all parent directories
var parentNames = new List<string> { filename };
// Get the parent directory
string? folderName = GetParentName(index);
if (folderName != null)
parentNames.Add(folderName);
// TODO: Should the section name/alias be used in the path as well?
// Reverse and assemble the filename
parentNames.Reverse();
#if NET20 || NET35
filename = parentNames[0];
for (int i = 1; i < parentNames.Count; i++)
{
filename = Path.Combine(filename, parentNames[i]);
}
#else
filename = Path.Combine([.. parentNames]);
#endif
// Get and adjust the file offset
long fileOffset = GetFileOffset(index);
fileOffset += FileDataOffset;
if (fileOffset < 0)
return false;
// Get the file sizes
long fileSize = GetCompressedSize(index);
long outputFileSize = GetUncompressedSize(index);
// Read the compressed data directly
var compressedData = ReadRangeFromSource((int)fileOffset, (int)fileSize);
if (compressedData == null)
return false;
// If the compressed and uncompressed sizes match
byte[] data;
if (fileSize == outputFileSize)
{
data = compressedData;
}
else
{
// Inflate the data into the buffer
var zstream = new ZLib.z_stream_s();
data = new byte[outputFileSize];
unsafe
{
fixed (byte* payloadPtr = compressedData)
fixed (byte* dataPtr = data)
{
zstream.next_in = payloadPtr;
zstream.avail_in = (uint)compressedData.Length;
zstream.total_in = (uint)compressedData.Length;
zstream.next_out = dataPtr;
zstream.avail_out = (uint)data.Length;
zstream.total_out = 0;
ZLib.inflateInit_(zstream, ZLib.zlibVersion(), compressedData.Length);
int zret = ZLib.inflate(zstream, 1);
ZLib.inflateEnd(zstream);
}
}
}
// If we have an invalid output directory
if (string.IsNullOrEmpty(outputDirectory))
return false;
// 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 && !System.IO.Directory.Exists(directoryName))
System.IO.Directory.CreateDirectory(directoryName);
// Try to write the data
try
{
// Open the output file for writing
using Stream fs = System.IO.File.OpenWrite(filename);
fs.Write(data, 0, data.Length);
fs.Flush();
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
return false;
}
}
}

View File

@@ -1,14 +1,10 @@
using System;
using System.Collections.Generic;
using System.IO;
using SabreTools.IO.Compression.zlib;
using SabreTools.IO.Extensions;
using SabreTools.Models.SGA;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public class SGA : WrapperBase<Archive>, IExtractable
public partial class SGA : WrapperBase<Archive>
{
#region Descriptive Properties
@@ -128,151 +124,6 @@ namespace SabreTools.Serialization.Wrappers
#endregion
#region Extraction
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// Get the file count
int fileCount = FileCount;
if (fileCount == 0)
return false;
// Loop through and extract all files to the output
bool allExtracted = true;
for (int i = 0; i < fileCount; i++)
{
allExtracted &= ExtractFile(i, outputDirectory, includeDebug);
}
return allExtracted;
}
/// <summary>
/// Extract a file from the SGA to an output directory by index
/// </summary>
/// <param name="index">File index to extract</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the file extracted, false otherwise</returns>
public bool ExtractFile(int index, string outputDirectory, bool includeDebug)
{
// Get the file count
int fileCount = FileCount;
if (fileCount == 0)
return false;
// If the files index is invalid
if (index < 0 || index >= fileCount)
return false;
// Create the filename
var filename = GetFileName(index);
if (filename == null)
return false;
// Loop through and get all parent directories
var parentNames = new List<string> { filename };
// Get the parent directory
string? folderName = GetParentName(index);
if (folderName != null)
parentNames.Add(folderName);
// TODO: Should the section name/alias be used in the path as well?
// Reverse and assemble the filename
parentNames.Reverse();
#if NET20 || NET35
filename = parentNames[0];
for (int i = 1; i < parentNames.Count; i++)
{
filename = Path.Combine(filename, parentNames[i]);
}
#else
filename = Path.Combine([.. parentNames]);
#endif
// Get and adjust the file offset
long fileOffset = GetFileOffset(index);
fileOffset += FileDataOffset;
if (fileOffset < 0)
return false;
// Get the file sizes
long fileSize = GetCompressedSize(index);
long outputFileSize = GetUncompressedSize(index);
// Read the compressed data directly
var compressedData = ReadRangeFromSource((int)fileOffset, (int)fileSize);
if (compressedData == null)
return false;
// If the compressed and uncompressed sizes match
byte[] data;
if (fileSize == outputFileSize)
{
data = compressedData;
}
else
{
// Inflate the data into the buffer
var zstream = new ZLib.z_stream_s();
data = new byte[outputFileSize];
unsafe
{
fixed (byte* payloadPtr = compressedData)
fixed (byte* dataPtr = data)
{
zstream.next_in = payloadPtr;
zstream.avail_in = (uint)compressedData.Length;
zstream.total_in = (uint)compressedData.Length;
zstream.next_out = dataPtr;
zstream.avail_out = (uint)data.Length;
zstream.total_out = 0;
ZLib.inflateInit_(zstream, ZLib.zlibVersion(), compressedData.Length);
int zret = ZLib.inflate(zstream, 1);
ZLib.inflateEnd(zstream);
}
}
}
// If we have an invalid output directory
if (string.IsNullOrEmpty(outputDirectory))
return false;
// 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 && !System.IO.Directory.Exists(directoryName))
System.IO.Directory.CreateDirectory(directoryName);
// Try to write the data
try
{
// Open the output file for writing
using Stream fs = System.IO.File.OpenWrite(filename);
fs.Write(data, 0, data.Length);
fs.Flush();
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
return false;
}
#endregion
#region File
/// <summary>

View File

@@ -0,0 +1,224 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using SabreTools.Serialization.Interfaces;
#if NET462_OR_GREATER || NETCOREAPP
using SharpCompress.Archives;
using SharpCompress.Archives.SevenZip;
using SharpCompress.Common;
using SharpCompress.Readers;
#endif
namespace SabreTools.Serialization.Wrappers
{
/// <summary>
/// This is a shell wrapper; one that does not contain
/// any actual parsing. It is used as a placeholder for
/// types that typically do not have models.
/// </summary>
public partial class SevenZip : IExtractable
{
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
=> Extract(outputDirectory, lookForHeader: false, includeDebug);
/// <inheritdoc cref="Extract(string, bool)"/>
public bool Extract(string outputDirectory, bool lookForHeader, bool includeDebug)
{
if (_dataSource == null || !_dataSource.CanRead)
return false;
#if NET462_OR_GREATER || NETCOREAPP
try
{
var readerOptions = new ReaderOptions() { LookForHeader = lookForHeader };
var sevenZip = SevenZipArchive.Open(_dataSource, readerOptions);
// If the file exists
if (!string.IsNullOrEmpty(Filename) && File.Exists(Filename!))
{
// Find all file parts
FileInfo[] parts = [.. ArchiveFactory.GetFileParts(new FileInfo(Filename))];
// If there are multiple parts
if (parts.Length > 1)
sevenZip = SevenZipArchive.Open(parts, readerOptions);
// Try to read the file path if no entries are found
else if (sevenZip.Entries.Count == 0)
sevenZip = SevenZipArchive.Open(parts, readerOptions);
}
// Currently doesn't flag solid 7z archives with only 1 solid block as solid, but practically speaking
// this is not much of a concern.
if (sevenZip.IsSolid)
return ExtractSolid(sevenZip, outputDirectory, includeDebug);
else
return ExtractNonSolid(sevenZip, outputDirectory, includeDebug);
}
catch (System.Exception ex)
{
if (includeDebug) System.Console.Error.WriteLine(ex);
return false;
}
#else
Console.WriteLine("Extraction is not supported for this framework!");
Console.WriteLine();
return false;
#endif
}
/// <summary>
/// Try to find all parts of the archive, if possible
/// </summary>
/// <param name="firstPart">Path of the first archive part</param>
/// <returns>List of all found parts, if possible</returns>
public static List<string> FindParts(string firstPart)
{
// Define the regex patterns
const string genericPattern = @"^(.*\.)([0-9]+)$";
// Ensure the full path is available
firstPart = Path.GetFullPath(firstPart);
string filename = Path.GetFileName(firstPart);
string? directory = Path.GetDirectoryName(firstPart);
// Make the output list
List<string> parts = [];
// Determine which pattern is being used
Match match;
Func<int, string> nextPartFunc;
if (Regex.IsMatch(filename, genericPattern, RegexOptions.IgnoreCase))
{
match = Regex.Match(filename, genericPattern, RegexOptions.IgnoreCase);
nextPartFunc = (i) =>
{
return string.Concat(
match.Groups[1].Value,
$"{i + 1}".PadLeft(match.Groups[2].Value.Length, '0')
);
};
}
else
{
return [firstPart];
}
// Loop and add the files
parts.Add(firstPart);
for (int i = 1; ; i++)
{
string nextPart = nextPartFunc(i);
if (directory != null)
nextPart = Path.Combine(directory, nextPart);
if (!File.Exists(nextPart))
break;
parts.Add(nextPart);
}
return parts;
}
#if NET462_OR_GREATER || NETCOREAPP
/// <summary>
/// Extraction method for non-solid archives. This iterates over each entry in the archive to extract every
/// file individually, in order to extract all valid files from the archive.
/// </summary>
private static bool ExtractNonSolid(SevenZipArchive sevenZip, string outputDirectory, bool includeDebug)
{
foreach (var entry in sevenZip.Entries)
{
try
{
// If the entry is a directory
if (entry.IsDirectory)
continue;
// If the entry has an invalid key
if (entry.Key == null)
continue;
// If we have a partial entry due to an incomplete multi-part archive, skip it
if (!entry.IsComplete)
continue;
// Ensure directory separators are consistent
string filename = entry.Key;
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);
entry.WriteToFile(filename);
}
catch (System.Exception ex)
{
if (includeDebug) System.Console.Error.WriteLine(ex);
}
}
return true;
}
/// <summary>
/// Extraction method for solid archives. Uses ExtractAllEntries because extraction for solid archives must be
/// done sequentially, and files beyond a corrupted point in a solid archive will be unreadable anyways.
/// </summary>
private static bool ExtractSolid(SevenZipArchive sevenZip, string outputDirectory, bool includeDebug)
{
try
{
if (!Directory.Exists(outputDirectory))
Directory.CreateDirectory(outputDirectory);
int index = 0;
var entries = sevenZip.ExtractAllEntries();
while (entries.MoveToNextEntry())
{
var entry = entries.Entry;
if (entry.IsDirectory)
continue;
// Ensure directory separators are consistent
string filename = entry.Key ?? $"extracted_file_{index}";
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);
// Write to file
using var fs = File.Open(filename, FileMode.Create, FileAccess.Write, FileShare.None);
entries.WriteEntryTo(fs);
fs.Flush();
// Increment the index
index++;
}
}
catch (System.Exception ex)
{
if (includeDebug) System.Console.Error.WriteLine(ex);
}
return true;
}
#endif
}
}

View File

@@ -1,14 +1,4 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using SabreTools.Serialization.Interfaces;
#if NET462_OR_GREATER || NETCOREAPP
using SharpCompress.Archives;
using SharpCompress.Archives.SevenZip;
using SharpCompress.Common;
using SharpCompress.Readers;
#endif
namespace SabreTools.Serialization.Wrappers
{
@@ -17,7 +7,7 @@ namespace SabreTools.Serialization.Wrappers
/// any actual parsing. It is used as a placeholder for
/// types that typically do not have models.
/// </summary>
public class SevenZip : WrapperBase, IExtractable
public partial class SevenZip : WrapperBase
{
#region Descriptive Properties
@@ -87,211 +77,5 @@ namespace SabreTools.Serialization.Wrappers
#endif
#endregion
#region Extraction
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
=> Extract(outputDirectory, lookForHeader: false, includeDebug);
/// <inheritdoc cref="Extract(string, bool)"/>
public bool Extract(string outputDirectory, bool lookForHeader, bool includeDebug)
{
if (_dataSource == null || !_dataSource.CanRead)
return false;
#if NET462_OR_GREATER || NETCOREAPP
try
{
var readerOptions = new ReaderOptions() { LookForHeader = lookForHeader };
var sevenZip = SevenZipArchive.Open(_dataSource, readerOptions);
// If the file exists
if (!string.IsNullOrEmpty(Filename) && File.Exists(Filename!))
{
// Find all file parts
FileInfo[] parts = [.. ArchiveFactory.GetFileParts(new FileInfo(Filename))];
// If there are multiple parts
if (parts.Length > 1)
sevenZip = SevenZipArchive.Open(parts, readerOptions);
// Try to read the file path if no entries are found
else if (sevenZip.Entries.Count == 0)
sevenZip = SevenZipArchive.Open(parts, readerOptions);
}
// Currently doesn't flag solid 7z archives with only 1 solid block as solid, but practically speaking
// this is not much of a concern.
if (sevenZip.IsSolid)
return ExtractSolid(sevenZip, outputDirectory, includeDebug);
else
return ExtractNonSolid(sevenZip, outputDirectory, includeDebug);
}
catch (System.Exception ex)
{
if (includeDebug) System.Console.Error.WriteLine(ex);
return false;
}
#else
Console.WriteLine("Extraction is not supported for this framework!");
Console.WriteLine();
return false;
#endif
}
/// <summary>
/// Try to find all parts of the archive, if possible
/// </summary>
/// <param name="firstPart">Path of the first archive part</param>
/// <returns>List of all found parts, if possible</returns>
public static List<string> FindParts(string firstPart)
{
// Define the regex patterns
const string genericPattern = @"^(.*\.)([0-9]+)$";
// Ensure the full path is available
firstPart = Path.GetFullPath(firstPart);
string filename = Path.GetFileName(firstPart);
string? directory = Path.GetDirectoryName(firstPart);
// Make the output list
List<string> parts = [];
// Determine which pattern is being used
Match match;
Func<int, string> nextPartFunc;
if (Regex.IsMatch(filename, genericPattern, RegexOptions.IgnoreCase))
{
match = Regex.Match(filename, genericPattern, RegexOptions.IgnoreCase);
nextPartFunc = (i) =>
{
return string.Concat(
match.Groups[1].Value,
$"{i + 1}".PadLeft(match.Groups[2].Value.Length, '0')
);
};
}
else
{
return [firstPart];
}
// Loop and add the files
parts.Add(firstPart);
for (int i = 1; ; i++)
{
string nextPart = nextPartFunc(i);
if (directory != null)
nextPart = Path.Combine(directory, nextPart);
if (!File.Exists(nextPart))
break;
parts.Add(nextPart);
}
return parts;
}
#if NET462_OR_GREATER || NETCOREAPP
/// <summary>
/// Extraction method for non-solid archives. This iterates over each entry in the archive to extract every
/// file individually, in order to extract all valid files from the archive.
/// </summary>
private static bool ExtractNonSolid(SevenZipArchive sevenZip, string outputDirectory, bool includeDebug)
{
foreach (var entry in sevenZip.Entries)
{
try
{
// If the entry is a directory
if (entry.IsDirectory)
continue;
// If the entry has an invalid key
if (entry.Key == null)
continue;
// If we have a partial entry due to an incomplete multi-part archive, skip it
if (!entry.IsComplete)
continue;
// Ensure directory separators are consistent
string filename = entry.Key;
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);
entry.WriteToFile(filename);
}
catch (System.Exception ex)
{
if (includeDebug) System.Console.Error.WriteLine(ex);
}
}
return true;
}
/// <summary>
/// Extraction method for solid archives. Uses ExtractAllEntries because extraction for solid archives must be
/// done sequentially, and files beyond a corrupted point in a solid archive will be unreadable anyways.
/// </summary>
private static bool ExtractSolid(SevenZipArchive sevenZip, string outputDirectory, bool includeDebug)
{
try
{
if (!Directory.Exists(outputDirectory))
Directory.CreateDirectory(outputDirectory);
int index = 0;
var entries = sevenZip.ExtractAllEntries();
while (entries.MoveToNextEntry())
{
var entry = entries.Entry;
if (entry.IsDirectory)
continue;
// Ensure directory separators are consistent
string filename = entry.Key ?? $"extracted_file_{index}";
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);
// Write to file
using var fs = File.Open(filename, FileMode.Create, FileAccess.Write, FileShare.None);
entries.WriteEntryTo(fs);
fs.Flush();
// Increment the index
index++;
}
}
catch (System.Exception ex)
{
if (includeDebug) System.Console.Error.WriteLine(ex);
}
return true;
}
#endif
#endregion
}
}

View File

@@ -0,0 +1,170 @@
using System;
using System.IO;
using SabreTools.Models.TAR;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public partial class TapeArchive : IExtractable
{
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// Ensure there are entries to extract
if (Entries == null || Entries.Length == 0)
return false;
try
{
// Loop through and extract the data
for (int i = 0; i < Entries.Length; i++)
{
var entry = Entries[i];
if (entry.Header == null)
{
if (includeDebug) Console.Error.WriteLine($"Invalid entry {i} found! Skipping...");
continue;
}
// Handle special entries
var header = entry.Header;
switch (header.TypeFlag)
{
// Skipped types
case TypeFlag.LNKTYPE:
case TypeFlag.SYMTYPE:
case TypeFlag.CHRTYPE:
case TypeFlag.BLKTYPE:
case TypeFlag.FIFOTYPE:
case TypeFlag.XHDTYPE:
case TypeFlag.XGLTYPE:
if (includeDebug) Console.WriteLine($"Unsupported entry type: {header.TypeFlag}");
continue;
// Skipped vendor types
case TypeFlag.VendorSpecificA:
case TypeFlag.VendorSpecificB:
case TypeFlag.VendorSpecificC:
case TypeFlag.VendorSpecificD:
case TypeFlag.VendorSpecificE:
case TypeFlag.VendorSpecificF:
case TypeFlag.VendorSpecificG:
case TypeFlag.VendorSpecificH:
case TypeFlag.VendorSpecificI:
case TypeFlag.VendorSpecificJ:
case TypeFlag.VendorSpecificK:
case TypeFlag.VendorSpecificL:
case TypeFlag.VendorSpecificM:
case TypeFlag.VendorSpecificN:
case TypeFlag.VendorSpecificO:
case TypeFlag.VendorSpecificP:
case TypeFlag.VendorSpecificQ:
case TypeFlag.VendorSpecificR:
case TypeFlag.VendorSpecificS:
case TypeFlag.VendorSpecificT:
case TypeFlag.VendorSpecificU:
case TypeFlag.VendorSpecificV:
case TypeFlag.VendorSpecificW:
case TypeFlag.VendorSpecificX:
case TypeFlag.VendorSpecificY:
case TypeFlag.VendorSpecificZ:
if (includeDebug) Console.WriteLine($"Unsupported vendor entry type: {header.TypeFlag}");
continue;
// Directories
case TypeFlag.DIRTYPE:
string? entryDirectory = header.FileName?.TrimEnd('\0');
if (entryDirectory == null)
{
if (includeDebug) Console.Error.WriteLine($"Entry {i} reported as directory, but no path found! Skipping...");
continue;
}
// Ensure directory separators are consistent
entryDirectory = Path.Combine(outputDirectory, entryDirectory);
if (Path.DirectorySeparatorChar == '\\')
entryDirectory = entryDirectory.Replace('/', '\\');
else if (Path.DirectorySeparatorChar == '/')
entryDirectory = entryDirectory.Replace('\\', '/');
// Create the director
Directory.CreateDirectory(entryDirectory);
continue;
}
// Ensure there are blocks to extract
if (entry.Blocks == null)
{
if (includeDebug) Console.WriteLine($"Entry {i} had no block data");
continue;
}
// Get the file size
string sizeOctalString = header.Size!.TrimEnd('\0');
if (sizeOctalString.Length == 0)
{
if (includeDebug) Console.WriteLine($"Entry {i} has an invalid size, skipping...");
continue;
}
int entrySize = Convert.ToInt32(sizeOctalString, 8);
// Setup the temporary buffer
byte[] dataBytes = new byte[entrySize];
int dataBytesPtr = 0;
// Loop through and copy the bytes to the array for writing
int blockNumber = 0;
while (entrySize > 0)
{
// Exit early if block number is invalid
if (blockNumber >= entry.Blocks.Length)
{
if (includeDebug) Console.Error.WriteLine($"Invalid block number {i + 1} of {entry.Blocks.Length}, file may be incomplete!");
break;
}
// Exit early if the block has no data
var block = entry.Blocks[blockNumber++];
if (block.Data == null || block.Data.Length != 512)
{
if (includeDebug) Console.Error.WriteLine($"Invalid data for block number {i + 1}, file may be incomplete!");
break;
}
int nextBytes = Math.Min(512, entrySize);
entrySize -= nextBytes;
Array.Copy(block.Data, 0, dataBytes, dataBytesPtr, nextBytes);
dataBytesPtr += nextBytes;
}
// Ensure directory separators are consistent
string filename = header.FileName?.TrimEnd('\0') ?? $"entry_{i}";
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);
// Write the file
using var fs = File.Open(filename, FileMode.Create, FileAccess.Write, FileShare.None);
fs.Write(dataBytes, 0, dataBytes.Length);
fs.Flush();
}
return true;
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
}
}
}

View File

@@ -1,11 +1,9 @@
using System;
using System.IO;
using SabreTools.Models.TAR;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public class TapeArchive : WrapperBase<Archive>, IExtractable
public partial class TapeArchive : WrapperBase<Archive>
{
#region Descriptive Properties
@@ -88,169 +86,5 @@ namespace SabreTools.Serialization.Wrappers
}
#endregion
#region Extraction
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// Ensure there are entries to extract
if (Entries == null || Entries.Length == 0)
return false;
try
{
// Loop through and extract the data
for (int i = 0; i < Entries.Length; i++)
{
var entry = Entries[i];
if (entry.Header == null)
{
if (includeDebug) Console.Error.WriteLine($"Invalid entry {i} found! Skipping...");
continue;
}
// Handle special entries
var header = entry.Header;
switch (header.TypeFlag)
{
// Skipped types
case TypeFlag.LNKTYPE:
case TypeFlag.SYMTYPE:
case TypeFlag.CHRTYPE:
case TypeFlag.BLKTYPE:
case TypeFlag.FIFOTYPE:
case TypeFlag.XHDTYPE:
case TypeFlag.XGLTYPE:
if (includeDebug) Console.WriteLine($"Unsupported entry type: {header.TypeFlag}");
continue;
// Skipped vendor types
case TypeFlag.VendorSpecificA:
case TypeFlag.VendorSpecificB:
case TypeFlag.VendorSpecificC:
case TypeFlag.VendorSpecificD:
case TypeFlag.VendorSpecificE:
case TypeFlag.VendorSpecificF:
case TypeFlag.VendorSpecificG:
case TypeFlag.VendorSpecificH:
case TypeFlag.VendorSpecificI:
case TypeFlag.VendorSpecificJ:
case TypeFlag.VendorSpecificK:
case TypeFlag.VendorSpecificL:
case TypeFlag.VendorSpecificM:
case TypeFlag.VendorSpecificN:
case TypeFlag.VendorSpecificO:
case TypeFlag.VendorSpecificP:
case TypeFlag.VendorSpecificQ:
case TypeFlag.VendorSpecificR:
case TypeFlag.VendorSpecificS:
case TypeFlag.VendorSpecificT:
case TypeFlag.VendorSpecificU:
case TypeFlag.VendorSpecificV:
case TypeFlag.VendorSpecificW:
case TypeFlag.VendorSpecificX:
case TypeFlag.VendorSpecificY:
case TypeFlag.VendorSpecificZ:
if (includeDebug) Console.WriteLine($"Unsupported vendor entry type: {header.TypeFlag}");
continue;
// Directories
case TypeFlag.DIRTYPE:
string? entryDirectory = header.FileName?.TrimEnd('\0');
if (entryDirectory == null)
{
if (includeDebug) Console.Error.WriteLine($"Entry {i} reported as directory, but no path found! Skipping...");
continue;
}
// Ensure directory separators are consistent
entryDirectory = Path.Combine(outputDirectory, entryDirectory);
if (Path.DirectorySeparatorChar == '\\')
entryDirectory = entryDirectory.Replace('/', '\\');
else if (Path.DirectorySeparatorChar == '/')
entryDirectory = entryDirectory.Replace('\\', '/');
// Create the director
Directory.CreateDirectory(entryDirectory);
continue;
}
// Ensure there are blocks to extract
if (entry.Blocks == null)
{
if (includeDebug) Console.WriteLine($"Entry {i} had no block data");
continue;
}
// Get the file size
string sizeOctalString = header.Size!.TrimEnd('\0');
if (sizeOctalString.Length == 0)
{
if (includeDebug) Console.WriteLine($"Entry {i} has an invalid size, skipping...");
continue;
}
int entrySize = Convert.ToInt32(sizeOctalString, 8);
// Setup the temporary buffer
byte[] dataBytes = new byte[entrySize];
int dataBytesPtr = 0;
// Loop through and copy the bytes to the array for writing
int blockNumber = 0;
while (entrySize > 0)
{
// Exit early if block number is invalid
if (blockNumber >= entry.Blocks.Length)
{
if (includeDebug) Console.Error.WriteLine($"Invalid block number {i + 1} of {entry.Blocks.Length}, file may be incomplete!");
break;
}
// Exit early if the block has no data
var block = entry.Blocks[blockNumber++];
if (block.Data == null || block.Data.Length != 512)
{
if (includeDebug) Console.Error.WriteLine($"Invalid data for block number {i + 1}, file may be incomplete!");
break;
}
int nextBytes = Math.Min(512, entrySize);
entrySize -= nextBytes;
Array.Copy(block.Data, 0, dataBytes, dataBytesPtr, nextBytes);
dataBytesPtr += nextBytes;
}
// Ensure directory separators are consistent
string filename = header.FileName?.TrimEnd('\0') ?? $"entry_{i}";
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);
// Write the file
using var fs = File.Open(filename, FileMode.Create, FileAccess.Write, FileShare.None);
fs.Write(dataBytes, 0, dataBytes.Length);
fs.Flush();
}
return true;
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
}
#endregion
}
}

View File

@@ -0,0 +1,95 @@
using System;
using System.IO;
using SabreTools.Models.BSP;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public partial class VBSP : IExtractable
{
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// If we have no lumps
if (Lumps == null || Lumps.Length == 0)
return false;
// Loop through and extract all lumps to the output
bool allExtracted = true;
for (int i = 0; i < Lumps.Length; i++)
{
allExtracted &= ExtractLump(i, outputDirectory, includeDebug);
}
return allExtracted;
}
/// <summary>
/// Extract a lump from the VBSP to an output directory by index
/// </summary>
/// <param name="index">Lump index to extract</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the lump extracted, false otherwise</returns>
public bool ExtractLump(int index, string outputDirectory, bool includeDebug)
{
// If we have no lumps
if (Lumps == null || Lumps.Length == 0)
return false;
// If the lumps index is invalid
if (index < 0 || index >= Lumps.Length)
return false;
// Read the data
var lump = Lumps[index];
var data = ReadRangeFromSource(lump.Offset, lump.Length);
if (data == null)
return false;
// If we have an invalid output directory
if (string.IsNullOrEmpty(outputDirectory))
return false;
// Create the filename
string filename = $"lump_{index}.bin";
switch ((LumpType)index)
{
case LumpType.LUMP_ENTITIES:
filename = "entities.ent";
break;
case LumpType.LUMP_PAKFILE:
filename = "pakfile.zip";
break;
}
// 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);
// Try to write the data
try
{
// Open the output file for writing
using Stream fs = File.OpenWrite(filename);
fs.Write(data, 0, data.Length);
fs.Flush();
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
return true;
}
}
}

View File

@@ -1,12 +1,9 @@
using System;
using System.IO;
using SabreTools.IO.Extensions;
using SabreTools.Models.BSP;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public class VBSP : WrapperBase<VbspFile>, IExtractable
public partial class VBSP : WrapperBase<VbspFile>
{
#region Descriptive Properties
@@ -89,94 +86,5 @@ namespace SabreTools.Serialization.Wrappers
}
#endregion
#region Extraction
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// If we have no lumps
if (Lumps == null || Lumps.Length == 0)
return false;
// Loop through and extract all lumps to the output
bool allExtracted = true;
for (int i = 0; i < Lumps.Length; i++)
{
allExtracted &= ExtractLump(i, outputDirectory, includeDebug);
}
return allExtracted;
}
/// <summary>
/// Extract a lump from the VBSP to an output directory by index
/// </summary>
/// <param name="index">Lump index to extract</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the lump extracted, false otherwise</returns>
public bool ExtractLump(int index, string outputDirectory, bool includeDebug)
{
// If we have no lumps
if (Lumps == null || Lumps.Length == 0)
return false;
// If the lumps index is invalid
if (index < 0 || index >= Lumps.Length)
return false;
// Read the data
var lump = Lumps[index];
var data = ReadRangeFromSource(lump.Offset, lump.Length);
if (data == null)
return false;
// If we have an invalid output directory
if (string.IsNullOrEmpty(outputDirectory))
return false;
// Create the filename
string filename = $"lump_{index}.bin";
switch ((LumpType)index)
{
case LumpType.LUMP_ENTITIES:
filename = "entities.ent";
break;
case LumpType.LUMP_PAKFILE:
filename = "pakfile.zip";
break;
}
// 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);
// Try to write the data
try
{
// Open the output file for writing
using Stream fs = File.OpenWrite(filename);
fs.Write(data, 0, data.Length);
fs.Flush();
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
return true;
}
#endregion
}
}

View File

@@ -0,0 +1,146 @@
using System;
using System.IO;
using SabreTools.IO.Extensions;
using SabreTools.Serialization.Interfaces;
using static SabreTools.Models.VPK.Constants;
namespace SabreTools.Serialization.Wrappers
{
public partial class VPK : IExtractable
{
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// If we have no directory items
if (DirectoryItems == null || DirectoryItems.Length == 0)
return false;
// Loop through and extract all files to the output
bool allExtracted = true;
for (int i = 0; i < DirectoryItems.Length; i++)
{
allExtracted &= ExtractFile(i, outputDirectory, includeDebug);
}
return allExtracted;
}
/// <summary>
/// Extract a file from the VPK to an output directory by index
/// </summary>
/// <param name="index">File index to extract</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the file extracted, false otherwise</returns>
public bool ExtractFile(int index, string outputDirectory, bool includeDebug)
{
// If we have no directory items
if (DirectoryItems == null || DirectoryItems.Length == 0)
return false;
// If the directory item index is invalid
if (index < 0 || index >= DirectoryItems.Length)
return false;
// Get the directory item
var directoryItem = DirectoryItems[index];
if (directoryItem.DirectoryEntry == null)
return false;
// If we have an item with no archive
byte[] data = [];
if (directoryItem.DirectoryEntry.ArchiveIndex == HL_VPK_NO_ARCHIVE)
{
if (directoryItem.PreloadData == null)
return false;
data = directoryItem.PreloadData;
}
else
{
// If we have invalid archives
if (ArchiveFilenames == null || ArchiveFilenames.Length == 0)
return false;
// If we have an invalid index
if (directoryItem.DirectoryEntry.ArchiveIndex < 0 || directoryItem.DirectoryEntry.ArchiveIndex >= ArchiveFilenames.Length)
return false;
// Get the archive filename
string archiveFileName = ArchiveFilenames[directoryItem.DirectoryEntry.ArchiveIndex];
if (string.IsNullOrEmpty(archiveFileName))
return false;
// If the archive doesn't exist
if (!File.Exists(archiveFileName))
return false;
// Try to open the archive
var archiveStream = default(Stream);
try
{
// Open the archive
archiveStream = File.Open(archiveFileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
// Seek to the data
archiveStream.Seek(directoryItem.DirectoryEntry.EntryOffset, SeekOrigin.Begin);
// Read the directory item bytes
data = archiveStream.ReadBytes((int)directoryItem.DirectoryEntry.EntryLength);
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
finally
{
archiveStream?.Close();
}
// If we have preload data, prepend it
if (data != null && directoryItem.PreloadData != null)
data = [.. directoryItem.PreloadData, .. data];
}
// If there is nothing to write out
if (data == null)
return false;
// If we have an invalid output directory
if (string.IsNullOrEmpty(outputDirectory))
return false;
// Ensure directory separators are consistent
string filename = $"{directoryItem.Name}.{directoryItem.Extension}";
if (!string.IsNullOrEmpty(directoryItem.Path))
filename = Path.Combine(directoryItem.Path, filename);
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);
// Try to write the data
try
{
// Open the output file for writing
using Stream fs = File.OpenWrite(filename);
fs.Write(data, 0, data.Length);
fs.Flush();
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
return true;
}
}
}

View File

@@ -1,12 +1,10 @@
using System;
using System.IO;
using SabreTools.IO.Extensions;
using SabreTools.Serialization.Interfaces;
using static SabreTools.Models.VPK.Constants;
namespace SabreTools.Serialization.Wrappers
{
public class VPK : WrapperBase<Models.VPK.File>, IExtractable
public partial class VPK : WrapperBase<Models.VPK.File>
{
#region Descriptive Properties
@@ -153,144 +151,5 @@ namespace SabreTools.Serialization.Wrappers
}
#endregion
#region Extraction
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// If we have no directory items
if (DirectoryItems == null || DirectoryItems.Length == 0)
return false;
// Loop through and extract all files to the output
bool allExtracted = true;
for (int i = 0; i < DirectoryItems.Length; i++)
{
allExtracted &= ExtractFile(i, outputDirectory, includeDebug);
}
return allExtracted;
}
/// <summary>
/// Extract a file from the VPK to an output directory by index
/// </summary>
/// <param name="index">File index to extract</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the file extracted, false otherwise</returns>
public bool ExtractFile(int index, string outputDirectory, bool includeDebug)
{
// If we have no directory items
if (DirectoryItems == null || DirectoryItems.Length == 0)
return false;
// If the directory item index is invalid
if (index < 0 || index >= DirectoryItems.Length)
return false;
// Get the directory item
var directoryItem = DirectoryItems[index];
if (directoryItem.DirectoryEntry == null)
return false;
// If we have an item with no archive
byte[] data = [];
if (directoryItem.DirectoryEntry.ArchiveIndex == HL_VPK_NO_ARCHIVE)
{
if (directoryItem.PreloadData == null)
return false;
data = directoryItem.PreloadData;
}
else
{
// If we have invalid archives
if (ArchiveFilenames == null || ArchiveFilenames.Length == 0)
return false;
// If we have an invalid index
if (directoryItem.DirectoryEntry.ArchiveIndex < 0 || directoryItem.DirectoryEntry.ArchiveIndex >= ArchiveFilenames.Length)
return false;
// Get the archive filename
string archiveFileName = ArchiveFilenames[directoryItem.DirectoryEntry.ArchiveIndex];
if (string.IsNullOrEmpty(archiveFileName))
return false;
// If the archive doesn't exist
if (!File.Exists(archiveFileName))
return false;
// Try to open the archive
var archiveStream = default(Stream);
try
{
// Open the archive
archiveStream = File.Open(archiveFileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
// Seek to the data
archiveStream.Seek(directoryItem.DirectoryEntry.EntryOffset, SeekOrigin.Begin);
// Read the directory item bytes
data = archiveStream.ReadBytes((int)directoryItem.DirectoryEntry.EntryLength);
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
finally
{
archiveStream?.Close();
}
// If we have preload data, prepend it
if (data != null && directoryItem.PreloadData != null)
data = [.. directoryItem.PreloadData, .. data];
}
// If there is nothing to write out
if (data == null)
return false;
// If we have an invalid output directory
if (string.IsNullOrEmpty(outputDirectory))
return false;
// Ensure directory separators are consistent
string filename = $"{directoryItem.Name}.{directoryItem.Extension}";
if (!string.IsNullOrEmpty(directoryItem.Path))
filename = Path.Combine(directoryItem.Path, filename);
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);
// Try to write the data
try
{
// Open the output file for writing
using Stream fs = File.OpenWrite(filename);
fs.Write(data, 0, data.Length);
fs.Flush();
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
return true;
}
#endregion
}
}

View File

@@ -0,0 +1,83 @@
using System;
using System.IO;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public partial class WAD3 : IExtractable
{
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// If we have no lumps
if (DirEntries == null || DirEntries.Length == 0)
return false;
// Loop through and extract all lumps to the output
bool allExtracted = true;
for (int i = 0; i < DirEntries.Length; i++)
{
allExtracted &= ExtractLump(i, outputDirectory, includeDebug);
}
return allExtracted;
}
/// <summary>
/// Extract a lump from the WAD3 to an output directory by index
/// </summary>
/// <param name="index">Lump index to extract</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the lump extracted, false otherwise</returns>
public bool ExtractLump(int index, string outputDirectory, bool includeDebug)
{
// If we have no lumps
if (DirEntries == null || DirEntries.Length == 0)
return false;
// If the lumps index is invalid
if (index < 0 || index >= DirEntries.Length)
return false;
// Read the data -- TODO: Handle uncompressed lumps (see BSP.ExtractTexture)
var lump = DirEntries[index];
var data = ReadRangeFromSource((int)lump.Offset, (int)lump.Length);
if (data == null)
return false;
// If we have an invalid output directory
if (string.IsNullOrEmpty(outputDirectory))
return false;
// Ensure directory separators are consistent
string filename = $"{lump.Name}.lmp";
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);
// Try to write the data
try
{
// Open the output file for writing
using Stream fs = File.OpenWrite(filename);
fs.Write(data, 0, data.Length);
fs.Flush();
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
return true;
}
}
}

View File

@@ -1,11 +1,8 @@
using System;
using System.IO;
using SabreTools.IO.Extensions;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public class WAD3 : WrapperBase<Models.WAD3.File>, IExtractable
public partial class WAD3 : WrapperBase<Models.WAD3.File>
{
#region Descriptive Properties
@@ -88,83 +85,5 @@ namespace SabreTools.Serialization.Wrappers
}
#endregion
#region Extraction
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// If we have no lumps
if (DirEntries == null || DirEntries.Length == 0)
return false;
// Loop through and extract all lumps to the output
bool allExtracted = true;
for (int i = 0; i < DirEntries.Length; i++)
{
allExtracted &= ExtractLump(i, outputDirectory, includeDebug);
}
return allExtracted;
}
/// <summary>
/// Extract a lump from the WAD3 to an output directory by index
/// </summary>
/// <param name="index">Lump index to extract</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the lump extracted, false otherwise</returns>
public bool ExtractLump(int index, string outputDirectory, bool includeDebug)
{
// If we have no lumps
if (DirEntries == null || DirEntries.Length == 0)
return false;
// If the lumps index is invalid
if (index < 0 || index >= DirEntries.Length)
return false;
// Read the data -- TODO: Handle uncompressed lumps (see BSP.ExtractTexture)
var lump = DirEntries[index];
var data = ReadRangeFromSource((int)lump.Offset, (int)lump.Length);
if (data == null)
return false;
// If we have an invalid output directory
if (string.IsNullOrEmpty(outputDirectory))
return false;
// Ensure directory separators are consistent
string filename = $"{lump.Name}.lmp";
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);
// Try to write the data
try
{
// Open the output file for writing
using Stream fs = File.OpenWrite(filename);
fs.Write(data, 0, data.Length);
fs.Flush();
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
return true;
}
#endregion
}
}

View File

@@ -0,0 +1,216 @@
using System;
using System.IO;
using SabreTools.Hashing;
using SabreTools.IO.Compression.Deflate;
using SabreTools.IO.Extensions;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public partial class WiseSectionHeader : IExtractable
{
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// Extract the header-defined files
bool extracted = ExtractHeaderDefinedFiles(outputDirectory, includeDebug);
if (!extracted)
{
if (includeDebug) Console.Error.WriteLine("Could not extract header-defined files");
return false;
}
return true;
}
// Currently unaware of any NE samples. That said, as they wouldn't have a .WISE section, it's unclear how such
// samples could be identified.
/// <summary>
/// Extract the predefined, static files defined in the header
/// </summary>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the files extracted successfully, false otherwise</returns>
private bool ExtractHeaderDefinedFiles(string outputDirectory, bool includeDebug)
{
lock (_dataSourceLock)
{
// Seek to the compressed data offset
_dataSource.Seek(CompressedDataOffset, SeekOrigin.Begin);
bool successful = true;
// Extract first executable, if it exists
if (ExtractFile("FirstExecutable.exe", outputDirectory, FirstExecutableFileEntryLength, includeDebug) != ExtractionStatus.GOOD)
successful = false;
// Extract second executable, if it exists
// If there's a size provided for the second executable but no size for the first executable, the size of
// the second executable appears to be some unrelated value that's larger than the second executable
// actually is. Currently unable to extract properly in these cases, as no header value in such installers
// seems to actually correspond to the real size of the second executable.
if (ExtractFile("SecondExecutable.exe", outputDirectory, SecondExecutableFileEntryLength, includeDebug) != ExtractionStatus.GOOD)
successful = false;
// Extract third executable, if it exists
if (ExtractFile("ThirdExecutable.exe", outputDirectory, ThirdExecutableFileEntryLength, includeDebug) != ExtractionStatus.GOOD)
successful = false;
// Extract main MSI file
if (ExtractFile("ExtractedMsi.msi", outputDirectory, MsiFileEntryLength, includeDebug) != ExtractionStatus.GOOD)
{
// Fallback- seek to the position that's the length of the MSI file entry from the end, then try and
// extract from there.
_dataSource.Seek(-MsiFileEntryLength + 1, SeekOrigin.End);
if (ExtractFile("ExtractedMsi.msi", outputDirectory, MsiFileEntryLength, includeDebug) != ExtractionStatus.GOOD)
return false; // The fallback also failed.
}
return successful;
}
}
/// <summary>
/// Attempt to extract a file defined by a filename
/// </summary>
/// <param name="filename">Output filename, null to auto-generate</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="entrySize">Expected size of the file plus crc32</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>Extraction status representing the final state</returns>
/// <remarks>Assumes that the current stream position is the end of where the data lives</remarks>
private ExtractionStatus ExtractFile(string filename,
string outputDirectory,
uint entrySize,
bool includeDebug)
{
if (includeDebug) Console.WriteLine($"Attempting to extract {filename}");
// Extract the file
var destination = new MemoryStream();
ExtractionStatus status;
if (!(Version != null && Version[1] == 0x01))
{
status = ExtractStreamWithChecksum(destination, entrySize, includeDebug);
}
else // hack for Codesited5.exe , very early and very strange.
{
status = ExtractStreamWithoutChecksum(destination, entrySize, includeDebug);
}
// If the extracted data is invalid
if (status != ExtractionStatus.GOOD || destination == null)
return status;
// 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);
// Write the output file
File.WriteAllBytes(filename, destination.ToArray());
return status;
}
/// <summary>
/// Extract source data with a trailing CRC-32 checksum
/// </summary>
/// <param name="destination">Stream where the file data will be written</param>
/// <param name="entrySize">Expected size of the file plus crc32</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns></returns>
private ExtractionStatus ExtractStreamWithChecksum(Stream destination, uint entrySize, bool includeDebug)
{
// Debug output
if (includeDebug) Console.WriteLine($"Offset: {_dataSource.Position:X8}, Expected Read: {entrySize}, Expected Write:{entrySize - 4}"); // clamp to zero
// Check the validity of the inputs
if (entrySize == 0)
{
if (includeDebug) Console.Error.WriteLine("Not attempting to extract, expected to read 0 bytes");
return ExtractionStatus.GOOD; // If size is 0, then it shouldn't be extracted
}
else if (entrySize > (_dataSource.Length - _dataSource.Position))
{
if (includeDebug) Console.Error.WriteLine($"Not attempting to extract, expected to read {entrySize} bytes but only {_dataSource.Position} bytes remain");
return ExtractionStatus.INVALID;
}
// Extract the file
try
{
byte[] actual = _dataSource.ReadBytes((int)entrySize - 4);
uint expectedCrc32 = _dataSource.ReadUInt32();
// Debug output
if (includeDebug) Console.WriteLine($"Expected CRC-32: {expectedCrc32:X8}");
byte[]? hashBytes = HashTool.GetByteArrayHashArray(actual, HashType.CRC32);
if (hashBytes != null)
{
uint actualCrc32 = BitConverter.ToUInt32(hashBytes, 0);
// Debug output
if (includeDebug) Console.WriteLine($"Actual CRC-32: {actualCrc32:X8}");
if (expectedCrc32 != actualCrc32)
{
if (includeDebug) Console.Error.WriteLine("Mismatched CRC-32 values!");
return ExtractionStatus.BAD_CRC;
}
}
destination.Write(actual, 0, actual.Length);
return ExtractionStatus.GOOD;
}
catch
{
if (includeDebug) Console.Error.WriteLine("Could not extract");
return ExtractionStatus.FAIL;
}
}
/// <summary>
/// Extract source data without a trailing CRC-32 checksum
/// </summary>
/// <param name="destination">Stream where the file data will be written</param>
/// <param name="entrySize">Expected size of the file</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns></returns>
private ExtractionStatus ExtractStreamWithoutChecksum(Stream destination, uint entrySize, bool includeDebug)
{
// Debug output
if (includeDebug) Console.WriteLine($"Offset: {_dataSource.Position:X8}, Expected Read: {entrySize}, Expected Write:{entrySize - 4}");
// Check the validity of the inputs
if (entrySize == 0)
{
if (includeDebug) Console.Error.WriteLine("Not attempting to extract, expected to read 0 bytes");
return ExtractionStatus.GOOD; // If size is 0, then it shouldn't be extracted
}
else if (entrySize > (_dataSource.Length - _dataSource.Position))
{
if (includeDebug) Console.Error.WriteLine($"Not attempting to extract, expected to read {entrySize} bytes but only {_dataSource.Position} bytes remain");
return ExtractionStatus.INVALID;
}
// Extract the file
try
{
byte[] actual = _dataSource.ReadBytes((int)entrySize);
// Debug output
if (includeDebug) Console.WriteLine("No CRC-32!");
destination.Write(actual, 0, actual.Length);
return ExtractionStatus.GOOD;
}
catch
{
if (includeDebug) Console.Error.WriteLine("Could not extract");
return ExtractionStatus.FAIL;
}
}
}
}

View File

@@ -1,14 +1,9 @@
using System;
using System.IO;
using SabreTools.Hashing;
using SabreTools.IO.Compression.Deflate;
using SabreTools.IO.Extensions;
using SabreTools.Models.WiseInstaller;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public class WiseSectionHeader : WrapperBase<SectionHeader>, IExtractable
public partial class WiseSectionHeader : WrapperBase<SectionHeader>
{
#region Descriptive Properties
@@ -168,213 +163,5 @@ namespace SabreTools.Serialization.Wrappers
}
#endregion
#region Extraction
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// Extract the header-defined files
bool extracted = ExtractHeaderDefinedFiles(outputDirectory, includeDebug);
if (!extracted)
{
if (includeDebug) Console.Error.WriteLine("Could not extract header-defined files");
return false;
}
return true;
}
// Currently unaware of any NE samples. That said, as they wouldn't have a .WISE section, it's unclear how such
// samples could be identified.
/// <summary>
/// Extract the predefined, static files defined in the header
/// </summary>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the files extracted successfully, false otherwise</returns>
private bool ExtractHeaderDefinedFiles(string outputDirectory, bool includeDebug)
{
lock (_dataSourceLock)
{
// Seek to the compressed data offset
_dataSource.Seek(CompressedDataOffset, SeekOrigin.Begin);
bool successful = true;
// Extract first executable, if it exists
if (ExtractFile("FirstExecutable.exe", outputDirectory, FirstExecutableFileEntryLength, includeDebug) != ExtractionStatus.GOOD)
successful = false;
// Extract second executable, if it exists
// If there's a size provided for the second executable but no size for the first executable, the size of
// the second executable appears to be some unrelated value that's larger than the second executable
// actually is. Currently unable to extract properly in these cases, as no header value in such installers
// seems to actually correspond to the real size of the second executable.
if (ExtractFile("SecondExecutable.exe", outputDirectory, SecondExecutableFileEntryLength, includeDebug) != ExtractionStatus.GOOD)
successful = false;
// Extract third executable, if it exists
if (ExtractFile("ThirdExecutable.exe", outputDirectory, ThirdExecutableFileEntryLength, includeDebug) != ExtractionStatus.GOOD)
successful = false;
// Extract main MSI file
if (ExtractFile("ExtractedMsi.msi", outputDirectory, MsiFileEntryLength, includeDebug) != ExtractionStatus.GOOD)
{
// Fallback- seek to the position that's the length of the MSI file entry from the end, then try and
// extract from there.
_dataSource.Seek(-MsiFileEntryLength + 1, SeekOrigin.End);
if (ExtractFile("ExtractedMsi.msi", outputDirectory, MsiFileEntryLength, includeDebug) != ExtractionStatus.GOOD)
return false; // The fallback also failed.
}
return successful;
}
}
/// <summary>
/// Attempt to extract a file defined by a filename
/// </summary>
/// <param name="filename">Output filename, null to auto-generate</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="entrySize">Expected size of the file plus crc32</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>Extraction status representing the final state</returns>
/// <remarks>Assumes that the current stream position is the end of where the data lives</remarks>
private ExtractionStatus ExtractFile(string filename,
string outputDirectory,
uint entrySize,
bool includeDebug)
{
if (includeDebug) Console.WriteLine($"Attempting to extract {filename}");
// Extract the file
var destination = new MemoryStream();
ExtractionStatus status;
if (!(Version != null && Version[1] == 0x01))
{
status = ExtractStreamWithChecksum(destination, entrySize, includeDebug);
}
else // hack for Codesited5.exe , very early and very strange.
{
status = ExtractStreamWithoutChecksum(destination, entrySize, includeDebug);
}
// If the extracted data is invalid
if (status != ExtractionStatus.GOOD || destination == null)
return status;
// 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);
// Write the output file
File.WriteAllBytes(filename, destination.ToArray());
return status;
}
/// <summary>
/// Extract source data with a trailing CRC-32 checksum
/// </summary>
/// <param name="destination">Stream where the file data will be written</param>
/// <param name="entrySize">Expected size of the file plus crc32</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns></returns>
private ExtractionStatus ExtractStreamWithChecksum(Stream destination, uint entrySize, bool includeDebug)
{
// Debug output
if (includeDebug) Console.WriteLine($"Offset: {_dataSource.Position:X8}, Expected Read: {entrySize}, Expected Write:{entrySize - 4}"); // clamp to zero
// Check the validity of the inputs
if (entrySize == 0)
{
if (includeDebug) Console.Error.WriteLine("Not attempting to extract, expected to read 0 bytes");
return ExtractionStatus.GOOD; // If size is 0, then it shouldn't be extracted
}
else if (entrySize > (_dataSource.Length - _dataSource.Position))
{
if (includeDebug) Console.Error.WriteLine($"Not attempting to extract, expected to read {entrySize} bytes but only {_dataSource.Position} bytes remain");
return ExtractionStatus.INVALID;
}
// Extract the file
try
{
byte[] actual = _dataSource.ReadBytes((int)entrySize - 4);
uint expectedCrc32 = _dataSource.ReadUInt32();
// Debug output
if (includeDebug) Console.WriteLine($"Expected CRC-32: {expectedCrc32:X8}");
byte[]? hashBytes = HashTool.GetByteArrayHashArray(actual, HashType.CRC32);
if (hashBytes != null)
{
uint actualCrc32 = BitConverter.ToUInt32(hashBytes, 0);
// Debug output
if (includeDebug) Console.WriteLine($"Actual CRC-32: {actualCrc32:X8}");
if (expectedCrc32 != actualCrc32)
{
if (includeDebug) Console.Error.WriteLine("Mismatched CRC-32 values!");
return ExtractionStatus.BAD_CRC;
}
}
destination.Write(actual, 0, actual.Length);
return ExtractionStatus.GOOD;
}
catch
{
if (includeDebug) Console.Error.WriteLine("Could not extract");
return ExtractionStatus.FAIL;
}
}
/// <summary>
/// Extract source data without a trailing CRC-32 checksum
/// </summary>
/// <param name="destination">Stream where the file data will be written</param>
/// <param name="entrySize">Expected size of the file</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns></returns>
private ExtractionStatus ExtractStreamWithoutChecksum(Stream destination, uint entrySize, bool includeDebug)
{
// Debug output
if (includeDebug) Console.WriteLine($"Offset: {_dataSource.Position:X8}, Expected Read: {entrySize}, Expected Write:{entrySize - 4}");
// Check the validity of the inputs
if (entrySize == 0)
{
if (includeDebug) Console.Error.WriteLine("Not attempting to extract, expected to read 0 bytes");
return ExtractionStatus.GOOD; // If size is 0, then it shouldn't be extracted
}
else if (entrySize > (_dataSource.Length - _dataSource.Position))
{
if (includeDebug) Console.Error.WriteLine($"Not attempting to extract, expected to read {entrySize} bytes but only {_dataSource.Position} bytes remain");
return ExtractionStatus.INVALID;
}
// Extract the file
try
{
byte[] actual = _dataSource.ReadBytes((int)entrySize);
// Debug output
if (includeDebug) Console.WriteLine("No CRC-32!");
destination.Write(actual, 0, actual.Length);
return ExtractionStatus.GOOD;
}
catch
{
if (includeDebug) Console.Error.WriteLine("Could not extract");
return ExtractionStatus.FAIL;
}
}
#endregion
}
}

View File

@@ -0,0 +1,65 @@
using System;
#if NET462_OR_GREATER || NETCOREAPP
using System.IO;
#endif
using SabreTools.Serialization.Interfaces;
#if NET462_OR_GREATER || NETCOREAPP
using SharpCompress.Compressors.Xz;
#endif
namespace SabreTools.Serialization.Wrappers
{
/// <summary>
/// This is a shell wrapper; one that does not contain
/// any actual parsing. It is used as a placeholder for
/// types that typically do not have models.
/// </summary>
public partial class XZ : IExtractable
{
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
if (_dataSource == null || !_dataSource.CanRead)
return false;
#if NET462_OR_GREATER || NETCOREAPP
try
{
// Try opening the stream
using var xzFile = new XZStream(_dataSource);
// Ensure directory separators are consistent
string filename = Filename != null
? Path.GetFileNameWithoutExtension(Filename)
: Guid.NewGuid().ToString();
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);
// Extract the file
using FileStream fs = File.OpenWrite(filename);
xzFile.CopyTo(fs);
fs.Flush();
return true;
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
#else
Console.WriteLine("Extraction is not supported for this framework!");
Console.WriteLine();
return false;
#endif
}
}
}

View File

@@ -1,9 +1,4 @@
using System;
using System.IO;
using SabreTools.Serialization.Interfaces;
#if NET462_OR_GREATER || NETCOREAPP
using SharpCompress.Compressors.Xz;
#endif
namespace SabreTools.Serialization.Wrappers
{
@@ -12,7 +7,7 @@ namespace SabreTools.Serialization.Wrappers
/// any actual parsing. It is used as a placeholder for
/// types that typically do not have models.
/// </summary>
public class XZ : WrapperBase, IExtractable
public partial class XZ : WrapperBase
{
#region Descriptive Properties
@@ -82,55 +77,5 @@ namespace SabreTools.Serialization.Wrappers
#endif
#endregion
#region Extraction
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
if (_dataSource == null || !_dataSource.CanRead)
return false;
#if NET462_OR_GREATER || NETCOREAPP
try
{
// Try opening the stream
using var xzFile = new XZStream(_dataSource);
// Ensure directory separators are consistent
string filename = Filename != null
? Path.GetFileNameWithoutExtension(Filename)
: Guid.NewGuid().ToString();
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);
// Extract the file
using FileStream fs = File.OpenWrite(filename);
xzFile.CopyTo(fs);
fs.Flush();
return true;
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
#else
Console.WriteLine("Extraction is not supported for this framework!");
Console.WriteLine();
return false;
#endif
}
#endregion
}
}

View File

@@ -0,0 +1,92 @@
using System;
using System.IO;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public partial class XZP : IExtractable
{
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// If we have no directory entries
if (DirectoryEntries == null || DirectoryEntries.Length == 0)
return false;
// Loop through and extract all files to the output
bool allExtracted = true;
for (int i = 0; i < DirectoryEntries.Length; i++)
{
allExtracted &= ExtractFile(i, outputDirectory, includeDebug);
}
return allExtracted;
}
/// <summary>
/// Extract a file from the XZP to an output directory by index
/// </summary>
/// <param name="index">File index to extract</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the file extracted, false otherwise</returns>
public bool ExtractFile(int index, string outputDirectory, bool includeDebug)
{
// If we have no directory entries
if (DirectoryEntries == null || DirectoryEntries.Length == 0)
return false;
// If we have no directory items
if (DirectoryItems == null || DirectoryItems.Length == 0)
return false;
// If the directory entry index is invalid
if (index < 0 || index >= DirectoryEntries.Length)
return false;
// Get the associated directory item
var directoryEntry = DirectoryEntries[index];
var directoryItem = Array.Find(DirectoryItems, di => di?.FileNameCRC == directoryEntry.FileNameCRC);
if (directoryItem == null)
return false;
// Load the item data
var data = ReadRangeFromSource((int)directoryEntry.EntryOffset, (int)directoryEntry.EntryLength);
if (data == null)
return false;
// If we have an invalid output directory
if (string.IsNullOrEmpty(outputDirectory))
return false;
// Ensure directory separators are consistent
string filename = directoryItem.Name ?? $"file{index}";
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);
// Try to write the data
try
{
// Open the output file for writing
using Stream fs = File.OpenWrite(filename);
fs.Write(data, 0, data.Length);
fs.Flush();
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
return true;
}
}
}

View File

@@ -1,11 +1,8 @@
using System;
using System.IO;
using SabreTools.IO.Extensions;
using SabreTools.Serialization.Interfaces;
namespace SabreTools.Serialization.Wrappers
{
public class XZP : WrapperBase<Models.XZP.File>, IExtractable
public partial class XZP : WrapperBase<Models.XZP.File>
{
#region Descriptive Properties
@@ -91,92 +88,5 @@ namespace SabreTools.Serialization.Wrappers
}
#endregion
#region Extraction
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
// If we have no directory entries
if (DirectoryEntries == null || DirectoryEntries.Length == 0)
return false;
// Loop through and extract all files to the output
bool allExtracted = true;
for (int i = 0; i < DirectoryEntries.Length; i++)
{
allExtracted &= ExtractFile(i, outputDirectory, includeDebug);
}
return allExtracted;
}
/// <summary>
/// Extract a file from the XZP to an output directory by index
/// </summary>
/// <param name="index">File index to extract</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>True if the file extracted, false otherwise</returns>
public bool ExtractFile(int index, string outputDirectory, bool includeDebug)
{
// If we have no directory entries
if (DirectoryEntries == null || DirectoryEntries.Length == 0)
return false;
// If we have no directory items
if (DirectoryItems == null || DirectoryItems.Length == 0)
return false;
// If the directory entry index is invalid
if (index < 0 || index >= DirectoryEntries.Length)
return false;
// Get the associated directory item
var directoryEntry = DirectoryEntries[index];
var directoryItem = Array.Find(DirectoryItems, di => di?.FileNameCRC == directoryEntry.FileNameCRC);
if (directoryItem == null)
return false;
// Load the item data
var data = ReadRangeFromSource((int)directoryEntry.EntryOffset, (int)directoryEntry.EntryLength);
if (data == null)
return false;
// If we have an invalid output directory
if (string.IsNullOrEmpty(outputDirectory))
return false;
// Ensure directory separators are consistent
string filename = directoryItem.Name ?? $"file{index}";
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);
// Try to write the data
try
{
// Open the output file for writing
using Stream fs = File.OpenWrite(filename);
fs.Write(data, 0, data.Length);
fs.Flush();
}
catch (Exception ex)
{
if (includeDebug) Console.Error.WriteLine(ex);
return false;
}
return true;
}
#endregion
}
}