Add Compact Pro archive format support with basic functionality

This commit is contained in:
2026-04-17 13:23:01 +01:00
parent 4837379c3c
commit d4001d0f53
11 changed files with 688 additions and 0 deletions

View File

@@ -0,0 +1,42 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using Aaru.CommonTypes.Interfaces;
namespace Aaru.Archives;
public sealed partial class CompactPro : IArchive
{
const string MODULE_NAME = "Compact Pro Archive Plugin";
string _comment;
Encoding _encoding;
List<Entry> _entries;
ArchiveSupportedFeature _features;
Stream _stream;
#region IArchive Members
/// <inheritdoc />
public string Name => "compactpro";
/// <inheritdoc />
public Guid Id => new("B7E2C3A1-4F8D-4E6A-9C1B-3D5F7A2E8B4C");
/// <inheritdoc />
public string Author => Authors.NataliaPortillo;
/// <inheritdoc />
public bool Opened { get; private set; }
/// <inheritdoc />
public ArchiveSupportedFeature ArchiveFeatures => !Opened
? ArchiveSupportedFeature.SupportsCompression |
ArchiveSupportedFeature.SupportsFilenames |
ArchiveSupportedFeature.HasEntryTimestamp |
ArchiveSupportedFeature.SupportsSubdirectories |
ArchiveSupportedFeature.HasExplicitDirectories |
ArchiveSupportedFeature.SupportsXAttrs
: _features;
/// <inheritdoc />
public int NumberOfEntries => Opened ? _entries.Count : -1;
#endregion
}

View File

@@ -0,0 +1,37 @@
namespace Aaru.Archives;
public sealed partial class CompactPro
{
const int MIN_HEADER_SIZE = 8;
/// <summary>First byte of a Compact Pro archive is always 0x01.</summary>
const byte MARKER = 0x01;
/// <summary>Encryption flag (bit 0 of entry flags).</summary>
const ushort FLAG_ENCRYPTED = 0x01;
/// <summary>LZH compression for resource fork (bit 1 of entry flags).</summary>
const ushort FLAG_RESOURCE_LZH = 0x02;
/// <summary>LZH compression for data fork (bit 2 of entry flags).</summary>
const ushort FLAG_DATA_LZH = 0x04;
/// <summary>Bit 7 in name length byte indicates a directory entry.</summary>
const byte NAME_DIRECTORY_FLAG = 0x80;
/// <summary>Mask for the actual name length (lower 7 bits).</summary>
const byte NAME_LENGTH_MASK = 0x7F;
/// <summary>Size of file entry metadata (excluding name) in bytes.</summary>
const int FILE_METADATA_SIZE = 45;
/// <summary>Size of directory entry metadata (excluding name) in bytes.</summary>
const int DIR_METADATA_SIZE = 2;
// XAttr name constants
const string XATTR_COMMENT = "comment";
const string XATTR_APPLE_RESOURCE_FORK = "com.apple.ResourceFork";
const string XATTR_APPLE_FINDER_INFO = "com.apple.FinderInfo";
const string XATTR_APPLE_HFS_TYPE = "hfs.type";
const string XATTR_APPLE_HFS_CREATOR = "hfs.creator";
}

View File

