// // URKArchive.mm // UnrarKit // // #import "URKArchive.h" #import "URKFileInfo.h" #import "UnrarKitMacros.h" #import "NSString+UnrarKit.h" #import "zlib.h" RarHppIgnore #import "rar.hpp" #pragma clang diagnostic pop NSString *URKErrorDomain = @"URKErrorDomain"; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wundef" #if UNIFIED_LOGGING_SUPPORTED os_log_t unrarkit_log; BOOL unrarkitIsAtLeast10_13SDK; #endif #pragma clang diagnostic pop static NSBundle *_resources = nil; typedef enum : NSUInteger { URKReadHeaderLoopActionStopReading, URKReadHeaderLoopActionContinueReading, } URKReadHeaderLoopAction; @interface URKArchive () - (instancetype)initWithFile:(NSURL *)fileURL password:(NSString*)password error:(NSError * __autoreleasing *)error // iOS 7, macOS 10.9 #if (defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED > 70000) || (defined(MAC_OS_X_VERSION_MIN_REQUIRED) && MAC_OS_X_VERSION_MIN_REQUIRED > 1090) NS_DESIGNATED_INITIALIZER #endif ; @property (assign) HANDLE rarFile; @property (assign) struct RARHeaderDataEx *header; @property (assign) struct RAROpenArchiveDataEx *flags; @property (strong) NSData *fileBookmark; @property (strong) NSObject *threadLock; @property (copy) NSString *lastArchivePath; @property (copy) NSString *lastFilepath; @end @implementation URKArchive #pragma mark - Deprecated Convenience Methods + (URKArchive *)rarArchiveAtPath:(NSString *)filePath { return [[URKArchive alloc] initWithPath:filePath error:nil]; } + (URKArchive *)rarArchiveAtURL:(NSURL *)fileURL { return [[URKArchive alloc] initWithURL:fileURL error:nil]; } + (URKArchive *)rarArchiveAtPath:(NSString *)filePath password:(NSString *)password { return [[URKArchive alloc] initWithPath:filePath password:password error:nil]; } + (URKArchive *)rarArchiveAtURL:(NSURL *)fileURL password:(NSString *)password { return [[URKArchive alloc] initWithURL:fileURL password:password error:nil]; } #pragma mark - Initializers + (void)initialize { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ NSBundle *mainBundle = [NSBundle mainBundle]; NSURL *resourcesURL = [mainBundle URLForResource:@"UnrarKitResources" withExtension:@"bundle"]; _resources = (resourcesURL ? [NSBundle bundleWithURL:resourcesURL] : mainBundle); URKLogInit(); }); } - (instancetype)init { URKLogError("Attempted to use -init method, which is no longer supported"); @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"-init is not a valid initializer for the class URKArchive" userInfo:nil]; return nil; } - (instancetype)initWithPath:(NSString *)filePath error:(NSError * __autoreleasing *)error { return [self initWithFile:[NSURL fileURLWithPath:filePath] error:error]; } - (instancetype)initWithURL:(NSURL *)fileURL error:(NSError * __autoreleasing *)error { return [self initWithFile:fileURL error:error]; } - (instancetype)initWithPath:(NSString *)filePath password:(NSString *)password error:(NSError * __autoreleasing *)error { return [self initWithFile:[NSURL fileURLWithPath:filePath] password:password error:error]; } - (instancetype)initWithURL:(NSURL *)fileURL password:(NSString *)password error:(NSError * __autoreleasing *)error { return [self initWithFile:fileURL password:password error:error]; } - (instancetype)initWithFile:(NSURL *)fileURL error:(NSError * __autoreleasing *)error { return [self initWithFile:fileURL password:nil error:error]; } - (instancetype)initWithFile:(NSURL *)fileURL password:(NSString*)password error:(NSError * __autoreleasing *)error { URKCreateActivity("Init Archive"); URKLogInfo("Initializing archive with URL %{public}@, path %{public}@, password %{public}@", fileURL, fileURL.path, [password length] != 0 ? @"given" : @"not given"); if (!fileURL) { URKLogError("Cannot initialize archive with nil URL"); return nil; } if ((self = [super init])) { if (error) { *error = nil; } NSURL *firstVolumeURL = [URKArchive firstVolumeURL:fileURL]; NSString * _Nonnull fileURLAbsoluteString = static_cast(fileURL.absoluteString); if (firstVolumeURL && ![firstVolumeURL.absoluteString isEqualToString:fileURLAbsoluteString]) { URKLogDebug("Overriding fileURL with first volume URL: %{public}@", firstVolumeURL); fileURL = firstVolumeURL; } URKLogDebug("Initializing private fields"); NSError *bookmarkError = nil; _fileBookmark = [fileURL bookmarkDataWithOptions:0 includingResourceValuesForKeys:@[] relativeToURL:nil error:&bookmarkError]; _password = password; _threadLock = [[NSObject alloc] init]; _lastArchivePath = nil; _lastFilepath = nil; _ignoreCRCMismatches = NO; if (bookmarkError) { URKLogFault("Error creating bookmark to RAR archive: %{public}@", bookmarkError); if (error) { *error = bookmarkError; } return nil; } } return self; } #pragma mark - Properties - (NSURL *)fileURL { URKCreateActivity("Read Archive URL"); BOOL bookmarkIsStale = NO; NSError *error = nil; NSURL *result = [NSURL URLByResolvingBookmarkData:self.fileBookmark options:0 relativeToURL:nil bookmarkDataIsStale:&bookmarkIsStale error:&error]; if (error) { URKLogFault("Error resolving bookmark to RAR archive: %{public}@", error); return nil; } if (bookmarkIsStale) { URKLogDebug("Refreshing stale bookmark"); self.fileBookmark = [result bookmarkDataWithOptions:0 includingResourceValuesForKeys:@[] relativeToURL:nil error:&error]; if (error) { URKLogFault("Error creating fresh bookmark to RAR archive: %{public}@", error); } } return result; } - (NSString *)filename { URKCreateActivity("Read Archive Filename"); NSURL *url = self.fileURL; if (!url) { return nil; } return url.path; } - (NSNumber *)uncompressedSize { URKCreateActivity("Read Archive Uncompressed Size"); NSError *listError = nil; NSArray *fileInfo = [self listFileInfo:&listError]; if (!fileInfo) { URKLogError("Error getting uncompressed size: %{public}@", listError); return nil; } if (fileInfo.count == 0) { URKLogInfo("No files in archive. Size == 0"); return 0; } return [fileInfo valueForKeyPath:@"@sum.uncompressedSize"]; } - (NSNumber *)compressedSize { URKCreateActivity("Read Archive Compressed Size"); NSString *filePath = self.filename; if (!filePath) { URKLogError("Can't get compressed size, since a file path can't be resolved"); return nil; } URKLogInfo("Reading archive file attributes..."); NSError *attributesError = nil; NSDictionary *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:&attributesError]; if (!attributes) { URKLogError("Error getting compressed size of %{public}@: %{public}@", filePath, attributesError); return nil; } return [NSNumber numberWithUnsignedLongLong:attributes.fileSize]; } - (BOOL)hasMultipleVolumes { URKCreateActivity("Check If Multi-Volume Archive"); NSError *listError = nil; NSArray *volumeURLs = [self listVolumeURLs:&listError]; if (!volumeURLs) { URKLogError("Error getting file volumes list: %{public}@", listError); return false; } return volumeURLs.count > 1; } #pragma mark - Zip file detection + (BOOL)pathIsARAR:(NSString *)filePath { URKCreateActivity("Determining File Type (Path)"); NSFileHandle *handle = [NSFileHandle fileHandleForReadingAtPath:filePath]; if (!handle) { URKLogError("No file handle returned for path: %{public}@", filePath); return NO; } @try { NSData *fileData = [handle readDataOfLength:8]; if (fileData.length < 8) { URKLogDebug("No file handle returned for path: %{public}@", filePath); return NO; } const unsigned char *dataBytes = (const unsigned char *)fileData.bytes; // Check the magic numbers for all versions (Rar!..) if (dataBytes[0] != 0x52 || dataBytes[1] != 0x61 || dataBytes[2] != 0x72 || dataBytes[3] != 0x21 || dataBytes[4] != 0x1A || dataBytes[5] != 0x07) { URKLogDebug("File is not a RAR. Magic numbers != 'Rar!..'"); return NO; } // Check for v1.5 and on if (dataBytes[6] == 0x00) { URKLogDebug("File is a RAR >= v1.5"); return YES; } // Check for v5.0 if (dataBytes[6] == 0x01 && dataBytes[7] == 0x00) { URKLogDebug("File is a RAR >= v5.0"); return YES; } URKLogDebug("File is not a RAR. Unknown contents in 7th and 8th bytes (%02X %02X)", dataBytes[6], dataBytes[7]); } @catch (NSException *e) { URKLogError("Error checking if %{public}@ is a RAR archive: %{public}@", filePath, e); } @finally { [handle closeFile]; } return NO; } + (BOOL)urlIsARAR:(NSURL *)fileURL { URKCreateActivity("Determining File Type (URL)"); if (!fileURL || !fileURL.path) { URKLogDebug("File is not a RAR: nil URL or path"); return NO; } NSString *_Nonnull path = static_cast(fileURL.path); return [URKArchive pathIsARAR:path]; } #pragma mark - Public Methods - (NSArray *)listFilenames:(NSError * __autoreleasing *)error { URKCreateActivity("Listing Filenames"); NSArray *files = [self listFileInfo:error]; return [files valueForKey:@"filename"]; } - (NSArray *)listFileInfo:(NSError * __autoreleasing *)error { URKCreateActivity("Listing File Info"); NSMutableSet *distinctFilenames = [NSMutableSet set]; NSMutableArray *distinctFileInfo = [NSMutableArray array]; NSError *innerError = nil; BOOL wasSuccessful = [self iterateFileInfo:^(URKFileInfo * _Nonnull fileInfo, BOOL * _Nonnull stop) { if (![distinctFilenames containsObject:fileInfo.filename]) { [distinctFileInfo addObject:fileInfo]; [distinctFilenames addObject:fileInfo.filename]; } else { URKLogDebug("Skipping %{public}@ from list of file info, since it's already represented (probably from another archive volume)", fileInfo.filename); } } error:&innerError]; if (!wasSuccessful) { URKLogError("Failed to iterate file info: %{public}@", innerError); if (error && innerError) { *error = innerError; } return nil; } URKLogDebug("Found %lu file info items", (unsigned long)distinctFileInfo.count); return [NSArray arrayWithArray:distinctFileInfo]; } - (BOOL) iterateFileInfo:(void(^)(URKFileInfo *fileInfo, BOOL *stop))action error:(NSError * __autoreleasing *)error { URKCreateActivity("Iterating File Info"); NSAssert(action != nil, @"'action' is a required argument"); NSError *innerError = nil; URKLogDebug("Beginning to iterate through contents of %{public}@", self.filename); BOOL wasSuccessful = [self iterateAllFileInfo:action error:&innerError]; if (!wasSuccessful) { URKLogError("Failed to iterate all file info: %{public}@", innerError); if (error && innerError) { *error = innerError; } return NO; } return YES; } - (nullable NSArray *)listVolumeURLs:(NSError * __autoreleasing *)error { URKCreateActivity("Listing Volume URLs"); NSArray *allFileInfo = [self allFileInfo:error]; if (!allFileInfo) { return nil; } NSMutableSet *volumeURLs = [[NSMutableSet alloc] init]; for (URKFileInfo* info in allFileInfo) { NSURL *archiveURL = [NSURL fileURLWithPath:info.archiveName]; if (archiveURL) { [volumeURLs addObject:archiveURL]; } } SEL sortBySelector = @selector(path); NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:NSStringFromSelector(sortBySelector) ascending:YES]; NSArray *sortedVolumes = [volumeURLs sortedArrayUsingDescriptors:@[sortDescriptor]]; return sortedVolumes; } - (BOOL)extractFilesTo:(NSString *)filePath overwrite:(BOOL)overwrite error:(NSError * __autoreleasing *)error { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" return [self extractFilesTo:filePath overwrite:overwrite progress:nil error:error]; #pragma clang diagnostic pop } - (BOOL)extractFilesTo:(NSString *)filePath overwrite:(BOOL)overwrite progress:(void (^)(URKFileInfo *currentFile, CGFloat percentArchiveDecompressed))progressBlock error:(NSError * __autoreleasing *)error { URKCreateActivity("Extracting Files to Directory"); __block BOOL result = YES; NSError *listError = nil; NSArray *fileInfos = [self listFileInfo:&listError]; if (!fileInfos || listError) { URKLogError("Error listing contents of archive: %{public}@", listError); if (error) { *error = listError; } return NO; } NSNumber *totalSize = [fileInfos valueForKeyPath:@"@sum.uncompressedSize"]; __block long long bytesDecompressed = 0; NSProgress *progress = [self beginProgressOperation:totalSize.longLongValue]; progress.kind = NSProgressKindFile; URKLogDebug("Archive has total size of %{iec-bytes}lld", totalSize.longLongValue); __weak URKArchive *welf = self; BOOL success = [self performActionWithArchiveOpen:^(NSError **innerError) { URKCreateActivity("Performing File Extraction"); int RHCode = 0, PFCode = 0, filesExtracted = 0; URKFileInfo *fileInfo; URKLogInfo("Extracting to %{public}@", filePath); URKLogDebug("Reading through RAR header looking for files..."); while ([welf readHeader:&RHCode info:&fileInfo] == URKReadHeaderLoopActionContinueReading) { URKLogDebug("Extracting %{public}@ (%{iec-bytes}lld)", fileInfo.filename, fileInfo.uncompressedSize); NSURL *extractedURL = [[NSURL fileURLWithPath:filePath] URLByAppendingPathComponent:fileInfo.filename]; [progress setUserInfoObject:extractedURL forKey:NSProgressFileURLKey]; [progress setUserInfoObject:fileInfo forKey:URKProgressInfoKeyFileInfoExtracting]; if ([welf headerContainsErrors:innerError]) { URKLogError("Header contains an error") result = NO; return; } if (progress.isCancelled) { NSString *errorName = nil; [welf assignError:innerError code:URKErrorCodeUserCancelled errorName:&errorName]; URKLogInfo("Halted file extraction due to user cancellation: %{public}@", errorName); result = NO; return; } char cFilePath[2048]; BOOL utf8ConversionSucceeded = [filePath getCString:cFilePath maxLength:sizeof(cFilePath) encoding:NSUTF8StringEncoding]; if (!utf8ConversionSucceeded) { NSString *errorName = nil; [welf assignError:innerError code:URKErrorCodeStringConversion errorName:&errorName]; URKLogError("Error converting file to UTF-8 (buffer too short?)"); result = NO; return; } BOOL (^shouldCancelBlock)() = ^BOOL { URKCreateActivity("shouldCancelBlock"); URKLogDebug("Progress.isCancelled: %{public}@", progress.isCancelled ? @"YES" : @"NO") return progress.isCancelled; }; RARSetCallback(welf.rarFile, AllowCancellationCallbackProc, (long)shouldCancelBlock); PFCode = RARProcessFile(welf.rarFile, RAR_EXTRACT, cFilePath, NULL); if (![welf didReturnSuccessfully:PFCode]) { RARSetCallback(welf.rarFile, NULL, NULL); NSString *errorName = nil; NSInteger errorCode = progress.isCancelled ? URKErrorCodeUserCancelled : PFCode; [welf assignError:innerError code:errorCode errorName:&errorName]; URKLogError("Error extracting file: %{public}@ (%ld)", errorName, (long)errorCode); result = NO; return; } [progress setUserInfoObject:@(++filesExtracted) forKey:NSProgressFileCompletedCountKey]; [progress setUserInfoObject:@(fileInfos.count) forKey:NSProgressFileTotalCountKey]; progress.completedUnitCount += fileInfo.uncompressedSize; URKLogDebug("Finished extracting %{public}@. Extraction %f complete", fileInfo.filename, progress.fractionCompleted); if (progressBlock) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdouble-promotion" // I would change the signature of this block, but it's been deprecated already, // so it'll just get dropped eventually, and it made sense to silence the warning progressBlock(fileInfo, bytesDecompressed / totalSize.floatValue); #pragma clang diagnostic pop } bytesDecompressed += fileInfo.uncompressedSize; } RARSetCallback(welf.rarFile, NULL, NULL); if (![welf didReturnSuccessfully:RHCode]) { NSString *errorName = nil; [welf assignError:innerError code:RHCode errorName:&errorName]; URKLogError("Error reading file header: %{public}@ (%d)", errorName, RHCode); result = NO; } if (progressBlock) { progressBlock(fileInfo, 1.0); } } inMode:RAR_OM_EXTRACT error:error]; return success && result; } - (NSData *)extractData:(URKFileInfo *)fileInfo error:(NSError * __autoreleasing *)error { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" return [self extractDataFromFile:fileInfo.filename progress:nil error:error]; #pragma clang diagnostic pop } - (NSData *)extractData:(URKFileInfo *)fileInfo progress:(void (^)(CGFloat percentDecompressed))progressBlock error:(NSError * __autoreleasing *)error { return [self extractDataFromFile:fileInfo.filename progress:progressBlock error:error]; } - (NSData *)extractDataFromFile:(NSString *)filePath error:(NSError * __autoreleasing *)error { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" return [self extractDataFromFile:filePath progress:nil error:error]; #pragma clang diagnostic pop } - (NSData *)extractDataFromFile:(NSString *)filePath progress:(void (^)(CGFloat percentDecompressed))progressBlock error:(NSError * __autoreleasing *)error { URKCreateActivity("Extracting Data from File"); NSProgress *progress = [self beginProgressOperation:0]; __block NSData *result = nil; __weak URKArchive *welf = self; BOOL success = [self performActionWithArchiveOpen:^(NSError **innerError) { URKCreateActivity("Performing Extraction"); int RHCode = 0, PFCode = 0; URKFileInfo *fileInfo; URKLogDebug("Reading through RAR header looking for files..."); while ([welf readHeader:&RHCode info:&fileInfo] == URKReadHeaderLoopActionContinueReading) { if ([welf headerContainsErrors:innerError]) { URKLogError("Header contains an error") return; } if ([fileInfo.filename isEqualToString:filePath]) { URKLogDebug("Extracting %{public}@", fileInfo.filename); break; } else { URKLogDebug("Skipping %{public}@", fileInfo.filename); if ((PFCode = RARProcessFileW(welf.rarFile, RAR_SKIP, NULL, NULL)) != 0) { NSString *errorName = nil; [welf assignError:innerError code:(NSInteger)PFCode errorName:&errorName]; URKLogError("Error skipping file: %{public}@ (%d)", errorName, PFCode); return; } } } if (RHCode != ERAR_SUCCESS) { NSString *errorName = nil; [welf assignError:innerError code:RHCode errorName:&errorName]; URKLogError("Error reading file header: %{public}@ (%d)", errorName, RHCode); return; } // Empty file, or a directory if (fileInfo.uncompressedSize == 0) { URKLogDebug("%{public}@ is empty or a directory", fileInfo.filename); result = [NSData data]; return; } NSMutableData *fileData = [NSMutableData dataWithCapacity:(NSUInteger)fileInfo.uncompressedSize]; CGFloat totalBytes = fileInfo.uncompressedSize; progress.totalUnitCount = totalBytes; __block long long bytesRead = 0; if (progressBlock) { progressBlock(0.0); } BOOL (^bufferedReadBlock)(NSData*) = ^BOOL(NSData *dataChunk) { URKLogDebug("Appending buffered data (%lu bytes)", (unsigned long)dataChunk.length); [fileData appendData:dataChunk]; progress.completedUnitCount += dataChunk.length; bytesRead += dataChunk.length; if (progressBlock) { progressBlock(bytesRead / totalBytes); } if (progress.isCancelled) { URKLogInfo("Cancellation initiated"); return NO; } return YES; }; RARSetCallback(welf.rarFile, BufferedReadCallbackProc, (long)bufferedReadBlock); URKLogInfo("Processing file..."); PFCode = RARProcessFile(welf.rarFile, RAR_TEST, NULL, NULL); RARSetCallback(welf.rarFile, NULL, NULL); if (progress.isCancelled) { NSString *errorName = nil; [welf assignError:innerError code:URKErrorCodeUserCancelled errorName:&errorName]; URKLogInfo("Returning nil data from extraction due to user cancellation: %{public}@", errorName); return; } if (![welf didReturnSuccessfully:PFCode]) { NSString *errorName = nil; [welf assignError:innerError code:(NSInteger)PFCode errorName:&errorName]; URKLogError("Error extracting file data: %{public}@ (%d)", errorName, PFCode); return; } result = [NSData dataWithData:fileData]; } inMode:RAR_OM_EXTRACT error:error]; if (!success) { return nil; } return result; } - (BOOL)performOnFilesInArchive:(void(^)(URKFileInfo *fileInfo, BOOL *stop))action error:(NSError * __autoreleasing *)error { URKCreateActivity("Performing Action on Each File"); URKLogInfo("Listing file info"); NSError *listError = nil; NSArray *fileInfo = [self listFileInfo:&listError]; if (listError || !fileInfo) { URKLogError("Failed to list the files in the archive: %{public}@", listError); if (error) { *error = listError; } return NO; } NSProgress *progress = [self beginProgressOperation:fileInfo.count]; URKLogInfo("Sorting file info by name/path"); NSArray *sortedFileInfo = [fileInfo sortedArrayUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"filename" ascending:YES]]]; { URKCreateActivity("Iterating Each File Info"); [sortedFileInfo enumerateObjectsUsingBlock:^(URKFileInfo *info, NSUInteger idx, BOOL *stop) { if (progress.isCancelled) { URKLogInfo("PerformOnFiles iteration was cancelled"); *stop = YES; } URKLogDebug("Performing action on %{public}@", info.filename); action(info, stop); progress.completedUnitCount += 1; if (*stop) { URKLogInfo("Action dictated an early stop"); progress.completedUnitCount = progress.totalUnitCount; } }]; } return YES; } - (BOOL)performOnDataInArchive:(void (^)(URKFileInfo *, NSData *, BOOL *))action error:(NSError * __autoreleasing *)error { URKCreateActivity("Performing Action on Each File's Data"); NSError *listError = nil; NSArray *fileInfo = [self listFileInfo:&listError]; if (!fileInfo || listError) { URKLogError("Error listing contents of archive: %{public}@", listError); if (error) { *error = listError; } return NO; } NSNumber *totalSize = [fileInfo valueForKeyPath:@"@sum.uncompressedSize"]; __weak URKArchive *welf = self; BOOL success = [self performActionWithArchiveOpen:^(NSError **innerError) { int RHCode = 0, PFCode = 0; BOOL stop = NO; NSProgress *progress = [welf beginProgressOperation:totalSize.longLongValue]; URKLogDebug("Reading through RAR header looking for files..."); URKFileInfo *info = nil; while ([welf readHeader:&RHCode info:&info] == URKReadHeaderLoopActionContinueReading) { if (stop || progress.isCancelled) { URKLogDebug("Action dictated an early stop"); return; } if ([welf headerContainsErrors:innerError]) { URKLogError("Header contains an error") return; } URKLogDebug("Performing action on %{public}@", info.filename); // Empty file, or a directory if (info.isDirectory || info.uncompressedSize == 0) { URKLogDebug("%{public}@ is an empty file, or a directory", info.filename); action(info, [NSData data], &stop); PFCode = RARProcessFile(welf.rarFile, RAR_SKIP, NULL, NULL); if (PFCode != 0) { NSString *errorName = nil; [welf assignError:innerError code:(NSInteger)PFCode errorName:&errorName]; URKLogError("Error skipping directory: %{public}@ (%d)", errorName, PFCode); return; } continue; } UInt8 *buffer = (UInt8 *)malloc((size_t)info.uncompressedSize * sizeof(UInt8)); UInt8 *callBackBuffer = buffer; RARSetCallback(welf.rarFile, CallbackProc, (long) &callBackBuffer); URKLogInfo("Processing file..."); PFCode = RARProcessFile(welf.rarFile, RAR_TEST, NULL, NULL); if (![welf didReturnSuccessfully:PFCode]) { NSString *errorName = nil; [welf assignError:innerError code:(NSInteger)PFCode errorName:&errorName]; URKLogError("Error processing file: %{public}@ (%d)", errorName, PFCode); return; } URKLogDebug("Performing action on data (%lld bytes)", info.uncompressedSize); NSData *data = [NSData dataWithBytesNoCopy:buffer length:(NSUInteger)info.uncompressedSize freeWhenDone:YES]; action(info, data, &stop); progress.completedUnitCount += data.length; } if (progress.isCancelled) { NSString *errorName = nil; [welf assignError:innerError code:URKErrorCodeUserCancelled errorName:&errorName]; URKLogInfo("Returning NO from performOnData:error: due to user cancellation: %{public}@", errorName); return; } if (![welf didReturnSuccessfully:RHCode]) { NSString *errorName = nil; [welf assignError:innerError code:RHCode errorName:&errorName]; URKLogError("Error reading file header: %{public}@ (%d)", errorName, RHCode); return; } } inMode:RAR_OM_EXTRACT error:error]; return success; } - (BOOL)extractBufferedDataFromFile:(NSString *)filePath error:(NSError * __autoreleasing *)error action:(void(^)(NSData *dataChunk, CGFloat percentDecompressed))action { URKCreateActivity("Extracting Buffered Data"); NSError *actionError = nil; NSProgress *progress = [self beginProgressOperation:0]; __weak URKArchive *welf = self; BOOL success = [self performActionWithArchiveOpen:^(NSError **innerError) { URKCreateActivity("Performing action"); int RHCode = 0, PFCode = 0; URKFileInfo *fileInfo; URKLogInfo("Looping through files, looking for %{public}@...", filePath); while ([welf readHeader:&RHCode info:&fileInfo] == URKReadHeaderLoopActionContinueReading) { if ([welf headerContainsErrors:innerError]) { URKLogDebug("Header contains error") return; } if ([fileInfo.filename isEqualToString:filePath]) { URKLogDebug("Found desired file"); break; } else { URKLogDebug("Skipping file..."); PFCode = RARProcessFile(welf.rarFile, RAR_SKIP, NULL, NULL); if (![welf didReturnSuccessfully:PFCode]) { NSString *errorName = nil; [welf assignError:innerError code:(NSInteger)PFCode errorName:&errorName]; URKLogError("Failed to skip file: %{public}@ (%d)", errorName, PFCode); return; } } } long long totalBytes = fileInfo.uncompressedSize; progress.totalUnitCount = totalBytes; if (![welf didReturnSuccessfully:RHCode]) { NSString *errorName = nil; [welf assignError:innerError code:RHCode errorName:&errorName]; URKLogError("Header read yielded error: %{public}@ (%d)", errorName, RHCode); return; } // Empty file, or a directory if (totalBytes == 0) { URKLogInfo("File is empty or a directory"); return; } __block long long bytesRead = 0; // Repeating the argument instead of using positional specifiers, because they don't work with the {} formatters URKLogDebug("Uncompressed size: %{iec-bytes}lld (%lld bytes) in file", totalBytes, totalBytes); BOOL (^bufferedReadBlock)(NSData*) = ^BOOL(NSData *dataChunk) { if (progress.isCancelled) { URKLogInfo("Buffered data read cancelled"); return NO; } bytesRead += dataChunk.length; progress.completedUnitCount += dataChunk.length; double progressPercent = bytesRead / static_cast(totalBytes); URKLogDebug("Read data chunk of size %lu (%.3f%% complete). Calling handler...", (unsigned long)dataChunk.length, progressPercent * 100); action(dataChunk, progressPercent); return YES; }; RARSetCallback(welf.rarFile, BufferedReadCallbackProc, (long)bufferedReadBlock); URKLogDebug("Processing file..."); PFCode = RARProcessFile(welf.rarFile, RAR_TEST, NULL, NULL); RARSetCallback(welf.rarFile, NULL, NULL); if (progress.isCancelled) { NSString *errorName = nil; [welf assignError:innerError code:URKErrorCodeUserCancelled errorName:&errorName]; URKLogError("Buffered data extraction has been cancelled: %{public}@", errorName); return; } if (![welf didReturnSuccessfully:PFCode]) { NSString *errorName = nil; [welf assignError:innerError code:(NSInteger)PFCode errorName:&errorName]; URKLogError("Error processing file: %{public}@ (%d)", errorName, PFCode); } } inMode:RAR_OM_EXTRACT error:&actionError]; if (error) { *error = actionError; if (actionError) { URKLogError("Error reading buffered data from file\nfilePath: %{public}@\nerror: %{public}@", filePath, actionError); } } return success && !actionError; } - (BOOL)isPasswordProtected { URKCreateActivity("Checking Password Protection"); @try { URKLogDebug("Opening archive"); NSError *error = nil; if (![self _unrarOpenFile:self.filename inMode:RAR_OM_EXTRACT withPassword:nil error:&error]) { URKLogError("Failed to open archive while checking for password: %{public}@", error); return NO; } URKLogDebug("Reading header and starting processing..."); int RHCode = RARReadHeaderEx(self.rarFile, self.header); int PFCode = RARProcessFile(self.rarFile, RAR_SKIP, NULL, NULL); URKLogDebug("Checking header"); if ([self headerContainsErrors:&error]) { if (error.code == ERAR_MISSING_PASSWORD) { URKLogDebug("Password is missing"); return YES; } URKLogError("Errors in header while checking for password: %{public}@", error); } if (RHCode == ERAR_MISSING_PASSWORD || PFCode == ERAR_MISSING_PASSWORD) { URKLogDebug("Missing password indicated by RHCode (%d) or PFCode (%d)", RHCode, PFCode); return YES; } } @finally { [self closeFile]; } URKLogDebug("Archive is not password protected"); return NO; } - (BOOL)validatePassword { URKCreateActivity("Validating Password"); __block NSError *error = nil; __block BOOL passwordIsGood = YES; __weak URKArchive *welf = self; BOOL success = [self performActionWithArchiveOpen:^(NSError **innerError) { URKCreateActivity("Performing action"); URKLogDebug("Opening and processing archive..."); int RHCode = RARReadHeaderEx(welf.rarFile, welf.header); int PFCode = RARProcessFile(welf.rarFile, RAR_TEST, NULL, NULL); if ([welf headerContainsErrors:innerError]) { if (error.code == ERAR_MISSING_PASSWORD) { URKLogDebug("Password invalidated by header"); passwordIsGood = NO; } else { URKLogError("Errors in header while validating password: %{public}@", error); } return; } if (RHCode == ERAR_MISSING_PASSWORD || PFCode == ERAR_MISSING_PASSWORD || RHCode == ERAR_BAD_PASSWORD || PFCode == ERAR_BAD_PASSWORD) { URKLogDebug("Missing/bad password indicated by RHCode (%d) or PFCode (%d)", RHCode, PFCode); passwordIsGood = NO; return; } if ([welf hasBadCRC:RHCode] || [welf hasBadCRC:PFCode]) { URKLogDebug("Missing/bad password indicated via CRC mismatch by RHCode (%d) or PFCode (%d)", RHCode, PFCode); passwordIsGood = NO; return; } } inMode:RAR_OM_EXTRACT error:&error]; if (!success) { URKLogError("Error validating password: %{public}@", error); return NO; } return passwordIsGood; } - (BOOL)checkDataIntegrity { return [self checkDataIntegrityOfFile:(NSString *_Nonnull)nil]; } - (BOOL)checkDataIntegrityIgnoringCRCMismatches:(BOOL(^)())ignoreCRCMismatches { int rhCode = [self dataIntegrityCodeOfFile:nil]; if (rhCode == ERAR_SUCCESS) { return YES; } if (rhCode == ERAR_BAD_DATA) { NSOperationQueue *mainQueue = [NSOperationQueue mainQueue]; __block BOOL blockResult; if ([NSOperationQueue currentQueue] == mainQueue) { blockResult = ignoreCRCMismatches(); } else { [mainQueue addOperations:@[[NSBlockOperation blockOperationWithBlock:^{ blockResult = ignoreCRCMismatches(); }]] waitUntilFinished:YES]; } self.ignoreCRCMismatches = blockResult; return self.ignoreCRCMismatches; } return NO; } - (BOOL)checkDataIntegrityOfFile:(NSString *)filePath { return [self dataIntegrityCodeOfFile:filePath] == ERAR_SUCCESS; } - (int)dataIntegrityCodeOfFile:(NSString *)filePath { URKCreateActivity("Checking Data Integrity"); URKLogInfo("Checking integrity of %{public}@", filePath ? filePath : @"whole archive"); __block int RHCode = 0; __block int PFCode = 0; __weak URKArchive *welf = self; NSError *performOnFilesError = nil; BOOL wasSuccessful = [self performActionWithArchiveOpen:^(NSError **innerError) { URKCreateActivity("Iterating through each file"); while (true) { URKFileInfo *fileInfo = nil; [welf readHeader:&RHCode info:&fileInfo]; welf.lastFilepath = nil; welf.lastArchivePath = nil; if (RHCode == ERAR_END_ARCHIVE) { RHCode = ERAR_SUCCESS; break; } if (filePath && ![fileInfo.filename isEqualToString:filePath]) continue; if (RHCode != ERAR_SUCCESS) { break; } if ((PFCode = RARProcessFile(welf.rarFile, RAR_TEST, NULL, NULL)) != ERAR_SUCCESS) { RHCode = PFCode; break; } if (filePath) { break; } } } inMode:RAR_OM_EXTRACT error:&performOnFilesError]; if (RHCode == ERAR_END_ARCHIVE) { RHCode = ERAR_SUCCESS; } if (performOnFilesError) { URKLogError("Error checking data integrity: %{public}@", performOnFilesError); } if (!wasSuccessful) { URKLogError("Error checking data integrity"); if (RHCode == ERAR_SUCCESS) { RHCode = ERAR_UNKNOWN; } } return RHCode; } #pragma mark - Callback Functions int CALLBACK CallbackProc(UINT msg, long UserData, long P1, long P2) { URKCreateActivity("CallbackProc"); UInt8 **buffer; switch(msg) { case UCM_CHANGEVOLUME: URKLogDebug("msg: UCM_CHANGEVOLUME"); break; case UCM_PROCESSDATA: URKLogDebug("msg: UCM_PROCESSDATA; Copying data"); buffer = (UInt8 **) UserData; memcpy(*buffer, (UInt8 *)P1, P2); // advance the buffer ptr, original m_buffer ptr is untouched *buffer += P2; break; case UCM_NEEDPASSWORD: URKLogDebug("msg: UCM_NEEDPASSWORD"); break; } return 0; } int CALLBACK BufferedReadCallbackProc(UINT msg, long UserData, long P1, long P2) { URKCreateActivity("BufferedReadCallbackProc"); BOOL (^bufferedReadBlock)(NSData*) = (__bridge BOOL(^)(NSData*))(void *)UserData; if (msg == UCM_PROCESSDATA) { @autoreleasepool { URKLogDebug("msg: UCM_PROCESSDATA; Copying data chunk and calling read block"); NSData *dataChunk = [NSData dataWithBytes:(UInt8 *)P1 length:P2]; BOOL cancelRequested = !bufferedReadBlock(dataChunk); if (cancelRequested) { return -1; } } } return 0; } int CALLBACK AllowCancellationCallbackProc(UINT msg, long UserData, long P1, long P2) { URKCreateActivity("AllowCancellationCallbackProc"); BOOL (^shouldCancelBlock)() = (__bridge BOOL(^)())(void *)UserData; if (!shouldCancelBlock) { return 0; } BOOL shouldCancel = shouldCancelBlock(); if (shouldCancel) { URKLogDebug("Operation cancelled in shouldCancelBlock()"); } return shouldCancel ? -1 : 0; } #pragma mark - Private Methods - (BOOL)performActionWithArchiveOpen:(void(^)(NSError **innerError))action inMode:(NSInteger)mode error:(NSError * __autoreleasing *)error { URKCreateActivity("-performActionWithArchiveOpen:inMode:error:"); @synchronized(self.threadLock) { URKLogDebug("Entered lock"); if (error) { URKLogDebug("Error pointer passed in"); *error = nil; } URKLogDebug("Opening archive"); NSError *openFileError = nil; if (![self _unrarOpenFile:self.filename inMode:mode withPassword:self.password error:&openFileError]) { URKLogError("Failed to open archive: %{public}@", openFileError); if (error) { *error = openFileError; } return NO; } NSError *actionError = nil; @try { URKLogDebug("Calling action block"); action(&actionError); } @finally { [self closeFile]; } if (actionError) { URKLogError("Action block returned error: %{public}@", actionError); if (error){ *error = actionError; } } return !actionError; } } - (BOOL)_unrarOpenFile:(NSString *)rarFile inMode:(NSInteger)mode withPassword:(NSString *)aPassword error:(NSError * __autoreleasing *)error { URKCreateActivity("-_unrarOpenFile:inMode:withPassword:error:"); if (error) { URKLogDebug("Error pointer passed in"); *error = nil; } URKLogDebug("Zeroing out fields..."); ErrHandler.Clean(); self.header = new RARHeaderDataEx; bzero(self.header, sizeof(RARHeaderDataEx)); self.flags = new RAROpenArchiveDataEx; bzero(self.flags, sizeof(RAROpenArchiveDataEx)); URKLogDebug("Setting archive name..."); self.flags->ArcName = strdup(rarFile.UTF8String); self.flags->OpenMode = (uint)mode; self.flags->OpFlags = self.ignoreCRCMismatches ? ROADOF_KEEPBROKEN : 0; URKLogDebug("Opening archive %{public}@...", rarFile); self.rarFile = RAROpenArchiveEx(self.flags); if (self.rarFile == 0 || self.flags->OpenResult != 0) { NSString *errorName = nil; [self assignError:error code:(NSInteger)self.flags->OpenResult errorName:&errorName]; URKLogError("Error opening archive: %{public}@ (%d)", errorName, self.flags->OpenResult); return NO; } self.lastFilepath = nil; self.lastArchivePath = nil; if (aPassword != nil) { URKLogDebug("Setting password..."); char cPassword[2048]; BOOL utf8ConversionSucceeded = [aPassword getCString:cPassword maxLength:sizeof(cPassword) encoding:NSUTF8StringEncoding]; if (!utf8ConversionSucceeded) { NSString *errorName = nil; [self assignError:error code:URKErrorCodeStringConversion errorName:&errorName]; URKLogError("Error converting password to UTF-8 (buffer too short?)"); return NO; } RARSetPassword(self.rarFile, cPassword); } return YES; } - (BOOL)closeFile { URKCreateActivity("-closeFile"); if (self.rarFile) { URKLogDebug("Closing archive %{public}@...", self.filename); RARCloseArchive(self.rarFile); } URKLogDebug("Cleaning up fields..."); self.rarFile = 0; if (self.flags) delete self.flags->ArcName; delete self.flags; self.flags = 0; delete self.header; self.header = 0; return YES; } - (BOOL) iterateAllFileInfo:(void(^)(URKFileInfo *fileInfo, BOOL *stop))action error:(NSError * __autoreleasing *)error { URKCreateActivity("-allFileInfo:"); NSAssert(action != nil, @"'action' is a required argument"); __weak URKArchive *welf = self; BOOL wasSuccessful = [self performActionWithArchiveOpen:^(NSError **innerError) { URKCreateActivity("Performing List Action"); int RHCode = 0, PFCode = 0; URKLogDebug("Reading through RAR header looking for files..."); URKFileInfo *info = nil; while ([welf readHeader:&RHCode info:&info] == URKReadHeaderLoopActionContinueReading) { URKLogDebug("Calling iterateAllFileInfo handler"); BOOL shouldStop = NO; action(info, &shouldStop); if (shouldStop) { URKLogDebug("iterateAllFileInfo got signal to stop"); return; } URKLogDebug("Skipping to next file..."); if ((PFCode = RARProcessFile(welf.rarFile, RAR_SKIP, NULL, NULL)) != 0) { NSString *errorName = nil; [welf assignError:innerError code:(NSInteger)PFCode errorName:&errorName]; URKLogError("Error skipping to next header file: %{public}@ (%d)", errorName, PFCode); return; } } if (![welf didReturnSuccessfully:RHCode]) { NSString *errorName = nil; [welf assignError:innerError code:RHCode errorName:&errorName]; URKLogError("Error reading RAR header: %{public}@ (%d)", errorName, RHCode); } } inMode:RAR_OM_LIST_INCSPLIT error:error]; return wasSuccessful; } - (NSArray *) allFileInfo:(NSError * __autoreleasing *)error { URKCreateActivity("-allFileInfo:"); NSMutableArray *fileInfos = [NSMutableArray array]; NSError *innerError = nil; URKLogDebug("Iterating all file info"); BOOL wasSuccessful = [self iterateAllFileInfo:^(URKFileInfo *fileInfo, BOOL *stop) { [fileInfos addObject:fileInfo]; } error:&innerError]; if (!wasSuccessful || !fileInfos) { URKLogError("File info iteration was not successful: %{public}@", innerError); if (error && innerError) { *error = innerError; } return nil; } URKLogDebug("Found %lu files", (unsigned long)fileInfos.count); return [NSArray arrayWithArray:fileInfos]; } - (NSString *)errorNameForErrorCode:(NSInteger)errorCode detail:(NSString * __autoreleasing *)errorDetail { NSAssert(errorDetail != NULL, @"errorDetail out parameter not given"); NSString *errorName; NSString *detail = @""; switch (errorCode) { case URKErrorCodeEndOfArchive: errorName = @"ERAR_END_ARCHIVE"; break; case URKErrorCodeNoMemory: errorName = @"ERAR_NO_MEMORY"; detail = NSLocalizedStringFromTableInBundle(@"Ran out of memory while reading archive", @"UnrarKit", _resources, @"Error detail string"); break; case URKErrorCodeBadData: errorName = @"ERAR_BAD_DATA"; detail = NSLocalizedStringFromTableInBundle(@"Archive has a corrupt header", @"UnrarKit", _resources, @"Error detail string"); break; case URKErrorCodeBadArchive: errorName = @"ERAR_BAD_ARCHIVE"; detail = NSLocalizedStringFromTableInBundle(@"File is not a valid RAR archive", @"UnrarKit", _resources, @"Error detail string"); break; case URKErrorCodeUnknownFormat: errorName = @"ERAR_UNKNOWN_FORMAT"; detail = NSLocalizedStringFromTableInBundle(@"RAR headers encrypted in unknown format", @"UnrarKit", _resources, @"Error detail string"); break; case URKErrorCodeOpen: errorName = @"ERAR_EOPEN"; detail = NSLocalizedStringFromTableInBundle(@"Failed to open a reference to the file", @"UnrarKit", _resources, @"Error detail string"); break; case URKErrorCodeCreate: errorName = @"ERAR_ECREATE"; detail = NSLocalizedStringFromTableInBundle(@"Failed to create the target directory for extraction", @"UnrarKit", _resources, @"Error detail string"); break; case URKErrorCodeClose: errorName = @"ERAR_ECLOSE"; detail = NSLocalizedStringFromTableInBundle(@"Error encountered while closing the archive", @"UnrarKit", _resources, @"Error detail string"); break; case URKErrorCodeRead: errorName = @"ERAR_EREAD"; detail = NSLocalizedStringFromTableInBundle(@"Error encountered while reading the archive", @"UnrarKit", _resources, @"Error detail string"); break; case URKErrorCodeWrite: errorName = @"ERAR_EWRITE"; detail = NSLocalizedStringFromTableInBundle(@"Error encountered while writing a file to disk", @"UnrarKit", _resources, @"Error detail string"); break; case URKErrorCodeSmall: errorName = @"ERAR_SMALL_BUF"; detail = NSLocalizedStringFromTableInBundle(@"Buffer too small to contain entire comments", @"UnrarKit", _resources, @"Error detail string"); break; case URKErrorCodeUnknown: errorName = @"ERAR_UNKNOWN"; detail = NSLocalizedStringFromTableInBundle(@"An unknown error occurred", @"UnrarKit", _resources, @"Error detail string"); break; case URKErrorCodeMissingPassword: errorName = @"ERAR_MISSING_PASSWORD"; detail = NSLocalizedStringFromTableInBundle(@"No password given to unlock a protected archive", @"UnrarKit", _resources, @"Error detail string"); break; case URKErrorCodeArchiveNotFound: errorName = @"ERAR_ARCHIVE_NOT_FOUND"; detail = NSLocalizedStringFromTableInBundle(@"Unable to find the archive", @"UnrarKit", _resources, @"Error detail string"); break; case URKErrorCodeUserCancelled: errorName = @"ERAR_USER_CANCELLED"; detail = NSLocalizedStringFromTableInBundle(@"User cancelled the operation in progress", @"UnrarKit", _resources, @"Error detail string"); break; case URKErrorCodeStringConversion: errorName = @"ERAR_UTF8_PATH_CONVERSION"; detail = NSLocalizedStringFromTableInBundle(@"Error converting a string to UTF-8", @"UnrarKit", _resources, @"Error detail string"); break; default: errorName = [NSString stringWithFormat:@"Unknown (%ld)", (long)errorCode]; detail = [NSString localizedStringWithFormat:NSLocalizedStringFromTableInBundle(@"Unknown error encountered (code %ld)", @"UnrarKit", _resources, @"Error detail string"), (long)errorCode]; break; } *errorDetail = detail; return errorName; } - (BOOL)assignError:(NSError * __autoreleasing *)error code:(NSInteger)errorCode errorName:(NSString * __autoreleasing *)outErrorName { return [self assignError:error code:errorCode underlyer:nil errorName:outErrorName]; } - (BOOL)assignError:(NSError * __autoreleasing *)error code:(NSInteger)errorCode underlyer:(NSError *)underlyingError errorName:(NSString * __autoreleasing *)outErrorName { NSAssert(outErrorName, @"An out variable for errorName must be provided"); NSString *errorDetail = nil; *outErrorName = [self errorNameForErrorCode:errorCode detail:&errorDetail]; if (error) { NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithDictionary: @{NSLocalizedFailureReasonErrorKey: *outErrorName, NSLocalizedDescriptionKey: errorDetail, NSLocalizedRecoverySuggestionErrorKey: errorDetail}]; if (self.fileURL) { userInfo[NSURLErrorKey] = self.fileURL; } if (underlyingError) { userInfo[NSUnderlyingErrorKey] = underlyingError; } *error = [NSError errorWithDomain:URKErrorDomain code:errorCode userInfo:userInfo]; } return NO; } - (BOOL)headerContainsErrors:(NSError * __autoreleasing *)error { URKCreateActivity("-headerContainsErrors:"); BOOL isPasswordProtected = self.header->Flags & 0x04; if (isPasswordProtected && !self.password) { NSString *errorName = nil; [self assignError:error code:ERAR_MISSING_PASSWORD errorName:&errorName]; URKLogError("Password protected and no password specified: %{public}@ (%d)", errorName, ERAR_MISSING_PASSWORD); return YES; } return NO; } - (NSProgress *)beginProgressOperation:(unsigned long long)totalUnitCount { URKCreateActivity("-beginProgressOperation:"); NSProgress *progress; progress = self.progress; self.progress = nil; if (!progress) { progress = [[NSProgress alloc] initWithParent:[NSProgress currentProgress] userInfo:nil]; } if (totalUnitCount > 0) { progress.totalUnitCount = totalUnitCount; } progress.cancellable = YES; progress.pausable = NO; return progress; } + (NSURL *)firstVolumeURL:(NSURL *)volumeURL { URKCreateActivity("+firstVolumeURL:"); URKLogDebug("Checking if the file is part of a multi-volume archive..."); if (!volumeURL) { URKLogError("+firstVolumeURL: nil volumeURL passed") } NSString *volumePath = volumeURL.path; NSError *regexError = nil; NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"(.part)([0-9]+)(.rar)$" options:NSRegularExpressionCaseInsensitive error:®exError]; if (!regex) { URKLogError("Error constructing filename regex") return nil; } NSString *firstVolumePath = nil; // Check if it's following the current convention, like "Archive.part03.rar" NSTextCheckingResult *match = [regex firstMatchInString:volumePath options:0 range:NSMakeRange(0, volumePath.length)]; if (match) { URKLogDebug("The file is part of a multi-volume archive"); NSRange numberRange = [match rangeAtIndex:2]; NSString * partOne = [[@"" stringByPaddingToLength:numberRange.length - 1 withString:@"0" startingAtIndex:0] stringByAppendingString:@"1"]; NSString * regexTemplate = [NSString stringWithFormat:@"$1%@$3", partOne]; firstVolumePath = [regex stringByReplacingMatchesInString:volumePath options:0 range:NSMakeRange(0, volumePath.length) withTemplate:regexTemplate]; } // It still might be a multivolume archive. Check for the legacy naming convention, like "Archive.r03" else { // After rXX, rar uses r-z and symbols like {}|~... so accepting anything but a number NSError *legacyRegexError = nil; regex = [NSRegularExpression regularExpressionWithPattern:@"(\\.[^0-9])([0-9]+)$" options:NSRegularExpressionCaseInsensitive error:&legacyRegexError]; if (!regex) { URKLogError("Error constructing legacy filename regex") return nil; } match = [regex firstMatchInString:volumePath options:0 range:NSMakeRange(0, volumePath.length)]; if (match) { URKLogDebug("The archive is part of a legacy volume"); firstVolumePath = [[volumePath stringByDeletingPathExtension] stringByAppendingPathExtension:@"rar"]; } } // If it's a volume of either naming convention, use it if (firstVolumePath) { if ([[NSFileManager defaultManager] fileExistsAtPath:firstVolumePath]) { URKLogDebug("First volume part %{public}@ found. Using as the main archive", firstVolumePath); return [NSURL fileURLWithPath:firstVolumePath]; } else { URKLogInfo("First volume part not found: %{public}@. Skipping first volume selection", firstVolumePath); return nil; } } return volumeURL; } - (URKReadHeaderLoopAction) readHeader:(int *)returnCode info:(URKFileInfo *__autoreleasing *)info { NSAssert(returnCode != NULL, @"otherReturnCode argument is required"); NSAssert(info != NULL, @"info argument is required"); URKLogDebug("Reading RAR header"); *returnCode = RARReadHeaderEx(self.rarFile, self.header); URKLogDebug("Reading file info from RAR header"); *info = [URKFileInfo fileInfo:self.header]; URKLogDebug("RARReadHeaderEx returned %d", *returnCode); URKReadHeaderLoopAction result; switch (*returnCode) { case ERAR_SUCCESS: result = URKReadHeaderLoopActionContinueReading; break; case ERAR_END_ARCHIVE: URKLogDebug("Successful return code from RARReadHeaderEx"); result = URKReadHeaderLoopActionStopReading; break; case ERAR_BAD_DATA: if (self.ignoreCRCMismatches) { URKLogError("Ignoring CRC mismatch in %{public}@", (*info).filename); result = URKReadHeaderLoopActionContinueReading; } else { URKLogError("CRC mismatch when reading %{public}@. To read the archive and ignore CRC mismatches, use -checkDataIntegrityIgnoringCRCMismatches:", (*info).filename); result = URKReadHeaderLoopActionStopReading; } break; default: result = URKReadHeaderLoopActionStopReading; break; } if (result == URKReadHeaderLoopActionContinueReading && [self.lastFilepath isEqualToString:(*info).filename] && [self.lastArchivePath isEqualToString:(*info).archiveName]) { URKLogInfo("Same header returned twice. Presuming archive done being read. Probably a bad CRC") result = URKReadHeaderLoopActionStopReading; } self.lastFilepath = (result == URKReadHeaderLoopActionStopReading ? nil : (*info).filename); self.lastArchivePath = (result == URKReadHeaderLoopActionStopReading ? nil : (*info).archiveName); return result; } - (BOOL)didReturnSuccessfully:(int)returnCode { return (returnCode == ERAR_SUCCESS || returnCode == ERAR_END_ARCHIVE || (returnCode == ERAR_BAD_DATA && self.ignoreCRCMismatches)); } - (BOOL)hasBadCRC:(int)returnCode { return returnCode == ERAR_BAD_DATA && !self.ignoreCRCMismatches; } @end