mirror of
https://github.com/SabreTools/SabreTools.Serialization.git
synced 2026-09-25 00:05:07 +00:00
Implement basic ZSTD detection and extraction. (#30)
* Implement basic ZSTD detection and extraction. * Fix comment * Remove newline * Add newline * Move section * more newlines banished to the ether * fix comment and name * fix final i think
This commit is contained in:
committed by
GitHub
parent
d24f8a2fce
commit
5ce085ad2b
@@ -289,6 +289,11 @@ namespace ExtractionTool.Features
|
||||
case XZP xzp:
|
||||
xzp.Extract(OutputPath, Debug);
|
||||
break;
|
||||
|
||||
// ZSTD
|
||||
case ZSTD zstd:
|
||||
zstd.Extract(OutputPath, Debug);
|
||||
break;
|
||||
|
||||
// Everything else
|
||||
default:
|
||||
|
||||
7
SabreTools.Serialization/Models/ZSTD/Constants.cs
Normal file
7
SabreTools.Serialization/Models/ZSTD/Constants.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace SabreTools.Data.Models.ZSTD
|
||||
{
|
||||
public static class Constants
|
||||
{
|
||||
public static readonly byte[] SignatureBytes = [0xB5, 0x2F, 0xFD];
|
||||
}
|
||||
}
|
||||
22
SabreTools.Serialization/Models/ZSTD/Header.cs
Normal file
22
SabreTools.Serialization/Models/ZSTD/Header.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
namespace SabreTools.Data.Models.ZSTD
|
||||
{
|
||||
/// <summary>
|
||||
/// Header
|
||||
/// </summary>
|
||||
/// <see cref="https://datatracker.ietf.org/doc/html/rfc8878#section-3.1.1-3.2"/>
|
||||
public sealed class Header
|
||||
{
|
||||
/// <summary>
|
||||
/// Despite never being referred to as such, and being hard to find in the documentation, the least significant
|
||||
/// byte changed with versions until 0.8.
|
||||
/// 0.1 used 0x1E, then it was 0.2(0x22)-0.8(0x28)
|
||||
/// </summary>
|
||||
/// <see cref="https://github.com/facebook/zstd/issues/713"/>
|
||||
public byte VersionByte;
|
||||
|
||||
/// <summary>
|
||||
/// "0x?? 0xB5 0x2F 0xFD"
|
||||
/// </summary>
|
||||
public byte[]? Magic;
|
||||
}
|
||||
}
|
||||
62
SabreTools.Serialization/Readers/ZSTD.cs
Normal file
62
SabreTools.Serialization/Readers/ZSTD.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
using System.IO;
|
||||
using SabreTools.Data.Models.ZSTD;
|
||||
using SabreTools.IO.Extensions;
|
||||
using static SabreTools.Data.Models.ZSTD.Constants;
|
||||
|
||||
namespace SabreTools.Serialization.Readers
|
||||
{
|
||||
public class ZSTD : BaseBinaryReader<Header>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override Header? Deserialize(Stream? data)
|
||||
{
|
||||
// If the data is invalid
|
||||
if (data == null || !data.CanRead)
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
// Cache the current offset
|
||||
long initialOffset = data.Position;
|
||||
|
||||
#region Header
|
||||
|
||||
var header = ParseHeader(data);
|
||||
if (header == null)
|
||||
return null;
|
||||
|
||||
// Valid versions are 0x1E and 0x22-0x28.
|
||||
// According to RFC-8878, the current version is still 0x28, and it should stay that way now that
|
||||
// it's a stable format.
|
||||
if ((header.VersionByte < 0x22 || header.VersionByte > 0x28) && header.VersionByte != 0x1E)
|
||||
return null;
|
||||
|
||||
#endregion
|
||||
|
||||
return header;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore the actual error
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a Stream into a Header
|
||||
/// </summary>
|
||||
/// <param name="data">Stream to parse</param>
|
||||
/// <returns>Filled Header on success, null on error</returns>
|
||||
public static Header? ParseHeader(Stream data)
|
||||
{
|
||||
var obj = new Header();
|
||||
|
||||
obj.VersionByte = data.ReadByteValue();
|
||||
obj.Magic = data.ReadBytes(3);
|
||||
if (!obj.Magic.EqualsExactly(SignatureBytes))
|
||||
return null;
|
||||
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,6 +60,7 @@ namespace SabreTools.Serialization
|
||||
WrapperType.WAD => WAD3.Create(data),
|
||||
WrapperType.XZ => XZ.Create(data),
|
||||
WrapperType.XZP => XZP.Create(data),
|
||||
WrapperType.ZSTD => ZSTD.Create(data),
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
@@ -824,6 +825,13 @@ namespace SabreTools.Serialization
|
||||
return WrapperType.XZP;
|
||||
|
||||
#endregion
|
||||
|
||||
#region ZSTD
|
||||
|
||||
if (magic.StartsWith([null, 0xB5, 0x2F, 0xFD]))
|
||||
return WrapperType.ZSTD;
|
||||
|
||||
#endregion
|
||||
|
||||
// We couldn't find a supported match
|
||||
return WrapperType.UNKNOWN;
|
||||
|
||||
@@ -252,5 +252,10 @@ namespace SabreTools.Serialization.Wrappers
|
||||
/// Xbox Package File
|
||||
/// </summary>
|
||||
XZP,
|
||||
|
||||
/// <summary>
|
||||
/// ZStandard compressed file
|
||||
/// </summary>
|
||||
ZSTD,
|
||||
}
|
||||
}
|
||||
|
||||
67
SabreTools.Serialization/Wrappers/ZSTD.Extraction.cs
Normal file
67
SabreTools.Serialization/Wrappers/ZSTD.Extraction.cs
Normal file
@@ -0,0 +1,67 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
#if NET462_OR_GREATER || NETCOREAPP
|
||||
using SharpCompress.Compressors.ZStandard;
|
||||
#endif
|
||||
|
||||
namespace SabreTools.Serialization.Wrappers
|
||||
{
|
||||
/// <summary>
|
||||
/// This is a shell wrapper; one that does not contain
|
||||
/// any actual parsing. It is used as a placeholder for
|
||||
/// types that typically do not have models.
|
||||
/// </summary>
|
||||
public partial class ZSTD : IExtractable
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public bool Extract(string outputDirectory, bool includeDebug)
|
||||
{
|
||||
// Ensure there is data to extract
|
||||
if (Magic == null)
|
||||
{
|
||||
if (includeDebug) Console.Error.WriteLine("Invalid archive detected, skipping...");
|
||||
return false;
|
||||
}
|
||||
|
||||
#if NET462_OR_GREATER || NETCOREAPP
|
||||
try
|
||||
{
|
||||
// Ensure directory separators are consistent
|
||||
string filename = (Filename != null ? Path.GetFileName(Filename).Replace(".zstd", string.Empty) : null)
|
||||
?? (Filename != null ? Path.GetFileName(Filename).Replace(".zst", 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 zStandard stream
|
||||
var zstdStream = new ZStandardStream(_dataSource, false);
|
||||
|
||||
// Write the file
|
||||
using var fs = File.Open(filename, FileMode.Create, FileAccess.Write, FileShare.None);
|
||||
zstdStream.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
|
||||
}
|
||||
}
|
||||
}
|
||||
101
SabreTools.Serialization/Wrappers/ZSTD.cs
Normal file
101
SabreTools.Serialization/Wrappers/ZSTD.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
using System.IO;
|
||||
using SabreTools.Data.Models.ZSTD;
|
||||
using SabreTools.IO.Extensions;
|
||||
|
||||
namespace SabreTools.Serialization.Wrappers
|
||||
{
|
||||
public partial class ZSTD : WrapperBase<Header>
|
||||
{
|
||||
#region Descriptive Properties
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string DescriptionString => "ZSTD file";
|
||||
|
||||
#endregion
|
||||
|
||||
#region Extension Properties
|
||||
|
||||
/// <inheritdoc cref="Header.VersionByte"/>
|
||||
public byte VersionByte => Model.VersionByte;
|
||||
|
||||
/// <inheritdoc cref="Header.Magic"/>
|
||||
public byte[]? Magic => Model.Magic;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ZSTD(Data.Models.ZSTD.Header model, byte[] data) : base(model, data) { }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ZSTD(Data.Models.ZSTD.Header model, byte[] data, int offset) : base(model, data, offset) { }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ZSTD(Data.Models.ZSTD.Header model, byte[] data, int offset, int length) : base(model, data, offset, length) { }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ZSTD(Data.Models.ZSTD.Header model, Stream data) : base(model, data) { }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ZSTD(Data.Models.ZSTD.Header model, Stream data, long offset) : base(model, data, offset) { }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ZSTD(Data.Models.ZSTD.Header model, Stream data, long offset, long length) : base(model, data, offset, length) { }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Static Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Create a ZSTD file from a byte array and offset
|
||||
/// </summary>
|
||||
/// <param name="data">Byte array representing the ZSTD file</param>
|
||||
/// <param name="offset">Offset within the array to parse</param>
|
||||
/// <returns>A ZSTD wrapper on success, null on failure</returns>
|
||||
public static ZSTD? Create(byte[]? data, int offset)
|
||||
{
|
||||
// If the data is invalid
|
||||
if (data == null || data.Length == 0)
|
||||
return null;
|
||||
|
||||
// If the offset is out of bounds
|
||||
if (offset < 0 || offset >= data.Length)
|
||||
return null;
|
||||
|
||||
// Create a memory stream and use that
|
||||
var dataStream = new MemoryStream(data, offset, data.Length - offset);
|
||||
return Create(dataStream);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a ZSTD file from a Stream
|
||||
/// </summary>
|
||||
/// <param name="data">Stream representing the ZSTD file </param>
|
||||
/// <returns>A ZSTD wrapper on success, null on failure</returns>
|
||||
public static ZSTD? Create(Stream? data)
|
||||
{
|
||||
// If the data is invalid
|
||||
if (data == null || !data.CanRead)
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
// Cache the current offset
|
||||
long currentOffset = data.Position;
|
||||
|
||||
var model = new Readers.ZSTD().Deserialize(data);
|
||||
if (model == null)
|
||||
return null;
|
||||
|
||||
return new ZSTD(model, data, currentOffset);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user