Intermediary step for extractable shells

This commit is contained in:
Matt Nadareski
2025-08-23 12:23:44 -04:00
parent b0b334103d
commit 456bb1e95b
7 changed files with 683 additions and 14 deletions

View File

@@ -41,6 +41,7 @@
<PackageReference Include="SabreTools.IO" Version="1.7.0" />
<PackageReference Include="SabreTools.Models" Version="1.6.1" />
<PackageReference Include="SabreTools.Matching" Version="1.6.0" />
<PackageReference Include="SharpCompress" Version="0.39.0" Condition="!$(TargetFramework.StartsWith(`net2`)) AND !$(TargetFramework.StartsWith(`net3`)) AND !$(TargetFramework.StartsWith(`net40`)) AND !$(TargetFramework.StartsWith(`net452`))" />
</ItemGroup>
</Project>

View File

@@ -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.
/// </summary>
public class BZip2 : WrapperBase
public class BZip2 : WrapperBase, IExtractable
{
#region Descriptive Properties
@@ -16,8 +19,43 @@ namespace SabreTools.Serialization.Wrappers
#endregion
#region Instance Variables
/// <summary>
/// Source filename for the wrapper
/// </summary>
private readonly string? _filename;
/// <summary>
/// Source stream for the wrapper
/// </summary>
private readonly Stream _stream;
#endregion
#region Constructors
/// <summary>
/// Construct a new instance of the wrapper from a file path
/// </summary>
public BZip2(string filename)
{
_filename = filename;
_stream = File.Open(_filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
}
/// <summary>
/// Construct a new instance of the wrapper from a Stream
/// </summary>
public BZip2(Stream stream)
{
_filename = null;
_stream = stream;
if (stream is FileStream fs)
_filename = fs.Name;
}
/// <summary>
/// Create a BZip2 archive from a byte array and offset
/// </summary>
@@ -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
/// <inheritdoc/>
public override string ExportJSON() => throw new System.NotImplementedException();
public override string ExportJSON() => throw new NotImplementedException();
#endif
#endregion
#region Extraction
/// <inheritdoc/>
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
}
}

View File

@@ -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.
/// </summary>
public class GZip : WrapperBase
public class GZip : WrapperBase, IExtractable
{
#region Descriptive Properties
@@ -16,8 +19,43 @@ namespace SabreTools.Serialization.Wrappers
#endregion
#region Instance Variables
/// <summary>
/// Source filename for the wrapper
/// </summary>
private readonly string? _filename;
/// <summary>
/// Source stream for the wrapper
/// </summary>
private readonly Stream _stream;
#endregion
#region Constructors
/// <summary>
/// Construct a new instance of the wrapper from a file path
/// </summary>
public GZip(string filename)
{
_filename = filename;
_stream = File.Open(_filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
}
/// <summary>
/// Construct a new instance of the wrapper from a Stream
/// </summary>
public GZip(Stream stream)
{
_filename = null;
_stream = stream;
if (stream is FileStream fs)
_filename = fs.Name;
}
/// <summary>
/// Create a GZip archive from a byte array and offset
/// </summary>
@@ -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
/// <inheritdoc/>
public override string ExportJSON() => throw new System.NotImplementedException();
public override string ExportJSON() => throw new NotImplementedException();
#endif
#endregion
#region Extraction
/// <inheritdoc/>
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
}
}

View File

