mirror of
https://github.com/SabreTools/SabreTools.Serialization.git
synced 2026-07-08 18:06:41 +00:00
Make partial classes for extraction
This commit is contained in:
111
SabreTools.Serialization/Wrappers/BFPK.Extraction.cs
Normal file
111
SabreTools.Serialization/Wrappers/BFPK.Extraction.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
95
SabreTools.Serialization/Wrappers/BSP.Extraction.cs
Normal file
95
SabreTools.Serialization/Wrappers/BSP.Extraction.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
53
SabreTools.Serialization/Wrappers/BZip2.Extraction.cs
Normal file
53
SabreTools.Serialization/Wrappers/BZip2.Extraction.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
135
SabreTools.Serialization/Wrappers/CFB.Extraction.cs
Normal file
135
SabreTools.Serialization/Wrappers/CFB.Extraction.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
114
SabreTools.Serialization/Wrappers/GCF.Extraction.cs
Normal file
114
SabreTools.Serialization/Wrappers/GCF.Extraction.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
71
SabreTools.Serialization/Wrappers/GZip.Extraction.cs
Normal file
71
SabreTools.Serialization/Wrappers/GZip.Extraction.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
66
SabreTools.Serialization/Wrappers/LZKWAJ.Extraction.cs
Normal file
66
SabreTools.Serialization/Wrappers/LZKWAJ.Extraction.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
62
SabreTools.Serialization/Wrappers/LZQBasic.Extraction.cs
Normal file
62
SabreTools.Serialization/Wrappers/LZQBasic.Extraction.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
102
SabreTools.Serialization/Wrappers/LZSZDD.Extraction.cs
Normal file
102
SabreTools.Serialization/Wrappers/LZSZDD.Extraction.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
292
SabreTools.Serialization/Wrappers/MicrosoftCabinet.Extraction.cs
Normal file
292
SabreTools.Serialization/Wrappers/MicrosoftCabinet.Extraction.cs
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
79
SabreTools.Serialization/Wrappers/MoPaQ.Extraction.cs
Normal file
79
SabreTools.Serialization/Wrappers/MoPaQ.Extraction.cs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
226
SabreTools.Serialization/Wrappers/NewExecutable.Extraction.cs
Normal file
226
SabreTools.Serialization/Wrappers/NewExecutable.Extraction.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
83
SabreTools.Serialization/Wrappers/PAK.Extraction.cs
Normal file
83
SabreTools.Serialization/Wrappers/PAK.Extraction.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
88
SabreTools.Serialization/Wrappers/PFF.Extraction.cs
Normal file
88
SabreTools.Serialization/Wrappers/PFF.Extraction.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
164
SabreTools.Serialization/Wrappers/PKZIP.Extraction.cs
Normal file
164
SabreTools.Serialization/Wrappers/PKZIP.Extraction.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
116
SabreTools.Serialization/Wrappers/Quantum.Extraction.cs
Normal file
116
SabreTools.Serialization/Wrappers/Quantum.Extraction.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
222
SabreTools.Serialization/Wrappers/RAR.Extraction.cs
Normal file
222
SabreTools.Serialization/Wrappers/RAR.Extraction.cs
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
152
SabreTools.Serialization/Wrappers/SGA.Extraction.cs
Normal file
152
SabreTools.Serialization/Wrappers/SGA.Extraction.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
224
SabreTools.Serialization/Wrappers/SevenZip.Extraction.cs
Normal file
224
SabreTools.Serialization/Wrappers/SevenZip.Extraction.cs
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
170
SabreTools.Serialization/Wrappers/TapeArchive.Extraction.cs
Normal file
170
SabreTools.Serialization/Wrappers/TapeArchive.Extraction.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
95
SabreTools.Serialization/Wrappers/VBSP.Extraction.cs
Normal file
95
SabreTools.Serialization/Wrappers/VBSP.Extraction.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
146
SabreTools.Serialization/Wrappers/VPK.Extraction.cs
Normal file
146
SabreTools.Serialization/Wrappers/VPK.Extraction.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
83
SabreTools.Serialization/Wrappers/WAD3.Extraction.cs
Normal file
83
SabreTools.Serialization/Wrappers/WAD3.Extraction.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
65
SabreTools.Serialization/Wrappers/XZ.Extraction.cs
Normal file
65
SabreTools.Serialization/Wrappers/XZ.Extraction.cs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
92
SabreTools.Serialization/Wrappers/XZP.Extraction.cs
Normal file
92
SabreTools.Serialization/Wrappers/XZP.Extraction.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user