diff --git a/src/SharpCompress/Common/Arc/ArcEntry.cs b/src/SharpCompress/Common/Arc/ArcEntry.cs new file mode 100644 index 000000000..a67f10d11 --- /dev/null +++ b/src/SharpCompress/Common/Arc/ArcEntry.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using SharpCompress.Common.GZip; +using SharpCompress.Common.Tar; + +namespace SharpCompress.Common.Arc +{ + public class ArcEntry : Entry + { + private readonly ArcFilePart? _filePart; + + internal ArcEntry(ArcFilePart? filePart) + { + _filePart = filePart; + } + + public override long Crc + { + get + { + if (_filePart == null) + { + return 0; + } + return _filePart.Header.Crc16; + } + } + + 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 => throw new NotImplementedException(); + + public override DateTime? LastModifiedTime => null; + + public override DateTime? CreatedTime => null; + + public override DateTime? LastAccessedTime => null; + + public override DateTime? ArchivedTime => null; + + public override bool IsEncrypted => false; + + public override bool IsDirectory => false; + + public override bool IsSplitAfter => false; + + internal override IEnumerable Parts => _filePart.Empty(); + } +} diff --git a/src/SharpCompress/Common/Arc/ArcEntryHeader.cs b/src/SharpCompress/Common/Arc/ArcEntryHeader.cs new file mode 100644 index 000000000..01243c92b --- /dev/null +++ b/src/SharpCompress/Common/Arc/ArcEntryHeader.cs @@ -0,0 +1,83 @@ +using System; +using System.IO; +using System.Linq; +using System.Text; + +namespace SharpCompress.Common.Arc +{ + public class ArcEntryHeader + { + public ArchiveEncoding ArchiveEncoding { get; } + public CompressionType CompressionMethod { get; private set; } + public string? Name { get; private set; } + public long CompressedSize { get; private set; } + public DateTime DateTime { get; private set; } + public int Crc16 { get; private set; } + public long OriginalSize { get; private set; } + public long DataStartPosition { get; private set; } + + public ArcEntryHeader(ArchiveEncoding archiveEncoding) + { + this.ArchiveEncoding = archiveEncoding; + } + + public ArcEntryHeader? ReadHeader(Stream stream) + { + byte[] headerBytes = new byte[29]; + if (stream.Read(headerBytes, 0, headerBytes.Length) != headerBytes.Length) + { + return null; + } + return LoadFrom(headerBytes); + } + + public ArcEntryHeader LoadFrom(byte[] headerBytes) + { + CompressionMethod = GetCompressionType(headerBytes[1]); + + // Read name + int nameEnd = Array.IndexOf(headerBytes, (byte)0, 1); // Find null terminator + Name = Encoding.UTF8.GetString(headerBytes, 2, nameEnd > 0 ? nameEnd - 2 : 12); + + int offset = 15; + CompressedSize = BitConverter.ToUInt32(headerBytes, offset); + offset += 4; + uint rawDateTime = BitConverter.ToUInt32(headerBytes, offset); + DateTime = ConvertToDateTime(rawDateTime); + offset += 4; + Crc16 = BitConverter.ToUInt16(headerBytes, offset); + offset += 2; + OriginalSize = BitConverter.ToUInt32(headerBytes, offset); + return this; + } + + private CompressionType GetCompressionType(byte value) + { + return value switch + { + 1 or 2 => CompressionType.None, + //3 => CompressionType.RLE90, + //4 => CompressionType.Squeezed, + //5 or 6 or 7 or 8 => CompressionType.Crunched, + //9 => CompressionType.Squashed, + //10 => CompressionType.Crushed, + //11 => CompressionType.Distilled, + _ => CompressionType.Unknown, + }; + } + + public static DateTime ConvertToDateTime(long rawDateTime) + { + // Extract components using bit manipulation + int year = (int)((rawDateTime >> 25) & 0x7F) + 1980; + int month = (int)((rawDateTime >> 21) & 0xF); + int day = (int)((rawDateTime >> 16) & 0x1F); + int hour = (int)((rawDateTime >> 11) & 0x1F); + int minute = (int)((rawDateTime >> 5) & 0x3F); + int second = (int)((rawDateTime & 0x1F) * 2); // Multiply by 2 since DOS seconds are stored as halves + + // Return as a DateTime object + return new DateTime(year, month, day, hour, minute, second); + } + } +} diff --git a/src/SharpCompress/Common/Arc/ArcFilePart.cs b/src/SharpCompress/Common/Arc/ArcFilePart.cs new file mode 100644 index 000000000..7796e5e88 --- /dev/null +++ b/src/SharpCompress/Common/Arc/ArcFilePart.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using SharpCompress.Common.GZip; +using SharpCompress.Common.Tar; +using SharpCompress.Common.Tar.Headers; +using SharpCompress.Common.Zip.Headers; +using SharpCompress.IO; + +namespace SharpCompress.Common.Arc +{ + public class ArcFilePart : FilePart + { + private readonly Stream? _stream; + + internal ArcFilePart(ArcEntryHeader localArcHeader, Stream? seekableStream) + : base(localArcHeader.ArchiveEncoding) + { + _stream = seekableStream; + Header = localArcHeader; + } + + internal ArcEntryHeader Header { get; set; } + + internal override string? FilePartName => Header.Name; + + internal override Stream GetCompressedStream() + { + if (_stream != null) + { + return new ReadOnlySubStream(_stream, Header.CompressedSize); + } + return _stream.NotNull(); + } + + internal override Stream? GetRawStream() => _stream; + } +} diff --git a/src/SharpCompress/Common/Arc/ArcVolume.cs b/src/SharpCompress/Common/Arc/ArcVolume.cs new file mode 100644 index 000000000..8ebd11ea9 --- /dev/null +++ b/src/SharpCompress/Common/Arc/ArcVolume.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.Arc +{ + public class ArcVolume : Volume + { + public ArcVolume(Stream stream, ReaderOptions readerOptions, int index = 0) + : base(stream, readerOptions, index) { } + } +} diff --git a/src/SharpCompress/Common/ArchiveType.cs b/src/SharpCompress/Common/ArchiveType.cs index f9c7db09c..503804c82 100644 --- a/src/SharpCompress/Common/ArchiveType.cs +++ b/src/SharpCompress/Common/ArchiveType.cs @@ -7,4 +7,5 @@ public enum ArchiveType Tar, SevenZip, GZip, + Arc, } diff --git a/src/SharpCompress/Factories/ArcFactory.cs b/src/SharpCompress/Factories/ArcFactory.cs new file mode 100644 index 000000000..469ee1368 --- /dev/null +++ b/src/SharpCompress/Factories/ArcFactory.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; +using SharpCompress.Common; +using SharpCompress.Readers; +using SharpCompress.Readers.Arc; +using static System.Net.Mime.MediaTypeNames; + +namespace SharpCompress.Factories +{ + public class ArcFactory : Factory, IReaderFactory + { + public override string Name => "Arc"; + + public override ArchiveType? KnownArchiveType => ArchiveType.Arc; + + public override IEnumerable GetSupportedExtensions() + { + yield return "arc"; + } + + public override bool IsArchive(Stream stream, string? password = null) + { + //You may have to use some(paranoid) checks to ensure that you actually are + //processing an ARC file, since other archivers also adopted the idea of putting + //a 01Ah byte at offset 0, namely the Hyper archiver. To check if you have a + //Hyper - archive, check the next two bytes for "HP" or "ST"(or look below for + //"HYP").Also the ZOO archiver also does put a 01Ah at the start of the file, + //see the ZOO entry below. + var bytes = new byte[2]; + stream.Read(bytes, 0, 2); + return bytes[0] == 0x1A && bytes[1] < 10; //rather thin, but this is all we have + } + + public IReader OpenReader(Stream stream, ReaderOptions? options) => + ArcReader.Open(stream, options); + } +} diff --git a/src/SharpCompress/Factories/Factory.cs b/src/SharpCompress/Factories/Factory.cs index baef3b852..426528d12 100644 --- a/src/SharpCompress/Factories/Factory.cs +++ b/src/SharpCompress/Factories/Factory.cs @@ -17,6 +17,7 @@ static Factory() RegisterFactory(new SevenZipFactory()); RegisterFactory(new GZipFactory()); RegisterFactory(new TarFactory()); + RegisterFactory(new ArcFactory()); } private static readonly HashSet _factories = new(); diff --git a/src/SharpCompress/Readers/Arc/ArcReader.cs b/src/SharpCompress/Readers/Arc/ArcReader.cs new file mode 100644 index 000000000..7d58b8d42 --- /dev/null +++ b/src/SharpCompress/Readers/Arc/ArcReader.cs @@ -0,0 +1,41 @@ +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.Arc; + +namespace SharpCompress.Readers.Arc +{ + public class ArcReader : AbstractReader + { + private ArcReader(Stream stream, ReaderOptions options) + : base(options, ArchiveType.Arc) => Volume = new ArcVolume(stream, options, 0); + + public override ArcVolume Volume { get; } + + /// + /// Opens an ArcReader for Non-seeking usage with a single volume + /// + /// + /// + /// + public static ArcReader Open(Stream stream, ReaderOptions? options = null) + { + stream.CheckNotNull(nameof(stream)); + return new ArcReader(stream, options ?? new ReaderOptions()); + } + + protected override IEnumerable GetEntries(Stream stream) + { + ArcEntryHeader headerReader = new ArcEntryHeader(new ArchiveEncoding()); + ArcEntryHeader? header; + while ((header = headerReader.ReadHeader(stream)) != null) + { + yield return new ArcEntry(new ArcFilePart(header, stream)); + } + } + } +} diff --git a/src/SharpCompress/Readers/ReaderFactory.cs b/src/SharpCompress/Readers/ReaderFactory.cs index a079c2355..0cf4c2eac 100644 --- a/src/SharpCompress/Readers/ReaderFactory.cs +++ b/src/SharpCompress/Readers/ReaderFactory.cs @@ -30,7 +30,7 @@ public static IReader Open(Stream stream, ReaderOptions? options = null) } throw new InvalidOperationException( - "Cannot determine compressed stream type. Supported Reader Formats: Zip, GZip, BZip2, Tar, Rar, LZip, XZ" + "Cannot determine compressed stream type. Supported Reader Formats: Arc, Zip, GZip, BZip2, Tar, Rar, LZip, XZ" ); } } diff --git a/tests/SharpCompress.Test/Arc/ArcReaderTests.cs b/tests/SharpCompress.Test/Arc/ArcReaderTests.cs new file mode 100644 index 000000000..45efe754c --- /dev/null +++ b/tests/SharpCompress.Test/Arc/ArcReaderTests.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using SharpCompress.Common; +using Xunit; + +namespace SharpCompress.Test.Arc +{ + public class ArcReaderTests : ReaderTests + { + public ArcReaderTests() + { + UseExtensionInsteadOfNameToVerify = true; + UseCaseInsensitiveToVerify = true; + } + + [Fact] + public void Arc_Uncompressed_Read() => Read("Arc.uncompressed.arc", CompressionType.None); + } +} diff --git a/tests/TestArchives/Archives/Arc.uncompressed.arc b/tests/TestArchives/Archives/Arc.uncompressed.arc new file mode 100644 index 000000000..78cff2bc1 Binary files /dev/null and b/tests/TestArchives/Archives/Arc.uncompressed.arc differ