diff --git a/src/SharpCompress/Archives/ArchiveFactory.cs b/src/SharpCompress/Archives/ArchiveFactory.cs index 870092f1..94368ece 100644 --- a/src/SharpCompress/Archives/ArchiveFactory.cs +++ b/src/SharpCompress/Archives/ArchiveFactory.cs @@ -20,7 +20,7 @@ public static class ArchiveFactory public static IArchive Open(Stream stream, ReaderOptions? readerOptions = null) { readerOptions ??= new ReaderOptions(); - stream = new SharpCompressStream(stream, bufferSize: readerOptions.BufferSize); + stream = SharpCompressStream.Create(stream, bufferSize: readerOptions.BufferSize); return FindFactory(stream).Open(stream, readerOptions); } diff --git a/src/SharpCompress/Common/ArchiveType.cs b/src/SharpCompress/Common/ArchiveType.cs index 503804c8..9d943895 100644 --- a/src/SharpCompress/Common/ArchiveType.cs +++ b/src/SharpCompress/Common/ArchiveType.cs @@ -8,4 +8,5 @@ public enum ArchiveType SevenZip, GZip, Arc, + Arj, } diff --git a/src/SharpCompress/Common/Arj/ArjEntry.cs b/src/SharpCompress/Common/Arj/ArjEntry.cs new file mode 100644 index 00000000..8dd84e87 --- /dev/null +++ b/src/SharpCompress/Common/Arj/ArjEntry.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using SharpCompress.Common.Arc; +using SharpCompress.Common.Arj.Headers; + +namespace SharpCompress.Common.Arj +{ + public class ArjEntry : Entry + { + private readonly ArjFilePart _filePart; + + internal ArjEntry(ArjFilePart filePart) + { + _filePart = filePart; + } + + public override long Crc => _filePart.Header.OriginalCrc32; + + public override string? Key => _filePart?.Header.Name; + + public override string? LinkTarget => null; + + public override long CompressedSize => _filePart?.Header.CompressedSize ?? 0; + + public override CompressionType CompressionType + { + get + { + if (_filePart.Header.CompressionMethod == CompressionMethod.Stored) + { + return CompressionType.None; + } + return CompressionType.ArjLZ77; + } + } + + public override long Size => _filePart?.Header.OriginalSize ?? 0; + + public override DateTime? LastModifiedTime => _filePart.Header.DateTimeModified.DateTime; + + public override DateTime? CreatedTime => _filePart.Header.DateTimeCreated.DateTime; + + public override DateTime? LastAccessedTime => _filePart.Header.DateTimeAccessed.DateTime; + + public override DateTime? ArchivedTime => null; + + public override bool IsEncrypted => false; + + public override bool IsDirectory => _filePart.Header.FileType == FileType.Directory; + + public override bool IsSplitAfter => false; + + internal override IEnumerable Parts => _filePart.Empty(); + } +} diff --git a/src/SharpCompress/Common/Arj/ArjFilePart.cs b/src/SharpCompress/Common/Arj/ArjFilePart.cs new file mode 100644 index 00000000..10fe24c6 --- /dev/null +++ b/src/SharpCompress/Common/Arj/ArjFilePart.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using SharpCompress.Common.Arj.Headers; +using SharpCompress.Compressors.Arj; +using SharpCompress.IO; + +namespace SharpCompress.Common.Arj +{ + public class ArjFilePart : FilePart + { + private readonly Stream _stream; + internal ArjLocalHeader Header { get; set; } + + internal ArjFilePart(ArjLocalHeader localArjHeader, Stream seekableStream) + : base(localArjHeader.ArchiveEncoding) + { + _stream = seekableStream; + Header = localArjHeader; + } + + internal override string? FilePartName => Header.Name; + + internal override Stream GetCompressedStream() + { + if (_stream != null) + { + Stream compressedStream; + switch (Header.CompressionMethod) + { + case CompressionMethod.Stored: + compressedStream = new ReadOnlySubStream( + _stream, + Header.DataStartPosition, + Header.CompressedSize + ); + break; + case CompressionMethod.CompressedFastest: + byte[] compressedData = new byte[Header.CompressedSize]; + _stream.Position = Header.DataStartPosition; + _stream.Read(compressedData, 0, compressedData.Length); + + byte[] decompressedData = LHDecoder.DecodeFastest( + compressedData, + (int)Header.OriginalSize // ARJ can only handle files up to 2GB, so casting to int should not be an issue. + ); + + compressedStream = new MemoryStream(decompressedData); + break; + default: + throw new NotSupportedException( + "CompressionMethod: " + Header.CompressionMethod + ); + } + return compressedStream; + } + return _stream.NotNull(); + } + + internal override Stream GetRawStream() => _stream; + } +} diff --git a/src/SharpCompress/Common/Arj/ArjVolume.cs b/src/SharpCompress/Common/Arj/ArjVolume.cs new file mode 100644 index 00000000..bf3ba109 --- /dev/null +++ b/src/SharpCompress/Common/Arj/ArjVolume.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using SharpCompress.Readers; + +namespace SharpCompress.Common.Arj +{ + public class ArjVolume : Volume + { + public ArjVolume(Stream stream, ReaderOptions readerOptions, int index = 0) + : base(stream, readerOptions, index) { } + } +} diff --git a/src/SharpCompress/Common/Arj/Headers/ArjHeader.cs b/src/SharpCompress/Common/Arj/Headers/ArjHeader.cs new file mode 100644 index 00000000..138f75e7 --- /dev/null +++ b/src/SharpCompress/Common/Arj/Headers/ArjHeader.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using SharpCompress.Common.Zip.Headers; +using SharpCompress.Crypto; + +namespace SharpCompress.Common.Arj.Headers +{ + public enum ArjHeaderType + { + MainHeader, + LocalHeader, + } + + public abstract class ArjHeader + { + private const int FIRST_HDR_SIZE = 34; + private const ushort ARJ_MAGIC = 0xEA60; + + public ArjHeader(ArjHeaderType type) + { + ArjHeaderType = type; + } + + public ArjHeaderType ArjHeaderType { get; } + public byte Flags { get; set; } + public FileType FileType { get; set; } + + public abstract ArjHeader? Read(Stream reader); + + public byte[] ReadHeader(Stream stream) + { + // check for magic bytes + Span magic = stackalloc byte[2]; + if (stream.Read(magic) != 2) + { + return Array.Empty(); + } + + var magicValue = (ushort)(magic[0] | magic[1] << 8); + if (magicValue != ARJ_MAGIC) + { + throw new InvalidDataException("Not an ARJ file (wrong magic bytes)"); + } + + // read header_size + byte[] headerBytes = new byte[2]; + stream.Read(headerBytes, 0, 2); + var headerSize = (ushort)(headerBytes[0] | headerBytes[1] << 8); + if (headerSize < 1) + { + return Array.Empty(); + } + + var body = new byte[headerSize]; + var read = stream.Read(body, 0, headerSize); + if (read < headerSize) + { + return Array.Empty(); + } + + byte[] crc = new byte[4]; + read = stream.Read(crc, 0, 4); + var checksum = Crc32Stream.Compute(body); + // Compute the hash value + if (checksum != BitConverter.ToUInt32(crc, 0)) + { + throw new InvalidDataException("Header checksum is invalid"); + } + return body; + } + + protected List ReadExtendedHeaders(Stream reader) + { + List extendedHeader = new List(); + byte[] buffer = new byte[2]; + + while (true) + { + int bytesRead = reader.Read(buffer, 0, 2); + if (bytesRead < 2) + { + throw new EndOfStreamException( + "Unexpected end of stream while reading extended header size." + ); + } + + var extHeaderSize = (ushort)(buffer[0] | (buffer[1] << 8)); + if (extHeaderSize == 0) + { + return extendedHeader; + } + + byte[] header = new byte[extHeaderSize]; + bytesRead = reader.Read(header, 0, extHeaderSize); + if (bytesRead < extHeaderSize) + { + throw new EndOfStreamException( + "Unexpected end of stream while reading extended header data." + ); + } + + byte[] crc = new byte[4]; + bytesRead = reader.Read(crc, 0, 4); + if (bytesRead < 4) + { + throw new EndOfStreamException( + "Unexpected end of stream while reading extended header CRC." + ); + } + + var checksum = Crc32Stream.Compute(header); + if (checksum != BitConverter.ToUInt32(crc, 0)) + { + throw new InvalidDataException("Extended header checksum is invalid"); + } + + extendedHeader.Add(header); + } + } + + // Flag helpers + public bool IsGabled => (Flags & 0x01) != 0; + public bool IsAnsiPage => (Flags & 0x02) != 0; + public bool IsVolume => (Flags & 0x04) != 0; + public bool IsArjProtected => (Flags & 0x08) != 0; + public bool IsPathSym => (Flags & 0x10) != 0; + public bool IsBackup => (Flags & 0x20) != 0; + public bool IsSecured => (Flags & 0x40) != 0; + public bool IsAltName => (Flags & 0x80) != 0; + + public static FileType FileTypeFromByte(byte value) + { + return Enum.IsDefined(typeof(FileType), value) + ? (FileType)value + : Headers.FileType.Unknown; + } + } +} diff --git a/src/SharpCompress/Common/Arj/Headers/ArjLocalHeader.cs b/src/SharpCompress/Common/Arj/Headers/ArjLocalHeader.cs new file mode 100644 index 00000000..f790b495 --- /dev/null +++ b/src/SharpCompress/Common/Arj/Headers/ArjLocalHeader.cs @@ -0,0 +1,161 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; + +namespace SharpCompress.Common.Arj.Headers +{ + public class ArjLocalHeader : ArjHeader + { + public ArchiveEncoding ArchiveEncoding { get; } + public long DataStartPosition { get; protected set; } + + public byte ArchiverVersionNumber { get; set; } + public byte MinVersionToExtract { get; set; } + public HostOS HostOS { get; set; } + public CompressionMethod CompressionMethod { get; set; } + public DosDateTime DateTimeModified { get; set; } = new DosDateTime(0); + public long CompressedSize { get; set; } + public long OriginalSize { get; set; } + public long OriginalCrc32 { get; set; } + public int FileSpecPosition { get; set; } + public int FileAccessMode { get; set; } + public byte FirstChapter { get; set; } + public byte LastChapter { get; set; } + public long ExtendedFilePosition { get; set; } + public DosDateTime DateTimeAccessed { get; set; } = new DosDateTime(0); + public DosDateTime DateTimeCreated { get; set; } = new DosDateTime(0); + public long OriginalSizeEvenForVolumes { get; set; } + public string Name { get; set; } = string.Empty; + public string Comment { get; set; } = string.Empty; + + private const byte StdHdrSize = 30; + private const byte R9HdrSize = 46; + + public ArjLocalHeader(ArchiveEncoding archiveEncoding) + : base(ArjHeaderType.LocalHeader) + { + ArchiveEncoding = + archiveEncoding ?? throw new ArgumentNullException(nameof(archiveEncoding)); + } + + public override ArjHeader? Read(Stream stream) + { + var body = ReadHeader(stream); + if (body.Length > 0) + { + ReadExtendedHeaders(stream); + var header = LoadFrom(body); + header.DataStartPosition = stream.Position; + return header; + } + return null; + } + + public ArjLocalHeader LoadFrom(byte[] headerBytes) + { + int offset = 0; + + int ReadInt16() + { + if (offset + 1 >= headerBytes.Length) + { + throw new EndOfStreamException(); + } + var v = headerBytes[offset] & 0xFF | (headerBytes[offset + 1] & 0xFF) << 8; + offset += 2; + return v; + } + long ReadInt32() + { + if (offset + 3 >= headerBytes.Length) + { + throw new EndOfStreamException(); + } + long v = + headerBytes[offset] & 0xFF + | (headerBytes[offset + 1] & 0xFF) << 8 + | (headerBytes[offset + 2] & 0xFF) << 16 + | (headerBytes[offset + 3] & 0xFF) << 24; + offset += 4; + return v; + } + + byte headerSize = headerBytes[offset++]; + ArchiverVersionNumber = headerBytes[offset++]; + MinVersionToExtract = headerBytes[offset++]; + HostOS hostOS = (HostOS)headerBytes[offset++]; + Flags = headerBytes[offset++]; + CompressionMethod = CompressionMethodFromByte(headerBytes[offset++]); + FileType = FileTypeFromByte(headerBytes[offset++]); + + offset++; // Skip 1 byte + + var rawTimestamp = ReadInt32(); + DateTimeModified = + rawTimestamp != 0 ? new DosDateTime(rawTimestamp) : new DosDateTime(0); + + CompressedSize = ReadInt32(); + OriginalSize = ReadInt32(); + OriginalCrc32 = ReadInt32(); + FileSpecPosition = ReadInt16(); + FileAccessMode = ReadInt16(); + + FirstChapter = headerBytes[offset++]; + LastChapter = headerBytes[offset++]; + + ExtendedFilePosition = 0; + OriginalSizeEvenForVolumes = 0; + + if (headerSize > StdHdrSize) + { + ExtendedFilePosition = ReadInt32(); + + if (headerSize >= R9HdrSize) + { + rawTimestamp = ReadInt32(); + DateTimeAccessed = + rawTimestamp != 0 ? new DosDateTime(rawTimestamp) : new DosDateTime(0); + rawTimestamp = ReadInt32(); + DateTimeCreated = + rawTimestamp != 0 ? new DosDateTime(rawTimestamp) : new DosDateTime(0); + OriginalSizeEvenForVolumes = ReadInt32(); + } + } + + Name = Encoding.ASCII.GetString( + headerBytes, + offset, + Array.IndexOf(headerBytes, (byte)0, offset) - offset + ); + offset += Name.Length + 1; + + Comment = Encoding.ASCII.GetString( + headerBytes, + offset, + Array.IndexOf(headerBytes, (byte)0, offset) - offset + ); + offset += Comment.Length + 1; + + return this; + } + + public static CompressionMethod CompressionMethodFromByte(byte value) + { + return value switch + { + 0 => CompressionMethod.Stored, + 1 => CompressionMethod.CompressedMost, + 2 => CompressionMethod.Compressed, + 3 => CompressionMethod.CompressedFaster, + 4 => CompressionMethod.CompressedFastest, + 8 => CompressionMethod.NoDataNoCrc, + 9 => CompressionMethod.NoData, + _ => CompressionMethod.Unknown, + }; + } + } +} diff --git a/src/SharpCompress/Common/Arj/Headers/ArjMainHeader.cs b/src/SharpCompress/Common/Arj/Headers/ArjMainHeader.cs new file mode 100644 index 00000000..6ba2b83c --- /dev/null +++ b/src/SharpCompress/Common/Arj/Headers/ArjMainHeader.cs @@ -0,0 +1,138 @@ +using System; +using System.IO; +using System.Text; +using SharpCompress.Compressors.Deflate; +using SharpCompress.Crypto; + +namespace SharpCompress.Common.Arj.Headers +{ + public class ArjMainHeader : ArjHeader + { + private const int FIRST_HDR_SIZE = 34; + private const ushort ARJ_MAGIC = 0xEA60; + + public ArchiveEncoding ArchiveEncoding { get; } + + public int ArchiverVersionNumber { get; private set; } + public int MinVersionToExtract { get; private set; } + public HostOS HostOs { get; private set; } + public int SecurityVersion { get; private set; } + public DosDateTime CreationDateTime { get; private set; } = new DosDateTime(0); + public long CompressedSize { get; private set; } + public long ArchiveSize { get; private set; } + public long SecurityEnvelope { get; private set; } + public int FileSpecPosition { get; private set; } + public int SecurityEnvelopeLength { get; private set; } + public int EncryptionVersion { get; private set; } + public int LastChapter { get; private set; } + + public int ArjProtectionFactor { get; private set; } + public int Flags2 { get; private set; } + public string Name { get; private set; } = string.Empty; + public string Comment { get; private set; } = string.Empty; + + public ArjMainHeader(ArchiveEncoding archiveEncoding) + : base(ArjHeaderType.MainHeader) + { + ArchiveEncoding = + archiveEncoding ?? throw new ArgumentNullException(nameof(archiveEncoding)); + } + + public override ArjHeader? Read(Stream stream) + { + var body = ReadHeader(stream); + ReadExtendedHeaders(stream); + return LoadFrom(body); + } + + public ArjMainHeader LoadFrom(byte[] headerBytes) + { + var offset = 1; + + byte ReadByte() + { + if (offset >= headerBytes.Length) + { + throw new EndOfStreamException(); + } + return (byte)(headerBytes[offset++] & 0xFF); + } + + int ReadInt16() + { + if (offset + 1 >= headerBytes.Length) + { + throw new EndOfStreamException(); + } + var v = headerBytes[offset] & 0xFF | (headerBytes[offset + 1] & 0xFF) << 8; + offset += 2; + return v; + } + + long ReadInt32() + { + if (offset + 3 >= headerBytes.Length) + { + throw new EndOfStreamException(); + } + long v = + headerBytes[offset] & 0xFF + | (headerBytes[offset + 1] & 0xFF) << 8 + | (headerBytes[offset + 2] & 0xFF) << 16 + | (headerBytes[offset + 3] & 0xFF) << 24; + offset += 4; + return v; + } + string ReadNullTerminatedString(byte[] x, int startIndex) + { + var result = new StringBuilder(); + int i = startIndex; + + while (i < x.Length && x[i] != 0) + { + result.Append((char)x[i]); + i++; + } + + // Skip the null terminator + i++; + if (i < x.Length) + { + byte[] remainder = new byte[x.Length - i]; + Array.Copy(x, i, remainder, 0, remainder.Length); + x = remainder; + } + + return result.ToString(); + } + + ArchiverVersionNumber = ReadByte(); + MinVersionToExtract = ReadByte(); + + var hostOsByte = ReadByte(); + HostOs = hostOsByte <= 11 ? (HostOS)hostOsByte : HostOS.Unknown; + + Flags = ReadByte(); + SecurityVersion = ReadByte(); + FileType = FileTypeFromByte(ReadByte()); + + offset++; // skip reserved + + CreationDateTime = new DosDateTime((int)ReadInt32()); + CompressedSize = ReadInt32(); + ArchiveSize = ReadInt32(); + + SecurityEnvelope = ReadInt32(); + FileSpecPosition = ReadInt16(); + SecurityEnvelopeLength = ReadInt16(); + + EncryptionVersion = ReadByte(); + LastChapter = ReadByte(); + + Name = ReadNullTerminatedString(headerBytes, offset); + Comment = ReadNullTerminatedString(headerBytes, offset + 1 + Name.Length); + + return this; + } + } +} diff --git a/src/SharpCompress/Common/Arj/Headers/CompressionMethod.cs b/src/SharpCompress/Common/Arj/Headers/CompressionMethod.cs new file mode 100644 index 00000000..acdf2ce9 --- /dev/null +++ b/src/SharpCompress/Common/Arj/Headers/CompressionMethod.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SharpCompress.Common.Arj.Headers +{ + public enum CompressionMethod + { + Stored = 0, + CompressedMost = 1, + Compressed = 2, + CompressedFaster = 3, + CompressedFastest = 4, + NoDataNoCrc = 8, + NoData = 9, + Unknown, + } +} diff --git a/src/SharpCompress/Common/Arj/Headers/DosDateTime.cs b/src/SharpCompress/Common/Arj/Headers/DosDateTime.cs new file mode 100644 index 00000000..d51045ad --- /dev/null +++ b/src/SharpCompress/Common/Arj/Headers/DosDateTime.cs @@ -0,0 +1,37 @@ +using System; + +namespace SharpCompress.Common.Arj.Headers +{ + public class DosDateTime + { + public DateTime DateTime { get; } + + public DosDateTime(long dosValue) + { + // Ensure only the lower 32 bits are used + int value = unchecked((int)(dosValue & 0xFFFFFFFF)); + + var date = (value >> 16) & 0xFFFF; + var time = value & 0xFFFF; + + var day = date & 0x1F; + var month = (date >> 5) & 0x0F; + var year = ((date >> 9) & 0x7F) + 1980; + + var second = (time & 0x1F) * 2; + var minute = (time >> 5) & 0x3F; + var hour = (time >> 11) & 0x1F; + + try + { + DateTime = new DateTime(year, month, day, hour, minute, second); + } + catch + { + DateTime = DateTime.MinValue; + } + } + + public override string ToString() => DateTime.ToString("yyyy-MM-dd HH:mm:ss"); + } +} diff --git a/src/SharpCompress/Common/Arj/Headers/FileType.cs b/src/SharpCompress/Common/Arj/Headers/FileType.cs new file mode 100644 index 00000000..39c529f0 --- /dev/null +++ b/src/SharpCompress/Common/Arj/Headers/FileType.cs @@ -0,0 +1,13 @@ +namespace SharpCompress.Common.Arj.Headers +{ + public enum FileType : byte + { + Binary = 0, + Text7Bit = 1, + CommentHeader = 2, + Directory = 3, + VolumeLabel = 4, + ChapterLabel = 5, + Unknown = 255, + } +} diff --git a/src/SharpCompress/Common/Arj/Headers/HostOS.cs b/src/SharpCompress/Common/Arj/Headers/HostOS.cs new file mode 100644 index 00000000..a74e7b0f --- /dev/null +++ b/src/SharpCompress/Common/Arj/Headers/HostOS.cs @@ -0,0 +1,19 @@ +namespace SharpCompress.Common.Arj.Headers +{ + public enum HostOS + { + MsDos = 0, + PrimOS = 1, + Unix = 2, + Amiga = 3, + MacOs = 4, + OS2 = 5, + AppleGS = 6, + AtariST = 7, + NeXT = 8, + VaxVMS = 9, + Win95 = 10, + Win32 = 11, + Unknown = 255, + } +} diff --git a/src/SharpCompress/Common/CompressionType.cs b/src/SharpCompress/Common/CompressionType.cs index 76ada667..57f65b05 100644 --- a/src/SharpCompress/Common/CompressionType.cs +++ b/src/SharpCompress/Common/CompressionType.cs @@ -29,4 +29,5 @@ public enum CompressionType Crushed, Distilled, ZStandard, + ArjLZ77, } diff --git a/src/SharpCompress/Compressors/Arj/DecodeFastest.cs b/src/SharpCompress/Compressors/Arj/DecodeFastest.cs new file mode 100644 index 00000000..f9f4ef44 --- /dev/null +++ b/src/SharpCompress/Compressors/Arj/DecodeFastest.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace SharpCompress.Compressors.Arj +{ + [CLSCompliant(true)] + public class BitReader + { + private readonly byte[] data; + private int bytePos = 0; + private int bitPos = 0; + + public BitReader(byte[] input) + { + data = input; + } + + public int ReadBits(int count) + { + int result = 0; + for (int i = 0; i < count; i++) + { + if (bytePos >= data.Length) + { + throw new EndOfStreamException(); + } + + int bit = (data[bytePos] >> (7 - bitPos)) & 1; + result = (result << 1) | bit; + + bitPos++; + if (bitPos == 8) + { + bitPos = 0; + bytePos++; + } + } + return result; + } + } + + [CLSCompliant(true)] + public static class LHDecoder + { + private const int THRESHOLD = 3; + + private static int DecodeVal(BitReader r, int from, int to) + { + int add = 0; + int bit = from; + + while (bit < to && r.ReadBits(1) == 1) + { + add |= 1 << bit; + bit++; + } + + int res = bit > 0 ? r.ReadBits(bit) : 0; + return res + add; + } + + public static byte[] DecodeFastest(byte[] data, int originalSize) + { + var res = new List(originalSize); + var r = new BitReader(data); + + while (res.Count < originalSize) + { + int len = DecodeVal(r, 0, 7); + if (len == 0) + { + byte nextChar = (byte)r.ReadBits(8); + res.Add(nextChar); + } + else + { + int repCount = len + THRESHOLD - 1; + int backPtr = DecodeVal(r, 9, 13); + + if (backPtr >= res.Count) + { + throw new InvalidDataException("invalid back_ptr"); + } + + int i = res.Count - 1 - backPtr; + for (int j = 0; j < repCount; j++) + { + res.Add(res[i]); + i++; + } + } + } + + return res.ToArray(); + } + } +} diff --git a/src/SharpCompress/Factories/ArjFactory.cs b/src/SharpCompress/Factories/ArjFactory.cs new file mode 100644 index 00000000..7499946b --- /dev/null +++ b/src/SharpCompress/Factories/ArjFactory.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using SharpCompress.Common; +using SharpCompress.Common.Arj.Headers; +using SharpCompress.Readers; +using SharpCompress.Readers.Arj; + +namespace SharpCompress.Factories +{ + public class ArjFactory : Factory, IReaderFactory + { + public override string Name => "Arj"; + + public override ArchiveType? KnownArchiveType => ArchiveType.Arj; + + public override IEnumerable GetSupportedExtensions() + { + yield return "arj"; + } + + public override bool IsArchive( + Stream stream, + string? password = null, + int bufferSize = ReaderOptions.DefaultBufferSize + ) + { + var arjHeader = new ArjMainHeader(new ArchiveEncoding()); + if (arjHeader.Read(stream) == null) + { + return false; + } + return true; + } + + public IReader OpenReader(Stream stream, ReaderOptions? options) => + ArjReader.Open(stream, options); + } +} diff --git a/src/SharpCompress/Factories/Factory.cs b/src/SharpCompress/Factories/Factory.cs index 2462f614..dba20177 100644 --- a/src/SharpCompress/Factories/Factory.cs +++ b/src/SharpCompress/Factories/Factory.cs @@ -18,6 +18,7 @@ public abstract class Factory : IFactory RegisterFactory(new GZipFactory()); RegisterFactory(new TarFactory()); RegisterFactory(new ArcFactory()); + RegisterFactory(new ArjFactory()); } private static readonly HashSet _factories = new(); diff --git a/src/SharpCompress/Readers/Arj/ArjReader.cs b/src/SharpCompress/Readers/Arj/ArjReader.cs new file mode 100644 index 00000000..d5441e27 --- /dev/null +++ b/src/SharpCompress/Readers/Arj/ArjReader.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using SharpCompress.Common; +using SharpCompress.Common.Arj; +using SharpCompress.Common.Arj.Headers; +using SharpCompress.Common.Zip; +using SharpCompress.Common.Zip.Headers; + +namespace SharpCompress.Readers.Arj +{ + public class ArjReader : AbstractReader + { + private ArjReader(Stream stream, ReaderOptions options) + : base(options, ArchiveType.Arj) => Volume = new ArjVolume(stream, options, 0); + + public override ArjVolume Volume { get; } + + /// + /// Opens an ArjReader for Non-seeking usage with a single volume + /// + /// + /// + /// + public static ArjReader Open(Stream stream, ReaderOptions? options = null) + { + stream.NotNull(nameof(stream)); + return new ArjReader(stream, options ?? new ReaderOptions()); + } + + protected override IEnumerable GetEntries(Stream stream) + { + ArchiveEncoding encoding = new ArchiveEncoding(); + var mainHeaderReader = new ArjMainHeader(encoding); + var localHeaderReader = new ArjLocalHeader(encoding); + var mainHeader = mainHeaderReader.Read(stream); + + while (true) + { + var localHeader = localHeaderReader.Read(stream); + if (localHeader == null) + break; + + yield return new ArjEntry(new ArjFilePart((ArjLocalHeader)localHeader, stream)); + } + } + } +} diff --git a/src/SharpCompress/Readers/ReaderFactory.cs b/src/SharpCompress/Readers/ReaderFactory.cs index 97846380..80995e41 100644 --- a/src/SharpCompress/Readers/ReaderFactory.cs +++ b/src/SharpCompress/Readers/ReaderFactory.cs @@ -70,7 +70,7 @@ public static class ReaderFactory } throw new InvalidFormatException( - "Cannot determine compressed stream type. Supported Reader Formats: Arc, Zip, GZip, BZip2, Tar, Rar, LZip, XZ, ZStandard" + "Cannot determine compressed stream type. Supported Reader Formats: Arc, Arj, Zip, GZip, BZip2, Tar, Rar, LZip, XZ, ZStandard" ); } } diff --git a/tests/SharpCompress.Test/ArchiveTests.cs b/tests/SharpCompress.Test/ArchiveTests.cs index 2849a71f..2f47e121 100644 --- a/tests/SharpCompress.Test/ArchiveTests.cs +++ b/tests/SharpCompress.Test/ArchiveTests.cs @@ -622,4 +622,36 @@ public class ArchiveTests : ReaderTests VerifyFiles(); } } + + [Fact] + public void ArchiveFactory_Open_WithPreWrappedStream() + { + // Test that ArchiveFactory.Open works correctly with a stream that's already wrapped + // This addresses the issue where ZIP files fail to open on Linux + var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "Zip.bzip2.noEmptyDirs.zip"); + + // Open with a pre-wrapped stream + using (var fileStream = File.OpenRead(testArchive)) + using (var wrappedStream = SharpCompressStream.Create(fileStream, bufferSize: 32768)) + using (var archive = ArchiveFactory.Open(wrappedStream)) + { + Assert.Equal(ArchiveType.Zip, archive.Type); + Assert.Equal(3, archive.Entries.Count()); + } + } + + [Fact] + public void ArchiveFactory_Open_WithRawFileStream() + { + // Test that ArchiveFactory.Open works correctly with a raw FileStream + // This is the common use case reported in the issue + var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "Zip.bzip2.noEmptyDirs.zip"); + + using (var stream = File.OpenRead(testArchive)) + using (var archive = ArchiveFactory.Open(stream)) + { + Assert.Equal(ArchiveType.Zip, archive.Type); + Assert.Equal(3, archive.Entries.Count()); + } + } } diff --git a/tests/SharpCompress.Test/Arj/ArjReaderTests.cs b/tests/SharpCompress.Test/Arj/ArjReaderTests.cs new file mode 100644 index 00000000..ee9cecca --- /dev/null +++ b/tests/SharpCompress.Test/Arj/ArjReaderTests.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using SharpCompress.Common; +using SharpCompress.Readers; +using Xunit; + +namespace SharpCompress.Test.Arj +{ + public class ArjReaderTests : ReaderTests + { + public ArjReaderTests() + { + UseExtensionInsteadOfNameToVerify = true; + UseCaseInsensitiveToVerify = true; + } + + [Fact] + public void Arj_Uncompressed_Read() => Read("Arj.store.arj", CompressionType.None); + + [Fact] + public void Arj_Method4_Read() => Read("Arj.method4.arj"); + } +} diff --git a/tests/SharpCompress.Test/ReaderTests.cs b/tests/SharpCompress.Test/ReaderTests.cs index b919331d..c5539fc8 100644 --- a/tests/SharpCompress.Test/ReaderTests.cs +++ b/tests/SharpCompress.Test/ReaderTests.cs @@ -13,29 +13,53 @@ namespace SharpCompress.Test; public abstract class ReaderTests : TestBase { + protected void Read(string testArchive, ReaderOptions? options = null) + { + ReadCore(testArchive, options, ReadImpl); + } + protected void Read( string testArchive, CompressionType expectedCompression, ReaderOptions? options = null ) { - testArchive = Path.Combine(TEST_ARCHIVES_PATH, testArchive); + ReadCore(testArchive, options, (path, opts) => ReadImpl(path, expectedCompression, opts)); + } - options ??= new ReaderOptions() { BufferSize = 0x20000 }; //test larger buffer size (need test rather than eyeballing debug logs :P) + private void ReadCore( + string testArchive, + ReaderOptions? options, + Action readImpl + ) + { + testArchive = Path.Combine(TEST_ARCHIVES_PATH, testArchive); + options ??= new ReaderOptions { BufferSize = 0x20000 }; options.LeaveStreamOpen = true; - ReadImpl(testArchive, expectedCompression, options); + readImpl(testArchive, options); options.LeaveStreamOpen = false; - ReadImpl(testArchive, expectedCompression, options); + readImpl(testArchive, options); + VerifyFiles(); } + private void ReadImpl(string testArchive, ReaderOptions options) + { + ReadImplCore(testArchive, options, UseReader); + } + private void ReadImpl( string testArchive, CompressionType expectedCompression, ReaderOptions options ) + { + ReadImplCore(testArchive, options, r => UseReader(r, expectedCompression)); + } + + private void ReadImplCore(string testArchive, ReaderOptions options, Action useReader) { using var file = File.OpenRead(testArchive); using var protectedStream = SharpCompressStream.Create( @@ -47,19 +71,17 @@ public abstract class ReaderTests : TestBase using var testStream = new TestStream(protectedStream); using (var reader = ReaderFactory.Open(testStream, options)) { - UseReader(reader, expectedCompression); + useReader(reader); protectedStream.ThrowOnDispose = false; Assert.False(testStream.IsDisposed, $"{nameof(testStream)} prematurely closed"); } - // Boolean XOR -- If the stream should be left open (true), then the stream should not be diposed (false) - // and if the stream should be closed (false), then the stream should be disposed (true) var message = $"{nameof(options.LeaveStreamOpen)} is set to '{options.LeaveStreamOpen}', so {nameof(testStream.IsDisposed)} should be set to '{!testStream.IsDisposed}', but is set to {testStream.IsDisposed}"; Assert.True(options.LeaveStreamOpen != testStream.IsDisposed, message); } - public void UseReader(IReader reader, CompressionType expectedCompression) + protected void UseReader(IReader reader, CompressionType expectedCompression) { while (reader.MoveToNextEntry()) { @@ -74,6 +96,20 @@ public abstract class ReaderTests : TestBase } } + private void UseReader(IReader reader) + { + while (reader.MoveToNextEntry()) + { + if (!reader.Entry.IsDirectory) + { + reader.WriteEntryToDirectory( + SCRATCH_FILES_PATH, + new ExtractionOptions { ExtractFullPath = true, Overwrite = true } + ); + } + } + } + protected async Task ReadAsync( string testArchive, CompressionType expectedCompression, diff --git a/tests/TestArchives/Archives/Arj.method4.arj b/tests/TestArchives/Archives/Arj.method4.arj new file mode 100644 index 00000000..5f3bbbf6 Binary files /dev/null and b/tests/TestArchives/Archives/Arj.method4.arj differ diff --git a/tests/TestArchives/Archives/Arj.store.arj b/tests/TestArchives/Archives/Arj.store.arj new file mode 100644 index 00000000..b6fa515c Binary files /dev/null and b/tests/TestArchives/Archives/Arj.store.arj differ