mirror of
https://github.com/aaru-dps/Aaru.git
synced 2026-07-08 17:56:18 +00:00
Add StuffIt 5 archive format support with basic functionality
This commit is contained in:
27
Aaru.Archives/StuffIt5/Consts.cs
Normal file
27
Aaru.Archives/StuffIt5/Consts.cs
Normal 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";
|
||||
}
|
||||
21
Aaru.Archives/StuffIt5/Enums.cs
Normal file
21
Aaru.Archives/StuffIt5/Enums.cs
Normal 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
|
||||
}
|
||||
184
Aaru.Archives/StuffIt5/Files.cs
Normal file
184
Aaru.Archives/StuffIt5/Files.cs
Normal 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
|
||||
}
|
||||
59
Aaru.Archives/StuffIt5/Info.cs
Normal file
59
Aaru.Archives/StuffIt5/Info.cs
Normal 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
|
||||
}
|
||||
383
Aaru.Archives/StuffIt5/Open.cs
Normal file
383
Aaru.Archives/StuffIt5/Open.cs
Normal 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
|
||||
}
|
||||
40
Aaru.Archives/StuffIt5/Structs.cs
Normal file
40
Aaru.Archives/StuffIt5/Structs.cs
Normal 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
|
||||
}
|
||||
43
Aaru.Archives/StuffIt5/StuffIt5.cs
Normal file
43
Aaru.Archives/StuffIt5/StuffIt5.cs
Normal 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
|
||||
}
|
||||
159
Aaru.Archives/StuffIt5/Xattrs.cs
Normal file
159
Aaru.Archives/StuffIt5/Xattrs.cs
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user