added iOS source code
[wl-app.git] / iOS / Pods / SSZipArchive / SSZipArchive / SSZipArchive.m
1 //
2 //  SSZipArchive.m
3 //  SSZipArchive
4 //
5 //  Created by Sam Soffes on 7/21/10.
6 //  Copyright (c) Sam Soffes 2010-2015. All rights reserved.
7 //
8
9 #import "SSZipArchive.h"
10 #include "unzip.h"
11 #include "zip.h"
12 #include "minishared.h"
13
14 #include <sys/stat.h>
15
16 NSString *const SSZipArchiveErrorDomain = @"SSZipArchiveErrorDomain";
17
18 #define CHUNK 16384
19
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);
22
23 #ifndef API_AVAILABLE
24 // Xcode 7- compatibility
25 #define API_AVAILABLE(...)
26 #endif
27
28 @interface NSData(SSZipArchive)
29 - (NSString *)_base64RFC4648 API_AVAILABLE(macos(10.9), ios(7.0), watchos(2.0), tvos(9.0));
30 - (NSString *)_hexString;
31 @end
32
33 @interface SSZipArchive ()
34 - (instancetype)init NS_DESIGNATED_INITIALIZER;
35 @end
36
37 @implementation SSZipArchive
38 {
39     /// path for zip file
40     NSString *_path;
41     zipFile _zip;
42 }
43
44 #pragma mark - Password check
45
46 + (BOOL)isFilePasswordProtectedAtPath:(NSString *)path {
47     // Begin opening
48     zipFile zip = unzOpen(path.fileSystemRepresentation);
49     if (zip == NULL) {
50         return NO;
51     }
52     
53     int ret = unzGoToFirstFile(zip);
54     if (ret == UNZ_OK) {
55         do {
56             ret = unzOpenCurrentFile(zip);
57             if (ret != UNZ_OK) {
58                 return NO;
59             }
60             unz_file_info fileInfo = {};
61             ret = unzGetCurrentFileInfo(zip, &fileInfo, NULL, 0, NULL, 0, NULL, 0);
62             if (ret != UNZ_OK) {
63                 return NO;
64             } else if ((fileInfo.flag & 1) == 1) {
65                 return YES;
66             }
67             
68             unzCloseCurrentFile(zip);
69             ret = unzGoToNextFile(zip);
70         } while (ret == UNZ_OK);
71     }
72     
73     return NO;
74 }
75
76 + (BOOL)isPasswordValidForArchiveAtPath:(NSString *)path password:(NSString *)pw error:(NSError **)error {
77     if (error) {
78         *error = nil;
79     }
80
81     zipFile zip = unzOpen(path.fileSystemRepresentation);
82     if (zip == NULL) {
83         if (error) {
84             *error = [NSError errorWithDomain:SSZipArchiveErrorDomain
85                                          code:SSZipArchiveErrorCodeFailedOpenZipFile
86                                      userInfo:@{NSLocalizedDescriptionKey: @"failed to open zip file"}];
87         }
88         return NO;
89     }
90
91     int ret = unzGoToFirstFile(zip);
92     if (ret == UNZ_OK) {
93         do {
94             if (pw.length == 0) {
95                 ret = unzOpenCurrentFile(zip);
96             } else {
97                 ret = unzOpenCurrentFilePassword(zip, [pw cStringUsingEncoding:NSUTF8StringEncoding]);
98             }
99             if (ret != UNZ_OK) {
100                 if (ret != UNZ_BADPASSWORD) {
101                     if (error) {
102                         *error = [NSError errorWithDomain:SSZipArchiveErrorDomain
103                                                      code:SSZipArchiveErrorCodeFailedOpenFileInZip
104                                                  userInfo:@{NSLocalizedDescriptionKey: @"failed to open first file in zip file"}];
105                     }
106                 }
107                 return NO;
108             }
109             unz_file_info fileInfo = {};
110             ret = unzGetCurrentFileInfo(zip, &fileInfo, NULL, 0, NULL, 0, NULL, 0);
111             if (ret != UNZ_OK) {
112                 if (error) {
113                     *error = [NSError errorWithDomain:SSZipArchiveErrorDomain
114                                                  code:SSZipArchiveErrorCodeFileInfoNotLoadable
115                                              userInfo:@{NSLocalizedDescriptionKey: @"failed to retrieve info for file"}];
116                 }
117                 return NO;
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));
121                 if (readBytes < 0) {
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) {
125                         if (error) {
126                             *error = [NSError errorWithDomain:SSZipArchiveErrorDomain
127                                                          code:SSZipArchiveErrorCodeFileContentNotReadable
128                                                      userInfo:@{NSLocalizedDescriptionKey: @"failed to read contents of file entry"}];
129                         }
130                     }
131                     return NO;
132                 }
133                 return YES;
134             }
135             
136             unzCloseCurrentFile(zip);
137             ret = unzGoToNextFile(zip);
138         } while (ret == UNZ_OK);
139     }
140     
141     // No password required
142     return YES;
143 }
144
145 #pragma mark - Unzipping
146
147 + (BOOL)unzipFileAtPath:(NSString *)path toDestination:(NSString *)destination
148 {
149     return [self unzipFileAtPath:path toDestination:destination delegate:nil];
150 }
151
152 + (BOOL)unzipFileAtPath:(NSString *)path toDestination:(NSString *)destination overwrite:(BOOL)overwrite password:(nullable NSString *)password error:(NSError **)error
153 {
154     return [self unzipFileAtPath:path toDestination:destination preserveAttributes:YES overwrite:overwrite password:password error:error delegate:nil progressHandler:nil completionHandler:nil];
155 }
156
157 + (BOOL)unzipFileAtPath:(NSString *)path toDestination:(NSString *)destination delegate:(nullable id<SSZipArchiveDelegate>)delegate
158 {
159     return [self unzipFileAtPath:path toDestination:destination preserveAttributes:YES overwrite:YES password:nil error:nil delegate:delegate progressHandler:nil completionHandler:nil];
160 }
161
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
168 {
169     return [self unzipFileAtPath:path toDestination:destination preserveAttributes:YES overwrite:overwrite password:password error:error delegate:delegate progressHandler:nil completionHandler:nil];
170 }
171
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
178 {
179     return [self unzipFileAtPath:path toDestination:destination preserveAttributes:YES overwrite:overwrite password:password error:nil delegate:nil progressHandler:progressHandler completionHandler:completionHandler];
180 }
181
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
186 {
187     return [self unzipFileAtPath:path toDestination:destination preserveAttributes:YES overwrite:YES password:nil error:nil delegate:nil progressHandler:progressHandler completionHandler:completionHandler];
188 }
189
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
197 {
198     return [self unzipFileAtPath:path toDestination:destination preserveAttributes:preserveAttributes overwrite:overwrite password:password error:error delegate:delegate progressHandler:nil completionHandler:nil];
199 }
200
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
210 {
211     return [self unzipFileAtPath:path toDestination:destination preserveAttributes:preserveAttributes overwrite:overwrite nestedZipLevel:0 password:password error:error delegate:delegate progressHandler:progressHandler completionHandler:completionHandler];
212 }
213
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
224 {
225     // Guard against empty strings
226     if (path.length == 0 || destination.length == 0)
227     {
228         NSDictionary *userInfo = @{NSLocalizedDescriptionKey: @"received invalid argument(s)"};
229         NSError *err = [NSError errorWithDomain:SSZipArchiveErrorDomain code:SSZipArchiveErrorCodeInvalidArguments userInfo:userInfo];
230         if (error)
231         {
232             *error = err;
233         }
234         if (completionHandler)
235         {
236             completionHandler(nil, NO, err);
237         }
238         return NO;
239     }
240     
241     // Begin opening
242     zipFile zip = unzOpen(path.fileSystemRepresentation);
243     if (zip == NULL)
244     {
245         NSDictionary *userInfo = @{NSLocalizedDescriptionKey: @"failed to open zip file"};
246         NSError *err = [NSError errorWithDomain:SSZipArchiveErrorDomain code:SSZipArchiveErrorCodeFailedOpenZipFile userInfo:userInfo];
247         if (error)
248         {
249             *error = err;
250         }
251         if (completionHandler)
252         {
253             completionHandler(nil, NO, err);
254         }
255         return NO;
256     }
257     
258     NSDictionary * fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:path error:nil];
259     unsigned long long fileSize = [fileAttributes[NSFileSize] unsignedLongLongValue];
260     unsigned long long currentPosition = 0;
261     
262     unz_global_info globalInfo = {};
263     unzGetGlobalInfo(zip, &globalInfo);
264     
265     // Begin unzipping
266     int ret = 0;
267     ret = unzGoToFirstFile(zip);
268     if (ret != UNZ_OK && ret != UNZ_END_OF_LIST_OF_FILE)
269     {
270         NSDictionary *userInfo = @{NSLocalizedDescriptionKey: @"failed to open first file in zip file"};
271         NSError *err = [NSError errorWithDomain:SSZipArchiveErrorDomain code:SSZipArchiveErrorCodeFailedOpenFileInZip userInfo:userInfo];
272         if (error)
273         {
274             *error = err;
275         }
276         if (completionHandler)
277         {
278             completionHandler(nil, NO, err);
279         }
280         return NO;
281     }
282     
283     BOOL success = YES;
284     BOOL canceled = NO;
285     int crc_ret = 0;
286     unsigned char buffer[4096] = {0};
287     NSFileManager *fileManager = [NSFileManager defaultManager];
288     NSMutableArray<NSDictionary *> *directoriesModificationDates = [[NSMutableArray alloc] init];
289     
290     // Message delegate
291     if ([delegate respondsToSelector:@selector(zipArchiveWillUnzipArchiveAtPath:zipInfo:)]) {
292         [delegate zipArchiveWillUnzipArchiveAtPath:path zipInfo:globalInfo];
293     }
294     if ([delegate respondsToSelector:@selector(zipArchiveProgressEvent:total:)]) {
295         [delegate zipArchiveProgressEvent:currentPosition total:fileSize];
296     }
297     
298     NSInteger currentFileNumber = -1;
299     NSError *unzippingError;
300     do {
301         currentFileNumber++;
302         if (ret == UNZ_END_OF_LIST_OF_FILE)
303             break;
304         @autoreleasepool {
305             if (password.length == 0) {
306                 ret = unzOpenCurrentFile(zip);
307             } else {
308                 ret = unzOpenCurrentFilePassword(zip, [password cStringUsingEncoding:NSUTF8StringEncoding]);
309             }
310             
311             if (ret != UNZ_OK) {
312                 unzippingError = [NSError errorWithDomain:@"SSZipArchiveErrorDomain" code:SSZipArchiveErrorCodeFailedOpenFileInZip userInfo:@{NSLocalizedDescriptionKey: @"failed to open file in zip file"}];
313                 success = NO;
314                 break;
315             }
316             
317             // Reading data and write to file
318             unz_file_info fileInfo;
319             memset(&fileInfo, 0, sizeof(unz_file_info));
320             
321             ret = unzGetCurrentFileInfo(zip, &fileInfo, NULL, 0, NULL, 0, NULL, 0);
322             if (ret != UNZ_OK) {
323                 unzippingError = [NSError errorWithDomain:@"SSZipArchiveErrorDomain" code:SSZipArchiveErrorCodeFileInfoNotLoadable userInfo:@{NSLocalizedDescriptionKey: @"failed to retrieve info for file"}];
324                 success = NO;
325                 unzCloseCurrentFile(zip);
326                 break;
327             }
328             
329             currentPosition += fileInfo.compressed_size;
330             
331             // Message delegate
332             if ([delegate respondsToSelector:@selector(zipArchiveShouldUnzipFileAtIndex:totalFiles:archivePath:fileInfo:)]) {
333                 if (![delegate zipArchiveShouldUnzipFileAtIndex:currentFileNumber
334                                                      totalFiles:(NSInteger)globalInfo.number_entry
335                                                     archivePath:path
336                                                        fileInfo:fileInfo]) {
337                     success = NO;
338                     canceled = YES;
339                     break;
340                 }
341             }
342             if ([delegate respondsToSelector:@selector(zipArchiveWillUnzipFileAtIndex:totalFiles:archivePath:fileInfo:)]) {
343                 [delegate zipArchiveWillUnzipFileAtIndex:currentFileNumber totalFiles:(NSInteger)globalInfo.number_entry
344                                              archivePath:path fileInfo:fileInfo];
345             }
346             if ([delegate respondsToSelector:@selector(zipArchiveProgressEvent:total:)]) {
347                 [delegate zipArchiveProgressEvent:(NSInteger)currentPosition total:(NSInteger)fileSize];
348             }
349             
350             char *filename = (char *)malloc(fileInfo.size_filename + 1);
351             if (filename == NULL)
352             {
353                 success = NO;
354                 break;
355             }
356             
357             unzGetCurrentFileInfo(zip, &fileInfo, filename, fileInfo.size_filename + 1, NULL, 0, NULL, 0);
358             filename[fileInfo.size_filename] = '\0';
359             
360             BOOL fileIsSymbolicLink = _fileIsSymbolicLink(&fileInfo);
361             
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);
367                 continue;
368             }
369             if (!strPath.length) {
370                 // if filename data is unsalvageable, we default to currentFileNumber
371                 strPath = @(currentFileNumber).stringValue;
372             }
373             
374             // Check if it contains directory
375             BOOL isDirectory = NO;
376             if (filename[fileInfo.size_filename-1] == '/' || filename[fileInfo.size_filename-1] == '\\') {
377                 isDirectory = YES;
378             }
379             free(filename);
380             
381             // Contains a path
382             if ([strPath rangeOfCharacterFromSet:[NSCharacterSet characterSetWithCharactersInString:@"/\\"]].location != NSNotFound) {
383                 strPath = [strPath stringByReplacingOccurrencesOfString:@"\\" withString:@"/"];
384             }
385             
386             NSString *fullPath = [destination stringByAppendingPathComponent:strPath];
387             NSError *err = nil;
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}];
393             }
394             if (isDirectory) {
395                 [fileManager createDirectoryAtPath:fullPath withIntermediateDirectories:YES attributes:directoryAttr error:&err];
396             } else {
397                 [fileManager createDirectoryAtPath:fullPath.stringByDeletingLastPathComponent withIntermediateDirectories:YES attributes:directoryAttr error:&err];
398             }
399             if (nil != err) {
400                 if ([err.domain isEqualToString:NSCocoaErrorDomain] &&
401                     err.code == 640) {
402                     unzippingError = err;
403                     unzCloseCurrentFile(zip);
404                     success = NO;
405                     break;
406                 }
407                 NSLog(@"[SSZipArchive] Error: %@", err.localizedDescription);
408             }
409             
410             if ([fileManager fileExistsAtPath:fullPath] && !isDirectory && !overwrite) {
411                 //FIXME: couldBe CRC Check?
412                 unzCloseCurrentFile(zip);
413                 ret = unzGoToNextFile(zip);
414                 continue;
415             }
416             
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");
422                     while (fp) {
423                         if (readBytes > 0) {
424                             if (0 == fwrite(buffer, readBytes, 1, fp)) {
425                                 if (ferror(fp)) {
426                                     NSString *message = [NSString stringWithFormat:@"Failed to write file (check your free space)"];
427                                     NSLog(@"[SSZipArchive] %@", message);
428                                     success = NO;
429                                     unzippingError = [NSError errorWithDomain:@"SSZipArchiveErrorDomain" code:SSZipArchiveErrorCodeFailedToWriteFile userInfo:@{NSLocalizedDescriptionKey: message}];
430                                     break;
431                                 }
432                             }
433                         } else {
434                             break;
435                         }
436                         readBytes = unzReadCurrentFile(zip, buffer, 4096);
437                         if (readBytes < 0) {
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
440                             success = NO;
441                         }
442                     }
443                     
444                     if (fp) {
445                         fclose(fp);
446                         
447                         if (nestedZipLevel
448                             && [fullPath.pathExtension.lowercaseString isEqualToString:@"zip"]
449                             && [self unzipFileAtPath:fullPath
450                                        toDestination:fullPath.stringByDeletingLastPathComponent
451                                   preserveAttributes:preserveAttributes
452                                            overwrite:overwrite
453                                       nestedZipLevel:nestedZipLevel - 1
454                                             password:password
455                                                error:nil
456                                             delegate:nil
457                                      progressHandler:nil
458                                    completionHandler:nil]) {
459                             [directoriesModificationDates removeLastObject];
460                             [[NSFileManager defaultManager] removeItemAtPath:fullPath error:nil];
461                         } else if (preserveAttributes) {
462                             
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};
467                                 
468                                 if (attr) {
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");
472                                     }
473                                 }
474                             }
475                             
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);
481                                 
482                                 // Retrieve any existing attributes
483                                 NSMutableDictionary *attrs = [[NSMutableDictionary alloc] initWithDictionary:[fileManager attributesOfItemAtPath:fullPath error:nil]];
484                                 
485                                 // Set the value in the attributes dict
486                                 attrs[NSFilePosixPermissions] = permissionsValue;
487                                 
488                                 // Update attributes
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");
492                                 }
493                             }
494                         }
495                     }
496                     else
497                     {
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
501                                                                        code:ENOSPC
502                                                                    userInfo:nil];
503                             unzippingError = enospcError;
504                             unzCloseCurrentFile(zip);
505                             success = NO;
506                             break;
507                         }
508                     }
509                 } else {
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
512                     success = NO;
513                     break;
514                 }
515             }
516             else
517             {
518                 // Assemble the path for the symbolic link
519                 NSMutableString *destinationPath = [NSMutableString string];
520                 int bytesRead = 0;
521                 while ((bytesRead = unzReadCurrentFile(zip, buffer, 4096)) > 0)
522                 {
523                     buffer[bytesRead] = 0;
524                     [destinationPath appendString:@((const char *)buffer)];
525                 }
526                 if (bytesRead < 0) {
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
529                     success = NO;
530                     break;
531                 }
532                 
533                 // Check if the symlink exists and delete it if we're overwriting
534                 if (overwrite)
535                 {
536                     if ([fileManager fileExistsAtPath:fullPath])
537                     {
538                         NSError *error = nil;
539                         BOOL removeSuccess = [fileManager removeItemAtPath:fullPath error:&error];
540                         if (!removeSuccess)
541                         {
542                             NSString *message = [NSString stringWithFormat:@"Failed to delete existing symbolic link at \"%@\"", error.localizedDescription];
543                             NSLog(@"[SSZipArchive] %@", message);
544                             success = NO;
545                             unzippingError = [NSError errorWithDomain:SSZipArchiveErrorDomain code:error.code userInfo:@{NSLocalizedDescriptionKey: message}];
546                         }
547                     }
548                 }
549                 
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]);
553                 
554                 if (symlinkError != 0)
555                 {
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);
559                     success = NO;
560                     unzippingError = [NSError errorWithDomain:NSPOSIXErrorDomain code:symlinkError userInfo:@{NSLocalizedDescriptionKey: message}];
561                 }
562             }
563             
564             crc_ret = unzCloseCurrentFile(zip);
565             if (crc_ret == UNZ_CRCERROR) {
566                 // CRC ERROR
567                 success = NO;
568                 break;
569             }
570             ret = unzGoToNextFile(zip);
571             
572             // Message delegate
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];
579             }
580             
581             if (progressHandler)
582             {
583                 progressHandler(strPath, fileInfo, currentFileNumber, globalInfo.number_entry);
584             }
585         }
586     } while (ret == UNZ_OK && success);
587     
588     // Close
589     unzClose(zip);
590     
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) {
595         NSError * err = nil;
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"]);
599             }
600             if (err) {
601                 NSLog(@"[SSZipArchive] Error setting directory file modification date attribute: %@", err.localizedDescription);
602             }
603         }
604     }
605     
606     // Message delegate
607     if (success && [delegate respondsToSelector:@selector(zipArchiveDidUnzipArchiveAtPath:zipInfo:unzippedPath:)]) {
608         [delegate zipArchiveDidUnzipArchiveAtPath:path zipInfo:globalInfo unzippedPath:destination];
609     }
610     // final progress event = 100%
611     if (!canceled && [delegate respondsToSelector:@selector(zipArchiveProgressEvent:total:)]) {
612         [delegate zipArchiveProgressEvent:fileSize total:fileSize];
613     }
614     
615     NSError *retErr = nil;
616     if (crc_ret == UNZ_CRCERROR)
617     {
618         NSDictionary *userInfo = @{NSLocalizedDescriptionKey: @"crc check failed for file"};
619         retErr = [NSError errorWithDomain:SSZipArchiveErrorDomain code:SSZipArchiveErrorCodeFileInfoNotLoadable userInfo:userInfo];
620     }
621     
622     if (error) {
623         if (unzippingError) {
624             *error = unzippingError;
625         }
626         else {
627             *error = retErr;
628         }
629     }
630     if (completionHandler)
631     {
632         if (unzippingError) {
633             completionHandler(path, success, unzippingError);
634         }
635         else
636         {
637             completionHandler(path, success, retErr);
638         }
639     }
640     return success;
641 }
642
643 #pragma mark - Zipping
644 + (BOOL)createZipFileAtPath:(NSString *)path withFilesAtPaths:(NSArray<NSString *> *)paths
645 {
646     return [SSZipArchive createZipFileAtPath:path withFilesAtPaths:paths withPassword:nil];
647 }
648 + (BOOL)createZipFileAtPath:(NSString *)path withContentsOfDirectory:(NSString *)directoryPath {
649     return [SSZipArchive createZipFileAtPath:path withContentsOfDirectory:directoryPath withPassword:nil];
650 }
651
652 + (BOOL)createZipFileAtPath:(NSString *)path withContentsOfDirectory:(NSString *)directoryPath keepParentDirectory:(BOOL)keepParentDirectory {
653     return [SSZipArchive createZipFileAtPath:path withContentsOfDirectory:directoryPath keepParentDirectory:keepParentDirectory withPassword:nil];
654 }
655
656 + (BOOL)createZipFileAtPath:(NSString *)path withFilesAtPaths:(NSArray<NSString *> *)paths withPassword:(NSString *)password
657 {
658     SSZipArchive *zipArchive = [[SSZipArchive alloc] initWithPath:path];
659     BOOL success = [zipArchive open];
660     if (success) {
661         for (NSString *filePath in paths) {
662             success &= [zipArchive writeFile:filePath withPassword:password];
663         }
664         success &= [zipArchive close];
665     }
666     return success;
667 }
668
669 + (BOOL)createZipFileAtPath:(NSString *)path withContentsOfDirectory:(NSString *)directoryPath withPassword:(nullable NSString *)password {
670     return [SSZipArchive createZipFileAtPath:path withContentsOfDirectory:directoryPath keepParentDirectory:NO withPassword:password];
671 }
672
673
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
680             ];
681 }
682
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];
689 }
690
691 + (BOOL)createZipFileAtPath:(NSString *)path
692     withContentsOfDirectory:(NSString *)directoryPath
693         keepParentDirectory:(BOOL)keepParentDirectory
694            compressionLevel:(int)compressionLevel
695                    password:(nullable NSString *)password
696                         AES:(BOOL)aes
697             progressHandler:(void(^ _Nullable)(NSUInteger entryNumber, NSUInteger total))progressHandler {
698     
699     SSZipArchive *zipArchive = [[SSZipArchive alloc] initWithPath:path];
700     BOOL success = [zipArchive open];
701     if (success) {
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;
707         NSString *fileName;
708         for (fileName in allObjects) {
709             BOOL isDir;
710             NSString *fullFilePath = [directoryPath stringByAppendingPathComponent:fileName];
711             [fileManager fileExistsAtPath:fullFilePath isDirectory:&isDir];
712             
713             if (keepParentDirectory)
714             {
715                 fileName = [directoryPath.lastPathComponent stringByAppendingPathComponent:fileName];
716             }
717             
718             if (!isDir) {
719                 success &= [zipArchive writeFileAtPath:fullFilePath withFileName:fileName compressionLevel:compressionLevel password:password AES:aes];
720             }
721             else
722             {
723                 if ([[NSFileManager defaultManager] subpathsOfDirectoryAtPath:fullFilePath error:nil].count == 0)
724                 {
725                     success &= [zipArchive writeFolderAtPath:fullFilePath withFolderName:fileName withPassword:password];
726                 }
727             }
728             complete++;
729             if (progressHandler) {
730                 progressHandler(complete, total);
731             }
732         }
733         success &= [zipArchive close];
734     }
735     return success;
736 }
737
738 // disabling `init` because designated initializer is `initWithPath:`
739 - (instancetype)init { @throw nil; }
740
741 // designated initializer
742 - (instancetype)initWithPath:(NSString *)path
743 {
744     if ((self = [super init])) {
745         _path = [path copy];
746     }
747     return self;
748 }
749
750
751 - (BOOL)open
752 {
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);
756 }
757
758 - (BOOL)writeFolderAtPath:(NSString *)path withFolderName:(NSString *)folderName withPassword:(nullable NSString *)password
759 {
760     NSAssert((_zip != NULL), @"Attempting to write to an archive which was never opened");
761     
762     zip_fileinfo zipInfo = {};
763     
764     [SSZipArchive zipInfo:&zipInfo setAttributesOfItemAtPath:path];
765     
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;
771 }
772
773 - (BOOL)writeFile:(NSString *)path withPassword:(nullable NSString *)password
774 {
775     return [self writeFileAtPath:path withFileName:nil withPassword:password];
776 }
777
778 - (BOOL)writeFileAtPath:(NSString *)path withFileName:(nullable NSString *)fileName withPassword:(nullable NSString *)password
779 {
780     return [self writeFileAtPath:path withFileName:fileName compressionLevel:Z_DEFAULT_COMPRESSION password:password AES:YES];
781 }
782
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
787 {
788     NSAssert((_zip != NULL), @"Attempting to write to an archive which was never opened");
789     
790     FILE *input = fopen(path.fileSystemRepresentation, "r");
791     if (NULL == input) {
792         return NO;
793     }
794     
795     if (!fileName) {
796         fileName = path.lastPathComponent;
797     }
798     
799     zip_fileinfo zipInfo = {};
800     
801     [SSZipArchive zipInfo:&zipInfo setAttributesOfItemAtPath:path];
802     
803     void *buffer = malloc(CHUNK);
804     if (buffer == NULL)
805     {
806         fclose(input);
807         return NO;
808     }
809     
810     int error = _zipOpenEntry(_zip, fileName, &zipInfo, compressionLevel, password, aes);
811     
812     while (!feof(input) && !ferror(input))
813     {
814         unsigned int len = (unsigned int) fread(buffer, 1, CHUNK, input);
815         zipWriteInFileInZip(_zip, buffer, len);
816     }
817     
818     zipCloseFileInZip(_zip);
819     free(buffer);
820     fclose(input);
821     return error == ZIP_OK;
822 }
823
824 - (BOOL)writeData:(NSData *)data filename:(nullable NSString *)filename withPassword:(nullable NSString *)password
825 {
826     return [self writeData:data filename:filename compressionLevel:Z_DEFAULT_COMPRESSION password:password AES:YES];
827 }
828
829 - (BOOL)writeData:(NSData *)data filename:(nullable NSString *)filename compressionLevel:(int)compressionLevel password:(nullable NSString *)password AES:(BOOL)aes
830 {
831     if (!_zip) {
832         return NO;
833     }
834     if (!data) {
835         return NO;
836     }
837     zip_fileinfo zipInfo = {};
838     [SSZipArchive zipInfo:&zipInfo setDate:[NSDate date]];
839     
840     int error = _zipOpenEntry(_zip, filename, &zipInfo, compressionLevel, password, aes);
841     
842     zipWriteInFileInZip(_zip, data.bytes, (unsigned int)data.length);
843     
844     zipCloseFileInZip(_zip);
845     return error == ZIP_OK;
846 }
847
848 - (BOOL)close
849 {
850     NSAssert((_zip != NULL), @"[SSZipArchive] Attempting to close an archive which was never opened");
851     int error = zipClose(_zip, NULL);
852     _zip = nil;
853     return error == UNZ_OK;
854 }
855
856 #pragma mark - Private
857
858 + (NSString *)_filenameStringWithCString:(const char *)filename size:(uint16_t)size_filename
859 {
860     NSString * strPath = @(filename);
861     if (strPath) {
862         return strPath;
863     }
864     // if filename is non-unicode, detect and transform Encoding
865     NSData *data = [NSData dataWithBytes:(const void *)filename length:sizeof(unsigned char) * size_filename];
866 #ifdef __MAC_10_13
867     // Xcode 9+
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];
871     }
872 #else
873     // Xcode 8-
874     if (floor(NSFoundationVersionNumber) > NSFoundationVersionNumber10_9_2) {
875         // supported encodings are in [NSString availableStringEncodings]
876         [NSString stringEncodingForData:data encodingOptions:nil convertedString:&strPath usedLossyConversion:nil];
877     }
878 #endif
879     else {
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)];
884             if (strPath) {
885                 break;
886             }
887         }
888     }
889     if (!strPath) {
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];
893     }
894     return strPath;
895 }
896
897 + (void)zipInfo:(zip_fileinfo *)zipInfo setAttributesOfItemAtPath:(NSString *)path
898 {
899     NSDictionary *attr = [[NSFileManager defaultManager] attributesOfItemAtPath:path error: nil];
900     if (attr)
901     {
902         NSDate *fileDate = (NSDate *)attr[NSFileModificationDate];
903         if (fileDate)
904         {
905             [self zipInfo:zipInfo setDate:fileDate];
906         }
907         
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;
914             
915             // Convert this into an octal by adding 010000, 010000 being the flag for a regular file
916             NSInteger permissionsOctal = 0100000 + permissionsShort;
917             
918             // Convert this into a long value
919             uLong permissionsLong = @(permissionsOctal).unsignedLongValue;
920             
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
922             
923             // Casted back to an unsigned int to match type of external_fa in minizip
924             zipInfo->external_fa = (unsigned int)(permissionsLong << 16L);
925         }
926     }
927 }
928
929 + (void)zipInfo:(zip_fileinfo *)zipInfo setDate:(NSDate *)date
930 {
931     NSCalendar *currentCalendar = SSZipArchive._gregorian;
932     NSCalendarUnit flags = NSCalendarUnitYear | NSCalendarUnitMonth | NSCalendarUnitDay | NSCalendarUnitHour | NSCalendarUnitMinute | NSCalendarUnitSecond;
933     NSDateComponents *components = [currentCalendar components:flags fromDate:date];
934     struct tm tmz_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);
944 }
945
946 + (NSCalendar *)_gregorian
947 {
948     static NSCalendar *gregorian;
949     static dispatch_once_t onceToken;
950     dispatch_once(&onceToken, ^{
951         gregorian = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
952     });
953     
954     return gregorian;
955 }
956
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).
961 //
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
965 {
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;
975     
976     NSAssert(0xFFFFFFFF == (kYearMask | kMonthMask | kDayMask | kHourMask | kMinuteMask | kSecondMask), @"[SSZipArchive] MSDOS date masks don't add up");
977     
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;
985     
986     NSDate *date = [self._gregorian dateFromComponents:components];
987     return date;
988 }
989
990 @end
991
992 int _zipOpenEntry(zipFile entry, NSString *name, const zip_fileinfo *zipfi, int level, NSString *password, BOOL aes)
993 {
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);
995 }
996
997 #pragma mark - Private tools for file info
998
999 BOOL _fileIsSymbolicLink(const unz_file_info *fileInfo)
1000 {
1001     //
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)
1008     //
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
1011     //
1012     const uLong ZipUNIXVersion = 3;
1013     const uLong BSD_SFMT = 0170000;
1014     const uLong BSD_IFLNK = 0120000;
1015     
1016     BOOL fileIsSymbolicLink = ((fileInfo->version >> 8) == ZipUNIXVersion) && BSD_IFLNK == (BSD_SFMT & (fileInfo->external_fa >> 16));
1017     return fileIsSymbolicLink;
1018 }
1019
1020 #pragma mark - Private tools for unreadable encodings
1021
1022 @implementation NSData (SSZipArchive)
1023
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
1029 {
1030     NSString *strName = [self base64EncodedStringWithOptions:0];
1031     strName = [strName stringByReplacingOccurrencesOfString:@"+" withString:@"-"];
1032     strName = [strName stringByReplacingOccurrencesOfString:@"/" withString:@"_"];
1033     return strName;
1034 }
1035
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
1040 {
1041     const char *hexChars = "0123456789ABCDEF";
1042     NSUInteger length = self.length;
1043     const unsigned char *bytes = self.bytes;
1044     char *chars = malloc(length * 2);
1045     char *s = chars;
1046     NSUInteger i = length;
1047     while (i--) {
1048         *s++ = hexChars[*bytes >> 4];
1049         *s++ = hexChars[*bytes & 0xF];
1050         bytes++;
1051     }
1052     NSString *str = [[NSString alloc] initWithBytesNoCopy:chars
1053                                                    length:length * 2
1054                                                  encoding:NSASCIIStringEncoding
1055                                              freeWhenDone:YES];
1056     return str;
1057 }
1058
1059 @end