5 // Created by Sam Soffes on 7/21/10.
6 // Copyright (c) Sam Soffes 2010-2015. All rights reserved.
9 #import "SSZipArchive.h"
12 #include "minishared.h"
16 NSString *const SSZipArchiveErrorDomain = @"SSZipArchiveErrorDomain";
20 int _zipOpenEntry(zipFile entry, NSString *name, const zip_fileinfo *zipfi, int level, NSString *password, BOOL aes);
21 BOOL _fileIsSymbolicLink(const unz_file_info *fileInfo);
24 // Xcode 7- compatibility
25 #define API_AVAILABLE(...)
28 @interface NSData(SSZipArchive)
29 - (NSString *)_base64RFC4648 API_AVAILABLE(macos(10.9), ios(7.0), watchos(2.0), tvos(9.0));
30 - (NSString *)_hexString;
33 @interface SSZipArchive ()
34 - (instancetype)init NS_DESIGNATED_INITIALIZER;
37 @implementation SSZipArchive
44 #pragma mark - Password check
46 + (BOOL)isFilePasswordProtectedAtPath:(NSString *)path {
48 zipFile zip = unzOpen(path.fileSystemRepresentation);
53 int ret = unzGoToFirstFile(zip);
56 ret = unzOpenCurrentFile(zip);
60 unz_file_info fileInfo = {};
61 ret = unzGetCurrentFileInfo(zip, &fileInfo, NULL, 0, NULL, 0, NULL, 0);
64 } else if ((fileInfo.flag & 1) == 1) {
68 unzCloseCurrentFile(zip);
69 ret = unzGoToNextFile(zip);
70 } while (ret == UNZ_OK);
76 + (BOOL)isPasswordValidForArchiveAtPath:(NSString *)path password:(NSString *)pw error:(NSError **)error {
81 zipFile zip = unzOpen(path.fileSystemRepresentation);
84 *error = [NSError errorWithDomain:SSZipArchiveErrorDomain
85 code:SSZipArchiveErrorCodeFailedOpenZipFile
86 userInfo:@{NSLocalizedDescriptionKey: @"failed to open zip file"}];
91 int ret = unzGoToFirstFile(zip);
95 ret = unzOpenCurrentFile(zip);
97 ret = unzOpenCurrentFilePassword(zip, [pw cStringUsingEncoding:NSUTF8StringEncoding]);
100 if (ret != UNZ_BADPASSWORD) {
102 *error = [NSError errorWithDomain:SSZipArchiveErrorDomain
103 code:SSZipArchiveErrorCodeFailedOpenFileInZip
104 userInfo:@{NSLocalizedDescriptionKey: @"failed to open first file in zip file"}];
109 unz_file_info fileInfo = {};
110 ret = unzGetCurrentFileInfo(zip, &fileInfo, NULL, 0, NULL, 0, NULL, 0);
113 *error = [NSError errorWithDomain:SSZipArchiveErrorDomain
114 code:SSZipArchiveErrorCodeFileInfoNotLoadable
115 userInfo:@{NSLocalizedDescriptionKey: @"failed to retrieve info for file"}];
118 } else if ((fileInfo.flag & 1) == 1) {
119 unsigned char buffer[10] = {0};
120 int readBytes = unzReadCurrentFile(zip, buffer, (unsigned)MIN(10UL,fileInfo.uncompressed_size));
122 // Let's assume error Z_DATA_ERROR is caused by an invalid password
123 // Let's assume other errors are caused by Content Not Readable
124 if (readBytes != Z_DATA_ERROR) {
126 *error = [NSError errorWithDomain:SSZipArchiveErrorDomain
127 code:SSZipArchiveErrorCodeFileContentNotReadable
128 userInfo:@{NSLocalizedDescriptionKey: @"failed to read contents of file entry"}];
136 unzCloseCurrentFile(zip);
137 ret = unzGoToNextFile(zip);
138 } while (ret == UNZ_OK);
141 // No password required
145 #pragma mark - Unzipping
147 + (BOOL)unzipFileAtPath:(NSString *)path toDestination:(NSString *)destination
149 return [self unzipFileAtPath:path toDestination:destination delegate:nil];
152 + (BOOL)unzipFileAtPath:(NSString *)path toDestination:(NSString *)destination overwrite:(BOOL)overwrite password:(nullable NSString *)password error:(NSError **)error
154 return [self unzipFileAtPath:path toDestination:destination preserveAttributes:YES overwrite:overwrite password:password error:error delegate:nil progressHandler:nil completionHandler:nil];
157 + (BOOL)unzipFileAtPath:(NSString *)path toDestination:(NSString *)destination delegate:(nullable id<SSZipArchiveDelegate>)delegate
159 return [self unzipFileAtPath:path toDestination:destination preserveAttributes:YES overwrite:YES password:nil error:nil delegate:delegate progressHandler:nil completionHandler:nil];
162 + (BOOL)unzipFileAtPath:(NSString *)path
163 toDestination:(NSString *)destination
164 overwrite:(BOOL)overwrite
165 password:(nullable NSString *)password
166 error:(NSError **)error
167 delegate:(nullable id<SSZipArchiveDelegate>)delegate
169 return [self unzipFileAtPath:path toDestination:destination preserveAttributes:YES overwrite:overwrite password:password error:error delegate:delegate progressHandler:nil completionHandler:nil];
172 + (BOOL)unzipFileAtPath:(NSString *)path
173 toDestination:(NSString *)destination
174 overwrite:(BOOL)overwrite
175 password:(NSString *)password
176 progressHandler:(void (^)(NSString *entry, unz_file_info zipInfo, long entryNumber, long total))progressHandler
177 completionHandler:(void (^)(NSString *path, BOOL succeeded, NSError * _Nullable error))completionHandler
179 return [self unzipFileAtPath:path toDestination:destination preserveAttributes:YES overwrite:overwrite password:password error:nil delegate:nil progressHandler:progressHandler completionHandler:completionHandler];
182 + (BOOL)unzipFileAtPath:(NSString *)path
183 toDestination:(NSString *)destination
184 progressHandler:(void (^_Nullable)(NSString *entry, unz_file_info zipInfo, long entryNumber, long total))progressHandler
185 completionHandler:(void (^_Nullable)(NSString *path, BOOL succeeded, NSError * _Nullable error))completionHandler
187 return [self unzipFileAtPath:path toDestination:destination preserveAttributes:YES overwrite:YES password:nil error:nil delegate:nil progressHandler:progressHandler completionHandler:completionHandler];
190 + (BOOL)unzipFileAtPath:(NSString *)path
191 toDestination:(NSString *)destination
192 preserveAttributes:(BOOL)preserveAttributes
193 overwrite:(BOOL)overwrite
194 password:(nullable NSString *)password
195 error:(NSError * *)error
196 delegate:(nullable id<SSZipArchiveDelegate>)delegate
198 return [self unzipFileAtPath:path toDestination:destination preserveAttributes:preserveAttributes overwrite:overwrite password:password error:error delegate:delegate progressHandler:nil completionHandler:nil];
201 + (BOOL)unzipFileAtPath:(NSString *)path
202 toDestination:(NSString *)destination
203 preserveAttributes:(BOOL)preserveAttributes
204 overwrite:(BOOL)overwrite
205 password:(nullable NSString *)password
206 error:(NSError **)error
207 delegate:(nullable id<SSZipArchiveDelegate>)delegate
208 progressHandler:(void (^_Nullable)(NSString *entry, unz_file_info zipInfo, long entryNumber, long total))progressHandler
209 completionHandler:(void (^_Nullable)(NSString *path, BOOL succeeded, NSError * _Nullable error))completionHandler
211 return [self unzipFileAtPath:path toDestination:destination preserveAttributes:preserveAttributes overwrite:overwrite nestedZipLevel:0 password:password error:error delegate:delegate progressHandler:progressHandler completionHandler:completionHandler];
214 + (BOOL)unzipFileAtPath:(NSString *)path
215 toDestination:(NSString *)destination
216 preserveAttributes:(BOOL)preserveAttributes
217 overwrite:(BOOL)overwrite
218 nestedZipLevel:(NSInteger)nestedZipLevel
219 password:(nullable NSString *)password
220 error:(NSError **)error
221 delegate:(nullable id<SSZipArchiveDelegate>)delegate
222 progressHandler:(void (^_Nullable)(NSString *entry, unz_file_info zipInfo, long entryNumber, long total))progressHandler
223 completionHandler:(void (^_Nullable)(NSString *path, BOOL succeeded, NSError * _Nullable error))completionHandler
225 // Guard against empty strings
226 if (path.length == 0 || destination.length == 0)
228 NSDictionary *userInfo = @{NSLocalizedDescriptionKey: @"received invalid argument(s)"};
229 NSError *err = [NSError errorWithDomain:SSZipArchiveErrorDomain code:SSZipArchiveErrorCodeInvalidArguments userInfo:userInfo];
234 if (completionHandler)
236 completionHandler(nil, NO, err);
242 zipFile zip = unzOpen(path.fileSystemRepresentation);
245 NSDictionary *userInfo = @{NSLocalizedDescriptionKey: @"failed to open zip file"};
246 NSError *err = [NSError errorWithDomain:SSZipArchiveErrorDomain code:SSZipArchiveErrorCodeFailedOpenZipFile userInfo:userInfo];
251 if (completionHandler)
253 completionHandler(nil, NO, err);
258 NSDictionary * fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:path error:nil];
259 unsigned long long fileSize = [fileAttributes[NSFileSize] unsignedLongLongValue];
260 unsigned long long currentPosition = 0;
262 unz_global_info globalInfo = {};
263 unzGetGlobalInfo(zip, &globalInfo);
267 ret = unzGoToFirstFile(zip);
268 if (ret != UNZ_OK && ret != UNZ_END_OF_LIST_OF_FILE)
270 NSDictionary *userInfo = @{NSLocalizedDescriptionKey: @"failed to open first file in zip file"};
271 NSError *err = [NSError errorWithDomain:SSZipArchiveErrorDomain code:SSZipArchiveErrorCodeFailedOpenFileInZip userInfo:userInfo];
276 if (completionHandler)
278 completionHandler(nil, NO, err);
286 unsigned char buffer[4096] = {0};
287 NSFileManager *fileManager = [NSFileManager defaultManager];
288 NSMutableArray<NSDictionary *> *directoriesModificationDates = [[NSMutableArray alloc] init];
291 if ([delegate respondsToSelector:@selector(zipArchiveWillUnzipArchiveAtPath:zipInfo:)]) {
292 [delegate zipArchiveWillUnzipArchiveAtPath:path zipInfo:globalInfo];
294 if ([delegate respondsToSelector:@selector(zipArchiveProgressEvent:total:)]) {
295 [delegate zipArchiveProgressEvent:currentPosition total:fileSize];
298 NSInteger currentFileNumber = -1;
299 NSError *unzippingError;
302 if (ret == UNZ_END_OF_LIST_OF_FILE)
305 if (password.length == 0) {
306 ret = unzOpenCurrentFile(zip);
308 ret = unzOpenCurrentFilePassword(zip, [password cStringUsingEncoding:NSUTF8StringEncoding]);
312 unzippingError = [NSError errorWithDomain:@"SSZipArchiveErrorDomain" code:SSZipArchiveErrorCodeFailedOpenFileInZip userInfo:@{NSLocalizedDescriptionKey: @"failed to open file in zip file"}];
317 // Reading data and write to file
318 unz_file_info fileInfo;
319 memset(&fileInfo, 0, sizeof(unz_file_info));
321 ret = unzGetCurrentFileInfo(zip, &fileInfo, NULL, 0, NULL, 0, NULL, 0);
323 unzippingError = [NSError errorWithDomain:@"SSZipArchiveErrorDomain" code:SSZipArchiveErrorCodeFileInfoNotLoadable userInfo:@{NSLocalizedDescriptionKey: @"failed to retrieve info for file"}];
325 unzCloseCurrentFile(zip);
329 currentPosition += fileInfo.compressed_size;
332 if ([delegate respondsToSelector:@selector(zipArchiveShouldUnzipFileAtIndex:totalFiles:archivePath:fileInfo:)]) {
333 if (![delegate zipArchiveShouldUnzipFileAtIndex:currentFileNumber
334 totalFiles:(NSInteger)globalInfo.number_entry
336 fileInfo:fileInfo]) {
342 if ([delegate respondsToSelector:@selector(zipArchiveWillUnzipFileAtIndex:totalFiles:archivePath:fileInfo:)]) {
343 [delegate zipArchiveWillUnzipFileAtIndex:currentFileNumber totalFiles:(NSInteger)globalInfo.number_entry
344 archivePath:path fileInfo:fileInfo];
346 if ([delegate respondsToSelector:@selector(zipArchiveProgressEvent:total:)]) {
347 [delegate zipArchiveProgressEvent:(NSInteger)currentPosition total:(NSInteger)fileSize];
350 char *filename = (char *)malloc(fileInfo.size_filename + 1);
351 if (filename == NULL)
357 unzGetCurrentFileInfo(zip, &fileInfo, filename, fileInfo.size_filename + 1, NULL, 0, NULL, 0);
358 filename[fileInfo.size_filename] = '\0';
360 BOOL fileIsSymbolicLink = _fileIsSymbolicLink(&fileInfo);
362 NSString * strPath = [SSZipArchive _filenameStringWithCString:filename size:fileInfo.size_filename];
363 if ([strPath hasPrefix:@"__MACOSX/"]) {
364 // ignoring resource forks: https://superuser.com/questions/104500/what-is-macosx-folder
365 unzCloseCurrentFile(zip);
366 ret = unzGoToNextFile(zip);
369 if (!strPath.length) {
370 // if filename data is unsalvageable, we default to currentFileNumber
371 strPath = @(currentFileNumber).stringValue;
374 // Check if it contains directory
375 BOOL isDirectory = NO;
376 if (filename[fileInfo.size_filename-1] == '/' || filename[fileInfo.size_filename-1] == '\\') {
382 if ([strPath rangeOfCharacterFromSet:[NSCharacterSet characterSetWithCharactersInString:@"/\\"]].location != NSNotFound) {
383 strPath = [strPath stringByReplacingOccurrencesOfString:@"\\" withString:@"/"];
386 NSString *fullPath = [destination stringByAppendingPathComponent:strPath];
388 NSDictionary *directoryAttr;
389 if (preserveAttributes) {
390 NSDate *modDate = [[self class] _dateWithMSDOSFormat:(UInt32)fileInfo.dos_date];
391 directoryAttr = @{NSFileCreationDate: modDate, NSFileModificationDate: modDate};
392 [directoriesModificationDates addObject: @{@"path": fullPath, @"modDate": modDate}];
395 [fileManager createDirectoryAtPath:fullPath withIntermediateDirectories:YES attributes:directoryAttr error:&err];
397 [fileManager createDirectoryAtPath:fullPath.stringByDeletingLastPathComponent withIntermediateDirectories:YES attributes:directoryAttr error:&err];
400 if ([err.domain isEqualToString:NSCocoaErrorDomain] &&
402 unzippingError = err;
403 unzCloseCurrentFile(zip);
407 NSLog(@"[SSZipArchive] Error: %@", err.localizedDescription);
410 if ([fileManager fileExistsAtPath:fullPath] && !isDirectory && !overwrite) {
411 //FIXME: couldBe CRC Check?
412 unzCloseCurrentFile(zip);
413 ret = unzGoToNextFile(zip);
417 if (!fileIsSymbolicLink) {
418 // ensure we are not creating stale file entries
419 int readBytes = unzReadCurrentFile(zip, buffer, 4096);
420 if (readBytes >= 0) {
421 FILE *fp = fopen(fullPath.fileSystemRepresentation, "wb");
424 if (0 == fwrite(buffer, readBytes, 1, fp)) {
426 NSString *message = [NSString stringWithFormat:@"Failed to write file (check your free space)"];
427 NSLog(@"[SSZipArchive] %@", message);
429 unzippingError = [NSError errorWithDomain:@"SSZipArchiveErrorDomain" code:SSZipArchiveErrorCodeFailedToWriteFile userInfo:@{NSLocalizedDescriptionKey: message}];
436 readBytes = unzReadCurrentFile(zip, buffer, 4096);
438 // Let's assume error Z_DATA_ERROR is caused by an invalid password
439 // Let's assume other errors are caused by Content Not Readable
448 && [fullPath.pathExtension.lowercaseString isEqualToString:@"zip"]
449 && [self unzipFileAtPath:fullPath
450 toDestination:fullPath.stringByDeletingLastPathComponent
451 preserveAttributes:preserveAttributes
453 nestedZipLevel:nestedZipLevel - 1
458 completionHandler:nil]) {
459 [directoriesModificationDates removeLastObject];
460 [[NSFileManager defaultManager] removeItemAtPath:fullPath error:nil];
461 } else if (preserveAttributes) {
463 // Set the original datetime property
464 if (fileInfo.dos_date != 0) {
465 NSDate *orgDate = [[self class] _dateWithMSDOSFormat:(UInt32)fileInfo.dos_date];
466 NSDictionary *attr = @{NSFileModificationDate: orgDate};
469 if (![fileManager setAttributes:attr ofItemAtPath:fullPath error:nil]) {
470 // Can't set attributes
471 NSLog(@"[SSZipArchive] Failed to set attributes - whilst setting modification date");
476 // Set the original permissions on the file (+read/write to solve #293)
477 uLong permissions = fileInfo.external_fa >> 16 | 0b110000000;
478 if (permissions != 0) {
479 // Store it into a NSNumber
480 NSNumber *permissionsValue = @(permissions);
482 // Retrieve any existing attributes
483 NSMutableDictionary *attrs = [[NSMutableDictionary alloc] initWithDictionary:[fileManager attributesOfItemAtPath:fullPath error:nil]];
485 // Set the value in the attributes dict
486 attrs[NSFilePosixPermissions] = permissionsValue;
489 if (![fileManager setAttributes:attrs ofItemAtPath:fullPath error:nil]) {
490 // Unable to set the permissions attribute
491 NSLog(@"[SSZipArchive] Failed to set attributes - whilst setting permissions");
498 // if we couldn't open file descriptor we can validate global errno to see the reason
499 if (errno == ENOSPC) {
500 NSError *enospcError = [NSError errorWithDomain:NSPOSIXErrorDomain
503 unzippingError = enospcError;
504 unzCloseCurrentFile(zip);
510 // Let's assume error Z_DATA_ERROR is caused by an invalid password
511 // Let's assume other errors are caused by Content Not Readable
518 // Assemble the path for the symbolic link
519 NSMutableString *destinationPath = [NSMutableString string];
521 while ((bytesRead = unzReadCurrentFile(zip, buffer, 4096)) > 0)
523 buffer[bytesRead] = 0;
524 [destinationPath appendString:@((const char *)buffer)];
527 // Let's assume error Z_DATA_ERROR is caused by an invalid password
528 // Let's assume other errors are caused by Content Not Readable
533 // Check if the symlink exists and delete it if we're overwriting
536 if ([fileManager fileExistsAtPath:fullPath])
538 NSError *error = nil;
539 BOOL removeSuccess = [fileManager removeItemAtPath:fullPath error:&error];
542 NSString *message = [NSString stringWithFormat:@"Failed to delete existing symbolic link at \"%@\"", error.localizedDescription];
543 NSLog(@"[SSZipArchive] %@", message);
545 unzippingError = [NSError errorWithDomain:SSZipArchiveErrorDomain code:error.code userInfo:@{NSLocalizedDescriptionKey: message}];
550 // Create the symbolic link (making sure it stays relative if it was relative before)
551 int symlinkError = symlink([destinationPath cStringUsingEncoding:NSUTF8StringEncoding],
552 [fullPath cStringUsingEncoding:NSUTF8StringEncoding]);
554 if (symlinkError != 0)
556 // Bubble the error up to the completion handler
557 NSString *message = [NSString stringWithFormat:@"Failed to create symbolic link at \"%@\" to \"%@\" - symlink() error code: %d", fullPath, destinationPath, errno];
558 NSLog(@"[SSZipArchive] %@", message);
560 unzippingError = [NSError errorWithDomain:NSPOSIXErrorDomain code:symlinkError userInfo:@{NSLocalizedDescriptionKey: message}];
564 crc_ret = unzCloseCurrentFile(zip);
565 if (crc_ret == UNZ_CRCERROR) {
570 ret = unzGoToNextFile(zip);
573 if ([delegate respondsToSelector:@selector(zipArchiveDidUnzipFileAtIndex:totalFiles:archivePath:fileInfo:)]) {
574 [delegate zipArchiveDidUnzipFileAtIndex:currentFileNumber totalFiles:(NSInteger)globalInfo.number_entry
575 archivePath:path fileInfo:fileInfo];
576 } else if ([delegate respondsToSelector: @selector(zipArchiveDidUnzipFileAtIndex:totalFiles:archivePath:unzippedFilePath:)]) {
577 [delegate zipArchiveDidUnzipFileAtIndex: currentFileNumber totalFiles: (NSInteger)globalInfo.number_entry
578 archivePath:path unzippedFilePath: fullPath];
583 progressHandler(strPath, fileInfo, currentFileNumber, globalInfo.number_entry);
586 } while (ret == UNZ_OK && success);
591 // The process of decompressing the .zip archive causes the modification times on the folders
592 // to be set to the present time. So, when we are done, they need to be explicitly set.
593 // set the modification date on all of the directories.
594 if (success && preserveAttributes) {
596 for (NSDictionary * d in directoriesModificationDates) {
597 if (![[NSFileManager defaultManager] setAttributes:@{NSFileModificationDate: d[@"modDate"]} ofItemAtPath:d[@"path"] error:&err]) {
598 NSLog(@"[SSZipArchive] Set attributes failed for directory: %@.", d[@"path"]);
601 NSLog(@"[SSZipArchive] Error setting directory file modification date attribute: %@", err.localizedDescription);
607 if (success && [delegate respondsToSelector:@selector(zipArchiveDidUnzipArchiveAtPath:zipInfo:unzippedPath:)]) {
608 [delegate zipArchiveDidUnzipArchiveAtPath:path zipInfo:globalInfo unzippedPath:destination];
610 // final progress event = 100%
611 if (!canceled && [delegate respondsToSelector:@selector(zipArchiveProgressEvent:total:)]) {
612 [delegate zipArchiveProgressEvent:fileSize total:fileSize];
615 NSError *retErr = nil;
616 if (crc_ret == UNZ_CRCERROR)
618 NSDictionary *userInfo = @{NSLocalizedDescriptionKey: @"crc check failed for file"};
619 retErr = [NSError errorWithDomain:SSZipArchiveErrorDomain code:SSZipArchiveErrorCodeFileInfoNotLoadable userInfo:userInfo];
623 if (unzippingError) {
624 *error = unzippingError;
630 if (completionHandler)
632 if (unzippingError) {
633 completionHandler(path, success, unzippingError);
637 completionHandler(path, success, retErr);
643 #pragma mark - Zipping
644 + (BOOL)createZipFileAtPath:(NSString *)path withFilesAtPaths:(NSArray<NSString *> *)paths
646 return [SSZipArchive createZipFileAtPath:path withFilesAtPaths:paths withPassword:nil];
648 + (BOOL)createZipFileAtPath:(NSString *)path withContentsOfDirectory:(NSString *)directoryPath {
649 return [SSZipArchive createZipFileAtPath:path withContentsOfDirectory:directoryPath withPassword:nil];
652 + (BOOL)createZipFileAtPath:(NSString *)path withContentsOfDirectory:(NSString *)directoryPath keepParentDirectory:(BOOL)keepParentDirectory {
653 return [SSZipArchive createZipFileAtPath:path withContentsOfDirectory:directoryPath keepParentDirectory:keepParentDirectory withPassword:nil];
656 + (BOOL)createZipFileAtPath:(NSString *)path withFilesAtPaths:(NSArray<NSString *> *)paths withPassword:(NSString *)password
658 SSZipArchive *zipArchive = [[SSZipArchive alloc] initWithPath:path];
659 BOOL success = [zipArchive open];
661 for (NSString *filePath in paths) {
662 success &= [zipArchive writeFile:filePath withPassword:password];
664 success &= [zipArchive close];
669 + (BOOL)createZipFileAtPath:(NSString *)path withContentsOfDirectory:(NSString *)directoryPath withPassword:(nullable NSString *)password {
670 return [SSZipArchive createZipFileAtPath:path withContentsOfDirectory:directoryPath keepParentDirectory:NO withPassword:password];
674 + (BOOL)createZipFileAtPath:(NSString *)path withContentsOfDirectory:(NSString *)directoryPath keepParentDirectory:(BOOL)keepParentDirectory withPassword:(nullable NSString *)password {
675 return [SSZipArchive createZipFileAtPath:path
676 withContentsOfDirectory:directoryPath
677 keepParentDirectory:keepParentDirectory
678 withPassword:password
679 andProgressHandler:nil
683 + (BOOL)createZipFileAtPath:(NSString *)path
684 withContentsOfDirectory:(NSString *)directoryPath
685 keepParentDirectory:(BOOL)keepParentDirectory
686 withPassword:(nullable NSString *)password
687 andProgressHandler:(void(^ _Nullable)(NSUInteger entryNumber, NSUInteger total))progressHandler {
688 return [self createZipFileAtPath:path withContentsOfDirectory:directoryPath keepParentDirectory:keepParentDirectory compressionLevel:Z_DEFAULT_COMPRESSION password:password AES:YES progressHandler:progressHandler];
691 + (BOOL)createZipFileAtPath:(NSString *)path
692 withContentsOfDirectory:(NSString *)directoryPath
693 keepParentDirectory:(BOOL)keepParentDirectory
694 compressionLevel:(int)compressionLevel
695 password:(nullable NSString *)password
697 progressHandler:(void(^ _Nullable)(NSUInteger entryNumber, NSUInteger total))progressHandler {
699 SSZipArchive *zipArchive = [[SSZipArchive alloc] initWithPath:path];
700 BOOL success = [zipArchive open];
702 // use a local fileManager (queue/thread compatibility)
703 NSFileManager *fileManager = [[NSFileManager alloc] init];
704 NSDirectoryEnumerator *dirEnumerator = [fileManager enumeratorAtPath:directoryPath];
705 NSArray<NSString *> *allObjects = dirEnumerator.allObjects;
706 NSUInteger total = allObjects.count, complete = 0;
708 for (fileName in allObjects) {
710 NSString *fullFilePath = [directoryPath stringByAppendingPathComponent:fileName];
711 [fileManager fileExistsAtPath:fullFilePath isDirectory:&isDir];
713 if (keepParentDirectory)
715 fileName = [directoryPath.lastPathComponent stringByAppendingPathComponent:fileName];
719 success &= [zipArchive writeFileAtPath:fullFilePath withFileName:fileName compressionLevel:compressionLevel password:password AES:aes];
723 if ([[NSFileManager defaultManager] subpathsOfDirectoryAtPath:fullFilePath error:nil].count == 0)
725 success &= [zipArchive writeFolderAtPath:fullFilePath withFolderName:fileName withPassword:password];
729 if (progressHandler) {
730 progressHandler(complete, total);
733 success &= [zipArchive close];
738 // disabling `init` because designated initializer is `initWithPath:`
739 - (instancetype)init { @throw nil; }
741 // designated initializer
742 - (instancetype)initWithPath:(NSString *)path
744 if ((self = [super init])) {
753 NSAssert((_zip == NULL), @"Attempting to open an archive which is already open");
754 _zip = zipOpen(_path.fileSystemRepresentation, APPEND_STATUS_CREATE);
755 return (NULL != _zip);
758 - (BOOL)writeFolderAtPath:(NSString *)path withFolderName:(NSString *)folderName withPassword:(nullable NSString *)password
760 NSAssert((_zip != NULL), @"Attempting to write to an archive which was never opened");
762 zip_fileinfo zipInfo = {};
764 [SSZipArchive zipInfo:&zipInfo setAttributesOfItemAtPath:path];
766 int error = _zipOpenEntry(_zip, [folderName stringByAppendingString:@"/"], &zipInfo, Z_NO_COMPRESSION, password, 0);
767 const void *buffer = NULL;
768 zipWriteInFileInZip(_zip, buffer, 0);
769 zipCloseFileInZip(_zip);
770 return error == ZIP_OK;
773 - (BOOL)writeFile:(NSString *)path withPassword:(nullable NSString *)password
775 return [self writeFileAtPath:path withFileName:nil withPassword:password];
778 - (BOOL)writeFileAtPath:(NSString *)path withFileName:(nullable NSString *)fileName withPassword:(nullable NSString *)password
780 return [self writeFileAtPath:path withFileName:fileName compressionLevel:Z_DEFAULT_COMPRESSION password:password AES:YES];
783 // supports writing files with logical folder/directory structure
784 // *path* is the absolute path of the file that will be compressed
785 // *fileName* is the relative name of the file how it is stored within the zip e.g. /folder/subfolder/text1.txt
786 - (BOOL)writeFileAtPath:(NSString *)path withFileName:(nullable NSString *)fileName compressionLevel:(int)compressionLevel password:(nullable NSString *)password AES:(BOOL)aes
788 NSAssert((_zip != NULL), @"Attempting to write to an archive which was never opened");
790 FILE *input = fopen(path.fileSystemRepresentation, "r");
796 fileName = path.lastPathComponent;
799 zip_fileinfo zipInfo = {};
801 [SSZipArchive zipInfo:&zipInfo setAttributesOfItemAtPath:path];
803 void *buffer = malloc(CHUNK);
810 int error = _zipOpenEntry(_zip, fileName, &zipInfo, compressionLevel, password, aes);
812 while (!feof(input) && !ferror(input))
814 unsigned int len = (unsigned int) fread(buffer, 1, CHUNK, input);
815 zipWriteInFileInZip(_zip, buffer, len);
818 zipCloseFileInZip(_zip);
821 return error == ZIP_OK;
824 - (BOOL)writeData:(NSData *)data filename:(nullable NSString *)filename withPassword:(nullable NSString *)password
826 return [self writeData:data filename:filename compressionLevel:Z_DEFAULT_COMPRESSION password:password AES:YES];
829 - (BOOL)writeData:(NSData *)data filename:(nullable NSString *)filename compressionLevel:(int)compressionLevel password:(nullable NSString *)password AES:(BOOL)aes
837 zip_fileinfo zipInfo = {};
838 [SSZipArchive zipInfo:&zipInfo setDate:[NSDate date]];
840 int error = _zipOpenEntry(_zip, filename, &zipInfo, compressionLevel, password, aes);
842 zipWriteInFileInZip(_zip, data.bytes, (unsigned int)data.length);
844 zipCloseFileInZip(_zip);
845 return error == ZIP_OK;
850 NSAssert((_zip != NULL), @"[SSZipArchive] Attempting to close an archive which was never opened");
851 int error = zipClose(_zip, NULL);
853 return error == UNZ_OK;
856 #pragma mark - Private
858 + (NSString *)_filenameStringWithCString:(const char *)filename size:(uint16_t)size_filename
860 NSString * strPath = @(filename);
864 // if filename is non-unicode, detect and transform Encoding
865 NSData *data = [NSData dataWithBytes:(const void *)filename length:sizeof(unsigned char) * size_filename];
868 if (@available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *)) {
869 // supported encodings are in [NSString availableStringEncodings]
870 [NSString stringEncodingForData:data encodingOptions:nil convertedString:&strPath usedLossyConversion:nil];
874 if (floor(NSFoundationVersionNumber) > NSFoundationVersionNumber10_9_2) {
875 // supported encodings are in [NSString availableStringEncodings]
876 [NSString stringEncodingForData:data encodingOptions:nil convertedString:&strPath usedLossyConversion:nil];
880 // fallback to a simple manual detect for macOS 10.9 or older
881 NSArray<NSNumber *> *encodings = @[@(kCFStringEncodingGB_18030_2000), @(kCFStringEncodingShiftJIS)];
882 for (NSNumber *encoding in encodings) {
883 strPath = [NSString stringWithCString:filename encoding:(NSStringEncoding)CFStringConvertEncodingToNSStringEncoding(encoding.unsignedIntValue)];
890 // if filename encoding is non-detected, we default to something based on data
891 // _hexString is more readable than _base64RFC4648 for debugging unknown encodings
892 strPath = [data _hexString];
897 + (void)zipInfo:(zip_fileinfo *)zipInfo setAttributesOfItemAtPath:(NSString *)path
899 NSDictionary *attr = [[NSFileManager defaultManager] attributesOfItemAtPath:path error: nil];
902 NSDate *fileDate = (NSDate *)attr[NSFileModificationDate];
905 [self zipInfo:zipInfo setDate:fileDate];
908 // Write permissions into the external attributes, for details on this see here: http://unix.stackexchange.com/a/14727
909 // Get the permissions value from the files attributes
910 NSNumber *permissionsValue = (NSNumber *)attr[NSFilePosixPermissions];
911 if (permissionsValue != nil) {
912 // Get the short value for the permissions
913 short permissionsShort = permissionsValue.shortValue;
915 // Convert this into an octal by adding 010000, 010000 being the flag for a regular file
916 NSInteger permissionsOctal = 0100000 + permissionsShort;
918 // Convert this into a long value
919 uLong permissionsLong = @(permissionsOctal).unsignedLongValue;
921 // Store this into the external file attributes once it has been shifted 16 places left to form part of the second from last byte
923 // Casted back to an unsigned int to match type of external_fa in minizip
924 zipInfo->external_fa = (unsigned int)(permissionsLong << 16L);
929 + (void)zipInfo:(zip_fileinfo *)zipInfo setDate:(NSDate *)date
931 NSCalendar *currentCalendar = SSZipArchive._gregorian;
932 NSCalendarUnit flags = NSCalendarUnitYear | NSCalendarUnitMonth | NSCalendarUnitDay | NSCalendarUnitHour | NSCalendarUnitMinute | NSCalendarUnitSecond;
933 NSDateComponents *components = [currentCalendar components:flags fromDate:date];
935 tmz_date.tm_sec = (unsigned int)components.second;
936 tmz_date.tm_min = (unsigned int)components.minute;
937 tmz_date.tm_hour = (unsigned int)components.hour;
938 tmz_date.tm_mday = (unsigned int)components.day;
939 // ISO/IEC 9899 struct tm is 0-indexed for January but NSDateComponents for gregorianCalendar is 1-indexed for January
940 tmz_date.tm_mon = (unsigned int)components.month - 1;
941 // ISO/IEC 9899 struct tm is 0-indexed for AD 1900 but NSDateComponents for gregorianCalendar is 1-indexed for AD 1
942 tmz_date.tm_year = (unsigned int)components.year - 1900;
943 zipInfo->dos_date = tm_to_dosdate(&tmz_date);
946 + (NSCalendar *)_gregorian
948 static NSCalendar *gregorian;
949 static dispatch_once_t onceToken;
950 dispatch_once(&onceToken, ^{
951 gregorian = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
957 // Format from http://newsgroups.derkeiler.com/Archive/Comp/comp.os.msdos.programmer/2009-04/msg00060.html
958 // Two consecutive words, or a longword, YYYYYYYMMMMDDDDD hhhhhmmmmmmsssss
959 // YYYYYYY is years from 1980 = 0
960 // sssss is (seconds/2).
962 // 3658 = 0011 0110 0101 1000 = 0011011 0010 11000 = 27 2 24 = 2007-02-24
963 // 7423 = 0111 0100 0010 0011 - 01110 100001 00011 = 14 33 3 = 14:33:06
964 + (NSDate *)_dateWithMSDOSFormat:(UInt32)msdosDateTime
966 // the whole `_dateWithMSDOSFormat:` method is equivalent but faster than this one line,
967 // essentially because `mktime` is slow:
968 //NSDate *date = [NSDate dateWithTimeIntervalSince1970:dosdate_to_time_t(msdosDateTime)];
969 static const UInt32 kYearMask = 0xFE000000;
970 static const UInt32 kMonthMask = 0x1E00000;
971 static const UInt32 kDayMask = 0x1F0000;
972 static const UInt32 kHourMask = 0xF800;
973 static const UInt32 kMinuteMask = 0x7E0;
974 static const UInt32 kSecondMask = 0x1F;
976 NSAssert(0xFFFFFFFF == (kYearMask | kMonthMask | kDayMask | kHourMask | kMinuteMask | kSecondMask), @"[SSZipArchive] MSDOS date masks don't add up");
978 NSDateComponents *components = [[NSDateComponents alloc] init];
979 components.year = 1980 + ((msdosDateTime & kYearMask) >> 25);
980 components.month = (msdosDateTime & kMonthMask) >> 21;
981 components.day = (msdosDateTime & kDayMask) >> 16;
982 components.hour = (msdosDateTime & kHourMask) >> 11;
983 components.minute = (msdosDateTime & kMinuteMask) >> 5;
984 components.second = (msdosDateTime & kSecondMask) * 2;
986 NSDate *date = [self._gregorian dateFromComponents:components];
992 int _zipOpenEntry(zipFile entry, NSString *name, const zip_fileinfo *zipfi, int level, NSString *password, BOOL aes)
994 return zipOpenNewFileInZip5(entry, name.fileSystemRepresentation, zipfi, NULL, 0, NULL, 0, NULL, 0, 0, Z_DEFLATED, level, 0, -MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY, password.UTF8String, aes);
997 #pragma mark - Private tools for file info
999 BOOL _fileIsSymbolicLink(const unz_file_info *fileInfo)
1002 // Determine whether this is a symbolic link:
1003 // - File is stored with 'version made by' value of UNIX (3),
1004 // as per http://www.pkware.com/documents/casestudies/APPNOTE.TXT
1005 // in the upper byte of the version field.
1006 // - BSD4.4 st_mode constants are stored in the high 16 bits of the
1007 // external file attributes (defacto standard, verified against libarchive)
1009 // The original constants can be found here:
1010 // http://minnie.tuhs.org/cgi-bin/utree.pl?file=4.4BSD/usr/include/sys/stat.h
1012 const uLong ZipUNIXVersion = 3;
1013 const uLong BSD_SFMT = 0170000;
1014 const uLong BSD_IFLNK = 0120000;
1016 BOOL fileIsSymbolicLink = ((fileInfo->version >> 8) == ZipUNIXVersion) && BSD_IFLNK == (BSD_SFMT & (fileInfo->external_fa >> 16));
1017 return fileIsSymbolicLink;
1020 #pragma mark - Private tools for unreadable encodings
1022 @implementation NSData (SSZipArchive)
1024 // `base64EncodedStringWithOptions` uses a base64 alphabet with '+' and '/'.
1025 // we got those alternatives to make it compatible with filenames: https://en.wikipedia.org/wiki/Base64
1026 // * modified Base64 encoding for IMAP mailbox names (RFC 3501): uses '+' and ','
1027 // * modified Base64 for URL and filenames (RFC 4648): uses '-' and '_'
1028 - (NSString *)_base64RFC4648
1030 NSString *strName = [self base64EncodedStringWithOptions:0];
1031 strName = [strName stringByReplacingOccurrencesOfString:@"+" withString:@"-"];
1032 strName = [strName stringByReplacingOccurrencesOfString:@"/" withString:@"_"];
1036 // initWithBytesNoCopy from NSProgrammer, Jan 25 '12: https://stackoverflow.com/a/9009321/1033581
1037 // hexChars from Peter, Aug 19 '14: https://stackoverflow.com/a/25378464/1033581
1038 // not implemented as too lengthy: a potential mapping improvement from Moose, Nov 3 '15: https://stackoverflow.com/a/33501154/1033581
1039 - (NSString *)_hexString
1041 const char *hexChars = "0123456789ABCDEF";
1042 NSUInteger length = self.length;
1043 const unsigned char *bytes = self.bytes;
1044 char *chars = malloc(length * 2);
1046 NSUInteger i = length;
1048 *s++ = hexChars[*bytes >> 4];
1049 *s++ = hexChars[*bytes & 0xF];
1052 NSString *str = [[NSString alloc] initWithBytesNoCopy:chars
1054 encoding:NSASCIIStringEncoding