using System.IO; using SabreTools.Data.Models.GZIP; using SabreTools.IO.Extensions; using SabreTools.Numerics.Extensions; namespace SabreTools.Wrappers { public partial class GZip : WrapperBase { #region Descriptive Properties /// public override string DescriptionString => "gzip Archive"; #endregion // TODO: Add alternative Torrent GZip header fields as properties #region Extension Properties /// /// Content CRC-32 as stored in the extra field /// /// Only guaranteed for Torrent GZip format 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; /// /// Content MD5 as stored in the extra field /// /// Only guaranteed for Torrent GZip format 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; /// /// Content size as stored in the extra field /// /// Only guaranteed for Torrent GZip format 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; /// /// Offset to the compressed data /// /// Returns -1 on error 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; /// public Header Header => Model.Header; /// /// 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. /// 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 // The following fields are included for alternate headers: // - 0x1C - Alternative header type // - 0x1D-0x2C - Alternative MD5 hash // - 0x2D-0x40 - Alternative SHA-1 hash // - 0x41-0x44 - Alternative CRC-32 checksum // - 0x45-0x4C - Alternative little-endian uncompressed file size if (Header.ExtraLength != 0x1C && Header.ExtraLength != 0x4D) return false; if (Header.ExtraFieldBytes is null || (Header.ExtraFieldBytes.Length != 0x1C && Header.ExtraFieldBytes.Length != 0x4D)) return false; return true; } } /// public Trailer Trailer => Model.Trailer; #endregion #region Constructors /// public GZip(Archive model, byte[] data) : base(model, data) { } /// public GZip(Archive model, byte[] data, int offset) : base(model, data, offset) { } /// public GZip(Archive model, byte[] data, int offset, int length) : base(model, data, offset, length) { } /// public GZip(Archive model, Stream data) : base(model, data) { } /// public GZip(Archive model, Stream data, long offset) : base(model, data, offset) { } /// public GZip(Archive model, Stream data, long offset, long length) : base(model, data, offset, length) { } #endregion #region Static Constructors /// /// Create a GZip archive from a byte array and offset /// /// Byte array representing the archive /// Offset within the array to parse /// A GZip wrapper on success, null on failure 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); } /// /// Create a GZip archive from a Stream /// /// Stream representing the archive /// A GZip wrapper on success, null on failure 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 } }