diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ddaf22a..673fca76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 2.9 * Added support for `NSProgress` and `NSProgressReporting` in all extraction and iteration methods (Issue #34) +* Added enhanced support for multivolume archives (PRs #59, #38 - Thanks to [@aonez](https://github.com/aonez) for the idea and implementation!) * Switched to Travis Build Stages instead of the unofficial Travis-After-All (Issue #42) * Added detailed logging using new unified logging framework. See [the readme](README.md) for more details (Issue #35) * Added localized details to returned `NSError` objects (Issue #45) diff --git a/Classes/URKArchive.h b/Classes/URKArchive.h index 0592f07f..871c9df1 100644 --- a/Classes/URKArchive.h +++ b/Classes/URKArchive.h @@ -153,6 +153,11 @@ extern NSString *URKErrorDomain; */ @property(nullable, readonly) NSNumber *compressedSize; +/** + * True if the file is one volume of a multi-part archive + */ +@property(readonly) BOOL hasMultipleVolumes; + /** * Can be used for progress reporting, but it's not necessary. You can also use * implicit progress reporting. If you don't use it, one will still be created, @@ -281,6 +286,15 @@ extern NSString *URKErrorDomain; */ - (nullable NSArray *)listFileInfo:(NSError **)error; +/** + * Lists the URLs of volumes in a single- or multi-volume archive + * + * @param error Contains an NSError object when there was an error reading the archive + * + * @return Returns the list of URLs of all volumes of the archive + */ +- (nullable NSArray *)listVolumeURLs:(NSError **)error; + /** * Writes all files in the archive to the given path. Supports NSProgress for progress reporting, which also * allows cancellation in the middle of extraction. Use the progress property (as explained in the README) to diff --git a/Classes/URKArchive.mm b/Classes/URKArchive.mm index 4b0deac4..dec2023a 100644 --- a/Classes/URKArchive.mm +++ b/Classes/URKArchive.mm @@ -134,7 +134,13 @@ - (instancetype)initWithFile:(NSURL *)fileURL password:(NSString*)password error if (error) { *error = nil; } - + + NSURL *firstVolumeURL = [URKArchive firstVolumeURL:fileURL]; + if (firstVolumeURL && ![firstVolumeURL.absoluteString isEqualToString:fileURL.absoluteString]) { + URKLogDebug("Overriding fileURL with first volume URL: %{public}@", firstVolumeURL); + fileURL = firstVolumeURL; + } + URKLogDebug("Initializing private fields"); NSError *bookmarkError = nil; @@ -253,6 +259,21 @@ - (NSNumber *)compressedSize 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 @@ -380,6 +401,33 @@ + (BOOL)urlIsARAR:(NSURL *)fileURL return [NSArray arrayWithArray:fileInfos]; } +- (nullable NSArray *)listVolumeURLs:(NSError **)error +{ + URKCreateActivity("Listing Volume URLs"); + + NSArray *listFileInfo = [self listFileInfo:error]; + + if (listFileInfo == nil) { + return nil; + } + + NSMutableSet *volumeURLs = [[NSMutableSet alloc] init]; + + for (URKFileInfo* info in listFileInfo) { + 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 **)error @@ -1262,6 +1310,8 @@ - (BOOL)headerContainsErrors:(NSError **)error - (NSProgress *)beginProgressOperation:(NSUInteger)totalUnitCount { + URKCreateActivity("-beginProgressOperation:"); + NSProgress *progress; progress = self.progress; if (!progress) { @@ -1279,4 +1329,79 @@ - (NSProgress *)beginProgressOperation:(NSUInteger)totalUnitCount 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; +} + @end diff --git a/Tests/FirstVolumeTests.m b/Tests/FirstVolumeTests.m new file mode 100644 index 00000000..cdd25fba --- /dev/null +++ b/Tests/FirstVolumeTests.m @@ -0,0 +1,109 @@ +// +// FirstVolumeTests.m +// UnrarKit +// +// Created by Dov Frankel on 2/9/17. +// +// + +#import "URKArchiveTestCase.h" + +@interface FirstVolumeTests : URKArchiveTestCase @end + +@interface URKArchive (Tests) + +// It's a private class method ++ (NSURL *)firstVolumeURL:(NSURL *)volumeURL; + +@end + +@implementation FirstVolumeTests + +- (void)testSingleVolume { + NSURL *onlyVolumeArchiveURL = self.testFileURLs[@"Test Archive.rar"]; + NSURL *returnedFirstVolumeURL = [URKArchive firstVolumeURL:onlyVolumeArchiveURL]; + + XCTAssertNotNil(returnedFirstVolumeURL, @"No URL returned"); + XCTAssertEqualObjects(returnedFirstVolumeURL, onlyVolumeArchiveURL, @"URL changed even though it's a single volume archive"); +} + + +#if !TARGET_OS_IPHONE +- (void)testMultipleVolume_UseFirstVolume { + NSArray *volumeURLs = [self multiPartArchiveWithName:@"FirstVolumeTests-testMultipleVolume_UseFirstVolume.rar"]; + NSURL *firstVolumeURL = volumeURLs.firstObject; + NSURL *returnedFirstVolumeURL = [URKArchive firstVolumeURL:firstVolumeURL]; + + XCTAssertNotNil(returnedFirstVolumeURL, @"No URL returned"); + XCTAssertEqualObjects(returnedFirstVolumeURL, firstVolumeURL, @"URL changed even though it was initialized with the first volume"); +} +#endif + +#if !TARGET_OS_IPHONE +- (void)testMultipleVolume_UseMiddleVolume { + NSArray *volumeURLs = [self multiPartArchiveWithName:@"ListVolumesTests-testMultipleVolume_UseFirstVolume.rar"]; + NSURL *firstVolumeURL = volumeURLs.firstObject; + NSURL *thirdVolumeURL = volumeURLs[2]; + + NSURL *returnedFirstVolumeURL = [URKArchive firstVolumeURL:thirdVolumeURL]; + + XCTAssertNotNil(returnedFirstVolumeURL, @"No URL returned"); + XCTAssertEqualObjects(returnedFirstVolumeURL.absoluteString, firstVolumeURL.absoluteString, @"Incorrect URL returned as first volume"); +} +#endif + +#if !TARGET_OS_IPHONE +- (void)testMultipleVolume_UseMiddleVolume_OneHundredParts { + NSArray *volumeURLs = [self multiPartArchiveWithName:@"ListVolumesTests-testMultipleVolume_UseFirstVolume.rar" fileSize:2500000]; + + NSURL *firstVolumeURL = volumeURLs.firstObject; + NSURL *hundredthVolumeURL = volumeURLs[100]; + + NSURL *returnedFirstVolumeURL = [URKArchive firstVolumeURL:hundredthVolumeURL]; + + XCTAssertNotNil(returnedFirstVolumeURL, @"No URL returned"); + XCTAssertEqualObjects(returnedFirstVolumeURL.absoluteString, firstVolumeURL.absoluteString, @"Incorrect URL returned as first volume"); +} +#endif + +#if !TARGET_OS_IPHONE +- (void)testMultipleVolume_UseFirstVolume_OldNamingScheme { + NSArray *volumeURLs = [self multiPartArchiveOldSchemeWithName:@"FirstVolumeTests-testMultipleVolume_UseFirstVolume_OldNamingScheme.rar"]; + NSURL *firstVolumeURL = volumeURLs.firstObject; + NSURL *returnedFirstVolumeURL = [URKArchive firstVolumeURL:firstVolumeURL]; + + XCTAssertNotNil(returnedFirstVolumeURL, @"No URL returned"); + XCTAssertEqualObjects(returnedFirstVolumeURL, firstVolumeURL, @"URL changed even though it was initialized with the first volume"); +} +#endif + +#if !TARGET_OS_IPHONE +- (void)testMultipleVolume_UseMiddleVolume_OldNamingScheme { + NSArray *volumeURLs = [self multiPartArchiveOldSchemeWithName:@"FirstVolumeTests-testMultipleVolume_UseMiddleVolume_OldNamingScheme.rar"]; + NSURL *firstVolumeURL = volumeURLs.firstObject; + NSURL *thirdVolumeURL = volumeURLs[2]; + + NSURL *returnedFirstVolumeURL = [URKArchive firstVolumeURL:thirdVolumeURL]; + + XCTAssertNotNil(returnedFirstVolumeURL, @"No URL returned"); + XCTAssertEqualObjects(returnedFirstVolumeURL.absoluteString, firstVolumeURL.absoluteString, @"Incorrect URL returned as first volume"); +} +#endif + +#if !TARGET_OS_IPHONE +- (void)testMultipleVolume_FirstVolumeMissing { + NSArray *volumeURLs = [self multiPartArchiveWithName:@"ListVolumesTests-testMultipleVolume_FirstVolumeMissing.rar"]; + + NSError *deleteError = nil; + [[NSFileManager defaultManager] removeItemAtURL:volumeURLs.firstObject + error:&deleteError]; + XCTAssertNil(deleteError, @"Error deleting first volume of archive"); + + NSURL *firstVolumeURL = volumeURLs.firstObject; + NSURL *returnedFirstVolumeURL = [URKArchive firstVolumeURL:firstVolumeURL]; + + XCTAssertNil(returnedFirstVolumeURL, @"First volume URL returned when it does not exist"); +} +#endif + +@end diff --git a/Tests/HasMultipleVolumesTests.m b/Tests/HasMultipleVolumesTests.m new file mode 100644 index 00000000..02bf2df8 --- /dev/null +++ b/Tests/HasMultipleVolumesTests.m @@ -0,0 +1,73 @@ +// +// HasMultipleVolumesTests.m +// UnrarKit +// +// Created by Dov Frankel on 2/9/17. +// +// + +#import "URKArchiveTestCase.h" + +@interface HasMultipleVolumesTests : URKArchiveTestCase + +@end + +@implementation HasMultipleVolumesTests + +- (void)testSingleVolume { + NSURL *testArchiveURL = self.testFileURLs[@"Test Archive.rar"]; + URKArchive *archive = [[URKArchive alloc] initWithURL:testArchiveURL error:nil]; + + BOOL hasMultipleParts = archive.hasMultipleVolumes; + + XCTAssertFalse(hasMultipleParts, @"Single-volume archive reported to have multiple parts"); +} + +#if !TARGET_OS_IPHONE +- (void)testMultipleVolume_UseFirstVolume { + NSArray *volumeURLs = [self multiPartArchiveWithName:@"HasMultipleVolumesTests-testMultipleVolume_UseFirstVolume.rar"]; + URKArchive *archive = [[URKArchive alloc] initWithURL:volumeURLs.firstObject error:nil]; + + BOOL hasMultipleParts = archive.hasMultipleVolumes; + XCTAssertTrue(hasMultipleParts, @"Multi-volume archive's first part not reported to have multiple volumes"); +} +#endif + +#if !TARGET_OS_IPHONE +- (void)testMultipleVolume_UseMiddleVolume { + NSArray *volumeURLs = [self multiPartArchiveWithName:@"HasMultipleVolumesTests-testMultipleVolume_UseMiddleVolume.rar"]; + URKArchive *archive = [[URKArchive alloc] initWithURL:volumeURLs[2] error:nil]; + + BOOL hasMultipleParts = archive.hasMultipleVolumes; + XCTAssertTrue(hasMultipleParts, @"Multi-volume archive's middle part not reported to have multiple volumes"); +} +#endif + +#if !TARGET_OS_IPHONE +- (void)testMultipleVolume_UseFirstVolume_OldNamingScheme { + NSArray *volumeURLs = [self multiPartArchiveOldSchemeWithName:@"HasMultipleVolumesTests-testMultipleVolume_UseFirstVolume_OldNamingScheme.rar"]; + URKArchive *archive = [[URKArchive alloc] initWithURL:volumeURLs.firstObject error:nil]; + + BOOL hasMultipleParts = archive.hasMultipleVolumes; + XCTAssertTrue(hasMultipleParts, @"Multi-volume archive's first part not reported to have multiple volumes"); +} +#endif + +#if !TARGET_OS_IPHONE +- (void)testMultipleVolume_UseMiddleVolume_OldNamingScheme { + NSArray *volumeURLs = [self multiPartArchiveOldSchemeWithName:@"HasMultipleVolumesTests-testMultipleVolume_UseMiddleVolume_OldNamingScheme.rar"]; + URKArchive *archive = [[URKArchive alloc] initWithURL:volumeURLs[2] error:nil]; + + BOOL hasMultipleParts = archive.hasMultipleVolumes; + XCTAssertTrue(hasMultipleParts, @"Multi-volume archive's middle part not reported to have multiple volumes"); +} +#endif + +- (void)testInvalidArchive { + URKArchive *archive = [[URKArchive alloc] initWithURL:self.testFileURLs[@"Test File A.txt"] error:nil]; + + BOOL hasMultipleParts = archive.hasMultipleVolumes; + XCTAssertFalse(hasMultipleParts, @"Invalid archive reported to have multiple volumes"); +} + +@end diff --git a/Tests/ListVolumesTests.m b/Tests/ListVolumesTests.m new file mode 100644 index 00000000..c30e9023 --- /dev/null +++ b/Tests/ListVolumesTests.m @@ -0,0 +1,82 @@ +// +// ListVolumesTests.m +// UnrarKit +// +// Created by Dov Frankel on 12/9/16. +// +// + +#import "URKArchiveTestCase.h" + +@interface ListVolumesTests : URKArchiveTestCase + +@end + +@implementation ListVolumesTests + +- (void)testSingleVolume { + NSURL *testArchiveURL = self.testFileURLs[@"Test Archive.rar"]; + URKArchive *archive = [[URKArchive alloc] initWithURL:testArchiveURL error:nil]; + + 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"]; + URKArchive *archive = [[URKArchive alloc] initWithURL:generatedVolumeURLs.firstObject error:nil]; + + NSMutableArray *expectedVolumeURLs = [NSMutableArray array]; + + // NSTemporaryDirectory() returns '/var', which maps to '/private/var' + for (NSURL *volumeURL in generatedVolumeURLs) { + NSString *originalPath = volumeURL.path; + NSString *privatePath = [@"/private" stringByAppendingString:originalPath]; + [expectedVolumeURLs addObject:[NSURL fileURLWithPath:privatePath]]; + } + + NSError *listVolumesError = nil; + NSArray *volumeURLs = [archive listVolumeURLs:&listVolumesError]; + + XCTAssertNil(listVolumesError, @"Error listing volume URLs"); + XCTAssertNotNil(volumeURLs, @"No URLs returned"); + XCTAssertEqual(volumeURLs.count, 5, @"Wrong number of volume URLs listed"); + XCTAssertTrue([expectedVolumeURLs isEqualToArray:volumeURLs], + @"Expected these URL:\n%@\n\nGot these:\n%@", expectedVolumeURLs, volumeURLs); +} +#endif + +#if !TARGET_OS_IPHONE +- (void)testMultipleVolume_UseMiddleVolume { + NSArray *generatedVolumeURLs = [self multiPartArchiveWithName:@"ListVolumesTests-testMultipleVolume_UseMiddleVolume.rar"]; + URKArchive *archive = [[URKArchive alloc] initWithURL:generatedVolumeURLs[2] error:nil]; + + NSMutableArray *expectedVolumeURLs = [NSMutableArray array]; + + // NSTemporaryDirectory() returns '/var', which maps to '/private/var' + for (NSURL *volumeURL in generatedVolumeURLs) { + NSString *originalPath = volumeURL.path; + NSString *privatePath = [@"/private" stringByAppendingString:originalPath]; + [expectedVolumeURLs addObject:[NSURL fileURLWithPath:privatePath]]; + } + + NSError *listVolumesError = nil; + NSArray *volumeURLs = [archive listVolumeURLs:&listVolumesError]; + + XCTAssertNil(listVolumesError, @"Error listing volume URLs"); + XCTAssertNotNil(volumeURLs, @"No URLs returned"); + XCTAssertEqual(volumeURLs.count, 5, @"Wrong number of volume URLs listed"); + XCTAssertTrue([expectedVolumeURLs isEqualToArray:volumeURLs], + @"Expected these URL:\n%@\n\nGot these:\n%@", expectedVolumeURLs, volumeURLs); +} +#endif + +@end diff --git a/Tests/URKArchiveTestCase.h b/Tests/URKArchiveTestCase.h index 92ed464a..8f7606f8 100644 --- a/Tests/URKArchiveTestCase.h +++ b/Tests/URKArchiveTestCase.h @@ -40,6 +40,28 @@ - (NSURL *)largeArchiveURL; - (NSInteger)numberOfOpenFileHandles; - (NSURL *)archiveWithFiles:(NSArray *)fileURLs; + +- (NSURL *)archiveWithFiles:(NSArray *)fileURLs arguments:(NSArray *)customArgs; +- (NSURL *)archiveWithFiles:(NSArray *)fileURLs arguments:(NSArray *)customArgs commandOutput:(NSString **)commandOutput; + +- (NSURL *)archiveWithFiles:(NSArray *)fileURLs name:(NSString *)archiveName; +- (NSURL *)archiveWithFiles:(NSArray *)fileURLs name:(NSString *)archiveName arguments:(NSArray *)customArgs; +- (NSURL *)archiveWithFiles:(NSArray *)fileURLs name:(NSString *)archiveName arguments:(NSArray *)customArgs commandOutput:(NSString **)commandOutput; + +- (NSArray *)multiPartArchiveWithName:(NSString *)baseName; +- (NSArray *)multiPartArchiveWithName:(NSString *)baseName fileSize:(NSUInteger)fileSize; +- (NSArray *)multiPartArchiveOldSchemeWithName:(NSString *)baseName; #endif @end + +@interface NSString (URKArchiveTestCaseExtensions) + +/** + * Returns all of the regex matches in the given string + * + * @param expression The regex expression to match. Must contain exactly one capture group + */ +- (NSArray *)regexMatches:(NSString *)expression; + +@end diff --git a/Tests/URKArchiveTestCase.m b/Tests/URKArchiveTestCase.m index f7b00c46..68d440b5 100644 --- a/Tests/URKArchiveTestCase.m +++ b/Tests/URKArchiveTestCase.m @@ -239,18 +239,59 @@ - (NSInteger)numberOfOpenFileHandles { } - (NSURL *)archiveWithFiles:(NSArray *)fileURLs { - NSString *uniqueString = [[NSProcessInfo processInfo] globallyUniqueString]; + 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 = [[self.tempDirectory URLByAppendingPathComponent:@"bin"] URLByAppendingPathComponent:@"rar"]; - NSURL *archiveURL = [[self.tempDirectory URLByAppendingPathComponent:uniqueString] - URLByAppendingPathExtension:@"rar"]; + 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.launchPath = rarExec.path; - task.arguments = [@[@"a", @"-ep", archiveURL.path] arrayByAddingObjectsFromArray:[fileURLs valueForKeyPath:@"path"]]; + task.arguments = rarArguments; + task.standardOutput = pipe; [task launch]; [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"); @@ -259,8 +300,98 @@ - (NSURL *)archiveWithFiles:(NSArray *)fileURLs { 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", @"-vn"] + 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 diff --git a/UnrarKit.xcodeproj/project.pbxproj b/UnrarKit.xcodeproj/project.pbxproj index 1cbcd440..488d60b5 100644 --- a/UnrarKit.xcodeproj/project.pbxproj +++ b/UnrarKit.xcodeproj/project.pbxproj @@ -12,6 +12,7 @@ 7A267F6E1F713B970004EAA6 /* ProgressReportingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A267F6D1F713B970004EAA6 /* ProgressReportingTests.m */; }; 964C8AC718D28EE000AD7321 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 964C8AC518D28EE000AD7321 /* InfoPlist.strings */; }; 9660D7AF1A3F4FF90059AC1E /* libz.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 9660D7AE1A3F4FF90059AC1E /* libz.dylib */; }; + 967872741E460FA70048A54C /* ListVolumesTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 967872731E460FA70048A54C /* ListVolumesTests.m */; }; 9699F91D1B3CB4D000B6D373 /* URKArchive.h in Headers */ = {isa = PBXBuildFile; fileRef = 489CFA0E128B5169005DCC2A /* URKArchive.h */; settings = {ATTRIBUTES = (Public, ); }; }; 9699F91E1B3CB4D000B6D373 /* URKFileInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = F3FCD4691A3262E5003612BF /* URKFileInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; 9699F91F1B3CB4D000B6D373 /* UnrarKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 969F17361A60297700665453 /* UnrarKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -79,6 +80,8 @@ 9699FA8D1B3D9B6F00B6D373 /* ListFilenamesTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 96F450781B385AF800679597 /* ListFilenamesTests.m */; }; 9699FA8E1B3D9B6F00B6D373 /* ValidatePasswordTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 96F450741B38527100679597 /* ValidatePasswordTests.m */; }; 96BF58BB1F3A487100BC24E1 /* UnrarKitMacros.h in Headers */ = {isa = PBXBuildFile; fileRef = 96BF58BA1F3A487100BC24E1 /* UnrarKitMacros.h */; }; + 96A043DE1E4CC8D500BD7013 /* FirstVolumeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 96A043DD1E4CC8D500BD7013 /* FirstVolumeTests.m */; }; + 96A043E01E4D232F00BD7013 /* HasMultipleVolumesTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 96A043DF1E4D232F00BD7013 /* HasMultipleVolumesTests.m */; }; 96E5D345198B333200A74340 /* Test Data in Resources */ = {isa = PBXBuildFile; fileRef = 964C8AD018D28F1600AD7321 /* Test Data */; }; 96EA53781B3CB2C700F79DC6 /* rar.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 96853F4A18DB722E00B5651B /* rar.cpp */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; 96EA53791B3CB2C700F79DC6 /* strlist.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 96853F6D18DB722E00B5651B /* strlist.cpp */; settings = {COMPILER_FLAGS = "-fno-objc-arc -w -Xanalyzer -analyzer-disable-all-checks"; }; }; @@ -206,6 +209,7 @@ 964C8AC818D28EE000AD7321 /* URKArchiveTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = URKArchiveTests.m; sourceTree = ""; }; 964C8AD018D28F1600AD7321 /* Test Data */ = {isa = PBXFileReference; lastKnownFileType = folder; path = "Test Data"; sourceTree = ""; }; 9660D7AE1A3F4FF90059AC1E /* libz.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libz.dylib; path = usr/lib/libz.dylib; sourceTree = SDKROOT; }; + 967872731E460FA70048A54C /* ListVolumesTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ListVolumesTests.m; sourceTree = ""; }; 96853ED418DB707000B5651B /* UnrarKit-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "UnrarKit-Info.plist"; sourceTree = ""; }; 96853F0718DB722E00B5651B /* arccmt.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = arccmt.cpp; sourceTree = ""; }; 96853F0818DB722E00B5651B /* archive.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = archive.cpp; sourceTree = ""; }; @@ -321,6 +325,8 @@ 96853F8518DB722F00B5651B /* win32stm.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = win32stm.cpp; sourceTree = ""; }; 969F17361A60297700665453 /* UnrarKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UnrarKit.h; sourceTree = ""; }; 96BF58BA1F3A487100BC24E1 /* UnrarKitMacros.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UnrarKitMacros.h; sourceTree = ""; }; + 96A043DD1E4CC8D500BD7013 /* FirstVolumeTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FirstVolumeTests.m; sourceTree = ""; }; + 96A043DF1E4D232F00BD7013 /* HasMultipleVolumesTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HasMultipleVolumesTests.m; sourceTree = ""; }; 96DBF7F71A3F72800033B759 /* NSString+UnrarKit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "NSString+UnrarKit.h"; path = "Categories/NSString+UnrarKit.h"; sourceTree = ""; }; 96DBF7F81A3F72800033B759 /* NSString+UnrarKit.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = "NSString+UnrarKit.mm"; path = "Categories/NSString+UnrarKit.mm"; sourceTree = ""; }; 96EA53311B3B462D00F79DC6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -426,8 +432,11 @@ 964547D11B384F7D00202B28 /* URKArchiveTestCase.m */, 964C8AC818D28EE000AD7321 /* URKArchiveTests.m */, 96F4507A1B385BCD00679597 /* ExtractFilesTests.m */, + 96A043DD1E4CC8D500BD7013 /* FirstVolumeTests.m */, + 96A043DF1E4D232F00BD7013 /* HasMultipleVolumesTests.m */, 96F450761B38552200679597 /* IsPasswordProtectedTests.m */, 96F450781B385AF800679597 /* ListFilenamesTests.m */, + 967872731E460FA70048A54C /* ListVolumesTests.m */, 7A267F6D1F713B970004EAA6 /* ProgressReportingTests.m */, 96F450741B38527100679597 /* ValidatePasswordTests.m */, 96EA532F1B3B462D00F79DC6 /* iOSUnitTestHostApp */, @@ -849,12 +858,15 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 96A043DE1E4CC8D500BD7013 /* FirstVolumeTests.m in Sources */, 9699FA891B3D9B5700B6D373 /* URKArchiveTests.m in Sources */, 9699FA8C1B3D9B6F00B6D373 /* IsPasswordProtectedTests.m in Sources */, 9699FA8E1B3D9B6F00B6D373 /* ValidatePasswordTests.m in Sources */, + 967872741E460FA70048A54C /* ListVolumesTests.m in Sources */, 9699FA8B1B3D9B6F00B6D373 /* ExtractFilesTests.m in Sources */, 7A267F6E1F713B970004EAA6 /* ProgressReportingTests.m in Sources */, 9699FA8A1B3D9B6F00B6D373 /* URKArchiveTestCase.m in Sources */, + 96A043E01E4D232F00BD7013 /* HasMultipleVolumesTests.m in Sources */, 9699FA8D1B3D9B6F00B6D373 /* ListFilenamesTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0;