XADMaster/XADMacArchiveParser.m

554 lines
18 KiB
Objective-C

/*
* XADMacArchiveParser.m
*
* Copyright (c) 2017-present, MacPaw Inc. All rights reserved.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
#import "XADMacArchiveParser.h"
#import "XADArchiveParserDescriptions.h"
#import "XADAppleDouble.h"
#import "CSMemoryHandle.h"
#import "NSDateXAD.h"
#import "CRC.h"
NSString *XADIsMacBinaryKey=@"XADIsMacBinary";
NSString *XADMightBeMacBinaryKey=@"XADMightBeMacBinary";
NSString *XADDisableMacForkExpansionKey=@"XADDisableMacForkExpansionKey";
@implementation XADMacArchiveParser
+(int)macBinaryVersionForHeader:(NSData *)header
{
if([header length]<128) return NO;
const uint8_t *bytes=[header bytes];
// Check zero fill bytes.
if(bytes[0]!=0) return 0;
if(bytes[74]!=0) return 0;
if(bytes[82]!=0) return 0;
for(int i=108;i<=115;i++) if(bytes[i]!=0) return 0;
// Check for a valid name.
if(bytes[1]==0||bytes[1]>63) return 0;
for(int i=0;i<bytes[1];i++) if(bytes[i+2]==0) return 0;
// Check for a valid checksum.
if(XADCalculateCRC(0,bytes,124,XADCRCReverseTable_1021)==
XADUnReverseCRC16(CSUInt16BE(bytes+124)))
{
// Check for a valid signature.
if(CSUInt32BE(bytes+102)=='mBIN') return 3; // MacBinary III
else return 2; // MacBinary II
}
// We aren't sure about the previous one checksum, but we have files that will fail
// unless we calculated this in this way (CRC16-USB)
uint32_t calculatedCRC = XADCalculateCRC(65535,bytes,124,XADCRCTable_a001) ^ 65535;
uint16_t storedCRC = CSUInt16BE(bytes+124);
if (calculatedCRC == storedCRC)
{
// Check for a valid signature.
if(CSUInt32BE(bytes+102)=='mBIN') return 3; // MacBinary III
else return 2; // MacBinary II
}
// Some final heuristics before accepting a version I file.
for(int i=99;i<=125;i++) if(bytes[i]!=0) return 0;
if(CSUInt32BE(bytes+83)>0x7fffffff) return 0; // Data fork size
if(CSUInt32BE(bytes+87)>0x7fffffff) return 0; // Resource fork size
if(CSUInt32BE(bytes+91)==0) return 0; // Creation date
if(CSUInt32BE(bytes+95)==0) return 0; // Last modified date
return 1; // MacBinary I
}
-(id)init
{
if((self=[super init]))
{
previousname=nil;
dittodirectorystack=[NSMutableArray new];
queueddittoentry=nil;
queueddittodata=nil;
cachedentry=nil;
cacheddata=nil;
cachedhandle=nil;
}
return self;
}
-(void)dealloc
{
[previousname release];
[dittodirectorystack release];
[queueddittoentry release];
[queueddittodata release];
[super dealloc];
}
-(void)parse
{
[self parseWithSeparateMacForks];
// If we have a queued ditto fork left over, get rid of it as it isn't a directory.
if(queueddittoentry) [self addQueuedDittoDictionaryAndRetainPosition:NO];
}
-(void)parseWithSeparateMacForks {}
-(void)addEntryWithDictionary:(NSMutableDictionary *)dict retainPosition:(BOOL)retainpos
{
if(retainpos) [XADException raiseNotSupportedException];
// Check if expansion of forks is disabled
NSNumber *disable=[properties objectForKey:XADDisableMacForkExpansionKey];
if(disable&&[disable boolValue])
{
NSNumber *isbin=[dict objectForKey:XADIsMacBinaryKey];
if(isbin&&[isbin boolValue]) [dict setObject:[NSNumber numberWithBool:YES] forKey:XADIsArchiveKey];
[super addEntryWithDictionary:dict retainPosition:retainpos];
return;
}
XADPath *name=[dict objectForKey:XADFileNameKey];
NSNumber *dirnum=[dict objectForKey:XADIsDirectoryKey];
BOOL isdir=dirnum && [dirnum boolValue];
// If we have a queued ditto fork, check if it has the same name as this entry,
// and get rid of it.
if(queueddittoentry)
{
XADPath *queuedname=[queueddittoentry objectForKey:XADFileNameKey];
if([queuedname isCanonicallyEqual:name])
{
[self addQueuedDittoDictionaryWithName:name isDirectory:isdir retainPosition:retainpos];
}
else
{
[self addQueuedDittoDictionaryAndRetainPosition:retainpos];
}
}
// Handle directories
if(isdir)
{
// Discard directories used for ditto forks
NSString *firstcomponent=[name firstPathComponentWithEncodingName:XADUTF8StringEncodingName];
if(firstcomponent && [firstcomponent isEqual:@"__MACOSX"]) return;
// Pop deeper directories off the directory stack, and push this directory
[self popDittoDirectoryStackUntilCanonicalPrefixFor:name];
[self pushDittoDirectory:name];
}
else
{
// Check for MacBinary files.
if([self parseMacBinaryWithDictionary:dict name:name retainPosition:retainpos]) return;
// Check if the file is a ditto fork.
if([self parseAppleDoubleWithDictionary:dict name:name retainPosition:retainpos]) return;
}
// Nothing else worked, it's a normal file. Remember its filename, and output it.
[self setPreviousFilename:[dict objectForKey:XADFileNameKey]];
[super addEntryWithDictionary:dict retainPosition:retainpos];
}
-(BOOL)parseAppleDoubleWithDictionary:(NSMutableDictionary *)dict
name:(XADPath *)name retainPosition:(BOOL)retainpos
{
// Ditto forks are only ever UTF-8.
if(![name canDecodeWithEncodingName:XADUTF8StringEncodingName]) return NO;
// Resource forks are at most 16 megabytes. Ignore larger files, as we will
// be reading the whole file into memory.
NSNumber *filesizenum=[dict objectForKey:XADFileSizeKey];
if(!filesizenum) return NO;
off_t filesize=[filesizenum longLongValue];
if(filesize>16*1024*1024+65536) return NO;
// Check the file name.
NSString *first=[name firstPathComponentWithEncodingName:XADUTF8StringEncodingName];
NSString *last=[name lastPathComponentWithEncodingName:XADUTF8StringEncodingName];
XADPath *basepath=[name pathByDeletingLastPathComponentWithEncodingName:XADUTF8StringEncodingName];
// Ditto forks are always prefixed with "._".
if(![last hasPrefix:@"._"]) return NO;
NSString *newlast=[last substringFromIndex:2];
// Sometimes, they are stored in a root directory named "__MACOSX".
// Get rid of this directory.
if([first isEqual:@"__MACOSX"]) basepath=[basepath pathByDeletingFirstPathComponentWithEncodingName:XADUTF8StringEncodingName];
// Recreate the original name and path.
XADPath *origname=[basepath pathByAppendingXADStringComponent:[self XADStringWithString:newlast]];
// Try to see if we can match this name against a previously encountered name.
// If so, set flags to remember we found a name, and replace the name with that
// of the earlier entry, to make isEqual: work right.
BOOL matchfound=NO,isdir=NO;
// Check if the name is canonically the same as the previous file unpacked.
if(previousname && [origname isCanonicallyEqual:previousname encodingName:XADUTF8StringEncodingName])
{
origname=previousname;
matchfound=YES;
}
// Pop deeper directories off the stack of directory names, and check if the
// name is the same as the top directory on the stack.
[self popDittoDirectoryStackUntilCanonicalPrefixFor:origname];
XADPath *stackname=[self topOfDittoDirectoryStack];
if(stackname && [origname isCanonicallyEqual:stackname encodingName:XADUTF8StringEncodingName])
{
origname=stackname;
isdir=YES;
matchfound=YES;
}
// Parse AppleDouble format.
off_t rsrcoffs,rsrclen;
NSDictionary *extattrs=nil;
NSData *dittodata=nil;
@try
{
CSHandle *fh=[self rawHandleForEntryWithDictionary:dict wantChecksum:YES];
dittodata=[fh remainingFileContents];
CSMemoryHandle *memhandle=[CSMemoryHandle memoryHandleForReadingData:dittodata];
if(![XADAppleDouble parseAppleDoubleWithHandle:memhandle
resourceForkOffset:&rsrcoffs resourceForkLength:&rsrclen
extendedAttributes:&extattrs]) @throw @"Failed to read AppleDouble format";
}
@catch(id e)
{
// Reading or parsing failed, so add this as a regular entry with the
// cached data, if any.
[self addEntryWithDictionary:dict retainPosition:retainpos data:dittodata];
return YES;
}
// Build a new entry dictionary for the fork.
NSMutableDictionary *newdict=[NSMutableDictionary dictionaryWithDictionary:dict];
[newdict setObject:dict forKey:@"MacOriginalDictionary"];
[newdict setObject:[NSNumber numberWithLongLong:rsrcoffs] forKey:@"MacDataOffset"];
[newdict setObject:[NSNumber numberWithLongLong:rsrclen] forKey:@"MacDataLength"];
[newdict setObject:[NSNumber numberWithLongLong:rsrclen] forKey:XADFileSizeKey];
[newdict setObject:[NSNumber numberWithBool:YES] forKey:XADIsResourceForkKey];
// Replace name, remove unused entries.
[newdict setObject:origname forKey:XADFileNameKey];
[newdict removeObjectsForKeys:[NSArray arrayWithObjects:
XADDataLengthKey,XADDataOffsetKey,XADPosixPermissionsKey,
XADPosixUserKey,XADPosixUserNameKey,XADPosixGroupKey,XADPosixGroupNameKey,
nil]];
// TODO: This replaces any existing attributes. None should
// exist, but maybe just in case they should be merged if they do.
if(extattrs) [newdict setObject:extattrs forKey:XADExtendedAttributesKey];
if(isdir) [newdict setObject:[NSNumber numberWithBool:YES] forKey:XADIsDirectoryKey];
if(matchfound)
{
// If we matched this entry with the name of an earlier one, it is done,
// and we can output it.
[self inspectEntryDictionary:newdict]; // This is probably not necessary.
[self addEntryWithDictionary:newdict retainPosition:retainpos data:dittodata];
}
else
{
// If we didn't find the name for this entry from a previous entry, we will
// need to keep it around until we can look at the next entry to see its name
// matches.
[self queueDittoDictionary:newdict data:dittodata];
}
return YES;
}
-(void)setPreviousFilename:(XADPath *)prevname
{
[previousname autorelease];
previousname=[prevname retain];
}
-(XADPath *)topOfDittoDirectoryStack
{
if(![dittodirectorystack count]) return nil;
return [dittodirectorystack lastObject];
}
-(void)pushDittoDirectory:(XADPath *)directory
{
[dittodirectorystack addObject:directory];
}
-(void)popDittoDirectoryStackUntilCanonicalPrefixFor:(XADPath *)path
{
while([dittodirectorystack count])
{
XADPath *dir=[dittodirectorystack lastObject];
if([path hasPrefix:dir]) return;
[dittodirectorystack removeLastObject];
}
}
-(void)queueDittoDictionary:(NSMutableDictionary *)dict data:(NSData *)data
{
[queueddittoentry autorelease];
[queueddittodata autorelease];
queueddittoentry=[dict retain];
queueddittodata=[data retain];
}
-(void)addQueuedDittoDictionaryAndRetainPosition:(BOOL)retainpos
{
[self addQueuedDittoDictionaryWithName:nil isDirectory:NO retainPosition:retainpos];
}
-(void)addQueuedDittoDictionaryWithName:(XADPath *)newname
isDirectory:(BOOL)isdir retainPosition:(BOOL)retainpos
{
if(newname) [queueddittoentry setObject:newname forKey:XADFileNameKey];
if(isdir) [queueddittoentry setObject:[NSNumber numberWithBool:YES] forKey:XADIsDirectoryKey];
[self inspectEntryDictionary:queueddittoentry];
[self addEntryWithDictionary:queueddittoentry retainPosition:retainpos data:queueddittodata];
[queueddittoentry release];
queueddittoentry=nil;
[queueddittodata release];
queueddittodata=nil;
}
-(BOOL)parseMacBinaryWithDictionary:(NSMutableDictionary *)dict
name:(XADPath *)name retainPosition:(BOOL)retainpos
{
NSNumber *isbinobj=[dict objectForKey:XADIsMacBinaryKey];
BOOL isbin=isbinobj?[isbinobj boolValue]:NO;
NSNumber *checkobj=[dict objectForKey:XADMightBeMacBinaryKey];
BOOL check=checkobj?[checkobj boolValue]:NO;
// Return if this file is not known or suspected to be MacBinary.
if(!isbin&&!check) return NO;
// Don't bother checking files inside unseekable streams unless known to be MacBinary.
if(!isbin&&[[self handle] isKindOfClass:[CSStreamHandle class]]) return NO;
CSHandle *fh=[self rawHandleForEntryWithDictionary:dict wantChecksum:YES];
NSData *header=[fh readDataOfLengthAtMost:128];
if([header length]!=128) return NO;
// Check the file if it is not known to be MacBinary.
if(!isbin&&[XADMacArchiveParser macBinaryVersionForHeader:header]==0) return NO;
// TODO: should this be turned on or not? probably not.
//[self setIsMacArchive:YES];
const uint8_t *bytes=[header bytes];
uint32_t datasize=CSUInt32BE(bytes+83);
uint32_t rsrcsize=CSUInt32BE(bytes+87);
int extsize=CSUInt16BE(bytes+120);
XADPath *newpath;
if(name)
{
XADPath *parent=[name pathByDeletingLastPathComponent];
XADString *namepart=[self XADStringWithBytes:bytes+2 length:bytes[1]];
newpath=[parent pathByAppendingXADStringComponent:namepart];
}
else
{
newpath=[self XADPathWithBytes:bytes+2 length:bytes[1] separators:XADNoPathSeparator];
}
NSMutableDictionary *template=[NSMutableDictionary dictionaryWithDictionary:dict];
[template setObject:dict forKey:@"MacOriginalDictionary"];
[template setObject:newpath forKey:XADFileNameKey];
[template setObject:[NSNumber numberWithUnsignedInt:CSUInt32BE(bytes+65)] forKey:XADFileTypeKey];
[template setObject:[NSNumber numberWithUnsignedInt:CSUInt32BE(bytes+69)] forKey:XADFileCreatorKey];
[template setObject:[NSNumber numberWithInt:bytes[101]+(bytes[73]<<8)] forKey:XADFinderFlagsKey];
[template setObject:[NSDate XADDateWithTimeIntervalSince1904:CSUInt32BE(bytes+91)] forKey:XADCreationDateKey];
[template setObject:[NSDate XADDateWithTimeIntervalSince1904:CSUInt32BE(bytes+95)] forKey:XADLastModificationDateKey];
[template removeObjectForKey:XADDataLengthKey];
[template removeObjectForKey:XADDataOffsetKey];
[template removeObjectForKey:XADIsMacBinaryKey];
[template removeObjectForKey:XADMightBeMacBinaryKey];
#define BlockSize(size) (((size)+127)&~127)
if(datasize||!rsrcsize)
{
NSMutableDictionary *newdict=[NSMutableDictionary dictionaryWithDictionary:template];
[newdict setObject:[NSNumber numberWithUnsignedInt:128+BlockSize(extsize)] forKey:@"MacDataOffset"];
[newdict setObject:[NSNumber numberWithUnsignedInt:datasize] forKey:@"MacDataLength"];
[newdict setObject:[NSNumber numberWithUnsignedInt:datasize] forKey:XADFileSizeKey];
[newdict setObject:[NSNumber numberWithUnsignedInt:BlockSize(datasize)] forKey:XADCompressedSizeKey];
[self inspectEntryDictionary:newdict];
[self addEntryWithDictionary:newdict retainPosition:retainpos handle:fh];
}
if(rsrcsize)
{
NSMutableDictionary *newdict=[NSMutableDictionary dictionaryWithDictionary:template];
[newdict setObject:[NSNumber numberWithUnsignedInt:128+BlockSize(extsize)+BlockSize(datasize)] forKey:@"MacDataOffset"];
[newdict setObject:[NSNumber numberWithUnsignedInt:rsrcsize] forKey:@"MacDataLength"];
[newdict setObject:[NSNumber numberWithUnsignedInt:rsrcsize] forKey:XADFileSizeKey];
[newdict setObject:[NSNumber numberWithUnsignedInt:BlockSize(rsrcsize)] forKey:XADCompressedSizeKey];
[newdict setObject:[NSNumber numberWithBool:YES] forKey:XADIsResourceForkKey];
[self inspectEntryDictionary:newdict];
[self addEntryWithDictionary:newdict retainPosition:retainpos handle:fh];
}
return YES;
}
-(void)addEntryWithDictionary:(NSMutableDictionary *)dict
retainPosition:(BOOL)retainpos data:(NSData *)data
{
cachedentry=dict;
cacheddata=data;
cachedhandle=nil;
[super addEntryWithDictionary:dict retainPosition:retainpos];
cachedentry=nil;
cacheddata=nil;
}
-(void)addEntryWithDictionary:(NSMutableDictionary *)dict
retainPosition:(BOOL)retainpos handle:(CSHandle *)handle
{
cachedentry=dict;
cacheddata=nil;
cachedhandle=handle;
[super addEntryWithDictionary:dict retainPosition:retainpos];
cachedentry=nil;
cachedhandle=nil;
}
-(CSHandle *)handleForEntryWithDictionary:(NSDictionary *)dict wantChecksum:(BOOL)checksum
{
NSDictionary *origdict=[dict objectForKey:@"MacOriginalDictionary"];
if(origdict)
{
off_t offset=[[dict objectForKey:@"MacDataOffset"] longLongValue];
off_t length=[[dict objectForKey:@"MacDataLength"] longLongValue];
if(!length) return [self zeroLengthHandleWithChecksum:checksum];
CSHandle *handle=nil;
if(cachedentry==dict)
{
if(cachedhandle) handle=cachedhandle;
else if(cacheddata) handle=[CSMemoryHandle memoryHandleForReadingData:cacheddata];
}
if(!handle) handle=[self rawHandleForEntryWithDictionary:origdict wantChecksum:checksum];
return [handle nonCopiedSubHandleFrom:offset length:length];
}
else
{
return [self rawHandleForEntryWithDictionary:dict wantChecksum:checksum];
}
}
-(NSString *)descriptionOfValueInDictionary:(NSDictionary *)dict key:(NSString *)key
{
id object=[dict objectForKey:key];
if(!object) return nil;
if([key isEqual:@"MacOriginalDictionary"])
{
if(![object isKindOfClass:[NSDictionary class]]) return [object description];
return XADHumanReadableEntryWithDictionary(object,self);
}
else if([key isEqual:XADMightBeMacBinaryKey])
{
if(![object isKindOfClass:[NSNumber class]]) return [object description];
return XADHumanReadableBoolean([object longLongValue]);
}
else
{
return [super descriptionOfValueInDictionary:dict key:key];
}
}
-(NSString *)descriptionOfKey:(NSString *)key
{
static NSDictionary *descriptions=nil;
if(!descriptions) descriptions=[[NSDictionary alloc] initWithObjectsAndKeys:
NSLocalizedString(@"Is an embedded MacBinary file",@""),XADIsMacBinaryKey,
NSLocalizedString(@"Check for MacBinary",@""),XADMightBeMacBinaryKey,
NSLocalizedString(@"Mac OS fork handling is disabled",@""),XADDisableMacForkExpansionKey,
NSLocalizedString(@"Original archive entry",@""),@"MacOriginalDictionary",
NSLocalizedString(@"Start of embedded data",@""),@"MacDataOffset",
NSLocalizedString(@"Length of embedded data",@""),@"MacDataLength",
nil];
NSString *description=[descriptions objectForKey:key];
if(description) return description;
return [super descriptionOfKey:key];
}
-(CSHandle *)rawHandleForEntryWithDictionary:(NSDictionary *)dict wantChecksum:(BOOL)checksum
{
return nil;
}
-(void)inspectEntryDictionary:(NSMutableDictionary *)dict
{
}
@end