@@ -0,0 +1,158 @@
using System;
using System.IO;
using Aaru.CommonTypes.Enums;
using Aaru.CommonTypes.Interfaces;
using Aaru.CommonTypes.Structs;
using Aaru.Compression.CompactPro;
using Aaru.Filters;
using Aaru.Helpers.IO;
using FileAttributes = Aaru.CommonTypes.Structs.FileAttributes;
namespace Aaru.Archives;
public sealed partial class CompactPro
{
#region IArchive Members
/// <inheritdoc />
public ErrorNumber GetFilename(int entryNumber, out string fileName)
{
fileName = null;
if(!Opened) return ErrorNumber.NotOpened;
if(entryNumber < 0 || entryNumber >= _entries.Count) return ErrorNumber.OutOfRange;
Entry entry = _entries[entryNumber];
fileName = entry.DirectoryPath is not null
? entry.DirectoryPath + "/" + (entry.Filename ?? "")
: entry.Filename ?? "";
return ErrorNumber.NoError;
}
/// <inheritdoc />
public ErrorNumber GetEntryNumber(string fileName, bool caseInsensitiveMatch, out int entryNumber)
{
entryNumber = -1;
if(!Opened) return ErrorNumber.NotOpened;
StringComparison comparison = caseInsensitiveMatch
? StringComparison.CurrentCultureIgnoreCase
: StringComparison.CurrentCulture;
for(int i = 0, count = _entries.Count; i < count; i++)
{
GetFilename(i, out string name);
if(name is null) continue;
if(!name.Equals(fileName, comparison)) continue;
entryNumber = i;
return ErrorNumber.NoError;
}
return ErrorNumber.NoSuchFile;
}
/// <inheritdoc />
public ErrorNumber GetCompressedSize(int entryNumber, out long length)
{
length = -1;
if(!Opened) return ErrorNumber.NotOpened;
if(entryNumber < 0 || entryNumber >= _entries.Count) return ErrorNumber.OutOfRange;
length = _entries[entryNumber].DataCompressedSize;
return ErrorNumber.NoError;
}
/// <inheritdoc />
public ErrorNumber GetUncompressedSize(int entryNumber, out long length)
{
length = -1;
if(!Opened) return ErrorNumber.NotOpened;
if(entryNumber < 0 || entryNumber >= _entries.Count) return ErrorNumber.OutOfRange;
length = _entries[entryNumber].DataUncompressedSize;
return ErrorNumber.NoError;
}
/// <inheritdoc />
public ErrorNumber Stat(int entryNumber, out FileEntryInfo stat)
{
stat = null;
if(!Opened) return ErrorNumber.NotOpened;
if(entryNumber < 0 || entryNumber >= _entries.Count) return ErrorNumber.OutOfRange;
Entry entry = _entries[entryNumber];
stat = new FileEntryInfo
{
Attributes = entry.IsDirectory ? FileAttributes.Directory : FileAttributes.File,
Blocks = entry.DataUncompressedSize / 512,
BlockSize = 512,
Length = entry.DataUncompressedSize,
LastWriteTimeUtc = entry.ModificationTime,
CreationTimeUtc = entry.CreationTime
};
if(entry.DataUncompressedSize % 512 != 0) stat.Blocks++;
return ErrorNumber.NoError;
}
/// <inheritdoc />
public ErrorNumber GetEntry(int entryNumber, out IFilter filter)
{
filter = null;
if(!Opened) return ErrorNumber.NotOpened;
if(entryNumber < 0 || entryNumber >= _entries.Count) return ErrorNumber.OutOfRange;
Entry entry = _entries[entryNumber];
if(entry.IsDirectory) return ErrorNumber.InvalidArgument;
if(entry.Encrypted) return ErrorNumber.NotSupported;
Stream stream;
if(entry.DataUncompressedSize == 0)
stream = new MemoryStream([]);
else
{
stream = new OffsetStream(new NonClosableStream(_stream),
entry.DataOffset,
entry.DataOffset + entry.DataCompressedSize - 1);
if(entry.DataLzh)
stream = new LzhStream(stream, entry.DataUncompressedSize);
else
stream = new RleStream(stream, entry.DataUncompressedSize);
}
filter = new ZZZNoFilter();
ErrorNumber errno = filter.Open(stream);
if(errno == ErrorNumber.NoError) return ErrorNumber.NoError;
stream.Close();
return errno;
}
#endregion
}

View File

@@ -0,0 +1,57 @@
using System.IO;
using System.Text;
using Aaru.CommonTypes.Interfaces;
using Aaru.Helpers;
namespace Aaru.Archives;
public sealed partial class CompactPro
{
#region IArchive Members
/// <inheritdoc />
public bool Identify(IFilter filter)
{
if(filter.DataForkLength < MIN_HEADER_SIZE) return false;
Stream stream = filter.GetDataForkStream();
stream.Position = 0;
var hdr = new byte[MIN_HEADER_SIZE];
stream.ReadExactly(hdr, 0, hdr.Length);
// First byte must be the marker
if(hdr[0] != MARKER) return false;
// Bytes 4-7 are the big-endian directory offset
var dirOffset = BigEndianBitConverter.ToUInt32(hdr, 4);
// Directory offset must be within the file, with room for at least the header CRC + numEntries + commentLen
if(dirOffset + 7 > (ulong)filter.DataForkLength) return false;
return true;
}
/// <inheritdoc />
public void GetInformation(IFilter filter, Encoding encoding, out string information)
{
information = "";
if(filter.DataForkLength < MIN_HEADER_SIZE) return;
Stream stream = filter.GetDataForkStream();
stream.Position = 0;
var hdr = new byte[MIN_HEADER_SIZE];
stream.ReadExactly(hdr, 0, hdr.Length);
if(hdr[0] != MARKER) return;
var sb = new StringBuilder();
sb.AppendLine(Localization.CompactPro_archive);
information = sb.ToString();
}
#endregion
}

