Merge pull request #903 from TwanVanDongen/master
Base Reader implementation of .ARC format
This commit is contained in:
commit
b41bcc349e
11 changed files with 308 additions and 1 deletions
60
src/SharpCompress/Common/Arc/ArcEntry.cs
Normal file
60
src/SharpCompress/Common/Arc/ArcEntry.cs
Normal file
|
|
@ -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<FilePart> Parts => _filePart.Empty();
|
||||
}
|
||||
}
|
||||
83
src/SharpCompress/Common/Arc/ArcEntryHeader.cs
Normal file
83
src/SharpCompress/Common/Arc/ArcEntryHeader.cs
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
41
src/SharpCompress/Common/Arc/ArcFilePart.cs
Normal file
41
src/SharpCompress/Common/Arc/ArcFilePart.cs
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
16
src/SharpCompress/Common/Arc/ArcVolume.cs
Normal file
16
src/SharpCompress/Common/Arc/ArcVolume.cs
Normal file
|
|
@ -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) { }
|
||||
}
|
||||
}
|
||||
|
|
@ -7,4 +7,5 @@ public enum ArchiveType
|
|||
Tar,
|
||||
SevenZip,
|
||||
GZip,
|
||||
Arc,
|
||||
}
|
||||
|
|
|
|||
42
src/SharpCompress/Factories/ArcFactory.cs
Normal file
42
src/SharpCompress/Factories/ArcFactory.cs
Normal file
|
|
@ -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<string> 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ public abstract class Factory : IFactory
|
|||
RegisterFactory(new SevenZipFactory());
|
||||
RegisterFactory(new GZipFactory());
|
||||
RegisterFactory(new TarFactory());
|
||||
RegisterFactory(new ArcFactory());
|
||||
}
|
||||
|
||||
private static readonly HashSet<Factory> _factories = new();
|
||||
|
|
|
|||
41
src/SharpCompress/Readers/Arc/ArcReader.cs
Normal file
41
src/SharpCompress/Readers/Arc/ArcReader.cs
Normal file
|
|
@ -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<ArcEntry, ArcVolume>
|
||||
{
|
||||
private ArcReader(Stream stream, ReaderOptions options)
|
||||
: base(options, ArchiveType.Arc) => Volume = new ArcVolume(stream, options, 0);
|
||||
|
||||
public override ArcVolume Volume { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Opens an ArcReader for Non-seeking usage with a single volume
|
||||
/// </summary>
|
||||
/// <param name="stream"></param>
|
||||
/// <param name="options"></param>
|
||||
/// <returns></returns>
|
||||
public static ArcReader Open(Stream stream, ReaderOptions? options = null)
|
||||
{
|
||||
stream.CheckNotNull(nameof(stream));
|
||||
return new ArcReader(stream, options ?? new ReaderOptions());
|
||||
}
|
||||
|
||||
protected override IEnumerable<ArcEntry> 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -30,7 +30,7 @@ public static class ReaderFactory
|
|||
}
|
||||
|
||||
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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
22
tests/SharpCompress.Test/Arc/ArcReaderTests.cs
Normal file
22
tests/SharpCompress.Test/Arc/ArcReaderTests.cs
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
BIN
tests/TestArchives/Archives/Arc.uncompressed.arc
Normal file
BIN
tests/TestArchives/Archives/Arc.uncompressed.arc
Normal file
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue