Add ACE 2.0 format reading support

Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2025-11-29 15:28:32 +00:00
parent c783a83a9c
commit f796aa1fa1
5 changed files with 135 additions and 14 deletions

View File

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

View File

@@ -7,17 +7,29 @@ namespace SharpCompress.Common.Ace;
/// <summary>
/// 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.
/// </summary>
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; }
/// <summary>
/// Gets the ACE archive version (10 for ACE 1.0, 20 for ACE 2.0).
/// </summary>
public byte AceVersion { get; private set; }
/// <summary>
/// Gets whether this is an ACE 2.0 archive.
/// </summary>
public bool IsAce20 => AceVersion >= 20;
/// <summary>
/// Gets the host operating system that created the archive.
/// </summary>
public byte HostOs { get; private set; }
/// <summary>
/// Gets whether the archive is solid.
/// </summary>
public bool IsSolid { get; private set; }
/// <summary>
/// Gets whether the archive is part of a multi-volume set.
/// </summary>
public bool IsMultiVolume { get; private set; }
public AceEntryHeader(ArchiveEncoding archiveEncoding)
{
ArchiveEncoding = archiveEncoding;
@@ -38,6 +75,7 @@ public class AceEntryHeader
/// <summary>
/// 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.
/// </summary>
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;
}
/// <summary>
/// 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.
/// </summary>
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,
};
}

View File

@@ -6,6 +6,7 @@ namespace SharpCompress.Common.Ace;
/// <summary>
/// Represents a file part within an ACE archive.
/// Supports both ACE 1.0 and ACE 2.0 formats.
/// </summary>
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."
);

View File

@@ -30,4 +30,6 @@ public enum CompressionType
Distilled,
ZStandard,
ArjLZ77,
Ace,
Ace2,
}

View File

@@ -7,10 +7,16 @@ namespace SharpCompress.Readers.Ace;
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// ACE 2.0 additions over ACE 1.0:
/// - Improved LZ77 compression (compression type 2)
/// - Recovery record support
/// - Additional header flags
/// </remarks>
public class AceReader : AbstractReader<AceEntry, AceVolume>
{
private readonly AceEntryHeader _mainHeaderReader;