// // URKArchiveTestCase.m // UnrarKit // // Created by Dov Frankel on 6/22/15. // // #import "URKArchiveTestCase.h" #import static NSURL *originalLargeArchiveURL; @implementation URKArchiveTestCase #pragma mark - Test Management - (void)setUp { [super setUp]; NSFileManager *fm = [NSFileManager defaultManager]; NSString *uniqueName = [self randomDirectoryName]; NSError *error = nil; NSArray *testFiles = @[@"Test Archive.rar", @"Test Archive (Password).rar", @"Test Archive (Header Password).rar", @"Test Archive (RAR5, Password).rar", @"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", @"bin/rar"]; NSArray *unicodeFiles = @[@"Ⓣest Ⓐrchive.rar", @"Test File Ⓐ.txt", @"Test File Ⓑ.jpg", @"Test File Ⓒ.m4a"]; NSString *tempDirSubtree = [@"UnrarKitTest" stringByAppendingPathComponent:uniqueName]; self.testFailed = NO; self.testFileURLs = [[NSMutableDictionary alloc] init]; self.unicodeFileURLs = [[NSMutableDictionary alloc] init]; self.tempDirectory = [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:tempDirSubtree] isDirectory:YES]; NSLog(@"Temp directory: %@", self.tempDirectory); [fm createDirectoryAtURL:self.tempDirectory withIntermediateDirectories:YES attributes:nil error:&error]; XCTAssertNil(error, @"Failed to create temp directory: %@", self.tempDirectory); NSMutableArray *filesToCopy = [NSMutableArray arrayWithArray:testFiles]; [filesToCopy addObjectsFromArray:unicodeFiles]; for (NSString *file in filesToCopy) { NSURL *testFileURL = [self urlOfTestFile:file]; BOOL testFileExists = [fm fileExistsAtPath:testFileURL.path]; XCTAssertTrue(testFileExists, @"%@ not found", file); NSURL *destinationURL = [self.tempDirectory URLByAppendingPathComponent:file isDirectory:NO]; NSError *error = nil; if (file.pathComponents.count > 1) { [fm createDirectoryAtPath:destinationURL.URLByDeletingLastPathComponent.path withIntermediateDirectories:YES attributes:nil error:&error]; XCTAssertNil(error, @"Failed to create directories for file %@", file); } [fm copyItemAtURL:testFileURL toURL:destinationURL error:&error]; XCTAssertNil(error, @"Failed to copy temp file %@ from %@ to %@", file, testFileURL, destinationURL); if ([testFiles containsObject:file]) { [self.testFileURLs setObject:destinationURL forKey:file]; } else if ([unicodeFiles containsObject:file]) { [self.unicodeFileURLs setObject:destinationURL forKey:file]; } } // Make a "corrupt" rar file NSURL *m4aFileURL = [self urlOfTestFile:@"Test File C.m4a"]; self.corruptArchive = [self.tempDirectory URLByAppendingPathComponent:@"corrupt.rar"]; [fm copyItemAtURL:m4aFileURL toURL:self.corruptArchive error:&error]; XCTAssertNil(error, @"Failed to create corrupt archive (copy from %@ to %@)", m4aFileURL, self.corruptArchive); } - (void)tearDown { NSString *largeArchiveDirectory = originalLargeArchiveURL.path.stringByDeletingLastPathComponent; BOOL tempDirContainsLargeArchive = [largeArchiveDirectory isEqualToString:self.tempDirectory.path]; if (!self.testFailed && !tempDirContainsLargeArchive) { __block NSError *error = nil; dispatch_semaphore_t sem = dispatch_semaphore_create(1); dispatch_async(dispatch_get_main_queue(), ^{ [[NSFileManager defaultManager] removeItemAtURL:self.tempDirectory error:&error]; dispatch_semaphore_signal(sem); }); dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, 5000)); XCTAssertNil(error, @"Error deleting temp directory"); } [super tearDown]; } - (void)recordIssue:(XCTIssue *)issue { self.testFailed = YES; [super recordIssue:issue]; } #pragma mark - Helper Methods - (NSURL *)urlOfTestFile:(NSString *)filename { NSString *baseDirectory = @"Test Data"; NSString *subPath = filename.stringByDeletingLastPathComponent; NSString *bundleSubdir = [baseDirectory stringByAppendingPathComponent:subPath]; return [[NSBundle bundleForClass:[self class]] URLForResource:filename.lastPathComponent withExtension:nil subdirectory:bundleSubdir]; } - (NSString *)randomDirectoryName { NSString *globallyUnique = [[NSProcessInfo processInfo] globallyUniqueString]; NSRange firstHyphen = [globallyUnique rangeOfString:@"-"]; return [globallyUnique substringToIndex:firstHyphen.location]; } - (NSString *)randomDirectoryWithPrefix:(NSString *)prefix { return [NSString stringWithFormat:@"%@ %@", prefix, [self randomDirectoryName]]; } - (NSURL *)randomTextFileOfLength:(NSUInteger)numberOfCharacters { NSString *letters = @"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 .,?!\n"; NSUInteger letterCount = letters.length; NSMutableString *randomString = [NSMutableString stringWithCapacity:numberOfCharacters]; for (NSUInteger i = 0; i < numberOfCharacters; i++) { uint32_t charIndex = arc4random_uniform((uint32_t)letterCount); [randomString appendFormat:@"%C", [letters characterAtIndex:charIndex]]; } NSURL *resultURL = [self.tempDirectory URLByAppendingPathComponent: [NSString stringWithFormat:@"%@.txt", [[NSProcessInfo processInfo] globallyUniqueString]]]; NSError *error = nil; [randomString writeToURL:resultURL atomically:YES encoding:NSUTF16StringEncoding error:&error]; XCTAssertNil(error, @"Error opening file handle for text file creation: %@", error); return resultURL; } - (NSUInteger)crcOfTestFile:(NSString *)filename { NSURL *fileURL = [self urlOfTestFile:filename]; NSData *fileContents = [[NSFileManager defaultManager] contentsAtPath:[fileURL path]]; return crc32(0, fileContents.bytes, (uint)fileContents.length); } #pragma mark - Mac Only #if !TARGET_OS_IPHONE - (NSURL *)largeArchiveURL { NSFileManager *fm = [NSFileManager defaultManager]; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ NSMutableArray *largeTextFiles = [NSMutableArray array]; for (NSInteger i = 0; i < 20; i++) { [largeTextFiles addObject:[self randomTextFileOfLength:3000000]]; } NSError *largeArchiveError = nil; NSURL *largeArchiveURLRandomName = [self archiveWithFiles:largeTextFiles]; originalLargeArchiveURL = [largeArchiveURLRandomName.URLByDeletingLastPathComponent URLByAppendingPathComponent:@"Large Archive (Original).rar"]; [fm moveItemAtURL:largeArchiveURLRandomName toURL:originalLargeArchiveURL error:&largeArchiveError]; XCTAssertNil(largeArchiveError, @"Error renaming original large archive: %@", largeArchiveError); }); NSString *largeArchiveName = @"Large Archive.rar"; NSURL *destinationURL = [self.tempDirectory URLByAppendingPathComponent:largeArchiveName isDirectory:NO]; NSError *fileCopyError = nil; [fm copyItemAtURL:originalLargeArchiveURL toURL:destinationURL error:&fileCopyError]; XCTAssertNil(fileCopyError, @"Failed to copy the Large Archive"); return destinationURL; } - (NSInteger)numberOfOpenFileHandles { int pid = [[NSProcessInfo processInfo] processIdentifier]; NSPipe *pipe = [NSPipe pipe]; NSFileHandle *file = pipe.fileHandleForReading; NSTask *task = [[NSTask alloc] init]; task.launchPath = @"/usr/sbin/lsof"; task.arguments = @[@"-P", @"-n", @"-p", [NSString stringWithFormat:@"%d", pid]]; task.standardOutput = pipe; [task launch]; NSData *data = [file readDataToEndOfFile]; [file closeFile]; NSString *lsofOutput = [[NSString alloc] initWithData: data encoding: NSUTF8StringEncoding]; // NSLog(@"LSOF:\n%@", lsofOutput); return [lsofOutput componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]].count; } - (NSURL *)archiveWithFiles:(NSArray *)fileURLs { return [self archiveWithFiles:fileURLs arguments:nil]; } - (NSURL *)archiveWithFiles:(NSArray *)fileURLs arguments:(NSArray *)customArgs { return [self archiveWithFiles:fileURLs arguments:customArgs commandOutput:NULL]; } - (NSURL *)archiveWithFiles:(NSArray *)fileURLs arguments:(NSArray *)customArgs commandOutput:(NSString **)commandOutput { return [self archiveWithFiles:fileURLs name:nil arguments:customArgs commandOutput:commandOutput]; } - (NSURL *)archiveWithFiles:(NSArray *)fileURLs name:(NSString *)archiveName { return [self archiveWithFiles:fileURLs name:archiveName arguments:nil]; } - (NSURL *)archiveWithFiles:(NSArray *)fileURLs name:(NSString *)archiveName arguments:(NSArray *)customArgs { return [self archiveWithFiles:fileURLs name:archiveName arguments:customArgs commandOutput:NULL]; } - (NSURL *)archiveWithFiles:(NSArray *)fileURLs name:(NSString *)archiveName arguments:(NSArray *)customArgs commandOutput:(NSString **)commandOutput { NSString *archiveFileName = archiveName; if (![archiveFileName length]) { NSString *uniqueString = [[NSProcessInfo processInfo] globallyUniqueString]; archiveFileName = [uniqueString stringByAppendingPathExtension:@"rar"]; } NSURL *rarExec = [[NSBundle bundleForClass:[self class]] URLForResource:@"rar" withExtension:nil subdirectory:@"Test Data/bin"]; NSURL *archiveURL = [self.tempDirectory URLByAppendingPathComponent:archiveFileName]; NSMutableArray *rarArguments = [NSMutableArray arrayWithArray:@[@"a", @"-ep", archiveURL.path]]; if (customArgs) { [rarArguments addObjectsFromArray:customArgs]; } [rarArguments addObjectsFromArray:[fileURLs valueForKeyPath:@"path"]]; NSPipe *pipe = [NSPipe pipe]; NSFileHandle *file = pipe.fileHandleForReading; NSTask *task = [[NSTask alloc] init]; task.executableURL = rarExec; task.arguments = rarArguments; task.standardOutput = pipe; NSError *launchError = nil; [task launchAndReturnError:&launchError]; XCTAssertNil(launchError); [task waitUntilExit]; NSData *data = [file readDataToEndOfFile]; [file closeFile]; if (commandOutput) { *commandOutput = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; } if (task.terminationStatus != 0) { NSLog(@"Failed to create RAR archive"); return nil; } return archiveURL; } - (NSArray *)multiPartArchiveWithName:(NSString *)baseName { return [self multiPartArchiveWithName:baseName fileSize:100000]; } - (NSArray *)multiPartArchiveWithName:(NSString *)baseName fileSize:(NSUInteger)fileSize { NSURL *textFile = [self randomTextFileOfLength:fileSize]; // Generate multi-volume archive, with parts no larger than 20 KB in size NSString *commandOutputString = nil; [self archiveWithFiles:@[textFile] name:baseName arguments:@[@"-v20k"] commandOutput:&commandOutputString]; NSMutableArray *volumePaths = [NSMutableArray arrayWithArray: [commandOutputString regexMatches:@"Creating archive (.+)"]]; NSString *intendedFirstVolumePath = volumePaths.firstObject; // Fill in leading zeroes according to number of parts (no fewer than 2 digits) int numDigits = MAX(2, floor(log10(volumePaths.count)) + 1); NSString *zeroPadding = [@"" stringByPaddingToLength:numDigits - 1 withString:@"0" startingAtIndex:0]; NSString *actualExtension = [NSString stringWithFormat:@"part%@1.rar", zeroPadding]; NSString *firstVolumeDir = [intendedFirstVolumePath stringByDeletingLastPathComponent]; NSString *actualFirstVolumePath = [firstVolumeDir stringByAppendingPathComponent: [baseName stringByReplacingOccurrencesOfString:@"rar" withString:actualExtension]]; [volumePaths replaceObjectAtIndex:0 withObject:actualFirstVolumePath]; NSMutableArray *result = [NSMutableArray array]; for (NSString *path in volumePaths) { [result addObject:[NSURL fileURLWithPath:path]]; } return result; } - (NSArray *)multiPartArchiveOldSchemeWithName:(NSString *)baseName { NSURL *textFile = [self randomTextFileOfLength:100000]; // Generate multi-volume archive, with parts no larger than 20 KB in size NSString *commandOutputString = nil; [self archiveWithFiles:@[textFile] name:baseName arguments:@[@"-v20k", // Volume size @"-vn", // Legacy naming @"-ma4"] // v4 archive format commandOutput:&commandOutputString]; NSArray *volumePaths = [commandOutputString regexMatches:@"Creating archive (.+)"]; NSMutableArray *result = [NSMutableArray array]; for (NSString *path in volumePaths) { [result addObject:[NSURL fileURLWithPath:path]]; } return result; } #endif @end @implementation NSString (URKArchiveTestCaseExtensions) - (NSArray *)regexMatches:(NSString *)expression { NSError *regexCreationError = nil; NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:expression options:0 error:®exCreationError]; if (!regex) { NSLog(@"Failed to create regex with pattern '%@': %@", expression, regexCreationError); return @[]; } NSMutableArray *results = [NSMutableArray array]; [regex enumerateMatchesInString:self options:0 range:NSMakeRange(0, self.length) usingBlock:^(NSTextCheckingResult * _Nullable result, NSMatchingFlags flags, BOOL * _Nonnull stop) { [results addObject:[self substringWithRange:[result rangeAtIndex:1]]]; }]; return results; } @end