diff --git a/SabreTools.Serialization/SabreTools.Serialization.csproj b/SabreTools.Serialization/SabreTools.Serialization.csproj index edc50bc0..0a0c3f89 100644 --- a/SabreTools.Serialization/SabreTools.Serialization.csproj +++ b/SabreTools.Serialization/SabreTools.Serialization.csproj @@ -41,6 +41,7 @@ + \ No newline at end of file diff --git a/SabreTools.Serialization/Wrappers/BZip2.cs b/SabreTools.Serialization/Wrappers/BZip2.cs index aae6d6d6..0664dec1 100644 --- a/SabreTools.Serialization/Wrappers/BZip2.cs +++ b/SabreTools.Serialization/Wrappers/BZip2.cs @@ -1,4 +1,7 @@ +using System; using System.IO; +using SabreTools.IO.Compression.BZip2; +using SabreTools.Serialization.Interfaces; namespace SabreTools.Serialization.Wrappers { @@ -7,7 +10,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 + public class BZip2 : WrapperBase, IExtractable { #region Descriptive Properties @@ -16,8 +19,43 @@ namespace SabreTools.Serialization.Wrappers #endregion + #region Instance Variables + + /// + /// Source filename for the wrapper + /// + private readonly string? _filename; + + /// + /// Source stream for the wrapper + /// + private readonly Stream _stream; + + #endregion + #region Constructors + /// + /// Construct a new instance of the wrapper from a file path + /// + public BZip2(string filename) + { + _filename = filename; + _stream = File.Open(_filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + } + + /// + /// Construct a new instance of the wrapper from a Stream + /// + public BZip2(Stream stream) + { + _filename = null; + _stream = stream; + + if (stream is FileStream fs) + _filename = fs.Name; + } + /// /// Create a BZip2 archive from a byte array and offset /// @@ -50,7 +88,7 @@ namespace SabreTools.Serialization.Wrappers if (data == null || !data.CanRead) return null; - return new BZip2(); + return new BZip2(data); } #endregion @@ -59,10 +97,52 @@ namespace SabreTools.Serialization.Wrappers #if NETCOREAPP /// - public override string ExportJSON() => throw new System.NotImplementedException(); + public override string ExportJSON() => throw new NotImplementedException(); #endif #endregion + + #region Extraction + + /// + public bool Extract(string outputDirectory, bool includeDebug) + { + if (_stream == null || !_stream.CanRead) + return false; + + try + { + // Try opening the stream + using var bz2File = new BZip2InputStream(_stream, 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/GZip.cs b/SabreTools.Serialization/Wrappers/GZip.cs index b3cb95d2..9c910deb 100644 --- a/SabreTools.Serialization/Wrappers/GZip.cs +++ b/SabreTools.Serialization/Wrappers/GZip.cs @@ -1,4 +1,7 @@ +using System; using System.IO; +using SabreTools.IO.Compression.Deflate; +using SabreTools.Serialization.Interfaces; namespace SabreTools.Serialization.Wrappers { @@ -7,7 +10,7 @@ namespace SabreTools.Serialization.Wrappers /// any actual parsing. It is used as a placeholder for /// types that typically do not have models. /// - public class GZip : WrapperBase + public class GZip : WrapperBase, IExtractable { #region Descriptive Properties @@ -16,8 +19,43 @@ namespace SabreTools.Serialization.Wrappers #endregion + #region Instance Variables + + /// + /// Source filename for the wrapper + /// + private readonly string? _filename; + + /// + /// Source stream for the wrapper + /// + private readonly Stream _stream; + + #endregion + #region Constructors + /// + /// Construct a new instance of the wrapper from a file path + /// + public GZip(string filename) + { + _filename = filename; + _stream = File.Open(_filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + } + + /// + /// Construct a new instance of the wrapper from a Stream + /// + public GZip(Stream stream) + { + _filename = null; + _stream = stream; + + if (stream is FileStream fs) + _filename = fs.Name; + } + /// /// Create a GZip archive from a byte array and offset /// @@ -50,7 +88,7 @@ namespace SabreTools.Serialization.Wrappers if (data == null || !data.CanRead) return null; - return new GZip(); + return new GZip(data); } #endregion @@ -59,9 +97,51 @@ namespace SabreTools.Serialization.Wrappers #if NETCOREAPP /// - public override string ExportJSON() => throw new System.NotImplementedException(); + public override string ExportJSON() => throw new NotImplementedException(); #endif #endregion + + #region Extraction + + /// + public bool Extract(string outputDirectory, bool includeDebug) + { + if (_stream == null || !_stream.CanRead) + return false; + + try + { + // Try opening the stream + using var gzipFile = new GZipStream(_stream, CompressionMode.Decompress, 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); + gzipFile.CopyTo(fs); + fs.Flush(); + + return true; + } + catch (Exception ex) + { + if (includeDebug) Console.Error.WriteLine(ex); + return false; + } + } + + #endregion } } diff --git a/SabreTools.Serialization/Wrappers/RAR.cs b/SabreTools.Serialization/Wrappers/RAR.cs index 6d914c4d..7951d8d0 100644 --- a/SabreTools.Serialization/Wrappers/RAR.cs +++ b/SabreTools.Serialization/Wrappers/RAR.cs @@ -1,4 +1,11 @@ using System.IO; +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 { @@ -7,7 +14,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 + public class RAR : WrapperBase, IExtractable { #region Descriptive Properties @@ -16,8 +23,43 @@ namespace SabreTools.Serialization.Wrappers #endregion + #region Instance Variables + + /// + /// Source filename for the wrapper + /// + private readonly string? _filename; + + /// + /// Source stream for the wrapper + /// + private readonly Stream _stream; + + #endregion + #region Constructors + /// + /// Construct a new instance of the wrapper from a file path + /// + public RAR(string filename) + { + _filename = filename; + _stream = File.Open(_filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + } + + /// + /// Construct a new instance of the wrapper from a Stream + /// + public RAR(Stream stream) + { + _filename = null; + _stream = stream; + + if (stream is FileStream fs) + _filename = fs.Name; + } + /// /// Create a RAR archive (or derived format) from a byte array and offset /// @@ -50,7 +92,7 @@ namespace SabreTools.Serialization.Wrappers if (data == null || !data.CanRead) return null; - return new RAR(); + return new RAR(data); } #endregion @@ -63,5 +105,122 @@ 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 (_stream == null || !_stream.CanRead) + return false; + +#if NET462_OR_GREATER || NETCOREAPP + try + { + var readerOptions = new ReaderOptions() { LookForHeader = lookForHeader }; + RarArchive rarFile = RarArchive.Open(_stream, readerOptions); + + // Try to read the file path if no entries are found + if (rarFile.Entries.Count == 0 && !string.IsNullOrEmpty(_filename) && File.Exists(_filename)) + rarFile = RarArchive.Open(_filename, readerOptions); + + if (!rarFile.IsComplete) + return false; + + 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 + return false; +#endif + } + +#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/SevenZip.cs b/SabreTools.Serialization/Wrappers/SevenZip.cs index dbacb9b1..21421d35 100644 --- a/SabreTools.Serialization/Wrappers/SevenZip.cs +++ b/SabreTools.Serialization/Wrappers/SevenZip.cs @@ -1,4 +1,11 @@ using System.IO; +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 { @@ -7,7 +14,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 + public class SevenZip : WrapperBase, IExtractable { #region Descriptive Properties @@ -16,8 +23,43 @@ namespace SabreTools.Serialization.Wrappers #endregion + #region Instance Variables + + /// + /// Source filename for the wrapper + /// + private readonly string? _filename; + + /// + /// Source stream for the wrapper + /// + private readonly Stream _stream; + + #endregion + #region Constructors + /// + /// Construct a new instance of the wrapper from a file path + /// + public SevenZip(string filename) + { + _filename = filename; + _stream = File.Open(_filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + } + + /// + /// Construct a new instance of the wrapper from a Stream + /// + public SevenZip(Stream stream) + { + _filename = null; + _stream = stream; + + if (stream is FileStream fs) + _filename = fs.Name; + } + /// /// Create a SevenZip archive (or derived format) from a byte array and offset /// @@ -50,7 +92,7 @@ namespace SabreTools.Serialization.Wrappers if (data == null || !data.CanRead) return null; - return new SevenZip(); + return new SevenZip(data); } #endregion @@ -63,5 +105,119 @@ 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 (_stream == null || !_stream.CanRead) + return false; + +#if NET462_OR_GREATER || NETCOREAPP + try + { + var readerOptions = new ReaderOptions() { LookForHeader = lookForHeader }; + var sevenZip = SevenZipArchive.Open(_stream, readerOptions); + // Try to read the file path if no entries are found + if (sevenZip.Entries.Count == 0 && !string.IsNullOrEmpty(_filename) && File.Exists(_filename)) + sevenZip = SevenZipArchive.Open(_filename, 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 + return false; +#endif + } + +#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); + + sevenZip.WriteToDirectory(outputDirectory, 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/TapeArchive.cs b/SabreTools.Serialization/Wrappers/TapeArchive.cs index fd39c125..c19448d4 100644 --- a/SabreTools.Serialization/Wrappers/TapeArchive.cs +++ b/SabreTools.Serialization/Wrappers/TapeArchive.cs @@ -1,4 +1,9 @@ using System.IO; +using SabreTools.Serialization.Interfaces; +#if NET462_OR_GREATER || NETCOREAPP +using SharpCompress.Archives; +using SharpCompress.Archives.Tar; +#endif namespace SabreTools.Serialization.Wrappers { @@ -7,7 +12,7 @@ namespace SabreTools.Serialization.Wrappers /// any actual parsing. It is used as a placeholder for /// types that typically do not have models. /// - public class TapeArchive : WrapperBase + public class TapeArchive : WrapperBase, IExtractable { #region Descriptive Properties @@ -16,8 +21,43 @@ namespace SabreTools.Serialization.Wrappers #endregion + #region Instance Variables + + /// + /// Source filename for the wrapper + /// + private readonly string? _filename; + + /// + /// Source stream for the wrapper + /// + private readonly Stream _stream; + + #endregion + #region Constructors + /// + /// Construct a new instance of the wrapper from a file path + /// + public TapeArchive(string filename) + { + _filename = filename; + _stream = File.Open(_filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + } + + /// + /// Construct a new instance of the wrapper from a Stream + /// + public TapeArchive(Stream stream) + { + _filename = null; + _stream = stream; + + if (stream is FileStream fs) + _filename = fs.Name; + } + /// /// Create a tape archive (or derived format) from a byte array and offset /// @@ -50,7 +90,7 @@ namespace SabreTools.Serialization.Wrappers if (data == null || !data.CanRead) return null; - return new TapeArchive(); + return new TapeArchive(data); } #endregion @@ -63,5 +103,73 @@ namespace SabreTools.Serialization.Wrappers #endif #endregion + + #region Extraction + + /// + public bool Extract(string outputDirectory, bool includeDebug) + { + if (_stream == null || !_stream.CanRead) + return false; + +#if NET462_OR_GREATER || NETCOREAPP + try + { + var tarFile = TarArchive.Open(_stream); + + // Try to read the file path if no entries are found + if (tarFile.Entries.Count == 0 && !string.IsNullOrEmpty(_filename) && File.Exists(_filename)) + tarFile = TarArchive.Open(_filename); + + foreach (var entry in tarFile.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; + } + catch (System.Exception ex) + { + if (includeDebug) System.Console.Error.WriteLine(ex); + return false; + } +#else + return false; +#endif + } + + #endregion } } diff --git a/SabreTools.Serialization/Wrappers/XZ.cs b/SabreTools.Serialization/Wrappers/XZ.cs index 0cd7a7da..5e4944db 100644 --- a/SabreTools.Serialization/Wrappers/XZ.cs +++ b/SabreTools.Serialization/Wrappers/XZ.cs @@ -1,4 +1,8 @@ using System.IO; +using SabreTools.Serialization.Interfaces; +#if NET462_OR_GREATER || NETCOREAPP +using SharpCompress.Compressors.Xz; +#endif namespace SabreTools.Serialization.Wrappers { @@ -7,7 +11,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 + public class XZ : WrapperBase, IExtractable { #region Descriptive Properties @@ -16,8 +20,43 @@ namespace SabreTools.Serialization.Wrappers #endregion + #region Instance Variables + + /// + /// Source filename for the wrapper + /// + private readonly string? _filename; + + /// + /// Source stream for the wrapper + /// + private readonly Stream _stream; + + #endregion + #region Constructors + /// + /// Construct a new instance of the wrapper from a file path + /// + public XZ(string filename) + { + _filename = filename; + _stream = File.Open(_filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + } + + /// + /// Construct a new instance of the wrapper from a Stream + /// + public XZ(Stream stream) + { + _filename = null; + _stream = stream; + + if (stream is FileStream fs) + _filename = fs.Name; + } + /// /// Create a XZ archive from a byte array and offset /// @@ -50,7 +89,7 @@ namespace SabreTools.Serialization.Wrappers if (data == null || !data.CanRead) return null; - return new XZ(); + return new XZ(data); } #endregion @@ -63,5 +102,51 @@ namespace SabreTools.Serialization.Wrappers #endif #endregion + + #region Extraction + + /// + public bool Extract(string outDir, bool includeDebug) + { +#if NET462_OR_GREATER || NETCOREAPP + if (_stream == null || !_stream.CanRead) + return false; + + try + { + // Try opening the stream + using var xzFile = new XZStream(_stream); + + // Ensure directory separators are consistent + string filename = System.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(outDir, 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 (System.Exception ex) + { + if (includeDebug) System.Console.Error.WriteLine(ex); + return false; + } +#else + return false; +#endif + } + + #endregion } }