XADMaster/XADTarParser.m
2018-03-21 14:24:50 +02:00

649 lines
22 KiB
Objective-C

/*
* XADTarParser.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
*/
// Parses tar files, except: Star, sparse tar files.
#import "XADTarParser.h"
#define TAR_FORMAT_V7 0
#define TAR_FORMAT_GNU 1 // GNU and OLDGNU are basically identical
#define TAR_FORMAT_USTAR 2 // POSIX-ish tar formats
#define TAR_FORMAT_STAR 3 // STAR is POSIX-ish, but not similiar enough to ustar and posix.2001 tar.
#define TAR_FORMAT_V7_RECOGNIZED 4
// TODO: star.
@implementation XADTarParser
+(int)requiredHeaderSize { return 512; }
+(int)getTarType:(NSData *)header
{
unsigned char head[512];
[header getBytes:head length:512];
int tarFormat = TAR_FORMAT_V7;
unsigned char magic[8];
[header getBytes:magic range:NSMakeRange(257,8)]; // "ustar\000" (ustar) / "ustar \0" (gnu)
unsigned char starExtendedMagic[4];
[header getBytes:starExtendedMagic range:NSMakeRange(508,4)]; // "tar\0"
unsigned int checksum = (unsigned int)[XADTarParser readOctalNumberInRangeFromBuffer:NSMakeRange(148,8) buffer:header];
if( [XADTarParser isTarChecksumCorrect:header checksum:checksum] == YES )
{
tarFormat = TAR_FORMAT_V7_RECOGNIZED;
}
if( memcmp( magic, (unsigned char[]){ 117, 115, 116, 97, 114, 0, 48, 48 }, 8 ) == 0 )
{
if( memcmp( starExtendedMagic, (unsigned char[]){ 116, 97, 114, 0 }, 4 ) == 0 )
{
tarFormat = TAR_FORMAT_STAR;
}
else
{
// printf("TAR: Ustar format.\n" );
tarFormat = TAR_FORMAT_USTAR;
}
}
else if( memcmp( magic, (unsigned char[]){ 117, 115, 116, 97, 114, 32, 32, 0 }, 8 ) == 0 )
{
tarFormat = TAR_FORMAT_GNU;
}
return( tarFormat );
}
// Recognize files by name or magic. (tar v7 files have no magic, are recognized as
// TAR_FORMAT_V7_RECOGNIZED via checksums)
+(BOOL)recognizeFileWithHandle:(CSHandle *)handle firstBytes:(NSData *)data name:(NSString *)name
{
if([data length]<512) return NO;
return(
[name matchedByPattern:@"^(.*)\\.tar$" options:REG_ICASE] ||
([XADTarParser getTarType:data] != TAR_FORMAT_V7)
);
}
+(double)doubleFromCString:(char *)buffer
{
NSString *readString = [[NSString alloc] initWithUTF8String:buffer];
NSScanner* scanner = [NSScanner scannerWithString:readString];
[readString release];
double returnValue;
if([scanner scanDouble:&returnValue] == YES) {
return( returnValue );
}
return( 0 );
}
+(int64_t)longFromCString:(char *)buffer
{
NSString *readString = [[NSString alloc] initWithUTF8String:buffer];
NSScanner* scanner = [NSScanner scannerWithString:readString];
[readString release];
long long returnValue;
if([scanner scanLongLong:&returnValue] == YES) {
return( returnValue );
}
return( 0 );
}
+(int64_t)readNumberInRangeFromBuffer:(NSRange)range buffer:(NSData *)buffer
{
NSString *readString = [[NSString alloc] initWithData:[buffer subdataWithRange:range] encoding:NSASCIIStringEncoding];
if(!readString) return 0;
NSScanner* scanner = [NSScanner scannerWithString:readString];
[readString release];
long long returnValue;
if([scanner scanLongLong:&returnValue] == YES) {
return( returnValue );
}
return( 0 );
}
+(int64_t)octalToDecimal:(int64_t)octal
{
int64_t decimal = 0;
int64_t temp = 0;
int64_t power_of_ten = 10000000000000;
int64_t power_of_eight = 549755813888;
while( power_of_ten != 1 )
{
power_of_ten = power_of_ten / 10;
power_of_eight = power_of_eight / 8;
temp = octal / power_of_ten;
decimal += temp * power_of_eight;
octal -= temp * power_of_ten;
}
return( decimal );
}
+(uint64_t)readOctalNumberInRangeFromBuffer:(NSRange)range buffer:(NSData *)buffer
{
// This implementation only supports numbers up to 2^64. If we ever need bigger numbers,
// that needs to change.
// Also, negative values are not supported. This could be an issue if the mtime field was
// supposed to be negative, but it is probably fair to assume that we will not be
// extracting star archives with files last modified before 1970.
uint8_t num_string[8];
uint8_t mask = 0x80;
[buffer getBytes:num_string range:NSMakeRange(range.location,8)];
if( (num_string[0]&mask) == 0x80 ) {
[buffer getBytes:num_string range:NSMakeRange(range.location + range.length - 8,8)];
return CSUInt64BE(num_string);
}
return( [XADTarParser octalToDecimal:[XADTarParser readNumberInRangeFromBuffer:range buffer:buffer]] );
}
// "Sum of all header bytes if the checksum field was 8 * ' '".
+(BOOL)isTarChecksumCorrect:(NSData *)header checksum:(int)checksum
{
unsigned char head[512];
[header getBytes:head length:512];
int signedChecksum = 0;
unsigned int unsignedChecksum = 0;
for( int i = 0; i < 148; i++ )
{
signedChecksum += (signed char)head[i];
unsignedChecksum += (unsigned char)head[i];
}
for( int i = 156; i < 512; i++ )
{
signedChecksum += (signed char)head[i];
unsignedChecksum += (unsigned char)head[i];
}
signedChecksum += 8 * ' ';
unsignedChecksum += 8 * ' ';
return( checksum == signedChecksum || checksum == unsignedChecksum );
}
-(void)dealloc
{
[currentGlobalHeader release];
[super dealloc];
}
-(void)parseSparseHeadersFromData:(NSData*)header numHeaders:(int)num toDict:(NSMutableDictionary *)dict
{
for( int i = 0; i < num; i++ )
{
off_t regionOffset = [XADTarParser readOctalNumberInRangeFromBuffer:NSMakeRange(24*i,12) buffer:header];
off_t regionLength = [XADTarParser readOctalNumberInRangeFromBuffer:NSMakeRange(24*i+12,12) buffer:header];
// printf( "Parse sparse header: %d -> %d\n", regionOffset, regionLength );
// Are we done yet?
if( regionLength == 0 && regionOffset == 0 ) {
break;
}
[[dict objectForKey:@"TARSparseRegionOffsets"] addObject:[NSNumber numberWithLongLong:regionOffset]];
[[dict objectForKey:@"TARSparseRegionLengths"] addObject:[NSNumber numberWithLongLong:regionLength]];
}
}
-(int)parseGenericTarHeader:(NSData *)header toDict:(NSMutableDictionary *)dict
{
char name[101];
[header getBytes:name range:NSMakeRange(0,100)];
name[100] = '\000';
[dict setObject:[self XADPathWithCString:name separators:XADUnixPathSeparator] forKey:XADFileNameKey];
unsigned int mode = (unsigned int)[XADTarParser readOctalNumberInRangeFromBuffer:NSMakeRange(100,8) buffer:header];
[dict setObject:[NSNumber numberWithInt:mode] forKey:XADPosixPermissionsKey];
unsigned int uid = (unsigned int)[XADTarParser readOctalNumberInRangeFromBuffer:NSMakeRange(108,8) buffer:header];
[dict setObject:[NSNumber numberWithInt:uid] forKey:XADPosixUserKey];
unsigned int gid = (unsigned int)[XADTarParser readOctalNumberInRangeFromBuffer:NSMakeRange(116,8) buffer:header];
[dict setObject:[NSNumber numberWithInt:gid] forKey:XADPosixGroupKey];
uint64_t size = [XADTarParser readOctalNumberInRangeFromBuffer:NSMakeRange(124,12) buffer:header];
[dict setObject:[NSNumber numberWithLongLong:size] forKey:XADFileSizeKey];
[dict setObject:[NSNumber numberWithLongLong:(size+(512-size%512))] forKey:XADCompressedSizeKey];
[dict setObject:[NSNumber numberWithLongLong:size] forKey:XADDataLengthKey];
unsigned long mtime = (unsigned int)[XADTarParser readOctalNumberInRangeFromBuffer:NSMakeRange(136,12) buffer:header];
[dict setObject:[NSDate dateWithTimeIntervalSince1970:mtime] forKey:XADLastModificationDateKey];
unsigned int checksum = (unsigned int)[XADTarParser readOctalNumberInRangeFromBuffer:NSMakeRange(148,8) buffer:header];
if( [XADTarParser isTarChecksumCorrect:header checksum:checksum] == NO )
{
[XADException raiseIllegalDataException];
}
char typeFlag;
[header getBytes:&typeFlag range:NSMakeRange(156,1)];
// There are only two type flags that tar, in general, supports.
// "Directory"
if( typeFlag == '5' )
{
[dict setObject:[NSNumber numberWithBool:YES] forKey:XADIsDirectoryKey];
}
// "Hard link / soft link"
if( typeFlag == '1' || typeFlag == '2' )
{
char linkName[101];
[header getBytes:linkName range:NSMakeRange(157,100)];
linkName[100] = '\000';
[dict setObject:[self XADStringWithCString:linkName] forKey:XADLinkDestinationKey];
[dict setObject:[NSNumber numberWithInt:(typeFlag%2)] forKey:XADIsHardLinkKey];
}
// KLUDGE: Fix broken directory typeflags.
if( typeFlag == 0 && name[strlen(name)-1] == '/' ) {
[dict setObject:[NSNumber numberWithBool:YES] forKey:XADIsDirectoryKey];
}
// KLUDGE: Fix broken ant tar wrong magic files.
if( strncmp( name, "././@LongLink", 13 ) == 0 ) {
return( 1 );
}
// printf( "Generictar: Name %s / %u\n", name, size );
return( 0 );
}
-(void)parsePaxTarHeader:(NSData *)header toDict:(NSMutableDictionary *)dict {
int position = 0;
while( position < [header length] ) {
// Get next pair length.
int start_pos = position;
int read_length = 0;
char current_char = '\0';
while( current_char != ' ' && current_char != 0 && read_length < 16 && position < [header length] ) {
[header getBytes:&current_char range:NSMakeRange(position,1)];
position++;
read_length++;
}
if( read_length == 0 ){
break;
}
// Grab the pair from the header.
int next_pair_size = (int)[XADTarParser readNumberInRangeFromBuffer:NSMakeRange(start_pos,read_length) buffer:header] - read_length;
int next_pair_offset = position + next_pair_size;
char key_val_pair[next_pair_size];
memset( key_val_pair, '\0', next_pair_size );
[header getBytes:key_val_pair range:NSMakeRange(position,next_pair_size - 1)];
// Parse the pair into key/value.
read_length = 0;
//start_pos = position;
int max_len = strlen( key_val_pair );
while( key_val_pair[read_length] != '=' && read_length < max_len ) {
position++;
read_length++;
}
char key[read_length + 1];
memcpy( key, key_val_pair, read_length );
key[read_length] = '\0';
char value[max_len - read_length + 1];
memcpy( value, key_val_pair + read_length + 1, (max_len - read_length) );
value[(max_len - read_length)] = '\0';
// Check keys and add proper value to dict.
// Accessed/Created/Last Modified
if( strcmp( key, "atime" ) == 0 ) {
[dict setObject:[NSDate dateWithTimeIntervalSince1970:[XADTarParser doubleFromCString:value]] forKey:XADLastAccessDateKey];
}
else if( strcmp( key, "ctime" ) == 0 ) {
[dict setObject:[NSDate dateWithTimeIntervalSince1970:[XADTarParser doubleFromCString:value]] forKey:XADCreationDateKey];
}
else if( strcmp( key, "mtime" ) == 0 ) {
[dict setObject:[NSDate dateWithTimeIntervalSince1970:[XADTarParser doubleFromCString:value]] forKey:XADLastModificationDateKey];
}
// User/Group ids/names.
else if( strcmp( key, "uname" ) == 0 ) {
[dict setObject:[self XADStringWithCString:value encodingName:XADUTF8StringEncodingName] forKey:XADPosixUserNameKey];
}
else if( strcmp( key, "gname" ) == 0 ) {
[dict setObject:[self XADStringWithCString:value encodingName:XADUTF8StringEncodingName] forKey:XADPosixGroupNameKey];
}
else if( strcmp( key, "uid" ) == 0 ) {
[dict setObject:[NSNumber numberWithInt:(int)[XADTarParser longFromCString:value]] forKey:XADPosixUserKey];
}
else if( strcmp( key, "gid" ) == 0 ) {
[dict setObject:[NSNumber numberWithInt:(int)[XADTarParser longFromCString:value]] forKey:XADPosixGroupKey];
}
// File path and link path.
else if( strcmp( key, "path" ) == 0 ) {
[dict setObject:[self XADPathWithCString:value encodingName:XADUTF8StringEncodingName separators:XADUnixPathSeparator] forKey:XADFileNameKey];
}
else if( strcmp( key, "linkpath" ) == 0 ) {
[dict setObject:[self XADStringWithCString:value encodingName:XADUTF8StringEncodingName] forKey:XADLinkDestinationKey];
}
// File size.
else if( strcmp( key, "size" ) == 0 ) {
[dict setObject:[NSNumber numberWithInt:(int)[XADTarParser longFromCString:value]] forKey:XADFileSizeKey];
}
// Comment.
else if( strcmp( key, "comment" ) == 0 ) {
[dict setObject:[self XADStringWithCString:value encodingName:XADUTF8StringEncodingName] forKey:XADCommentKey];
}
// Continue after the pair.
position = next_pair_offset;
}
}
-(void)parseUstarTarHeader:(NSData *)header toDict:(NSMutableDictionary *)dict
{
char userName[33];
[header getBytes:userName range:NSMakeRange(265,32)];
userName[32] = '\000';
[dict setObject:[self XADStringWithCString:userName] forKey:XADPosixUserNameKey];
char groupName[33];
[header getBytes:groupName range:NSMakeRange(297,32)];
groupName[32] = '\000';
[dict setObject:[self XADStringWithCString:groupName] forKey:XADPosixGroupNameKey];
unsigned int devMajor = (unsigned int)[XADTarParser readOctalNumberInRangeFromBuffer:NSMakeRange(329,8) buffer:header];
unsigned int devMinor = (unsigned int)[XADTarParser readOctalNumberInRangeFromBuffer:NSMakeRange(337,8) buffer:header];
char prefix[156];
[header getBytes:prefix range:NSMakeRange(345,155)];
prefix[155] = '\000';
// Prefix is not null => name is prefix . name
char fullName[257];
if( !(prefix[0] == '\000') ) {
char name[101];
[header getBytes:name range:NSMakeRange(0,100)];
name[100] = '\000';
fullName[0] = '\000';
strcat( fullName, prefix );
strcat( fullName, "/" );
strcat( fullName, name );
fullName[256] = '\000';
[dict setObject:[self XADPathWithCString:fullName separators:XADUnixPathSeparator] forKey:XADFileNameKey];
}
char typeFlag;
[header getBytes:&typeFlag range:NSMakeRange(156,1)];
// Global header parse.
[self parsePaxTarHeader:currentGlobalHeader toDict:dict];
// printf( "Ustar header parse after global\n" );
// Needed later for extended headers, possibly.
CSHandle *handle = [self handle];
long size = [[dict objectForKey:XADDataLengthKey] longValue];
off_t offset = [handle offsetInFile];;
offset += size;
offset += (offset % 512 == 0 ? 0 : 512 - (offset % 512) );
switch( typeFlag ) {
// Device files
case '3':
[dict setObject:[NSNumber numberWithInt:1] forKey:XADIsCharacterDeviceKey];
[dict setObject:[NSNumber numberWithInt:devMajor] forKey:XADDeviceMajorKey];
[dict setObject:[NSNumber numberWithInt:devMinor] forKey:XADDeviceMinorKey];
break;
case '4':
[dict setObject:[NSNumber numberWithInt:1] forKey:XADIsBlockDeviceKey];
[dict setObject:[NSNumber numberWithInt:devMajor] forKey:XADDeviceMajorKey];
[dict setObject:[NSNumber numberWithInt:devMinor] forKey:XADDeviceMinorKey];
break;
// FIFOs
case '6':
[dict setObject:[NSNumber numberWithInt:1] forKey:XADIsFIFOKey];
break;
// POSIX.2001 global header.
case 'g': {
// Read in the header and store for parsing
[currentGlobalHeader release];
currentGlobalHeader = [[handle readDataOfLength:size] retain];
[handle seekToFileOffset:offset];
// Parse next header.
NSData *header = [handle readDataOfLength:512];
[dict removeAllObjects];
[self parseGenericTarHeader:header toDict:dict];
[self parseUstarTarHeader:header toDict:dict];
} break;
// POSIX.2001 extended header.
case 'x': {
// Read in the header.
NSData *extendedHeader = [handle readDataOfLength:size];
[handle seekToFileOffset:offset];
// Prepare a new dictionary with the next header.
NSData *header = [handle readDataOfLength:512];
[dict removeAllObjects];
[self parseGenericTarHeader:header toDict:dict];
[self parseUstarTarHeader:header toDict:dict];
// Parse extended header.
[self parsePaxTarHeader:extendedHeader toDict:dict];
} break;
}
}
-(void)parseGnuTarHeader:(NSData *)header toDict:(NSMutableDictionary *)dict
{
char typeFlag;
[header getBytes:&typeFlag range:NSMakeRange(156,1)];
// In case of LongName / LongLink, we need the data.
CSHandle *handle = [self handle];
long size = [[dict objectForKey:XADDataLengthKey] longValue];
off_t offset = [handle offsetInFile];
offset += size;
offset += (offset % 512 == 0 ? 0 : 512 - (offset % 512) );
// LongName or LongLink?
if( typeFlag == 'L' || typeFlag == 'K' ) {
// Read in the header
NSData *longHeader = [handle readDataOfLength:size];
[handle seekToFileOffset:offset];
// Check if there is a terminating null byte, and eliminate it.
int length=[longHeader length];
const uint8_t *bytes=[longHeader bytes];
if(length>0 && bytes[length-1]==0) longHeader=[longHeader subdataWithRange:NSMakeRange(0,length-1)];
// Prepare a new dictionary with the next header.
NSData *header = [handle readDataOfLength:512];
[dict removeAllObjects];
[self parseGenericTarHeader:header toDict:dict];
[self parseGnuTarHeader:header toDict:dict];
// Set the proper key.
if( typeFlag == 'L' ) {
[dict setObject:[self XADPathWithData:longHeader separators:XADUnixPathSeparator] forKey:XADFileNameKey];
}
else {
[dict setObject:[self XADStringWithData:longHeader] forKey:XADLinkDestinationKey];
}
}
// Sparse header?
if( typeFlag == 'S' ) {
// For now, pass out a "Not supported".
// // Get and store real size.
// off_t size = [XADTarParser readOctalNumberInRangeFromBuffer:NSMakeRange(483,12) buffer:header];
// [dict setObject:[NSNumber numberWithLongLong:size] forKey:XADFileSizeKey];
// [dict setObject:[NSNumber numberWithLongLong:size] forKey:XADDataLengthKey];
//
// // Set up sparse map storage.
[dict setObject:[NSNumber numberWithBool:YES] forKey:@"TARIsSparseFile"];
// [dict setObject:[[NSMutableArray alloc] init] forKey:@"TARSparseRegionOffsets"];
// [dict setObject:[[NSMutableArray alloc] init] forKey:@"TARSparseRegionLengths"];
//
// NSData* sparseMap = [header subdataWithRange:NSMakeRange(386,96)];
// [self parseSparseHeadersFromData:sparseMap numHeaders:4 toDict:dict];
//
// // Handle extended sparse headers.
// char hasExtended;
// [header getBytes:&hasExtended range:NSMakeRange(482,1)];
// printf( "Hasx: %d\n", hasExtended );
// while( hasExtended == 1 ) {
// NSData *sparseMap = [handle readDataOfLength:512];
// [self parseSparseHeadersFromData:sparseMap numHeaders:21 toDict:dict];
// [sparseMap getBytes:&hasExtended range:NSMakeRange(504,1)];
// }
}
}
-(void)addTarEntryWithDictionaryAndSeek:(NSMutableDictionary *)dict
{
CSHandle *handle = [self handle];
off_t size = [[dict objectForKey:XADDataLengthKey] longLongValue];
off_t offset = [handle offsetInFile];
[dict setObject:[NSNumber numberWithLongLong:offset] forKey:XADDataOffsetKey];
[self addEntryWithDictionary:dict];
offset += size;
offset += (offset % 512 == 0 ? 0 : 512 - (offset % 512) );
[handle seekToFileOffset:offset];
}
-(void)parseWithSeparateMacForks
{
// Reset global current header for posix.2001;
currentGlobalHeader = [NSData new];
CSHandle *handle = [self handle];
int tarFormat = -1;
while([self shouldKeepParsing])
{
NSAutoreleasePool *pool = [NSAutoreleasePool new];
// Read next header.
if([handle atEndOfFile]) break;
NSData *header = [handle readDataOfLength:512];
// Figure out format if we haven't yet done so.
if( tarFormat == -1 ) tarFormat = [XADTarParser getTarType:header];
// See if there are 512 nullbytes. That means the file is over.
const char *firstBytes = [header bytes];
BOOL isArchiverOver = YES;
for( int i = 0; i < 512; i++ ) {
if( firstBytes[i] != '\000' ) {
isArchiverOver = NO;
}
}
if( isArchiverOver )
{
[pool release];
break;
}
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
// Reset sparseity.
[dict setObject:[NSNumber numberWithBool:NO] forKey:@"TARIsSparseFile"];
int wrongFormat = [self parseGenericTarHeader:header toDict:dict];
if( wrongFormat == 1 ) {
tarFormat = TAR_FORMAT_GNU;
}
if( tarFormat == TAR_FORMAT_V7 || tarFormat == TAR_FORMAT_V7_RECOGNIZED ) {
[self addTarEntryWithDictionaryAndSeek:dict];
}
else if( tarFormat == TAR_FORMAT_USTAR )
{
[self parseUstarTarHeader:header toDict:dict];
[self addTarEntryWithDictionaryAndSeek:dict];
}
else if( tarFormat == TAR_FORMAT_GNU )
{
[self parseGnuTarHeader:header toDict:dict];
[self addTarEntryWithDictionaryAndSeek:dict];
}
else
{
// Handled like an ustar archive.
[self parseUstarTarHeader:header toDict:dict];
[self addTarEntryWithDictionaryAndSeek:dict];
}
[pool release];
}
}
-(CSHandle *)rawHandleForEntryWithDictionary:(NSDictionary *)dict wantChecksum:(BOOL)checksum
{
CSHandle* retHandle = [self handleAtDataOffsetForDictionary:dict];
if( [[dict objectForKey:@"TARIsSparseFile"] boolValue] ) {
// Just return nil.
return( nil );
// XADTarSparseHandle* sparseHandle = [[XADTarSparseHandle alloc] initWithHandle:retHandle size:[[dict objectForKey:XADDataLengthKey] longValue]];
// NSArray* offsets = [dict objectForKey:@"TARSparseRegionOffsets"];
// NSArray* lengths = [dict objectForKey:@"TARSparseRegionLengths"];
// int sparseRegionCount = [offsets count];
// for( int i = 0; i < sparseRegionCount; i++ ) {
// // GNU tar is pretty special about some things.
// if( [[lengths objectAtIndex:i] longValue] == 0 )
// {
// // The final region is also the first - special case!
// if( i == 0 )
// {
// printf( "Final region add (not really)!\n" );
// [sparseHandle setSingleEmptySparseRegion];
// }
// else
// {
// printf( "Final region add!\n" );
// [sparseHandle addFinalSparseRegionEndingAt:[[offsets objectAtIndex:i] longValue]];
// }
// }
// else
// {
// printf( "Normal region add!\n" );
// [sparseHandle addSparseRegionFrom:[[offsets objectAtIndex:i] longValue] length:[[lengths objectAtIndex:i] longValue]];
// }
// }
// return( sparseHandle );
}
// printf( "Handle return" );
return( retHandle );
}
// This should maybe return USTAR or POSIX Tar or whatever the proper tar type is.
-(NSString *)formatName { return @"Tar"; }
@end