Add StuffIt 5 archive format support with basic functionality

This commit is contained in:
2026-04-17 14:57:16 +01:00
parent 308ad72d55
commit 8c10907344
8 changed files with 916 additions and 0 deletions

View File

@@ -0,0 +1,27 @@
namespace Aaru.Archives;
public sealed partial class StuffIt5
{
const uint ENTRY_ID = 0xA5A5A5A5;
const int MIN_HEADER_SIZE = 100;
const byte ARCHIVE_VERSION = 5;
// Entry flags
const byte FLAGS_DIRECTORY = 0x40;
const byte FLAGS_ENCRYPTED = 0x20;
// Archive-level flags
const byte ARCHIVE_FLAGS_14BYTES = 0x10;
const byte ARCHIVE_FLAGS_20 = 0x20;
const byte ARCHIVE_FLAGS_40 = 0x40;
const byte ARCHIVE_FLAGS_CRYPTED = 0x80;
const int KEY_LENGTH = 5;
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,21 @@
namespace Aaru.Archives;
public sealed partial class StuffIt5
{
#region Nested type: CompressionMethod
enum CompressionMethod : byte
{
None = 0,
Rle = 1,
Compress = 2,
Huffman = 3,
Lzah = 5,
Mw = 8,
LzHuffman = 13,
Installer = 14,
Arsenic = 15
}
#endregion
}

View File

@@ -0,0 +1,184 @@
using System;
using System.IO;
using Aaru.CommonTypes.Enums;
using Aaru.CommonTypes.Interfaces;
using Aaru.CommonTypes.Structs;
using Aaru.Compression.StuffIt;
using Aaru.Filters;
using Aaru.Helpers.IO;
using FileAttributes = Aaru.CommonTypes.Structs.FileAttributes;
namespace Aaru.Archives;
public sealed partial class StuffIt5
{
Stream DecompressStream(Stream compressedStream, CompressionMethod method, long uncompressedSize)
{
switch(method)
{
case CompressionMethod.None:
return compressedStream;
case CompressionMethod.Rle:
return new Rle90Stream(compressedStream, uncompressedSize);
case CompressionMethod.Compress:
return new CompressStream(compressedStream, uncompressedSize);
case CompressionMethod.Huffman:
return new HuffmanStream(compressedStream, uncompressedSize);
case CompressionMethod.Lzah:
return new LzahStream(compressedStream, uncompressedSize);
case CompressionMethod.Mw:
return new MWStream(compressedStream, uncompressedSize);
case CompressionMethod.LzHuffman:
return new Method13Stream(compressedStream, uncompressedSize);
case CompressionMethod.Installer:
return new Method14Stream(compressedStream, uncompressedSize);
case CompressionMethod.Arsenic:
return new ArsenicStream(compressedStream, uncompressedSize);
default:
return null;
}
}
#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 && entry.DataCompressedSize == 0)
stream = new MemoryStream([]);
else
{
stream = new OffsetStream(new NonClosableStream(_stream),
entry.DataOffset,
entry.DataOffset + entry.DataCompressedSize - 1);
stream = DecompressStream(stream, entry.DataMethod, entry.DataUncompressedSize);
if(stream is null) return ErrorNumber.NotSupported;
}
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,59 @@
using System.IO;
using System.Text;
using Aaru.CommonTypes.Interfaces;
namespace Aaru.Archives;
public sealed partial class StuffIt5
{
/// <summary>
/// Header signature pattern. Bytes with value 0xFF are wildcards (year digits that vary).
/// </summary>
static readonly byte[] _headerPattern =
{
(byte)'S', (byte)'t', (byte)'u', (byte)'f', (byte)'f', (byte)'I', (byte)'t', (byte)' ', (byte)'(', (byte)'c',
(byte)')', (byte)'1', (byte)'9', (byte)'9', (byte)'7', (byte)'-', 0xFF, 0xFF, 0xFF, 0xFF, (byte)' ', (byte)'A',
(byte)'l', (byte)'a', (byte)'d', (byte)'d', (byte)'i', (byte)'n', (byte)' ', (byte)'S', (byte)'y', (byte)'s',
(byte)'t', (byte)'e', (byte)'m', (byte)'s', (byte)',', (byte)' ', (byte)'I', (byte)'n', (byte)'c', (byte)'.',
(byte)',', (byte)' ', (byte)'h', (byte)'t', (byte)'t', (byte)'p', (byte)':', (byte)'/', (byte)'/', (byte)'w',
(byte)'w', (byte)'w', (byte)'.', (byte)'a', (byte)'l', (byte)'a', (byte)'d', (byte)'d', (byte)'i', (byte)'n',
(byte)'s', (byte)'y', (byte)'s', (byte)'.', (byte)'c', (byte)'o', (byte)'m', (byte)'/', (byte)'S', (byte)'t',
(byte)'u', (byte)'f', (byte)'f', (byte)'I', (byte)'t', (byte)'/', (byte)'\r', (byte)'\n'
};
#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);
// Match the header pattern (0xFF bytes are wildcards for year)
for(var i = 0; i < _headerPattern.Length; i++)
{
if(_headerPattern[i] == 0xFF) continue;
if(hdr[i] != _headerPattern[i]) return false;
}
return true;
}
/// <inheritdoc />
public void GetInformation(IFilter filter, Encoding encoding, out string information)
{
information = "";
if(!Identify(filter)) return;
information = Localization.StuffIt5_archive;
}
#endregion
}

View File

@@ -0,0 +1,383 @@
using System.Collections.Generic;
using System.Text;
using Aaru.CommonTypes.Enums;
using Aaru.CommonTypes.Interfaces;
using Aaru.Helpers;
namespace Aaru.Archives;
public sealed partial class StuffIt5
{
#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;
long baseOffset = _stream.Position;
// Skip the 82-byte header text
_stream.Position = baseOffset + 82;
int version = _stream.ReadByte();
int flags = _stream.ReadByte();
if(version != ARCHIVE_VERSION) return ErrorNumber.InvalidArgument;
var archiveHdr = new byte[16];
_stream.ReadExactly(archiveHdr, 0, 16);
// uint totalSize = BigEndianBitConverter.ToUInt32(archiveHdr, 0);
// uint something = BigEndianBitConverter.ToUInt32(archiveHdr, 4);
var numFiles = BigEndianBitConverter.ToUInt16(archiveHdr, 8);
var firstOffs = BigEndianBitConverter.ToUInt32(archiveHdr, 10);
// ushort headerCrc = BigEndianBitConverter.ToUInt16(archiveHdr, 14);
if((flags & ARCHIVE_FLAGS_14BYTES) != 0) _stream.Position += 14;
var commentSize = 0;
var lengthB = 0;
if((flags & ARCHIVE_FLAGS_20) != 0)
{
var buf = new byte[4];
_stream.ReadExactly(buf, 0, 4);
commentSize = BigEndianBitConverter.ToUInt16(buf, 0);
lengthB = BigEndianBitConverter.ToUInt16(buf, 2);
}
if((flags & ARCHIVE_FLAGS_CRYPTED) != 0)
{
int hashSize = _stream.ReadByte();
if(hashSize != KEY_LENGTH) return ErrorNumber.InvalidArgument;
// Skip the hash — encryption not supported
_stream.Position += hashSize;
}
if((flags & ARCHIVE_FLAGS_40) != 0)
{
var lenBuf = new byte[2];
_stream.ReadExactly(lenBuf, 0, 2);
int lengthN = BigEndianBitConverter.ToUInt16(lenBuf, 0);
// Each entry: 4 uint32s + 5 int16s + 2 uint8s + 3 uint16s = 30 bytes
for(var i = 0; i < lengthN; i++) _stream.Position += 30;
}
if((flags & ARCHIVE_FLAGS_20) != 0)
{
if(commentSize > 0)
{
var commentBytes = new byte[commentSize];
_stream.ReadExactly(commentBytes, 0, commentSize);
_archiveComment = _encoding.GetString(commentBytes);
}
_stream.Position += lengthB;
}
_stream.Position = firstOffs + baseOffset;
_entries = [];
var dirs = new Dictionary<long, string>();
int numEntries = numFiles;
for(var i = 0; i < numEntries; i++)
{
if(_stream.Position + 34 > _stream.Length) break;
long entryOffset = _stream.Position;
var idBuf = new byte[4];
_stream.ReadExactly(idBuf, 0, 4);
var headId = BigEndianBitConverter.ToUInt32(idBuf, 0);
if(headId != ENTRY_ID) return ErrorNumber.InvalidArgument;
int entryVersion = _stream.ReadByte();
_stream.Position++; // skip unknown byte
var hdrSizeBuf = new byte[2];
_stream.ReadExactly(hdrSizeBuf, 0, 2);
var headerSize = BigEndianBitConverter.ToUInt16(hdrSizeBuf, 0);
long headerEnd = entryOffset + headerSize;
_stream.Position++; // skip system ID byte
int entryFlags = _stream.ReadByte();
var dateBuf = new byte[16];
_stream.ReadExactly(dateBuf, 0, 16);
var creationDate = BigEndianBitConverter.ToUInt32(dateBuf, 0);
var modificationDate = BigEndianBitConverter.ToUInt32(dateBuf, 4);
// uint prevOffs = BigEndianBitConverter.ToUInt32(dateBuf, 8);
// uint nextOffs = BigEndianBitConverter.ToUInt32(dateBuf, 12);
var dirOffsBuf = new byte[4];
_stream.ReadExactly(dirOffsBuf, 0, 4);
var dirOffs = BigEndianBitConverter.ToUInt32(dirOffsBuf, 0);
var nameLenBuf = new byte[2];
_stream.ReadExactly(nameLenBuf, 0, 2);
var nameLength = BigEndianBitConverter.ToUInt16(nameLenBuf, 0);
// Skip header CRC
_stream.Position += 2;
var sizeBuf = new byte[8];
_stream.ReadExactly(sizeBuf, 0, 8);
var dataLength = BigEndianBitConverter.ToUInt32(sizeBuf, 0);
var dataCompLen = BigEndianBitConverter.ToUInt32(sizeBuf, 4);
var crcBuf = new byte[4];
_stream.ReadExactly(crcBuf, 0, 4);
var dataCrc = BigEndianBitConverter.ToUInt16(crcBuf, 0);
// skip 2 unknown bytes
CompressionMethod dataMethod = CompressionMethod.None;
var numDirFiles = 0;
bool encrypted = (entryFlags & FLAGS_ENCRYPTED) != 0;
if((entryFlags & FLAGS_DIRECTORY) != 0)
{
// Directory entry — next 2 bytes are numfiles
var numFilesBuf = new byte[2];
_stream.ReadExactly(numFilesBuf, 0, 2);
numDirFiles = BigEndianBitConverter.ToUInt16(numFilesBuf, 0);
// Sentinel entry check (datalength == 0xffffffff)
if(dataLength == 0xFFFFFFFF)
{
numEntries++;
continue;
}
}
else
{
// File entry
dataMethod = (CompressionMethod)_stream.ReadByte();
int passLen = _stream.ReadByte();
if(encrypted && dataLength > 0)
{
if(passLen != KEY_LENGTH) return ErrorNumber.InvalidArgument;
// Skip entry key — encryption not supported
_stream.Position += passLen;
}
else if(passLen > 0)
{
// Unexpected password data
_stream.Position += passLen;
}
}
// Read filename
var nameData = new byte[nameLength];
_stream.ReadExactly(nameData, 0, nameLength);
string name = _encoding.GetString(nameData);
// Read optional comment
string entryComment = null;
if(_stream.Position < headerEnd)
{
var commentBuf = new byte[4];
_stream.ReadExactly(commentBuf, 0, 4);
int entryCommentSize = BigEndianBitConverter.ToUInt16(commentBuf, 0);
// skip 2 unknown bytes
if(entryCommentSize > 0)
{
var commentData = new byte[entryCommentSize];
_stream.ReadExactly(commentData, 0, entryCommentSize);
entryComment = _encoding.GetString(commentData);
}
}
// Read second block: finder info and Mac metadata
var finderBuf = new byte[4];
_stream.ReadExactly(finderBuf, 0, 4);
var something = BigEndianBitConverter.ToUInt16(finderBuf, 0);
// skip 2 unknown bytes
var typeBuf = new byte[10];
_stream.ReadExactly(typeBuf, 0, 10);
var fileType = BigEndianBitConverter.ToUInt32(typeBuf, 0);
var fileCreator = BigEndianBitConverter.ToUInt32(typeBuf, 4);
var finderFlags = BigEndianBitConverter.ToUInt16(typeBuf, 8);
// Version-dependent skip
if(entryVersion == 1)
_stream.Position += 22;
else
_stream.Position += 18;
// Resource fork info
uint resourceLength = 0;
uint resourceCompLen = 0;
ushort resourceCrc = 0;
CompressionMethod resourceMethod = CompressionMethod.None;
bool hasResource = (something & 0x01) != 0;
if(hasResource)
{
var rsrcBuf = new byte[12];
_stream.ReadExactly(rsrcBuf, 0, 12);
resourceLength = BigEndianBitConverter.ToUInt32(rsrcBuf, 0);
resourceCompLen = BigEndianBitConverter.ToUInt32(rsrcBuf, 4);
resourceCrc = BigEndianBitConverter.ToUInt16(rsrcBuf, 8);
// skip 2 unknown bytes at rsrcBuf[10..11]
resourceMethod = (CompressionMethod)_stream.ReadByte();
int passLen = _stream.ReadByte();
if(encrypted && resourceLength > 0)
{
if(passLen != KEY_LENGTH) return ErrorNumber.InvalidArgument;
// Skip resource entry key
_stream.Position += passLen;
}
else if(passLen > 0) _stream.Position += passLen;
}
long dataStart = _stream.Position;
// Resolve parent directory path
var parentPath = "";
if(dirs.TryGetValue(dirOffs, out string dirPath)) parentPath = dirPath;
string fullPath = parentPath.Length > 0 ? parentPath + "/" + name : name;
if((entryFlags & FLAGS_DIRECTORY) != 0)
{
// Register directory
dirs[entryOffset] = fullPath;
var dirEntry = new Entry
{
Filename = name,
DirectoryPath = parentPath.Length > 0 ? parentPath : null,
IsDirectory = true,
Encrypted = encrypted,
Comment = entryComment,
FinderFlags = finderFlags,
FileType = fileType,
Creator = fileCreator,
CreationTime = DateHandlers.MacToDateTime(creationDate),
ModificationTime = DateHandlers.MacToDateTime(modificationDate)
};
_entries.Add(dirEntry);
// The directory's child entries will be parsed in subsequent iterations
_stream.Position = dataStart;
numEntries += numDirFiles;
}
else
{
// File entry
var entry = new Entry
{
Filename = name,
DirectoryPath = parentPath.Length > 0 ? parentPath : null,
IsDirectory = false,
Encrypted = encrypted,
Comment = entryComment,
DataOffset = dataStart + resourceCompLen,
DataCompressedSize = dataCompLen,
DataUncompressedSize = dataLength,
DataMethod = dataMethod,
DataCrc16 = dataCrc,
ResourceOffset = dataStart,
ResourceCompressedSize = resourceCompLen,
ResourceUncompressedSize = resourceLength,
ResourceMethod = resourceMethod,
ResourceCrc16 = resourceCrc,
FileType = fileType,
Creator = fileCreator,
FinderFlags = finderFlags,
CreationTime = DateHandlers.MacToDateTime(creationDate),
ModificationTime = DateHandlers.MacToDateTime(modificationDate)
};
_entries.Add(entry);
_stream.Position = dataStart + resourceCompLen + dataCompLen;
}
}
_features = ArchiveSupportedFeature.SupportsFilenames | ArchiveSupportedFeature.HasEntryTimestamp;
var hasCompressionFlag = false;
var hasSubdirectories = false;
var hasDirectories = false;
var hasXAttrs = false;
var hasProtection = false;
for(int i = 0, count = _entries.Count; i < count; i++)
{
Entry entry = _entries[i];
if(entry.IsDirectory) hasDirectories = true;
if(entry.DataMethod != CompressionMethod.None || entry.ResourceMethod != CompressionMethod.None)
hasCompressionFlag = true;
if(entry.DirectoryPath is not null) hasSubdirectories = true;
if(entry.Encrypted) hasProtection = true;
if(!entry.IsDirectory && (entry.ResourceUncompressedSize > 0 || entry.FileType != 0 || entry.Creator != 0))
hasXAttrs = true;
if(entry.Comment is not null) hasXAttrs = true;
}
if(_archiveComment is not null) hasXAttrs = true;
if(hasCompressionFlag) _features |= ArchiveSupportedFeature.SupportsCompression;
if(hasSubdirectories) _features |= ArchiveSupportedFeature.SupportsSubdirectories;
if(hasDirectories) _features |= ArchiveSupportedFeature.HasExplicitDirectories;
if(hasXAttrs) _features |= ArchiveSupportedFeature.SupportsXAttrs;
if(hasProtection) _features |= ArchiveSupportedFeature.SupportsProtection;
Opened = true;
return ErrorNumber.NoError;
}
/// <inheritdoc />
public void Close()
{
if(!Opened) return;
_entries = null;
_archiveComment = null;
Opened = false;
}
#endregion
}

