From 79a32ccf66ced62028687cf59d25fdf859493ee6 Mon Sep 17 00:00:00 2001 From: Dov Frankel Date: Mon, 23 Sep 2019 16:33:21 -0400 Subject: [PATCH] Implemented a way for users to opt into ignoring mismatched CRCs in an archive header (Issue #82), and consistently replaced all references to 'self' inside blocks with 'welf' --- Classes/URKArchive.h | 21 ++- Classes/URKArchive.mm | 295 ++++++++++++++++++++++--------- Classes/URKFileInfo.m | 2 +- Tests/CheckDataTests.m | 66 +++++++ Tests/ExtractBufferedDataTests.m | 57 ++++++ Tests/ExtractDataTests.m | 35 +++- Tests/ExtractFilesTests.m | 81 ++++++++- Tests/IterateFileInfoTests.m | 45 ++++- Tests/ListFileInfoTests.m | 37 +++- Tests/ListFilenamesTests.m | 41 ++++- Tests/ListVolumesTests.m | 33 ++++ Tests/PerformOnDataTests.m | 48 ++++- Tests/PerformOnFilesTests.m | 42 +++++ Tests/Test Data/README.md | 275 ++++++++++++++++++++++++++++ Tests/URKArchiveTestCase.m | 1 + 15 files changed, 978 insertions(+), 101 deletions(-) create mode 100644 Tests/Test Data/README.md diff --git a/Classes/URKArchive.h b/Classes/URKArchive.h index bd064cb..eab6571 100644 --- a/Classes/URKArchive.h +++ b/Classes/URKArchive.h @@ -477,11 +477,30 @@ extern NSString *URKErrorDomain; /** Extract each file in the archive, checking whether the data matches the CRC checksum stored at the time it was written - + @return YES if the data is all correct, false if any check failed */ - (BOOL)checkDataIntegrity; +/** + Extract each file in the archive, checking whether the data matches the CRC checksum + stored at the time it was written. If any file doesn't match, run the given block + to allow the API consumer to decide whether to ignore mismatches. NOTE: This may be a + security risk. The block is intended to prompt the user, which is why it's forced onto + the main thread, rather than making a design-time decision + + @param ignoreCRCMismatches This block, called on the main thread, allows a consuming API to + prompt the user whether or not he'd like to ignore CRC mismatches. + This block is called the first time a CRC mismatch is detected, if + at all. It won't be called if all CRCs match. If this returns YES, + then all further CRC mismatches will be ignored for the + archive instance + + @return YES if the data is all correct and/or the block returns YES; returns false if + any check failed and the given block also returns NO + */ +- (BOOL)checkDataIntegrityIgnoringCRCMismatches:(BOOL(^)(void))ignoreCRCMismatches; + /** Extract a particular file, to determine if its data matches the CRC checksum stored at the time it written diff --git a/Classes/URKArchive.mm b/Classes/URKArchive.mm index d00c3b8..6dcb2d8 100644 --- a/Classes/URKArchive.mm +++ b/Classes/URKArchive.mm @@ -28,6 +28,11 @@ static NSBundle *_resources = nil; +typedef enum : NSUInteger { + URKReadHeaderLoopActionStopReading, + URKReadHeaderLoopActionContinueReading, +} URKReadHeaderLoopAction; + @interface URKArchive () @@ -46,6 +51,10 @@ - (instancetype)initWithFile:(NSURL *)fileURL password:(NSString*)password error @property (strong) NSObject *threadLock; +@property (assign) NSString *lastArchivePath; +@property (assign) NSString *lastFilepath; +@property (assign) BOOL ignoreCRCMismatches; + @end @@ -160,6 +169,10 @@ - (instancetype)initWithFile:(NSURL *)fileURL password:(NSString*)password error 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); @@ -508,8 +521,7 @@ - (BOOL)extractFilesTo:(NSString *)filePath URKLogInfo("Extracting to %{public}@", filePath); URKLogDebug("Reading through RAR header looking for files..."); - while ((RHCode = RARReadHeaderEx(welf.rarFile, welf.header)) == ERAR_SUCCESS) { - fileInfo = [URKFileInfo fileInfo:welf.header]; + 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 @@ -517,7 +529,7 @@ - (BOOL)extractFilesTo:(NSString *)filePath [progress setUserInfoObject:fileInfo forKey:URKProgressInfoKeyFileInfoExtracting]; - if ([self headerContainsErrors:innerError]) { + if ([welf headerContainsErrors:innerError]) { URKLogError("Header contains an error") result = NO; return; @@ -525,7 +537,7 @@ - (BOOL)extractFilesTo:(NSString *)filePath if (progress.isCancelled) { NSString *errorName = nil; - [self assignError:innerError code:URKErrorCodeUserCancelled errorName:&errorName]; + [welf assignError:innerError code:URKErrorCodeUserCancelled errorName:&errorName]; URKLogInfo("Halted file extraction due to user cancellation: %{public}@", errorName); result = NO; return; @@ -537,7 +549,7 @@ - (BOOL)extractFilesTo:(NSString *)filePath encoding:NSUTF8StringEncoding]; if (!utf8ConversionSucceeded) { NSString *errorName = nil; - [self assignError:innerError code:URKErrorCodeStringConversion errorName:&errorName]; + [welf assignError:innerError code:URKErrorCodeStringConversion errorName:&errorName]; URKLogError("Error converting file to UTF-8 (buffer too short?)"); result = NO; return; @@ -550,12 +562,13 @@ - (BOOL)extractFilesTo:(NSString *)filePath }; RARSetCallback(welf.rarFile, AllowCancellationCallbackProc, (long)shouldCancelBlock); - if ((PFCode = RARProcessFile(welf.rarFile, RAR_EXTRACT, cFilePath, NULL)) != 0) { + 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; - [self assignError:innerError code:errorCode errorName:&errorName]; + [welf assignError:innerError code:errorCode errorName:&errorName]; URKLogError("Error extracting file: %{public}@ (%ld)", errorName, (long)errorCode); result = NO; return; @@ -582,14 +595,14 @@ - (BOOL)extractFilesTo:(NSString *)filePath } RARSetCallback(welf.rarFile, NULL, NULL); - - if (RHCode != ERAR_SUCCESS && RHCode != ERAR_END_ARCHIVE) { + + if (![welf didReturnSuccessfully:RHCode]) { NSString *errorName = nil; - [self assignError:innerError code:RHCode errorName:&errorName]; + [welf assignError:innerError code:RHCode errorName:&errorName]; URKLogError("Error reading file header: %{public}@ (%d)", errorName, RHCode); result = NO; } - + if (progressBlock) { progressBlock(fileInfo, 1.0); } @@ -642,14 +655,12 @@ - (NSData *)extractDataFromFile:(NSString *)filePath URKFileInfo *fileInfo; URKLogDebug("Reading through RAR header looking for files..."); - while ((RHCode = RARReadHeaderEx(welf.rarFile, welf.header)) == ERAR_SUCCESS) { - if ([self headerContainsErrors:innerError]) { + while ([welf readHeader:&RHCode info:&fileInfo] == URKReadHeaderLoopActionContinueReading) { + if ([welf headerContainsErrors:innerError]) { URKLogError("Header contains an error") return; } - fileInfo = [URKFileInfo fileInfo:welf.header]; - if ([fileInfo.filename isEqualToString:filePath]) { URKLogDebug("Extracting %{public}@", fileInfo.filename); break; @@ -658,7 +669,7 @@ - (NSData *)extractDataFromFile:(NSString *)filePath URKLogDebug("Skipping %{public}@", fileInfo.filename); if ((PFCode = RARProcessFileW(welf.rarFile, RAR_SKIP, NULL, NULL)) != 0) { NSString *errorName = nil; - [self assignError:innerError code:(NSInteger)PFCode errorName:&errorName]; + [welf assignError:innerError code:(NSInteger)PFCode errorName:&errorName]; URKLogError("Error skipping file: %{public}@ (%d)", errorName, PFCode); return; } @@ -667,7 +678,7 @@ - (NSData *)extractDataFromFile:(NSString *)filePath if (RHCode != ERAR_SUCCESS) { NSString *errorName = nil; - [self assignError:innerError code:RHCode errorName:&errorName]; + [welf assignError:innerError code:RHCode errorName:&errorName]; URKLogError("Error reading file header: %{public}@ (%d)", errorName, RHCode); return; } @@ -715,14 +726,14 @@ - (NSData *)extractDataFromFile:(NSString *)filePath if (progress.isCancelled) { NSString *errorName = nil; - [self assignError:innerError code:URKErrorCodeUserCancelled errorName:&errorName]; + [welf assignError:innerError code:URKErrorCodeUserCancelled errorName:&errorName]; URKLogInfo("Returning nil data from extraction due to user cancellation: %{public}@", errorName); return; } - if (PFCode != 0) { + if (![welf didReturnSuccessfully:PFCode]) { NSString *errorName = nil; - [self assignError:innerError code:(NSInteger)PFCode errorName:&errorName]; + [welf assignError:innerError code:(NSInteger)PFCode errorName:&errorName]; URKLogError("Error extracting file data: %{public}@ (%d)", errorName, PFCode); return; } @@ -813,27 +824,36 @@ - (BOOL)performOnDataInArchive:(void (^)(URKFileInfo *, NSData *, BOOL *))action BOOL stop = NO; - NSProgress *progress = [self beginProgressOperation:totalSize.longLongValue]; + NSProgress *progress = [welf beginProgressOperation:totalSize.longLongValue]; URKLogDebug("Reading through RAR header looking for files..."); - while ((RHCode = RARReadHeaderEx(welf.rarFile, welf.header)) == 0) { + + URKFileInfo *info = nil; + while ([welf readHeader:&RHCode info:&info] == URKReadHeaderLoopActionContinueReading) { if (stop || progress.isCancelled) { URKLogDebug("Action dictated an early stop"); return; } - if ([self headerContainsErrors:innerError]) { + if ([welf headerContainsErrors:innerError]) { URKLogError("Header contains an error") return; } - URKFileInfo *info = [URKFileInfo fileInfo:welf.header]; URKLogDebug("Performing action on %{public}@", info.filename); // Empty file, or a directory - if (info.uncompressedSize == 0) { + 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; } @@ -845,9 +865,9 @@ - (BOOL)performOnDataInArchive:(void (^)(URKFileInfo *, NSData *, BOOL *))action URKLogInfo("Processing file..."); PFCode = RARProcessFile(welf.rarFile, RAR_TEST, NULL, NULL); - if (PFCode != 0) { + if (![welf didReturnSuccessfully:PFCode]) { NSString *errorName = nil; - [self assignError:innerError code:(NSInteger)PFCode errorName:&errorName]; + [welf assignError:innerError code:(NSInteger)PFCode errorName:&errorName]; URKLogError("Error processing file: %{public}@ (%d)", errorName, PFCode); return; } @@ -861,14 +881,14 @@ - (BOOL)performOnDataInArchive:(void (^)(URKFileInfo *, NSData *, BOOL *))action if (progress.isCancelled) { NSString *errorName = nil; - [self assignError:innerError code:URKErrorCodeUserCancelled errorName:&errorName]; + [welf assignError:innerError code:URKErrorCodeUserCancelled errorName:&errorName]; URKLogInfo("Returning NO from performOnData:error: due to user cancellation: %{public}@", errorName); return; } - if (RHCode != ERAR_SUCCESS && RHCode != ERAR_END_ARCHIVE) { + if (![welf didReturnSuccessfully:RHCode]) { NSString *errorName = nil; - [self assignError:innerError code:RHCode errorName:&errorName]; + [welf assignError:innerError code:RHCode errorName:&errorName]; URKLogError("Error reading file header: %{public}@ (%d)", errorName, RHCode); return; } @@ -897,24 +917,22 @@ - (BOOL)extractBufferedDataFromFile:(NSString *)filePath URKLogInfo("Looping through files, looking for %{public}@...", filePath); - while ((RHCode = RARReadHeaderEx(welf.rarFile, welf.header)) == ERAR_SUCCESS) { - if ([self headerContainsErrors:innerError]) { + while ([welf readHeader:&RHCode info:&fileInfo] == URKReadHeaderLoopActionContinueReading) { + if ([welf headerContainsErrors:innerError]) { URKLogDebug("Header contains error") return; } - URKLogDebug("Getting file info from header"); - fileInfo = [URKFileInfo fileInfo:welf.header]; - if ([fileInfo.filename isEqualToString:filePath]) { URKLogDebug("Found desired file"); break; } else { URKLogDebug("Skipping file..."); - if ((PFCode = RARProcessFile(welf.rarFile, RAR_SKIP, NULL, NULL)) != 0) { + PFCode = RARProcessFile(welf.rarFile, RAR_SKIP, NULL, NULL); + if (![welf didReturnSuccessfully:PFCode]) { NSString *errorName = nil; - [self assignError:innerError code:(NSInteger)PFCode errorName:&errorName]; + [welf assignError:innerError code:(NSInteger)PFCode errorName:&errorName]; URKLogError("Failed to skip file: %{public}@ (%d)", errorName, PFCode); return; } @@ -924,9 +942,9 @@ - (BOOL)extractBufferedDataFromFile:(NSString *)filePath long long totalBytes = fileInfo.uncompressedSize; progress.totalUnitCount = totalBytes; - if (RHCode != ERAR_SUCCESS) { + if (![welf didReturnSuccessfully:RHCode]) { NSString *errorName = nil; - [self assignError:innerError code:RHCode errorName:&errorName]; + [welf assignError:innerError code:RHCode errorName:&errorName]; URKLogError("Header read yielded error: %{public}@ (%d)", errorName, RHCode); return; } @@ -965,14 +983,14 @@ - (BOOL)extractBufferedDataFromFile:(NSString *)filePath if (progress.isCancelled) { NSString *errorName = nil; - [self assignError:innerError code:URKErrorCodeUserCancelled errorName:&errorName]; + [welf assignError:innerError code:URKErrorCodeUserCancelled errorName:&errorName]; URKLogError("Buffered data extraction has been cancelled: %{public}@", errorName); return; } - if (PFCode != 0) { + if (![welf didReturnSuccessfully:PFCode]) { NSString *errorName = nil; - [self assignError:innerError code:(NSInteger)PFCode errorName:&errorName]; + [welf assignError:innerError code:(NSInteger)PFCode errorName:&errorName]; URKLogError("Error processing file: %{public}@ (%d)", errorName, PFCode); } } inMode:RAR_OM_EXTRACT error:&actionError]; @@ -1049,7 +1067,7 @@ - (BOOL)validatePassword int RHCode = RARReadHeaderEx(welf.rarFile, welf.header); int PFCode = RARProcessFile(welf.rarFile, RAR_TEST, NULL, NULL); - if ([self headerContainsErrors:innerError]) { + if ([welf headerContainsErrors:innerError]) { if (error.code == ERAR_MISSING_PASSWORD) { URKLogDebug("Password invalidated by header"); passwordIsGood = NO; @@ -1062,13 +1080,18 @@ - (BOOL)validatePassword } if (RHCode == ERAR_MISSING_PASSWORD || PFCode == ERAR_MISSING_PASSWORD - || RHCode == ERAR_BAD_DATA || PFCode == ERAR_BAD_DATA || 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) { @@ -1084,47 +1107,83 @@ - (BOOL)checkDataIntegrity return [self checkDataIntegrityOfFile:(NSString *_Nonnull)nil]; } -- (BOOL)checkDataIntegrityOfFile:(NSString *)filePath +- (BOOL)checkDataIntegrityIgnoringCRCMismatches:(BOOL(^)())ignoreCRCMismatches +{ + int rhCode = [self dataIntegrityCodeOfFile:nil]; + if (rhCode == ERAR_SUCCESS) { + return YES; + } + + if (rhCode == ERAR_BAD_DATA) { + self.ignoreCRCMismatches = ignoreCRCMismatches(); + 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 BOOL corruptDataFound = YES; + __block int RHCode = 0; + __block int PFCode = 0; + + __weak URKArchive *welf = self; NSError *performOnFilesError = nil; - [self performOnFilesInArchive:^(URKFileInfo *fileInfo, BOOL *stop) { + BOOL wasSuccessful = [self performActionWithArchiveOpen:^(NSError **innerError) { URKCreateActivity("Iterating through each file"); - corruptDataFound = NO; // Set inside here so invalid archives are marked as corrupt - if (filePath && ![fileInfo.filename isEqualToString:filePath]) return; - - URKLogDebug("Extracting '%{public}@ to check its CRC...", fileInfo.filename); - NSError *extractError = nil; - NSData *fileData = [self extractData:fileInfo error:&extractError]; - if (!fileData) { - URKLogError("Error extracting %{public}@: %{public}@", fileInfo.filename, extractError); - *stop = YES; - return; - } - - uLong expectedCRC = fileInfo.CRC; - uLong actualCRC = crc32((uLong)0, (const Bytef*)fileData.bytes, (uint)fileData.length); - URKLogDebug("Checking integrity of %{public}@. Expected CRC: %010lu vs. Actual: %010lu", - fileInfo.filename, expectedCRC, actualCRC); - if (expectedCRC != actualCRC) { - corruptDataFound = YES; - URKLogError("Corrupt data found (filename: %{public}@, expected CRC: %010lu, actual CRC: %010lu", - fileInfo.filename, expectedCRC, actualCRC); + 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; + } } - - if (filePath) *stop = YES; - } error:&performOnFilesError]; + } inMode:RAR_OM_EXTRACT error:&performOnFilesError]; + + if (RHCode == ERAR_END_ARCHIVE) { + RHCode = ERAR_SUCCESS; + } if (performOnFilesError) { URKLogError("Error checking data integrity: %{public}@", performOnFilesError); } - return !corruptDataFound; + if (!wasSuccessful) { + URKLogError("Error checking data integrity"); + if (RHCode == ERAR_SUCCESS) { + RHCode = ERAR_UNKNOWN; + } + } + + return RHCode; } @@ -1269,22 +1328,23 @@ - (BOOL)_unrarOpenFile:(NSString *)rarFile inMode:(NSInteger)mode withPassword:( URKLogDebug("Setting archive name..."); - const char *filenameData = (const char *) [rarFile UTF8String]; - self.flags->ArcName = new char[strlen(filenameData) + 1]; - strcpy(self.flags->ArcName, filenameData); - self.flags->OpenMode = (uint)mode; + const char *filenameData = (const char *) [rarFile UTF8String]; + self.flags->ArcName = new char[strlen(filenameData) + 1]; + strcpy(self.flags->ArcName, filenameData); + 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) { + 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; } - if(aPassword != nil) { + if (aPassword != nil) { URKLogDebug("Setting password..."); char cPassword[2048]; @@ -1339,10 +1399,10 @@ - (BOOL) iterateAllFileInfo:(void(^)(URKFileInfo *fileInfo, BOOL *stop))action URKLogDebug("Reading through RAR header looking for files..."); - while ((RHCode = RARReadHeaderEx(welf.rarFile, welf.header)) == 0) { + URKFileInfo *info = nil; + while ([welf readHeader:&RHCode info:&info] == URKReadHeaderLoopActionContinueReading) { URKLogDebug("Calling iterateAllFileInfo handler"); BOOL shouldStop = NO; - URKFileInfo *info = [URKFileInfo fileInfo:welf.header]; action(info, &shouldStop); if (shouldStop) { @@ -1353,15 +1413,15 @@ - (BOOL) iterateAllFileInfo:(void(^)(URKFileInfo *fileInfo, BOOL *stop))action URKLogDebug("Skipping to next file..."); if ((PFCode = RARProcessFile(welf.rarFile, RAR_SKIP, NULL, NULL)) != 0) { NSString *errorName = nil; - [self assignError:innerError code:(NSInteger)PFCode errorName:&errorName]; + [welf assignError:innerError code:(NSInteger)PFCode errorName:&errorName]; URKLogError("Error skipping to next header file: %{public}@ (%d)", errorName, PFCode); return; } } - if (RHCode != ERAR_SUCCESS && RHCode != ERAR_END_ARCHIVE) { + if (![welf didReturnSuccessfully:RHCode]) { NSString *errorName = nil; - [self assignError:innerError code:RHCode errorName:&errorName]; + [welf assignError:innerError code:RHCode errorName:&errorName]; URKLogError("Error reading RAR header: %{public}@ (%d)", errorName, RHCode); } } inMode:RAR_OM_LIST_INCSPLIT error:error]; @@ -1641,4 +1701,71 @@ + (NSURL *)firstVolumeURL:(NSURL *)volumeURL { 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 diff --git a/Classes/URKFileInfo.m b/Classes/URKFileInfo.m index 24232eb..6b1c083 100644 --- a/Classes/URKFileInfo.m +++ b/Classes/URKFileInfo.m @@ -40,7 +40,7 @@ - (instancetype)initWithFileHeader:(struct RARHeaderDataEx *)fileHeader _isEncryptedWithPassword = fileHeader->Flags & (1 << 2); //_fileHasComment = fileHeader->Flags & (1 << 3) - _isDirectory = fileHeader->Flags & RHDF_DIRECTORY; + _isDirectory = (fileHeader->Flags & RHDF_DIRECTORY) ? YES : NO; } return self; diff --git a/Tests/CheckDataTests.m b/Tests/CheckDataTests.m index 6228c67..f8fa0a4 100644 --- a/Tests/CheckDataTests.m +++ b/Tests/CheckDataTests.m @@ -91,4 +91,70 @@ - (void)testCheckDataIntegrityForFile_ModifiedCRC { XCTAssertFalse(success, @"Data integrity check passed for archive with modified CRC"); } +- (void)testCheckDataIntegrityIgnoringCRCMismatches { + NSArray *testArchives = @[@"Test Archive.rar", + @"Test Archive (Password).rar", + @"Test Archive (Header Password).rar"]; + + for (NSString *testArchiveName in testArchives) { + NSLog(@"Testing data integrity of file in archive %@", testArchiveName); + NSURL *testArchiveURL = self.testFileURLs[testArchiveName]; + NSString *password = ([testArchiveName rangeOfString:@"Password"].location != NSNotFound + ? @"password" + : nil); + URKArchive *archive = [[URKArchive alloc] initWithURL:testArchiveURL password:password error:nil]; + + __block BOOL blockInvoked = NO; + BOOL success = [archive checkDataIntegrityIgnoringCRCMismatches:^BOOL{ + blockInvoked = YES; + return NO; + }]; + + XCTAssertTrue(success, @"Data integrity check failed for %@", testArchiveName); + XCTAssertFalse(blockInvoked, @"Block prompting whether to ignore CRC mismatches should not have been called"); + } +} + +- (void)testCheckDataIntegrityIgnoringCRCMismatches_NotAnArchive { + NSURL *testArchiveURL = self.testFileURLs[@"Test File B.jpg"]; + URKArchive *archive = [[URKArchive alloc] initWithURL:testArchiveURL error:nil]; + + __block BOOL blockInvoked = NO; + BOOL success = [archive checkDataIntegrityIgnoringCRCMismatches:^BOOL{ + blockInvoked = YES; + return NO; + }]; + + XCTAssertFalse(success, @"Data integrity check passed for non-archive"); + XCTAssertFalse(blockInvoked, @"Block prompting whether to ignore CRC mismatches should not have been called"); +} + +- (void)testCheckDataIntegrityIgnoringCRCMismatches_ModifiedCRC_Ignore { + NSURL *testArchiveURL = self.testFileURLs[@"Modified CRC Archive.rar"]; + URKArchive *archive = [[URKArchive alloc] initWithURL:testArchiveURL error:nil]; + + __block BOOL blockInvoked = NO; + BOOL success = [archive checkDataIntegrityIgnoringCRCMismatches:^BOOL{ + blockInvoked = YES; + return YES; + }]; + + XCTAssertTrue(success, @"Data integrity check failed for archive with modified CRC, when instructed to ignore"); + XCTAssertTrue(blockInvoked, @"Block prompting whether to ignore CRC mismatches should have been called"); +} + +- (void)testCheckDataIntegrityIgnoringCRCMismatches_ModifiedCRC_DontIgnore { + NSURL *testArchiveURL = self.testFileURLs[@"Modified CRC Archive.rar"]; + URKArchive *archive = [[URKArchive alloc] initWithURL:testArchiveURL error:nil]; + + __block BOOL blockInvoked = NO; + BOOL success = [archive checkDataIntegrityIgnoringCRCMismatches:^BOOL{ + blockInvoked = YES; + return NO; + }]; + + XCTAssertFalse(success, @"Data integrity check passed for archive with modified CRC"); + XCTAssertTrue(blockInvoked, @"Block prompting whether to ignore CRC mismatches should have been called"); +} + @end diff --git a/Tests/ExtractBufferedDataTests.m b/Tests/ExtractBufferedDataTests.m index 69fb9d0..634601b 100644 --- a/Tests/ExtractBufferedDataTests.m +++ b/Tests/ExtractBufferedDataTests.m @@ -52,6 +52,63 @@ - (void)testExtractBufferedData @"File extracted in buffer not returned correctly"); } +- (void)testExtractBufferedData_ModifiedCRC +{ + NSURL *archiveURL = self.testFileURLs[@"Modified CRC Archive.rar"]; + NSString *extractedFile = @"README.md"; + URKArchive *archive = [[URKArchive alloc] initWithURL:archiveURL error:nil]; + + NSError *error = nil; + NSMutableData *reconstructedFile = [NSMutableData data]; + BOOL success = [archive extractBufferedDataFromFile:extractedFile + error:&error + action: + ^(NSData *dataChunk, CGFloat percentDecompressed) { + NSLog(@"Decompressed: %f%%", percentDecompressed); + [reconstructedFile appendBytes:dataChunk.bytes + length:dataChunk.length]; + }]; + + XCTAssertFalse(success, @"Failed to read buffered data"); + XCTAssertNotNil(error, @"Error reading buffered data"); + + NSData *originalFile = [NSData dataWithContentsOfURL:self.testFileURLs[extractedFile]]; + XCTAssertTrue([originalFile isEqualToData:reconstructedFile], + @"File extracted in buffer not returned correctly"); +} + +- (void)testExtractBufferedData_ModifiedCRC_IgnoringMismatches +{ + NSURL *archiveURL = self.testFileURLs[@"Modified CRC Archive.rar"]; + NSString *extractedFile = @"README.md"; + URKArchive *archive = [[URKArchive alloc] initWithURL:archiveURL error:nil]; + + BOOL checkIntegritySuccess = [archive checkDataIntegrityIgnoringCRCMismatches:^BOOL{ + return YES; + }]; + + XCTAssertTrue(checkIntegritySuccess, @"Data integrity check failed for archive with modified CRC, when instructed to ignore"); + + NSError *error = nil; + NSMutableData *reconstructedFile = [NSMutableData data]; + BOOL success = [archive extractBufferedDataFromFile:extractedFile + error:&error + action: + ^(NSData *dataChunk, CGFloat percentDecompressed) { + NSLog(@"Decompressed: %f%%", percentDecompressed); + [reconstructedFile appendBytes:dataChunk.bytes + length:dataChunk.length]; + }]; + + XCTAssertTrue(success, @"Failed to read buffered data"); + XCTAssertNil(error, @"Error reading buffered data"); + XCTAssertGreaterThan(reconstructedFile.length, 0, @"No data returned"); + + NSData *originalFile = [NSData dataWithContentsOfURL:self.testFileURLs[extractedFile]]; + XCTAssertTrue([originalFile isEqualToData:reconstructedFile], + @"File extracted in buffer not returned correctly"); +} + #if !TARGET_OS_IPHONE && __MAC_OS_X_VERSION_MIN_REQUIRED >= 101200 - (void)testExtractBufferedData_VeryLarge { diff --git a/Tests/ExtractDataTests.m b/Tests/ExtractDataTests.m index d6a1c03..88e0b7a 100644 --- a/Tests/ExtractDataTests.m +++ b/Tests/ExtractDataTests.m @@ -19,7 +19,7 @@ - (void)testExtractData @"Test Archive (Header Password).rar"]; NSSet *expectedFileSet = [self.testFileURLs keysOfEntriesPassingTest:^BOOL(NSString *key, id obj, BOOL *stop) { - return ![key hasSuffix:@"rar"]; + return ![key hasSuffix:@"rar"] && ![key hasSuffix:@"md"]; }]; NSArray *expectedFiles = [[expectedFileSet allObjects] sortedArrayUsingSelector:@selector(compare:)]; @@ -60,7 +60,7 @@ - (void)testExtractData - (void)testExtractData_Unicode { NSSet *expectedFileSet = [self.unicodeFileURLs keysOfEntriesPassingTest:^BOOL(NSString *key, id obj, BOOL *stop) { - return ![key hasSuffix:@"rar"]; + return ![key hasSuffix:@"rar"] && ![key hasSuffix:@"md"]; }]; NSArray *expectedFiles = [[expectedFileSet allObjects] sortedArrayUsingSelector:@selector(compare:)]; @@ -121,4 +121,35 @@ - (void)testExtractData_InvalidArchive XCTAssertEqual(error.code, URKErrorCodeBadArchive, @"Unexpected error code returned"); } +- (void)testExtractData_ModifiedCRC +{ + URKArchive *archive = [[URKArchive alloc] initWithURL:self.testFileURLs[@"Modified CRC Archive.rar"] error:nil]; + + NSError *error = nil; + NSData *data = [archive extractDataFromFile:@"README.md" error:&error]; + + XCTAssertNotNil(error, @"Extract data for invalid archive succeeded"); + XCTAssertNil(data, @"Data returned for invalid archive"); + XCTAssertEqual(error.code, URKErrorCodeBadData, @"Unexpected error code returned"); +} + +- (void)testExtractData_ModifiedCRC_IgnoringMismatches +{ + URKArchive *archive = [[URKArchive alloc] initWithURL:self.testFileURLs[@"Modified CRC Archive.rar"] error:nil]; + + BOOL checkIntegritySuccess = [archive checkDataIntegrityIgnoringCRCMismatches:^BOOL{ + return YES; + }]; + + XCTAssertTrue(checkIntegritySuccess, @"Data integrity check failed for archive with modified CRC, when instructed to ignore"); + + NSError *error = nil; + NSData *data = [archive extractDataFromFile:@"README.md" error:&error]; + NSData *expectedData = [NSData dataWithContentsOfURL:self.testFileURLs[@"README.md"]]; + + XCTAssertNil(error, @"Extract data for invalid archive succeeded"); + XCTAssertNotNil(data, @"Data returned for invalid archive"); + XCTAssertEqualObjects(data, expectedData); +} + @end diff --git a/Tests/ExtractFilesTests.m b/Tests/ExtractFilesTests.m index 2f2bde7..0845161 100644 --- a/Tests/ExtractFilesTests.m +++ b/Tests/ExtractFilesTests.m @@ -21,7 +21,7 @@ - (void)testExtractFiles @"Test Archive (Header Password).rar"]; NSSet *expectedFileSet = [self.testFileURLs keysOfEntriesPassingTest:^BOOL(NSString *key, id obj, BOOL *stop) { - return ![key hasSuffix:@"rar"]; + return ![key hasSuffix:@"rar"] && ![key hasSuffix:@"md"]; }]; NSArray *expectedFiles = [[expectedFileSet allObjects] sortedArrayUsingSelector:@selector(compare:)]; @@ -142,7 +142,7 @@ - (void)testExtractFiles_RAR5 - (void)testExtractFiles_Unicode { NSSet *expectedFileSet = [self.unicodeFileURLs keysOfEntriesPassingTest:^BOOL(NSString *key, id obj, BOOL *stop) { - return ![key hasSuffix:@"rar"]; + return ![key hasSuffix:@"rar"] && ![key hasSuffix:@"md"]; }]; NSArray *expectedFiles = [[expectedFileSet allObjects] sortedArrayUsingSelector:@selector(compare:)]; @@ -240,4 +240,81 @@ - (void)testExtractFiles_InvalidArchive XCTAssertFalse(dirExists, @"Directory successfully created for invalid archive"); } +- (void)testExtractFiles_ModifiedCRC +{ + NSFileManager *fm = [NSFileManager defaultManager]; + + URKArchive *archive = [[URKArchive alloc] initWithURL:self.testFileURLs[@"Modified CRC Archive.rar"] error:nil]; + + NSString *extractDirectory = [self randomDirectoryWithPrefix:@"ExtractInvalidArchive"]; + NSURL *extractURL = [self.tempDirectory URLByAppendingPathComponent:extractDirectory]; + + NSError *error = nil; + BOOL success = [archive extractFilesTo:extractURL.path + overwrite:NO + error:&error]; + BOOL dirExists = [fm fileExistsAtPath:extractURL.path]; + + XCTAssertFalse(success, @"Extract invalid archive succeeded"); + XCTAssertEqual(error.code, URKErrorCodeBadData, @"Unexpected error code returned"); + XCTAssertFalse(dirExists, @"Directory successfully created for invalid archive"); +} + +- (void)testExtractFiles_ModifiedCRC_IgnoreCRCMismatches +{ + NSFileManager *fm = [NSFileManager defaultManager]; + + URKArchive *archive = [[URKArchive alloc] initWithURL:self.testFileURLs[@"Modified CRC Archive.rar"] error:nil]; + + BOOL fileHasIntegrity = [archive checkDataIntegrityIgnoringCRCMismatches:^BOOL{ + return YES; + }]; + + XCTAssertTrue(fileHasIntegrity); + + NSString *extractDirectory = [self randomDirectoryWithPrefix:@"ExtractInvalidArchive"]; + NSURL *extractURL = [self.tempDirectory URLByAppendingPathComponent:extractDirectory]; + + NSError *error = nil; + BOOL success = [archive extractFilesTo:extractURL.path + overwrite:NO + error:&error]; + + XCTAssertTrue(success, @"CRC mismatch not ignored"); + XCTAssertNil(error, @"Unexpected error code returned"); + + BOOL dirExists = [fm fileExistsAtPath:extractURL.path]; + XCTAssertTrue(dirExists, @"Directory successfully created for invalid archive"); + + NSURL *extractedFileURL = [extractURL URLByAppendingPathComponent:@"README.md"]; + XCTAssertTrue([extractedFileURL checkResourceIsReachableAndReturnError:NULL]); +} + +- (void)testExtractFiles_ModifiedCRC_DontIgnoreCRCMismatches +{ + NSFileManager *fm = [NSFileManager defaultManager]; + + URKArchive *archive = [[URKArchive alloc] initWithURL:self.testFileURLs[@"Modified CRC Archive.rar"] error:nil]; + + BOOL fileHasIntegrity = [archive checkDataIntegrityIgnoringCRCMismatches:^BOOL{ + return NO; + }]; + + XCTAssertFalse(fileHasIntegrity); + + NSString *extractDirectory = [self randomDirectoryWithPrefix:@"ExtractInvalidArchive"]; + NSURL *extractURL = [self.tempDirectory URLByAppendingPathComponent:extractDirectory]; + + NSError *error = nil; + BOOL success = [archive extractFilesTo:extractURL.path + overwrite:NO + error:&error]; + + XCTAssertFalse(success, @"CRC mismatch not ignored"); + XCTAssertNotNil(error, @"Unexpected error code returned"); + + BOOL dirExists = [fm fileExistsAtPath:extractURL.path]; + XCTAssertFalse(dirExists, @"Directory successfully created for invalid archive"); +} + @end diff --git a/Tests/IterateFileInfoTests.m b/Tests/IterateFileInfoTests.m index a3238a9..28f3cdb 100644 --- a/Tests/IterateFileInfoTests.m +++ b/Tests/IterateFileInfoTests.m @@ -20,7 +20,7 @@ - (void)testIterateFileInfo NSArray *testArchives = @[@"Test Archive.rar", @"Test Archive (Password).rar"]; NSSet *expectedFileSet = [self.testFileURLs keysOfEntriesPassingTest:^BOOL(NSString *key, id obj, BOOL *stop) { - return ![key hasSuffix:@"rar"]; + return ![key hasSuffix:@"rar"] && ![key hasSuffix:@"md"]; }]; NSArray *expectedFiles = [[expectedFileSet allObjects] sortedArrayUsingSelector:@selector(compare:)]; @@ -57,7 +57,7 @@ - (void)testIterateFileInfo - (void)testIterateFileInfo_Unicode { NSSet *expectedFileSet = [self.unicodeFileURLs keysOfEntriesPassingTest:^BOOL(NSString *key, id obj, BOOL *stop) { - return ![key hasSuffix:@"rar"]; + return ![key hasSuffix:@"rar"] && ![key hasSuffix:@"md"]; }]; NSArray *expectedFiles = [[expectedFileSet allObjects] sortedArrayUsingSelector:@selector(compare:)]; @@ -118,7 +118,7 @@ - (void)testIterateFileInfo_HeaderPassword NSArray *testArchives = @[@"Test Archive (Header Password).rar"]; NSSet *expectedFileSet = [self.testFileURLs keysOfEntriesPassingTest:^BOOL(NSString *key, id obj, BOOL *stop) { - return ![key hasSuffix:@"rar"]; + return ![key hasSuffix:@"rar"] && ![key hasSuffix:@"md"]; }]; NSArray *expectedFiles = [[expectedFileSet allObjects] sortedArrayUsingSelector:@selector(compare:)]; @@ -210,5 +210,44 @@ - (void)testIterateFileInfo_InvalidArchive XCTAssertEqual(error.code, URKErrorCodeBadArchive, @"Unexpected error code returned"); } +- (void)testIterateFileInfo_ModifiedCRC +{ + URKArchive *archive = [[URKArchive alloc] initWithURL:self.testFileURLs[@"Modified CRC Archive.rar"] error:nil]; + + NSError *error = nil; + __block BOOL called = NO; + BOOL success = [archive iterateFileInfo:^(URKFileInfo * _Nonnull fileInfo, BOOL * _Nonnull stop) { + called = YES; + } + error:&error]; + + XCTAssertNotNil(error, @"Iteration of invalid archive succeeded"); + XCTAssertFalse(success, @"Iteration for invalid archive succeeded"); + XCTAssertTrue(called, @"Iteration for invalid archive called action block"); + XCTAssertEqual(error.code, URKErrorCodeBadData, @"Unexpected error code returned"); +} + +- (void)testIterateFileInfo_ModifiedCRC_IgnoringMismatches +{ + URKArchive *archive = [[URKArchive alloc] initWithURL:self.testFileURLs[@"Modified CRC Archive.rar"] error:nil]; + + BOOL checkIntegritySuccess = [archive checkDataIntegrityIgnoringCRCMismatches:^BOOL{ + return YES; + }]; + + XCTAssertTrue(checkIntegritySuccess, @"Data integrity check failed for archive with modified CRC, when instructed to ignore"); + + NSError *error = nil; + __block int calledCount = 0; + BOOL iterateSuccess = [archive iterateFileInfo:^(URKFileInfo * _Nonnull fileInfo, BOOL * _Nonnull stop) { + calledCount = 1; + } + error:&error]; + + XCTAssertNil(error, @"Modified CRC not ignored"); + XCTAssertTrue(iterateSuccess, @"Iteration for invalid archive succeeded"); + XCTAssertEqual(1, calledCount, @"Iterated too many times for archive with modified CRC"); +} + @end diff --git a/Tests/ListFileInfoTests.m b/Tests/ListFileInfoTests.m index 1b665fb..139dd2d 100644 --- a/Tests/ListFileInfoTests.m +++ b/Tests/ListFileInfoTests.m @@ -16,7 +16,7 @@ - (void)testListFileInfo { URKArchive *archive = [[URKArchive alloc] initWithURL:self.testFileURLs[@"Test Archive.rar"] error:nil]; NSSet *expectedFileSet = [self.testFileURLs keysOfEntriesPassingTest:^BOOL(NSString *key, id obj, BOOL *stop) { - return ![key hasSuffix:@"rar"]; + return ![key hasSuffix:@"rar"] && ![key hasSuffix:@"md"]; }]; NSArray *expectedFiles = [[expectedFileSet allObjects] sortedArrayUsingSelector:@selector(compare:)]; @@ -83,7 +83,7 @@ - (void)testListFileInfo { - (void)testListFileInfo_Unicode { NSSet *expectedFileSet = [self.unicodeFileURLs keysOfEntriesPassingTest:^BOOL(NSString *key, id obj, BOOL *stop) { - return ![key hasSuffix:@"rar"]; + return ![key hasSuffix:@"rar"] && ![key hasSuffix:@"md"]; }]; NSArray *expectedFiles = [[expectedFileSet allObjects] sortedArrayUsingSelector:@selector(compare:)]; @@ -125,7 +125,7 @@ - (void)testListFileInfo_HeaderPassword NSArray *testArchives = @[@"Test Archive (Header Password).rar"]; NSSet *expectedFileSet = [self.testFileURLs keysOfEntriesPassingTest:^BOOL(NSString *key, id obj, BOOL *stop) { - return ![key hasSuffix:@"rar"]; + return ![key hasSuffix:@"rar"] && ![key hasSuffix:@"md"]; }]; NSArray *expectedFiles = [[expectedFileSet allObjects] sortedArrayUsingSelector:@selector(compare:)]; @@ -184,4 +184,35 @@ - (void)testListFileInfo_InvalidArchive XCTAssertEqual(error.code, URKErrorCodeBadArchive, @"Unexpected error code returned"); } +- (void)testListFileInfo_ModifiedCRC +{ + URKArchive *archive = [[URKArchive alloc] initWithURL:self.testFileURLs[@"Modified CRC Archive.rar"] error:nil]; + + NSError *error = nil; + NSArray *files = [archive listFileInfo:&error]; + + XCTAssertNotNil(error, @"List files of invalid archive succeeded"); + XCTAssertNil(files, @"List returned for invalid archive"); + XCTAssertEqual(error.code, URKErrorCodeBadData, @"Unexpected error code returned"); +} + +- (void)testListFileInfo_ModifiedCRC_IgnoringMismatches +{ + URKArchive *archive = [[URKArchive alloc] initWithURL:self.testFileURLs[@"Modified CRC Archive.rar"] error:nil]; + + BOOL checkIntegritySuccess = [archive checkDataIntegrityIgnoringCRCMismatches:^BOOL{ + return YES; + }]; + + XCTAssertTrue(checkIntegritySuccess, @"Data integrity check failed for archive with modified CRC, when instructed to ignore"); + + NSError *error = nil; + NSArray *files = [archive listFileInfo:&error]; + + XCTAssertNil(error, @"List files of invalid archive succeeded"); + XCTAssertNotNil(files, @"List returned for invalid archive"); + XCTAssertEqual(files.count, 1); + XCTAssertEqualObjects(files[0].filename, @"README.md"); +} + @end diff --git a/Tests/ListFilenamesTests.m b/Tests/ListFilenamesTests.m index 4c94e11..6b87f31 100644 --- a/Tests/ListFilenamesTests.m +++ b/Tests/ListFilenamesTests.m @@ -20,7 +20,7 @@ - (void)testListFilenames NSArray *testArchives = @[@"Test Archive.rar", @"Test Archive (Password).rar"]; NSSet *expectedFileSet = [self.testFileURLs keysOfEntriesPassingTest:^BOOL(NSString *key, id obj, BOOL *stop) { - return ![key hasSuffix:@"rar"]; + return ![key hasSuffix:@"rar"] && ![key hasSuffix:@"md"]; }]; NSArray *expectedFiles = [[expectedFileSet allObjects] sortedArrayUsingSelector:@selector(compare:)]; @@ -53,7 +53,7 @@ - (void)testListFilenames - (void)testListFilenames_Unicode { NSSet *expectedFileSet = [self.unicodeFileURLs keysOfEntriesPassingTest:^BOOL(NSString *key, id obj, BOOL *stop) { - return ![key hasSuffix:@"rar"]; + return ![key hasSuffix:@"rar"] && ![key hasSuffix:@"md"]; }]; NSArray *expectedFiles = [[expectedFileSet allObjects] sortedArrayUsingSelector:@selector(compare:)]; @@ -106,7 +106,7 @@ - (void)testListFilenames_HeaderPassword NSArray *testArchives = @[@"Test Archive (Header Password).rar"]; NSSet *expectedFileSet = [self.testFileURLs keysOfEntriesPassingTest:^BOOL(NSString *key, id obj, BOOL *stop) { - return ![key hasSuffix:@"rar"]; + return ![key hasSuffix:@"rar"] && ![key hasSuffix:@"md"]; }]; NSArray *expectedFiles = [[expectedFileSet allObjects] sortedArrayUsingSelector:@selector(compare:)]; @@ -179,5 +179,40 @@ - (void)testListFilenames_InvalidArchive XCTAssertEqual(error.code, URKErrorCodeBadArchive, @"Unexpected error code returned"); } +- (void)testListFilenames_ModifiedCRC +{ + NSURL *testArchiveURL = self.testFileURLs[@"Modified CRC Archive.rar"]; + URKArchive *archive = [[URKArchive alloc] initWithURL:testArchiveURL error:nil]; + + NSError *error = nil; + NSArray *filesInArchive = [archive listFilenames:&error]; + + XCTAssertNotNil(error, @"Error returned by listFilenames"); + XCTAssertNil(filesInArchive, @"No list of files returned"); + XCTAssertEqual(error.code, URKErrorCodeBadData, @"Unexpected error code returned"); +} + +- (void)testListFilenames_ModifiedCRC_IgnoringMismatch +{ + NSURL *testArchiveURL = self.testFileURLs[@"Modified CRC Archive.rar"]; + URKArchive *archive = [[URKArchive alloc] initWithURL:testArchiveURL error:nil]; + + BOOL checkIntegritySuccess = [archive checkDataIntegrityIgnoringCRCMismatches:^BOOL{ + return YES; + }]; + + XCTAssertTrue(checkIntegritySuccess, @"Data integrity check failed for archive with modified CRC, when instructed to ignore"); + + NSError *error = nil; + NSArray *filesInArchive = [archive listFilenames:&error]; + + XCTAssertNil(error, @"Error returned by listFilenames"); + XCTAssertNotNil(filesInArchive, @"No list of files returned"); + XCTAssertEqual(filesInArchive.count, 1, + @"Incorrect number of files listed in archive"); + + XCTAssertEqualObjects(filesInArchive[0], @"README.md", @"Incorrect filename listed"); +} + @end diff --git a/Tests/ListVolumesTests.m b/Tests/ListVolumesTests.m index c30e902..4e97f89 100644 --- a/Tests/ListVolumesTests.m +++ b/Tests/ListVolumesTests.m @@ -29,6 +29,39 @@ - (void)testSingleVolume { @"Wrong URL returned"); } +- (void)testSingleVolume_ModifiedCRC { + NSURL *testArchiveURL = self.testFileURLs[@"Modified CRC Archive.rar"]; + URKArchive *archive = [[URKArchive alloc] initWithURL:testArchiveURL error:nil]; + + NSError *listVolumesError = nil; + NSArray *volumeURLs = [archive listVolumeURLs:&listVolumesError]; + + XCTAssertNotNil(listVolumesError, @"Error listing volume URLs"); + XCTAssertNil(volumeURLs, @"No URLs returned"); + +} + +- (void)testSingleVolume_ModifiedCRC_IgnoringMismatch { + NSURL *testArchiveURL = self.testFileURLs[@"Modified CRC Archive.rar"]; + URKArchive *archive = [[URKArchive alloc] initWithURL:testArchiveURL error:nil]; + + BOOL checkIntegritySuccess = [archive checkDataIntegrityIgnoringCRCMismatches:^BOOL{ + return YES; + }]; + + XCTAssertTrue(checkIntegritySuccess, @"Data integrity check failed for archive with modified CRC, when instructed to ignore"); + + NSError *listVolumesError = nil; + NSArray *volumeURLs = [archive listVolumeURLs:&listVolumesError]; + + XCTAssertNil(listVolumesError, @"Error listing volume URLs"); + XCTAssertNotNil(volumeURLs, @"No URLs returned"); + XCTAssertEqual(volumeURLs.count, 1, @"Wrong number of volume URLs listed"); + + XCTAssertEqualObjects(volumeURLs[0].lastPathComponent, testArchiveURL.path.lastPathComponent, + @"Wrong URL returned"); +} + #if !TARGET_OS_IPHONE - (void)testMultipleVolume_UseFirstVolume { NSArray *generatedVolumeURLs = [self multiPartArchiveWithName:@"ListVolumesTests-testMultipleVolume_UseFirstVolume.rar"]; diff --git a/Tests/PerformOnDataTests.m b/Tests/PerformOnDataTests.m index 41e63f5..daaa2ab 100644 --- a/Tests/PerformOnDataTests.m +++ b/Tests/PerformOnDataTests.m @@ -19,7 +19,7 @@ - (void)testPerformOnData @"Test Archive (Header Password).rar"]; NSSet *expectedFileSet = [self.testFileURLs keysOfEntriesPassingTest:^BOOL(NSString *key, id obj, BOOL *stop) { - return ![key hasSuffix:@"rar"]; + return ![key hasSuffix:@"rar"] && ![key hasSuffix:@"md"]; }]; NSArray *expectedFiles = [[expectedFileSet allObjects] sortedArrayUsingSelector:@selector(compare:)]; @@ -53,7 +53,7 @@ - (void)testPerformOnData - (void)testPerformOnData_Unicode { NSSet *expectedFileSet = [self.unicodeFileURLs keysOfEntriesPassingTest:^BOOL(NSString *key, id obj, BOOL *stop) { - return ![key hasSuffix:@"rar"]; + return ![key hasSuffix:@"rar"] && ![key hasSuffix:@"md"]; }]; NSArray *expectedFiles = [[expectedFileSet allObjects] sortedArrayUsingSelector:@selector(compare:)]; @@ -79,6 +79,50 @@ - (void)testPerformOnData_Unicode XCTAssertEqual(fileIndex, expectedFiles.count, @"Incorrect number of files encountered"); } +- (void)testPerformOnData_ModifiedCRC +{ + NSURL *testArchiveURL = self.testFileURLs[@"Modified CRC Archive.rar"]; + URKArchive *archive = [[URKArchive alloc] initWithURL:testArchiveURL error:nil]; + + __block BOOL blockCalled = NO; + NSError *error = nil; + + [archive performOnDataInArchive: + ^(URKFileInfo *fileInfo, NSData *fileData, BOOL *stop) { + blockCalled = YES; + } error:&error]; + + XCTAssertNotNil(error, @"Error iterating through files"); + XCTAssertFalse(blockCalled); +} + +- (void)testPerformOnData_ModifiedCRC_IgnoringMismatches +{ + NSURL *testArchiveURL = self.testFileURLs[@"Modified CRC Archive.rar"]; + URKArchive *archive = [[URKArchive alloc] initWithURL:testArchiveURL error:nil]; + + BOOL checkIntegritySuccess = [archive checkDataIntegrityIgnoringCRCMismatches:^BOOL{ + return YES; + }]; + + XCTAssertTrue(checkIntegritySuccess, @"Data integrity check failed for archive with modified CRC, when instructed to ignore"); + + __block NSUInteger fileIndex = 0; + NSError *error = nil; + + [archive performOnDataInArchive: + ^(URKFileInfo *fileInfo, NSData *fileData, BOOL *stop) { + XCTAssertEqual(fileIndex++, 0, @"performOnDataInArchive called too many times"); + XCTAssertEqualObjects(fileInfo.filename, @"README.md"); + NSData *expectedFileData = [NSData dataWithContentsOfURL:self.testFileURLs[@"README.md"]]; + + XCTAssertNotNil(fileData, @"No data extracted"); + XCTAssertTrue([expectedFileData isEqualToData:fileData], @"File data doesn't match original file"); + } error:&error]; + + XCTAssertNil(error, @"Error iterating through files"); +} + #if !TARGET_OS_IPHONE - (void)testPerformOnData_FileMoved { diff --git a/Tests/PerformOnFilesTests.m b/Tests/PerformOnFilesTests.m index a6e36bf..52062da 100644 --- a/Tests/PerformOnFilesTests.m +++ b/Tests/PerformOnFilesTests.m @@ -45,6 +45,48 @@ - (void)testPerformOnFiles } } +- (void)testPerformOnFiles_ModifiedCRC +{ + NSURL *testArchiveURL = self.testFileURLs[@"Modified CRC Archive.rar"]; + NSString *password = nil; + URKArchive *archive = [[URKArchive alloc] initWithURL:testArchiveURL password:password error:nil]; + + __block BOOL blockCalled = NO; + NSError *error = nil; + + [archive performOnFilesInArchive: + ^(URKFileInfo *fileInfo, BOOL *stop) { + blockCalled = YES; + } error:&error]; + + XCTAssertNotNil(error, @"Error iterating through files"); + XCTAssertFalse(blockCalled); +} + +- (void)testPerformOnFiles_ModifiedCRC_MismatchesIgnored +{ + NSURL *testArchiveURL = self.testFileURLs[@"Modified CRC Archive.rar"]; + NSString *password = nil; + URKArchive *archive = [[URKArchive alloc] initWithURL:testArchiveURL password:password error:nil]; + + BOOL checkIntegritySuccess = [archive checkDataIntegrityIgnoringCRCMismatches:^BOOL{ + return YES; + }]; + + XCTAssertTrue(checkIntegritySuccess, @"Data integrity check failed for archive with modified CRC, when instructed to ignore"); + + __block NSUInteger fileIndex = 0; + NSError *error = nil; + + [archive performOnFilesInArchive: + ^(URKFileInfo *fileInfo, BOOL *stop) { + XCTAssertEqual(fileIndex++, 0, @"performOnFilesInArchive called too many times"); + XCTAssertEqualObjects(fileInfo.filename, @"README.md"); + } error:&error]; + + XCTAssertNil(error, @"Error iterating through files"); +} + - (void)testPerformOnFiles_Unicode { NSSet *expectedFileSet = [self.unicodeFileURLs keysOfEntriesPassingTest:^BOOL(NSString *key, id obj, BOOL *stop) { diff --git a/Tests/Test Data/README.md b/Tests/Test Data/README.md new file mode 100644 index 0000000..b5b5e87 --- /dev/null +++ b/Tests/Test Data/README.md @@ -0,0 +1,275 @@ +[![Build Status](https://travis-ci.org/abbeycode/UnrarKit.svg?branch=master)](https://travis-ci.org/abbeycode/UnrarKit) +[![Documentation Coverage](https://img.shields.io/cocoapods/metrics/doc-percent/UnrarKit.svg)](http://cocoadocs.org/docsets/UnrarKit) + +# About + +UnrarKit is here to enable Mac and iOS apps to easily work with RAR files for read-only operations. It is currently based on version 5.2.1 of the [UnRAR library](http://www.rarlab.com/rar/unrarsrc-5.2.1.tar.gz). + +There is a main project, with unit tests, and a basic iOS example project, which demonstrates how to use the library. To see all of these, open the main workspace file. + +I'm always open to improvements, so please submit your pull requests, or [create issues](https://github.com/abbeycode/UnrarKit/issues) for someone else to implement. + + +# Installation + +UnrarKit supports both [CocoaPods](https://cocoapods.org/) and [Carthage](https://github.com/Carthage/Carthage). CocoaPods does not support dynamic framework targets (as of v0.39.0), so in that case, please use Carthage. + +Cartfile: + + github "abbeycode/UnrarKit" + +Podfile: + + pod "UnrarKit" + +# Example Usage + +```Objective-C +NSError *archiveError = nil; +URKArchive *archive = [[URKArchive alloc] initWithPath:@"An Archive.rar" error:&archiveError]; +NSError *error = nil; +``` + +## Listing the file names in an archive +```Objective-C +NSArray *filesInArchive = [archive listFilenames:&error]; +for (NSString *name in filesInArchive) { + NSLog(@"Archived file: %@", name); +} +``` + +## Listing the file details in an archive +```Objective-C +NSArray *fileInfosInArchive = [archive listFileInfo:&error]; +for (URKFileInfo *info in fileInfosInArchive) { + NSLog(@"Archive name: %@ | File name: %@ | Size: %lld", info.archiveName, info.filename, info.uncompressedSize); +} +``` + +## Working with passwords +```Objective-C +NSArray *fileInfosInArchive = [archive listFileInfo:&error]; +if (archive.isPasswordProtected) { + NSString *givenPassword = // prompt user + archive.password = givenPassword +} + +// You can now extract the files +``` + +## Extracting files to a directory +```Objective-C +BOOL extractFilesSuccessful = [archive extractFilesTo:@"some/directory" + overWrite:NO + progress: + ^(URKFileInfo *currentFile, CGFloat percentArchiveDecompressed) { + NSLog(@"Extracting %@: %f%% complete", currentFile.filename, percentArchiveDecompressed); + } + error:&error]; +``` + +## Extracting a file into memory +```Objective-C +NSData *extractedData = [archive extractDataFromFile:@"a file in the archive.jpg" + progress:^(CGFloat percentDecompressed) { + NSLog(@"Extracting, %f%% complete", percentDecompressed); + } + error:&error]; +``` + +## Streaming a file + +For large files, you may not want the whole contents in memory at once. You can handle it one "chunk" at a time, like so: + +```Objective-C +BOOL success = [archive extractBufferedDataFromFile:@"a file in the archive.jpg" + error:&error + action: + ^(NSData *dataChunk, CGFloat percentDecompressed) { + NSLog(@"Decompressed: %f%%", percentDecompressed); + // Do something with the NSData chunk + }]; +``` + +# Progress Reporting + +The following methods support `NSProgress` and `NSProgressReporting`: + +* `extractFilesTo:overwrite:error:` +* `extractData:error:` +* `extractDataFromFile:error:` +* `performOnFilesInArchive:error:` +* `performOnDataInArchive:error:` +* `extractBufferedDataFromFile:error:action:` + +## Using implicit `NSProgress` hierarchy + +You can create your own instance of `NSProgress` and observe its `fractionCompleted` property with KVO to monitor progress like so: + +```Objective-C + static void *ExtractDataContext = &ExtractDataContext; + + URKArchive *archive = [[URKArchive alloc] initWithURL:aFileURL error:nil]; + + NSProgress *extractDataProgress = [NSProgress progressWithTotalUnitCount:1]; + [extractDataProgress becomeCurrentWithPendingUnitCount:1]; + + NSString *observedSelector = NSStringFromSelector(@selector(fractionCompleted)); + + [extractDataProgress addObserver:self + forKeyPath:observedSelector + options:NSKeyValueObservingOptionInitial + context:ExtractDataContext]; + + NSError *extractError = nil; + NSData *data = [archive extractDataFromFile:firstFile error:&extractError]; + + [extractDataProgress resignCurrent]; + [extractDataProgress removeObserver:self forKeyPath:observedSelector]; +``` + +## Using your own explicit `NSProgress` instance + +If you don't have a hierarchy of `NSProgress` instances, or if you want to observe more details during progress updates in `extractFilesTo:overwrite:error:`, you can create your own instance of `NSProgress` and set the `URKArchive` instance's `progress` property, like so: + +```Objective-C + static void *ExtractFilesContext = &ExtractFilesContext; + + URKArchive *archive = [[URKArchive alloc] initWithURL:aFileURL error:nil]; + + NSProgress *extractFilesProgress = [NSProgress progressWithTotalUnitCount:1]; + archive.progress = extractFilesProgress; + + NSString *observedSelector = NSStringFromSelector(@selector(localizedDescription)); + + [self.descriptionsReported removeAllObjects]; + [extractFilesProgress addObserver:self + forKeyPath:observedSelector + options:NSKeyValueObservingOptionInitial + context:ExtractFilesContext]; + + NSError *extractError = nil; + BOOL success = [archive extractFilesTo:extractURL.path + overwrite:NO + error:&extractError]; + + [extractFilesProgress removeObserver:self forKeyPath:observedSelector]; +``` + +## Cancellation with `NSProgress` + +Using either method above, you can call `[progress cancel]` to stop the operation in progress. It will cause the operation to fail, returning `nil` or `NO` (depending on the return type, and give an error with error code `URKErrorCodeUserCancelled`. + + +# Notes + +To open in Xcode, use the [UnrarKit.xcworkspace](UnrarKit.xcworkspace) file, which includes the other projects. + +# Documentation + +Full documentation for the project is available on [CocoaDocs](http://cocoadocs.org/docsets/UnrarKit). + + +# Logging + +For all OS versions from 2016 onward (macOS 10.12, iOS 10, tvOS 10, watchOS 3), UnzipKit uses the new [Unified Logging framework](https://developer.apple.com/documentation/os/logging) for logging and Activity Tracing. You can view messages at the Info or Debug level to view more details of how UnzipKit is working, and use Activity Tracing to help pinpoint the code path that's causing a particular error. + +As a fallback, regular `NSLog` is used on older OSes, with all messages logged at the same level. + +When debugging your own code, if you'd like to decrease the verbosity of the UnrarKit framework, you can run the following command: + + sudo log config --mode "level:default" --subsystem com.abbey-code.UnrarKit + +The available levels, in order of increasing verbosity, are `default`, `info`, `debug`, with `debug` being the default. + +## Logging guidelines + +These are the general rules governing the particulars of how activities and log messages are classified and written. They were written after the initial round of log messages were, so there may be some inconsistencies (such as an incorrect log level). If you think you spot one, open an issue or a pull request! + +### Logging + +Log messages should follow these conventions. + +1. Log messages don't have final punctuation (like these list items) +1. Messages that note a C function is about to be called, rather than a higher level UnrarKit or Cocoa method, end with "...", since it's not expected for them to log any details of their own + +#### Default log level + +There should be no messages at this level, so that it's possible for a consumer of the API to turn off _all_ diagnostic logging from it, as detailed above. It's only possible to `log config --mode "level:off"` for a process, not a subsystem. + +#### Info log level + +Info level log statements serve the following specific purposes. + +1. Major action is taken, such as initializing an archive object, or deleting a file from an archive +1. Noting each public method has been called, and the arguments with which it was called +1. Signposting the major actions a public method takes +1. Notifying that an atypical condition has occurred (such as an action causing an early stop in a block or a NO return value) +1. Noting that a loop is about to occur, which will contain debug-level messages for each iteration + +#### Debug log level + +Most messages fall into this category, making it extremely verbose. All non-error messages that don't fall into either of the other two categories should be debug-level, with some examples of specific cases below. + +1. Any log message in a private method +1. Noting variable and argument values in a method +1. Indicating that everything is working as expected +1. Indicating what happens during each iteration of a loop (or documenting that an iteration has happened at all) + +#### Error log level + +1. Every `NSError` generated should get logged with the same detail message as the `NSError` object itself +1. `NSError` log messages should contain the string of the error code's enumeration value (e.g. `"URKErrorCodeArchiveNotFound"`) when it is known at design time +1. Errors should reported everywhere they're encountered, making it easier to trace their flows through the call stack +1. Early exits that result in desired work not being performed + +#### Fault log level + +So far, there is only one case that gets logged at Fault-level: when a Cocoa framework methods that come back with an error + +### Activities +1. Public methods have an English activity names with spaces, and are title-case +1. Private methods each have an activity with the method's name +1. Sub-activities are created for significant scope changes, such as when inside an action block, but not if no significant work is done before entering that action +1. Top-level activities within a method have variables named `activity`, with more specific labels given to sub-activities +1. If a method is strictly an overload that calls out to another overload without doing anything else, it should not define its own activity + +# Pushing a new CocoaPods version + +New tagged builds (in any branch) get pushed to CocoaPods automatically, provided they meet the following criteria: + +1. All builds and tests succeed +2. The library builds successfully for CocoaPods and for Carthage +3. The build is tagged with something resembling a version number (`#.#.#(-beta#)`, e.g. **2.9** or **2.9-beta5**) +4. `pod spec lint` passes, making sure the CocoaPod is 100% valid + +Before pushing a build, you must: + +1. Add the release notes to the [CHANGELOG.md](CHANGELOG.md), and commit +2. Run [set-version](Scripts/set-version.sh), like so: + + `./Scripts/set-version.sh ` + + This does the following: + + 1. Updates the [UnrarKit-Info.plist](Resources/UnrarKit-Info.plist) file to indicate the new version number, and commits it + + 2. Makes an annotated tag whose message contains the release notes entered in Step 1 + +Once that's done, you can call `git push --follow-tags` [1](#f1), and let [Travis CI](https://travis-ci.org/abbeycode/UnrarKit/builds) take care of the rest. + +# Credits + +* Dov Frankel (dov@abbey-code.com) +* Rogerio Pereira Araujo (rogerio.araujo@gmail.com) +* Vicent Scott (vkan388@gmail.com) + + + +
+ +1: Or set `followTags = true` in your git config to always get this behavior: + + git config --global push.followTags true + +[↩](#a1) \ No newline at end of file diff --git a/Tests/URKArchiveTestCase.m b/Tests/URKArchiveTestCase.m index eac35c5..de66237 100644 --- a/Tests/URKArchiveTestCase.m +++ b/Tests/URKArchiveTestCase.m @@ -36,6 +36,7 @@ - (void)setUp @"Test Archive (RAR5).rar", @"Folder Archive.rar", @"Modified CRC Archive.rar", + @"README.md", @"Test File A.txt", @"Test File B.jpg", @"Test File C.m4a",