Skip to content
Merged
1 change: 1 addition & 0 deletions src/SharpCompress/Common/ArchiveType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ public enum ArchiveType
SevenZip,
GZip,
Arc,
Arj,
}
58 changes: 58 additions & 0 deletions src/SharpCompress/Common/Arj/ArjEntry.cs
Original file line number Diff line number Diff line change
@@ -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<FilePart> Parts => _filePart.Empty();
}
}
65 changes: 65 additions & 0 deletions src/SharpCompress/Common/Arj/ArjFilePart.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
16 changes: 16 additions & 0 deletions src/SharpCompress/Common/Arj/ArjVolume.cs
Original file line number Diff line number Diff line change
@@ -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) { }
}
}
142 changes: 142 additions & 0 deletions src/SharpCompress/Common/Arj/Headers/ArjHeader.cs
Original file line number Diff line number Diff line change
@@ -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<byte> magic = stackalloc byte[2];
if (stream.Read(magic) != 2)
{
return Array.Empty<byte>();
}

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<byte>();
}

var body = new byte[headerSize];
var read = stream.Read(body, 0, headerSize);
if (read < headerSize)
{
return Array.Empty<byte>();
}

byte[] crc = new byte[4];
read = stream.Read(crc, 0, 4);
Copy link

Copilot AI Oct 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assignment to read is useless, since its value is never read.

Suggested change
read = stream.Read(crc, 0, 4);
if (stream.Read(crc, 0, 4) < 4)
throw new EndOfStreamException("Unexpected end of stream while reading header CRC.");

Copilot uses AI. Check for mistakes.
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<byte[]> ReadExtendedHeaders(Stream reader)
{
List<byte[]> extendedHeader = new List<byte[]>();
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;
}
}
}
Loading
Loading