mirror of
https://github.com/adamhathcock/sharpcompress.git
synced 2026-02-08 05:27:04 +00:00
Compare commits
4 Commits
master
...
copilot/ad
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f796aa1fa1 | ||
|
|
c783a83a9c | ||
|
|
6ae63f6a47 | ||
|
|
7149781b0f |
@@ -10,6 +10,7 @@
|
||||
|
||||
| Archive Format | Compression Format(s) | Compress/Decompress | Archive API | Reader API | Writer API |
|
||||
| ---------------------- | ------------------------------------------------- | ------------------- | --------------- | ---------- | ------------- |
|
||||
| Ace (5) | None (stored only) | Decompress | N/A | AceReader | N/A |
|
||||
| Rar | Rar | Decompress (1) | RarArchive | RarReader | N/A |
|
||||
| Zip (2) | None, Shrink, Reduce, Implode, DEFLATE, Deflate64, BZip2, LZMA/LZMA2, PPMd | Both | ZipArchive | ZipReader | ZipWriter |
|
||||
| Tar | None | Both | TarArchive | TarReader | TarWriter (3) |
|
||||
@@ -25,7 +26,8 @@
|
||||
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. 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.
|
||||
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
|
||||
|
||||
|
||||
58
src/SharpCompress/Common/Ace/AceEntry.cs
Normal file
58
src/SharpCompress/Common/Ace/AceEntry.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SharpCompress.Common.Ace;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an entry (file or directory) within an ACE archive.
|
||||
/// </summary>
|
||||
public class AceEntry : Entry
|
||||
{
|
||||
private readonly AceFilePart? _filePart;
|
||||
|
||||
internal AceEntry(AceFilePart? filePart)
|
||||
{
|
||||
_filePart = filePart;
|
||||
}
|
||||
|
||||
public override long Crc
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_filePart is null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
return _filePart.Header.Crc32;
|
||||
}
|
||||
}
|
||||
|
||||
public override string? Key => _filePart?.Header.Name;
|
||||
|
||||
public override string? LinkTarget => null;
|
||||
|
||||
public override long CompressedSize => _filePart?.Header.CompressedSize ?? 0;
|
||||
|
||||
public override CompressionType CompressionType =>
|
||||
_filePart?.Header.CompressionMethod ?? CompressionType.Unknown;
|
||||
|
||||
public override long Size => _filePart?.Header.OriginalSize ?? 0;
|
||||
|
||||
public override DateTime? LastModifiedTime => _filePart?.Header.DateTime;
|
||||
|
||||
public override DateTime? CreatedTime => null;
|
||||
|
||||
public override DateTime? LastAccessedTime => null;
|
||||
|
||||
public override DateTime? ArchivedTime => null;
|
||||
|
||||
public override bool IsEncrypted => false;
|
||||
|
||||
public override bool IsDirectory => _filePart?.Header.IsDirectory ?? false;
|
||||
|
||||
public override bool IsSplitAfter => false;
|
||||
|
||||
public override int? Attrib => _filePart?.Header.FileAttributes;
|
||||
|
||||
internal override IEnumerable<FilePart> Parts => _filePart.Empty();
|
||||
}
|
||||
339
src/SharpCompress/Common/Ace/AceEntryHeader.cs
Normal file
339
src/SharpCompress/Common/Ace/AceEntryHeader.cs
Normal file
@@ -0,0 +1,339 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
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 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; }
|
||||
public string? Name { get; private set; }
|
||||
public long CompressedSize { get; private set; }
|
||||
public long OriginalSize { get; private set; }
|
||||
public DateTime DateTime { get; private set; }
|
||||
internal uint Crc32 { get; private set; }
|
||||
public int FileAttributes { get; private set; }
|
||||
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;
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
// Read header CRC (2 bytes) and header size (2 bytes)
|
||||
var headerPrefix = new byte[4];
|
||||
if (stream.Read(headerPrefix, 0, 4) != 4)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int headerSize = BitConverter.ToUInt16(headerPrefix, 2);
|
||||
if (headerSize < 1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read the rest of the header
|
||||
var headerData = new byte[headerSize];
|
||||
if (stream.Read(headerData, 0, headerSize) != headerSize)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int offset = 0;
|
||||
|
||||
// Header type should be 0 for main header
|
||||
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)
|
||||
{
|
||||
// Read header CRC (2 bytes) and header size (2 bytes)
|
||||
var headerPrefix = new byte[4];
|
||||
int bytesRead = stream.Read(headerPrefix, 0, 4);
|
||||
if (bytesRead < 4)
|
||||
{
|
||||
return null; // End of archive
|
||||
}
|
||||
|
||||
// ushort headerCrc = BitConverter.ToUInt16(headerPrefix, 0); // CRC for validation
|
||||
ushort headerSize = BitConverter.ToUInt16(headerPrefix, 2);
|
||||
|
||||
if (headerSize == 0)
|
||||
{
|
||||
return null; // End of archive marker
|
||||
}
|
||||
|
||||
// Read the header data
|
||||
var headerData = new byte[headerSize];
|
||||
if (stream.Read(headerData, 0, headerSize) != headerSize)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
int offset = 0;
|
||||
|
||||
// Header type (1 byte)
|
||||
byte headerType = headerData[offset++];
|
||||
|
||||
// Check for end marker or non-file header
|
||||
if (headerType == HeaderTypeMain)
|
||||
{
|
||||
// Skip main header if encountered
|
||||
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
|
||||
return ReadHeader(stream);
|
||||
}
|
||||
|
||||
// Header flags (2 bytes)
|
||||
ushort headerFlags = BitConverter.ToUInt16(headerData, offset);
|
||||
offset += 2;
|
||||
|
||||
// Packed size (4 bytes)
|
||||
CompressedSize = BitConverter.ToUInt32(headerData, offset);
|
||||
offset += 4;
|
||||
|
||||
// Original size (4 bytes)
|
||||
OriginalSize = BitConverter.ToUInt32(headerData, offset);
|
||||
offset += 4;
|
||||
|
||||
// File date/time in DOS format (4 bytes)
|
||||
uint dosDateTime = BitConverter.ToUInt32(headerData, offset);
|
||||
DateTime = ConvertDosDateTime(dosDateTime);
|
||||
offset += 4;
|
||||
|
||||
// File attributes (4 bytes)
|
||||
FileAttributes = (int)BitConverter.ToUInt32(headerData, offset);
|
||||
offset += 4;
|
||||
|
||||
// CRC32 (4 bytes)
|
||||
Crc32 = BitConverter.ToUInt32(headerData, offset);
|
||||
offset += 4;
|
||||
|
||||
// Compression type (1 byte)
|
||||
byte compressionType = headerData[offset++];
|
||||
CompressionMethod = GetCompressionType(compressionType);
|
||||
|
||||
// Compression quality/parameter (1 byte) - skip
|
||||
offset++;
|
||||
|
||||
// Parameters (2 bytes) - skip
|
||||
offset += 2;
|
||||
|
||||
// Reserved (2 bytes) - skip
|
||||
offset += 2;
|
||||
|
||||
// Filename length (2 bytes)
|
||||
ushort filenameLength = BitConverter.ToUInt16(headerData, offset);
|
||||
offset += 2;
|
||||
|
||||
// Filename
|
||||
if (offset + filenameLength <= headerData.Length)
|
||||
{
|
||||
Name = ArchiveEncoding.Decode(headerData, offset, filenameLength);
|
||||
offset += filenameLength;
|
||||
}
|
||||
|
||||
// Check if entry is a directory based on attributes or name
|
||||
IsDirectory = (FileAttributes & 0x10) != 0 || (Name?.EndsWith('/') ?? false);
|
||||
|
||||
// Handle comment if present
|
||||
if ((headerFlags & FileFlagComment) != 0)
|
||||
{
|
||||
// Comment length (2 bytes)
|
||||
if (offset + 2 <= headerData.Length)
|
||||
{
|
||||
ushort commentLength = BitConverter.ToUInt16(headerData, offset);
|
||||
offset += 2 + commentLength; // Skip comment
|
||||
}
|
||||
}
|
||||
|
||||
// Store the data start position
|
||||
DataStartPosition = stream.Position;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
private static CompressionType GetCompressionType(byte value)
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
0 => CompressionType.None, // Stored
|
||||
1 => CompressionType.Ace, // ACE 1.0 LZ77 compression
|
||||
2 => CompressionType.Ace2, // ACE 2.0 compression (improved LZ77)
|
||||
_ => CompressionType.Unknown,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts DOS date/time format to DateTime.
|
||||
/// </summary>
|
||||
private static DateTime ConvertDosDateTime(uint dosDateTime)
|
||||
{
|
||||
try
|
||||
{
|
||||
int second = (int)(dosDateTime & 0x1F) * 2;
|
||||
int minute = (int)((dosDateTime >> 5) & 0x3F);
|
||||
int hour = (int)((dosDateTime >> 11) & 0x1F);
|
||||
int day = (int)((dosDateTime >> 16) & 0x1F);
|
||||
int month = (int)((dosDateTime >> 21) & 0x0F);
|
||||
int year = (int)((dosDateTime >> 25) & 0x7F) + 1980;
|
||||
|
||||
if (
|
||||
day < 1
|
||||
|| day > 31
|
||||
|| month < 1
|
||||
|| month > 12
|
||||
|| hour > 23
|
||||
|| minute > 59
|
||||
|| second > 59
|
||||
)
|
||||
{
|
||||
return DateTime.MinValue;
|
||||
}
|
||||
|
||||
return new DateTime(year, month, day, hour, minute, second);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return DateTime.MinValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
59
src/SharpCompress/Common/Ace/AceFilePart.cs
Normal file
59
src/SharpCompress/Common/Ace/AceFilePart.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using SharpCompress.IO;
|
||||
|
||||
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
|
||||
{
|
||||
private readonly Stream? _stream;
|
||||
|
||||
internal AceFilePart(AceEntryHeader header, Stream? stream)
|
||||
: base(header.ArchiveEncoding)
|
||||
{
|
||||
_stream = stream;
|
||||
Header = header;
|
||||
}
|
||||
|
||||
internal AceEntryHeader Header { get; }
|
||||
|
||||
internal override string? FilePartName => Header.Name;
|
||||
|
||||
internal override Stream GetCompressedStream()
|
||||
{
|
||||
if (_stream is null)
|
||||
{
|
||||
throw new InvalidOperationException("Stream is not available.");
|
||||
}
|
||||
|
||||
switch (Header.CompressionMethod)
|
||||
{
|
||||
case CompressionType.None:
|
||||
// Stored - no compression
|
||||
return new ReadOnlySubStream(
|
||||
_stream,
|
||||
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:
|
||||
throw new NotSupportedException(
|
||||
$"ACE compression method '{Header.CompressionMethod}' is not supported. Only stored (uncompressed) entries can be extracted."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
internal override Stream? GetRawStream() => _stream;
|
||||
}
|
||||
13
src/SharpCompress/Common/Ace/AceVolume.cs
Normal file
13
src/SharpCompress/Common/Ace/AceVolume.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using System.IO;
|
||||
using SharpCompress.Readers;
|
||||
|
||||
namespace SharpCompress.Common.Ace;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a volume (file) of an ACE archive.
|
||||
/// </summary>
|
||||
public class AceVolume : Volume
|
||||
{
|
||||
public AceVolume(Stream stream, ReaderOptions readerOptions, int index = 0)
|
||||
: base(stream, readerOptions, index) { }
|
||||
}
|
||||
@@ -9,4 +9,5 @@ public enum ArchiveType
|
||||
GZip,
|
||||
Arc,
|
||||
Arj,
|
||||
Ace,
|
||||
}
|
||||
|
||||
@@ -30,4 +30,6 @@ public enum CompressionType
|
||||
Distilled,
|
||||
ZStandard,
|
||||
ArjLZ77,
|
||||
Ace,
|
||||
Ace2,
|
||||
}
|
||||
|
||||
64
src/SharpCompress/Factories/AceFactory.cs
Normal file
64
src/SharpCompress/Factories/AceFactory.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Readers;
|
||||
using SharpCompress.Readers.Ace;
|
||||
|
||||
namespace SharpCompress.Factories;
|
||||
|
||||
/// <summary>
|
||||
/// Factory for ACE archive format detection and reader creation.
|
||||
/// </summary>
|
||||
public class AceFactory : Factory, IReaderFactory
|
||||
{
|
||||
// ACE signature: bytes at offset 7 should be "**ACE**"
|
||||
private static readonly byte[] AceSignature =
|
||||
[
|
||||
(byte)'*',
|
||||
(byte)'*',
|
||||
(byte)'A',
|
||||
(byte)'C',
|
||||
(byte)'E',
|
||||
(byte)'*',
|
||||
(byte)'*',
|
||||
];
|
||||
|
||||
public override string Name => "Ace";
|
||||
|
||||
public override ArchiveType? KnownArchiveType => ArchiveType.Ace;
|
||||
|
||||
public override IEnumerable<string> GetSupportedExtensions()
|
||||
{
|
||||
yield return "ace";
|
||||
}
|
||||
|
||||
public override bool IsArchive(
|
||||
Stream stream,
|
||||
string? password = null,
|
||||
int bufferSize = ReaderOptions.DefaultBufferSize
|
||||
)
|
||||
{
|
||||
// ACE files have a specific signature
|
||||
// First two bytes are typically 0x60 0xEA (signature bytes)
|
||||
// At offset 7, there should be "**ACE**" (7 bytes)
|
||||
var bytes = new byte[14];
|
||||
if (stream.Read(bytes, 0, 14) != 14)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for "**ACE**" at offset 7
|
||||
for (int i = 0; i < AceSignature.Length; i++)
|
||||
{
|
||||
if (bytes[7 + i] != AceSignature[i])
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public IReader OpenReader(Stream stream, ReaderOptions? options) =>
|
||||
AceReader.Open(stream, options);
|
||||
}
|
||||
@@ -19,6 +19,7 @@ public abstract class Factory : IFactory
|
||||
RegisterFactory(new TarFactory());
|
||||
RegisterFactory(new ArcFactory());
|
||||
RegisterFactory(new ArjFactory());
|
||||
RegisterFactory(new AceFactory());
|
||||
}
|
||||
|
||||
private static readonly HashSet<Factory> _factories = new();
|
||||
|
||||
67
src/SharpCompress/Readers/Ace/AceReader.cs
Normal file
67
src/SharpCompress/Readers/Ace/AceReader.cs
Normal file
@@ -0,0 +1,67 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Common.Ace;
|
||||
|
||||
namespace SharpCompress.Readers.Ace;
|
||||
|
||||
/// <summary>
|
||||
/// Reader for ACE archives.
|
||||
/// 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;
|
||||
private bool _mainHeaderRead;
|
||||
|
||||
private AceReader(Stream stream, ReaderOptions options)
|
||||
: base(options, ArchiveType.Ace)
|
||||
{
|
||||
Volume = new AceVolume(stream, options, 0);
|
||||
_mainHeaderReader = new AceEntryHeader(Options.ArchiveEncoding);
|
||||
}
|
||||
|
||||
public override AceVolume Volume { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Opens an AceReader for non-seeking usage with a single volume.
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream containing the ACE archive.</param>
|
||||
/// <param name="options">Reader options.</param>
|
||||
/// <returns>An AceReader instance.</returns>
|
||||
public static AceReader Open(Stream stream, ReaderOptions? options = null)
|
||||
{
|
||||
stream.NotNull(nameof(stream));
|
||||
return new AceReader(stream, options ?? new ReaderOptions());
|
||||
}
|
||||
|
||||
protected override IEnumerable<AceEntry> GetEntries(Stream stream)
|
||||
{
|
||||
// First, skip past the main header if we haven't already
|
||||
if (!_mainHeaderRead)
|
||||
{
|
||||
if (!_mainHeaderReader.ReadMainHeader(stream))
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
_mainHeaderRead = true;
|
||||
}
|
||||
|
||||
// Read file entries - create new header reader for each entry
|
||||
// since ReadHeader modifies the object state
|
||||
AceEntryHeader entryHeaderReader = new AceEntryHeader(Options.ArchiveEncoding);
|
||||
AceEntryHeader? header;
|
||||
while ((header = entryHeaderReader.ReadHeader(stream)) != null)
|
||||
{
|
||||
yield return new AceEntry(new AceFilePart(header, stream));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,7 +70,7 @@ public static class ReaderFactory
|
||||
}
|
||||
|
||||
throw new InvalidFormatException(
|
||||
"Cannot determine compressed stream type. Supported Reader Formats: Arc, Arj, Zip, GZip, BZip2, Tar, Rar, LZip, XZ, ZStandard"
|
||||
"Cannot determine compressed stream type. Supported Reader Formats: Ace, Arc, Arj, Zip, GZip, BZip2, Tar, Rar, LZip, XZ, ZStandard"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
92
tests/SharpCompress.Test/Ace/AceReaderTests.cs
Normal file
92
tests/SharpCompress.Test/Ace/AceReaderTests.cs
Normal file
@@ -0,0 +1,92 @@
|
||||
using System.IO;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Factories;
|
||||
using SharpCompress.Readers;
|
||||
using SharpCompress.Readers.Ace;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpCompress.Test.Ace;
|
||||
|
||||
public class AceReaderTests : ReaderTests
|
||||
{
|
||||
public AceReaderTests()
|
||||
{
|
||||
UseExtensionInsteadOfNameToVerify = true;
|
||||
UseCaseInsensitiveToVerify = true;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ace_Stored_Read()
|
||||
{
|
||||
var archivePath = Path.Combine(TEST_ARCHIVES_PATH, "Ace.stored.ace");
|
||||
using var stream = File.OpenRead(archivePath);
|
||||
using var reader = AceReader.Open(stream, new ReaderOptions());
|
||||
|
||||
int entryCount = 0;
|
||||
while (reader.MoveToNextEntry())
|
||||
{
|
||||
Assert.Equal(CompressionType.None, reader.Entry.CompressionType);
|
||||
Assert.False(reader.Entry.IsDirectory);
|
||||
Assert.NotNull(reader.Entry.Key);
|
||||
entryCount++;
|
||||
}
|
||||
Assert.True(entryCount > 0, "Expected at least one entry in the archive");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ace_Stored_Extract()
|
||||
{
|
||||
var archivePath = Path.Combine(TEST_ARCHIVES_PATH, "Ace.stored.ace");
|
||||
using var stream = File.OpenRead(archivePath);
|
||||
using var reader = AceReader.Open(stream, new ReaderOptions());
|
||||
|
||||
while (reader.MoveToNextEntry())
|
||||
{
|
||||
if (!reader.Entry.IsDirectory)
|
||||
{
|
||||
reader.WriteEntryToDirectory(
|
||||
SCRATCH_FILES_PATH,
|
||||
new ExtractionOptions { ExtractFullPath = true, Overwrite = true }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Verify that files were extracted
|
||||
var extractedFiles = Directory.GetFiles(
|
||||
SCRATCH_FILES_PATH,
|
||||
"*.*",
|
||||
SearchOption.AllDirectories
|
||||
);
|
||||
Assert.True(extractedFiles.Length > 0, "Expected at least one file to be extracted");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ace_Factory_Detection()
|
||||
{
|
||||
var archivePath = Path.Combine(TEST_ARCHIVES_PATH, "Ace.stored.ace");
|
||||
using var stream = File.OpenRead(archivePath);
|
||||
|
||||
var aceFactory = new AceFactory();
|
||||
Assert.True(aceFactory.IsArchive(stream));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ace_Reader_Properties()
|
||||
{
|
||||
var archivePath = Path.Combine(TEST_ARCHIVES_PATH, "Ace.stored.ace");
|
||||
using var stream = File.OpenRead(archivePath);
|
||||
using var reader = AceReader.Open(stream, new ReaderOptions());
|
||||
|
||||
Assert.Equal(ArchiveType.Ace, reader.ArchiveType);
|
||||
Assert.NotNull(reader.Volume);
|
||||
|
||||
if (reader.MoveToNextEntry())
|
||||
{
|
||||
Assert.Equal("test.txt", reader.Entry.Key);
|
||||
Assert.Equal(CompressionType.None, reader.Entry.CompressionType);
|
||||
Assert.False(reader.Entry.IsEncrypted);
|
||||
Assert.False(reader.Entry.IsDirectory);
|
||||
Assert.False(reader.Entry.IsSplitAfter);
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
tests/TestArchives/Archives/Ace.stored.ace
Normal file
BIN
tests/TestArchives/Archives/Ace.stored.ace
Normal file
Binary file not shown.
Reference in New Issue
Block a user