Index: src/OFZIPArchive.h ================================================================== --- src/OFZIPArchive.h +++ src/OFZIPArchive.h @@ -20,28 +20,52 @@ OF_ASSUME_NONNULL_BEGIN @class OFArray OF_GENERIC(ObjectType); @class OFMutableArray OF_GENERIC(ObjectType); @class OFMutableDictionary OF_GENERIC(KeyType, ObjectType); +@class OFSeekableStream; @class OFStream; +@class OFZIPArchive; + +/** + * @protocol OFZIPArchiveDelegate OFZIPArchive.h ObjFW/OFZIPArchive.h + * + * @brief A delegate for OFZIPArchive. + */ +@protocol OFZIPArchiveDelegate +@optional +/** + * @brief A callback that is called when an @ref OFZIPArchive wants to read a + * different archive part. + * + * @param archive The archive that wants to read another part + * @param partNumber The number of the part the archive wants to read + * @param totalNumber The total number of parts of the archive + * @return The stream to read the needed part, or `nil` if no such part exists + */ +- (nullable OFSeekableStream *)archive: (OFZIPArchive *)archive + wantsPartNumbered: (unsigned int)partNumber + totalNumberOfParts: (unsigned int)totalNumber; +@end /** * @class OFZIPArchive OFZIPArchive.h ObjFW/OFZIPArchive.h * * @brief A class for accessing and manipulating ZIP files. */ OF_SUBCLASSING_RESTRICTED @interface OFZIPArchive: OFObject { + OFObject *_Nullable _delegate; OF_KINDOF(OFStream *) _stream; #ifdef OF_ZIP_ARCHIVE_M @public #endif int64_t _offset; @protected uint_least8_t _mode; - uint32_t _diskNumber, _centralDirectoryDisk; + uint32_t _diskNumber, _numDisks, _centralDirectoryDisk; uint64_t _centralDirectoryEntriesInDisk, _centralDirectoryEntries; uint64_t _centralDirectorySize; int64_t _centralDirectoryOffset; OFString *_Nullable _archiveComment; #ifdef OF_ZIP_ARCHIVE_M @@ -51,10 +75,16 @@ OFMutableDictionary OF_GENERIC(OFString *, OFZIPArchiveEntry *) *_pathToEntryMap; OFStream *_Nullable _lastReturnedStream; } +/** + * @brief The delegate of the ZIP archive. + */ +@property OF_NULLABLE_PROPERTY (assign, nonatomic) + OFObject *delegate; + /** * @brief The archive comment. */ @property OF_NULLABLE_PROPERTY (copy, nonatomic) OFString *archiveComment; Index: src/OFZIPArchive.m ================================================================== --- src/OFZIPArchive.m +++ src/OFZIPArchive.m @@ -148,26 +148,12 @@ *size -= 8; return field; } -static void -seekOrThrowInvalidFormat(OFSeekableStream *stream, - OFStreamOffset offset, OFSeekWhence whence) -{ - @try { - [stream seekToOffset: offset whence: whence]; - } @catch (OFSeekFailedException *e) { - if (e.errNo == EINVAL) - @throw [OFInvalidFormatException exception]; - - @throw e; - } -} - @implementation OFZIPArchive -@synthesize archiveComment = _archiveComment; +@synthesize delegate = _delegate, archiveComment = _archiveComment; + (instancetype)archiveWithStream: (OFStream *)stream mode: (OFString *)mode { return [[[self alloc] initWithStream: stream mode: mode] autorelease]; } @@ -213,11 +199,11 @@ [self of_readEntries]; } if (_mode == modeAppend) { _offset = _centralDirectoryOffset; - seekOrThrowInvalidFormat(_stream, + seekOrThrowInvalidFormat(self, NULL, (OFStreamOffset)_offset, OFSeekSet); } } @catch (id e) { /* * If we are in write or append mode, we do not want -[close] @@ -266,20 +252,49 @@ [_entries release]; [_pathToEntryMap release]; [super dealloc]; } + +static void +seekOrThrowInvalidFormat(OFZIPArchive *archive, const uint32_t *diskNumber, + OFStreamOffset offset, OFSeekWhence whence) +{ + if (diskNumber != NULL && *diskNumber != archive->_diskNumber) { + OFStream *oldStream = archive->_stream; + OFSeekableStream *stream = + [archive->_delegate archive: archive + wantsPartNumbered: *diskNumber + totalNumberOfParts: archive->_numDisks]; + + if (stream == nil) + @throw [OFInvalidFormatException exception]; + + archive->_diskNumber = *diskNumber; + archive->_stream = [stream retain]; + [oldStream release]; + } + + @try { + [archive->_stream seekToOffset: offset whence: whence]; + } @catch (OFSeekFailedException *e) { + if (e.errNo == EINVAL) + @throw [OFInvalidFormatException exception]; + + @throw e; + } +} - (void)of_readZIPInfo { void *pool = objc_autoreleasePoolPush(); uint16_t commentLength; OFStreamOffset offset = -22; bool valid = false; do { - seekOrThrowInvalidFormat(_stream, offset, OFSeekEnd); + seekOrThrowInvalidFormat(self, NULL, offset, OFSeekEnd); if ([_stream readLittleEndianInt32] == 0x06054B50) { valid = true; break; } @@ -286,11 +301,11 @@ } while (--offset >= -65557); if (!valid) @throw [OFInvalidFormatException exception]; - _diskNumber = [_stream readLittleEndianInt16]; + _diskNumber = _numDisks = [_stream readLittleEndianInt16]; _centralDirectoryDisk = [_stream readLittleEndianInt16]; _centralDirectoryEntriesInDisk = [_stream readLittleEndianInt16]; _centralDirectoryEntries = [_stream readLittleEndianInt16]; _centralDirectorySize = [_stream readLittleEndianInt32]; _centralDirectoryOffset = [_stream readLittleEndianInt32]; @@ -298,20 +313,21 @@ commentLength = [_stream readLittleEndianInt16]; _archiveComment = [[_stream readStringWithLength: commentLength encoding: OFStringEncodingCodepage437] copy]; - if (_diskNumber == 0xFFFF || + if (_numDisks == 0xFFFF || _centralDirectoryDisk == 0xFFFF || _centralDirectoryEntriesInDisk == 0xFFFF || _centralDirectoryEntries == 0xFFFF || _centralDirectorySize == 0xFFFFFFFF || _centralDirectoryOffset == 0xFFFFFFFF) { + uint32_t diskNumber; int64_t offset64; uint64_t size; - seekOrThrowInvalidFormat(_stream, offset - 20, OFSeekEnd); + seekOrThrowInvalidFormat(self, NULL, offset - 20, OFSeekEnd); if ([_stream readLittleEndianInt32] != 0x07064B50) { objc_autoreleasePoolPop(pool); return; } @@ -318,17 +334,18 @@ /* * FIXME: Handle number of the disk containing ZIP64 end of * central directory record. */ - [_stream readLittleEndianInt32]; + diskNumber = [_stream readLittleEndianInt32]; offset64 = [_stream readLittleEndianInt64]; + _numDisks = [_stream readLittleEndianInt32]; if (offset64 < 0 || (OFStreamOffset)offset64 != offset64) @throw [OFOutOfRangeException exception]; - seekOrThrowInvalidFormat(_stream, + seekOrThrowInvalidFormat(self, &diskNumber, (OFStreamOffset)offset64, OFSeekSet); if ([_stream readLittleEndianInt32] != 0x06064B50) @throw [OFInvalidFormatException exception]; @@ -339,11 +356,13 @@ /* version made by */ [_stream readLittleEndianInt16]; /* version needed to extract */ [_stream readLittleEndianInt16]; - _diskNumber = [_stream readLittleEndianInt32]; + if ([_stream readLittleEndianInt32] != _diskNumber) + @throw [OFInvalidFormatException exception]; + _centralDirectoryDisk = [_stream readLittleEndianInt32]; _centralDirectoryEntriesInDisk = [_stream readLittleEndianInt64]; _centralDirectoryEntries = [_stream readLittleEndianInt64]; _centralDirectorySize = [_stream readLittleEndianInt64]; @@ -364,11 +383,11 @@ if (_centralDirectoryOffset < 0 || (OFStreamOffset)_centralDirectoryOffset != _centralDirectoryOffset) @throw [OFOutOfRangeException exception]; - seekOrThrowInvalidFormat(_stream, + seekOrThrowInvalidFormat(self, &_centralDirectoryDisk, (OFStreamOffset)_centralDirectoryOffset, OFSeekSet); for (size_t i = 0; i < _centralDirectoryEntries; i++) { OFZIPArchiveEntry *entry = [[[OFZIPArchiveEntry alloc] of_initWithStream: _stream] autorelease]; @@ -411,10 +430,11 @@ - (OFStream *)streamForReadingFile: (OFString *)path { void *pool = objc_autoreleasePoolPush(); OFZIPArchiveEntry *entry; OFZIPArchiveLocalFileHeader *localFileHeader; + uint32_t startDiskNumber; int64_t offset64; if (_stream == nil) @throw [OFNotOpenException exceptionWithObject: self]; @@ -431,15 +451,17 @@ } @catch (OFNotOpenException *e) { /* Might have already been closed by the user - that's fine. */ } _lastReturnedStream = nil; + startDiskNumber = entry.of_startDiskNumber; offset64 = entry.of_localFileHeaderOffset; if (offset64 < 0 || (OFStreamOffset)offset64 != offset64) @throw [OFOutOfRangeException exception]; - seekOrThrowInvalidFormat(_stream, (OFStreamOffset)offset64, OFSeekSet); + seekOrThrowInvalidFormat(self, &startDiskNumber, + (OFStreamOffset)offset64, OFSeekSet); localFileHeader = [[[OFZIPArchiveLocalFileHeader alloc] initWithStream: _stream] autorelease]; if (![localFileHeader matchesEntry: entry]) @throw [OFInvalidFormatException exception]; Index: src/OFZIPArchiveEntry+Private.h ================================================================== --- src/OFZIPArchiveEntry+Private.h +++ src/OFZIPArchiveEntry+Private.h @@ -18,10 +18,11 @@ OF_ASSUME_NONNULL_BEGIN @interface OFZIPArchiveEntry () @property (readonly, nonatomic) uint16_t of_lastModifiedFileTime, of_lastModifiedFileDate; +@property (readonly, nonatomic) uint32_t of_startDiskNumber; @property (readonly, nonatomic) int64_t of_localFileHeaderOffset; - (instancetype)of_init OF_METHOD_FAMILY(init); - (instancetype)of_initWithStream: (OFStream *)stream OF_METHOD_FAMILY(init) OF_DIRECT; Index: src/OFZIPArchiveEntry.m ================================================================== --- src/OFZIPArchiveEntry.m +++ src/OFZIPArchiveEntry.m @@ -405,10 +405,15 @@ - (uint16_t)of_lastModifiedFileDate { return _lastModifiedFileDate; } + +- (uint32_t)of_startDiskNumber +{ + return _startDiskNumber; +} - (int64_t)of_localFileHeaderOffset { return _localFileHeaderOffset; } Index: utils/ofarc/ZIPArchive.h ================================================================== --- utils/ofarc/ZIPArchive.h +++ utils/ofarc/ZIPArchive.h @@ -15,10 +15,11 @@ #import "OFZIPArchive.h" #import "Archive.h" -@interface ZIPArchive: OFObject +@interface ZIPArchive: OFObject { + OFString *_path; OFZIPArchive *_archive; } @end Index: utils/ofarc/ZIPArchive.m ================================================================== --- utils/ofarc/ZIPArchive.m +++ utils/ofarc/ZIPArchive.m @@ -100,12 +100,14 @@ encoding: (OFStringEncoding)encoding { self = [super init]; @try { + _path = [path copy]; _archive = [[OFZIPArchive alloc] initWithStream: stream mode: mode]; + _archive.delegate = self; } @catch (id e) { [self release]; @throw e; } @@ -112,14 +114,44 @@ return self; } - (void)dealloc { + [_path release]; [_archive release]; [super dealloc]; } + +- (OFSeekableStream *)archive: (OFZIPArchive *)archive + wantsPartNumbered: (unsigned int)partNumber + totalNumberOfParts: (unsigned int)totalNumber +{ + OFString *path; + + if ([_path.pathExtension caseInsensitiveCompare: @"zip"] != + OFOrderedSame) + return nil; + + if (partNumber > 98) + return nil; + + if (partNumber == totalNumber) + path = _path; + else + path = [_path.stringByDeletingPathExtension + stringByAppendingFormat: @".z%02u", partNumber + 1]; + + @try { + return [OFFile fileWithPath: path mode: @"r"]; + } @catch (OFOpenItemFailedException *e) { + if (e.errNo != ENOENT) + @throw e; + + return nil; + } +} - (void)listFiles { for (OFZIPArchiveEntry *entry in _archive.entries) { void *pool = objc_autoreleasePoolPush();