View File

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

View File

@@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using Aaru.CommonTypes.Interfaces;
namespace Aaru.Archives;
public sealed partial class StuffIt5 : IArchive
{
const string MODULE_NAME = "StuffIt 5 Archive Plugin";
string _archiveComment;
Encoding _encoding;
List<Entry> _entries;
ArchiveSupportedFeature _features;
Stream _stream;
#region IArchive Members
/// <inheritdoc />
public string Name => "stuffit5";
/// <inheritdoc />
public Guid Id => new("B4C2D1E3-6F7A-8B9C-0D1E-2F3A4B5C6D7E");
/// <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 |
ArchiveSupportedFeature.SupportsProtection
: _features;
/// <inheritdoc />
public int NumberOfEntries => Opened ? _entries.Count : -1;
#endregion
}

View File

@@ -0,0 +1,159 @@
using System.Collections.Generic;
using System.IO;
using System.Text;
using Aaru.CommonTypes.Enums;
using Aaru.Helpers.IO;
namespace Aaru.Archives;
public sealed partial class StuffIt5
{
ErrorNumber ReadResourceFork(Entry entry, out byte[] buffer)
{
buffer = null;
if(entry.ResourceCompressedSize <= 0) return ErrorNumber.NoSuchExtendedAttribute;
Stream stream = new OffsetStream(new NonClosableStream(_stream),
entry.ResourceOffset,
entry.ResourceOffset + entry.ResourceCompressedSize - 1);
try
{
stream = DecompressStream(stream, entry.ResourceMethod, entry.ResourceUncompressedSize);
if(stream is null) return ErrorNumber.NotSupported;
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)
{
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(_archiveComment is not null) xattrs.Add(XATTR_COMMENT);
if(entry.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:
{
// Prefer entry comment, fall back to archive comment
string comment = entry.Comment ?? _archiveComment;
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
}