diff --git a/Aaru.Archives/CompactPro/CompactPro.cs b/Aaru.Archives/CompactPro/CompactPro.cs new file mode 100644 index 000000000..617766c83 --- /dev/null +++ b/Aaru.Archives/CompactPro/CompactPro.cs @@ -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 _entries; + ArchiveSupportedFeature _features; + Stream _stream; + +#region IArchive Members + + /// + public string Name => "compactpro"; + /// + public Guid Id => new("B7E2C3A1-4F8D-4E6A-9C1B-3D5F7A2E8B4C"); + /// + public string Author => Authors.NataliaPortillo; + /// + public bool Opened { get; private set; } + /// + public ArchiveSupportedFeature ArchiveFeatures => !Opened + ? ArchiveSupportedFeature.SupportsCompression | + ArchiveSupportedFeature.SupportsFilenames | + ArchiveSupportedFeature.HasEntryTimestamp | + ArchiveSupportedFeature.SupportsSubdirectories | + ArchiveSupportedFeature.HasExplicitDirectories | + ArchiveSupportedFeature.SupportsXAttrs + : _features; + /// + public int NumberOfEntries => Opened ? _entries.Count : -1; + +#endregion +} \ No newline at end of file diff --git a/Aaru.Archives/CompactPro/Consts.cs b/Aaru.Archives/CompactPro/Consts.cs new file mode 100644 index 000000000..5beeb8b95 --- /dev/null +++ b/Aaru.Archives/CompactPro/Consts.cs @@ -0,0 +1,37 @@ +namespace Aaru.Archives; + +public sealed partial class CompactPro +{ + const int MIN_HEADER_SIZE = 8; + + /// First byte of a Compact Pro archive is always 0x01. + const byte MARKER = 0x01; + + /// Encryption flag (bit 0 of entry flags). + const ushort FLAG_ENCRYPTED = 0x01; + + /// LZH compression for resource fork (bit 1 of entry flags). + const ushort FLAG_RESOURCE_LZH = 0x02; + + /// LZH compression for data fork (bit 2 of entry flags). + const ushort FLAG_DATA_LZH = 0x04; + + /// Bit 7 in name length byte indicates a directory entry. + const byte NAME_DIRECTORY_FLAG = 0x80; + + /// Mask for the actual name length (lower 7 bits). + const byte NAME_LENGTH_MASK = 0x7F; + + /// Size of file entry metadata (excluding name) in bytes. + const int FILE_METADATA_SIZE = 45; + + /// Size of directory entry metadata (excluding name) in bytes. + 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"; +} \ No newline at end of file diff --git a/Aaru.Archives/CompactPro/Files.cs b/Aaru.Archives/CompactPro/Files.cs new file mode 100644 index 000000000..04737d26f --- /dev/null +++ b/Aaru.Archives/CompactPro/Files.cs @@ -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 + + /// + 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; + } + + /// + 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; + } + + /// + 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; + } + + /// + 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; + } + + /// + 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; + } + + /// + 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 +} \ No newline at end of file diff --git a/Aaru.Archives/CompactPro/Info.cs b/Aaru.Archives/CompactPro/Info.cs new file mode 100644 index 000000000..bc0ab9e18 --- /dev/null +++ b/Aaru.Archives/CompactPro/Info.cs @@ -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 + + /// + 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; + } + + /// + 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 +} \ No newline at end of file diff --git a/Aaru.Archives/CompactPro/Open.cs b/Aaru.Archives/CompactPro/Open.cs new file mode 100644 index 000000000..479af8461 --- /dev/null +++ b/Aaru.Archives/CompactPro/Open.cs @@ -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 + + /// + 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; + } + + /// + public void Close() + { + if(!Opened) return; + + _entries = null; + _comment = null; + Opened = false; + } + +#endregion +} \ No newline at end of file diff --git a/Aaru.Archives/CompactPro/Structs.cs b/Aaru.Archives/CompactPro/Structs.cs new file mode 100644 index 000000000..57c735e75 --- /dev/null +++ b/Aaru.Archives/CompactPro/Structs.cs @@ -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 +} \ No newline at end of file diff --git a/Aaru.Archives/CompactPro/Xattrs.cs b/Aaru.Archives/CompactPro/Xattrs.cs new file mode 100644 index 000000000..5de703cea --- /dev/null +++ b/Aaru.Archives/CompactPro/Xattrs.cs @@ -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 + + /// + public ErrorNumber ListXAttr(int entryNumber, out List 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; + } + + /// + 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 +} \ No newline at end of file diff --git a/Aaru.Archives/Localization/Localization.Designer.cs b/Aaru.Archives/Localization/Localization.Designer.cs index ddc44da22..c3eb5fe0e 100644 --- a/Aaru.Archives/Localization/Localization.Designer.cs +++ b/Aaru.Archives/Localization/Localization.Designer.cs @@ -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); + } + } } } diff --git a/Aaru.Archives/Localization/Localization.es.resx b/Aaru.Archives/Localization/Localization.es.resx index 8766033be..4042b1177 100644 --- a/Aaru.Archives/Localization/Localization.es.resx +++ b/Aaru.Archives/Localization/Localization.es.resx @@ -432,4 +432,7 @@ [slateblue1]Comentario del archivo: [green]{0}[/][/] + + [bold][blue]Archivo Compact Pro[/][/] + \ No newline at end of file diff --git a/Aaru.Archives/Localization/Localization.resx b/Aaru.Archives/Localization/Localization.resx index 43138538b..099383269 100644 --- a/Aaru.Archives/Localization/Localization.resx +++ b/Aaru.Archives/Localization/Localization.resx @@ -440,4 +440,7 @@ [slateblue1]Archive comment: [green]{0}[/][/] + + [bold][blue]Compact Pro archive[/][/] + \ No newline at end of file diff --git a/README.md b/README.md index 0e7536b94..7ff98bee1 100644 --- a/README.md +++ b/README.md @@ -165,6 +165,7 @@ Supported archives * ARC (.ARC) * ARJ (.ARJ) * ARJZ (.ARZ) +* Compact Pro (.CPT) * CPIO (.cpio) * Expert Witness Format (EWF) * HA (.HA)