From fe86f8572550e1f6d0b409de582e67210cf30b6e Mon Sep 17 00:00:00 2001 From: Glen Low Date: Sat, 7 Nov 2015 16:24:50 +0800 Subject: [PATCH] Add fine-grained filename encoding * Add entry encoding property based on the EFS flag of the general purpose bit flags. * Add entry fileNameWithEncoding: methods to force a particular entry-level encoding for the file name. * Remove archive-level encoding for file name. * Default encoding is now CP-437, as per the Zip file format specification. This is a breaking change since previous default encoding was UTF-8. * Update unit tests. * Update podspec to 8.1. --- ZipZap/ZZArchive.mm | 7 +------ ZipZap/ZZArchiveEntry.h | 14 +++++++++++++- ZipZap/ZZArchiveEntry.m | 12 +++++++++++- ZipZap/ZZConstants.h | 5 ----- ZipZap/ZZConstants.m | 2 -- ZipZap/ZZHeaders.h | 2 +- ZipZap/ZZNewArchiveEntry.h | 5 ++++- ZipZap/ZZNewArchiveEntry.mm | 14 ++++++++++++-- ZipZap/ZZNewArchiveEntryWriter.mm | 2 +- ZipZap/ZZOldArchiveEntry.h | 7 ++++--- ZipZap/ZZOldArchiveEntry.mm | 29 +++++++++++++---------------- ZipZapTests/ZZUnzipTests.m | 9 +++++++-- ZipZapTests/ZZZipTests.m | 9 +++++++-- zipzap.podspec.json | 4 ++-- 14 files changed, 76 insertions(+), 45 deletions(-) diff --git a/ZipZap/ZZArchive.mm b/ZipZap/ZZArchive.mm index efb8914c..724b4b75 100644 --- a/ZipZap/ZZArchive.mm +++ b/ZipZap/ZZArchive.mm @@ -35,7 +35,6 @@ - (BOOL)loadCanMiss:(BOOL)canMiss error:(out NSError**)error; @implementation ZZArchive { id _channel; - NSStringEncoding _encoding; } + (instancetype)archiveWithURL:(NSURL*)URL @@ -80,9 +79,6 @@ - (instancetype)initWithChannel:(id)channel { _channel = channel; - NSNumber* encoding = options[ZZOpenOptionsEncodingKey]; - _encoding = encoding ? encoding.unsignedIntegerValue : NSUTF8StringEncoding; - NSNumber* createIfMissing = options[ZZOpenOptionsCreateIfMissingKey]; if (![self loadCanMiss:createIfMissing.boolValue error:error]) return nil; @@ -164,8 +160,7 @@ - (BOOL)loadCanMiss:(BOOL)canMiss error:(out NSError**)error + nextCentralFileHeader->relativeOffsetOfLocalHeader); [entries addObject:[[ZZOldArchiveEntry alloc] initWithCentralFileHeader:nextCentralFileHeader - localFileHeader:nextLocalFileHeader - encoding:_encoding]]; + localFileHeader:nextLocalFileHeader]]; nextCentralFileHeader = nextCentralFileHeader->nextCentralFileHeader(); } diff --git a/ZipZap/ZZArchiveEntry.h b/ZipZap/ZZArchiveEntry.h index 6775229d..2fa9b083 100644 --- a/ZipZap/ZZArchiveEntry.h +++ b/ZipZap/ZZArchiveEntry.h @@ -83,7 +83,7 @@ NS_ASSUME_NONNULL_BEGIN @property (readonly, nonatomic) mode_t fileMode; /** - * The file name of the entry. + * The file name of the entry. Encoding is either CP-437 or UTF-8, depending on the EFS flag of the general purpose bit flags. */ @property (readonly, nonatomic) NSString* fileName; @@ -92,6 +92,11 @@ NS_ASSUME_NONNULL_BEGIN */ @property (readonly, nonatomic) NSData* rawFileName; +/** + * The encoding of filename and comment fields, according to the EFS flag of the general purpose bit flags. + */ +@property (readonly, nonatomic) NSStringEncoding encoding; + /** * Creates a new file entry from a streaming callback. * @@ -169,6 +174,13 @@ NS_ASSUME_NONNULL_BEGIN */ - (BOOL)check:(out NSError**)error; +/** + * The file name of the entry with the given encoding. + * + * @param encoding The encoding for the filename. + */ +- (NSString*)fileNameWithEncoding:(NSStringEncoding)encoding; + /** * Creates a stream to represent the entry file. * diff --git a/ZipZap/ZZArchiveEntry.m b/ZipZap/ZZArchiveEntry.m index 50280142..b3e45a4b 100644 --- a/ZipZap/ZZArchiveEntry.m +++ b/ZipZap/ZZArchiveEntry.m @@ -117,7 +117,7 @@ - (mode_t)fileMode - (NSString*)fileName { - return nil; + return [self fileNameWithEncoding:self.encoding]; } - (NSData*)rawFileName @@ -125,6 +125,11 @@ - (NSData*)rawFileName return nil; } +- (NSStringEncoding)encoding +{ + return (NSStringEncoding)0; +} + - (NSInputStream*)newStreamWithError:(NSError**)error { return [self newStreamWithPassword:nil error:error]; @@ -140,6 +145,11 @@ - (BOOL)check:(NSError**)error return YES; } +- (NSString*)fileNameWithEncoding:(NSStringEncoding)encoding +{ + return nil; +} + - (NSData*)newDataWithError:(NSError**)error { return [self newDataWithPassword:nil error:error]; diff --git a/ZipZap/ZZConstants.h b/ZipZap/ZZConstants.h index dcb1f8d7..69884185 100644 --- a/ZipZap/ZZConstants.h +++ b/ZipZap/ZZConstants.h @@ -73,11 +73,6 @@ typedef NS_ENUM(uint8_t, ZZAESEncryptionStrength) ZZAESEncryptionStrength256 = 0x03 }; -/** - * An NSNumber object that contains the string encoding to use for entry file names and comments. Default is NSUTF8StringEncoding. - */ -extern NSString* const ZZOpenOptionsEncodingKey; - /** * An NSNumber object that determines whether to create the archive file if it is missing. Creation occurs during -[ZZArchive updateEntries:error:]. Default is @NO. */ diff --git a/ZipZap/ZZConstants.m b/ZipZap/ZZConstants.m index 3fd2d83e..a3f7ab11 100644 --- a/ZipZap/ZZConstants.m +++ b/ZipZap/ZZConstants.m @@ -8,6 +8,4 @@ #import "ZZConstants.h" -NSString* const ZZOpenOptionsEncodingKey = @"ZZOpenOptionsEncodingKey"; - NSString* const ZZOpenOptionsCreateIfMissingKey = @"ZZOpenOptionsCreateIfMissingKey"; diff --git a/ZipZap/ZZHeaders.h b/ZipZap/ZZHeaders.h index 72d686df..1eb2e9f7 100644 --- a/ZipZap/ZZHeaders.h +++ b/ZipZap/ZZHeaders.h @@ -42,7 +42,7 @@ enum class ZZGeneralPurposeBitFlag: uint16_t superFastCompression = (1 << 1) | (1 << 2), sizeInDataDescriptor = 1 << 3, encryptionStrong = 1 << 6, - fileNameUTF8Encoded = 1 << 11 + languageEncoding = 1 << 11 }; inline ZZGeneralPurposeBitFlag operator|(ZZGeneralPurposeBitFlag lhs, ZZGeneralPurposeBitFlag rhs) diff --git a/ZipZap/ZZNewArchiveEntry.h b/ZipZap/ZZNewArchiveEntry.h index 883f67bb..16719bfb 100644 --- a/ZipZap/ZZNewArchiveEntry.h +++ b/ZipZap/ZZNewArchiveEntry.h @@ -23,7 +23,8 @@ NS_ASSUME_NONNULL_BEGIN @property (readonly, nonatomic) BOOL compressed; @property (readonly, nonatomic) NSDate* lastModified; @property (readonly, nonatomic) mode_t fileMode; -@property (readonly, nonatomic) NSString* fileName; +@property (readonly, nonatomic) NSData* rawFileName; +@property (readonly, nonatomic) NSStringEncoding encoding; - (instancetype)init NS_UNAVAILABLE; @@ -35,6 +36,8 @@ NS_ASSUME_NONNULL_BEGIN streamBlock:(nullable BOOL(^)(NSOutputStream* stream, NSError** error))streamBlock dataConsumerBlock:(nullable BOOL(^)(CGDataConsumerRef dataConsumer, NSError** error))dataConsumerBlock NS_DESIGNATED_INITIALIZER; +- (NSString*)fileNameWithEncoding:(NSStringEncoding)encoding; + @end NS_ASSUME_NONNULL_END diff --git a/ZipZap/ZZNewArchiveEntry.mm b/ZipZap/ZZNewArchiveEntry.mm index 53009c63..70eab1fd 100644 --- a/ZipZap/ZZNewArchiveEntry.mm +++ b/ZipZap/ZZNewArchiveEntry.mm @@ -56,9 +56,14 @@ - (mode_t)fileMode return _fileMode; } -- (NSString*)fileName +- (NSData*)rawFileName { - return _fileName; + return [_fileName dataUsingEncoding:NSUTF8StringEncoding]; +} + +- (NSStringEncoding)encoding +{ + return NSUTF8StringEncoding; } - (id)newWriterCanSkipLocalFile:(BOOL)canSkipLocalFile @@ -72,5 +77,10 @@ - (NSString*)fileName dataConsumerBlock:_dataConsumerBlock]; } +- (NSString*)fileNameWithEncoding:(NSStringEncoding)encoding +{ + return _fileName; +} + @end diff --git a/ZipZap/ZZNewArchiveEntryWriter.mm b/ZipZap/ZZNewArchiveEntryWriter.mm index f1f03fdc..8a377063 100644 --- a/ZipZap/ZZNewArchiveEntryWriter.mm +++ b/ZipZap/ZZNewArchiveEntryWriter.mm @@ -102,7 +102,7 @@ - (instancetype)initWithFileName:(NSString*)fileName compressionFlag = ZZGeneralPurposeBitFlag::maximumCompression; break; } - centralFileHeader->generalPurposeBitFlag = localFileHeader->generalPurposeBitFlag = compressionFlag | ZZGeneralPurposeBitFlag::sizeInDataDescriptor | ZZGeneralPurposeBitFlag::fileNameUTF8Encoded; + centralFileHeader->generalPurposeBitFlag = localFileHeader->generalPurposeBitFlag = compressionFlag | ZZGeneralPurposeBitFlag::sizeInDataDescriptor | ZZGeneralPurposeBitFlag::languageEncoding; centralFileHeader->compressionMethod = localFileHeader->compressionMethod = compressionLevel ? ZZCompressionMethod::deflated : ZZCompressionMethod::stored; diff --git a/ZipZap/ZZOldArchiveEntry.h b/ZipZap/ZZOldArchiveEntry.h index 4a6ac84d..36b8aa0c 100644 --- a/ZipZap/ZZOldArchiveEntry.h +++ b/ZipZap/ZZOldArchiveEntry.h @@ -29,14 +29,15 @@ NS_ASSUME_NONNULL_BEGIN @property (readonly, nonatomic) NSUInteger compressedSize; @property (readonly, nonatomic) NSUInteger uncompressedSize; @property (readonly, nonatomic) mode_t fileMode; -@property (readonly, nonatomic) NSString* fileName; @property (readonly, nonatomic) NSData* rawFileName; +@property (readonly, nonatomic) NSStringEncoding encoding; - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithCentralFileHeader:(struct ZZCentralFileHeader*)centralFileHeader - localFileHeader:(struct ZZLocalFileHeader*)localFileHeader - encoding:(NSStringEncoding)encoding NS_DESIGNATED_INITIALIZER; + localFileHeader:(struct ZZLocalFileHeader*)localFileHeader NS_DESIGNATED_INITIALIZER; + +- (NSString*)fileNameWithEncoding:(NSStringEncoding)encoding; @end diff --git a/ZipZap/ZZOldArchiveEntry.mm b/ZipZap/ZZOldArchiveEntry.mm index 319dc7f9..18ae2ff6 100644 --- a/ZipZap/ZZOldArchiveEntry.mm +++ b/ZipZap/ZZOldArchiveEntry.mm @@ -24,7 +24,6 @@ @interface ZZOldArchiveEntry () - (NSData*)fileData; -- (NSString*)stringWithBytes:(uint8_t*)bytes length:(NSUInteger)length; - (BOOL)checkEncryptionAndCompression:(out NSError**)error; - (NSInputStream*)streamForData:(NSData*)data withPassword:(NSString*)password error:(out NSError**)error; @@ -35,19 +34,16 @@ @implementation ZZOldArchiveEntry { ZZCentralFileHeader* _centralFileHeader; ZZLocalFileHeader* _localFileHeader; - NSStringEncoding _encoding; ZZEncryptionMode _encryptionMode; } - (instancetype)initWithCentralFileHeader:(struct ZZCentralFileHeader*)centralFileHeader localFileHeader:(struct ZZLocalFileHeader*)localFileHeader - encoding:(NSStringEncoding)encoding { if ((self = [super init])) { _centralFileHeader = centralFileHeader; _localFileHeader = localFileHeader; - _encoding = encoding; if ((_centralFileHeader->generalPurposeBitFlag & ZZGeneralPurposeBitFlag::encrypted) != ZZGeneralPurposeBitFlag::none) { @@ -91,14 +87,6 @@ - (NSData*)fileData return [NSData dataWithBytesNoCopy:dataStart length:dataLength freeWhenDone:NO]; } -- (NSString*)stringWithBytes:(uint8_t*)bytes length:(NSUInteger)length -{ - // if EFS bit is set, use UTF-8; otherwise use fallback encoding - return [[NSString alloc] initWithBytes:bytes - length:length - encoding:(_centralFileHeader->generalPurposeBitFlag & ZZGeneralPurposeBitFlag::fileNameUTF8Encoded) == ZZGeneralPurposeBitFlag::none ? _encoding :NSUTF8StringEncoding]; -} - - (ZZCompressionMethod)compressionMethod { if (_encryptionMode == ZZEncryptionModeWinZipAES) @@ -169,15 +157,17 @@ - (mode_t)fileMode } } -- (NSString*)fileName +- (NSData*)rawFileName { - return [self stringWithBytes:_centralFileHeader->fileName() + return [NSData dataWithBytes:_centralFileHeader->fileName() length:_centralFileHeader->fileNameLength]; } -- (NSData*)rawFileName +- (NSStringEncoding)encoding { - return [NSData dataWithBytes:_centralFileHeader->fileName() length:_centralFileHeader->fileNameLength]; + return (_centralFileHeader->generalPurposeBitFlag & ZZGeneralPurposeBitFlag::languageEncoding) == ZZGeneralPurposeBitFlag::none ? + CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingDOSLatinUS) : // CP-437 + NSUTF8StringEncoding; // UTF-8 } - (BOOL)check:(out NSError**)error @@ -258,6 +248,13 @@ - (BOOL)check:(out NSError**)error return YES; } +- (NSString*)fileNameWithEncoding:(NSStringEncoding)encoding +{ + return [[NSString alloc] initWithBytes:_centralFileHeader->fileName() + length:_centralFileHeader->fileNameLength + encoding:encoding]; +} + - (BOOL)checkEncryptionAndCompression:(out NSError**)error { switch (_encryptionMode) diff --git a/ZipZapTests/ZZUnzipTests.m b/ZipZapTests/ZZUnzipTests.m index 69599c03..93cfecbe 100644 --- a/ZipZapTests/ZZUnzipTests.m +++ b/ZipZapTests/ZZUnzipTests.m @@ -138,11 +138,16 @@ - (void)testZipEntryConsistentWithOriginalFile ZZArchiveEntry* nextEntry = _zipFile.entries[index]; NSString* nextEntryFilePath = _entryFilePaths[index]; - XCTAssertEqualObjects(nextEntry.fileName, + XCTAssertEqualObjects([nextEntry fileNameWithEncoding:NSUTF8StringEncoding], nextEntryFilePath, @"zipFile.entries[%lu].fileName must match the original file name.", (unsigned long)index); - + + XCTAssertEqualObjects([nextEntry rawFileName], + [nextEntryFilePath dataUsingEncoding:NSUTF8StringEncoding], + @"zipFile.entries[%lu].rawFileName must match the original raw file name.", + (unsigned long)index); + NSData* fileData = [self dataAtFilePath:nextEntryFilePath]; XCTAssertEqual(nextEntry.crc32, crc32(0, (const Bytef*)fileData.bytes, (uInt)fileData.length), diff --git a/ZipZapTests/ZZZipTests.m b/ZipZapTests/ZZZipTests.m index 028ec6cd..b1aa95b5 100644 --- a/ZipZapTests/ZZZipTests.m +++ b/ZipZapTests/ZZZipTests.m @@ -57,7 +57,8 @@ - (NSArray*)recordsForZipEntries:(NSArray*)zipEntries @"fileMode": @(zipEntry.fileMode), @"compressed": @(zipEntry.compressed), @"lastModified": zipEntry.lastModified, - @"fileName": zipEntry.fileName + @"fileName": [zipEntry fileNameWithEncoding:NSUTF8StringEncoding], + @"rawFileName": zipEntry.rawFileName }]; return records; } @@ -124,7 +125,11 @@ - (void)checkZipEntryRecords:(NSArray*)newEntryRecords nextNewEntry[@"fileName"], @"Zip entry #%lu file name must match new entry file name.", (unsigned long)index); - + + XCTAssertEqualObjects([nextZipInfo[8] dataUsingEncoding:NSUTF8StringEncoding], + nextNewEntry[@"rawFileName"], + @"Zip entry #%lu raw file name must match new entry raw file name.", + (unsigned long)index); if (checkerBlock) { diff --git a/zipzap.podspec.json b/zipzap.podspec.json index 42321956..6b70f4bb 100644 --- a/zipzap.podspec.json +++ b/zipzap.podspec.json @@ -1,6 +1,6 @@ { "name": "zipzap", - "version": "8.0.6", + "version": "8.1", "authors": { "Pixelglow Software": "glen.low@pixelglow.com" }, @@ -8,7 +8,7 @@ "homepage": "https://github.com/pixelglow/ZipZap", "source": { "git": "https://github.com/pixelglow/ZipZap.git", - "tag": "8.0.6" + "tag": "8.1" }, "summary": "ZipZap is a zip file I/O library for Mac OS X and iOS.", "description": "The zip file is an ideal container for compound Objective-C documents. Zip files are widely used and well understood. You can randomly access their parts. The format compresses decently and has extensive operating system and tool support. So we want to make this format an even easier choice for you. Thus, the library features:\n\n- Easy-to-use interface: The public API offers just three classes! Yet you can look through zip files using familiar NSArray collections and properties. And you can zip, unzip and rezip zip files through familiar NSData, NSStream and Image I/O classes.\n- Efficient implementation: We've optimized zip file reading and writing to reduce virtual memory pressure and disk file thrashing. Depending on how your compound document is organized, updating a single entry can be faster than writing the same data to a separate file.\n- File format compatibility: Since ZipZap closely follows the zip file format specification, it is works with most Mac, Linux and Windows zip tools.\n",