@appzung/react-native-code-push 7.1.1 → 9.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/.azurepipelines/build-rn-code-push-1es.yml +104 -0
  2. package/.azurepipelines/test-rn-code-push.yml +94 -0
  3. package/.config/CredScanSuppressions.json +14 -0
  4. package/CONTRIBUTING.md +3 -3
  5. package/CodePush.js +20 -21
  6. package/CodePush.podspec +3 -3
  7. package/README.md +7 -5
  8. package/android/app/build.gradle +3 -1
  9. package/android/app/src/main/AndroidManifest.xml +2 -7
  10. package/android/app/src/main/java/com/microsoft/codepush/react/CodePush.java +14 -5
  11. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushConstants.java +1 -0
  12. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushNativeModule.java +9 -1
  13. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushUpdateUtils.java +18 -10
  14. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushUtils.java +3 -1
  15. package/android/build.gradle +6 -2
  16. package/android/codepush.gradle +72 -26
  17. package/docs/api-js.md +1 -1
  18. package/docs/setup-android.md +27 -3
  19. package/docs/setup-ios.md +1 -1
  20. package/docs/setup-windows.md +23 -0
  21. package/ios/CodePush/CodePush.m +2 -1
  22. package/ios/CodePush/CodePushPackage.m +4 -0
  23. package/ios/CodePush/SSZipArchive/Info.plist +26 -0
  24. package/ios/CodePush/SSZipArchive/README.md +1 -1
  25. package/ios/CodePush/SSZipArchive/SSZipArchive.h +129 -27
  26. package/ios/CodePush/SSZipArchive/SSZipArchive.m +1119 -314
  27. package/ios/CodePush/SSZipArchive/SSZipCommon.h +71 -0
  28. package/ios/CodePush/SSZipArchive/Supporting Files/PrivacyInfo.xcprivacy +23 -0
  29. package/ios/CodePush/SSZipArchive/include/ZipArchive.h +25 -0
  30. package/ios/CodePush/SSZipArchive/minizip/LICENSE +17 -0
  31. package/ios/CodePush/SSZipArchive/minizip/mz.h +273 -0
  32. package/ios/CodePush/SSZipArchive/minizip/mz_compat.c +1306 -0
  33. package/ios/CodePush/SSZipArchive/minizip/mz_compat.h +346 -0
  34. package/ios/CodePush/SSZipArchive/minizip/mz_crypt.c +187 -0
  35. package/ios/CodePush/SSZipArchive/minizip/mz_crypt.h +65 -0
  36. package/ios/CodePush/SSZipArchive/minizip/mz_crypt_apple.c +526 -0
  37. package/ios/CodePush/SSZipArchive/minizip/mz_os.c +348 -0
  38. package/ios/CodePush/SSZipArchive/minizip/mz_os.h +176 -0
  39. package/ios/CodePush/SSZipArchive/minizip/mz_os_posix.c +350 -0
  40. package/ios/CodePush/SSZipArchive/minizip/mz_strm.c +556 -0
  41. package/ios/CodePush/SSZipArchive/minizip/mz_strm.h +132 -0
  42. package/ios/CodePush/SSZipArchive/minizip/mz_strm_buf.c +383 -0
  43. package/ios/CodePush/SSZipArchive/minizip/mz_strm_buf.h +42 -0
  44. package/ios/CodePush/SSZipArchive/minizip/mz_strm_mem.c +269 -0
  45. package/ios/CodePush/SSZipArchive/minizip/mz_strm_mem.h +48 -0
  46. package/ios/CodePush/SSZipArchive/minizip/mz_strm_os.h +40 -0
  47. package/ios/CodePush/SSZipArchive/minizip/mz_strm_os_posix.c +203 -0
  48. package/ios/CodePush/SSZipArchive/minizip/mz_strm_pkcrypt.c +334 -0
  49. package/ios/CodePush/SSZipArchive/minizip/mz_strm_pkcrypt.h +46 -0
  50. package/ios/CodePush/SSZipArchive/minizip/mz_strm_split.c +429 -0
  51. package/ios/CodePush/SSZipArchive/minizip/mz_strm_split.h +43 -0
  52. package/ios/CodePush/SSZipArchive/minizip/mz_strm_wzaes.c +360 -0
  53. package/ios/CodePush/SSZipArchive/minizip/mz_strm_wzaes.h +46 -0
  54. package/ios/CodePush/SSZipArchive/minizip/mz_strm_zlib.c +389 -0
  55. package/ios/CodePush/SSZipArchive/minizip/mz_strm_zlib.h +43 -0
  56. package/ios/CodePush/SSZipArchive/minizip/mz_zip.c +2782 -0
  57. package/ios/CodePush/SSZipArchive/minizip/mz_zip.h +262 -0
  58. package/ios/CodePush/SSZipArchive/minizip/mz_zip_rw.c +1942 -0
  59. package/ios/CodePush/SSZipArchive/minizip/mz_zip_rw.h +285 -0
  60. package/ios/CodePush.xcodeproj/project.pbxproj +245 -130
  61. package/ios/PrivacyInfo.xcprivacy +31 -0
  62. package/package.json +2 -2
  63. package/typings/react-native-code-push.d.ts +1 -1
  64. package/windows/CodePush/CodePush.vcxproj +2 -3
  65. package/windows-legacy/CodePush.Net46/CodePush.Net46.csproj +2 -2
  66. package/windows-legacy/CodePush.Net46.Test/CodePush.Net46.Test.csproj +2 -2
  67. package/ios/CodePush/SSZipArchive/Common.h +0 -81
  68. package/ios/CodePush/SSZipArchive/aes/aes.h +0 -198
  69. package/ios/CodePush/SSZipArchive/aes/aes_via_ace.h +0 -541
  70. package/ios/CodePush/SSZipArchive/aes/aescrypt.c +0 -294
  71. package/ios/CodePush/SSZipArchive/aes/aeskey.c +0 -548
  72. package/ios/CodePush/SSZipArchive/aes/aesopt.h +0 -739
  73. package/ios/CodePush/SSZipArchive/aes/aestab.c +0 -391
  74. package/ios/CodePush/SSZipArchive/aes/aestab.h +0 -173
  75. package/ios/CodePush/SSZipArchive/aes/brg_endian.h +0 -126
  76. package/ios/CodePush/SSZipArchive/aes/brg_types.h +0 -219
  77. package/ios/CodePush/SSZipArchive/aes/entropy.c +0 -54
  78. package/ios/CodePush/SSZipArchive/aes/entropy.h +0 -16
  79. package/ios/CodePush/SSZipArchive/aes/fileenc.c +0 -144
  80. package/ios/CodePush/SSZipArchive/aes/fileenc.h +0 -121
  81. package/ios/CodePush/SSZipArchive/aes/hmac.c +0 -145
  82. package/ios/CodePush/SSZipArchive/aes/hmac.h +0 -103
  83. package/ios/CodePush/SSZipArchive/aes/prng.c +0 -155
  84. package/ios/CodePush/SSZipArchive/aes/prng.h +0 -82
  85. package/ios/CodePush/SSZipArchive/aes/pwd2key.c +0 -103
  86. package/ios/CodePush/SSZipArchive/aes/pwd2key.h +0 -57
  87. package/ios/CodePush/SSZipArchive/aes/sha1.c +0 -258
  88. package/ios/CodePush/SSZipArchive/aes/sha1.h +0 -73
  89. package/ios/CodePush/SSZipArchive/minizip/crypt.h +0 -130
  90. package/ios/CodePush/SSZipArchive/minizip/ioapi.c +0 -369
  91. package/ios/CodePush/SSZipArchive/minizip/ioapi.h +0 -175
  92. package/ios/CodePush/SSZipArchive/minizip/mztools.c +0 -284
  93. package/ios/CodePush/SSZipArchive/minizip/mztools.h +0 -31
  94. package/ios/CodePush/SSZipArchive/minizip/unzip.c +0 -1839
  95. package/ios/CodePush/SSZipArchive/minizip/unzip.h +0 -248
  96. package/ios/CodePush/SSZipArchive/minizip/zip.c +0 -1910
  97. package/ios/CodePush/SSZipArchive/minizip/zip.h +0 -202
@@ -3,29 +3,215 @@
3
3
  // SSZipArchive
4
4
  //
5
5
  // Created by Sam Soffes on 7/21/10.
6
- // Copyright (c) Sam Soffes 2010-2015. All rights reserved.
7
6
  //
8
- #import "SSZipArchive.h"
9
- #include "unzip.h"
10
- #include "zip.h"
11
- #import "zlib.h"
12
- #import "zconf.h"
13
7
 
8
+ #import "SSZipArchive.h"
9
+ #include "minizip/mz_compat.h"
10
+ #include "minizip/mz_zip.h"
11
+ #include "minizip/mz_os.h"
12
+ #include <zlib.h>
14
13
  #include <sys/stat.h>
15
14
 
15
+ NSString *const SSZipArchiveErrorDomain = @"SSZipArchiveErrorDomain";
16
+
16
17
  #define CHUNK 16384
17
18
 
19
+ int _zipOpenEntry(zipFile entry, NSString *name, const zip_fileinfo *zipfi, int level, NSString *password, BOOL aes);
20
+ BOOL _fileIsSymbolicLink(const unz_file_info *fileInfo);
21
+
22
+ #ifndef API_AVAILABLE
23
+ // Xcode 7- compatibility
24
+ #define API_AVAILABLE(...)
25
+ #endif
26
+
27
+ @interface NSData(SSZipArchive)
28
+ - (NSString *)_base64RFC4648 API_AVAILABLE(macos(10.9), ios(7.0), watchos(2.0), tvos(9.0));
29
+ - (NSString *)_hexString;
30
+ @end
31
+
32
+ @interface NSString (SSZipArchive)
33
+ - (NSString *)_sanitizedPath;
34
+ - (BOOL)_escapesTargetDirectory:(NSString *)targetDirectory;
35
+ @end
36
+
18
37
  @interface SSZipArchive ()
19
- + (NSDate *)_dateWithMSDOSFormat:(UInt32)msdosDateTime;
38
+ - (instancetype)init NS_DESIGNATED_INITIALIZER;
20
39
  @end
21
40
 
22
41
  @implementation SSZipArchive
23
42
  {
43
+ /// path for zip file
24
44
  NSString *_path;
25
- NSString *_filename;
26
45
  zipFile _zip;
27
46
  }
28
47
 
