Skip to content

Commit 6426519

Browse files
authored
Merge pull request #59 from abbeycode/multivolume
Add enhanced support for multi-volume archives
2 parents f34356f + 2bc269d commit 6426519

9 files changed

Lines changed: 574 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## 2.9
44

55
* Added support for `NSProgress` and `NSProgressReporting` in all extraction and iteration methods (Issue #34)
6+
* Added enhanced support for multivolume archives (PRs #59, #38 - Thanks to [@aonez](https://github.com/aonez) for the idea and implementation!)
67
* Switched to Travis Build Stages instead of the unofficial Travis-After-All (Issue #42)
78
* Added detailed logging using new unified logging framework. See [the readme](README.md) for more details (Issue #35)
89
* Added localized details to returned `NSError` objects (Issue #45)

Classes/URKArchive.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,11 @@ extern NSString *URKErrorDomain;
153153
*/
154154
@property(nullable, readonly) NSNumber *compressedSize;
155155

156+
/**
157+
* True if the file is one volume of a multi-part archive
158+
*/
159+
@property(readonly) BOOL hasMultipleVolumes;
160+
156161
/**
157162
* Can be used for progress reporting, but it's not necessary. You can also use
158163
* implicit progress reporting. If you don't use it, one will still be created,
@@ -281,6 +286,15 @@ extern NSString *URKErrorDomain;
281286
*/
282287
- (nullable NSArray<URKFileInfo*> *)listFileInfo:(NSError **)error;
283288

289+
/**
290+
* Lists the URLs of volumes in a single- or multi-volume archive
291+
*
292+
* @param error Contains an NSError object when there was an error reading the archive
293+
*
294+
* @return Returns the list of URLs of all volumes of the archive
295+
*/
296+
- (nullable NSArray<NSURL*> *)listVolumeURLs:(NSError **)error;
297+
284298
/**
285299
* Writes all files in the archive to the given path. Supports NSProgress for progress reporting, which also
286300
* allows cancellation in the middle of extraction. Use the progress property (as explained in the README) to

Classes/URKArchive.mm

Lines changed: 126 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,13 @@ - (instancetype)initWithFile:(NSURL *)fileURL password:(NSString*)password error
134134
if (error) {
135135
*error = nil;
136136
}
137-
137+
138+
NSURL *firstVolumeURL = [URKArchive firstVolumeURL:fileURL];
139+
if (firstVolumeURL && ![firstVolumeURL.absoluteString isEqualToString:fileURL.absoluteString]) {
140+
URKLogDebug("Overriding fileURL with first volume URL: %{public}@", firstVolumeURL);
141+
fileURL = firstVolumeURL;
142+
}
143+
138144
URKLogDebug("Initializing private fields");
139145

140146
NSError *bookmarkError = nil;
@@ -253,6 +259,21 @@ - (NSNumber *)compressedSize
253259
return [NSNumber numberWithUnsignedLongLong:attributes.fileSize];
254260
}
255261

262+
- (BOOL)hasMultipleVolumes
263+
{
264+
URKCreateActivity("Check If Multi-Volume Archive");
265+
266+
NSError *listError = nil;
267+
NSArray<NSURL*> *volumeURLs = [self listVolumeURLs:&listError];
268+
269+
if (!volumeURLs) {
270+
URKLogError("Error getting file volumes list: %{public}@", listError);
271+
return false;
272+
}
273+
274+
return volumeURLs.count > 1;
275+
}
276+
256277

257278

258279
#pragma mark - Zip file detection
@@ -380,6 +401,33 @@ + (BOOL)urlIsARAR:(NSURL *)fileURL
380401
return [NSArray arrayWithArray:fileInfos];
381402
}
382403

404+
- (nullable NSArray<NSURL*> *)listVolumeURLs:(NSError **)error
405+
{
406+
URKCreateActivity("Listing Volume URLs");
407+
408+
NSArray<URKFileInfo*> *listFileInfo = [self listFileInfo:error];
409+
410+
if (listFileInfo == nil) {
411+
return nil;
412+
}
413+
414+
NSMutableSet<NSURL*> *volumeURLs = [[NSMutableSet alloc] init];
415+
416+
for (URKFileInfo* info in listFileInfo) {
417+
NSURL *archiveURL = [NSURL fileURLWithPath:info.archiveName];
418+
419+
if (archiveURL) {
420+
[volumeURLs addObject:archiveURL];
421+
}
422+
}
423+
424+
SEL sortBySelector = @selector(path);
425+
NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:NSStringFromSelector(sortBySelector) ascending:YES];
426+
NSArray<NSURL*> *sortedVolumes = [volumeURLs sortedArrayUsingDescriptors:@[sortDescriptor]];
427+
428+
return sortedVolumes;
429+
}
430+
383431
- (BOOL)extractFilesTo:(NSString *)filePath
384432
overwrite:(BOOL)overwrite
385433
error:(NSError **)error
@@ -1262,6 +1310,8 @@ - (BOOL)headerContainsErrors:(NSError **)error
12621310

12631311
- (NSProgress *)beginProgressOperation:(NSUInteger)totalUnitCount
12641312
{
1313+
URKCreateActivity("-beginProgressOperation:");
1314+
12651315
NSProgress *progress;
12661316
progress = self.progress;
12671317
if (!progress) {
@@ -1279,4 +1329,79 @@ - (NSProgress *)beginProgressOperation:(NSUInteger)totalUnitCount
12791329
return progress;
12801330
}
12811331

1332+
+ (NSURL *)firstVolumeURL:(NSURL *)volumeURL {
1333+
URKCreateActivity("+firstVolumeURL:");
1334+
1335+
URKLogDebug("Checking if the file is part of a multi-volume archive...");
1336+
1337+
if (!volumeURL) {
1338+
URKLogError("+firstVolumeURL: nil volumeURL passed")
1339+
}
1340+
1341+
NSString *volumePath = volumeURL.path;
1342+
1343+
NSError *regexError = nil;
1344+
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"(.part)([0-9]+)(.rar)$"
1345+
options:NSRegularExpressionCaseInsensitive
1346+
error:&regexError];
1347+
if (!regex) {
1348+
URKLogError("Error constructing filename regex")
1349+
return nil;
1350+
}
1351+
1352+
NSString *firstVolumePath = nil;
1353+
1354+
// Check if it's following the current convention, like "Archive.part03.rar"
1355+
NSTextCheckingResult *match = [regex firstMatchInString:volumePath options:0 range:NSMakeRange(0, volumePath.length)];
1356+
if (match) {
1357+
URKLogDebug("The file is part of a multi-volume archive");
1358+
1359+
NSRange numberRange = [match rangeAtIndex:2];
1360+
NSString * partOne = [[@"" stringByPaddingToLength:numberRange.length - 1
1361+
withString:@"0"
1362+
startingAtIndex:0]
1363+
stringByAppendingString:@"1"];
1364+
1365+
NSString * regexTemplate = [NSString stringWithFormat:@"$1%@$3", partOne];
1366+
firstVolumePath = [regex stringByReplacingMatchesInString:volumePath
1367+
options:0
1368+
range:NSMakeRange(0, volumePath.length)
1369+
withTemplate:regexTemplate];
1370+
}
1371+
1372+
// It still might be a multivolume archive. Check for the legacy naming convention, like "Archive.r03"
1373+
else {
1374+
// After rXX, rar uses r-z and symbols like {}|~... so accepting anything but a number
1375+
NSError *legacyRegexError = nil;
1376+
regex = [NSRegularExpression regularExpressionWithPattern:@"(\\.[^0-9])([0-9]+)$"
1377+
options:NSRegularExpressionCaseInsensitive
1378+
error:&legacyRegexError];
1379+
1380+
if (!regex) {
1381+
URKLogError("Error constructing legacy filename regex")
1382+
return nil;
1383+
}
1384+
1385+
match = [regex firstMatchInString:volumePath options:0 range:NSMakeRange(0, volumePath.length)];
1386+
if (match) {
1387+
URKLogDebug("The archive is part of a legacy volume");
1388+
firstVolumePath = [[volumePath stringByDeletingPathExtension] stringByAppendingPathExtension:@"rar"];
1389+
}
1390+
}
1391+
1392+
// If it's a volume of either naming convention, use it
1393+
if (firstVolumePath) {
1394+
if ([[NSFileManager defaultManager] fileExistsAtPath:firstVolumePath]) {
1395+
URKLogDebug("First volume part %{public}@ found. Using as the main archive", firstVolumePath);
1396+
return [NSURL fileURLWithPath:firstVolumePath];
1397+
}
1398+
else {
1399+
URKLogInfo("First volume part not found: %{public}@. Skipping first volume selection", firstVolumePath);
1400+
return nil;
1401+
}
1402+
}
1403+
1404+
return volumeURL;
1405+
}
1406+
12821407
@end

Tests/FirstVolumeTests.m

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
//
2+
// FirstVolumeTests.m
3+
// UnrarKit
4+
//
5+
// Created by Dov Frankel on 2/9/17.
6+
//
7+
//
8+
9+
#import "URKArchiveTestCase.h"
10+
11+
@interface FirstVolumeTests : URKArchiveTestCase @end
12+
13+
@interface URKArchive (Tests)
14+
15+
// It's a private class method
16+
+ (NSURL *)firstVolumeURL:(NSURL *)volumeURL;
17+
18+
@end
19+
20+
@implementation FirstVolumeTests
21+
22+
- (void)testSingleVolume {
23+
NSURL *onlyVolumeArchiveURL = self.testFileURLs[@"Test Archive.rar"];
24+
NSURL *returnedFirstVolumeURL = [URKArchive firstVolumeURL:onlyVolumeArchiveURL];
25+
26+
XCTAssertNotNil(returnedFirstVolumeURL, @"No URL returned");
27+
XCTAssertEqualObjects(returnedFirstVolumeURL, onlyVolumeArchiveURL, @"URL changed even though it's a single volume archive");
28+
}
29+
30+
31+
#if !TARGET_OS_IPHONE
32+
- (void)testMultipleVolume_UseFirstVolume {
33+
NSArray<NSURL*> *volumeURLs = [self multiPartArchiveWithName:@"FirstVolumeTests-testMultipleVolume_UseFirstVolume.rar"];
34+
NSURL *firstVolumeURL = volumeURLs.firstObject;
35+
NSURL *returnedFirstVolumeURL = [URKArchive firstVolumeURL:firstVolumeURL];
36+
37+
XCTAssertNotNil(returnedFirstVolumeURL, @"No URL returned");
38+
XCTAssertEqualObjects(returnedFirstVolumeURL, firstVolumeURL, @"URL changed even though it was initialized with the first volume");
39+
}
40+
#endif
41+
42+
#if !TARGET_OS_IPHONE
43+
- (void)testMultipleVolume_UseMiddleVolume {
44+
NSArray<NSURL*> *volumeURLs = [self multiPartArchiveWithName:@"ListVolumesTests-testMultipleVolume_UseFirstVolume.rar"];
45+
NSURL *firstVolumeURL = volumeURLs.firstObject;
46+
NSURL *thirdVolumeURL = volumeURLs[2];
47+
48+
NSURL *returnedFirstVolumeURL = [URKArchive firstVolumeURL:thirdVolumeURL];
49+
50+
XCTAssertNotNil(returnedFirstVolumeURL, @"No URL returned");
51+
XCTAssertEqualObjects(returnedFirstVolumeURL.absoluteString, firstVolumeURL.absoluteString, @"Incorrect URL returned as first volume");
52+
}
53+
#endif
54+
55+
#if !TARGET_OS_IPHONE
56+
- (void)testMultipleVolume_UseMiddleVolume_OneHundredParts {
57+
NSArray<NSURL*> *volumeURLs = [self multiPartArchiveWithName:@"ListVolumesTests-testMultipleVolume_UseFirstVolume.rar" fileSize:2500000];
58+
59+
NSURL *firstVolumeURL = volumeURLs.firstObject;
60+
NSURL *hundredthVolumeURL = volumeURLs[100];
61+
62+
NSURL *returnedFirstVolumeURL = [URKArchive firstVolumeURL:hundredthVolumeURL];
63+
64+
XCTAssertNotNil(returnedFirstVolumeURL, @"No URL returned");
65+
XCTAssertEqualObjects(returnedFirstVolumeURL.absoluteString, firstVolumeURL.absoluteString, @"Incorrect URL returned as first volume");
66+
}
67+
#endif
68+
69+
#if !TARGET_OS_IPHONE
70+
- (void)testMultipleVolume_UseFirstVolume_OldNamingScheme {
71+
NSArray<NSURL*> *volumeURLs = [self multiPartArchiveOldSchemeWithName:@"FirstVolumeTests-testMultipleVolume_UseFirstVolume_OldNamingScheme.rar"];
72+
NSURL *firstVolumeURL = volumeURLs.firstObject;
73+
NSURL *returnedFirstVolumeURL = [URKArchive firstVolumeURL:firstVolumeURL];
74+
75+
XCTAssertNotNil(returnedFirstVolumeURL, @"No URL returned");
76+
XCTAssertEqualObjects(returnedFirstVolumeURL, firstVolumeURL, @"URL changed even though it was initialized with the first volume");
77+
}
78+
#endif
79+
80+
#if !TARGET_OS_IPHONE
81+
- (void)testMultipleVolume_UseMiddleVolume_OldNamingScheme {
82+
NSArray<NSURL*> *volumeURLs = [self multiPartArchiveOldSchemeWithName:@"FirstVolumeTests-testMultipleVolume_UseMiddleVolume_OldNamingScheme.rar"];
83+
NSURL *firstVolumeURL = volumeURLs.firstObject;
84+
NSURL *thirdVolumeURL = volumeURLs[2];
85+
86+
NSURL *returnedFirstVolumeURL = [URKArchive firstVolumeURL:thirdVolumeURL];
87+
88+
XCTAssertNotNil(returnedFirstVolumeURL, @"No URL returned");
89+
XCTAssertEqualObjects(returnedFirstVolumeURL.absoluteString, firstVolumeURL.absoluteString, @"Incorrect URL returned as first volume");
90+
}
91+
#endif
92+
93+
#if !TARGET_OS_IPHONE
94+
- (void)testMultipleVolume_FirstVolumeMissing {
95+
NSArray<NSURL*> *volumeURLs = [self multiPartArchiveWithName:@"ListVolumesTests-testMultipleVolume_FirstVolumeMissing.rar"];
96+
97+
NSError *deleteError = nil;
98+
[[NSFileManager defaultManager] removeItemAtURL:volumeURLs.firstObject
99+
error:&deleteError];
100+
XCTAssertNil(deleteError, @"Error deleting first volume of archive");
101+
102+
NSURL *firstVolumeURL = volumeURLs.firstObject;
103+
NSURL *returnedFirstVolumeURL = [URKArchive firstVolumeURL:firstVolumeURL];
104+
105+
XCTAssertNil(returnedFirstVolumeURL, @"First volume URL returned when it does not exist");
106+
}
107+
#endif
108+
109+
@end

Tests/HasMultipleVolumesTests.m

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
//
2+
// HasMultipleVolumesTests.m
3+
// UnrarKit
4+
//
5+
// Created by Dov Frankel on 2/9/17.
6+
//
7+
//
8+
9+
#import "URKArchiveTestCase.h"
10+
11+
@interface HasMultipleVolumesTests : URKArchiveTestCase
12+
13+
@end
14+
15+
@implementation HasMultipleVolumesTests
16+
17+
- (void)testSingleVolume {
18+
NSURL *testArchiveURL = self.testFileURLs[@"Test Archive.rar"];
19+
URKArchive *archive = [[URKArchive alloc] initWithURL:testArchiveURL error:nil];
20+
21+
BOOL hasMultipleParts = archive.hasMultipleVolumes;
22+
23+
XCTAssertFalse(hasMultipleParts, @"Single-volume archive reported to have multiple parts");
24+
}
25+
26+
#if !TARGET_OS_IPHONE
27+
- (void)testMultipleVolume_UseFirstVolume {
28+
NSArray<NSURL*> *volumeURLs = [self multiPartArchiveWithName:@"HasMultipleVolumesTests-testMultipleVolume_UseFirstVolume.rar"];
29+
URKArchive *archive = [[URKArchive alloc] initWithURL:volumeURLs.firstObject error:nil];
30+
31+
BOOL hasMultipleParts = archive.hasMultipleVolumes;
32+
XCTAssertTrue(hasMultipleParts, @"Multi-volume archive's first part not reported to have multiple volumes");
33+
}
34+
#endif
35+
36+
#if !TARGET_OS_IPHONE
37+
- (void)testMultipleVolume_UseMiddleVolume {
38+
NSArray<NSURL*> *volumeURLs = [self multiPartArchiveWithName:@"HasMultipleVolumesTests-testMultipleVolume_UseMiddleVolume.rar"];
39+
URKArchive *archive = [[URKArchive alloc] initWithURL:volumeURLs[2] error:nil];
40+
41+
BOOL hasMultipleParts = archive.hasMultipleVolumes;
42+
XCTAssertTrue(hasMultipleParts, @"Multi-volume archive's middle part not reported to have multiple volumes");
43+
}
44+
#endif
45+
46+
#if !TARGET_OS_IPHONE
47+
- (void)testMultipleVolume_UseFirstVolume_OldNamingScheme {
48+
NSArray<NSURL*> *volumeURLs = [self multiPartArchiveOldSchemeWithName:@"HasMultipleVolumesTests-testMultipleVolume_UseFirstVolume_OldNamingScheme.rar"];
49+
URKArchive *archive = [[URKArchive alloc] initWithURL:volumeURLs.firstObject error:nil];
50+
51+
BOOL hasMultipleParts = archive.hasMultipleVolumes;
52+
XCTAssertTrue(hasMultipleParts, @"Multi-volume archive's first part not reported to have multiple volumes");
53+
}
54+
#endif
55+
56+
#if !TARGET_OS_IPHONE
57+
- (void)testMultipleVolume_UseMiddleVolume_OldNamingScheme {
58+
NSArray<NSURL*> *volumeURLs = [self multiPartArchiveOldSchemeWithName:@"HasMultipleVolumesTests-testMultipleVolume_UseMiddleVolume_OldNamingScheme.rar"];
59+
URKArchive *archive = [[URKArchive alloc] initWithURL:volumeURLs[2] error:nil];
60+
61+
BOOL hasMultipleParts = archive.hasMultipleVolumes;
62+
XCTAssertTrue(hasMultipleParts, @"Multi-volume archive's middle part not reported to have multiple volumes");
63+
}
64+
#endif
65+
66+
- (void)testInvalidArchive {
67+
URKArchive *archive = [[URKArchive alloc] initWithURL:self.testFileURLs[@"Test File A.txt"] error:nil];
68+
69+
BOOL hasMultipleParts = archive.hasMultipleVolumes;
70+
XCTAssertFalse(hasMultipleParts, @"Invalid archive reported to have multiple volumes");
71+
}
72+
73+
@end

0 commit comments

Comments
 (0)