diff --git a/SabreTools.Serialization/Wrappers/BFPK.Extraction.cs b/SabreTools.Serialization/Wrappers/BFPK.Extraction.cs
new file mode 100644
index 00000000..71c89492
--- /dev/null
+++ b/SabreTools.Serialization/Wrappers/BFPK.Extraction.cs
@@ -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
+ {
+ ///
+ 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;
+ }
+
+ ///
+ /// Extract a file from the BFPK to an output directory by index
+ ///
+ /// File index to extract
+ /// Output directory to write to
+ /// True to include debug data, false otherwise
+ /// True if the file extracted, false otherwise
+ public bool ExtractFile(int index, string outputDirectory, bool includeDebug)
+ {
+ // If we have no 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;
+ }
+ }
+}
diff --git a/SabreTools.Serialization/Wrappers/BFPK.cs b/SabreTools.Serialization/Wrappers/BFPK.cs
index bcad47a9..b3a2b771 100644
--- a/SabreTools.Serialization/Wrappers/BFPK.cs
+++ b/SabreTools.Serialization/Wrappers/BFPK.cs
@@ -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, IExtractable
+ public partial class BFPK : WrapperBase
{
#region Descriptive Properties
@@ -90,110 +86,5 @@ namespace SabreTools.Serialization.Wrappers
}
#endregion
-
- #region Extraction
-
- ///
- 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;
- }
-
- ///
- /// Extract a file from the BFPK to an output directory by index
- ///
- /// File index to extract
- /// Output directory to write to
- /// True to include debug data, false otherwise
- /// True if the file extracted, false otherwise
- public bool ExtractFile(int index, string outputDirectory, bool includeDebug)
- {
- // If we have no 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
}
}
diff --git a/SabreTools.Serialization/Wrappers/BSP.Extraction.cs b/SabreTools.Serialization/Wrappers/BSP.Extraction.cs
new file mode 100644
index 00000000..a61cf128
--- /dev/null
+++ b/SabreTools.Serialization/Wrappers/BSP.Extraction.cs
@@ -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
+ {
+ ///
+ 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;
+ }
+
+ ///
+ /// Extract a lump from the BSP to an output directory by index
+ ///
+ /// Lump index to extract
+ /// Output directory to write to
+ /// True to include debug data, false otherwise
+ /// True if the lump extracted, false otherwise
+ 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;
+ }
+ }
+}
diff --git a/SabreTools.Serialization/Wrappers/BSP.cs b/SabreTools.Serialization/Wrappers/BSP.cs
index d8def264..fc9d9c34 100644
--- a/SabreTools.Serialization/Wrappers/BSP.cs
+++ b/SabreTools.Serialization/Wrappers/BSP.cs
@@ -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, IExtractable
+ public partial class BSP : WrapperBase
{
#region Descriptive Properties
@@ -89,94 +86,5 @@ namespace SabreTools.Serialization.Wrappers
}
#endregion
-
- #region Extraction
-
- ///
- 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;
- }
-
- ///
- /// Extract a lump from the BSP to an output directory by index
- ///
- /// Lump index to extract
- /// Output directory to write to
- /// True to include debug data, false otherwise
- /// True if the lump extracted, false otherwise
- 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
}
}
diff --git a/SabreTools.Serialization/Wrappers/BZip2.Extraction.cs b/SabreTools.Serialization/Wrappers/BZip2.Extraction.cs
new file mode 100644
index 00000000..220ad279
--- /dev/null
+++ b/SabreTools.Serialization/Wrappers/BZip2.Extraction.cs
@@ -0,0 +1,53 @@
+using System;
+using System.IO;
+using SabreTools.IO.Compression.BZip2;
+using SabreTools.Serialization.Interfaces;
+
+namespace SabreTools.Serialization.Wrappers
+{
+ ///
+ /// 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.
+ ///
+ public partial class BZip2 : IExtractable
+ {
+ ///
+ 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;
+ }
+ }
+ }
+}
diff --git a/SabreTools.Serialization/Wrappers/BZip2.cs b/SabreTools.Serialization/Wrappers/BZip2.cs
index 50b49164..e98b8a40 100644
--- a/SabreTools.Serialization/Wrappers/BZip2.cs
+++ b/SabreTools.Serialization/Wrappers/BZip2.cs
@@ -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.
///
- public class BZip2 : WrapperBase, IExtractable
+ public partial class BZip2 : WrapperBase
{
#region Descriptive Properties
@@ -76,52 +73,10 @@ namespace SabreTools.Serialization.Wrappers
#if NETCOREAPP
///
- public override string ExportJSON() => throw new NotImplementedException();
+ public override string ExportJSON() => throw new System.NotImplementedException();
#endif
#endregion
-
- #region Extraction
-
- ///
- 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
}
}
diff --git a/SabreTools.Serialization/Wrappers/CFB.Extraction.cs b/SabreTools.Serialization/Wrappers/CFB.Extraction.cs
new file mode 100644
index 00000000..a1de1461
--- /dev/null
+++ b/SabreTools.Serialization/Wrappers/CFB.Extraction.cs
@@ -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
+ {
+ ///
+ 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;
+ }
+
+ ///
+ /// Extract a file from the CFB to an output directory by index
+ ///
+ /// Entry index to extract
+ /// Output directory to write to
+ /// True to include debug data, false otherwise
+ /// True if the file extracted, false otherwise
+ 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;
+ }
+
+ ///
+ /// Read the entry data for a single directory entry, if possible
+ ///
+ /// Entry to try to retrieve data for
+ /// Byte array representing the entry data on success, null otherwise
+ 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;
+ }
+ }
+}
diff --git a/SabreTools.Serialization/Wrappers/CFB.cs b/SabreTools.Serialization/Wrappers/CFB.cs
index 9f2e59d9..94cda190 100644
--- a/SabreTools.Serialization/Wrappers/CFB.cs
+++ b/SabreTools.Serialization/Wrappers/CFB.cs
@@ -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, IExtractable
+ public partial class CFB : WrapperBase
{
#region Descriptive Properties
@@ -136,133 +134,6 @@ namespace SabreTools.Serialization.Wrappers
#endregion
- #region Extraction
-
- ///
- 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;
- }
-
- ///
- /// Extract a file from the CFB to an output directory by index
- ///
- /// Entry index to extract
- /// Output directory to write to
- /// True to include debug data, false otherwise
- /// True if the file extracted, false otherwise
- 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;
- }
-
- ///
- /// Read the entry data for a single directory entry, if possible
- ///
- /// Entry to try to retrieve data for
- /// Byte array representing the entry data on success, null otherwise
- 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
///
diff --git a/SabreTools.Serialization/Wrappers/GCF.Extraction.cs b/SabreTools.Serialization/Wrappers/GCF.Extraction.cs
new file mode 100644
index 00000000..5096d398
--- /dev/null
+++ b/SabreTools.Serialization/Wrappers/GCF.Extraction.cs
@@ -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
+ {
+ ///
+ 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;
+ }
+
+ ///
+ /// Extract a file from the GCF to an output directory by index
+ ///
+ /// File index to extract
+ /// Output directory to write to
+ /// True to include debug data, false otherwise
+ /// True if the file extracted, false otherwise
+ public bool ExtractFile(int index, string outputDirectory, bool includeDebug)
+ {
+ // If we have no 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();
+ 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;
+ }
+ }
+}
diff --git a/SabreTools.Serialization/Wrappers/GCF.cs b/SabreTools.Serialization/Wrappers/GCF.cs
index 293550a4..62c00c3b 100644
--- a/SabreTools.Serialization/Wrappers/GCF.cs
+++ b/SabreTools.Serialization/Wrappers/GCF.cs
@@ -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, IExtractable
+ public partial class GCF : WrapperBase
{
#region Descriptive Properties
@@ -232,114 +229,6 @@ namespace SabreTools.Serialization.Wrappers
#endregion
- #region Extraction
-
- ///
- 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;
- }
-
- ///
- /// Extract a file from the GCF to an output directory by index
- ///
- /// File index to extract
- /// Output directory to write to
- /// True to include debug data, false otherwise
- /// True if the file extracted, false otherwise
- public bool ExtractFile(int index, string outputDirectory, bool includeDebug)
- {
- // If we have no 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();
- 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
///
diff --git a/SabreTools.Serialization/Wrappers/GZip.Extraction.cs b/SabreTools.Serialization/Wrappers/GZip.Extraction.cs
new file mode 100644
index 00000000..fec9f94c
--- /dev/null
+++ b/SabreTools.Serialization/Wrappers/GZip.Extraction.cs
@@ -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
+ {
+ ///
+ 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;
+ }
+ }
+ }
+}
diff --git a/SabreTools.Serialization/Wrappers/GZip.cs b/SabreTools.Serialization/Wrappers/GZip.cs
index cda154f6..e2c53d6a 100644
--- a/SabreTools.Serialization/Wrappers/GZip.cs
+++ b/SabreTools.Serialization/Wrappers/GZip.cs
@@ -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, IExtractable
+ public partial class GZip : WrapperBase
{
#region Descriptive Properties
@@ -138,69 +135,5 @@ namespace SabreTools.Serialization.Wrappers
}
#endregion
-
- #region Extraction
-
- ///
- 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
}
}
diff --git a/SabreTools.Serialization/Wrappers/InstallShieldArchiveV3.Extraction.cs b/SabreTools.Serialization/Wrappers/InstallShieldArchiveV3.Extraction.cs
new file mode 100644
index 00000000..5a4c6363
--- /dev/null
+++ b/SabreTools.Serialization/Wrappers/InstallShieldArchiveV3.Extraction.cs
@@ -0,0 +1,127 @@
+using System;
+using System.IO;
+using SabreTools.IO.Compression.Blast;
+using SabreTools.Serialization.Interfaces;
+
+namespace SabreTools.Serialization.Wrappers
+{
+ ///
+ /// Reference (de)compressor: https://www.sac.sk/download/pack/icomp95.zip
+ ///
+ ///
+ public partial class InstallShieldArchiveV3 : IExtractable
+ {
+ ///
+ 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;
+ }
+
+ ///
+ /// Extract a file from the ISAv3 to an output directory by index
+ ///
+ /// File index to extract
+ /// Output directory to write to
+ /// True to include debug data, false otherwise
+ /// True if the file extracted, false otherwise
+ public bool ExtractFile(int index, string outputDirectory, bool includeDebug)
+ {
+ // If 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;
+ }
+ }
+}
diff --git a/SabreTools.Serialization/Wrappers/InstallShieldArchiveV3.cs b/SabreTools.Serialization/Wrappers/InstallShieldArchiveV3.cs
index 5d27a75b..26440f2b 100644
--- a/SabreTools.Serialization/Wrappers/InstallShieldArchiveV3.cs
+++ b/SabreTools.Serialization/Wrappers/InstallShieldArchiveV3.cs
@@ -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
///
///
- public partial class InstallShieldArchiveV3 : WrapperBase, IExtractable
+ public partial class InstallShieldArchiveV3 : WrapperBase
{
#region Descriptive Properties
@@ -176,122 +172,5 @@ namespace SabreTools.Serialization.Wrappers
}
#endregion
-
- #region Extraction
-
- ///
- 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;
- }
-
- ///
- /// Extract a file from the ISAv3 to an output directory by index
- ///
- /// File index to extract
- /// Output directory to write to
- /// True to include debug data, false otherwise
- /// True if the file extracted, false otherwise
- public bool ExtractFile(int index, string outputDirectory, bool includeDebug)
- {
- // If 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
}
}
diff --git a/SabreTools.Serialization/Wrappers/InstallShieldCabinet.Extraction.cs b/SabreTools.Serialization/Wrappers/InstallShieldCabinet.Extraction.cs
new file mode 100644
index 00000000..6e301577
--- /dev/null
+++ b/SabreTools.Serialization/Wrappers/InstallShieldCabinet.Extraction.cs
@@ -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, IExtractable
+ {
+ #region Extension Properties
+
+ ///
+ /// Reference to the next cabinet header
+ ///
+ /// Only used in multi-file
+ public InstallShieldCabinet? Next { get; set; }
+
+ ///
+ /// Reference to the next previous header
+ ///
+ /// Only used in multi-file
+ public InstallShieldCabinet? Prev { get; set; }
+
+ ///
+ /// Volume index ID, 0 for headers
+ ///
+ /// Only used in multi-file
+ public ushort VolumeID { get; set; }
+
+ #endregion
+
+ #region Extraction State
+
+ ///
+ /// Base filename path for related CAB files
+ ///
+ internal string? FilenamePattern { get; set; }
+
+ #endregion
+
+ #region Constants
+
+ ///
+ /// Default buffer size
+ ///
+ private const int BUFFER_SIZE = 64 * 1024;
+
+ ///
+ /// Maximum size of the window in bits
+ ///
+ private const int MAX_WBITS = 15;
+
+ #endregion
+
+ #region Cabinet Set
+
+ ///
+ /// Open a cabinet set for reading, if possible
+ ///
+ /// Filename pattern for matching cabinet files
+ /// Wrapper representing the set, null on error
+ 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;
+ }
+
+ ///
+ /// Open the numbered cabinet set volume
+ ///
+ /// Volume ID, 1-indexed
+ /// Wrapper representing the volume on success, null otherwise
+ 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;
+ }
+
+ ///
+ /// Open a cabinet file for reading
+ ///
+ /// Cabinet part index to be opened
+ /// Cabinet files suffix (e.g. `.cab`)
+ /// A Stream representing the cabinet part, null on error
+ public Stream? OpenFileForReading(int index, string suffix)
+ => OpenFileForReading(FilenamePattern, index, suffix);
+
+ ///
+ /// Create the generic filename pattern to look for from the input filename
+ ///
+ /// String representing the filename pattern for a cabinet set, null on error
+ 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);
+ }
+
+ ///
+ /// Open a cabinet file for reading
+ ///
+ /// Filename pattern for matching cabinet files
+ /// Cabinet part index to be opened
+ /// Cabinet files suffix (e.g. `.cab`)
+ /// A Stream representing the cabinet part, null on error
+ 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
+
+ ///
+ 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;
+ }
+ }
+
+ ///
+ /// Save the file at the given index to the filename specified
+ ///
+ 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;
+ }
+
+ ///
+ /// Save the file at the given index to the filename specified as raw
+ ///
+ 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;
+ }
+
+ ///
+ /// Uncompress a source byte array to a destination
+ ///
+ 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);
+ }
+ }
+
+ ///
+ /// Uncompress a source byte array to a destination (old version)
+ ///
+ 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
+
+ ///
+ /// Deobfuscate a buffer
+ ///
+ public static void Deobfuscate(byte[] buffer, long size, ref uint offset)
+ {
+ offset = Deobfuscate(buffer, size, offset);
+ }
+
+ ///
+ /// Deobfuscate a buffer with a seed value
+ ///
+ /// Seed is 0 at file start
+ 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;
+ }
+
+ ///
+ /// Obfuscate a buffer
+ ///
+ public static void Obfuscate(byte[] buffer, long size, ref uint offset)
+ {
+ offset = Obfuscate(buffer, size, offset);
+ }
+
+ ///
+ /// Obfuscate a buffer with a seed value
+ ///
+ /// Seed is 0 at file start
+ 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;
+ }
+
+ ///
+ /// Rotate Right 8
+ ///
+ private static int ROR8(int x, byte n) => (x >> n) | (x << (8 - n));
+
+ ///
+ /// Rotate Left 8
+ ///
+ private static int ROL8(int x, byte n) => (x << n) | (x >> (8 - n));
+
+ #endregion
+
+ #region Helper Classes
+
+ ///
+ /// Helper to read a single file from a cabinet set
+ ///
+ private class Reader : IDisposable
+ {
+ #region Private Instance Variables
+
+ ///
+ /// Cabinet file to read from
+ ///
+ private readonly InstallShieldCabinet _cabinet;
+
+ ///
+ /// Currently selected index
+ ///
+ private readonly uint _index;
+
+ ///
+ /// File descriptor defining the currently selected index
+ ///
+ private readonly FileDescriptor _fileDescriptor;
+
+ ///
+ /// Offset in the data where the file exists
+ ///
+ private ulong _dataOffset;
+
+ ///
+ /// Number of bytes left in the current volume
+ ///
+ private ulong _volumeBytesLeft;
+
+ ///
+ /// Handle to the current volume stream
+ ///
+ private Stream? _volumeFile;
+
+ ///
+ /// Current volume header
+ ///
+ private VolumeHeader? _volumeHeader;
+
+ ///
+ /// Current volume ID
+ ///
+ private ushort _volumeId;
+
+ ///
+ /// Offset for obfuscation seed
+ ///
+ private uint _obfuscationOffset;
+
+ #endregion
+
+ #region Constructors
+
+ private Reader(InstallShieldCabinet cabinet, uint index, FileDescriptor fileDescriptor)
+ {
+ _cabinet = cabinet;
+ _index = index;
+ _fileDescriptor = fileDescriptor;
+ }
+
+ #endregion
+
+ ///
+ /// Create a new from an existing cabinet, index, and file descriptor
+ ///
+ 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;
+ }
+
+ ///
+ /// Dispose of the current object
+ ///
+ public void Dispose()
+ {
+ _volumeFile?.Close();
+ }
+
+ #region Reading
+
+ ///
+ /// Read a certain number of bytes from the current volume
+ ///
+ 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;
+ }
+
+ ///
+ /// Open the next volume based on the current index
+ ///
+ private bool OpenNextVolume(out ushort nextVolume)
+ {
+ nextVolume = (ushort)(_volumeId + 1);
+ return OpenVolume(nextVolume);
+ }
+
+ ///
+ /// Open the volume at the inputted index
+ ///
+ 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
+ }
+}
diff --git a/SabreTools.Serialization/Wrappers/InstallShieldCabinet.cs b/SabreTools.Serialization/Wrappers/InstallShieldCabinet.cs
index 84be99fd..0cbaa1a9 100644
--- a/SabreTools.Serialization/Wrappers/InstallShieldCabinet.cs
+++ b/SabreTools.Serialization/Wrappers/InstallShieldCabinet.cs
@@ -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, IExtractable
+ public partial class InstallShieldCabinet : WrapperBase
{
#region Descriptive Properties
@@ -68,47 +63,6 @@ namespace SabreTools.Serialization.Wrappers
///
public VolumeHeader? VolumeHeader => Model.VolumeHeader;
- ///
- /// Reference to the next cabinet header
- ///
- /// Only used in multi-file
- public InstallShieldCabinet? Next { get; set; }
-
- ///
- /// Reference to the next previous header
- ///
- /// Only used in multi-file
- public InstallShieldCabinet? Prev { get; set; }
-
- ///
- /// Volume index ID, 0 for headers
- ///
- /// Only used in multi-file
- public ushort VolumeID { get; set; }
-
- #endregion
-
- #region Extraction State
-
- ///
- /// Base filename path for related CAB files
- ///
- internal string? FilenamePattern { get; set; }
-
- #endregion
-
- #region Constants
-
- ///
- /// Default buffer size
- ///
- private const int BUFFER_SIZE = 64 * 1024;
-
- ///
- /// Maximum size of the window in bits
- ///
- private const int MAX_WBITS = 15;
-
#endregion
#region Constructors
@@ -179,148 +133,6 @@ namespace SabreTools.Serialization.Wrappers
#endregion
- #region Cabinet Set
-
- ///
- /// Open a cabinet set for reading, if possible
- ///
- /// Filename pattern for matching cabinet files
- /// Wrapper representing the set, null on error
- 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;
- }
-
- ///
- /// Open the numbered cabinet set volume
- ///
- /// Volume ID, 1-indexed
- /// Wrapper representing the volume on success, null otherwise
- 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;
- }
-
- ///
- /// Open a cabinet file for reading
- ///
- /// Cabinet part index to be opened
- /// Cabinet files suffix (e.g. `.cab`)
- /// A Stream representing the cabinet part, null on error
- public Stream? OpenFileForReading(int index, string suffix)
- => OpenFileForReading(FilenamePattern, index, suffix);
-
- ///
- /// Create the generic filename pattern to look for from the input filename
- ///
- /// String representing the filename pattern for a cabinet set, null on error
- 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);
- }
-
- ///
- /// Open a cabinet file for reading
- ///
- /// Filename pattern for matching cabinet files
- /// Cabinet part index to be opened
- /// Cabinet files suffix (e.g. `.cab`)
- /// A Stream representing the cabinet part, null on error
- 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
///
@@ -374,336 +186,6 @@ namespace SabreTools.Serialization.Wrappers
#endregion
- #region Extraction
-
- ///
- 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;
- }
- }
-
- ///
- /// Save the file at the given index to the filename specified
- ///
- 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;
- }
-
- ///
- /// Save the file at the given index to the filename specified as raw
- ///
- 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;
- }
-
- ///
- /// Uncompress a source byte array to a destination
- ///
- 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);
- }
- }
-
- ///
- /// Uncompress a source byte array to a destination (old version)
- ///
- 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
///
@@ -882,312 +364,5 @@ namespace SabreTools.Serialization.Wrappers
=> GetFileGroupFromFile(index)?.Name;
#endregion
-
- #region Obfuscation
-
- ///
- /// Deobfuscate a buffer
- ///
- public static void Deobfuscate(byte[] buffer, long size, ref uint offset)
- {
- offset = Deobfuscate(buffer, size, offset);
- }
-
- ///
- /// Deobfuscate a buffer with a seed value
- ///
- /// Seed is 0 at file start
- 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;
- }
-
- ///
- /// Obfuscate a buffer
- ///
- public static void Obfuscate(byte[] buffer, long size, ref uint offset)
- {
- offset = Obfuscate(buffer, size, offset);
- }
-
- ///
- /// Obfuscate a buffer with a seed value
- ///
- /// Seed is 0 at file start
- 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;
- }
-
- ///
- /// Rotate Right 8
- ///
- private static int ROR8(int x, byte n) => (x >> n) | (x << (8 - n));
-
- ///
- /// Rotate Left 8
- ///
- private static int ROL8(int x, byte n) => (x << n) | (x >> (8 - n));
-
- #endregion
-
- #region Helper Classes
-
- ///
- /// Helper to read a single file from a cabinet set
- ///
- private class Reader : IDisposable
- {
- #region Private Instance Variables
-
- ///
- /// Cabinet file to read from
- ///
- private readonly InstallShieldCabinet _cabinet;
-
- ///
- /// Currently selected index
- ///
- private readonly uint _index;
-
- ///
- /// File descriptor defining the currently selected index
- ///
- private readonly FileDescriptor _fileDescriptor;
-
- ///
- /// Offset in the data where the file exists
- ///
- private ulong _dataOffset;
-
- ///
- /// Number of bytes left in the current volume
- ///
- private ulong _volumeBytesLeft;
-
- ///
- /// Handle to the current volume stream
- ///
- private Stream? _volumeFile;
-
- ///
- /// Current volume header
- ///
- private VolumeHeader? _volumeHeader;
-
- ///
- /// Current volume ID
- ///
- private ushort _volumeId;
-
- ///
- /// Offset for obfuscation seed
- ///
- private uint _obfuscationOffset;
-
- #endregion
-
- #region Constructors
-
- private Reader(InstallShieldCabinet cabinet, uint index, FileDescriptor fileDescriptor)
- {
- _cabinet = cabinet;
- _index = index;
- _fileDescriptor = fileDescriptor;
- }
-
- #endregion
-
- ///
- /// Create a new from an existing cabinet, index, and file descriptor
- ///
- 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;
- }
-
- ///
- /// Dispose of the current object
- ///
- public void Dispose()
- {
- _volumeFile?.Close();
- }
-
- #region Reading
-
- ///
- /// Read a certain number of bytes from the current volume
- ///
- 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;
- }
-
- ///
- /// Open the next volume based on the current index
- ///
- private bool OpenNextVolume(out ushort nextVolume)
- {
- nextVolume = (ushort)(_volumeId + 1);
- return OpenVolume(nextVolume);
- }
-
- ///
- /// Open the volume at the inputted index
- ///
- 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
}
}
diff --git a/SabreTools.Serialization/Wrappers/LZKWAJ.Extraction.cs b/SabreTools.Serialization/Wrappers/LZKWAJ.Extraction.cs
new file mode 100644
index 00000000..236695a6
--- /dev/null
+++ b/SabreTools.Serialization/Wrappers/LZKWAJ.Extraction.cs
@@ -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
+ {
+ ///
+ 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;
+ }
+ }
+}
diff --git a/SabreTools.Serialization/Wrappers/LZKWAJ.cs b/SabreTools.Serialization/Wrappers/LZKWAJ.cs
index aed3bcd7..04a5106f 100644
--- a/SabreTools.Serialization/Wrappers/LZKWAJ.cs
+++ b/SabreTools.Serialization/Wrappers/LZKWAJ.cs
@@ -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, IExtractable
+ public partial class LZKWAJ : WrapperBase
{
#region Descriptive Properties
@@ -99,65 +95,5 @@ namespace SabreTools.Serialization.Wrappers
}
#endregion
-
- #region Extraction
-
- ///
- 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
}
}
diff --git a/SabreTools.Serialization/Wrappers/LZQBasic.Extraction.cs b/SabreTools.Serialization/Wrappers/LZQBasic.Extraction.cs
new file mode 100644
index 00000000..195645cc
--- /dev/null
+++ b/SabreTools.Serialization/Wrappers/LZQBasic.Extraction.cs
@@ -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
+ {
+ ///
+ 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;
+ }
+ }
+}
diff --git a/SabreTools.Serialization/Wrappers/LZQBasic.cs b/SabreTools.Serialization/Wrappers/LZQBasic.cs
index 30c30360..1d5f4fc4 100644
--- a/SabreTools.Serialization/Wrappers/LZQBasic.cs
+++ b/SabreTools.Serialization/Wrappers/LZQBasic.cs
@@ -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, IExtractable
+ public partial class LZQBasic : WrapperBase
{
#region Descriptive Properties
@@ -83,61 +79,5 @@ namespace SabreTools.Serialization.Wrappers
}
#endregion
-
- #region Extraction
-
- ///
- 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
}
}
diff --git a/SabreTools.Serialization/Wrappers/LZSZDD.Extraction.cs b/SabreTools.Serialization/Wrappers/LZSZDD.Extraction.cs
new file mode 100644
index 00000000..89ca44de
--- /dev/null
+++ b/SabreTools.Serialization/Wrappers/LZSZDD.Extraction.cs
@@ -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
+ {
+ ///
+ public bool Extract(string outputDirectory, bool includeDebug)
+ => Extract(string.Empty, outputDirectory, includeDebug);
+
+ ///
+ /// Original name of the file to convert to the output name
+ 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;
+ }
+
+ ///
+ /// Get the full name of the input file
+ ///
+ 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;
+ }
+ }
+}
diff --git a/SabreTools.Serialization/Wrappers/LZSZDD.cs b/SabreTools.Serialization/Wrappers/LZSZDD.cs
index 9973c879..a645406a 100644
--- a/SabreTools.Serialization/Wrappers/LZSZDD.cs
+++ b/SabreTools.Serialization/Wrappers/LZSZDD.cs
@@ -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, IExtractable
+ public partial class LZSZDD : WrapperBase
{
#region Descriptive Properties
@@ -90,101 +86,5 @@ namespace SabreTools.Serialization.Wrappers
}
#endregion
-
- #region Extraction
-
- ///
- public bool Extract(string outputDirectory, bool includeDebug)
- => Extract(string.Empty, outputDirectory, includeDebug);
-
- ///
- /// Original name of the file to convert to the output name
- 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;
- }
-
- ///
- /// Get the full name of the input file
- ///
- 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
}
}
diff --git a/SabreTools.Serialization/Wrappers/MicrosoftCabinet.Extraction.cs b/SabreTools.Serialization/Wrappers/MicrosoftCabinet.Extraction.cs
new file mode 100644
index 00000000..df360da9
--- /dev/null
+++ b/SabreTools.Serialization/Wrappers/MicrosoftCabinet.Extraction.cs
@@ -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
+
+ ///
+ /// Reference to the next cabinet header
+ ///
+ /// Only used in multi-file
+ public MicrosoftCabinet? Next { get; set; }
+
+ ///
+ /// Reference to the next previous header
+ ///
+ /// Only used in multi-file
+ public MicrosoftCabinet? Prev { get; set; }
+
+ #endregion
+
+ #region Cabinet Set
+
+ ///
+ /// Open a cabinet set for reading, if possible
+ ///
+ /// Filename for one cabinet in the set
+ /// Wrapper representing the set, null on error
+ 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;
+ }
+
+ ///
+ /// Open the next archive, if possible
+ ///
+ /// Filename for one cabinet in the set
+ 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);
+ }
+
+ ///
+ /// Open the previous archive, if possible
+ ///
+ /// Filename for one cabinet in the set
+ 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
+
+ ///
+ 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;
+ }
+ }
+
+ ///
+ /// Extract the contents of a single folder
+ ///
+ /// Filename for one cabinet in the set, if available
+ /// Path to the output directory
+ /// Folder containing the blocks to decompress
+ /// Index of the folder in the cabinet
+ /// True to include debug data, false otherwise
+ /// True if all files extracted, false otherwise
+ 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;
+ }
+
+ ///
+ /// Extract the contents of a single file
+ ///
+ /// Path to the output directory
+ /// Stream representing the uncompressed block data
+ /// File information
+ /// True to include debug data, false otherwise
+ /// True if the file extracted, false otherwise
+ private static bool 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
+
+ ///
+ /// 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).
+ ///
+ 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];
+ }
+
+ ///
+ /// Individual algorithmic step
+ ///
+ 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
+ }
+}
diff --git a/SabreTools.Serialization/Wrappers/MicrosoftCabinet.cs b/SabreTools.Serialization/Wrappers/MicrosoftCabinet.cs
index 6fdce386..4b01b77f 100644
--- a/SabreTools.Serialization/Wrappers/MicrosoftCabinet.cs
+++ b/SabreTools.Serialization/Wrappers/MicrosoftCabinet.cs
@@ -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, IExtractable
+ public partial class MicrosoftCabinet : WrapperBase
{
#region Descriptive Properties
@@ -36,21 +34,9 @@ namespace SabreTools.Serialization.Wrappers
///
public string? CabinetNext => Header?.CabinetNext;
- ///
- /// Reference to the next cabinet header
- ///
- /// Only used in multi-file
- public MicrosoftCabinet? Next { get; set; }
-
///
public string? CabinetPrev => Header?.CabinetPrev;
- ///
- /// Reference to the next previous header
- ///
- /// Only used in multi-file
- public MicrosoftCabinet? Prev { get; set; }
-
#endregion
#region Constructors
@@ -121,271 +107,6 @@ namespace SabreTools.Serialization.Wrappers
#endregion
- #region Extraction
-
- ///
- 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;
- }
- }
-
- ///
- /// Extract the contents of a single folder
- ///
- /// Filename for one cabinet in the set, if available
- /// Path to the output directory
- /// Folder containing the blocks to decompress
- /// Index of the folder in the cabinet
- /// True to include debug data, false otherwise
- /// True if all files extracted, false otherwise
- 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;
- }
-
- ///
- /// Extract the contents of a single file
- ///
- /// Path to the output directory
- /// Stream representing the uncompressed block data
- /// File information
- /// True to include debug data, false otherwise
- /// True if the file extracted, false otherwise
- private static bool 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
-
- ///
- /// Open a cabinet set for reading, if possible
- ///
- /// Filename for one cabinet in the set
- /// Wrapper representing the set, null on error
- 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;
- }
-
- ///
- /// Open the next archive, if possible
- ///
- /// Filename for one cabinet in the set
- 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);
- }
-
- ///
- /// Open the previous archive, if possible
- ///
- /// Filename for one cabinet in the set
- 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
-
- ///
- /// 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).
- ///
- 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];
- }
-
- ///
- /// Individual algorithmic step
- ///
- 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
///
diff --git a/SabreTools.Serialization/Wrappers/MoPaQ.Extraction.cs b/SabreTools.Serialization/Wrappers/MoPaQ.Extraction.cs
new file mode 100644
index 00000000..5c058bf0
--- /dev/null
+++ b/SabreTools.Serialization/Wrappers/MoPaQ.Extraction.cs
@@ -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
+ {
+ ///
+ 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
+ }
+ }
+}
diff --git a/SabreTools.Serialization/Wrappers/MoPaQ.cs b/SabreTools.Serialization/Wrappers/MoPaQ.cs
index 9dd04130..ada9b35a 100644
--- a/SabreTools.Serialization/Wrappers/MoPaQ.cs
+++ b/SabreTools.Serialization/Wrappers/MoPaQ.cs
@@ -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, IExtractable
+ public partial class MoPaQ : WrapperBase
{
#region Descriptive Properties
@@ -100,77 +95,5 @@ namespace SabreTools.Serialization.Wrappers
}
#endregion
-
- #region Extraction
-
- ///
- 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
}
}
diff --git a/SabreTools.Serialization/Wrappers/NewExecutable.Extraction.cs b/SabreTools.Serialization/Wrappers/NewExecutable.Extraction.cs
new file mode 100644
index 00000000..2f853ad8
--- /dev/null
+++ b/SabreTools.Serialization/Wrappers/NewExecutable.Extraction.cs
@@ -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
+ {
+ ///
+ ///
+ /// This extracts the following data:
+ /// - Archives and executables in the overlay
+ /// - Wise installers
+ ///
+ public bool Extract(string outputDirectory, bool includeDebug)
+ {
+ bool overlay = ExtractFromOverlay(outputDirectory, includeDebug);
+ bool wise = ExtractWise(outputDirectory, includeDebug);
+
+ return overlay | wise;
+ }
+
+ ///
+ /// Extract data from the overlay
+ ///
+ /// Output directory to write to
+ /// True to include debug data, false otherwise
+ /// True if extraction succeeded, false otherwise
+ 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;
+ }
+ }
+
+ ///
+ /// Extract data from a Wise installer
+ ///
+ /// Output directory to write to
+ /// True to include debug data, false otherwise
+ /// True if extraction succeeded, false otherwise
+ 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;
+ }
+
+ ///
+ /// Extract using Wise overlay
+ ///
+ /// Output directory to write to
+ /// True to include debug data, false otherwise
+ /// Potentially multi-part stream to read
+ /// Offset to the start of the overlay header
+ /// True if extraction succeeded, false otherwise
+ 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);
+ }
+ }
+}
diff --git a/SabreTools.Serialization/Wrappers/NewExecutable.cs b/SabreTools.Serialization/Wrappers/NewExecutable.cs
index e3d3a2d0..8133b004 100644
--- a/SabreTools.Serialization/Wrappers/NewExecutable.cs
+++ b/SabreTools.Serialization/Wrappers/NewExecutable.cs
@@ -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, IExtractable
+ public partial class NewExecutable : WrapperBase
{
#region Descriptive Properties
@@ -354,225 +352,6 @@ namespace SabreTools.Serialization.Wrappers
#endregion
- #region Extraction
-
- ///
- ///
- /// This extracts the following data:
- /// - Archives and executables in the overlay
- /// - Wise installers
- ///
- public bool Extract(string outputDirectory, bool includeDebug)
- {
- bool overlay = ExtractFromOverlay(outputDirectory, includeDebug);
- bool wise = ExtractWise(outputDirectory, includeDebug);
-
- return overlay | wise;
- }
-
- ///
- /// Extract data from the overlay
- ///
- /// Output directory to write to
- /// True to include debug data, false otherwise
- /// True if extraction succeeded, false otherwise
- 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;
- }
- }
-
- ///
- /// Extract data from a Wise installer
- ///
- /// Output directory to write to
- /// True to include debug data, false otherwise
- /// True if extraction succeeded, false otherwise
- 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;
- }
-
- ///
- /// Extract using Wise overlay
- ///
- /// Output directory to write to
- /// True to include debug data, false otherwise
- /// Potentially multi-part stream to read
- /// Offset to the start of the overlay header
- /// True if extraction succeeded, false otherwise
- 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
///
diff --git a/SabreTools.Serialization/Wrappers/PAK.Extraction.cs b/SabreTools.Serialization/Wrappers/PAK.Extraction.cs
new file mode 100644
index 00000000..eaf7ad5d
--- /dev/null
+++ b/SabreTools.Serialization/Wrappers/PAK.Extraction.cs
@@ -0,0 +1,83 @@
+using System;
+using System.IO;
+using SabreTools.Serialization.Interfaces;
+
+namespace SabreTools.Serialization.Wrappers
+{
+ public partial class PAK : IExtractable
+ {
+ ///
+ 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;
+ }
+
+ ///
+ /// Extract a file from the PAK to an output directory by index
+ ///
+ /// File index to extract
+ /// Output directory to write to
+ /// True to include debug data, false otherwise
+ /// True if the file extracted, false otherwise
+ public bool ExtractFile(int index, string outputDirectory, bool includeDebug)
+ {
+ // If we have no 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;
+ }
+ }
+}
diff --git a/SabreTools.Serialization/Wrappers/PAK.cs b/SabreTools.Serialization/Wrappers/PAK.cs
index 4b9d7206..ef034b63 100644
--- a/SabreTools.Serialization/Wrappers/PAK.cs
+++ b/SabreTools.Serialization/Wrappers/PAK.cs
@@ -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, IExtractable
+ public partial class PAK : WrapperBase
{
#region Descriptive Properties
@@ -89,83 +86,5 @@ namespace SabreTools.Serialization.Wrappers
}
#endregion
-
- #region Extraction
-
- ///
- 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;
- }
-
- ///
- /// Extract a file from the PAK to an output directory by index
- ///
- /// File index to extract
- /// Output directory to write to
- /// True to include debug data, false otherwise
- /// True if the file extracted, false otherwise
- public bool ExtractFile(int index, string outputDirectory, bool includeDebug)
- {
- // If we have no 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
}
}
diff --git a/SabreTools.Serialization/Wrappers/PFF.Extraction.cs b/SabreTools.Serialization/Wrappers/PFF.Extraction.cs
new file mode 100644
index 00000000..001e6acc
--- /dev/null
+++ b/SabreTools.Serialization/Wrappers/PFF.Extraction.cs
@@ -0,0 +1,88 @@
+using System;
+using System.IO;
+using SabreTools.Serialization.Interfaces;
+
+namespace SabreTools.Serialization.Wrappers
+{
+ public partial class PFF : IExtractable
+ {
+ ///
+ 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;
+ }
+
+ ///
+ /// Extract a segment from the PFF to an output directory by index
+ ///
+ /// Segment index to extract
+ /// Output directory to write to
+ /// True to include debug data, false otherwise
+ /// True if the segment extracted, false otherwise
+ 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;
+ }
+ }
+ }
+}
diff --git a/SabreTools.Serialization/Wrappers/PFF.cs b/SabreTools.Serialization/Wrappers/PFF.cs
index c0a44adf..052b52ff 100644
--- a/SabreTools.Serialization/Wrappers/PFF.cs
+++ b/SabreTools.Serialization/Wrappers/PFF.cs
@@ -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, IExtractable
+ public partial class PFF : WrapperBase
{
#region Descriptive Properties
@@ -94,88 +91,5 @@ namespace SabreTools.Serialization.Wrappers
}
#endregion
-
- #region Extraction
-
- ///
- 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;
- }
-
- ///
- /// Extract a segment from the PFF to an output directory by index
- ///
- /// Segment index to extract
- /// Output directory to write to
- /// True to include debug data, false otherwise
- /// True if the segment extracted, false otherwise
- 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
}
}
diff --git a/SabreTools.Serialization/Wrappers/PKZIP.Extraction.cs b/SabreTools.Serialization/Wrappers/PKZIP.Extraction.cs
new file mode 100644
index 00000000..68d8461d
--- /dev/null
+++ b/SabreTools.Serialization/Wrappers/PKZIP.Extraction.cs
@@ -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
+ {
+ ///
+ public bool Extract(string outputDirectory, bool includeDebug)
+ => Extract(outputDirectory, lookForHeader: false, includeDebug);
+
+ ///
+ 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
+ }
+
+ ///
+ /// Try to find all parts of the archive, if possible
+ ///
+ /// Path of the first archive part
+ /// List of all found parts, if possible
+ public static List 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 parts = [];
+
+ // Determine which pattern is being used
+ Match match;
+ Func 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;
+ }
+ }
+}
diff --git a/SabreTools.Serialization/Wrappers/PKZIP.cs b/SabreTools.Serialization/Wrappers/PKZIP.cs
index 6b0daadd..cc045d5c 100644
--- a/SabreTools.Serialization/Wrappers/PKZIP.cs
+++ b/SabreTools.Serialization/Wrappers/PKZIP.cs
@@ -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, IExtractable
+ public partial class PKZIP : WrapperBase
{
#region Descriptive Properties
@@ -123,157 +114,5 @@ namespace SabreTools.Serialization.Wrappers
}
#endregion
-
- #region Extraction
-
- ///
- public bool Extract(string outputDirectory, bool includeDebug)
- => Extract(outputDirectory, lookForHeader: false, includeDebug);
-
- ///
- 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
- }
-
- ///
- /// Try to find all parts of the archive, if possible
- ///
- /// Path of the first archive part
- /// List of all found parts, if possible
- public static List 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 parts = [];
-
- // Determine which pattern is being used
- Match match;
- Func 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
}
}
diff --git a/SabreTools.Serialization/Wrappers/PortableExecutable.Extraction.cs b/SabreTools.Serialization/Wrappers/PortableExecutable.Extraction.cs
new file mode 100644
index 00000000..8dda12e9
--- /dev/null
+++ b/SabreTools.Serialization/Wrappers/PortableExecutable.Extraction.cs
@@ -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
+ {
+ ///
+ ///
+ /// 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
+ ///
+ 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;
+ }
+
+ ///
+ /// Extract a CExe-compressed executable
+ ///
+ /// Output directory to write to
+ /// True to include debug data, false otherwise
+ /// True if extraction succeeded, false otherwise
+ 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;
+ }
+ }
+
+ ///
+ /// Extract data from the overlay
+ ///
+ /// Output directory to write to
+ /// True to include debug data, false otherwise
+ /// True if extraction succeeded, false otherwise
+ 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;
+ }
+ }
+
+ ///
+ /// Extract data from the resources
+ ///
+ /// Output directory to write to
+ /// True to include debug data, false otherwise
+ /// True if extraction succeeded, false otherwise
+ 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;
+ }
+ }
+
+ ///
+ /// Extract data from a Wise installer
+ ///
+ /// Output directory to write to
+ /// True to include debug data, false otherwise
+ /// True if extraction succeeded, false otherwise
+ 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;
+ }
+
+ ///
+ /// Decompress CExe data compressed with LZ
+ ///
+ /// Resource data to inflate
+ /// Inflated data on success, null otherwise
+ 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;
+ }
+ }
+
+ ///
+ /// Decompress CExe data compressed with zlib
+ ///
+ /// Resource data to inflate
+ /// Inflated data on success, null otherwise
+ 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(data, 0, (int)read).ToArray();
+#endif
+ return data;
+ }
+ catch
+ {
+ // Reset the data
+ return null;
+ }
+ }
+
+ ///
+ /// Extract using Wise overlay
+ ///
+ /// Output directory to write to
+ /// True to include debug data, false otherwise
+ /// Potentially multi-part stream to read
+ /// Offset to the start of the overlay header
+ /// True if extraction succeeded, false otherwise
+ 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);
+ }
+
+ ///
+ /// Extract using Wise section
+ ///
+ /// Output directory to write to
+ /// True to include debug data, false otherwise
+ /// Potentially multi-part stream to read
+ /// Wise section information
+ /// True if extraction succeeded, false otherwise
+ 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);
+ }
+ }
+}
diff --git a/SabreTools.Serialization/Wrappers/PortableExecutable.cs b/SabreTools.Serialization/Wrappers/PortableExecutable.cs
index 5e74e55f..7358d19e 100644
--- a/SabreTools.Serialization/Wrappers/PortableExecutable.cs
+++ b/SabreTools.Serialization/Wrappers/PortableExecutable.cs
@@ -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, IExtractable
+ public partial class PortableExecutable : WrapperBase
{
#region Descriptive Properties
@@ -1136,555 +1134,6 @@ namespace SabreTools.Serialization.Wrappers
#endregion
- #region Extraction
-
- ///
- ///
- /// 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
- ///
- 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;
- }
-
- ///
- /// Extract a CExe-compressed executable
- ///
- /// Output directory to write to
- /// True to include debug data, false otherwise
- /// True if extraction succeeded, false otherwise
- 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;
- }
- }
-
- ///
- /// Extract data from the overlay
- ///
- /// Output directory to write to
- /// True to include debug data, false otherwise
- /// True if extraction succeeded, false otherwise
- 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;
- }
- }
-
- ///
- /// Extract data from the resources
- ///
- /// Output directory to write to
- /// True to include debug data, false otherwise
- /// True if extraction succeeded, false otherwise
- 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;
- }
- }
-
- ///
- /// Extract data from a Wise installer
- ///
- /// Output directory to write to
- /// True to include debug data, false otherwise
- /// True if extraction succeeded, false otherwise
- 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;
- }
-
- ///
- /// Decompress CExe data compressed with LZ
- ///
- /// Resource data to inflate
- /// Inflated data on success, null otherwise
- 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;
- }
- }
-
- ///
- /// Decompress CExe data compressed with zlib
- ///
- /// Resource data to inflate
- /// Inflated data on success, null otherwise
- 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(data, 0, (int)read).ToArray();
-#endif
- return data;
- }
- catch
- {
- // Reset the data
- return null;
- }
- }
-
- ///
- /// Extract using Wise overlay
- ///
- /// Output directory to write to
- /// True to include debug data, false otherwise
- /// Potentially multi-part stream to read
- /// Offset to the start of the overlay header
- /// True if extraction succeeded, false otherwise
- 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);
- }
-
- ///
- /// Extract using Wise section
- ///
- /// Output directory to write to
- /// True to include debug data, false otherwise
- /// Potentially multi-part stream to read
- /// Wise section information
- /// True if extraction succeeded, false otherwise
- 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
///
diff --git a/SabreTools.Serialization/Wrappers/Quantum.Extraction.cs b/SabreTools.Serialization/Wrappers/Quantum.Extraction.cs
new file mode 100644
index 00000000..0e2cb772
--- /dev/null
+++ b/SabreTools.Serialization/Wrappers/Quantum.Extraction.cs
@@ -0,0 +1,116 @@
+using System;
+using SabreTools.Serialization.Interfaces;
+
+namespace SabreTools.Serialization.Wrappers
+{
+ public partial class Quantum : IExtractable
+ {
+ ///
+ 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;
+ }
+
+ ///
+ /// Extract a file from the Quantum archive to an output directory by index
+ ///
+ /// File index to extract
+ /// Output directory to write to
+ /// True to include debug data, false otherwise
+ /// True if the file extracted, false otherwise
+ public bool ExtractFile(int index, string outputDirectory, bool includeDebug)
+ {
+ // If we have no 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(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;
+ }
+ }
+}
diff --git a/SabreTools.Serialization/Wrappers/Quantum.cs b/SabreTools.Serialization/Wrappers/Quantum.cs
index f8660cdc..e30b6fe8 100644
--- a/SabreTools.Serialization/Wrappers/Quantum.cs
+++ b/SabreTools.Serialization/Wrappers/Quantum.cs
@@ -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, IExtractable
+ public partial class Quantum : WrapperBase
{
#region Descriptive Properties
@@ -98,117 +95,5 @@ namespace SabreTools.Serialization.Wrappers
}
#endregion
-
- #region Extraction
-
- ///
- 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;
- }
-
- ///
- /// Extract a file from the Quantum archive to an output directory by index
- ///
- /// File index to extract
- /// Output directory to write to
- /// True to include debug data, false otherwise
- /// True if the file extracted, false otherwise
- public bool ExtractFile(int index, string outputDirectory, bool includeDebug)
- {
- // If we have no 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(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
}
}
diff --git a/SabreTools.Serialization/Wrappers/RAR.Extraction.cs b/SabreTools.Serialization/Wrappers/RAR.Extraction.cs
new file mode 100644
index 00000000..327640ea
--- /dev/null
+++ b/SabreTools.Serialization/Wrappers/RAR.Extraction.cs
@@ -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
+{
+ ///
+ /// 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.
+ ///
+ public partial class RAR : IExtractable
+ {
+ ///
+ public bool Extract(string outputDirectory, bool includeDebug)
+ => Extract(outputDirectory, lookForHeader: false, includeDebug);
+
+ ///
+ 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
+ }
+
+ ///
+ /// Try to find all parts of the archive, if possible
+ ///
+ /// Path of the first archive part
+ /// List of all found parts, if possible
+ public static List 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 parts = [];
+
+ // Determine which pattern is being used
+ Match match;
+ Func 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
+ ///
+ /// 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.
+ ///
+ 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;
+ }
+
+ ///
+ /// 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.
+ ///
+ 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
+ }
+}
diff --git a/SabreTools.Serialization/Wrappers/RAR.cs b/SabreTools.Serialization/Wrappers/RAR.cs
index 4ba71d3a..28450bbc 100644
--- a/SabreTools.Serialization/Wrappers/RAR.cs
+++ b/SabreTools.Serialization/Wrappers/RAR.cs
@@ -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.
///
- 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
-
- ///
- public bool Extract(string outputDirectory, bool includeDebug)
- => Extract(outputDirectory, lookForHeader: false, includeDebug);
-
- ///
- 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
- }
-
- ///
- /// Try to find all parts of the archive, if possible
- ///
- /// Path of the first archive part
- /// List of all found parts, if possible
- public static List 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 parts = [];
-
- // Determine which pattern is being used
- Match match;
- Func 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
-
- ///
- /// 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.
- ///
- 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;
- }
-
- ///
- /// 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.
- ///
- 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
}
}
diff --git a/SabreTools.Serialization/Wrappers/SGA.Extraction.cs b/SabreTools.Serialization/Wrappers/SGA.Extraction.cs
new file mode 100644
index 00000000..391cce5e
--- /dev/null
+++ b/SabreTools.Serialization/Wrappers/SGA.Extraction.cs
@@ -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
+ {
+ ///
+ 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;
+ }
+
+ ///
+ /// Extract a file from the SGA to an output directory by index
+ ///
+ /// File index to extract
+ /// Output directory to write to
+ /// True to include debug data, false otherwise
+ /// True if the file extracted, false otherwise
+ public bool ExtractFile(int index, string outputDirectory, bool includeDebug)
+ {
+ // 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 { 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;
+ }
+ }
+}
diff --git a/SabreTools.Serialization/Wrappers/SGA.cs b/SabreTools.Serialization/Wrappers/SGA.cs
index e8cf7776..2f383edc 100644
--- a/SabreTools.Serialization/Wrappers/SGA.cs
+++ b/SabreTools.Serialization/Wrappers/SGA.cs
@@ -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, IExtractable
+ public partial class SGA : WrapperBase
{
#region Descriptive Properties
@@ -128,151 +124,6 @@ namespace SabreTools.Serialization.Wrappers
#endregion
- #region Extraction
-
- ///
- 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;
- }
-
- ///
- /// Extract a file from the SGA to an output directory by index
- ///
- /// File index to extract
- /// Output directory to write to
- /// True to include debug data, false otherwise
- /// True if the file extracted, false otherwise
- public bool ExtractFile(int index, string outputDirectory, bool includeDebug)
- {
- // 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 { 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
///
diff --git a/SabreTools.Serialization/Wrappers/SevenZip.Extraction.cs b/SabreTools.Serialization/Wrappers/SevenZip.Extraction.cs
new file mode 100644
index 00000000..8ce62cc5
--- /dev/null
+++ b/SabreTools.Serialization/Wrappers/SevenZip.Extraction.cs
@@ -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
+{
+ ///
+ /// 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.
+ ///
+ public partial class SevenZip : IExtractable
+ {
+ ///
+ public bool Extract(string outputDirectory, bool includeDebug)
+ => Extract(outputDirectory, lookForHeader: false, includeDebug);
+
+ ///
+ 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
+ }
+
+ ///
+ /// Try to find all parts of the archive, if possible
+ ///
+ /// Path of the first archive part
+ /// List of all found parts, if possible
+ public static List 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 parts = [];
+
+ // Determine which pattern is being used
+ Match match;
+ Func 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
+ ///
+ /// 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.
+ ///
+ 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;
+ }
+
+ ///
+ /// 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.
+ ///
+ 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
+ }
+}
diff --git a/SabreTools.Serialization/Wrappers/SevenZip.cs b/SabreTools.Serialization/Wrappers/SevenZip.cs
index ce409251..b0f5b2cb 100644
--- a/SabreTools.Serialization/Wrappers/SevenZip.cs
+++ b/SabreTools.Serialization/Wrappers/SevenZip.cs
@@ -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.
///
- 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
-
- ///
- public bool Extract(string outputDirectory, bool includeDebug)
- => Extract(outputDirectory, lookForHeader: false, includeDebug);
-
- ///
- 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
- }
-
- ///
- /// Try to find all parts of the archive, if possible
- ///
- /// Path of the first archive part
- /// List of all found parts, if possible
- public static List 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 parts = [];
-
- // Determine which pattern is being used
- Match match;
- Func 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
- ///
- /// 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.
- ///
- 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;
- }
-
- ///
- /// 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.
- ///
- 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
}
}
diff --git a/SabreTools.Serialization/Wrappers/TapeArchive.Extraction.cs b/SabreTools.Serialization/Wrappers/TapeArchive.Extraction.cs
new file mode 100644
index 00000000..9ad54614
--- /dev/null
+++ b/SabreTools.Serialization/Wrappers/TapeArchive.Extraction.cs
@@ -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
+ {
+ ///
+ 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;
+ }
+ }
+ }
+}
diff --git a/SabreTools.Serialization/Wrappers/TapeArchive.cs b/SabreTools.Serialization/Wrappers/TapeArchive.cs
index 5c5d97c5..2ac83fb3 100644
--- a/SabreTools.Serialization/Wrappers/TapeArchive.cs
+++ b/SabreTools.Serialization/Wrappers/TapeArchive.cs
@@ -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, IExtractable
+ public partial class TapeArchive : WrapperBase
{
#region Descriptive Properties
@@ -88,169 +86,5 @@ namespace SabreTools.Serialization.Wrappers
}
#endregion
-
- #region Extraction
-
- ///
- 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
}
}
diff --git a/SabreTools.Serialization/Wrappers/VBSP.Extraction.cs b/SabreTools.Serialization/Wrappers/VBSP.Extraction.cs
new file mode 100644
index 00000000..c057698f
--- /dev/null
+++ b/SabreTools.Serialization/Wrappers/VBSP.Extraction.cs
@@ -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
+ {
+ ///
+ 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;
+ }
+
+ ///
+ /// Extract a lump from the VBSP to an output directory by index
+ ///
+ /// Lump index to extract
+ /// Output directory to write to
+ /// True to include debug data, false otherwise
+ /// True if the lump extracted, false otherwise
+ 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;
+ }
+ }
+}
diff --git a/SabreTools.Serialization/Wrappers/VBSP.cs b/SabreTools.Serialization/Wrappers/VBSP.cs
index d9c79a47..99340ef3 100644
--- a/SabreTools.Serialization/Wrappers/VBSP.cs
+++ b/SabreTools.Serialization/Wrappers/VBSP.cs
@@ -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, IExtractable
+ public partial class VBSP : WrapperBase
{
#region Descriptive Properties
@@ -89,94 +86,5 @@ namespace SabreTools.Serialization.Wrappers
}
#endregion
-
- #region Extraction
-
- ///
- 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;
- }
-
- ///
- /// Extract a lump from the VBSP to an output directory by index
- ///
- /// Lump index to extract
- /// Output directory to write to
- /// True to include debug data, false otherwise
- /// True if the lump extracted, false otherwise
- 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
}
}
diff --git a/SabreTools.Serialization/Wrappers/VPK.Extraction.cs b/SabreTools.Serialization/Wrappers/VPK.Extraction.cs
new file mode 100644
index 00000000..1e118cee
--- /dev/null
+++ b/SabreTools.Serialization/Wrappers/VPK.Extraction.cs
@@ -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
+ {
+ ///
+ 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;
+ }
+
+ ///
+ /// Extract a file from the VPK to an output directory by index
+ ///
+ /// File index to extract
+ /// Output directory to write to
+ /// True to include debug data, false otherwise
+ /// True if the file extracted, false otherwise
+ public bool ExtractFile(int index, string outputDirectory, bool includeDebug)
+ {
+ // If we have no 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;
+ }
+ }
+}
diff --git a/SabreTools.Serialization/Wrappers/VPK.cs b/SabreTools.Serialization/Wrappers/VPK.cs
index edda8214..e08e25f8 100644
--- a/SabreTools.Serialization/Wrappers/VPK.cs
+++ b/SabreTools.Serialization/Wrappers/VPK.cs
@@ -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, IExtractable
+ public partial class VPK : WrapperBase
{
#region Descriptive Properties
@@ -153,144 +151,5 @@ namespace SabreTools.Serialization.Wrappers
}
#endregion
-
- #region Extraction
-
- ///
- 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;
- }
-
- ///
- /// Extract a file from the VPK to an output directory by index
- ///
- /// File index to extract
- /// Output directory to write to
- /// True to include debug data, false otherwise
- /// True if the file extracted, false otherwise
- public bool ExtractFile(int index, string outputDirectory, bool includeDebug)
- {
- // If we have no 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
}
}
diff --git a/SabreTools.Serialization/Wrappers/WAD3.Extraction.cs b/SabreTools.Serialization/Wrappers/WAD3.Extraction.cs
new file mode 100644
index 00000000..ad82bc23
--- /dev/null
+++ b/SabreTools.Serialization/Wrappers/WAD3.Extraction.cs
@@ -0,0 +1,83 @@
+using System;
+using System.IO;
+using SabreTools.Serialization.Interfaces;
+
+namespace SabreTools.Serialization.Wrappers
+{
+ public partial class WAD3 : IExtractable
+ {
+ ///
+ 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;
+ }
+
+ ///
+ /// Extract a lump from the WAD3 to an output directory by index
+ ///
+ /// Lump index to extract
+ /// Output directory to write to
+ /// True to include debug data, false otherwise
+ /// True if the lump extracted, false otherwise
+ 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;
+ }
+ }
+}
diff --git a/SabreTools.Serialization/Wrappers/WAD3.cs b/SabreTools.Serialization/Wrappers/WAD3.cs
index 6b625a8a..69db48e7 100644
--- a/SabreTools.Serialization/Wrappers/WAD3.cs
+++ b/SabreTools.Serialization/Wrappers/WAD3.cs
@@ -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, IExtractable
+ public partial class WAD3 : WrapperBase
{
#region Descriptive Properties
@@ -88,83 +85,5 @@ namespace SabreTools.Serialization.Wrappers
}
#endregion
-
- #region Extraction
-
- ///
- 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;
- }
-
- ///
- /// Extract a lump from the WAD3 to an output directory by index
- ///
- /// Lump index to extract
- /// Output directory to write to
- /// True to include debug data, false otherwise
- /// True if the lump extracted, false otherwise
- 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
}
}
diff --git a/SabreTools.Serialization/Wrappers/WiseSectionHeader.Extraction.cs b/SabreTools.Serialization/Wrappers/WiseSectionHeader.Extraction.cs
new file mode 100644
index 00000000..d5bd8a07
--- /dev/null
+++ b/SabreTools.Serialization/Wrappers/WiseSectionHeader.Extraction.cs
@@ -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
+ {
+ ///
+ 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.
+
+ ///
+ /// Extract the predefined, static files defined in the header
+ ///
+ /// Output directory to write to
+ /// True to include debug data, false otherwise
+ /// True if the files extracted successfully, false otherwise
+ 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;
+ }
+ }
+
+ ///
+ /// Attempt to extract a file defined by a filename
+ ///
+ /// Output filename, null to auto-generate
+ /// Output directory to write to
+ /// Expected size of the file plus crc32
+ /// True to include debug data, false otherwise
+ /// Extraction status representing the final state
+ /// Assumes that the current stream position is the end of where the data lives
+ 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;
+ }
+
+ ///
+ /// Extract source data with a trailing CRC-32 checksum
+ ///
+ /// Stream where the file data will be written
+ /// Expected size of the file plus crc32
+ /// True to include debug data, false otherwise
+ ///
+ 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;
+ }
+ }
+
+ ///
+ /// Extract source data without a trailing CRC-32 checksum
+ ///
+ /// Stream where the file data will be written
+ /// Expected size of the file
+ /// True to include debug data, false otherwise
+ ///
+ 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;
+ }
+ }
+ }
+}
diff --git a/SabreTools.Serialization/Wrappers/WiseSectionHeader.cs b/SabreTools.Serialization/Wrappers/WiseSectionHeader.cs
index be2c4cb2..8b48b298 100644
--- a/SabreTools.Serialization/Wrappers/WiseSectionHeader.cs
+++ b/SabreTools.Serialization/Wrappers/WiseSectionHeader.cs
@@ -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, IExtractable
+ public partial class WiseSectionHeader : WrapperBase
{
#region Descriptive Properties
@@ -168,213 +163,5 @@ namespace SabreTools.Serialization.Wrappers
}
#endregion
-
- #region Extraction
-
- ///
- 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.
-
- ///
- /// Extract the predefined, static files defined in the header
- ///
- /// Output directory to write to
- /// True to include debug data, false otherwise
- /// True if the files extracted successfully, false otherwise
- 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;
- }
- }
-
- ///
- /// Attempt to extract a file defined by a filename
- ///
- /// Output filename, null to auto-generate
- /// Output directory to write to
- /// Expected size of the file plus crc32
- /// True to include debug data, false otherwise
- /// Extraction status representing the final state
- /// Assumes that the current stream position is the end of where the data lives
- 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;
- }
-
- ///
- /// Extract source data with a trailing CRC-32 checksum
- ///
- /// Stream where the file data will be written
- /// Expected size of the file plus crc32
- /// True to include debug data, false otherwise
- ///
- 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;
- }
- }
-
- ///
- /// Extract source data without a trailing CRC-32 checksum
- ///
- /// Stream where the file data will be written
- /// Expected size of the file
- /// True to include debug data, false otherwise
- ///
- 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
}
}
diff --git a/SabreTools.Serialization/Wrappers/XZ.Extraction.cs b/SabreTools.Serialization/Wrappers/XZ.Extraction.cs
new file mode 100644
index 00000000..4510dd65
--- /dev/null
+++ b/SabreTools.Serialization/Wrappers/XZ.Extraction.cs
@@ -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
+{
+ ///
+ /// 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.
+ ///
+ public partial class XZ : IExtractable
+ {
+ ///
+ 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
+ }
+ }
+}
diff --git a/SabreTools.Serialization/Wrappers/XZ.cs b/SabreTools.Serialization/Wrappers/XZ.cs
index 9e7b6b9d..f791f729 100644
--- a/SabreTools.Serialization/Wrappers/XZ.cs
+++ b/SabreTools.Serialization/Wrappers/XZ.cs
@@ -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.
///
- 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
-
- ///
- 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
}
}
diff --git a/SabreTools.Serialization/Wrappers/XZP.Extraction.cs b/SabreTools.Serialization/Wrappers/XZP.Extraction.cs
new file mode 100644
index 00000000..52f6848a
--- /dev/null
+++ b/SabreTools.Serialization/Wrappers/XZP.Extraction.cs
@@ -0,0 +1,92 @@
+using System;
+using System.IO;
+using SabreTools.Serialization.Interfaces;
+
+namespace SabreTools.Serialization.Wrappers
+{
+ public partial class XZP : IExtractable
+ {
+ ///
+ 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;
+ }
+
+ ///
+ /// Extract a file from the XZP to an output directory by index
+ ///
+ /// File index to extract
+ /// Output directory to write to
+ /// True to include debug data, false otherwise
+ /// True if the file extracted, false otherwise
+ public bool ExtractFile(int index, string outputDirectory, bool includeDebug)
+ {
+ // If we have no 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;
+ }
+ }
+}
diff --git a/SabreTools.Serialization/Wrappers/XZP.cs b/SabreTools.Serialization/Wrappers/XZP.cs
index ae557494..01f5454d 100644
--- a/SabreTools.Serialization/Wrappers/XZP.cs
+++ b/SabreTools.Serialization/Wrappers/XZP.cs
@@ -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, IExtractable
+ public partial class XZP : WrapperBase
{
#region Descriptive Properties
@@ -91,92 +88,5 @@ namespace SabreTools.Serialization.Wrappers
}
#endregion
-
- #region Extraction
-
- ///
- 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;
- }
-
- ///
- /// Extract a file from the XZP to an output directory by index
- ///
- /// File index to extract
- /// Output directory to write to
- /// True to include debug data, false otherwise
- /// True if the file extracted, false otherwise
- public bool ExtractFile(int index, string outputDirectory, bool includeDebug)
- {
- // If we have no 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
}
}