48
+ #pragma mark - Password check
49
+
50
+ + (BOOL)isFilePasswordProtectedAtPath:(NSString *)path {
51
+ // Begin opening
52
+ zipFile zip = unzOpen(path.fileSystemRepresentation);
53
+ if (zip == NULL) {
54
+ return NO;
55
+ }
56
+
57
+ BOOL passwordProtected = NO;
58
+ int ret = unzGoToFirstFile(zip);
59
+ if (ret == UNZ_OK) {
60
+ do {
61
+ ret = unzOpenCurrentFile(zip);
62
+ if (ret != UNZ_OK) {
63
+ // attempting with an arbitrary password to workaround `unzOpenCurrentFile` limitation on AES encrypted files
64
+ ret = unzOpenCurrentFilePassword(zip, "");
65
+ unzCloseCurrentFile(zip);
66
+ if (ret == UNZ_OK || ret == MZ_PASSWORD_ERROR) {
67
+ passwordProtected = YES;
68
+ }
69
+ break;
70
+ }
71
+ unz_file_info fileInfo = {};
72
+ ret = unzGetCurrentFileInfo(zip, &fileInfo, NULL, 0, NULL, 0, NULL, 0);
73
+ unzCloseCurrentFile(zip);
74
+ if (ret != UNZ_OK) {
75
+ break;
76
+ } else if ((fileInfo.flag & MZ_ZIP_FLAG_ENCRYPTED) == 1) {
77
+ passwordProtected = YES;
78
+ break;
79
+ }
80
+
81
+ ret = unzGoToNextFile(zip);
82
+ } while (ret == UNZ_OK);
83
+ }
84
+
85
+ unzClose(zip);
86
+ return passwordProtected;
87
+ }
88
+
89
+ + (BOOL)isPasswordValidForArchiveAtPath:(NSString *)path password:(NSString *)pw error:(NSError **)error {
90
+ if (error) {
91
+ *error = nil;
92
+ }
93
+
94
+ zipFile zip = unzOpen(path.fileSystemRepresentation);
95
+ if (zip == NULL) {
96
+ if (error) {
97
+ *error = [NSError errorWithDomain:SSZipArchiveErrorDomain
98
+ code:SSZipArchiveErrorCodeFailedOpenZipFile
99
+ userInfo:@{NSLocalizedDescriptionKey: @"failed to open zip file"}];
100
+ }
101
+ return NO;
102
+ }
103
+
104
+ // Initialize passwordValid to YES (No password required)
105
+ BOOL passwordValid = YES;
106
+ int ret = unzGoToFirstFile(zip);
107
+ if (ret == UNZ_OK) {
108
+ do {
109
+ if (pw.length == 0) {
110
+ ret = unzOpenCurrentFile(zip);
111
+ } else {
112
+ ret = unzOpenCurrentFilePassword(zip, [pw cStringUsingEncoding:NSUTF8StringEncoding]);
113
+ }
114
+ if (ret != UNZ_OK) {
115
+ if (ret != MZ_PASSWORD_ERROR) {
116
+ if (error) {
117
+ *error = [NSError errorWithDomain:SSZipArchiveErrorDomain
118
+ code:SSZipArchiveErrorCodeFailedOpenFileInZip
119
+ userInfo:@{NSLocalizedDescriptionKey: @"failed to open file in zip archive"}];
120
+ }
121
+ }
122
+ passwordValid = NO;
123
+ break;
124
+ }
125
+ unz_file_info fileInfo = {};
126
+ ret = unzGetCurrentFileInfo(zip, &fileInfo, NULL, 0, NULL, 0, NULL, 0);
127
+ if (ret != UNZ_OK) {
128
+ if (error) {
129
+ *error = [NSError errorWithDomain:SSZipArchiveErrorDomain
130
+ code:SSZipArchiveErrorCodeFileInfoNotLoadable
131
+ userInfo:@{NSLocalizedDescriptionKey: @"failed to retrieve info for file"}];
132
+ }
133
+ passwordValid = NO;
134
+ break;
135
+ } else if ((fileInfo.flag & 1) == 1) {
136
+ unsigned char buffer[10] = {0};
137
+ int readBytes = unzReadCurrentFile(zip, buffer, (unsigned)MIN(10UL,fileInfo.uncompressed_size));
138
+ if (readBytes < 0) {
139
+ // Let's assume error Z_DATA_ERROR is caused by an invalid password
140
+ // Let's assume other errors are caused by Content Not Readable
141
+ if (readBytes != Z_DATA_ERROR) {
142
+ if (error) {
143
+ *error = [NSError errorWithDomain:SSZipArchiveErrorDomain
144
+ code:SSZipArchiveErrorCodeFileContentNotReadable
145
+ userInfo:@{NSLocalizedDescriptionKey: @"failed to read contents of file entry"}];
146
+ }
147
+ }
148
+ passwordValid = NO;
149
+ break;
150
+ }
151
+ passwordValid = YES;
152
+ break;
153
+ }
154
+
155
+ unzCloseCurrentFile(zip);
156
+ ret = unzGoToNextFile(zip);
157
+ } while (ret == UNZ_OK);
158
+ }
159
+
160
+ unzClose(zip);
161
+ return passwordValid;
162
+ }
163
+
164
+ + (NSNumber *)payloadSizeForArchiveAtPath:(NSString *)path error:(NSError **)error {
165
+ if (error) {
166
+ *error = nil;
167
+ }
168
+
169
+ zipFile zip = unzOpen(path.fileSystemRepresentation);
170
+ if (zip == NULL) {
171
+ if (error) {
172
+ *error = [NSError errorWithDomain:SSZipArchiveErrorDomain
173
+ code:SSZipArchiveErrorCodeFailedOpenZipFile
174
+ userInfo:@{NSLocalizedDescriptionKey: @"failed to open zip file"}];
175
+ }
176
+ return @0;
177
+ }
178
+
179
+ unsigned long long totalSize = 0;
180
+ int ret = unzGoToFirstFile(zip);
181
+ if (ret == UNZ_OK) {
182
+ do {
183
+ ret = unzOpenCurrentFile(zip);
184
+ if (ret != UNZ_OK) {
185
+ if (error) {
186
+ *error = [NSError errorWithDomain:SSZipArchiveErrorDomain
187
+ code:SSZipArchiveErrorCodeFailedOpenFileInZip
188
+ userInfo:@{NSLocalizedDescriptionKey: @"failed to open file in zip archive"}];
189
+ }
190
+ break;
191
+ }
192
+ unz_file_info fileInfo = {};
193
+ ret = unzGetCurrentFileInfo(zip, &fileInfo, NULL, 0, NULL, 0, NULL, 0);
194
+ if (ret != UNZ_OK) {
195
+ if (error) {
196
+ *error = [NSError errorWithDomain:SSZipArchiveErrorDomain
197
+ code:SSZipArchiveErrorCodeFileInfoNotLoadable
198
+ userInfo:@{NSLocalizedDescriptionKey: @"failed to retrieve info for file"}];
199
+ }
200
+ break;
201
+ }
202
+
203
+ totalSize += fileInfo.uncompressed_size;
204
+
205
+ unzCloseCurrentFile(zip);
206
+ ret = unzGoToNextFile(zip);
207
+ } while (ret == UNZ_OK);
208
+ }
209
+
210
+ unzClose(zip);
211
+
212
+ return [NSNumber numberWithUnsignedLongLong:totalSize];
213
+ }
214
+
29
215
  #pragma mark - Unzipping
30
216
 
31
217
  + (BOOL)unzipFileAtPath:(NSString *)path toDestination:(NSString *)destination
@@ -33,19 +219,24 @@
33
219
  return [self unzipFileAtPath:path toDestination:destination delegate:nil];
34
220
  }
35
221
 
36
- + (BOOL)unzipFileAtPath:(NSString *)path toDestination:(NSString *)destination overwrite:(BOOL)overwrite password:(NSString *)password error:(NSError **)error
222
+ + (BOOL)unzipFileAtPath:(NSString *)path toDestination:(NSString *)destination overwrite:(BOOL)overwrite password:(nullable NSString *)password error:(NSError **)error
37
223
  {
38
- return [self unzipFileAtPath:path toDestination:destination overwrite:overwrite password:password error:error delegate:nil progressHandler:nil completionHandler:nil];
224
+ return [self unzipFileAtPath:path toDestination:destination preserveAttributes:YES overwrite:overwrite password:password error:error delegate:nil progressHandler:nil completionHandler:nil];
39
225
  }
40
226
 
41
- + (BOOL)unzipFileAtPath:(NSString *)path toDestination:(NSString *)destination delegate:(id<SSZipArchiveDelegate>)delegate
227
+ + (BOOL)unzipFileAtPath:(NSString *)path toDestination:(NSString *)destination delegate:(nullable id<SSZipArchiveDelegate>)delegate
42
228
  {
43
- return [self unzipFileAtPath:path toDestination:destination overwrite:YES password:nil error:nil delegate:delegate progressHandler:nil completionHandler:nil];
229
+ return [self unzipFileAtPath:path toDestination:destination preserveAttributes:YES overwrite:YES password:nil error:nil delegate:delegate progressHandler:nil completionHandler:nil];
44
230
  }
45
231
 
46
- + (BOOL)unzipFileAtPath:(NSString *)path toDestination:(NSString *)destination overwrite:(BOOL)overwrite password:(NSString *)password error:(NSError **)error delegate:(id<SSZipArchiveDelegate>)delegate
232
+ + (BOOL)unzipFileAtPath:(NSString *)path
233
+ toDestination:(NSString *)destination
234
+ overwrite:(BOOL)overwrite
235
+ password:(nullable NSString *)password
236
+ error:(NSError **)error
237
+ delegate:(nullable id<SSZipArchiveDelegate>)delegate
47
238
  {
48
- return [self unzipFileAtPath:path toDestination:destination overwrite:overwrite password:password error:error delegate:delegate progressHandler:nil completionHandler:nil];
239
+ return [self unzipFileAtPath:path toDestination:destination preserveAttributes:YES overwrite:overwrite password:password error:error delegate:delegate progressHandler:nil completionHandler:nil];
49
240
  }
50
241
 
51
242
  + (BOOL)unzipFileAtPath:(NSString *)path
@@ -53,34 +244,102 @@
53
244
  overwrite:(BOOL)overwrite
54
245
  password:(NSString *)password
55
246
  progressHandler:(void (^)(NSString *entry, unz_file_info zipInfo, long entryNumber, long total))progressHandler
56
- completionHandler:(void (^)(NSString *path, BOOL succeeded, NSError *error))completionHandler
247
+ completionHandler:(void (^)(NSString *path, BOOL succeeded, NSError * _Nullable error))completionHandler
57
248
  {
58
- return [self unzipFileAtPath:path toDestination:destination overwrite:overwrite password:password error:nil delegate:nil progressHandler:progressHandler completionHandler:completionHandler];
249
+ return [self unzipFileAtPath:path toDestination:destination preserveAttributes:YES overwrite:overwrite password:password error:nil delegate:nil progressHandler:progressHandler completionHandler:completionHandler];
59
250
  }
60
251
 
61
252
  + (BOOL)unzipFileAtPath:(NSString *)path
62
253
  toDestination:(NSString *)destination
63
- progressHandler:(void (^)(NSString *entry, unz_file_info zipInfo, long entryNumber, long total))progressHandler
64
- completionHandler:(void (^)(NSString *path, BOOL succeeded, NSError *error))completionHandler
254
+ progressHandler:(void (^_Nullable)(NSString *entry, unz_file_info zipInfo, long entryNumber, long total))progressHandler
255
+ completionHandler:(void (^_Nullable)(NSString *path, BOOL succeeded, NSError * _Nullable error))completionHandler
65
256
  {
66
- return [self unzipFileAtPath:path toDestination:destination overwrite:YES password:nil error:nil delegate:nil progressHandler:progressHandler completionHandler:completionHandler];
257
+ return [self unzipFileAtPath:path toDestination:destination preserveAttributes:YES overwrite:YES password:nil error:nil delegate:nil progressHandler:progressHandler completionHandler:completionHandler];
67
258
  }
68
259
 
69
260
  + (BOOL)unzipFileAtPath:(NSString *)path
70
261
  toDestination:(NSString *)destination
262
+ preserveAttributes:(BOOL)preserveAttributes
71
263
  overwrite:(BOOL)overwrite
72
- password:(NSString *)password
264
+ password:(nullable NSString *)password
265
+ error:(NSError * *)error
266
+ delegate:(nullable id<SSZipArchiveDelegate>)delegate
267
+ {
268
+ return [self unzipFileAtPath:path toDestination:destination preserveAttributes:preserveAttributes overwrite:overwrite password:password error:error delegate:delegate progressHandler:nil completionHandler:nil];
269
+ }
270
+
271
+ + (BOOL)unzipFileAtPath:(NSString *)path
272
+ toDestination:(NSString *)destination
273
+ preserveAttributes:(BOOL)preserveAttributes
274
+ overwrite:(BOOL)overwrite
275
+ password:(nullable NSString *)password
73
276
  error:(NSError **)error