View File

@@ -0,0 +1,190 @@
using System.Text;
using Aaru.CommonTypes.Enums;
using Aaru.CommonTypes.Interfaces;
using Aaru.Helpers;
namespace Aaru.Archives;
public sealed partial class CompactPro
{
void ParseDirectory(string parentPath, int numEntries)
{
while(numEntries > 0)
{
int namelenByte = _stream.ReadByte();
if(namelenByte < 0) break;
int nameLen = namelenByte & NAME_LENGTH_MASK;
bool isDir = (namelenByte & NAME_DIRECTORY_FLAG) != 0;
var name = "";
if(nameLen > 0)
{
var nameBytes = new byte[nameLen];
_stream.ReadExactly(nameBytes, 0, nameLen);
name = _encoding.GetString(nameBytes);
}
if(isDir)
{
// Directory entry: 2 bytes for number of child entries
var dirMeta = new byte[DIR_METADATA_SIZE];
_stream.ReadExactly(dirMeta, 0, DIR_METADATA_SIZE);
var numDirEntries = BigEndianBitConverter.ToUInt16(dirMeta, 0);
string dirPath = parentPath.Length > 0 ? parentPath + "/" + name : name;
var dirEntry = new Entry
{
Filename = name,
DirectoryPath = parentPath.Length > 0 ? parentPath : null,
IsDirectory = true
};
_entries.Add(dirEntry);
ParseDirectory(dirPath, numDirEntries);
// The directory entry plus its children count as numDirEntries + 1
numEntries -= numDirEntries + 1;
}
else
{
// File entry: 45 bytes of metadata
var fileMeta = new byte[FILE_METADATA_SIZE];
_stream.ReadExactly(fileMeta, 0, FILE_METADATA_SIZE);
// Parse fields (all big-endian)
// byte volume = fileMeta[0];
var fileOffset = BigEndianBitConverter.ToUInt32(fileMeta, 1);
var fileType = BigEndianBitConverter.ToUInt32(fileMeta, 5);
var creator = BigEndianBitConverter.ToUInt32(fileMeta, 9);
var creationDate = BigEndianBitConverter.ToUInt32(fileMeta, 13);
var modificationDate = BigEndianBitConverter.ToUInt32(fileMeta, 17);
var finderFlags = BigEndianBitConverter.ToUInt16(fileMeta, 21);
var crc = BigEndianBitConverter.ToUInt32(fileMeta, 23);
var flags = BigEndianBitConverter.ToUInt16(fileMeta, 27);
var resourceLen = BigEndianBitConverter.ToUInt32(fileMeta, 29);
var dataLen = BigEndianBitConverter.ToUInt32(fileMeta, 33);
var resourceCompLen = BigEndianBitConverter.ToUInt32(fileMeta, 37);
var dataCompLen = BigEndianBitConverter.ToUInt32(fileMeta, 41);
var entry = new Entry
{
Filename = name,
DirectoryPath = parentPath.Length > 0 ? parentPath : null,
IsDirectory = false,
DataOffset = fileOffset + resourceCompLen,
DataCompressedSize = dataCompLen,
DataUncompressedSize = dataLen,
DataLzh = (flags & FLAG_DATA_LZH) != 0,
ResourceOffset = fileOffset,
ResourceCompressedSize = resourceCompLen,
ResourceUncompressedSize = resourceLen,
ResourceLzh = (flags & FLAG_RESOURCE_LZH) != 0,
FileType = fileType,
Creator = creator,
FinderFlags = finderFlags,
Crc32 = crc,
Encrypted = (flags & FLAG_ENCRYPTED) != 0,
CreationTime = DateHandlers.MacToDateTime(creationDate),
ModificationTime = DateHandlers.MacToDateTime(modificationDate)
};
_entries.Add(entry);
numEntries--;
}
}
}
#region IArchive Members
/// <inheritdoc />
public ErrorNumber Open(IFilter filter, Encoding encoding)
{
if(filter.DataForkLength < MIN_HEADER_SIZE) return ErrorNumber.InvalidArgument;
_encoding = encoding ?? Encoding.GetEncoding("macintosh");
_stream = filter.GetDataForkStream();
_stream.Position = 0;
var hdr = new byte[MIN_HEADER_SIZE];
_stream.ReadExactly(hdr, 0, hdr.Length);
if(hdr[0] != MARKER) return ErrorNumber.InvalidArgument;
// byte 1 = volume, bytes 2-3 = xmagic (skip)
var dirOffset = BigEndianBitConverter.ToUInt32(hdr, 4);
if(dirOffset + 7 > (ulong)_stream.Length) return ErrorNumber.InvalidArgument;
_stream.Position = dirOffset;
// Read directory header: CRC(4) + numEntries(2) + commentLen(1)
var dirHdr = new byte[7];
_stream.ReadExactly(dirHdr, 0, 7);
// Skip CRC (bytes 0-3)
var numEntries = BigEndianBitConverter.ToUInt16(dirHdr, 4);
byte commentLen = dirHdr[6];
if(commentLen > 0)
{
var commentBytes = new byte[commentLen];
_stream.ReadExactly(commentBytes, 0, commentLen);
_comment = _encoding.GetString(commentBytes);
}
_entries = [];
ParseDirectory("", numEntries);
_features = ArchiveSupportedFeature.SupportsFilenames | ArchiveSupportedFeature.HasEntryTimestamp;
var hasCompression = false;
var hasSubdirectories = false;
var hasDirectories = false;
var hasXAttrs = false;
for(int i = 0, count = _entries.Count; i < count; i++)
{
Entry entry = _entries[i];
if(entry.IsDirectory) hasDirectories = true;
if(entry.DataLzh || entry.ResourceLzh) hasCompression = true;
if(entry.DirectoryPath is not null) hasSubdirectories = true;
if(!entry.IsDirectory && (entry.ResourceUncompressedSize > 0 || entry.FileType != 0 || entry.Creator != 0))
hasXAttrs = true;
}
if(_comment is not null) hasXAttrs = true;
if(hasCompression) _features |= ArchiveSupportedFeature.SupportsCompression;
if(hasSubdirectories) _features |= ArchiveSupportedFeature.SupportsSubdirectories;
if(hasDirectories) _features |= ArchiveSupportedFeature.HasExplicitDirectories;
if(hasXAttrs) _features |= ArchiveSupportedFeature.SupportsXAttrs;
Opened = true;
return ErrorNumber.NoError;
}
/// <inheritdoc />
public void Close()
{
if(!Opened) return;
_entries = null;
_comment = null;
Opened = false;
}
#endregion
}

