diff --git a/SSZipArchive.m b/SSZipArchive.m index d07bf93..a9f9a46 100644 --- a/SSZipArchive.m +++ b/SSZipArchive.m @@ -105,11 +105,36 @@ [delegate zipArchiveWillUnzipFileAtIndex:currentFileNumber totalFiles:(NSInteger)globalInfo.number_entry archivePath:path fileInfo:fileInfo]; } - + char *filename = (char *)malloc(fileInfo.size_filename + 1); unzGetCurrentFileInfo(zip, &fileInfo, filename, fileInfo.size_filename + 1, NULL, 0, NULL, 0); filename[fileInfo.size_filename] = '\0'; - + + // + // NOTE + // I used the ZIP spec from here: + // http://www.pkware.com/documents/casestudies/APPNOTE.TXT + // + // ...to deduce this method of detecting whether the file in the ZIP is a symbolic link. + // If it is, it is listed as a directory but has an uncompressed data size greater than + // zero (real directories have it equal to 0) and the included, uncompressed data is the + // symbolic link path. + // + + const uLong ZipDirectoryVersion = 10; + const uLong ZipCompressionMethodStore = 0; + + BOOL fileIsSymbolicLink = NO; + + if((fileInfo.version_needed == ZipDirectoryVersion) && // Is it a directory? + (fileInfo.compression_method == ZipCompressionMethodStore) && // Is it compressed? + (fileInfo.compressed_size > 0)) // Is there any data there? + { + fileIsSymbolicLink = YES; + } + + //NSLog(@"\"%s\" is symbolic link? %@", filename, fileIsSymbolicLink ? @"Yes." : @"No."); + // Check if it contains directory NSString *strPath = [NSString stringWithCString:filename encoding:NSUTF8StringEncoding]; BOOL isDirectory = NO; @@ -137,41 +162,72 @@ NSLog(@"[SSZipArchive] Error: %@", err.localizedDescription); } - [directoriesModificationDates addObject: [NSDictionary dictionaryWithObjectsAndKeys:fullPath, @"path", modDate, @"modDate", nil]]; + if(!fileIsSymbolicLink) + [directoriesModificationDates addObject: [NSDictionary dictionaryWithObjectsAndKeys:fullPath, @"path", modDate, @"modDate", nil]]; if ([fileManager fileExistsAtPath:fullPath] && !isDirectory && !overwrite) { unzCloseCurrentFile(zip); ret = unzGoToNextFile(zip); continue; } - - FILE *fp = fopen((const char*)[fullPath UTF8String], "wb"); - while (fp) { - int readBytes = unzReadCurrentFile(zip, buffer, 4096); + + if(!fileIsSymbolicLink) + { + FILE *fp = fopen((const char*)[fullPath UTF8String], "wb"); + while (fp) { + int readBytes = unzReadCurrentFile(zip, buffer, 4096); - if (readBytes > 0) { - fwrite(buffer, readBytes, 1, fp ); - } else { - break; - } - } - - if (fp) { - fclose(fp); - - // Set the original datetime property - if (fileInfo.dosDate != 0) { - NSDate *orgDate = [[self class] _dateWithMSDOSFormat:(UInt32)fileInfo.dosDate]; - NSDictionary *attr = [NSDictionary dictionaryWithObject:orgDate forKey:NSFileModificationDate]; - - if (attr) { - if ([fileManager setAttributes:attr ofItemAtPath:fullPath error:nil] == NO) { - // Can't set attributes - NSLog(@"[SSZipArchive] Failed to set attributes"); - } - } - } - } + if (readBytes > 0) { + fwrite(buffer, readBytes, 1, fp ); + } else { + break; + } + } + + if (fp) { + fclose(fp); + + // Set the original datetime property + if (fileInfo.dosDate != 0) { + NSDate *orgDate = [[self class] _dateWithMSDOSFormat:(UInt32)fileInfo.dosDate]; + NSDictionary *attr = [NSDictionary dictionaryWithObject:orgDate forKey:NSFileModificationDate]; + + if (attr) { + if ([fileManager setAttributes:attr ofItemAtPath:fullPath error:nil] == NO) { + // Can't set attributes + NSLog(@"[SSZipArchive] Failed to set attributes"); + } + } + } + } + } + else + { + // Get the path for the symbolic link + + NSURL* symlinkURL = [NSURL fileURLWithPath:fullPath]; + NSMutableString* destinationPath = [NSMutableString string]; + + int bytesRead = 0; + while((bytesRead = unzReadCurrentFile(zip, buffer, 4096)) > 0) + { + buffer[bytesRead] = 0; + [destinationPath appendString:[NSString stringWithUTF8String:(const char*)buffer]]; + } + + //NSLog(@"Symlinking to: %@", destinationPath); + + NSURL* destinationURL = [NSURL fileURLWithPath:destinationPath]; + + // Create the symbolic link + NSError* symlinkError = nil; + [fileManager createSymbolicLinkAtURL:symlinkURL withDestinationURL:destinationURL error:&symlinkError]; + + if(symlinkError != nil) + { + NSLog(@"Failed to create symbolic link at \"%@\" to \"%@\". Error: %@", symlinkURL.absoluteString, destinationURL.absoluteString, symlinkError.localizedDescription); + } + } unzCloseCurrentFile( zip ); ret = unzGoToNextFile( zip ); diff --git a/Tests/SSZipArchive.xcodeproj/project.pbxproj b/Tests/SSZipArchive.xcodeproj/project.pbxproj index f398753..d8c946b 100644 --- a/Tests/SSZipArchive.xcodeproj/project.pbxproj +++ b/Tests/SSZipArchive.xcodeproj/project.pbxproj @@ -19,6 +19,7 @@ B215FB6D143AD6FF003AC546 /* TestArchive.zip in Resources */ = {isa = PBXBuildFile; fileRef = B215FB6C143AD6FF003AC546 /* TestArchive.zip */; }; B23FCC7F1558F1B70026375C /* TestPasswordArchive.zip in Resources */ = {isa = PBXBuildFile; fileRef = B23FCC7E1558F1B70026375C /* TestPasswordArchive.zip */; }; C5AE4E64155A12760045F3ED /* IncorrectHeaders.zip in Resources */ = {isa = PBXBuildFile; fileRef = C5AE4E63155A12760045F3ED /* IncorrectHeaders.zip */; }; + C5AE4E6D155A8B010045F3ED /* SymbolicLink.zip in Resources */ = {isa = PBXBuildFile; fileRef = C5AE4E6C155A8B010045F3ED /* SymbolicLink.zip */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -42,6 +43,7 @@ B215FB6C143AD6FF003AC546 /* TestArchive.zip */ = {isa = PBXFileReference; lastKnownFileType = archive.zip; path = TestArchive.zip; sourceTree = ""; }; B23FCC7E1558F1B70026375C /* TestPasswordArchive.zip */ = {isa = PBXFileReference; lastKnownFileType = archive.zip; path = TestPasswordArchive.zip; sourceTree = ""; }; C5AE4E63155A12760045F3ED /* IncorrectHeaders.zip */ = {isa = PBXFileReference; lastKnownFileType = archive.zip; path = IncorrectHeaders.zip; sourceTree = ""; }; + C5AE4E6C155A8B010045F3ED /* SymbolicLink.zip */ = {isa = PBXFileReference; lastKnownFileType = archive.zip; path = SymbolicLink.zip; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -121,6 +123,7 @@ C5AE4E63155A12760045F3ED /* IncorrectHeaders.zip */, B215FB6C143AD6FF003AC546 /* TestArchive.zip */, B23FCC7E1558F1B70026375C /* TestPasswordArchive.zip */, + C5AE4E6C155A8B010045F3ED /* SymbolicLink.zip */, ); name = SSZipArchiveTests; sourceTree = ""; @@ -180,6 +183,7 @@ B215FB6D143AD6FF003AC546 /* TestArchive.zip in Resources */, B23FCC7F1558F1B70026375C /* TestPasswordArchive.zip in Resources */, C5AE4E64155A12760045F3ED /* IncorrectHeaders.zip in Resources */, + C5AE4E6D155A8B010045F3ED /* SymbolicLink.zip in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Tests/SSZipArchiveTests.m b/Tests/SSZipArchiveTests.m index 6bb542c..4fabf97 100644 --- a/Tests/SSZipArchiveTests.m +++ b/Tests/SSZipArchiveTests.m @@ -84,6 +84,23 @@ STAssertTrue([actualReadmeTxtMD5 isEqualToString:intendedReadmeTxtMD5], @"Readme.txt MD5 digest should match original."); } +- (void)testUnzippingWithSymlinkedFileInside { + + NSString* zipPath = [[NSBundle bundleForClass:[self class]] pathForResource:@"SymbolicLink" ofType:@"zip"]; + NSString* outputPath = [self _cachesPath:@"SymbolicLink"]; + + [SSZipArchive unzipFileAtPath:zipPath toDestination:outputPath delegate:self]; + + NSString* testSymlink = [outputPath stringByAppendingPathComponent:@"SymbolicLink/GitHub.app"]; + + NSError* error = nil; + NSString* symlinkPath = [[NSFileManager defaultManager] destinationOfSymbolicLinkAtPath:testSymlink error:&error]; + + bool symbolicLinkPersists = ((symlinkPath != nil) && [symlinkPath isEqualToString:@"/Applications/GitHub.app"]) && (error == nil); + + STAssertTrue(symbolicLinkPersists, @"Symbolic links should persist from the original archive to the outputted files."); +} + // Commented out to avoid checking in several gig file into the repository. Simply add a file named // `LargeArchive.zip` to the project and uncomment out these lines to test. diff --git a/Tests/SymbolicLink.zip b/Tests/SymbolicLink.zip new file mode 100644 index 0000000..952827d Binary files /dev/null and b/Tests/SymbolicLink.zip differ