Files
Matt Nadareski 8f49e190d8 Fix everything
2026-03-24 19:17:25 -04:00

248 lines
7.9 KiB
C#

using System.IO;
using SabreTools.Data.Models.GZIP;
using SabreTools.IO.Extensions;
using SabreTools.Numerics.Extensions;
namespace SabreTools.Wrappers
{
public partial class GZip : WrapperBase<Archive>
{
#region Descriptive Properties
/// <inheritdoc/>
public override string DescriptionString => "gzip Archive";
#endregion
#region Extension Properties
/// <summary>
/// Content CRC-32 as stored in the extra field
/// </summary>
/// <remarks>Only guaranteed for Torrent GZip format</remarks>
public byte[]? ContentCrc32
{
get
{
// Only valid for Torrent GZip
if (!IsTorrentGZip)
return null;
// Use the cached value, if it exists
if (field is not null)
return field;
// CRC-32 is the second packed field
int extraIndex = 0x10;
field = Header.ExtraFieldBytes.ReadBytes(ref extraIndex, 0x04);
return field;
}
} = null;
/// <summary>
/// Content MD5 as stored in the extra field
/// </summary>
/// <remarks>Only guaranteed for Torrent GZip format</remarks>
public byte[]? ContentMd5
{
get
{
// Only valid for Torrent GZip
if (!IsTorrentGZip)
return null;
// Use the cached value, if it exists
if (field is not null)
return field;
// MD5 is the first packed field
int extraIndex = 0x00;
field = Header.ExtraFieldBytes.ReadBytes(ref extraIndex, 0x10);
return field;
}
} = null;
/// <summary>
/// Content size as stored in the extra field
/// </summary>
/// <remarks>Only guaranteed for Torrent GZip format</remarks>
public ulong ContentSize
{
get
{
// Only valid for Torrent GZip
if (!IsTorrentGZip)
return 0;
// Use the cached value, if it exists
if (field > 0)
return field;
// Size is the third packed field
int extraIndex = 0x14;
field = Header.ExtraFieldBytes.ReadUInt64LittleEndian(ref extraIndex);
return field;
}
} = 0;
/// <summary>
/// Offset to the compressed data
/// </summary>
/// <remarks>Returns -1 on error</remarks>
public long DataOffset
{
get
{
// Use the cached value, if it exists
if (field > -1)
return field;
// Minimum offset is 10 bytes:
// - ID1 (1)
// - ID2 (1)
// - CompressionMethod (1)
// - Flags (1)
// - LastModifiedTime (4)
// - ExtraFlags (1)
// - OperatingSystem (1)
field = 10;
// Add extra lengths
field += Header.ExtraLength;
if (Header.OriginalFileName is not null)
field += Header.OriginalFileName.Length + 1;
if (Header.FileComment is not null)
field += Header.FileComment.Length + 1;
if (Header.CRC16 is not null)
field += 2;
return field;
}
} = -1;
/// <inheritdoc cref="Archive.Header"/>
public Header Header => Model.Header;
/// <summary>
/// Indicates if the archive is in the standard
/// "Torrent GZip" format. This format is used by
/// some programs to store extended hashes in the
/// header while maintaining the format otherwise.
/// </summary>
public bool IsTorrentGZip
{
get
{
// Torrent GZip uses normal deflate, not GZIP deflate
if (Header.CompressionMethod != CompressionMethod.Deflate)
return false;
// Only the extra field should be present
if (Header.Flags != Flags.FEXTRA)
return false;
// The modification should be 0x00000000, but some implementations
// do not set this correctly, so it is skipped.
// No extra flags are set
if (Header.ExtraFlags != 0x00)
return false;
// The OS should be FAT, regardless of the original platform, but
// some implementations do not set this correctly, so it is skipped.
// The extra field is non-standard, using the following format:
// - 0x00-0x0F - MD5 hash of the internal file
// - 0x10-0x13 - CRC-32 checksum of the internal file
// - 0x14-0x1B - Little-endian file size of the internal file
if (Header.ExtraLength != 0x1C)
return false;
if (Header.ExtraFieldBytes is null || Header.ExtraFieldBytes.Length != 0x1C)
return false;
return true;
}
}
/// <inheritdoc cref="Archive.Trailer"/>
public Trailer Trailer => Model.Trailer;
#endregion
#region Constructors
/// <inheritdoc/>
public GZip(Archive model, byte[] data) : base(model, data) { }
/// <inheritdoc/>
public GZip(Archive model, byte[] data, int offset) : base(model, data, offset) { }
/// <inheritdoc/>
public GZip(Archive model, byte[] data, int offset, int length) : base(model, data, offset, length) { }
/// <inheritdoc/>
public GZip(Archive model, Stream data) : base(model, data) { }
/// <inheritdoc/>
public GZip(Archive model, Stream data, long offset) : base(model, data, offset) { }
/// <inheritdoc/>
public GZip(Archive model, Stream data, long offset, long length) : base(model, data, offset, length) { }
#endregion
#region Static Constructors
/// <summary>
/// Create a GZip archive from a byte array and offset
/// </summary>
/// <param name="data">Byte array representing the archive</param>
/// <param name="offset">Offset within the array to parse</param>
/// <returns>A GZip wrapper on success, null on failure</returns>
public static GZip? Create(byte[]? data, int offset)
{
// If the data is invalid
if (data is 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 GZip archive from a Stream
/// </summary>
/// <param name="data">Stream representing the archive</param>
/// <returns>A GZip wrapper on success, null on failure</returns>
public static GZip? Create(Stream? data)
{
// If the data is invalid
if (data is null || !data.CanRead)
return null;
try
{
// Cache the current offset
long currentOffset = data.Position;
var model = new Serialization.Readers.GZip().Deserialize(data);
if (model is null)
return null;
return new GZip(model, data, currentOffset);
}
catch
{
return null;
}
}
#endregion
}
}