View File

@@ -0,0 +1,38 @@
using System;
namespace Aaru.Archives;
public sealed partial class CompactPro
{
#region Nested type: Entry
struct Entry
{
public string Filename;
public string DirectoryPath;
public bool IsDirectory;
// Data fork
public long DataOffset;
public long DataCompressedSize;
public long DataUncompressedSize;
public bool DataLzh;
// Resource fork
public long ResourceOffset;
public long ResourceCompressedSize;
public long ResourceUncompressedSize;
public bool ResourceLzh;
// Mac metadata
public uint FileType;
public uint Creator;
public ushort FinderFlags;
public DateTime CreationTime;
public DateTime ModificationTime;
public uint Crc32;
public bool Encrypted;
}
#endregion
}

View File

@@ -0,0 +1,153 @@
using System.Collections.Generic;
using System.IO;
using System.Text;
using Aaru.CommonTypes.Enums;
using Aaru.Compression.CompactPro;
using Aaru.Helpers.IO;
namespace Aaru.Archives;
public sealed partial class CompactPro
{
ErrorNumber ReadResourceFork(Entry entry, out byte[] buffer)
{
buffer = null;
Stream stream = new OffsetStream(new NonClosableStream(_stream),
entry.ResourceOffset,
entry.ResourceOffset + entry.ResourceCompressedSize - 1);
try
{
if(entry.ResourceLzh)
stream = new LzhStream(stream, entry.ResourceUncompressedSize);
else
stream = new RleStream(stream, entry.ResourceUncompressedSize);
buffer = new byte[stream.Length];
stream.Position = 0;
stream.ReadExactly(buffer, 0, buffer.Length);
return ErrorNumber.NoError;
}
catch
{
return ErrorNumber.InOutError;
}
}
static byte[] BuildFinderInfo(Entry entry)
{
// Classic Mac FinderInfo is 16 bytes: type(4) + creator(4) + flags(2) + location(4) + folder(2)
var info = new byte[16];
info[0] = (byte)(entry.FileType >> 24);
info[1] = (byte)(entry.FileType >> 16);
info[2] = (byte)(entry.FileType >> 8);
info[3] = (byte)entry.FileType;
info[4] = (byte)(entry.Creator >> 24);
info[5] = (byte)(entry.Creator >> 16);
info[6] = (byte)(entry.Creator >> 8);
info[7] = (byte)entry.Creator;
info[8] = (byte)(entry.FinderFlags >> 8);
info[9] = (byte)entry.FinderFlags;
return info;
}
static byte[] TypeCodeToBytes(uint code)
{
var bytes = new byte[4];
bytes[0] = (byte)(code >> 24);
bytes[1] = (byte)(code >> 16);
bytes[2] = (byte)(code >> 8);
bytes[3] = (byte)code;
return bytes;
}
#region IArchive Members
/// <inheritdoc />
public ErrorNumber ListXAttr(int entryNumber, out List<string> xattrs)
{
xattrs = null;
if(!Opened) return ErrorNumber.NotOpened;
if(entryNumber < 0 || entryNumber >= _entries.Count) return ErrorNumber.OutOfRange;
xattrs = [];
Entry entry = _entries[entryNumber];
if(_comment is not null) xattrs.Add(XATTR_COMMENT);
if(entry.IsDirectory) return ErrorNumber.NoError;
if(entry.ResourceUncompressedSize > 0) xattrs.Add(XATTR_APPLE_RESOURCE_FORK);
if(entry.FileType != 0 || entry.Creator != 0 || entry.FinderFlags != 0) xattrs.Add(XATTR_APPLE_FINDER_INFO);
if(entry.FileType != 0) xattrs.Add(XATTR_APPLE_HFS_TYPE);
if(entry.Creator != 0) xattrs.Add(XATTR_APPLE_HFS_CREATOR);
return ErrorNumber.NoError;
}
/// <inheritdoc />
public ErrorNumber GetXattr(int entryNumber, string xattr, ref byte[] buffer)
{
buffer = null;
if(!Opened) return ErrorNumber.NotOpened;
if(entryNumber < 0 || entryNumber >= _entries.Count) return ErrorNumber.OutOfRange;
Entry entry = _entries[entryNumber];
switch(xattr)
{
case XATTR_COMMENT:
if(_comment is null) return ErrorNumber.NoSuchExtendedAttribute;
buffer = Encoding.UTF8.GetBytes(_comment);
return ErrorNumber.NoError;
case XATTR_APPLE_RESOURCE_FORK:
if(entry.IsDirectory || entry.ResourceUncompressedSize == 0) return ErrorNumber.NoSuchExtendedAttribute;
if(entry.Encrypted) return ErrorNumber.NotSupported;
return ReadResourceFork(entry, out buffer);
case XATTR_APPLE_FINDER_INFO:
if(entry.IsDirectory) return ErrorNumber.NoSuchExtendedAttribute;
if(entry.FileType == 0 && entry.Creator == 0 && entry.FinderFlags == 0)
return ErrorNumber.NoSuchExtendedAttribute;
buffer = BuildFinderInfo(entry);
return ErrorNumber.NoError;
case XATTR_APPLE_HFS_TYPE:
if(entry.IsDirectory || entry.FileType == 0) return ErrorNumber.NoSuchExtendedAttribute;
buffer = TypeCodeToBytes(entry.FileType);
return ErrorNumber.NoError;
case XATTR_APPLE_HFS_CREATOR:
if(entry.IsDirectory || entry.Creator == 0) return ErrorNumber.NoSuchExtendedAttribute;
buffer = TypeCodeToBytes(entry.Creator);
return ErrorNumber.NoError;
default:
return ErrorNumber.NoSuchExtendedAttribute;
}
}
#endregion
}

View File

@@ -878,5 +878,11 @@ namespace Aaru.Archives {
return ResourceManager.GetString("ZIP_comment_0", resourceCulture);
}
}
internal static string CompactPro_archive {
get {
return ResourceManager.GetString("CompactPro_archive", resourceCulture);
}
}
}
}

View File

@@ -432,4 +432,7 @@
<data name="ZIP_comment_0" xml:space="preserve">
<value>[slateblue1]Comentario del archivo: [green]{0}[/][/]</value>
</data>
<data name="CompactPro_archive" xml:space="preserve">
<value>[bold][blue]Archivo Compact Pro[/][/]</value>
</data>
</root>

View File

@@ -440,4 +440,7 @@
<data name="ZIP_comment_0" xml:space="preserve">
<value>[slateblue1]Archive comment: [green]{0}[/][/]</value>
</data>
<data name="CompactPro_archive" xml:space="preserve">
<value>[bold][blue]Compact Pro archive[/][/]</value>
</data>
</root>

View File

@@ -165,6 +165,7 @@ Supported archives
* ARC (.ARC)
* ARJ (.ARJ)
* ARJZ (.ARZ)
* Compact Pro (.CPT)
* CPIO (.cpio)
* Expert Witness Format (EWF)
* HA (.HA)