@@ -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.
/// </summary>
public class RAR : WrapperBase
public class RAR : WrapperBase, IExtractable
{
#region Descriptive Properties
@@ -16,8 +23,43 @@ namespace SabreTools.Serialization.Wrappers
#endregion
#region Instance Variables
/// <summary>
/// Source filename for the wrapper
/// </summary>
private readonly string? _filename;
/// <summary>
/// Source stream for the wrapper
/// </summary>
private readonly Stream _stream;
#endregion
#region Constructors
/// <summary>
/// Construct a new instance of the wrapper from a file path
/// </summary>
public RAR(string filename)
{
_filename = filename;
_stream = File.Open(_filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
}
/// <summary>
/// Construct a new instance of the wrapper from a Stream
/// </summary>
public RAR(Stream stream)
{
_filename = null;
_stream = stream;
if (stream is FileStream fs)
_filename = fs.Name;
}
/// <summary>
/// Create a RAR archive (or derived format) from a byte array and offset
/// </summary>
@@ -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
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
=> Extract(outputDirectory, lookForHeader: false, includeDebug);
/// <inheritdoc cref="Extract(string, bool)"/>
public bool Extract(string outputDirectory, bool lookForHeader, bool includeDebug)
{
if (_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
/// <summary>
/// Extraction method for non-solid archives. This iterates over each entry in the archive to extract every
/// file individually, in order to extract all valid files from the archive.
/// </summary>
private static bool ExtractNonSolid(RarArchive rarFile, string outDir, bool includeDebug)
{
foreach (var entry in rarFile.Entries)
{
try
{
// If the entry is a directory
if (entry.IsDirectory)
continue;
// If the entry has an invalid key
if (entry.Key == null)
continue;
// If we have a partial entry due to an incomplete multi-part archive, skip it
if (!entry.IsComplete)
continue;
// Ensure directory separators are consistent
string filename = entry.Key;
if (Path.DirectorySeparatorChar == '\\')
filename = filename.Replace('/', '\\');
else if (Path.DirectorySeparatorChar == '/')
filename = filename.Replace('\\', '/');
// Ensure the full output directory exists
filename = Path.Combine(outDir, filename);
var directoryName = Path.GetDirectoryName(filename);
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
entry.WriteToFile(filename);
}
catch (System.Exception ex)
{
if (includeDebug) System.Console.Error.WriteLine(ex);
}
}
return true;
}
/// <summary>
/// Extraction method for solid archives. Uses ExtractAllEntries because extraction for solid archives must be
/// done sequentially, and files beyond a corrupted point in a solid archive will be unreadable anyways.
/// </summary>
private static bool ExtractSolid(RarArchive rarFile, string outDir, bool includeDebug)
{
try
{
if (!Directory.Exists(outDir))
Directory.CreateDirectory(outDir);
rarFile.WriteToDirectory(outDir, new ExtractionOptions()
{
ExtractFullPath = true,
Overwrite = true,
});
}
catch (System.Exception ex)
{
if (includeDebug) System.Console.Error.WriteLine(ex);
}
return true;
}
#endif
#endregion
}
}

View File

@@ -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.
/// </summary>
public class SevenZip : WrapperBase
public class SevenZip : WrapperBase, IExtractable
{
#region Descriptive Properties
@@ -16,8 +23,43 @@ namespace SabreTools.Serialization.Wrappers
#endregion
#region Instance Variables
/// <summary>
/// Source filename for the wrapper
/// </summary>
private readonly string? _filename;
/// <summary>
/// Source stream for the wrapper
/// </summary>
private readonly Stream _stream;
#endregion
#region Constructors
/// <summary>
/// Construct a new instance of the wrapper from a file path
/// </summary>
public SevenZip(string filename)
{
_filename = filename;
_stream = File.Open(_filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
}
/// <summary>
/// Construct a new instance of the wrapper from a Stream
/// </summary>
public SevenZip(Stream stream)
{
_filename = null;
_stream = stream;
if (stream is FileStream fs)
_filename = fs.Name;
}
/// <summary>
/// Create a SevenZip archive (or derived format) from a byte array and offset
/// </summary>
@@ -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
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
=> Extract(outputDirectory, lookForHeader: false, includeDebug);
/// <inheritdoc cref="Extract(string, bool)"/>
public bool Extract(string outputDirectory, bool lookForHeader, bool includeDebug)
{
if (_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
/// <summary>
/// Extraction method for non-solid archives. This iterates over each entry in the archive to extract every
/// file individually, in order to extract all valid files from the archive.
/// </summary>
private static bool ExtractNonSolid(SevenZipArchive sevenZip, string outputDirectory, bool includeDebug)
{
foreach (var entry in sevenZip.Entries)
{
try
{
// If the entry is a directory
if (entry.IsDirectory)
continue;
// If the entry has an invalid key
if (entry.Key == null)
continue;
// If we have a partial entry due to an incomplete multi-part archive, skip it
if (!entry.IsComplete)
continue;
// Ensure directory separators are consistent
string filename = entry.Key;
if (Path.DirectorySeparatorChar == '\\')
filename = filename.Replace('/', '\\');
else if (Path.DirectorySeparatorChar == '/')
filename = filename.Replace('\\', '/');
// Ensure the full output directory exists
filename = Path.Combine(outputDirectory, filename);
var directoryName = Path.GetDirectoryName(filename);
if (directoryName != null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
entry.WriteToFile(filename);
}
catch (System.Exception ex)
{
if (includeDebug) System.Console.Error.WriteLine(ex);
}
}
return true;
}
/// <summary>
/// Extraction method for solid archives. Uses ExtractAllEntries because extraction for solid archives must be
/// done sequentially, and files beyond a corrupted point in a solid archive will be unreadable anyways.
/// </summary>
private static bool ExtractSolid(SevenZipArchive sevenZip, string outputDirectory, bool includeDebug)
{
try
{
if (!Directory.Exists(outputDirectory))
Directory.CreateDirectory(outputDirectory);
sevenZip.WriteToDirectory(outputDirectory, new ExtractionOptions()
{
ExtractFullPath = true,
Overwrite = true,
});
}
catch (System.Exception ex)
{
if (includeDebug) System.Console.Error.WriteLine(ex);
}
return true;
}
#endif
#endregion
}
}

View File

@@ -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.
/// </summary>
public class TapeArchive : WrapperBase
public class TapeArchive : WrapperBase, IExtractable
{
#region Descriptive Properties
@@ -16,8 +21,43 @@ namespace SabreTools.Serialization.Wrappers
#endregion
#region Instance Variables
/// <summary>
/// Source filename for the wrapper
/// </summary>
private readonly string? _filename;
/// <summary>
/// Source stream for the wrapper
/// </summary>
private readonly Stream _stream;
#endregion
#region Constructors
/// <summary>
/// Construct a new instance of the wrapper from a file path
/// </summary>
public TapeArchive(string filename)
{
_filename = filename;
_stream = File.Open(_filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
}
/// <summary>
/// Construct a new instance of the wrapper from a Stream
/// </summary>
public TapeArchive(Stream stream)
{
_filename = null;
_stream = stream;
if (stream is FileStream fs)
_filename = fs.Name;
}
/// <summary>
/// Create a tape archive (or derived format) from a byte array and offset
/// </summary>
@@ -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
/// <inheritdoc/>
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
}
}

View File

@@ -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.
/// </summary>
public class XZ : WrapperBase
public class XZ : WrapperBase, IExtractable
{
#region Descriptive Properties
@@ -16,8 +20,43 @@ namespace SabreTools.Serialization.Wrappers
#endregion
#region Instance Variables
/// <summary>
/// Source filename for the wrapper
/// </summary>
private readonly string? _filename;
/// <summary>
/// Source stream for the wrapper
/// </summary>
private readonly Stream _stream;
#endregion
#region Constructors
/// <summary>
/// Construct a new instance of the wrapper from a file path
/// </summary>
public XZ(string filename)
{
_filename = filename;
_stream = File.Open(_filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
}
/// <summary>
/// Construct a new instance of the wrapper from a Stream
/// </summary>
public XZ(Stream stream)
{
_filename = null;
_stream = stream;
if (stream is FileStream fs)
_filename = fs.Name;
}
/// <summary>
/// Create a XZ archive from a byte array and offset
/// </summary>
@@ -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
/// <inheritdoc/>
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
}
}