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:
HeroponRikiBestest
2025-10-29 22:01:23 -04:00
committed by GitHub
parent d24f8a2fce
commit 5ce085ad2b
8 changed files with 277 additions and 0 deletions

View File

@@ -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:

View File

@@ -0,0 +1,7 @@
namespace SabreTools.Data.Models.ZSTD
{
public static class Constants
{
public static readonly byte[] SignatureBytes = [0xB5, 0x2F, 0xFD];
}
}

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

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

View File

@@ -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;

View File

@@ -252,5 +252,10 @@ namespace SabreTools.Serialization.Wrappers
/// Xbox Package File
/// </summary>
XZP,
/// <summary>
/// ZStandard compressed file
/// </summary>
ZSTD,
}
}

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

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