From f796aa1fa1981387283d5798e454e7fa3336c854 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 29 Nov 2025 15:28:32 +0000 Subject: [PATCH] Add ACE 2.0 format reading support Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com> --- FORMATS.md | 2 +- .../Common/Ace/AceEntryHeader.cs | 121 ++++++++++++++++-- src/SharpCompress/Common/Ace/AceFilePart.cs | 12 +- src/SharpCompress/Common/CompressionType.cs | 2 + src/SharpCompress/Readers/Ace/AceReader.cs | 12 +- 5 files changed, 135 insertions(+), 14 deletions(-) diff --git a/FORMATS.md b/FORMATS.md index 7e1a74e4..b75a2d9f 100644 --- a/FORMATS.md +++ b/FORMATS.md @@ -26,7 +26,7 @@ 2. Zip format supports pkware and WinzipAES encryption. However, encrypted LZMA is not supported. Zip64 reading/writing is supported but only with seekable streams as the Zip spec doesn't support Zip64 data in post data descriptors. Deflate64 is only supported for reading. 3. The Tar format requires a file size in the header. If no size is specified to the TarWriter and the stream is not seekable, then an exception will be thrown. 4. The 7Zip format doesn't allow for reading as a forward-only stream so 7Zip is only supported through the Archive API -5. ACE is a proprietary archive format. Only reading of stored (uncompressed) entries is supported due to the proprietary nature of the compression algorithms. ACE version 1 format is supported. +5. ACE is a proprietary archive format. Both ACE 1.0 and ACE 2.0 formats are supported for reading. Only stored (uncompressed) entries can be extracted due to the proprietary nature of the compression algorithms (ACE LZ77 and ACE 2.0 improved LZ77). 6. LZip has no support for extra data like the file name or timestamp. There is a default filename used when looking at the entry Key on the archive. ## Compression Streams diff --git a/src/SharpCompress/Common/Ace/AceEntryHeader.cs b/src/SharpCompress/Common/Ace/AceEntryHeader.cs index ea78cdbe..bb0684e8 100644 --- a/src/SharpCompress/Common/Ace/AceEntryHeader.cs +++ b/src/SharpCompress/Common/Ace/AceEntryHeader.cs @@ -7,17 +7,29 @@ namespace SharpCompress.Common.Ace; /// /// Represents header information for an ACE archive entry. /// ACE format uses little-endian byte ordering. +/// Supports both ACE 1.0 and ACE 2.0 formats. /// public class AceEntryHeader { // Header type constants private const byte HeaderTypeMain = 0; private const byte HeaderTypeFile = 1; + private const byte HeaderTypeRecovery = 2; - // Header flags - private const ushort FlagAddSize = 0x0001; - private const ushort FlagComment = 0x0002; - private const ushort FlagSolid = 0x8000; + // Header flags for main header + private const ushort MainFlagComment = 0x0002; + private const ushort MainFlagSfx = 0x0200; + private const ushort MainFlagLocked = 0x0400; + private const ushort MainFlagSolid = 0x0800; + private const ushort MainFlagMultiVolume = 0x1000; + private const ushort MainFlagAv = 0x2000; + private const ushort MainFlagRecovery = 0x4000; + + // Header flags for file header + private const ushort FileFlagAddSize = 0x0001; + private const ushort FileFlagComment = 0x0002; + private const ushort FileFlagContinued = 0x4000; + private const ushort FileFlagContinuing = 0x8000; public ArchiveEncoding ArchiveEncoding { get; } public CompressionType CompressionMethod { get; private set; } @@ -30,6 +42,31 @@ public class AceEntryHeader public long DataStartPosition { get; private set; } public bool IsDirectory { get; private set; } + /// + /// Gets the ACE archive version (10 for ACE 1.0, 20 for ACE 2.0). + /// + public byte AceVersion { get; private set; } + + /// + /// Gets whether this is an ACE 2.0 archive. + /// + public bool IsAce20 => AceVersion >= 20; + + /// + /// Gets the host operating system that created the archive. + /// + public byte HostOs { get; private set; } + + /// + /// Gets whether the archive is solid. + /// + public bool IsSolid { get; private set; } + + /// + /// Gets whether the archive is part of a multi-volume set. + /// + public bool IsMultiVolume { get; private set; } + public AceEntryHeader(ArchiveEncoding archiveEncoding) { ArchiveEncoding = archiveEncoding; @@ -38,6 +75,7 @@ public class AceEntryHeader /// /// Reads the main archive header from the stream. /// Returns true if this is a valid ACE archive. + /// Supports both ACE 1.0 and ACE 2.0 formats. /// public bool ReadMainHeader(Stream stream) { @@ -61,18 +99,78 @@ public class AceEntryHeader return false; } + int offset = 0; + // Header type should be 0 for main header - if (headerData[0] != HeaderTypeMain) + if (headerData[offset++] != HeaderTypeMain) { return false; } + // Header flags (2 bytes) + ushort headerFlags = BitConverter.ToUInt16(headerData, offset); + offset += 2; + + IsSolid = (headerFlags & MainFlagSolid) != 0; + IsMultiVolume = (headerFlags & MainFlagMultiVolume) != 0; + + // Skip signature "**ACE**" (7 bytes) + offset += 7; + + // ACE version (1 byte) - 10 for ACE 1.0, 20 for ACE 2.0 + if (offset < headerData.Length) + { + AceVersion = headerData[offset++]; + } + + // Extract version needed (1 byte) + if (offset < headerData.Length) + { + offset++; // Skip version needed + } + + // Host OS (1 byte) + if (offset < headerData.Length) + { + HostOs = headerData[offset++]; + } + + // Volume number (1 byte) + if (offset < headerData.Length) + { + offset++; // Skip volume number + } + + // Creation date/time (4 bytes) + if (offset + 4 <= headerData.Length) + { + offset += 4; // Skip datetime + } + + // Reserved fields (8 bytes) + if (offset + 8 <= headerData.Length) + { + offset += 8; + } + + // Skip additional fields based on flags + // Handle comment if present + if ((headerFlags & MainFlagComment) != 0) + { + if (offset + 2 <= headerData.Length) + { + ushort commentLength = BitConverter.ToUInt16(headerData, offset); + offset += 2 + commentLength; + } + } + return true; } /// /// Reads the next file entry header from the stream. /// Returns null if no more entries or end of archive. + /// Supports both ACE 1.0 and ACE 2.0 formats. /// public AceEntryHeader? ReadHeader(Stream stream) { @@ -111,6 +209,13 @@ public class AceEntryHeader return ReadHeader(stream); } + // Skip recovery record headers (ACE 2.0 feature) + if (headerType == HeaderTypeRecovery) + { + // Skip to next header + return ReadHeader(stream); + } + if (headerType != HeaderTypeFile) { // Unknown header type - skip @@ -170,7 +275,7 @@ public class AceEntryHeader IsDirectory = (FileAttributes & 0x10) != 0 || (Name?.EndsWith('/') ?? false); // Handle comment if present - if ((headerFlags & FlagComment) != 0) + if ((headerFlags & FileFlagComment) != 0) { // Comment length (2 bytes) if (offset + 2 <= headerData.Length) @@ -191,8 +296,8 @@ public class AceEntryHeader return value switch { 0 => CompressionType.None, // Stored - 1 => CompressionType.Lzw, // LZ77 - closest equivalent - 2 => CompressionType.Lzw, // ACE v2.0 compression + 1 => CompressionType.Ace, // ACE 1.0 LZ77 compression + 2 => CompressionType.Ace2, // ACE 2.0 compression (improved LZ77) _ => CompressionType.Unknown, }; } diff --git a/src/SharpCompress/Common/Ace/AceFilePart.cs b/src/SharpCompress/Common/Ace/AceFilePart.cs index 21506be5..1c1567e8 100644 --- a/src/SharpCompress/Common/Ace/AceFilePart.cs +++ b/src/SharpCompress/Common/Ace/AceFilePart.cs @@ -6,6 +6,7 @@ namespace SharpCompress.Common.Ace; /// /// Represents a file part within an ACE archive. +/// Supports both ACE 1.0 and ACE 2.0 formats. /// public class AceFilePart : FilePart { @@ -38,9 +39,16 @@ public class AceFilePart : FilePart Header.DataStartPosition, Header.CompressedSize ); + case CompressionType.Ace: + case CompressionType.Ace2: + // ACE 1.0 and 2.0 use proprietary compression methods + // The algorithms are not publicly documented + throw new NotSupportedException( + $"ACE compression method '{Header.CompressionMethod}' is not supported. " + + "Only stored (uncompressed) entries can be extracted. " + + "ACE uses proprietary compression algorithms that are not publicly documented." + ); default: - // ACE uses proprietary compression methods that are not publicly documented - // For now, we throw an exception for compressed entries throw new NotSupportedException( $"ACE compression method '{Header.CompressionMethod}' is not supported. Only stored (uncompressed) entries can be extracted." ); diff --git a/src/SharpCompress/Common/CompressionType.cs b/src/SharpCompress/Common/CompressionType.cs index 7c50758a..46753a30 100644 --- a/src/SharpCompress/Common/CompressionType.cs +++ b/src/SharpCompress/Common/CompressionType.cs @@ -30,4 +30,6 @@ public enum CompressionType Distilled, ZStandard, ArjLZ77, + Ace, + Ace2, } diff --git a/src/SharpCompress/Readers/Ace/AceReader.cs b/src/SharpCompress/Readers/Ace/AceReader.cs index f64480e8..c185e8e6 100644 --- a/src/SharpCompress/Readers/Ace/AceReader.cs +++ b/src/SharpCompress/Readers/Ace/AceReader.cs @@ -7,10 +7,16 @@ namespace SharpCompress.Readers.Ace; /// /// Reader for ACE archives. -/// ACE is a proprietary archive format. This implementation supports version 1 archives -/// and can extract uncompressed (stored) entries. Compressed entries require proprietary -/// decompression algorithms that are not publicly documented. +/// ACE is a proprietary archive format. This implementation supports both ACE 1.0 and ACE 2.0 formats +/// and can read archive metadata and extract uncompressed (stored) entries. +/// Compressed entries require proprietary decompression algorithms that are not publicly documented. /// +/// +/// ACE 2.0 additions over ACE 1.0: +/// - Improved LZ77 compression (compression type 2) +/// - Recovery record support +/// - Additional header flags +/// public class AceReader : AbstractReader { private readonly AceEntryHeader _mainHeaderReader;