@@ -1,7 +1,7 @@ /* - * Copyright (c) 2008-2022 Jonathan Schleifer + * Copyright (c) 2008-2024 Jonathan Schleifer * * All rights reserved. * * This file is part of ObjFW. It may be distributed under the terms of the * Q Public License 1.0, which can be found in the file LICENSE.QPL included in @@ -20,21 +20,21 @@ #include #import "OFZIPArchive.h" #import "OFZIPArchiveEntry.h" #import "OFZIPArchiveEntry+Private.h" -#import "OFArchiveURIHandler.h" +#import "OFArchiveIRIHandler.h" #import "OFArray.h" #import "OFCRC32.h" #import "OFData.h" #import "OFDictionary.h" +#import "OFIRI.h" +#import "OFIRIHandler.h" #import "OFInflate64Stream.h" #import "OFInflateStream.h" #import "OFSeekableStream.h" #import "OFStream.h" -#import "OFURI.h" -#import "OFURIHandler.h" #import "OFChecksumMismatchException.h" #import "OFInvalidArgumentException.h" #import "OFInvalidFormatException.h" #import "OFNotImplementedException.h" @@ -45,12 +45,11 @@ #import "OFTruncatedDataException.h" #import "OFUnsupportedVersionException.h" #import "OFWriteFailedException.h" /* - * FIXME: Current limitations: - * - Split archives are not supported. + * TODO: Current limitations: * - Encrypted files cannot be read. */ enum { modeRead, @@ -83,11 +82,12 @@ OF_DIRECT_MEMBERS @interface OFZIPArchiveFileReadStream: OFStream { OFZIPArchive *_archive; - OFStream *_stream, *_decompressedStream; + OFZIPArchiveEntryCompressionMethod _compressionMethod; + OF_KINDOF(OFStream *) _decompressedStream; OFZIPArchiveEntry *_entry; unsigned long long _toRead; uint32_t _CRC32; bool _atEndOfStream; } @@ -99,20 +99,23 @@ OF_DIRECT_MEMBERS @interface OFZIPArchiveFileWriteStream: OFStream { OFZIPArchive *_archive; - OFStream *_stream; + OF_KINDOF(OFStream *) _stream; uint32_t _CRC32; + OFStreamOffset _CRC32Offset, _size64Offset; @public unsigned long long _bytesWritten; OFMutableZIPArchiveEntry *_entry; } - (instancetype)of_initWithArchive: (OFZIPArchive *)archive stream: (OFStream *)stream - entry: (OFMutableZIPArchiveEntry *)entry; + entry: (OFMutableZIPArchiveEntry *)entry + CRC32Offset: (OFStreamOffset)CRC32Offset + size64Offset: (OFStreamOffset)size64Offset; @end uint32_t OFZIPArchiveReadField32(const uint8_t **data, uint16_t *size) { @@ -145,40 +148,60 @@ *size -= 8; return field; } +@implementation OFZIPArchive +@synthesize delegate = _delegate, archiveComment = _archiveComment; + static void -seekOrThrowInvalidFormat(OFSeekableStream *stream, +seekOrThrowInvalidFormat(OFZIPArchive *archive, const uint32_t *diskNumber, OFStreamOffset offset, OFSeekWhence whence) { + if (diskNumber != NULL && *diskNumber != archive->_diskNumber) { + OFStream *oldStream = archive->_stream; + OFSeekableStream *stream; + + if (archive->_mode != modeRead || + *diskNumber > archive->_lastDiskNumber) + @throw [OFInvalidFormatException exception]; + + stream = [archive->_delegate archive: archive + wantsPartNumbered: *diskNumber + lastPartNumber: archive->_lastDiskNumber]; + + if (stream == nil) + @throw [OFInvalidFormatException exception]; + + archive->_diskNumber = *diskNumber; + archive->_stream = [stream retain]; + [oldStream release]; + } + @try { - [stream seekToOffset: offset whence: whence]; + [archive->_stream seekToOffset: offset whence: whence]; } @catch (OFSeekFailedException *e) { if (e.errNo == EINVAL) @throw [OFInvalidFormatException exception]; @throw e; } } -@implementation OFZIPArchive -@synthesize archiveComment = _archiveComment; - + (instancetype)archiveWithStream: (OFStream *)stream mode: (OFString *)mode { return [[[self alloc] initWithStream: stream mode: mode] autorelease]; } -+ (instancetype)archiveWithURI: (OFURI *)URI mode: (OFString *)mode ++ (instancetype)archiveWithIRI: (OFIRI *)IRI mode: (OFString *)mode { - return [[[self alloc] initWithURI: URI mode: mode] autorelease]; + return [[[self alloc] initWithIRI: IRI mode: mode] autorelease]; } -+ (OFURI *)URIForFilePath: (OFString *)path inArchiveWithURI: (OFURI *)URI ++ (OFIRI *)IRIForFilePath: (OFString *)path inArchiveWithIRI: (OFIRI *)IRI { - return OFArchiveURIHandlerURIForFileInArchive(@"zip", path, URI); + return OFArchiveIRIHandlerIRIForFileInArchive(@"zip", path, IRI); } - (instancetype)init { OF_INVALID_INIT_METHOD @@ -210,11 +233,11 @@ [self of_readEntries]; } if (_mode == modeAppend) { _offset = _centralDirectoryOffset; - seekOrThrowInvalidFormat((OFSeekableStream *)_stream, + seekOrThrowInvalidFormat(self, NULL, (OFStreamOffset)_offset, OFSeekSet); } } @catch (id e) { /* * If we are in write or append mode, we do not want -[close] @@ -229,20 +252,20 @@ } return self; } -- (instancetype)initWithURI: (OFURI *)URI mode: (OFString *)mode +- (instancetype)initWithIRI: (OFIRI *)IRI mode: (OFString *)mode { void *pool = objc_autoreleasePoolPush(); OFStream *stream; @try { if ([mode isEqual: @"a"]) - stream = [OFURIHandler openItemAtURI: URI mode: @"r+"]; + stream = [OFIRIHandler openItemAtIRI: IRI mode: @"r+"]; else - stream = [OFURIHandler openItemAtURI: URI mode: mode]; + stream = [OFIRIHandler openItemAtIRI: IRI mode: mode]; } @catch (id e) { [self release]; @throw e; } @@ -272,12 +295,11 @@ uint16_t commentLength; OFStreamOffset offset = -22; bool valid = false; do { - seekOrThrowInvalidFormat((OFSeekableStream *)_stream, - offset, OFSeekEnd); + seekOrThrowInvalidFormat(self, NULL, offset, OFSeekEnd); if ([_stream readLittleEndianInt32] == 0x06054B50) { valid = true; break; } @@ -284,11 +306,11 @@ } while (--offset >= -65557); if (!valid) @throw [OFInvalidFormatException exception]; - _diskNumber = [_stream readLittleEndianInt16]; + _diskNumber = _lastDiskNumber = [_stream readLittleEndianInt16]; _centralDirectoryDisk = [_stream readLittleEndianInt16]; _centralDirectoryEntriesInDisk = [_stream readLittleEndianInt16]; _centralDirectoryEntries = [_stream readLittleEndianInt16]; _centralDirectorySize = [_stream readLittleEndianInt32]; _centralDirectoryOffset = [_stream readLittleEndianInt32]; @@ -296,21 +318,21 @@ commentLength = [_stream readLittleEndianInt16]; _archiveComment = [[_stream readStringWithLength: commentLength encoding: OFStringEncodingCodepage437] copy]; - if (_diskNumber == 0xFFFF || + if (_lastDiskNumber == 0xFFFF || _centralDirectoryDisk == 0xFFFF || _centralDirectoryEntriesInDisk == 0xFFFF || _centralDirectoryEntries == 0xFFFF || _centralDirectorySize == 0xFFFFFFFF || _centralDirectoryOffset == 0xFFFFFFFF) { + uint32_t diskNumber; int64_t offset64; uint64_t size; - seekOrThrowInvalidFormat((OFSeekableStream *)_stream, - offset - 20, OFSeekEnd); + seekOrThrowInvalidFormat(self, NULL, offset - 20, OFSeekEnd); if ([_stream readLittleEndianInt32] != 0x07064B50) { objc_autoreleasePoolPop(pool); return; } @@ -317,17 +339,21 @@ /* * FIXME: Handle number of the disk containing ZIP64 end of * central directory record. */ - [_stream readLittleEndianInt32]; + diskNumber = [_stream readLittleEndianInt32]; offset64 = [_stream readLittleEndianInt64]; + _lastDiskNumber = [_stream readLittleEndianInt32]; + if (_lastDiskNumber == 0) + @throw [OFInvalidFormatException exception]; + _lastDiskNumber--; if (offset64 < 0 || (OFStreamOffset)offset64 != offset64) @throw [OFOutOfRangeException exception]; - seekOrThrowInvalidFormat((OFSeekableStream *)_stream, + seekOrThrowInvalidFormat(self, &diskNumber, (OFStreamOffset)offset64, OFSeekSet); if ([_stream readLittleEndianInt32] != 0x06064B50) @throw [OFInvalidFormatException exception]; @@ -338,11 +364,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]; @@ -363,15 +391,48 @@ if (_centralDirectoryOffset < 0 || (OFStreamOffset)_centralDirectoryOffset != _centralDirectoryOffset) @throw [OFOutOfRangeException exception]; - seekOrThrowInvalidFormat((OFSeekableStream *)_stream, + seekOrThrowInvalidFormat(self, &_centralDirectoryDisk, (OFStreamOffset)_centralDirectoryOffset, OFSeekSet); for (size_t i = 0; i < _centralDirectoryEntries; i++) { - OFZIPArchiveEntry *entry = [[[OFZIPArchiveEntry alloc] + OFZIPArchiveEntry *entry; + char buffer; + + /* + * The stream might have 0 bytes left to read, but might not + * realize that before a read is attempted, where it will then + * return a length of 0. But OFZIPArchiveEntry expects to be + * able to read the entire entry and will then throw an + * OFTruncatedDataException. Therefore, try to peek one byte to + * make sure the stream realizes that it's at the end. + */ + if ([_stream readIntoBuffer: &buffer length: 1] == 1) + [_stream unreadFromBuffer: &buffer length: 1]; + + if ([_stream isAtEndOfStream]) { + OFStream *oldStream = _stream; + OFSeekableStream *stream; + + if (_diskNumber >= _lastDiskNumber) + @throw [OFTruncatedDataException exception]; + + stream = [_delegate archive: self + wantsPartNumbered: _diskNumber + 1 + lastPartNumber: _lastDiskNumber]; + + if (stream == nil) + @throw [OFInvalidFormatException exception]; + + _diskNumber++; + _stream = [stream retain]; + [oldStream release]; + } + + entry = [[[OFZIPArchiveEntry alloc] of_initWithStream: _stream] autorelease]; if ([_pathToEntryMap objectForKey: entry.fileName] != nil) @throw [OFInvalidFormatException exception]; @@ -410,10 +471,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]; @@ -430,15 +492,16 @@ } @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((OFSeekableStream *)_stream, + seekOrThrowInvalidFormat(self, &startDiskNumber, (OFStreamOffset)offset64, OFSeekSet); localFileHeader = [[[OFZIPArchiveLocalFileHeader alloc] initWithStream: _stream] autorelease]; if (![localFileHeader matchesEntry: entry]) @@ -463,15 +526,16 @@ return _lastReturnedStream; } - (OFStream *)streamForWritingEntry: (OFZIPArchiveEntry *)entry_ { - /* TODO: Avoid data descriptor when _stream is an OFSeekableStream */ int64_t offsetAdd = 0; void *pool; OFMutableZIPArchiveEntry *entry; OFString *fileName; + bool seekable; + OFStreamOffset CRC32Offset = 0, size64Offset = 0; OFData *extraField; uint16_t fileNameLength, extraFieldLength; if (_stream == nil) @throw [OFNotOpenException exceptionWithObject: self]; @@ -505,25 +569,29 @@ extraFieldLength = extraField.count; if (UINT16_MAX - extraFieldLength < 20) @throw [OFOutOfRangeException exception]; + seekable = [_stream isKindOfClass: [OFSeekableStream class]]; + entry.versionMadeBy = (entry.versionMadeBy & 0xFF00) | 45; entry.minVersionNeeded = (entry.minVersionNeeded & 0xFF00) | 45; entry.compressedSize = 0; entry.uncompressedSize = 0; entry.CRC32 = 0; - entry.generalPurposeBitFlag |= (1u << 3) | (1u << 11); + entry.generalPurposeBitFlag |= (seekable ? 0 : (1u << 3)) | (1u << 11); entry.of_localFileHeaderOffset = _offset; [_stream writeLittleEndianInt32: 0x04034B50]; [_stream writeLittleEndianInt16: entry.minVersionNeeded]; [_stream writeLittleEndianInt16: entry.generalPurposeBitFlag]; [_stream writeLittleEndianInt16: entry.compressionMethod]; [_stream writeLittleEndianInt16: entry.of_lastModifiedFileTime]; [_stream writeLittleEndianInt16: entry.of_lastModifiedFileDate]; - /* We use the data descriptor */ + /* Written later or data descriptor used instead */ + if (seekable) + CRC32Offset = [_stream seekToOffset: 0 whence: OFSeekCurrent]; [_stream writeLittleEndianInt32: 0]; /* We use ZIP64 */ [_stream writeLittleEndianInt32: 0xFFFFFFFF]; [_stream writeLittleEndianInt32: 0xFFFFFFFF]; [_stream writeLittleEndianInt16: fileNameLength]; @@ -533,11 +601,13 @@ [_stream writeString: fileName]; offsetAdd += fileNameLength; [_stream writeLittleEndianInt16: OFZIPArchiveEntryExtraFieldTagZIP64]; [_stream writeLittleEndianInt16: 16]; - /* We use the data descriptor */ + /* Written later or data descriptor used instead */ + if (seekable) + size64Offset = [_stream seekToOffset: 0 whence: OFSeekCurrent]; [_stream writeLittleEndianInt64: 0]; [_stream writeLittleEndianInt64: 0]; offsetAdd += (2 * 2) + (2 * 8); if (extraField != nil) @@ -550,11 +620,13 @@ _offset += offsetAdd; _lastReturnedStream = [[OFZIPArchiveFileWriteStream alloc] of_initWithArchive: self stream: _stream - entry: entry]; + entry: entry + CRC32Offset: CRC32Offset + size64Offset: size64Offset]; objc_autoreleasePoolPop(pool); return [_lastReturnedStream autorelease]; } @@ -734,23 +806,23 @@ { self = [super init]; @try { _archive = [archive retain]; - _stream = [stream retain]; + _compressionMethod = entry.compressionMethod; - switch (entry.compressionMethod) { + switch (_compressionMethod) { case OFZIPArchiveEntryCompressionMethodNone: - _decompressedStream = [stream retain]; + _decompressedStream = [_archive->_stream retain]; break; case OFZIPArchiveEntryCompressionMethodDeflate: _decompressedStream = [[OFInflateStream alloc] - initWithStream: stream]; + initWithStream: _archive->_stream]; break; case OFZIPArchiveEntryCompressionMethodDeflate64: _decompressedStream = [[OFInflate64Stream alloc] - initWithStream: stream]; + initWithStream: _archive->_stream]; break; default: @throw [OFNotImplementedException exceptionWithSelector: _cmd object: nil]; @@ -767,11 +839,11 @@ return self; } - (void)dealloc { - if (_stream != nil || _decompressedStream != nil) + if (_decompressedStream != nil) [self close]; [_entry release]; if (_archive->_lastReturnedStream == self) @@ -782,28 +854,63 @@ [super dealloc]; } - (bool)lowlevelIsAtEndOfStream { - if (_stream == nil) + if (_decompressedStream == nil) @throw [OFNotOpenException exceptionWithObject: self]; return _atEndOfStream; } - (size_t)lowlevelReadIntoBuffer: (void *)buffer length: (size_t)length { size_t ret; - if (_stream == nil) + if (_decompressedStream == nil) @throw [OFNotOpenException exceptionWithObject: self]; if (_atEndOfStream) return 0; - if (_stream.atEndOfStream && !_decompressedStream.hasDataInReadBuffer) - @throw [OFTruncatedDataException exception]; + if ([_archive->_stream isAtEndOfStream] && + ![_decompressedStream hasDataInReadBuffer]) { + OFStream *oldStream = _archive->_stream, *oldDecompressedStream; + OFSeekableStream *stream; + + if (_archive->_diskNumber >= _archive->_lastDiskNumber) + @throw [OFTruncatedDataException exception]; + + stream = [_archive->_delegate + archive: _archive + wantsPartNumbered: _archive->_diskNumber + 1 + lastPartNumber: _archive->_lastDiskNumber]; + + if (stream == nil) + @throw [OFInvalidFormatException exception]; + + _archive->_diskNumber++; + _archive->_stream = [stream retain]; + [oldStream release]; + + switch (_compressionMethod) { + case OFZIPArchiveEntryCompressionMethodNone: + oldDecompressedStream = _decompressedStream; + _decompressedStream = [_archive->_stream retain]; + [oldDecompressedStream release]; + break; + case OFZIPArchiveEntryCompressionMethodDeflate: + case OFZIPArchiveEntryCompressionMethodDeflate64: + [_decompressedStream + setUnderlyingStream: _archive->_stream]; + break; + default: + @throw [OFNotImplementedException + exceptionWithSelector: _cmd + object: nil]; + } + } #if SIZE_MAX >= UINT64_MAX if (length > UINT64_MAX) @throw [OFOutOfRangeException exception]; #endif @@ -832,14 +939,13 @@ } return ret; } -- (bool)hasDataInReadBuffer +- (bool)lowlevelHasDataInReadBuffer { - return (super.hasDataInReadBuffer || - _decompressedStream.hasDataInReadBuffer); + return ((OFStream *)_decompressedStream).hasDataInReadBuffer; } - (int)fileDescriptorForReading { return ((id )_decompressedStream) @@ -846,16 +952,13 @@ .fileDescriptorForReading; } - (void)close { - if (_stream == nil || _decompressedStream == nil) + if (_decompressedStream == nil) @throw [OFNotOpenException exceptionWithObject: self]; - [_stream release]; - _stream = nil; - [_decompressedStream release]; _decompressedStream = nil; [super close]; } @@ -863,17 +966,21 @@ @implementation OFZIPArchiveFileWriteStream - (instancetype)of_initWithArchive: (OFZIPArchive *)archive stream: (OFStream *)stream entry: (OFMutableZIPArchiveEntry *)entry + CRC32Offset: (OFStreamOffset)CRC32Offset + size64Offset: (OFStreamOffset)size64Offset { self = [super init]; _archive = [archive retain]; _stream = [stream retain]; _entry = [entry retain]; _CRC32 = ~0; + _CRC32Offset = CRC32Offset; + _size64Offset = size64Offset; return self; } - (void)dealloc @@ -921,30 +1028,48 @@ return length; } - (void)close { + bool seekable; + if (_stream == nil) @throw [OFNotOpenException exceptionWithObject: self]; if (_bytesWritten > UINT64_MAX) @throw [OFOutOfRangeException exception]; - [_stream writeLittleEndianInt32: 0x08074B50]; - [_stream writeLittleEndianInt32: _CRC32]; - [_stream writeLittleEndianInt64: (uint64_t)_bytesWritten]; - [_stream writeLittleEndianInt64: (uint64_t)_bytesWritten]; + seekable = [_stream isKindOfClass: [OFSeekableStream class]]; + + if (seekable) { + OFStreamOffset offset = [_stream seekToOffset: 0 + whence: OFSeekCurrent]; + + [_stream seekToOffset: _CRC32Offset whence: OFSeekSet]; + [_stream writeLittleEndianInt32: ~_CRC32]; + [_stream seekToOffset: _size64Offset whence: OFSeekSet]; + [_stream writeLittleEndianInt64: (uint64_t)_bytesWritten]; + [_stream writeLittleEndianInt64: (uint64_t)_bytesWritten]; + + [_stream seekToOffset: offset whence: OFSeekSet]; + } else { + [_stream writeLittleEndianInt32: 0x08074B50]; + [_stream writeLittleEndianInt32: ~_CRC32]; + [_stream writeLittleEndianInt64: (uint64_t)_bytesWritten]; + [_stream writeLittleEndianInt64: (uint64_t)_bytesWritten]; + } [_stream release]; _stream = nil; _entry.CRC32 = ~_CRC32; _entry.compressedSize = _bytesWritten; _entry.uncompressedSize = _bytesWritten; [_entry makeImmutable]; - _bytesWritten += (2 * 4 + 2 * 8); + if (!seekable) + _bytesWritten += (2 * 4 + 2 * 8); [_archive->_entries addObject: _entry]; [_archive->_pathToEntryMap setObject: _entry forKey: _entry.fileName]; if (ULLONG_MAX - _archive->_offset < _bytesWritten)