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 } }