74
- delegate:(id<SSZipArchiveDelegate>)delegate
75
- progressHandler:(void (^)(NSString *entry, unz_file_info zipInfo, long entryNumber, long total))progressHandler
76
- completionHandler:(void (^)(NSString *path, BOOL succeeded, NSError *error))completionHandler
277
+ delegate:(nullable id<SSZipArchiveDelegate>)delegate
278
+ progressHandler:(void (^_Nullable)(NSString *entry, unz_file_info zipInfo, long entryNumber, long total))progressHandler
279
+ completionHandler:(void (^_Nullable)(NSString *path, BOOL succeeded, NSError * _Nullable error))completionHandler
280
+ {
281
+ return [self unzipFileAtPath:path toDestination:destination preserveAttributes:preserveAttributes overwrite:overwrite nestedZipLevel:0 password:password error:error delegate:delegate progressHandler:progressHandler completionHandler:completionHandler];
282
+ }
283
+
284
+ + (BOOL)unzipFileAtPath:(NSString *)path
285
+ toDestination:(NSString *)destination
286
+ preserveAttributes:(BOOL)preserveAttributes
287
+ overwrite:(BOOL)overwrite
288
+ nestedZipLevel:(NSInteger)nestedZipLevel
289
+ password:(nullable NSString *)password
290
+ error:(NSError **)error
291
+ delegate:(nullable id<SSZipArchiveDelegate>)delegate
292
+ progressHandler:(void (^_Nullable)(NSString *entry, unz_file_info zipInfo, long entryNumber, long total))progressHandler
293
+ completionHandler:(void (^_Nullable)(NSString *path, BOOL succeeded, NSError * _Nullable error))completionHandler
77
294
  {
295
+ return [self unzipFileAtPath:path
296
+ toDestination:destination
297
+ preserveAttributes:preserveAttributes
298
+ overwrite:overwrite
299
+ symlinksValidWithin:destination
300
+ nestedZipLevel:nestedZipLevel
301
+ password:password
302
+ error:error
303
+ delegate:delegate
304
+ progressHandler:progressHandler
305
+ completionHandler:completionHandler];
306
+ }
307
+
308
+
309
+ + (BOOL)unzipFileAtPath:(NSString *)path
310
+ toDestination:(NSString *)destination
311
+ preserveAttributes:(BOOL)preserveAttributes
312
+ overwrite:(BOOL)overwrite
313
+ symlinksValidWithin:(nullable NSString *)symlinksValidWithin
314
+ nestedZipLevel:(NSInteger)nestedZipLevel
315
+ password:(nullable NSString *)password
316
+ error:(NSError **)error
317
+ delegate:(nullable id<SSZipArchiveDelegate>)delegate
318
+ progressHandler:(void (^_Nullable)(NSString *entry, unz_file_info zipInfo, long entryNumber, long total))progressHandler
319
+ completionHandler:(void (^_Nullable)(NSString *path, BOOL succeeded, NSError * _Nullable error))completionHandler
320
+ {
321
+ // Guard against empty strings
322
+ if (path.length == 0 || destination.length == 0)
323
+ {
324
+ NSDictionary *userInfo = @{NSLocalizedDescriptionKey: @"received invalid argument(s)"};
325
+ NSError *err = [NSError errorWithDomain:SSZipArchiveErrorDomain code:SSZipArchiveErrorCodeInvalidArguments userInfo:userInfo];
326
+ if (error)
327
+ {
328
+ *error = err;
329
+ }
330
+ if (completionHandler)
331
+ {
332
+ completionHandler(nil, NO, err);
333
+ }
334
+ return NO;
335
+ }
336
+
78
337
  // Begin opening
79
- zipFile zip = unzOpen((const char*)[path UTF8String]);
338
+ zipFile zip = unzOpen(path.fileSystemRepresentation);
80
339
  if (zip == NULL)
81
340
  {
82
341
  NSDictionary *userInfo = @{NSLocalizedDescriptionKey: @"failed to open zip file"};
83
- NSError *err = [NSError errorWithDomain:@"SSZipArchiveErrorDomain" code:-1 userInfo:userInfo];
342
+ NSError *err = [NSError errorWithDomain:SSZipArchiveErrorDomain code:SSZipArchiveErrorCodeFailedOpenZipFile userInfo:userInfo];
84
343
  if (error)
85
344
  {
86
345
  *error = err;
@@ -93,17 +352,19 @@
93
352
  }
94
353
 
95
354
  NSDictionary * fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:path error:nil];
96
- unsigned long long fileSize = [fileAttributes[NSFileSize] unsignedLongLongValue];
355
+ unsigned long long fileSize = [[fileAttributes objectForKey:NSFileSize] unsignedLongLongValue];
97
356
  unsigned long long currentPosition = 0;
98
357
 
99
- unz_global_info globalInfo = {0ul, 0ul};
358
+ unz_global_info globalInfo = {};
100
359
  unzGetGlobalInfo(zip, &globalInfo);
101
360
 
102
361
  // Begin unzipping
103
- if (unzGoToFirstFile(zip) != UNZ_OK)
362
+ int ret = 0;
363
+ ret = unzGoToFirstFile(zip);
364
+ if (ret != UNZ_OK && ret != MZ_END_OF_LIST)
104
365
  {
105
366
  NSDictionary *userInfo = @{NSLocalizedDescriptionKey: @"failed to open first file in zip file"};
106
- NSError *err = [NSError errorWithDomain:@"SSZipArchiveErrorDomain" code:-2 userInfo:userInfo];
367
+ NSError *err = [NSError errorWithDomain:SSZipArchiveErrorDomain code:SSZipArchiveErrorCodeFailedOpenFileInZip userInfo:userInfo];
107
368
  if (error)
108
369
  {
109
370
  *error = err;
@@ -112,35 +373,41 @@
112
373
  {
113
374
  completionHandler(nil, NO, err);
114
375
  }
376
+ unzClose(zip);
115
377
  return NO;
116
378
  }
117
379
 
118
380
  BOOL success = YES;
119
381
  BOOL canceled = NO;
120
- int ret = 0;
121
- int crc_ret =0;
382
+ int crc_ret = 0;
122
383
  unsigned char buffer[4096] = {0};
123
384
  NSFileManager *fileManager = [NSFileManager defaultManager];
124
- NSMutableSet *directoriesModificationDates = [[NSMutableSet alloc] init];
385
+ NSMutableArray<NSDictionary *> *directoriesModificationDates = [[NSMutableArray alloc] init];
125
386
 
126
387
  // Message delegate
127
388
  if ([delegate respondsToSelector:@selector(zipArchiveWillUnzipArchiveAtPath:zipInfo:)]) {
128
389
  [delegate zipArchiveWillUnzipArchiveAtPath:path zipInfo:globalInfo];
129
390
  }
130
391
  if ([delegate respondsToSelector:@selector(zipArchiveProgressEvent:total:)]) {
131
- [delegate zipArchiveProgressEvent:(NSInteger)currentPosition total:(NSInteger)fileSize];
392
+ [delegate zipArchiveProgressEvent:currentPosition total:fileSize];
132
393
  }
133
394
 
134
- NSInteger currentFileNumber = 0;
395
+ NSInteger currentFileNumber = -1;
396
+ NSError *unzippingError;
135
397
  do {
398
+ currentFileNumber++;
399
+ if (ret == MZ_END_OF_LIST) {
400
+ break;
401
+ }
136
402
  @autoreleasepool {
137
- if ([password length] == 0) {
403
+ if (password.length == 0) {
138
404
  ret = unzOpenCurrentFile(zip);
139
405
  } else {
140
- ret = unzOpenCurrentFilePassword(zip, [password cStringUsingEncoding:NSASCIIStringEncoding]);
406
+ ret = unzOpenCurrentFilePassword(zip, [password cStringUsingEncoding:NSUTF8StringEncoding]);
141
407
  }
142
408
 
143
409
  if (ret != UNZ_OK) {
410
+ unzippingError = [NSError errorWithDomain:@"SSZipArchiveErrorDomain" code:SSZipArchiveErrorCodeFailedOpenFileInZip userInfo:@{NSLocalizedDescriptionKey: @"failed to open file in zip file"}];
144
411
  success = NO;
145
412
  break;
146
413
  }
@@ -151,6 +418,7 @@
151
418
 
152
419
  ret = unzGetCurrentFileInfo(zip, &fileInfo, NULL, 0, NULL, 0, NULL, 0);
153
420
  if (ret != UNZ_OK) {
421
+ unzippingError = [NSError errorWithDomain:@"SSZipArchiveErrorDomain" code:SSZipArchiveErrorCodeFileInfoNotLoadable userInfo:@{NSLocalizedDescriptionKey: @"failed to retrieve info for file"}];
154
422
  success = NO;
155
423
  unzCloseCurrentFile(zip);
156
424
  break;
@@ -162,7 +430,8 @@
162
430
  if ([delegate respondsToSelector:@selector(zipArchiveShouldUnzipFileAtIndex:totalFiles:archivePath:fileInfo:)]) {
163
431
  if (![delegate zipArchiveShouldUnzipFileAtIndex:currentFileNumber
164
432
  totalFiles:(NSInteger)globalInfo.number_entry
165
- archivePath:path fileInfo:fileInfo]) {
433
+ archivePath:path
434
+ fileInfo:fileInfo]) {
166
435
  success = NO;
167
436
  canceled = YES;
168
437
  break;
@@ -177,59 +446,67 @@
177
446
  }
178
447
 
179
448
  char *filename = (char *)malloc(fileInfo.size_filename + 1);
449
+ if (filename == NULL)
450
+ {
451
+ success = NO;
452
+ break;
453
+ }
454
+
180
455
  unzGetCurrentFileInfo(zip, &fileInfo, filename, fileInfo.size_filename + 1, NULL, 0, NULL, 0);
181
456
  filename[fileInfo.size_filename] = '\0';
182
457
 
183
- //
184
- // Determine whether this is a symbolic link:
185
- // - File is stored with 'version made by' value of UNIX (3),
186
- // as per http://www.pkware.com/documents/casestudies/APPNOTE.TXT
187
- // in the upper byte of the version field.
188
- // - BSD4.4 st_mode constants are stored in the high 16 bits of the
189
- // external file attributes (defacto standard, verified against libarchive)
190
- //
191
- // The original constants can be found here:
192
- // http://minnie.tuhs.org/cgi-bin/utree.pl?file=4.4BSD/usr/include/sys/stat.h
193
- //
194
- const uLong ZipUNIXVersion = 3;
195
- const uLong BSD_SFMT = 0170000;
196
- const uLong BSD_IFLNK = 0120000;
458
+ BOOL fileIsSymbolicLink = _fileIsSymbolicLink(&fileInfo);
197
459
 
198
- BOOL fileIsSymbolicLink = NO;
199
- if (((fileInfo.version >> 8) == ZipUNIXVersion) && BSD_IFLNK == (BSD_SFMT & (fileInfo.external_fa >> 16))) {
200
- fileIsSymbolicLink = NO;
460
+ NSString * strPath = [SSZipArchive _filenameStringWithCString:filename
461
+ version_made_by:fileInfo.version
462
+ general_purpose_flag:fileInfo.flag
463
+ size:fileInfo.size_filename];
464
+ if ([strPath hasPrefix:@"__MACOSX/"]) {
465
+ // ignoring resource forks: https://superuser.com/questions/104500/what-is-macosx-folder
466
+ unzCloseCurrentFile(zip);
467
+ ret = unzGoToNextFile(zip);
468
+ free(filename);
469
+ continue;
201
470
  }
202
471
 
203
472
  // Check if it contains directory
204
- NSString *strPath = @(filename);
205
473
  BOOL isDirectory = NO;
206
474
  if (filename[fileInfo.size_filename-1] == '/' || filename[fileInfo.size_filename-1] == '\\') {
207
475
  isDirectory = YES;
208
476
  }
209
477
  free(filename);
210
478
 
211
- // Contains a path
212
- if ([strPath rangeOfCharacterFromSet:[NSCharacterSet characterSetWithCharactersInString:@"/\\"]].location != NSNotFound) {
213
- strPath = [strPath stringByReplacingOccurrencesOfString:@"\\" withString:@"/"];
479
+ // Sanitize paths in the file name.
480
+ strPath = [strPath _sanitizedPath];
481
+ if (!strPath.length) {
482
+ // if filename data is unsalvageable, we default to currentFileNumber
483
+ strPath = @(currentFileNumber).stringValue;
214
484
  }
215
485
 
216
486
  NSString *fullPath = [destination stringByAppendingPathComponent:strPath];
217
487
  NSError *err = nil;
218
- NSDate *modDate = [[self class] _dateWithMSDOSFormat:(UInt32)fileInfo.dosDate];
219
- NSDictionary *directoryAttr = @{NSFileCreationDate: modDate, NSFileModificationDate: modDate};
220
-
488
+ NSDictionary *directoryAttr;
489
+ if (preserveAttributes) {
490
+ NSDate *modDate = [[self class] _dateWithMSDOSFormat:(UInt32)fileInfo.mz_dos_date];
491
+ directoryAttr = @{NSFileCreationDate: modDate, NSFileModificationDate: modDate};
492
+ [directoriesModificationDates addObject: @{@"path": fullPath, @"modDate": modDate}];
493
+ }
221
494
  if (isDirectory) {
222
- [fileManager createDirectoryAtPath:fullPath withIntermediateDirectories:YES attributes:directoryAttr error:&err];
495
+ [fileManager createDirectoryAtPath:fullPath withIntermediateDirectories:YES attributes:directoryAttr error:&err];
223
496
  } else {
224
- [fileManager createDirectoryAtPath:[fullPath stringByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:directoryAttr error:&err];
497
+ [fileManager createDirectoryAtPath:fullPath.stringByDeletingLastPathComponent withIntermediateDirectories:YES attributes:directoryAttr error:&err];
225
498
  }
226
- if (nil != err) {
499
+ if (err != nil) {
500
+ if ([err.domain isEqualToString:NSCocoaErrorDomain] &&
501
+ err.code == 640) {
502
+ unzippingError = err;
503
+ unzCloseCurrentFile(zip);
504
+ success = NO;
505
+ break;
506
+ }
227
507
  NSLog(@"[SSZipArchive] Error: %@", err.localizedDescription);
228
508
  }
229
509
 
230
- if(!fileIsSymbolicLink)
231
- [directoriesModificationDates addObject: @{@"path": fullPath, @"modDate": modDate}];
232
-
233
510
  if ([fileManager fileExistsAtPath:fullPath] && !isDirectory && !overwrite) {
234
511
  //FIXME: couldBe CRC Check?
235
512
  unzCloseCurrentFile(zip);
@@ -237,92 +514,208 @@
237
514
  continue;
238
515
  }
239
516
 
240
- if (!fileIsSymbolicLink) {
241
- FILE *fp = fopen((const char*)[fullPath UTF8String], "wb");
242
- while (fp) {
243
- int readBytes = unzReadCurrentFile(zip, buffer, 4096);
244
-
245
- if (readBytes > 0) {
246
- fwrite(buffer, readBytes, 1, fp );
247
- } else {
248
- break;
249
- }
250
- }
251
-
252
- if (fp) {
253
- if ([[[fullPath pathExtension] lowercaseString] isEqualToString:@"zip"]) {
254
- NSLog(@"Unzipping nested .zip file: %@", [fullPath lastPathComponent]);
255
- if ([self unzipFileAtPath:fullPath toDestination:[fullPath stringByDeletingLastPathComponent] overwrite:overwrite password:password error:nil delegate:nil]) {
256
- [[NSFileManager defaultManager] removeItemAtPath:fullPath error:nil];
517
+ if (isDirectory && !fileIsSymbolicLink) {
518
+ // nothing to read/write for a directory
519
+ } else if (!fileIsSymbolicLink) {
520
+ // ensure we are not creating stale file entries
521
+ int readBytes = unzReadCurrentFile(zip, buffer, 4096);
522
+ if (readBytes >= 0) {
523
+ FILE *fp = fopen(fullPath.fileSystemRepresentation, "wb");
524
+ while (fp) {
525
+ if (readBytes > 0) {
526
+ if (0 == fwrite(buffer, readBytes, 1, fp)) {
527
+ if (ferror(fp)) {
528
+ NSString *message = [NSString stringWithFormat:@"Failed to write file (check your free space)"];
529
+ NSLog(@"[SSZipArchive] %@", message);
530
+ success = NO;
531
+ unzippingError = [NSError errorWithDomain:@"SSZipArchiveErrorDomain" code:SSZipArchiveErrorCodeFailedToWriteFile userInfo:@{NSLocalizedDescriptionKey: message}];
532
+ break;
533
+ }
534
+ }
535
+ } else {
536
+ break;
537
+ }
538
+ readBytes = unzReadCurrentFile(zip, buffer, 4096);
539
+ if (readBytes < 0) {
540
+ // Let's assume error Z_DATA_ERROR is caused by an invalid password
541
+ // Let's assume other errors are caused by Content Not Readable
542
+ success = NO;
257
543
  }
258
544
  }
259
545
 
260
- fclose(fp);
261
-
262
- // Set the original datetime property
263
- if (fileInfo.dosDate != 0) {
264
- NSDate *orgDate = [[self class] _dateWithMSDOSFormat:(UInt32)fileInfo.dosDate];
265
- NSDictionary *attr = @{NSFileModificationDate: orgDate};
546
+ if (fp) {
547
+ fclose(fp);
266
548
 
267
- if (attr) {
268
- if ([fileManager setAttributes:attr ofItemAtPath:fullPath error:nil] == NO) {
269
- // Can't set attributes
270
- NSLog(@"[SSZipArchive] Failed to set attributes - whilst setting modification date");
549
+ if (nestedZipLevel
550
+ && [fullPath.pathExtension.lowercaseString isEqualToString:@"zip"]
551
+ && [self unzipFileAtPath:fullPath
552
+ toDestination:fullPath.stringByDeletingLastPathComponent
553
+ preserveAttributes:preserveAttributes
554
+ overwrite:overwrite
555
+ symlinksValidWithin:symlinksValidWithin
556
+ nestedZipLevel:nestedZipLevel - 1
557
+ password:password
558
+ error:nil
559
+ delegate:nil
560
+ progressHandler:nil
561
+ completionHandler:nil]) {
562
+ [directoriesModificationDates removeLastObject];
563
+ [[NSFileManager defaultManager] removeItemAtPath:fullPath error:nil];
564
+ } else if (preserveAttributes) {
565
+
566
+ // Set the original datetime property
567
+ if (fileInfo.mz_dos_date != 0) {
568
+ NSDate *orgDate = [[self class] _dateWithMSDOSFormat:(UInt32)fileInfo.mz_dos_date];
569
+ NSDictionary *attr = @{NSFileModificationDate: orgDate};
570
+
571
+ if (attr) {
572
+ if (![fileManager setAttributes:attr ofItemAtPath:fullPath error:nil]) {
573
+ // Can't set attributes
574
+ NSLog(@"[SSZipArchive] Failed to set attributes - whilst setting modification date");
575
+ }
576
+ }
577
+ }
578
+
579
+ // Set the original permissions on the file (+read/write to solve #293)
580
+ uLong permissions = fileInfo.external_fa >> 16 | 0b110000000;
581
+ if (permissions != 0) {
582
+ // Store it into a NSNumber
583
+ NSNumber *permissionsValue = @(permissions);
584
+
585
+ // Retrieve any existing attributes
586
+ NSMutableDictionary *attrs = [[NSMutableDictionary alloc] initWithDictionary:[fileManager attributesOfItemAtPath:fullPath error:nil]];
587
+
588
+ // Set the value in the attributes dict
589
+ [attrs setObject:permissionsValue forKey:NSFilePosixPermissions];
590
+
591
+ // Update attributes
592
+ if (![fileManager setAttributes:attrs ofItemAtPath:fullPath error:nil]) {
593
+ // Unable to set the permissions attribute
594
+ NSLog(@"[SSZipArchive] Failed to set attributes - whilst setting permissions");
595
+ }
271
596
  }
272
597
  }
273
598
  }
274
-
275
- // Set the original permissions on the file
276
- uLong permissions = fileInfo.external_fa >> 16;
277
- if (permissions != 0) {
278
- // Store it into a NSNumber
279
- NSNumber *permissionsValue = @(permissions);
280
-
281
- // Retrieve any existing attributes
282
- NSMutableDictionary *attrs = [[NSMutableDictionary alloc] initWithDictionary:[fileManager attributesOfItemAtPath:fullPath error:nil]];
283
-
284
- // Set the value in the attributes dict
285
- attrs[NSFilePosixPermissions] = permissionsValue;
286
-
287
- // Update attributes
288
- if ([fileManager setAttributes:attrs ofItemAtPath:fullPath error:nil] == NO) {
289
- // Unable to set the permissions attribute
290
- NSLog(@"[SSZipArchive] Failed to set attributes - whilst setting permissions");
599
+ else
600
+ {
601
+ // if we couldn't open file descriptor we can validate global errno to see the reason
602
+ int errnoSave = errno;
603
+ BOOL isSeriousError = NO;
604
+ switch (errnoSave) {
605
+ case EISDIR:
606
+ // Is a directory
607
+ // assumed case
608
+ break;
609
+
610
+ case ENOSPC:
611
+ case EMFILE:
612
+ // No space left on device
613
+ // or
614
+ // Too many open files
615
+ isSeriousError = YES;
616
+ break;
617
+
618
+ default:
619
+ // ignore case
620
+ // Just log the error
621
+ {
622
+ NSError *errorObject = [NSError errorWithDomain:NSPOSIXErrorDomain
623
+ code:errnoSave
624
+ userInfo:nil];
625
+ NSLog(@"[SSZipArchive] Failed to open file on unzipping.(%@)", errorObject);
626
+ }
627
+ break;
291
628
  }
292
629
 
293
- #if !__has_feature(objc_arc)
294
- [attrs release];
295
- #endif
630
+ if (isSeriousError) {
631
+ // serious case
632
+ unzippingError = [NSError errorWithDomain:NSPOSIXErrorDomain
633
+ code:errnoSave
634
+ userInfo:nil];
635
+ unzCloseCurrentFile(zip);
636
+ // Log the error
637
+ NSLog(@"[SSZipArchive] Failed to open file on unzipping.(%@)", unzippingError);
638
+
639
+ // Break unzipping
640
+ success = NO;
641
+ break;
642
+ }
296
643
  }
644
+ } else {
645
+ // Let's assume error Z_DATA_ERROR is caused by an invalid password
646
+ // Let's assume other errors are caused by Content Not Readable
647
+ success = NO;
648
+ break;
297
649
  }
298
650
  }
299
651
  else
300
652
  {
301
653
  // Assemble the path for the symbolic link
302
- NSMutableString* destinationPath = [NSMutableString string];
654
+ NSMutableString *destinationPath = [NSMutableString string];
303
655
  int bytesRead = 0;
304
- while((bytesRead = unzReadCurrentFile(zip, buffer, 4096)) > 0)
656
+ while ((bytesRead = unzReadCurrentFile(zip, buffer, 4096)) > 0)
305
657
  {
306
- buffer[bytesRead] = (int)0;
307
- [destinationPath appendString:@((const char*)buffer)];
658
+ buffer[bytesRead] = 0;
659
+ [destinationPath appendString:@((const char *)buffer)];
660
+ }
661
+ if (bytesRead < 0) {
662
+ // Let's assume error Z_DATA_ERROR is caused by an invalid password
663
+ // Let's assume other errors are caused by Content Not Readable
664
+ success = NO;
665
+ break;
308
666
  }
309
667
 
310
- // Create the symbolic link (making sure it stays relative if it was relative before)
311
- int symlinkError = symlink([destinationPath cStringUsingEncoding:NSUTF8StringEncoding],
312
- [fullPath cStringUsingEncoding:NSUTF8StringEncoding]);
668
+ // compose symlink full path
669
+ NSString *symlinkFullDestinationPath = destinationPath;
670
+ if (![symlinkFullDestinationPath isAbsolutePath]) {
671
+ symlinkFullDestinationPath = [[fullPath stringByDeletingLastPathComponent] stringByAppendingPathComponent:destinationPath];
672
+ }
313
673
 
314
- if(symlinkError != 0)
315
- {
316
- NSLog(@"Failed to create symbolic link at \"%@\" to \"%@\". symlink() error code: %d", fullPath, destinationPath, errno);
674
+ if (symlinksValidWithin != nil && [symlinkFullDestinationPath _escapesTargetDirectory: symlinksValidWithin]) {
675
+ NSString *message = [NSString stringWithFormat:@"Symlink escapes target directory \"~%@ -> %@\"", strPath, destinationPath];
676
+ NSLog(@"[SSZipArchive] %@", message);
677
+ success = NO;
678
+ unzippingError = [NSError errorWithDomain:SSZipArchiveErrorDomain code:SSZipArchiveErrorCodeSymlinkEscapesTargetDirectory userInfo:@{NSLocalizedDescriptionKey: message}];
679
+ } else {
680
+ // Check if the symlink exists and delete it if we're overwriting
681
+ if (overwrite)
682
+ {
683
+ if ([fileManager fileExistsAtPath:fullPath])
684
+ {
685
+ NSError *localError = nil;
686
+ BOOL removeSuccess = [fileManager removeItemAtPath:fullPath error:&localError];
687
+ if (!removeSuccess)
688
+ {
689
+ NSString *message = [NSString stringWithFormat:@"Failed to delete existing symbolic link at \"%@\"", localError.localizedDescription];
690
+ NSLog(@"[SSZipArchive] %@", message);
691
+ success = NO;
692
+ unzippingError = [NSError errorWithDomain:SSZipArchiveErrorDomain code:localError.code userInfo:@{NSLocalizedDescriptionKey: message}];
693
+ }
694
+ }
695
+ }
696
+
697
+ // Create the symbolic link (making sure it stays relative if it was relative before)
698
+ int symlinkError = symlink([destinationPath cStringUsingEncoding:NSUTF8StringEncoding],
699
+ [fullPath cStringUsingEncoding:NSUTF8StringEncoding]);
700
+
701
+ if (symlinkError != 0)
702
+ {
703
+ // Bubble the error up to the completion handler
704
+ NSString *message = [NSString stringWithFormat:@"Failed to create symbolic link at \"%@\" to \"%@\" - symlink() error code: %d", fullPath, destinationPath, errno];
705
+ NSLog(@"[SSZipArchive] %@", message);
706
+ success = NO;
707
+ unzippingError = [NSError errorWithDomain:NSPOSIXErrorDomain code:symlinkError userInfo:@{NSLocalizedDescriptionKey: message}];
708
+ }
317
709
  }
318
710
  }
319
711
 
320
- crc_ret = unzCloseCurrentFile( zip );
321
- if (crc_ret == UNZ_CRCERROR) {
322
- //CRC ERROR
323
- return NO;
712
+ crc_ret = unzCloseCurrentFile(zip);
713
+ if (crc_ret == MZ_CRC_ERROR) {
714
+ // CRC ERROR
715
+ success = NO;
716
+ break;
324
717
  }
325
- ret = unzGoToNextFile( zip );
718
+ ret = unzGoToNextFile(zip);
326
719
 
327
720
  // Message delegate
328
721
  if ([delegate respondsToSelector:@selector(zipArchiveDidUnzipFileAtIndex:totalFiles:archivePath:fileInfo:)]) {
@@ -333,13 +726,12 @@
333
726
  archivePath:path unzippedFilePath: fullPath];
334
727
  }
335
728
 
336
- currentFileNumber++;
337
729
  if (progressHandler)
338
730
  {
339
731
  progressHandler(strPath, fileInfo, currentFileNumber, globalInfo.number_entry);
340
732
  }
341
733
  }
342
- } while(ret == UNZ_OK && ret != UNZ_END_OF_LIST_OF_FILE);
734
+ } while (ret == UNZ_OK && success);
343
735
 
344
736
  // Close
345
737
  unzClose(zip);
@@ -347,20 +739,18 @@
347
739
  // The process of decompressing the .zip archive causes the modification times on the folders
348
740
  // to be set to the present time. So, when we are done, they need to be explicitly set.
349
741
  // set the modification date on all of the directories.
350
- NSError * err = nil;
351
- for (NSDictionary * d in directoriesModificationDates) {
352
- if (![[NSFileManager defaultManager] setAttributes:@{NSFileModificationDate: d[@"modDate"]} ofItemAtPath:d[@"path"] error:&err]) {
353
- NSLog(@"[SSZipArchive] Set attributes failed for directory: %@.", d[@"path"]);
354
- }
355
- if (err) {
356
- NSLog(@"[SSZipArchive] Error setting directory file modification date attribute: %@",err.localizedDescription);
742
+ if (success && preserveAttributes) {
743
+ NSError * err = nil;
744
+ for (NSDictionary * d in directoriesModificationDates) {
745
+ if (![[NSFileManager defaultManager] setAttributes:@{NSFileModificationDate: [d objectForKey:@"modDate"]} ofItemAtPath:[d objectForKey:@"path"] error:&err]) {
746
+ NSLog(@"[SSZipArchive] Set attributes failed for directory: %@.", [d objectForKey:@"path"]);
747
+ }
748
+ if (err) {
749
+ NSLog(@"[SSZipArchive] Error setting directory file modification date attribute: %@", err.localizedDescription);
750
+ }
357
751
  }
358
752
  }
359
753
 
360
- #if !__has_feature(objc_arc)
361
- [directoriesModificationDates release];
362
- #endif
363
-
364
754
  // Message delegate
365
755
  if (success && [delegate respondsToSelector:@selector(zipArchiveDidUnzipArchiveAtPath:zipInfo:unzippedPath:)]) {
366
756
  [delegate zipArchiveDidUnzipArchiveAtPath:path zipInfo:globalInfo unzippedPath:destination];
@@ -370,94 +760,277 @@
370
760
  [delegate zipArchiveProgressEvent:fileSize total:fileSize];
371
761
  }
372
762
 
763
+ NSError *retErr = nil;
764
+ if (crc_ret == MZ_CRC_ERROR)
765
+ {
766
+ NSDictionary *userInfo = @{NSLocalizedDescriptionKey: @"crc check failed for file"};
767
+ retErr = [NSError errorWithDomain:SSZipArchiveErrorDomain code:SSZipArchiveErrorCodeFileInfoNotLoadable userInfo:userInfo];
768
+ }
769
+
770
+ if (error) {
771
+ if (unzippingError) {
772
+ *error = unzippingError;
773
+ }
774
+ else {
775
+ *error = retErr;
776
+ }
777
+ }
373
778
  if (completionHandler)
374
779
  {
375
- completionHandler(path, YES, nil);
780
+ if (unzippingError) {
781
+ completionHandler(path, success, unzippingError);
782
+ }
783
+ else
784
+ {
785
+ completionHandler(path, success, retErr);
786
+ }
376
787
  }
377
788
  return success;
378
789
  }
379
790
 
380
791
  #pragma mark - Zipping
381
- + (BOOL)createZipFileAtPath:(NSString *)path withFilesAtPaths:(NSArray *)paths
792
+ + (BOOL)createZipFileAtPath:(NSString *)path withFilesAtPaths:(NSArray<NSString *> *)paths
382
793
  {
383
794
  return [SSZipArchive createZipFileAtPath:path withFilesAtPaths:paths withPassword:nil];
384
795
  }
385
- + (BOOL)createZipFileAtPath:(NSString *)path withContentsOfDirectory:(NSString *)directoryPath{
796
+ + (BOOL)createZipFileAtPath:(NSString *)path withContentsOfDirectory:(NSString *)directoryPath {
386
797
  return [SSZipArchive createZipFileAtPath:path withContentsOfDirectory:directoryPath withPassword:nil];
387
798
  }
388
799
 
389
- + (BOOL)createZipFileAtPath:(NSString *)path withContentsOfDirectory:(NSString *)directoryPath keepParentDirectory:(BOOL)keepParentDirector{
390
- return [SSZipArchive createZipFileAtPath:path withContentsOfDirectory:directoryPath keepParentDirectory:keepParentDirector withPassword:nil];
800
+ + (BOOL)createZipFileAtPath:(NSString *)path withContentsOfDirectory:(NSString *)directoryPath keepParentDirectory:(BOOL)keepParentDirectory {
801
+ return [SSZipArchive createZipFileAtPath:path withContentsOfDirectory:directoryPath keepParentDirectory:keepParentDirectory withPassword:nil];
391
802
  }
392
803
 
393
- + (BOOL)createZipFileAtPath:(NSString *)path withFilesAtPaths:(NSArray *)paths withPassword:(NSString *)password
804
+ + (BOOL)createZipFileAtPath:(NSString *)path withFilesAtPaths:(NSArray<NSString *> *)paths withPassword:(NSString *)password {
805
+ return [self createZipFileAtPath:path withFilesAtPaths:paths withPassword:password progressHandler:nil];
806
+ }
807
+
808
+ + (BOOL)createZipFileAtPath:(NSString *)path withFilesAtPaths:(NSArray<NSString *> *)paths withPassword:(NSString *)password progressHandler:(void(^ _Nullable)(NSUInteger entryNumber, NSUInteger total))progressHandler
394
809
  {
395
- BOOL success = NO;
396
810
  SSZipArchive *zipArchive = [[SSZipArchive alloc] initWithPath:path];
397
- if ([zipArchive open]) {
811
+ BOOL success = [zipArchive open];
812
+ if (success) {
813
+ NSUInteger total = paths.count, complete = 0;
398
814
  for (NSString *filePath in paths) {
399
- [zipArchive writeFile:filePath withPassword:password];
815
+ success &= [zipArchive writeFile:filePath withPassword:password];
816
+ if (progressHandler) {
817
+ complete++;
818
+ progressHandler(complete, total);
819
+ }
400
820
  }
401
- success = [zipArchive close];
821
+ success &= [zipArchive close];
402
822
  }
403
-
404
- #if !__has_feature(objc_arc)
405
- [zipArchive release];
406
- #endif
407
-
408
823
  return success;
409
824
  }
410
825
 
411
- + (BOOL)createZipFileAtPath:(NSString *)path withContentsOfDirectory:(NSString *)directoryPath withPassword:(NSString *)password{
412
- return [self createZipFileAtPath:path withContentsOfDirectory:directoryPath keepParentDirectory:NO withPassword:password];
826
+ + (BOOL)createZipFileAtPath:(NSString *)path withContentsOfDirectory:(NSString *)directoryPath withPassword:(nullable NSString *)password {
827
+ return [SSZipArchive createZipFileAtPath:path withContentsOfDirectory:directoryPath keepParentDirectory:NO withPassword:password];
828
+ }
829
+
830
+
831
+ + (BOOL)createZipFileAtPath:(NSString *)path withContentsOfDirectory:(NSString *)directoryPath keepParentDirectory:(BOOL)keepParentDirectory withPassword:(nullable NSString *)password {
832
+ return [SSZipArchive createZipFileAtPath:path
833
+ withContentsOfDirectory:directoryPath
834
+ keepParentDirectory:keepParentDirectory
835
+ withPassword:password
836
+ andProgressHandler:nil
837
+ ];
413
838
  }
414
839
 
840
+ + (BOOL)createZipFileAtPath:(NSString *)path
841
+ withContentsOfDirectory:(NSString *)directoryPath
842
+ keepParentDirectory:(BOOL)keepParentDirectory
843
+ withPassword:(nullable NSString *)password
844
+ andProgressHandler:(void(^ _Nullable)(NSUInteger entryNumber, NSUInteger total))progressHandler {
845
+ return [self createZipFileAtPath:path withContentsOfDirectory:directoryPath keepParentDirectory:keepParentDirectory compressionLevel:Z_DEFAULT_COMPRESSION password:password AES:YES progressHandler:progressHandler];
846
+ }
415
847
 
416
- + (BOOL)createZipFileAtPath:(NSString *)path withContentsOfDirectory:(NSString *)directoryPath keepParentDirectory:(BOOL)keepParentDirectory withPassword:(NSString *)password{
417
- BOOL success = NO;
848
+ + (BOOL)createZipFileAtPath:(NSString *)path
849
+ withContentsOfDirectory:(NSString *)directoryPath
850
+ keepParentDirectory:(BOOL)keepParentDirectory
851
+ compressionLevel:(int)compressionLevel
852
+ password:(nullable NSString *)password
853
+ AES:(BOOL)aes
854
+ progressHandler:(void(^ _Nullable)(NSUInteger entryNumber, NSUInteger total))progressHandler {
418
855
 
419
- NSFileManager *fileManager = nil;
420
856
  SSZipArchive *zipArchive = [[SSZipArchive alloc] initWithPath:path];
421
-
422
- if ([zipArchive open]) {
423
- // use a local filemanager (queue/thread compatibility)
424
- fileManager = [[NSFileManager alloc] init];
857
+ BOOL success = [zipArchive open];
858
+ if (success) {
859
+ // use a local fileManager (queue/thread compatibility)
860
+ NSFileManager *fileManager = [[NSFileManager alloc] init];
425
861
  NSDirectoryEnumerator *dirEnumerator = [fileManager enumeratorAtPath:directoryPath];
426
- NSString *fileName;
427
- while ((fileName = [dirEnumerator nextObject])) {
428
- BOOL isDir;
862
+ NSArray<NSString *> *allObjects = dirEnumerator.allObjects;
863
+ NSUInteger total = allObjects.count, complete = 0;
864
+ if (keepParentDirectory && !total) {
865
+ allObjects = @[@""];
866
+ total = 1;
867
+ }
868
+ for (__strong NSString *fileName in allObjects) {
429
869
  NSString *fullFilePath = [directoryPath stringByAppendingPathComponent:fileName];
870
+ if ([fullFilePath isEqualToString:path]) {
871
+ NSLog(@"[SSZipArchive] the archive path and the file path: %@ are the same, which is forbidden.", fullFilePath);
872
+ continue;
873
+ }
874
+
875
+ if (keepParentDirectory) {
876
+ fileName = [directoryPath.lastPathComponent stringByAppendingPathComponent:fileName];
877
+ }
878
+
879
+ BOOL isDir;
430
880
  [fileManager fileExistsAtPath:fullFilePath isDirectory:&isDir];
431
881
  if (!isDir) {
432
- if (keepParentDirectory)
433
- {
434
- fileName = [[directoryPath lastPathComponent] stringByAppendingPathComponent:fileName];
882
+ // file
883
+ success &= [zipArchive writeFileAtPath:fullFilePath withFileName:fileName compressionLevel:compressionLevel password:password AES:aes];
884
+ } else {
885
+ // directory
886
+ if (![fileManager enumeratorAtPath:fullFilePath].nextObject) {
887
+ // empty directory
888
+ success &= [zipArchive writeFolderAtPath:fullFilePath withFolderName:fileName withPassword:password];
435
889
  }
436
- [zipArchive writeFileAtPath:fullFilePath withFileName:fileName withPassword:password];
437
890
  }
438
- else
439
- {
440
- if([[NSFileManager defaultManager] subpathsOfDirectoryAtPath:fullFilePath error:nil].count == 0)
441
- {
442
- NSString *tempName = [fullFilePath stringByAppendingPathComponent:@".DS_Store"];
443
- [@"" writeToFile:tempName atomically:YES encoding:NSUTF8StringEncoding error:nil];
444
- [zipArchive writeFileAtPath:tempName withFileName:[fileName stringByAppendingPathComponent:@".DS_Store"] withPassword:password];
445
- [[NSFileManager defaultManager] removeItemAtPath:tempName error:nil];
891
+ if (progressHandler) {
892
+ complete++;
893
+ progressHandler(complete, total);
894
+ }
895
+ }
896
+ success &= [zipArchive close];
897
+ }
898
+ return success;
899
+ }
900
+
901
+ + (BOOL)createZipFileAtPath:(NSString *)path withFilesAtPaths:(NSArray<NSString *> *)paths withPassword:(nullable NSString *)password keepSymlinks:(BOOL)keeplinks {
902
+ if (!keeplinks) {
903
+ return [SSZipArchive createZipFileAtPath:path withFilesAtPaths:paths withPassword:password];
904
+ } else {
905
+ SSZipArchive *zipArchive = [[SSZipArchive alloc] initWithPath:path];
906
+ BOOL success = [zipArchive open];
907
+ if (success) {
908
+ for (NSString *filePath in paths) {
909
+ //is symlink
910
+ if (mz_os_is_symlink(filePath.fileSystemRepresentation) == MZ_OK) {
911
+ success &= [zipArchive writeSymlinkFileAtPath:filePath withFileName:nil compressionLevel:Z_DEFAULT_COMPRESSION password:password AES:YES];
912
+ } else {
913
+ success &= [zipArchive writeFile:filePath withPassword:password];
914
+ }
915
+ }
916
+ success &= [zipArchive close];
917
+ }
918
+ return success;
919
+ }
920
+ }
921
+
922
+ + (BOOL)createZipFileAtPath:(NSString *)path
923
+ withContentsOfDirectory:(NSString *)directoryPath
924
+ keepParentDirectory:(BOOL)keepParentDirectory
925
+ compressionLevel:(int)compressionLevel
926
+ password:(nullable NSString *)password
927
+ AES:(BOOL)aes
928
+ progressHandler:(void(^ _Nullable)(NSUInteger entryNumber, NSUInteger total))progressHandler
929
+ keepSymlinks:(BOOL)keeplinks {
930
+ if (!keeplinks) {
931
+ return [SSZipArchive createZipFileAtPath:path
932
+ withContentsOfDirectory:directoryPath
933
+ keepParentDirectory:keepParentDirectory
934
+ compressionLevel:compressionLevel
935
+ password:password
936
+ AES:aes
937
+ progressHandler:progressHandler];
938
+ } else {
939
+ SSZipArchive *zipArchive = [[SSZipArchive alloc] initWithPath:path];
940
+ BOOL success = [zipArchive open];
941
+ if (success) {
942
+ // use a local fileManager (queue/thread compatibility)
943
+ NSFileManager *fileManager = [[NSFileManager alloc] init];
944
+ NSDirectoryEnumerator *dirEnumerator = [fileManager enumeratorAtPath:directoryPath];
945
+ NSArray<NSString *> *allObjects = dirEnumerator.allObjects;
946
+ NSUInteger total = allObjects.count, complete = 0;
947
+ if (keepParentDirectory && !total) {
948
+ allObjects = @[@""];
949
+ total = 1;
950
+ }
951
+ for (__strong NSString *fileName in allObjects) {
952
+ NSString *fullFilePath = [directoryPath stringByAppendingPathComponent:fileName];
953
+
954
+ if (keepParentDirectory) {
955
+ fileName = [directoryPath.lastPathComponent stringByAppendingPathComponent:fileName];
956
+ }
957
+ //is symlink
958
+ BOOL isSymlink = NO;
959
+ if (mz_os_is_symlink(fullFilePath.fileSystemRepresentation) == MZ_OK)
960
+ isSymlink = YES;
961
+ BOOL isDir;
962
+ [fileManager fileExistsAtPath:fullFilePath isDirectory:&isDir];
963
+ if (!isDir || isSymlink) {
964
+ // file or symlink
965
+ if (!isSymlink) {
966
+ success &= [zipArchive writeFileAtPath:fullFilePath withFileName:fileName compressionLevel:compressionLevel password:password AES:aes];
967
+ } else {
968
+ success &= [zipArchive writeSymlinkFileAtPath:fullFilePath withFileName:fileName compressionLevel:compressionLevel password:password AES:aes];
969
+ }
970
+ } else {
971
+ // directory
972
+ if (![fileManager enumeratorAtPath:fullFilePath].nextObject) {
973
+ // empty directory
974
+ success &= [zipArchive writeFolderAtPath:fullFilePath withFolderName:fileName withPassword:password];
975
+ }
976
+ }
977
+ if (progressHandler) {
978
+ complete++;
979
+ progressHandler(complete, total);
446
980
  }
447
981
  }
982
+ success &= [zipArchive close];
448
983
  }
449
- success = [zipArchive close];
984
+ return success;
985
+ }
986
+ }
987
+
988
+ - (BOOL)writeSymlinkFileAtPath:(NSString *)path withFileName:(nullable NSString *)fileName compressionLevel:(int)compressionLevel password:(nullable NSString *)password AES:(BOOL)aes
989
+ {
990
+ NSAssert((_zip != NULL), @"Attempting to write to an archive which was never opened");
991
+ //read symlink
992
+ char link_path[1024];
993
+ int32_t err = MZ_OK;
994
+ err = mz_os_read_symlink(path.fileSystemRepresentation, link_path, sizeof(link_path));
995
+ if (err != MZ_OK) {
996
+ NSLog(@"[SSZipArchive] Failed to read sylink");
997
+ return NO;
998
+ }
999
+
1000
+ if (!fileName) {
1001
+ fileName = path.lastPathComponent;
450
1002
  }
451
1003
 
452
- #if !__has_feature(objc_arc)
453
- [fileManager release];
454
- [zipArchive release];
455
- #endif
1004
+ zip_fileinfo zipInfo = {};
1005
+ [SSZipArchive zipInfo:&zipInfo setAttributesOfItemAtPath:path];
456
1006
 
457
- return success;
1007
+ //unpdate zipInfo.external_fa
1008
+ uint32_t target_attrib = 0;
1009
+ uint32_t src_attrib = 0;
1010
+ uint32_t src_sys = 0;
1011
+ mz_os_get_file_attribs(path.fileSystemRepresentation, &src_attrib);
1012
+ src_sys = MZ_HOST_SYSTEM(MZ_VERSION_MADEBY);
1013
+
1014
+ if ((src_sys != MZ_HOST_SYSTEM_MSDOS) && (src_sys != MZ_HOST_SYSTEM_WINDOWS_NTFS)) {
1015
+ /* High bytes are OS specific attributes, low byte is always DOS attributes */
1016
+ if (mz_zip_attrib_convert(src_sys, src_attrib, MZ_HOST_SYSTEM_MSDOS, &target_attrib) == MZ_OK)
1017
+ zipInfo.external_fa = target_attrib;
1018
+ zipInfo.external_fa |= (src_attrib << 16);
1019
+ } else {
1020
+ zipInfo.external_fa = src_attrib;
1021
+ }
1022
+
1023
+ uint16_t version_madeby = 3 << 8;//UNIX
1024
+ int error = zipOpenNewFileInZip5(_zip, fileName.fileSystemRepresentation, &zipInfo, NULL, 0, NULL, 0, NULL, Z_DEFLATED, compressionLevel, 0, -MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY, password.UTF8String, 0, aes, version_madeby, 0, 0);
1025
+ zipWriteInFileInZip(_zip, link_path, (uint32_t)strlen(link_path));
1026
+ zipCloseFileInZip(_zip);
1027
+ return error == ZIP_OK;
458
1028
  }
459
1029
 
1030
+ // disabling `init` because designated initializer is `initWithPath:`
1031
+ - (instancetype)init { @throw nil; }
460
1032
 
1033
+ // designated initializer
461
1034
  - (instancetype)initWithPath:(NSString *)path
462
1035
  {
463
1036
  if ((self = [super init])) {
@@ -467,153 +1040,92 @@
467
1040
  }
468
1041
 
469
1042
 
470
- #if !__has_feature(objc_arc)
471
- - (void)dealloc
472
- {
473
- [_path release];
474
- [super dealloc];
475
- }
476
- #endif
477
-
478
-
479
1043
  - (BOOL)open
480
1044
  {
481
- NSAssert((_zip == NULL), @"Attempting open an archive which is already open");
482
- _zip = zipOpen([_path UTF8String], APPEND_STATUS_CREATE);
1045
+ NSAssert((_zip == NULL), @"Attempting to open an archive which is already open");
1046
+ _zip = zipOpen(_path.fileSystemRepresentation, APPEND_STATUS_CREATE);
483
1047
  return (NULL != _zip);
484
1048
  }
485
1049
 
486
-
487
- - (void)zipInfo:(zip_fileinfo*)zipInfo setDate:(NSDate*)date
1050
+ - (BOOL)openForAppending
488
1051
  {
489
- NSCalendar *currentCalendar = [NSCalendar currentCalendar];
490
- #if defined(__IPHONE_8_0) || defined(__MAC_10_10)
491
- uint flags = NSCalendarUnitYear | NSCalendarUnitMonth | NSCalendarUnitDay | NSCalendarUnitHour | NSCalendarUnitMinute | NSCalendarUnitSecond;
492
- #else
493
- uint flags = NSYearCalendarUnit | NSMonthCalendarUnit | NSDayCalendarUnit | NSHourCalendarUnit | NSMinuteCalendarUnit | NSSecondCalendarUnit;
494
- #endif
495
- NSDateComponents *components = [currentCalendar components:flags fromDate:date];
496
- zipInfo->tmz_date.tm_sec = (unsigned int)components.second;
497
- zipInfo->tmz_date.tm_min = (unsigned int)components.minute;
498
- zipInfo->tmz_date.tm_hour = (unsigned int)components.hour;
499
- zipInfo->tmz_date.tm_mday = (unsigned int)components.day;
500
- zipInfo->tmz_date.tm_mon = (unsigned int)components.month - 1;
501
- zipInfo->tmz_date.tm_year = (unsigned int)components.year;
1052
+ NSAssert((_zip == NULL), @"Attempting to open an archive which is already open");
1053
+ _zip = zipOpen(_path.fileSystemRepresentation, APPEND_STATUS_ADDINZIP);
1054
+ return (NULL != _zip);
502
1055
  }
503
1056
 
504
- - (BOOL)writeFolderAtPath:(NSString *)path withFolderName:(NSString *)folderName withPassword:(NSString *)password
1057
+ - (BOOL)writeFolderAtPath:(NSString *)path withFolderName:(NSString *)folderName withPassword:(nullable NSString *)password
505
1058
  {
506
1059
  NSAssert((_zip != NULL), @"Attempting to write to an archive which was never opened");
507
1060
 
508
- zip_fileinfo zipInfo = {{0}};
1061
+ zip_fileinfo zipInfo = {};
509
1062
 
510
- NSDictionary *attr = [[NSFileManager defaultManager] attributesOfItemAtPath:path error: nil];
511
- if( attr )
512
- {
513
- NSDate *fileDate = (NSDate *)attr[NSFileModificationDate];
514
- if( fileDate )
515
- {
516
- [self zipInfo:&zipInfo setDate: fileDate ];
517
- }
518
-
519
- // Write permissions into the external attributes, for details on this see here: http://unix.stackexchange.com/a/14727
520
- // Get the permissions value from the files attributes
521
- NSNumber *permissionsValue = (NSNumber *)attr[NSFilePosixPermissions];
522
- if (permissionsValue) {
523
- // Get the short value for the permissions
524
- short permissionsShort = permissionsValue.shortValue;
525
-
526
- // Convert this into an octal by adding 010000, 010000 being the flag for a regular file
527
- NSInteger permissionsOctal = 0100000 + permissionsShort;
528
-
529
- // Convert this into a long value
530
- uLong permissionsLong = @(permissionsOctal).unsignedLongValue;
531
-
532
- // Store this into the external file attributes once it has been shifted 16 places left to form part of the second from last byte
533
- zipInfo.external_fa = permissionsLong << 16L;
534
- }
535
- }
1063
+ [SSZipArchive zipInfo:&zipInfo setAttributesOfItemAtPath:path];
536
1064
 
537
- unsigned int len = 0;
538
- zipOpenNewFileInZip3(_zip, [[folderName stringByAppendingString:@"/"] UTF8String], &zipInfo, NULL, 0, NULL, 0, NULL, Z_DEFLATED, Z_NO_COMPRESSION, 0, -MAX_WBITS, DEF_MEM_LEVEL,
539
- Z_DEFAULT_STRATEGY, [password UTF8String], 0);
540
- zipWriteInFileInZip(_zip, &len, 0);
1065
+ int error = _zipOpenEntry(_zip, [folderName stringByAppendingString:@"/"], &zipInfo, Z_NO_COMPRESSION, password, NO);
1066
+ const void *buffer = NULL;
1067
+ zipWriteInFileInZip(_zip, buffer, 0);
541
1068
  zipCloseFileInZip(_zip);
542
- return YES;
1069
+ return error == ZIP_OK;
543
1070
  }
544
1071
 
545
- - (BOOL)writeFile:(NSString *)path withPassword:(NSString *)password;
1072
+ - (BOOL)writeFile:(NSString *)path withPassword:(nullable NSString *)password
546
1073
  {
547
1074
  return [self writeFileAtPath:path withFileName:nil withPassword:password];
548
1075
  }
549
1076
 
1077
+ - (BOOL)writeFileAtPath:(NSString *)path withFileName:(nullable NSString *)fileName withPassword:(nullable NSString *)password
1078
+ {
1079
+ return [self writeFileAtPath:path withFileName:fileName compressionLevel:Z_DEFAULT_COMPRESSION password:password AES:YES];
1080
+ }
1081
+
550
1082
  // supports writing files with logical folder/directory structure
551
1083
  // *path* is the absolute path of the file that will be compressed
552
1084
  // *fileName* is the relative name of the file how it is stored within the zip e.g. /folder/subfolder/text1.txt
553
- - (BOOL)writeFileAtPath:(NSString *)path withFileName:(NSString *)fileName withPassword:(NSString *)password
1085
+ - (BOOL)writeFileAtPath:(NSString *)path withFileName:(nullable NSString *)fileName compressionLevel:(int)compressionLevel password:(nullable NSString *)password AES:(BOOL)aes
554
1086
  {
555
1087
  NSAssert((_zip != NULL), @"Attempting to write to an archive which was never opened");
556
1088
 
557
- FILE *input = fopen([path UTF8String], "r");
1089
+ FILE *input = fopen(path.fileSystemRepresentation, "r");
558
1090
  if (NULL == input) {
559
1091
  return NO;
560
1092
  }
561
1093
 
562
- const char *afileName;
563
1094
  if (!fileName) {
564
- afileName = [path.lastPathComponent UTF8String];
565
- }
566
- else {
567
- afileName = [fileName UTF8String];
1095
+ fileName = path.lastPathComponent;
568
1096
  }
569
1097
 
570
- zip_fileinfo zipInfo = {{0}};
1098
+ zip_fileinfo zipInfo = {};
571
1099
 
572
- NSDictionary *attr = [[NSFileManager defaultManager] attributesOfItemAtPath:path error: nil];
573
- if( attr )
1100
+ [SSZipArchive zipInfo:&zipInfo setAttributesOfItemAtPath:path];
1101
+
1102
+ void *buffer = malloc(CHUNK);
1103
+ if (buffer == NULL)
574
1104
  {
575
- NSDate *fileDate = (NSDate *)attr[NSFileModificationDate];
576
- if( fileDate )
577
- {
578
- [self zipInfo:&zipInfo setDate: fileDate ];
579
- }
580
-
581
- // Write permissions into the external attributes, for details on this see here: http://unix.stackexchange.com/a/14727
582
- // Get the permissions value from the files attributes
583
- NSNumber *permissionsValue = (NSNumber *)attr[NSFilePosixPermissions];
584
- if (permissionsValue) {
585
- // Get the short value for the permissions
586
- short permissionsShort = permissionsValue.shortValue;
587
-
588
- // Convert this into an octal by adding 010000, 010000 being the flag for a regular file
589
- NSInteger permissionsOctal = 0100000 + permissionsShort;
590
-
591
- // Convert this into a long value
592
- uLong permissionsLong = @(permissionsOctal).unsignedLongValue;
593
-
594
- // Store this into the external file attributes once it has been shifted 16 places left to form part of the second from last byte
595
- zipInfo.external_fa = permissionsLong << 16L;
596
- }
1105
+ fclose(input);
1106
+ return NO;
597
1107
  }
598
1108
 
599
- zipOpenNewFileInZip3(_zip, afileName, &zipInfo, NULL, 0, NULL, 0, NULL, Z_DEFLATED, Z_DEFAULT_COMPRESSION, 0, -MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY, [password UTF8String], 0);
600
-
601
- void *buffer = malloc(CHUNK);
602
- unsigned int len = 0;
1109
+ int error = _zipOpenEntry(_zip, fileName, &zipInfo, compressionLevel, password, aes);
603
1110
 
604
- while (!feof(input))
1111
+ while (!feof(input) && !ferror(input))
605
1112
  {
606
- len = (unsigned int) fread(buffer, 1, CHUNK, input);
1113
+ unsigned int len = (unsigned int) fread(buffer, 1, CHUNK, input);
607
1114
  zipWriteInFileInZip(_zip, buffer, len);
608
1115
  }
609
1116
 
610
1117
  zipCloseFileInZip(_zip);
611
1118
  free(buffer);
612
1119
  fclose(input);
613
- return YES;
1120
+ return error == ZIP_OK;
614
1121
  }
615
1122
 
616
- - (BOOL)writeData:(NSData *)data filename:(NSString *)filename withPassword:(NSString *)password;
1123
+ - (BOOL)writeData:(NSData *)data filename:(nullable NSString *)filename withPassword:(nullable NSString *)password
1124
+ {
1125
+ return [self writeData:data filename:filename compressionLevel:Z_DEFAULT_COMPRESSION password:password AES:YES];
1126
+ }
1127
+
1128
+ - (BOOL)writeData:(NSData *)data filename:(nullable NSString *)filename compressionLevel:(int)compressionLevel password:(nullable NSString *)password AES:(BOOL)aes
617
1129
  {
618
1130
  if (!_zip) {
619
1131
  return NO;
@@ -621,27 +1133,158 @@
621
1133
  if (!data) {
622
1134
  return NO;
623
1135
  }
624
- zip_fileinfo zipInfo = {{0,0,0,0,0,0},0,0,0};
625
- [self zipInfo:&zipInfo setDate:[NSDate date]];
1136
+ zip_fileinfo zipInfo = {};
1137
+ [SSZipArchive zipInfo:&zipInfo setDate:[NSDate date]];
626
1138
 
627
- zipOpenNewFileInZip3(_zip, [filename UTF8String], &zipInfo, NULL, 0, NULL, 0, NULL, Z_DEFLATED, Z_DEFAULT_COMPRESSION, 0, -MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY, [password UTF8String], 0);
1139
+ int error = _zipOpenEntry(_zip, filename, &zipInfo, compressionLevel, password, aes);
628
1140
 
629
1141
  zipWriteInFileInZip(_zip, data.bytes, (unsigned int)data.length);
630
1142
 
631
1143
  zipCloseFileInZip(_zip);
632
- return YES;
1144
+ return error == ZIP_OK;
633
1145
  }
634
1146
 
635
-
636
1147
  - (BOOL)close
637
1148
  {
638
1149
  NSAssert((_zip != NULL), @"[SSZipArchive] Attempting to close an archive which was never opened");
639
- zipClose(_zip, NULL);
640
- return YES;
1150
+ int error = zipClose(_zip, NULL);
1151
+ _zip = nil;
1152
+ return error == ZIP_OK;
641
1153
  }
642
1154
 
643
1155
  #pragma mark - Private
644
1156
 
1157
+ + (NSString *)_filenameStringWithCString:(const char *)filename
1158
+ version_made_by:(uint16_t)version_made_by
1159
+ general_purpose_flag:(uint16_t)flag
1160
+ size:(uint16_t)size_filename {
1161
+
1162
+ // Respect Language encoding flag only reading filename as UTF-8 when this is set
1163
+ // when file entry created on dos system.
1164
+ //
1165
+ // https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT
1166
+ // Bit 11: Language encoding flag (EFS). If this bit is set,
1167
+ // the filename and comment fields for this file
1168
+ // MUST be encoded using UTF-8. (see APPENDIX D)
1169
+ uint16_t made_by = version_made_by >> 8;
1170
+ BOOL made_on_dos = made_by == 0;
1171
+ BOOL languageEncoding = (flag & (1 << 11)) != 0;
1172
+ if (!languageEncoding && made_on_dos) {
1173
+ // APPNOTE.TXT D.1:
1174
+ // D.2 If general purpose bit 11 is unset, the file name and comment should conform
1175
+ // to the original ZIP character encoding. If general purpose bit 11 is set, the
1176
+ // filename and comment must support The Unicode Standard, Version 4.1.0 or
1177
+ // greater using the character encoding form defined by the UTF-8 storage
1178
+ // specification. The Unicode Standard is published by the The Unicode
1179
+ // Consortium (www.unicode.org). UTF-8 encoded data stored within ZIP files
1180
+ // is expected to not include a byte order mark (BOM).
1181
+
1182
+ // Code Page 437 corresponds to kCFStringEncodingDOSLatinUS
1183
+ NSStringEncoding encoding = CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingDOSLatinUS);
1184
+ NSString* strPath = [NSString stringWithCString:filename encoding:encoding];
1185
+ if (strPath) {
1186
+ return strPath;
1187
+ }
1188
+ }
1189
+
1190
+ // attempting unicode encoding
1191
+ NSString * strPath = @(filename);
1192
+ if (strPath) {
1193
+ return strPath;
1194
+ }
1195
+
1196
+ // if filename is non-unicode, detect and transform Encoding
1197
+ NSData *data = [NSData dataWithBytes:(const void *)filename length:sizeof(unsigned char) * size_filename];
1198
+ // Testing availability of @available (https://stackoverflow.com/a/46927445/1033581)
1199
+ #if __clang_major__ < 9
1200
+ // Xcode 8-
1201
+ if (floor(NSFoundationVersionNumber) > NSFoundationVersionNumber10_9_2) {
1202
+ #else
1203
+ // Xcode 9+
1204
+ if (@available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *)) {
1205
+ #endif
1206
+ // supported encodings are in [NSString availableStringEncodings]
1207
+ [NSString stringEncodingForData:data encodingOptions:nil convertedString:&strPath usedLossyConversion:nil];
1208
+ } else {
1209
+ // fallback to a simple manual detect for macOS 10.9 or older
1210
+ NSArray<NSNumber *> *encodings = @[@(kCFStringEncodingGB_18030_2000), @(kCFStringEncodingShiftJIS)];
1211
+ for (NSNumber *encoding in encodings) {
1212
+ strPath = [NSString stringWithCString:filename encoding:(NSStringEncoding)CFStringConvertEncodingToNSStringEncoding(encoding.unsignedIntValue)];
1213
+ if (strPath) {
1214
+ break;
1215
+ }
1216
+ }
1217
+ }
1218
+ if (strPath) {
1219
+ return strPath;
1220
+ }
1221
+
1222
+ // if filename encoding is non-detected, we default to something based on data
1223
+ // _hexString is more readable than _base64RFC4648 for debugging unknown encodings
1224
+ strPath = [data _hexString];
1225
+ return strPath;
1226
+ }
1227
+
1228
+ + (void)zipInfo:(zip_fileinfo *)zipInfo setAttributesOfItemAtPath:(NSString *)path
1229
+ {
1230
+ NSDictionary *attr = [[NSFileManager defaultManager] attributesOfItemAtPath:path error: nil];
1231
+ if (attr)
1232
+ {
1233
+ NSDate *fileDate = (NSDate *)[attr objectForKey:NSFileModificationDate];
1234
+ if (fileDate)
1235
+ {
1236
+ [self zipInfo:zipInfo setDate:fileDate];
1237
+ }
1238
+
1239
+ // Write permissions into the external attributes, for details on this see here: https://unix.stackexchange.com/a/14727
1240
+ // Get the permissions value from the files attributes
1241
+ NSNumber *permissionsValue = (NSNumber *)[attr objectForKey:NSFilePosixPermissions];
1242
+ if (permissionsValue != nil) {
1243
+ // Get the short value for the permissions
1244
+ short permissionsShort = permissionsValue.shortValue;
1245
+
1246
+ // Convert this into an octal by adding 010000, 010000 being the flag for a regular file
1247
+ NSInteger permissionsOctal = 0100000 + permissionsShort;
1248
+
1249
+ // Convert this into a long value
1250
+ uLong permissionsLong = @(permissionsOctal).unsignedLongValue;
1251
+
1252
+ // Store this into the external file attributes once it has been shifted 16 places left to form part of the second from last byte
1253
+
1254
+ // Casted back to an unsigned int to match type of external_fa in minizip
1255
+ zipInfo->external_fa = (unsigned int)(permissionsLong << 16L);
1256
+ }
1257
+ }
1258
+ }
1259
+
1260
+ + (void)zipInfo:(zip_fileinfo *)zipInfo setDate:(NSDate *)date
1261
+ {
1262
+ NSCalendar *currentCalendar = SSZipArchive._gregorian;
1263
+ NSCalendarUnit flags = NSCalendarUnitYear | NSCalendarUnitMonth | NSCalendarUnitDay | NSCalendarUnitHour | NSCalendarUnitMinute | NSCalendarUnitSecond;
1264
+ NSDateComponents *components = [currentCalendar components:flags fromDate:date];
1265
+ struct tm tmz_date;
1266
+ tmz_date.tm_sec = (unsigned int)components.second;
1267
+ tmz_date.tm_min = (unsigned int)components.minute;
1268
+ tmz_date.tm_hour = (unsigned int)components.hour;
1269
+ tmz_date.tm_mday = (unsigned int)components.day;
1270
+ // ISO/IEC 9899 struct tm is 0-indexed for January but NSDateComponents for gregorianCalendar is 1-indexed for January
1271
+ tmz_date.tm_mon = (unsigned int)components.month - 1;
1272
+ // ISO/IEC 9899 struct tm is 0-indexed for AD 1900 but NSDateComponents for gregorianCalendar is 1-indexed for AD 1
1273
+ tmz_date.tm_year = (unsigned int)components.year - 1900;
1274
+ zipInfo->mz_dos_date = mz_zip_tm_to_dosdate(&tmz_date);
1275
+ }
1276
+
1277
+ + (NSCalendar *)_gregorian
1278
+ {
1279
+ static NSCalendar *gregorian;
1280
+ static dispatch_once_t onceToken;
1281
+ dispatch_once(&onceToken, ^{
1282
+ gregorian = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
1283
+ });
1284
+
1285
+ return gregorian;
1286
+ }
1287
+
645
1288
  // Format from http://newsgroups.derkeiler.com/Archive/Comp/comp.os.msdos.programmer/2009-04/msg00060.html
646
1289
  // Two consecutive words, or a longword, YYYYYYYMMMMDDDDD hhhhhmmmmmmsssss
647
1290
  // YYYYYYY is years from 1980 = 0
@@ -651,6 +1294,9 @@
651
1294
  // 7423 = 0111 0100 0010 0011 - 01110 100001 00011 = 14 33 3 = 14:33:06
652
1295
  + (NSDate *)_dateWithMSDOSFormat:(UInt32)msdosDateTime
653
1296
  {
1297
+ // the whole `_dateWithMSDOSFormat:` method is equivalent but faster than this one line,
1298
+ // essentially because `mktime` is slow:
1299
+ //NSDate *date = [NSDate dateWithTimeIntervalSince1970:dosdate_to_time_t(msdosDateTime)];
654
1300
  static const UInt32 kYearMask = 0xFE000000;
655
1301
  static const UInt32 kMonthMask = 0x1E00000;
656
1302
  static const UInt32 kDayMask = 0x1F0000;
@@ -658,34 +1304,193 @@
658
1304
  static const UInt32 kMinuteMask = 0x7E0;
659
1305
  static const UInt32 kSecondMask = 0x1F;
660
1306
 
661
- static NSCalendar *gregorian;
662
- static dispatch_once_t onceToken;
663
- dispatch_once(&onceToken, ^{
664
- #if defined(__IPHONE_8_0) || defined(__MAC_10_10)
665
- gregorian = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
1307
+ NSAssert(0xFFFFFFFF == (kYearMask | kMonthMask | kDayMask | kHourMask | kMinuteMask | kSecondMask), @"[SSZipArchive] MSDOS date masks don't add up");
1308
+
1309
+ NSDateComponents *components = [[NSDateComponents alloc] init];
1310
+ components.year = 1980 + ((msdosDateTime & kYearMask) >> 25);
1311
+ components.month = (msdosDateTime & kMonthMask) >> 21;
1312
+ components.day = (msdosDateTime & kDayMask) >> 16;
1313
+ components.hour = (msdosDateTime & kHourMask) >> 11;
1314
+ components.minute = (msdosDateTime & kMinuteMask) >> 5;
1315
+ components.second = (msdosDateTime & kSecondMask) * 2;
1316
+
1317
+ NSDate *date = [self._gregorian dateFromComponents:components];
1318
+ return date;
1319
+ }
1320
+
1321
+ @end
1322
+
1323
+ int _zipOpenEntry(zipFile entry, NSString *name, const zip_fileinfo *zipfi, int level, NSString *password, BOOL aes)
1324
+ {
1325
+ // https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT
1326
+ uint16_t made_on_darwin = 19 << 8;
1327
+ //MZ_ZIP_FLAG_UTF8
1328
+ uint16_t flag_base = 1 << 11;
1329
+ return zipOpenNewFileInZip5(entry, name.fileSystemRepresentation, zipfi, NULL, 0, NULL, 0, NULL, Z_DEFLATED, level, 0, -MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY, password.UTF8String, 0, aes, made_on_darwin, flag_base, 1);
1330
+ }
1331
+
1332
+ #pragma mark - Private tools for file info
1333
+
1334
+ BOOL _fileIsSymbolicLink(const unz_file_info *fileInfo)
1335
+ {
1336
+ //
1337
+ // Determine whether this is a symbolic link:
1338
+ // - File is stored with 'version made by' value of UNIX (3),
1339
+ // as per https://www.pkware.com/documents/casestudies/APPNOTE.TXT
1340
+ // in the upper byte of the version field.
1341
+ // - BSD4.4 st_mode constants are stored in the high 16 bits of the
1342
+ // external file attributes (defacto standard, verified against libarchive)
1343
+ //
1344
+ // The original constants can be found here:
1345
+ // https://minnie.tuhs.org/cgi-bin/utree.pl?file=4.4BSD/usr/include/sys/stat.h
1346
+ //
1347
+ const uLong ZipUNIXVersion = 3;
1348
+ const uLong BSD_SFMT = 0170000;
1349
+ const uLong BSD_IFLNK = 0120000;
1350
+
1351
+ BOOL fileIsSymbolicLink = ((fileInfo->version >> 8) == ZipUNIXVersion) && BSD_IFLNK == (BSD_SFMT & (fileInfo->external_fa >> 16));
1352
+ return fileIsSymbolicLink;
1353
+ }
1354
+
1355
+ #pragma mark - Private tools for unreadable encodings
1356
+
1357
+ @implementation NSData (SSZipArchive)
1358
+
1359
+ // `base64EncodedStringWithOptions` uses a base64 alphabet with '+' and '/'.
1360
+ // we got those alternatives to make it compatible with filenames: https://en.wikipedia.org/wiki/Base64
1361
+ // * modified Base64 encoding for IMAP mailbox names (RFC 3501): uses '+' and ','
1362
+ // * modified Base64 for URL and filenames (RFC 4648): uses '-' and '_'
1363
+ - (NSString *)_base64RFC4648
1364
+ {
1365
+ NSString *strName = [self base64EncodedStringWithOptions:0];
1366
+ strName = [strName stringByReplacingOccurrencesOfString:@"+" withString:@"-"];
1367
+ strName = [strName stringByReplacingOccurrencesOfString:@"/" withString:@"_"];
1368
+ return strName;
1369
+ }
1370
+
1371
+ // initWithBytesNoCopy from NSProgrammer, Jan 25 '12: https://stackoverflow.com/a/9009321/1033581
1372
+ // hexChars from Peter, Aug 19 '14: https://stackoverflow.com/a/25378464/1033581
1373
+ // not implemented as too lengthy: a potential mapping improvement from Moose, Nov 3 '15: https://stackoverflow.com/a/33501154/1033581
1374
+ - (NSString *)_hexString
1375
+ {
1376
+ const char *hexChars = "0123456789ABCDEF";
1377
+ NSUInteger length = self.length;
1378
+ const unsigned char *bytes = self.bytes;
1379
+ char *chars = malloc(length * 2);
1380
+ if (chars == NULL) {
1381
+ // we directly raise an exception instead of using NSAssert to make sure assertion is not disabled as this is irrecoverable
1382
+ [NSException raise:@"NSInternalInconsistencyException" format:@"failed malloc" arguments:nil];
1383
+ return nil;
1384
+ }
1385
+ char *s = chars;
1386
+ NSUInteger i = length;
1387
+ while (i--) {
1388
+ *s++ = hexChars[*bytes >> 4];
1389
+ *s++ = hexChars[*bytes & 0xF];
1390
+ bytes++;
1391
+ }
1392
+ NSString *str = [[NSString alloc] initWithBytesNoCopy:chars
1393
+ length:length * 2
1394
+ encoding:NSASCIIStringEncoding
1395
+ freeWhenDone:YES];
1396
+ return str;
1397
+ }
1398
+
1399
+ @end
1400
+
1401
+ #pragma mark Private tools for security
1402
+
1403
+ @implementation NSString (SSZipArchive)
1404
+
1405
+ // One implementation alternative would be to use the algorithm found at mz_path_resolve from https://github.com/nmoinvaz/minizip/blob/dev/mz_os.c,
1406
+ // but making sure to work with unichar values and not ascii values to avoid breaking Unicode characters containing 2E ('.') or 2F ('/') in their decomposition
1407
+ /// Sanitize path traversal characters to prevent directory backtracking. Ignoring these characters mimicks the default behavior of the Unarchiving tool on macOS.
1408
+ - (NSString *)_sanitizedPath
1409
+ {
1410
+ // Change Windows paths to Unix paths: https://en.wikipedia.org/wiki/Path_(computing)
1411
+ // Possible improvement: only do this if the archive was created on a non-Unix system
1412
+ NSString *strPath = [self stringByReplacingOccurrencesOfString:@"\\" withString:@"/"];
1413
+
1414
+ // Percent-encode file path (where path is defined by https://tools.ietf.org/html/rfc8089)
1415
+ // The key part is to allow characters "." and "/" and disallow "%".
1416
+ // CharacterSet.urlPathAllowed seems to do the job
1417
+ #if (__MAC_OS_X_VERSION_MIN_REQUIRED >= 1090 || __IPHONE_OS_VERSION_MIN_REQUIRED >= 70000 || __WATCH_OS_VERSION_MIN_REQUIRED >= 20000 || __TV_OS_VERSION_MIN_REQUIRED >= 90000)
1418
+ strPath = [strPath stringByAddingPercentEncodingWithAllowedCharacters:NSCharacterSet.URLPathAllowedCharacterSet];
666
1419
  #else
667
- gregorian = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
1420
+ // Testing availability of @available (https://stackoverflow.com/a/46927445/1033581)
1421
+ #if __clang_major__ < 9
1422
+ // Xcode 8-
1423
+ if (floor(NSFoundationVersionNumber) > NSFoundationVersionNumber10_8_4) {
1424
+ #else
1425
+ // Xcode 9+
1426
+ if (@available(macOS 10.9, iOS 7.0, watchOS 2.0, tvOS 9.0, *)) {
1427
+ #endif
1428
+ strPath = [strPath stringByAddingPercentEncodingWithAllowedCharacters:NSCharacterSet.URLPathAllowedCharacterSet];
1429
+ } else {
1430
+ strPath = [strPath stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
1431
+ }
668
1432
  #endif
669
- });
670
1433
 
671
- NSDateComponents *components = [[NSDateComponents alloc] init];
1434
+ // `NSString.stringByAddingPercentEncodingWithAllowedCharacters:` may theorically fail: https://stackoverflow.com/questions/33558933/
1435
+ // But because we auto-detect encoding using `NSString.stringEncodingForData:encodingOptions:convertedString:usedLossyConversion:`,
1436
+ // we likely already prevent UTF-16, UTF-32 and invalid Unicode in the form of unpaired surrogate chars: https://stackoverflow.com/questions/53043876/
1437
+ // To be on the safe side, we will still perform a guard check.
1438
+ if (strPath == nil) {
1439
+ return nil;
1440
+ }
672
1441
 
673
- NSAssert(0xFFFFFFFF == (kYearMask | kMonthMask | kDayMask | kHourMask | kMinuteMask | kSecondMask), @"[SSZipArchive] MSDOS date masks don't add up");
1442
+ // Add scheme "file:///" to support sanitation on names with a colon like "file:a/../../../usr/bin"
1443
+ strPath = [@"file:///" stringByAppendingString:strPath];
674
1444
 
675
- [components setYear:1980 + ((msdosDateTime & kYearMask) >> 25)];
676
- [components setMonth:(msdosDateTime & kMonthMask) >> 21];
677
- [components setDay:(msdosDateTime & kDayMask) >> 16];
678
- [components setHour:(msdosDateTime & kHourMask) >> 11];
679
- [components setMinute:(msdosDateTime & kMinuteMask) >> 5];
680
- [components setSecond:(msdosDateTime & kSecondMask) * 2];
1445
+ // Sanitize path traversal characters to prevent directory backtracking. Ignoring these characters mimicks the default behavior of the Unarchiving tool on macOS.
1446
+ // "../../../../../../../../../../../tmp/test.txt" -> "tmp/test.txt"
1447
+ // "a/b/../c.txt" -> "a/c.txt"
1448
+ strPath = [NSURL URLWithString:strPath].standardizedURL.absoluteString;
681
1449
 
682
- NSDate *date = [NSDate dateWithTimeInterval:0 sinceDate:[gregorian dateFromComponents:components]];
1450
+ // Remove the "file:///" scheme
1451
+ strPath = strPath.length < 8 ? @"" : [strPath substringFromIndex:8];
683
1452
 
684
- #if !__has_feature(objc_arc)
685
- [components release];
1453
+ // Remove the percent-encoding
1454
+ #if (__MAC_OS_X_VERSION_MIN_REQUIRED >= 1090 || __IPHONE_OS_VERSION_MIN_REQUIRED >= 70000 || __WATCH_OS_VERSION_MIN_REQUIRED >= 20000 || __TV_OS_VERSION_MIN_REQUIRED >= 90000)
1455
+ strPath = strPath.stringByRemovingPercentEncoding;
1456
+ #else
1457
+ // Testing availability of @available (https://stackoverflow.com/a/46927445/1033581)
1458
+ #if __clang_major__ < 9
1459
+ // Xcode 8-
1460
+ if (floor(NSFoundationVersionNumber) > NSFoundationVersionNumber10_8_4) {
1461
+ #else
1462
+ // Xcode 9+
1463
+ if (@available(macOS 10.9, iOS 7.0, watchOS 2.0, tvOS 9.0, *)) {
1464
+ #endif
1465
+ strPath = strPath.stringByRemovingPercentEncoding;
1466
+ } else {
1467
+ strPath = [strPath stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
1468
+ }
686
1469
  #endif
687
1470
 
688
- return date;
1471
+ return strPath;
1472
+ }
1473
+
1474
+ /// Detects if the path represented in this string is pointing outside of the targetDirectory passed as argument.
1475
+ ///
1476
+ /// Helps detecting and avoiding a security vulnerability described here:
1477
+ /// https://nvd.nist.gov/vuln/detail/CVE-2022-36943
1478
+ - (BOOL)_escapesTargetDirectory:(NSString *)targetDirectory {
1479
+ NSString *standardizedPath = [[self stringByStandardizingPath] stringByResolvingSymlinksInPath];
1480
+ NSString *standardizedTargetPath = [[targetDirectory stringByStandardizingPath] stringByResolvingSymlinksInPath];
1481
+
1482
+ NSArray *targetPathComponents = [standardizedTargetPath pathComponents];
1483
+ NSArray *pathComponents = [standardizedPath pathComponents];
1484
+
1485
+ if (pathComponents.count < targetPathComponents.count) return YES;
1486
+
1487
+ for (int idx = 0; idx < targetPathComponents.count; idx++) {
1488
+ if (![pathComponents[idx] isEqual: targetPathComponents[idx]]) {
1489
+ return YES;
1490
+ }
1491
+ }
1492
+
1493
+ return NO;
689
1494
  }
690
1495
 
691
- @end
1496
+ @end