XADMaster/XADRARParser.m

895 lines
25 KiB
Objective-C

/*
* XADRARParser.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 "XADRARParser.h"
#import "XADRARInputHandle.h"
#import "XADRAR15Handle.h"
#import "XADRAR20Handle.h"
#import "XADRAR30Handle.h"
#import "XADRAR13CryptHandle.h"
#import "XADRAR15CryptHandle.h"
#import "XADRAR20CryptHandle.h"
#import "XADRARAESHandle.h"
#import "XADCRCHandle.h"
#import "CSFileHandle.h"
#import "CSMemoryHandle.h"
#import "XADException.h"
#import "NSDateXAD.h"
#import "Scanning.h"
#define RARFLAG_SKIP_IF_UNKNOWN 0x4000
#define RARFLAG_LONG_BLOCK 0x8000
#define MHD_VOLUME 0x0001
#define MHD_COMMENT 0x0002
#define MHD_LOCK 0x0004
#define MHD_SOLID 0x0008
#define MHD_PACK_COMMENT 0x0010
#define MHD_NEWNUMBERING 0x0010
#define MHD_AV 0x0020
#define MHD_PROTECT 0x0040
#define MHD_PASSWORD 0x0080
#define MHD_FIRSTVOLUME 0x0100
#define MHD_ENCRYPTVER 0x0200
#define LHD_SPLIT_BEFORE 0x0001
#define LHD_SPLIT_AFTER 0x0002
#define LHD_PASSWORD 0x0004
#define LHD_COMMENT 0x0008
#define LHD_SOLID 0x0010
#define LHD_WINDOWMASK 0x00e0
#define LHD_WINDOW64 0x0000
#define LHD_WINDOW128 0x0020
#define LHD_WINDOW256 0x0040
#define LHD_WINDOW512 0x0060
#define LHD_WINDOW1024 0x0080
#define LHD_WINDOW2048 0x00a0
#define LHD_WINDOW4096 0x00c0
#define LHD_DIRECTORY 0x00e0
#define LHD_LARGE 0x0100
#define LHD_UNICODE 0x0200
#define LHD_SALT 0x0400
#define LHD_VERSION 0x0800
#define LHD_EXTTIME 0x1000
#define LHD_EXTFLAGS 0x2000
#define RARMETHOD_STORE 0x30
#define RARMETHOD_FASTEST 0x31
#define RARMETHOD_FAST 0x32
#define RARMETHOD_NORMAL 0x33
#define RARMETHOD_GOOD 0x34
#define RARMETHOD_BEST 0x35
static RARBlock ZeroBlock={0};
static inline BOOL IsZeroBlock(RARBlock block) { return block.start==0; }
static BOOL IsRARSignature(const uint8_t *ptr)
{
return ptr[0]=='R' && ptr[1]=='a' && ptr[2]=='r' && ptr[3]=='!' &&
ptr[4]==0x1a && ptr[5]==0x07 && ptr[6]==0x00;
}
static BOOL IsAncientRARSignature(const uint8_t *ptr)
{
return ptr[0]==0x52 && ptr[1]==0x45 && ptr[2]==0x7e && ptr[3]==0x5e;
}
static const uint8_t *FindSignature(const uint8_t *ptr,int length)
{
if(length<7) return NULL;
for(int i=0;i<=length-7;i++) if(IsRARSignature(&ptr[i])) return &ptr[i];
return NULL;
}
@implementation XADRARParser
+(int)requiredHeaderSize
{
return 7;
}
+(BOOL)recognizeFileWithHandle:(CSHandle *)handle firstBytes:(NSData *)data name:(NSString *)name
{
const uint8_t *bytes=[data bytes];
int length=[data length];
if(length<7) return NO; // TODO: fix to use correct min size
if(IsRARSignature(bytes)) return YES;
if(IsAncientRARSignature(bytes)) return YES;
return NO;
}
+(NSArray *)volumesForHandle:(CSHandle *)handle firstBytes:(NSData *)data name:(NSString *)name
{
if([data length]<12) return nil;
const uint8_t *header=[data bytes];
uint16_t flags=CSUInt16LE(&header[10]);
// Don't bother looking for volumes if it the volume bit is not set.
if(!(flags&1)) return nil;
// Check the old/new naming bit.
if(flags&0x10)
{
// New naming scheme. Find the last number in the name, and look for other files
// with the same number of digits in the same location.
NSArray *matches;
if((matches=[name substringsCapturedByPattern:@"^(.*[^0-9])([0-9]+)(.*)\\.rar$" options:REG_ICASE]))
return [self scanForVolumesWithFilename:name
regex:[XADRegex regexWithPattern:[NSString stringWithFormat:@"^%@[0-9]{%ld}%@.rar$",
[[matches objectAtIndex:1] escapedPattern],
(long)[(NSString *)[matches objectAtIndex:2] length],
[[matches objectAtIndex:3] escapedPattern]] options:REG_ICASE]
];
}
// Old naming scheme. Just look for rar/r01/s01/... files.
NSArray *matches;
if((matches=[name substringsCapturedByPattern:@"^(.*)\\.(rar|[r-z][0-9]{2})$" options:REG_ICASE]))
{
return [self scanForVolumesWithFilename:name
regex:[XADRegex regexWithPattern:[NSString stringWithFormat:@"^%@\\.(rar|[r-z][0-9]{2})$",
[[matches objectAtIndex:1] escapedPattern]] options:REG_ICASE]
firstFileExtension:@"rar"];
}
return nil;
}
-(id)init
{
if((self=[super init]))
{
keys=nil;
}
return self;
}
-(void)dealloc
{
[keys release];
[super dealloc];
}
-(void)setPassword:(NSString *)newpassword
{
// Make sure to clear key cache if password changes.
[keys release];
keys=nil;
[super setPassword:newpassword];
}
-(void)parse
{
// Parsing the RAR format and keeping track of missing volumes here is quite a mess.
// The RAR5 parser is somewhat cleaner, and this code should probably be rewritten
// to match its structure.
CSHandle *handle=[self handle];
uint8_t buf[7];
[handle readBytes:7 toBuffer:buf];
if(IsAncientRARSignature(buf))
{
[self reportInterestingFileWithReason:@"Very old RAR file"];
[XADException raiseNotSupportedException];
// [fh skipBytes:-3];
// TODO: handle old RARs.
}
archiveflags=0;
NSMutableArray *currfiles=nil;
NSMutableArray *currparts=nil;
RARBlock previousblock;
RARFileHeader previousheader;
BOOL firstfileheader=YES;
off_t totalfilesize=0;
off_t totalsolidsize=0;
RARBlock block;
while([self shouldKeepParsing])
{
block=[self readBlockHeader];
if(IsZeroBlock(block))
{
// We hit the end of the file. If we have parts that have no been
// emitted yet, do so now, and mark as corrupted as we are missing the
// last part.
if(currparts)
{
// Add current file to solid file list, creating it if necessary.
if(!currfiles) currfiles=[NSMutableArray array];
[currfiles addObject:[NSDictionary dictionaryWithObjectsAndKeys:
currparts,@"Parts",
[NSNumber numberWithLongLong:previousheader.size],@"OutputLength",
[NSNumber numberWithInt:previousheader.version],@"Version",
[NSNumber numberWithBool:(block.flags&LHD_PASSWORD)?YES:NO],@"Encrypted",
previousheader.salt,@"Salt", // Ends the list if nil.
nil]];
[self addEntryWithBlock:&previousblock header:&previousheader
compressedSize:totalfilesize files:currfiles solidOffset:totalsolidsize
isCorrupted:YES];
}
break;
}
CSHandle *fh=block.fh;
switch(block.type)
{
case 0x72: // File marker header (magic number).
[self skipBlock:block];
break;
case 0x73: // Archive header.
archiveflags=block.flags;
[fh skipBytes:6]; // Skip signature stuff.
if(block.flags&MHD_ENCRYPTVER)
{
encryptversion=[fh readUInt8];
}
else encryptversion=0; // ?
if(block.flags&MHD_COMMENT) // 2.0-style comment.
{
NSData *comment=[self readComment];
[self setObject:[self XADStringWithData:comment] forPropertyKey:XADCommentKey];
}
[self skipBlock:block];
break;
case 0x74: // File header.
{
RARFileHeader header=[self readFileHeaderWithBlock:&block];
BOOL first=(block.flags&LHD_SPLIT_BEFORE)?NO:YES;
BOOL last=(block.flags&LHD_SPLIT_AFTER)?NO:YES;
BOOL mismatch=NO;
if(currparts)
{
// We are currently collecting more parts for a file. If the new
// part is marked as the first part, or if the name doesn't match
// the last part, something is wrong.
if(first || ![header.namedata isEqual:previousheader.namedata])
{
// Emit as much as we have of the previous file.
[self addEntryWithBlock:&previousblock header:&previousheader
compressedSize:totalfilesize files:currfiles solidOffset:totalsolidsize
isCorrupted:YES];
// Start new part and solid file lists.
currparts=nil;
currfiles=nil;
mismatch=YES;
}
}
else
{
// We are starting a new file.
// Make sure we are getting the start part of the file.
if(!first)
{
// If this is not the start of a new file, something is wrong.
// Start new solid file list.
currfiles=nil;
mismatch=YES;
}
}
// Add current part to part list, creating it if necessary.
if(!currparts)
{
currparts=[NSMutableArray array];
totalfilesize=0;
}
[currparts addObject:[NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithLongLong:block.datastart],@"Offset",
[NSNumber numberWithLongLong:block.datasize],@"InputLength",
[NSNumber numberWithUnsignedInt:header.crc],@"CRC32",
nil]];
totalfilesize+=block.datasize;
if(last)
{
// Figure out if this file is solid.
BOOL solid;
if(header.version<20) solid=(archiveflags&MHD_SOLID)&&!firstfileheader; // TODO: Should this be < or <=?
else solid=(block.flags&LHD_SOLID)!=0;
// If it is not solid, restart the solid file list.
if(!solid) currfiles=nil;
// Add current file to solid file list, creating it if necessary.
if(!currfiles)
{
currfiles=[NSMutableArray array];
totalsolidsize=0;
}
[currfiles addObject:[NSDictionary dictionaryWithObjectsAndKeys:
currparts,@"Parts",
[NSNumber numberWithLongLong:header.size],@"OutputLength",
[NSNumber numberWithInt:header.version],@"Version",
[NSNumber numberWithBool:(block.flags&LHD_PASSWORD)?YES:NO],@"Encrypted",
header.salt,@"Salt", // Ends the list if nil.
nil]];
// Emit this file.
[self addEntryWithBlock:&block header:&header
compressedSize:totalfilesize files:currfiles solidOffset:totalsolidsize
isCorrupted:mismatch];
totalsolidsize+=header.size;
// If this file was corrupted, restart the solid file list.
if(mismatch) currfiles=nil;
// Restart part list.
currparts=nil;
}
else
{
previousblock=block;
previousheader=header;
}
firstfileheader=NO;
[self skipBlock:block];
}
break;
case 0x7a: // Newsub header.
[self skipBlock:block];
break;
case 0x7b: // End header
{
archiveflags=0;
[self skipBlock:block];
CSHandle *handle=[self currentHandle];
if([handle offsetInFile]!=0) [handle seekToEndOfFile];
}
break;
default:
[self skipBlock:block];
break;
}
}
}
-(RARFileHeader)readFileHeaderWithBlock:(RARBlock *)block
{
CSHandle *fh=block->fh;
RARFileHeader header;
header.size=[fh readUInt32LE];
header.os=[fh readUInt8];
header.crc=[fh readUInt32LE];
header.dostime=[fh readUInt32LE];
header.version=[fh readUInt8];
header.method=[fh readUInt8];
header.namelength=[fh readUInt16LE];
header.attrs=[fh readUInt32LE];
if(block->flags&LHD_LARGE)
{
block->datasize+=(off_t)[fh readUInt32LE]<<32;
header.size+=(off_t)[fh readUInt32LE]<<32;
}
header.namedata=[fh readDataOfLength:header.namelength];
if(block->flags&LHD_SALT) header.salt=[fh readDataOfLength:8];
else header.salt=nil;
return header;
}
-(NSData *)readComment
{
// Read 2.0-style comment block.
RARBlock block=[self readBlockHeader];
CSHandle *fh=block.fh;
int commentsize=[fh readUInt16LE];
int version=[fh readUInt8];
/*int method=*/[fh readUInt8];
/*int crc=*/[fh readUInt16LE];
// TODO: should this be [self handle] or block.fh?
NSArray *parts=[NSArray arrayWithObject:[NSDictionary dictionaryWithObjectsAndKeys:
[NSArray arrayWithObject:[NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithLongLong:[[self handle] offsetInFile]],@"Offset",
[NSNumber numberWithLongLong:block.headersize-13],@"InputLength",
//[NSNumber numberWithUnsignedInt:header.crc],@"CRC32",
nil]],@"Parts",
[NSNumber numberWithLongLong:commentsize],@"OutputLength",
[NSNumber numberWithInt:version],@"Version",
[NSNumber numberWithBool:NO],@"Encrypted",
nil]];
CSHandle *handle=[self handleForSolidStreamWithObject:parts wantChecksum:NO];
return [handle readDataOfLength:commentsize];
}
-(RARBlock)readBlockHeader
{
CSHandle *fh=[self handle];
if([fh atEndOfFile]) return ZeroBlock;
RARBlock block;
block.start=[[self handle] offsetInFile];
if(archiveflags&MHD_PASSWORD)
{
NSData *salt=[fh readDataOfLength:8];
fh=[[[XADRARAESHandle alloc] initWithHandle:fh key:[self keyForSalt:salt]] autorelease];
}
block.fh=fh;
@try
{
block.crc=[fh readUInt16LE];
block.type=[fh readUInt8];
block.flags=[fh readUInt16LE];
block.headersize=[fh readUInt16LE];
}
@catch(id e) { return ZeroBlock; }
if(block.headersize<7) [XADException raiseIllegalDataException];
// Removed CRC checking because RAR uses it completely inconsitently
/* if(block.crc!=0x6152||block.type!=0x72||block.flags!=0x1a21||block.headersize!=7)
{
off_t pos=[fh offsetInFile];
uint32_t crc=0xffffffff;
@try
{
crc=XADCRC(crc,block.type,XADCRCTable_edb88320);
crc=XADCRC(crc,(block.flags&0xff),XADCRCTable_edb88320);
crc=XADCRC(crc,((block.flags>>8)&0xff),XADCRCTable_edb88320);
crc=XADCRC(crc,(block.headersize&0xff),XADCRCTable_edb88320);
crc=XADCRC(crc,((block.headersize>>8)&0xff),XADCRCTable_edb88320);
for(int i=7;i<block.headersize;i++)
{
NSLog(@"%04x %04x %s",~crc&0xffff,block.crc,(~crc&0xffff)==block.crc?"<-------":"");
crc=XADCRC(crc,[fh readUInt8],XADCRCTable_edb88320);
}
}
@catch(id e) {}
if((~crc&0xffff)!=block.crc)
{
if(archiveflags&MHD_PASSWORD) [XADException raisePasswordException];
else [XADException raiseIllegalDataException];
}
[fh seekToFileOffset:pos];
}*/
// RAR ignores the LONG_BLOCK flag for most chunks. FILE_HEAD, NEWSUB_HEAD,
// PROTECT_HEAD, and SUB_HEAD are always treated as long, while most others
// are always treated as short. The flag is only used for unknown blocks.
// To work around broken archives, we add an exception for FILE_HEAD, at least.
if((block.flags&RARFLAG_LONG_BLOCK)||block.type==0x74) block.datasize=[fh readUInt32LE];
else block.datasize=0;
if(archiveflags&MHD_PASSWORD) block.datastart=block.start+((block.headersize+15)&~15)+8;
else block.datastart=block.start+block.headersize;
//NSLog(@"block:%x flags:%x headsize:%d datasize:%qu ",block.type,block.flags,block.headersize,block.datasize);
return block;
}
-(void)skipBlock:(RARBlock)block
{
[[self handle] seekToFileOffset:block.datastart+block.datasize];
}
-(void)addEntryWithBlock:(const RARBlock *)block header:(const RARFileHeader *)header
compressedSize:(off_t)compsize files:(NSArray *)files solidOffset:(off_t)solidoffs
isCorrupted:(BOOL)iscorrupted
{
NSMutableDictionary *dict=[NSMutableDictionary dictionaryWithObjectsAndKeys:
[self parseNameData:header->namedata flags:block->flags],XADFileNameKey,
[NSNumber numberWithLongLong:solidoffs],XADSolidOffsetKey,
[NSNumber numberWithLongLong:header->size],XADSolidLengthKey,
[NSNumber numberWithLongLong:header->size],XADFileSizeKey, // TODO: this right?
[NSNumber numberWithLongLong:compsize],XADCompressedSizeKey,
[NSDate XADDateWithMSDOSDateTime:header->dostime],XADLastModificationDateKey,
files,XADSolidObjectKey,
[NSNumber numberWithInt:block->flags],@"RARFlags",
[NSNumber numberWithInt:header->version],@"RARCompressionVersion",
[NSNumber numberWithInt:header->method],@"RARCompressionMethod",
[NSNumber numberWithUnsignedInt:header->crc],@"RARCRC32",
[NSNumber numberWithInt:header->os],@"RAROS",
[NSNumber numberWithUnsignedInt:header->attrs],@"RARAttributes",
[NSNumber numberWithInt:[files count]-1],@"RARSolidIndex",
nil];
if(iscorrupted) [dict setObject:[NSNumber numberWithBool:YES] forKey:XADIsCorruptedKey];
if(block->flags&LHD_PASSWORD) [dict setObject:[NSNumber numberWithBool:YES] forKey:XADIsEncryptedKey];
if((block->flags&LHD_WINDOWMASK)==LHD_DIRECTORY) [dict setObject:[NSNumber numberWithBool:YES] forKey:XADIsDirectoryKey];
if(header->version==15 && header->os==0 && (header->attrs&0x10)) [dict setObject:[NSNumber numberWithBool:YES] forKey:XADIsDirectoryKey];
NSString *osname=nil;
switch(header->os)
{
case 0: osname=@"MS-DOS"; break;
case 1: osname=@"OS/2"; break;
case 2: osname=@"Win32"; break;
case 3: osname=@"Unix"; break;
}
if(osname) [dict setObject:[self XADStringWithString:osname] forKey:@"RAROSName"];
switch(header->os)
{
case 0: [dict setObject:[NSNumber numberWithUnsignedInt:header->attrs] forKey:XADDOSFileAttributesKey]; break;
case 2: [dict setObject:[NSNumber numberWithUnsignedInt:header->attrs] forKey:XADWindowsFileAttributesKey]; break;
case 3: [dict setObject:[NSNumber numberWithUnsignedInt:header->attrs] forKey:XADPosixPermissionsKey]; break;
}
NSString *methodname=nil;
switch(header->method)
{
case 0x30: methodname=@"None"; break;
case 0x31: methodname=[NSString stringWithFormat:@"Fastest v%d.%d",header->version/10,header->version%10]; break;
case 0x32: methodname=[NSString stringWithFormat:@"Fast v%d.%d",header->version/10,header->version%10]; break;
case 0x33: methodname=[NSString stringWithFormat:@"Normal v%d.%d",header->version/10,header->version%10]; break;
case 0x34: methodname=[NSString stringWithFormat:@"Good v%d.%d",header->version/10,header->version%10]; break;
case 0x35: methodname=[NSString stringWithFormat:@"Best v%d.%d",header->version/10,header->version%10]; break;
}
if(methodname) [dict setObject:[self XADStringWithString:methodname] forKey:XADCompressionNameKey];
[self addEntryWithDictionary:dict];
}
-(XADPath *)parseNameData:(NSData *)data flags:(int)flags
{
if(flags&LHD_UNICODE)
{
int length=[data length];
const uint8_t *bytes=[data bytes];
int n=0;
while(n<length&&bytes[n]) n++;
if(n==length) return [self XADPathWithData:data encodingName:XADUTF8StringEncodingName separators:XADWindowsPathSeparator];
int num=length-n-1;
if(num<=1) return [self XADPathWithCString:(const char *)bytes separators:XADWindowsPathSeparator];
CSMemoryHandle *fh=[CSMemoryHandle memoryHandleForReadingBuffer:bytes+n+1 length:num];
NSMutableString *str=[NSMutableString string];
@try
{
int highbyte=[fh readUInt8]<<8;
int flagbyte,flagbits=0;
while(![fh atEndOfFile])
{
if(flagbits==0)
{
flagbyte=[fh readUInt8];
flagbits=8;
}
flagbits-=2;
switch((flagbyte>>flagbits)&3)
{
case 0: [str appendFormat:@"%C",(unichar)[fh readUInt8]]; break;
case 1: [str appendFormat:@"%C",(unichar)(highbyte+[fh readUInt8])]; break;
case 2: [str appendFormat:@"%C",[fh readUInt16LE]]; break;
case 3:
{
int len=[fh readUInt8];
if(len&0x80)
{
int correction=[fh readUInt8];
for(int i=0;i<(len&0x7f)+2;i++) {
if ([str length] < length) {
[str appendFormat:@"%C",(unichar)(highbyte+(bytes[[str length]]+correction&0xff))];
}
}
}
else {
for(int i=0;i<(len&0x7f)+2;i++) {
if ([str length] < length) {
[str appendFormat:@"%C",(unichar)(bytes[[str length]])];
}
}
}
}
break;
}
}
}
@catch(id e) {}
// TODO: avoid re-encoding
return [self XADPathWithData:[str dataUsingEncoding:NSUTF8StringEncoding]
encodingName:XADUTF8StringEncodingName separators:XADWindowsPathSeparator];
}
else return [self XADPathWithData:data separators:XADWindowsPathSeparator];
}
-(CSHandle *)handleForEntryWithDictionary:(NSDictionary *)dict wantChecksum:(BOOL)checksum
{
// Give the caller some ahead notice if we will be using a password.
NSNumber *encryptnum=[dict objectForKey:XADIsEncryptedKey];
if(encryptnum && [encryptnum boolValue])
{
if([[dict objectForKey:@"RARCompressionVersion"] intValue]<=20) caresaboutpasswordencoding=YES;
[self password];
if(![self hasPassword]) return nil;
}
CSHandle *handle;
if([[dict objectForKey:@"RARCompressionMethod"] intValue]==0x30)
{
NSArray *files=[dict objectForKey:XADSolidObjectKey];
int index=[[dict objectForKey:@"RARSolidIndex"] intValue];
handle=[self inputHandleForFileWithIndex:index files:files];
off_t length=[[dict objectForKey:XADSolidLengthKey] longLongValue];
if(length!=[handle fileSize]) handle=[handle nonCopiedSubHandleOfLength:length];
}
else
{
off_t length=[[dict objectForKey:XADSolidLengthKey] longLongValue];
// Avoid 0-length files because they make trouble in solid streams.
if(length==0) handle=[self zeroLengthHandleWithChecksum:YES];
else handle=[self subHandleFromSolidStreamForEntryWithDictionary:dict];
}
if(checksum) handle=[XADCRCHandle IEEECRC32HandleWithHandle:handle length:[handle fileSize]
correctCRC:[[dict objectForKey:@"RARCRC32"] unsignedIntValue] conditioned:YES];
return handle;
}
-(CSHandle *)handleForSolidStreamWithObject:(id)obj wantChecksum:(BOOL)checksum
{
int version=[[[obj objectAtIndex:0] objectForKey:@"Version"] intValue];
switch(version)
{
case 15:
return [[[XADRAR15Handle alloc] initWithRARParser:self files:obj] autorelease];
case 20:
case 26:
return [[[XADRAR20Handle alloc] initWithRARParser:self files:obj] autorelease];
case 29:
case 36:
return [[[XADRAR30Handle alloc] initWithRARParser:self files:obj] autorelease];
default:
return nil;
}
}
-(CSInputBuffer *)inputBufferForFileWithIndex:(int)file files:(NSArray *)files
{
return CSInputBufferAlloc([self inputHandleForFileWithIndex:file files:files],16384);
}
-(CSHandle *)inputHandleForFileWithIndex:(int)file files:(NSArray *)files
{
if(file>=[files count]) [XADException raiseExceptionWithXADError:XADInputError]; // TODO: better error
NSDictionary *dict=[files objectAtIndex:file];
CSHandle *handle=[self inputHandleWithParts:[dict objectForKey:@"Parts"]
encrypted:[[dict objectForKey:@"Encrypted"] longLongValue]
cryptoVersion:[[dict objectForKey:@"Version"] intValue]
salt:[dict objectForKey:@"Salt"]];
return handle;
}
-(CSHandle *)inputHandleWithParts:(NSArray *)parts encrypted:(BOOL)encrypted
cryptoVersion:(int)version salt:(NSData *)salt
{
CSHandle *handle=[[[XADRARInputHandle alloc] initWithHandle:[self handle] parts:parts] autorelease];
if(encrypted)
{
switch(version)
{
case 13: return [[[XADRAR13CryptHandle alloc] initWithHandle:handle
length:[handle fileSize] password:[self encodedPassword]] autorelease];
case 15: return [[[XADRAR15CryptHandle alloc] initWithHandle:handle
length:[handle fileSize] password:[self encodedPassword]] autorelease];
case 20: return [[[XADRAR20CryptHandle alloc] initWithHandle:handle
length:[handle fileSize] password:[self encodedPassword]] autorelease];
default: return [[[XADRARAESHandle alloc] initWithHandle:handle
length:[handle fileSize] key:[self keyForSalt:salt]] autorelease];
}
}
else return handle;
}
-(NSData *)keyForSalt:(NSData *)salt
{
if(!keys) keys=[NSMutableDictionary new];
NSData *key=[keys objectForKey:salt];
if(key) return key;
key=[XADRARAESHandle keyForPassword:[self password] salt:salt brokenHash:encryptversion<36];
[keys setObject:key forKey:salt];
return key;
}
-(off_t)outputLengthOfFileWithIndex:(int)file files:(NSArray *)files
{
if(file>=[files count]) [XADException raiseExceptionWithXADError:XADInputError]; // TODO: better error
NSDictionary *dict=[files objectAtIndex:file];
return [[dict objectForKey:@"OutputLength"] longLongValue];
}
-(NSString *)formatName
{
return @"RAR";
}
@end
@implementation XADEmbeddedRARParser
+(int)requiredHeaderSize
{
return 0x80000;
}
+(BOOL)recognizeFileWithHandle:(CSHandle *)handle firstBytes:(NSData *)data
name:(NSString *)name propertiesToAdd:(NSMutableDictionary *)props
{
const uint8_t *bytes=[data bytes];
int length=[data length];
const uint8_t *header=FindSignature(bytes,length);
if(header)
{
[props setObject:[NSNumber numberWithLongLong:header-bytes] forKey:@"RAREmbedOffset"];
[props setObject:[NSNumber numberWithLongLong:header-bytes] forKey:XADSignatureOffset];
return YES;
}
return NO;
}
+(NSArray *)volumesForHandle:(CSHandle *)handle firstBytes:(NSData *)data name:(NSString *)name
{
const uint8_t *bytes=[data bytes];
int length=[data length];
const uint8_t *header=FindSignature(bytes,length);
if(!header) return nil; // Shouldn't happen
uint16_t flags=CSUInt16LE(&header[10]);
// Don't bother looking for volumes if it the volume bit is not set.
if(!(flags&0x01)) return nil;
// Don't bother looking for volumes if it the new naming bit is not set.
if(!(flags&0x10)) return nil;
// New naming scheme. Find the last number in the name, and look for other files
// with the same number of digits in the same location.
NSArray *matches;
if((matches=[name substringsCapturedByPattern:@"^(.*[^0-9])([0-9]+)(.*)\\.(exe|sfx)$" options:REG_ICASE]))
return [self scanForVolumesWithFilename:name
regex:[XADRegex regexWithPattern:[NSString stringWithFormat:@"^%@[0-9]{%ld}%@.(rar|exe|sfx)$",
[[matches objectAtIndex:1] escapedPattern],
(long)[(NSString *)[matches objectAtIndex:2] length],
[[matches objectAtIndex:3] escapedPattern]] options:REG_ICASE]
];
return nil;
}
-(void)parse
{
off_t offs=[[[self properties] objectForKey:@"RAREmbedOffset"] longLongValue];
[[self handle] seekToFileOffset:offs];
[super parse];
}
-(NSString *)formatName
{
return @"Embedded RAR";
}
@end