Skip to content

Commit

Permalink
Merge pull request #79 from abbeycode/lazy-file-iteration
Browse files Browse the repository at this point in the history
Added new method -iterateFileInfo:error: for lazy file info iteration
  • Loading branch information
abbeycode authored Jul 19, 2018
2 parents 4fea60b + 8e88f84 commit babe80c
Show file tree
Hide file tree
Showing 4 changed files with 315 additions and 23 deletions.
18 changes: 18 additions & 0 deletions Classes/URKArchive.h
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,24 @@ extern NSString *URKErrorDomain;
*/
- (nullable NSArray<URKFileInfo*> *)listFileInfo:(NSError **)error;

/**
* Iterates the header of the archive, calling the block with each archived file's info.
*
* WARNING: There is no filtering of duplicate header entries. If a file is listed twice, `action`
* will be called twice with that file's path
*
* @param action The action to perform using the data. Must be non-nil
*
* - *fileInfo* The metadata of the file within the archive
* - *stop* Set to YES to stop reading the archive
*
* @param error Contains an NSError object when there was an error reading the archive
*
* @return Returns NO if an error was encountered
*/
- (BOOL) iterateFileInfo:(void(^)(URKFileInfo *fileInfo, BOOL *stop))action
error:(NSError **)error;

/**
* Lists the URLs of volumes in a single- or multi-volume archive
*
Expand Down
102 changes: 79 additions & 23 deletions Classes/URKArchive.mm
Original file line number Diff line number Diff line change
Expand Up @@ -375,30 +375,58 @@ + (BOOL)urlIsARAR:(NSURL *)fileURL
{
URKCreateActivity("Listing File Info");

NSArray<URKFileInfo*> *allFileInfo = [self allFileInfo:error];
NSMutableSet<NSString*> *distinctFilenames = [NSMutableSet set];
NSMutableArray<URKFileInfo*> *distinctFileInfo = [NSMutableArray array];
NSError *innerError = nil;

BOOL wasSuccessful = [self iterateFileInfo:^(URKFileInfo * _Nonnull fileInfo, BOOL * _Nonnull stop) {
if (![distinctFilenames containsObject:fileInfo.filename]) {
[distinctFileInfo addObject:fileInfo];
[distinctFilenames addObject:fileInfo.filename];
} else {
URKLogDebug("Skipping %{public}@ from list of file info, since it's already represented (probably from another archive volume)", fileInfo.filename);
}
}
error:&innerError];

if (!allFileInfo) {
URKLogError("-allFileInfo: returned an error")
if (!wasSuccessful) {
URKLogError("Failed to iterate file info: %{public}@", innerError);

if (error && innerError) {
*error = innerError;
}

return nil;
}

URKLogDebug("Found %lu total file info items", (unsigned long)allFileInfo.count);
URKLogDebug("Found %lu file info items", (unsigned long)distinctFileInfo.count);
return [NSArray arrayWithArray:distinctFileInfo];
}

- (BOOL) iterateFileInfo:(void(^)(URKFileInfo *fileInfo, BOOL *stop))action
error:(NSError * __autoreleasing *)error
{
URKCreateActivity("Iterating File Info");
NSAssert(action != nil, @"'action' is a required argument");

NSError *innerError = nil;

URKLogDebug("Beginning to iterate through contents of %{public}@", self.filename);

NSMutableSet<NSString*> *distinctFilenames = [NSMutableSet set];
NSMutableArray<URKFileInfo*> *distinctFileInfo = [NSMutableArray array];
BOOL wasSuccessful = [self iterateAllFileInfo:action
error:&innerError];

for (URKFileInfo *info in allFileInfo) {
if (![distinctFilenames containsObject:info.filename]) {
[distinctFileInfo addObject:info];
[distinctFilenames addObject:info.filename];
} else {
URKLogDebug("Skipping %{public}@ from list of file info, since it's already represented (probably from another archive volume)", info.filename);
if (!wasSuccessful) {
URKLogError("Failed to iterate all file info: %{public}@", innerError);

if (error && innerError) {
*error = innerError;
}

return NO;
}

URKLogDebug("Found %lu distinct file info items", (unsigned long)distinctFileInfo.count);

return [NSArray arrayWithArray:distinctFileInfo];
return YES;
}

- (nullable NSArray<NSURL*> *)listVolumeURLs:(NSError * __autoreleasing *)error
Expand Down Expand Up @@ -1301,29 +1329,37 @@ - (BOOL)closeFile
return YES;
}

- (NSArray<URKFileInfo *> *) allFileInfo:(NSError * __autoreleasing *)error {
- (BOOL) iterateAllFileInfo:(void(^)(URKFileInfo *fileInfo, BOOL *stop))action
error:(NSError * __autoreleasing *)error
{
URKCreateActivity("-allFileInfo:");
NSAssert(action != nil, @"'action' is a required argument");

__block NSMutableArray *fileInfos = [NSMutableArray array];
__weak URKArchive *welf = self;

BOOL success = [self performActionWithArchiveOpen:^(NSError **innerError) {
BOOL wasSuccessful = [self performActionWithArchiveOpen:^(NSError **innerError) {
URKCreateActivity("Performing List Action");

int RHCode = 0, PFCode = 0;

URKLogDebug("Reading through RAR header looking for files...");

while ((RHCode = RARReadHeaderEx(welf.rarFile, welf.header)) == 0) {
URKLogDebug("Adding object");
[fileInfos addObject:[URKFileInfo fileInfo:welf.header]];
URKLogDebug("Calling iterateAllFileInfo handler");
BOOL shouldStop = NO;
URKFileInfo *info = [URKFileInfo fileInfo:welf.header];
action(info, &shouldStop);

if (shouldStop) {
URKLogDebug("iterateAllFileInfo got signal to stop");
return;
}

URKLogDebug("Skipping to next file...");
if ((PFCode = RARProcessFile(welf.rarFile, RAR_SKIP, NULL, NULL)) != 0) {
NSString *errorName = nil;
[self assignError:innerError code:(NSInteger)PFCode errorName:&errorName];
URKLogError("Error skipping to next header file: %{public}@ (%d)", errorName, PFCode);
fileInfos = nil;
return;
}
}
Expand All @@ -1332,11 +1368,31 @@ - (BOOL)closeFile
NSString *errorName = nil;
[self assignError:innerError code:RHCode errorName:&errorName];
URKLogError("Error reading RAR header: %{public}@ (%d)", errorName, RHCode);
fileInfos = nil;
}
} inMode:RAR_OM_LIST_INCSPLIT error:error];

if (!success || !fileInfos) {
return wasSuccessful;
}

- (NSArray<URKFileInfo *> *) allFileInfo:(NSError * __autoreleasing *)error {
URKCreateActivity("-allFileInfo:");

NSMutableArray *fileInfos = [NSMutableArray array];
NSError *innerError = nil;

URKLogDebug("Iterating all file info");
BOOL wasSuccessful = [self iterateAllFileInfo:^(URKFileInfo *fileInfo, BOOL *stop) {
[fileInfos addObject:fileInfo];
}
error:&innerError];

if (!wasSuccessful || !fileInfos) {
URKLogError("File info iteration was not successful: %{public}@", innerError);

if (error && innerError) {
*error = innerError;
}

return nil;
}

Expand Down
214 changes: 214 additions & 0 deletions Tests/IterateFileInfoTests.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
//
// IterateFileInfoTests.m
// UnrarKit
//
// Created by Dov Frankel on 5/22/18.
//
//

#import "URKArchiveTestCase.h"

@interface IterateFileInfoTests : URKArchiveTestCase

@end

@implementation IterateFileInfoTests


- (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"];
}];

NSArray *expectedFiles = [[expectedFileSet allObjects] sortedArrayUsingSelector:@selector(compare:)];

for (NSString *testArchiveName in testArchives) {
NSLog(@"Testing list files of archive %@", testArchiveName);
NSURL *testArchiveURL = self.testFileURLs[testArchiveName];

URKArchive *archive = [[URKArchive alloc] initWithURL:testArchiveURL error:nil];

NSError *error = nil;
NSMutableArray *iteratedFiles = [NSMutableArray array];
BOOL success = [archive iterateFileInfo:^(URKFileInfo * _Nonnull fileInfo, BOOL * _Nonnull stop) {
[iteratedFiles addObject:fileInfo.filename];
}
error:&error];

XCTAssertTrue(success, @"Error returned by iterateFileInfo");
XCTAssertNil(error, @"Error returned by iterateFileInfo");
XCTAssertEqual(iteratedFiles.count, expectedFileSet.count,
@"Incorrect number of files listed in archive");

for (NSInteger i = 0; i < iteratedFiles.count; i++) {
NSString *archiveFilename = iteratedFiles[i];
NSString *expectedFilename = expectedFiles[i];

NSLog(@"Testing for file %@", expectedFilename);

XCTAssertEqualObjects(archiveFilename, expectedFilename, @"Incorrect filename listed");
}
}
}

- (void)testIterateFileInfo_Unicode
{
NSSet *expectedFileSet = [self.unicodeFileURLs keysOfEntriesPassingTest:^BOOL(NSString *key, id obj, BOOL *stop) {
return ![key hasSuffix:@"rar"];
}];

NSArray *expectedFiles = [[expectedFileSet allObjects] sortedArrayUsingSelector:@selector(compare:)];

NSURL *testArchiveURL = self.unicodeFileURLs[@"Ⓣest Ⓐrchive.rar"];
URKArchive *archive = [[URKArchive alloc] initWithURL:testArchiveURL error:nil];

NSError *error = nil;
NSMutableArray *iteratedFiles = [NSMutableArray array];
BOOL success = [archive iterateFileInfo:^(URKFileInfo * _Nonnull fileInfo, BOOL * _Nonnull stop) {
[iteratedFiles addObject:fileInfo.filename];
}
error:&error];

XCTAssertTrue(success, @"Error returned by iterateFileInfo");
XCTAssertNil(error, @"Error returned by iterateFileInfo");
XCTAssertEqual(iteratedFiles.count, expectedFileSet.count,
@"Incorrect number of files listed in archive");

for (NSInteger i = 0; i < iteratedFiles.count; i++) {
NSString *archiveFilename = iteratedFiles[i];
NSString *expectedFilename = expectedFiles[i];

XCTAssertEqualObjects(archiveFilename, expectedFilename, @"Incorrect filename listed");
}
}

- (void)testIterateFileInfo_RAR5
{
NSArray *expectedFiles = @[@"yohoho_ws.txt",
@"nopw.txt"];

NSURL *testArchiveURL = self.testFileURLs[@"Test Archive (RAR5).rar"];
URKArchive *archive = [[URKArchive alloc] initWithURL:testArchiveURL error:nil];

NSError *error = nil;
NSMutableArray *iteratedFiles = [NSMutableArray array];
BOOL success = [archive iterateFileInfo:^(URKFileInfo * _Nonnull fileInfo, BOOL * _Nonnull stop) {
[iteratedFiles addObject:fileInfo.filename];
}
error:&error];

XCTAssertTrue(success, @"Error returned by iterateFileInfo");
XCTAssertNil(error, @"Error returned by iterateFileInfo");
XCTAssertEqual(iteratedFiles.count, expectedFiles.count,
@"Incorrect number of files listed in archive");

for (NSInteger i = 0; i < iteratedFiles.count; i++) {
NSString *archiveFilename = iteratedFiles[i];
NSString *expectedFilename = expectedFiles[i];

XCTAssertEqualObjects(archiveFilename, expectedFilename, @"Incorrect filename listed");
}
}

- (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"];
}];

NSArray *expectedFiles = [[expectedFileSet allObjects] sortedArrayUsingSelector:@selector(compare:)];

for (NSString *testArchiveName in testArchives) {
NSLog(@"Testing list files of archive %@", testArchiveName);
NSURL *testArchiveURL = self.testFileURLs[testArchiveName];

URKArchive *archiveNoPassword = [[URKArchive alloc] initWithURL:testArchiveURL error:nil];

NSError *passwordError = nil;
NSArray *filesInArchive = [archiveNoPassword listFilenames:&passwordError];

XCTAssertNotNil(passwordError, @"No error returned by listFilenames (no password given)");
XCTAssertNil(filesInArchive, @"List of files returned (no password given)");

URKArchive *archive = [[URKArchive alloc] initWithURL:testArchiveURL password:@"password" error:nil];

NSError *error = nil;
NSMutableArray *iteratedFiles = [NSMutableArray array];
BOOL success = [archive iterateFileInfo:^(URKFileInfo * _Nonnull fileInfo, BOOL * _Nonnull stop) {
[iteratedFiles addObject:fileInfo.filename];
}
error:&error];

XCTAssertTrue(success, @"Error returned by iterateFileInfo");
XCTAssertNil(error, @"Error returned by iterateFileInfo");
XCTAssertEqual(iteratedFiles.count, expectedFiles.count,
@"Incorrect number of files listed in archive");

for (NSInteger i = 0; i < iteratedFiles.count; i++) {
NSString *archiveFilename = iteratedFiles[i];
NSString *expectedFilename = expectedFiles[i];

NSLog(@"Testing for file %@", expectedFilename);

XCTAssertEqualObjects(archiveFilename, expectedFilename, @"Incorrect filename listed");
}
}
}

- (void)testIterateFileInfo_NoHeaderPasswordGiven
{
URKArchive *archive = [[URKArchive alloc] initWithURL:self.testFileURLs[@"Test Archive (Header Password).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 without password returned no error");
XCTAssertFalse(success, @"Iteration without password succeeded");
XCTAssertFalse(called, @"Iteration without password called action block");
XCTAssertEqual(error.code, URKErrorCodeMissingPassword, @"Unexpected error code returned");
}

- (void)testIterateFileInfo_NoFilePasswordGiven
{
URKArchive *archive = [[URKArchive alloc] initWithURL:self.testFileURLs[@"Test Archive (Password).rar"] error:nil];

NSError *error = nil;
__block BOOL called = NO;
BOOL success = [archive iterateFileInfo:^(URKFileInfo * _Nonnull fileInfo, BOOL * _Nonnull stop) {
called = YES;
}
error:&error];

XCTAssertNil(error, @"Iteration without file password failed");
XCTAssertTrue(success, @"Iteration without file password failed");
XCTAssertTrue(called, @"Iteration without file password didn't call action block");
}

- (void)testIterateFileInfo_InvalidArchive
{
URKArchive *archive = [[URKArchive alloc] initWithURL:self.testFileURLs[@"Test File A.txt"] 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");
XCTAssertFalse(called, @"Iteration for invalid archive called action block");
XCTAssertEqual(error.code, URKErrorCodeBadArchive, @"Unexpected error code returned");
}


@end
Loading

0 comments on commit babe80c

Please sign in to comment.