/* * Copyright (c) 2008-2023 Jonathan Schleifer <js@nil.im> * * 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 * the packaging of this file. * * Alternatively, it may be distributed under the terms of the GNU General * Public License, either version 2 or 3, which can be found in the file * LICENSE.GPLv2 or LICENSE.GPLv3 respectively included in the packaging of this * file. */ #define OF_ZIP_ARCHIVE_M #include "config.h" #include <errno.h> #import "OFZIPArchive.h" #import "OFZIPArchiveEntry.h" #import "OFZIPArchiveEntry+Private.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 "OFChecksumMismatchException.h" #import "OFInvalidArgumentException.h" #import "OFInvalidFormatException.h" #import "OFNotImplementedException.h" #import "OFNotOpenException.h" #import "OFOpenItemFailedException.h" #import "OFOutOfRangeException.h" #import "OFSeekFailedException.h" #import "OFTruncatedDataException.h" #import "OFUnsupportedVersionException.h" #import "OFWriteFailedException.h" /* * TODO: Current limitations: * - Encrypted files cannot be read. */ enum { modeRead, modeWrite, modeAppend }; OF_DIRECT_MEMBERS @interface OFZIPArchive () - (void)of_readZIPInfo; - (void)of_readEntries; - (void)of_writeCentralDirectory; @end OF_DIRECT_MEMBERS @interface OFZIPArchiveLocalFileHeader: OFObject { @public uint16_t _minVersionNeeded, _generalPurposeBitFlag, _compressionMethod; uint16_t _lastModifiedFileTime, _lastModifiedFileDate; uint32_t _CRC32; uint64_t _compressedSize, _uncompressedSize; OFString *_fileName; OFData *_extraField; } - (instancetype)initWithStream: (OFStream *)stream; - (bool)matchesEntry: (OFZIPArchiveEntry *)entry; @end OF_DIRECT_MEMBERS @interface OFZIPArchiveFileReadStream: OFStream { OFZIPArchive *_archive; OFZIPArchiveEntryCompressionMethod _compressionMethod; OF_KINDOF(OFStream *) _decompressedStream; OFZIPArchiveEntry *_entry; unsigned long long _toRead; uint32_t _CRC32; bool _atEndOfStream; } - (instancetype)of_initWithArchive: (OFZIPArchive *)archive stream: (OFStream *)stream entry: (OFZIPArchiveEntry *)entry; @end OF_DIRECT_MEMBERS @interface OFZIPArchiveFileWriteStream: OFStream { OFZIPArchive *_archive; 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 CRC32Offset: (OFStreamOffset)CRC32Offset size64Offset: (OFStreamOffset)size64Offset; @end uint32_t OFZIPArchiveReadField32(const uint8_t **data, uint16_t *size) { uint32_t field = 0; if (*size < 4) @throw [OFInvalidFormatException exception]; for (uint8_t i = 0; i < 4; i++) field |= (uint32_t)(*data)[i] << (i * 8); *data += 4; *size -= 4; return field; } uint64_t OFZIPArchiveReadField64(const uint8_t **data, uint16_t *size) { uint64_t field = 0; if (*size < 8) @throw [OFInvalidFormatException exception]; for (uint8_t i = 0; i < 8; i++) field |= (uint64_t)(*data)[i] << (i * 8); *data += 8; *size -= 8; return field; } @implementation OFZIPArchive @synthesize delegate = _delegate, archiveComment = _archiveComment; 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; 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 { [archive->_stream seekToOffset: offset whence: whence]; } @catch (OFSeekFailedException *e) { if (e.errNo == EINVAL) @throw [OFInvalidFormatException exception]; @throw e; } } + (instancetype)archiveWithStream: (OFStream *)stream mode: (OFString *)mode { return [[[self alloc] initWithStream: stream mode: mode] autorelease]; } + (instancetype)archiveWithIRI: (OFIRI *)IRI mode: (OFString *)mode { return [[[self alloc] initWithIRI: IRI mode: mode] autorelease]; } + (OFIRI *)IRIForFilePath: (OFString *)path inArchiveWithIRI: (OFIRI *)IRI { return OFArchiveIRIHandlerIRIForFileInArchive(@"zip", path, IRI); } - (instancetype)init { OF_INVALID_INIT_METHOD } - (instancetype)initWithStream: (OFStream *)stream mode: (OFString *)mode { self = [super init]; @try { if ([mode isEqual: @"r"]) _mode = modeRead; else if ([mode isEqual: @"w"]) _mode = modeWrite; else if ([mode isEqual: @"a"]) _mode = modeAppend; else @throw [OFInvalidArgumentException exception]; _stream = [stream retain]; _entries = [[OFMutableArray alloc] init]; _pathToEntryMap = [[OFMutableDictionary alloc] init]; if (_mode == modeRead || _mode == modeAppend) { if (![stream isKindOfClass: [OFSeekableStream class]]) @throw [OFInvalidArgumentException exception]; [self of_readZIPInfo]; [self of_readEntries]; } if (_mode == modeAppend) { _offset = _centralDirectoryOffset; seekOrThrowInvalidFormat(self, NULL, (OFStreamOffset)_offset, OFSeekSet); } } @catch (id e) { /* * If we are in write or append mode, we do not want -[close] * to write anything to it on error - after all, it might not * be a ZIP file which we would destroy otherwise. */ [_stream release]; _stream = nil; [self release]; @throw e; } return self; } - (instancetype)initWithIRI: (OFIRI *)IRI mode: (OFString *)mode { void *pool = objc_autoreleasePoolPush(); OFStream *stream; @try { if ([mode isEqual: @"a"]) stream = [OFIRIHandler openItemAtIRI: IRI mode: @"r+"]; else stream = [OFIRIHandler openItemAtIRI: IRI mode: mode]; } @catch (id e) { [self release]; @throw e; } self = [self initWithStream: stream mode: mode]; objc_autoreleasePoolPop(pool); return self; } - (void)dealloc { if (_stream != nil) [self close]; [_stream release]; [_archiveComment release]; [_entries release]; [_pathToEntryMap release]; [super dealloc]; } - (void)of_readZIPInfo { void *pool = objc_autoreleasePoolPush(); uint16_t commentLength; OFStreamOffset offset = -22; bool valid = false; do { seekOrThrowInvalidFormat(self, NULL, offset, OFSeekEnd); if ([_stream readLittleEndianInt32] == 0x06054B50) { valid = true; break; } } while (--offset >= -65557); if (!valid) @throw [OFInvalidFormatException exception]; _diskNumber = _lastDiskNumber = [_stream readLittleEndianInt16]; _centralDirectoryDisk = [_stream readLittleEndianInt16]; _centralDirectoryEntriesInDisk = [_stream readLittleEndianInt16]; _centralDirectoryEntries = [_stream readLittleEndianInt16]; _centralDirectorySize = [_stream readLittleEndianInt32]; _centralDirectoryOffset = [_stream readLittleEndianInt32]; commentLength = [_stream readLittleEndianInt16]; _archiveComment = [[_stream readStringWithLength: commentLength encoding: OFStringEncodingCodepage437] copy]; if (_lastDiskNumber == 0xFFFF || _centralDirectoryDisk == 0xFFFF || _centralDirectoryEntriesInDisk == 0xFFFF || _centralDirectoryEntries == 0xFFFF || _centralDirectorySize == 0xFFFFFFFF || _centralDirectoryOffset == 0xFFFFFFFF) { uint32_t diskNumber; int64_t offset64; uint64_t size; seekOrThrowInvalidFormat(self, NULL, offset - 20, OFSeekEnd); if ([_stream readLittleEndianInt32] != 0x07064B50) { objc_autoreleasePoolPop(pool); return; } /* * FIXME: Handle number of the disk containing ZIP64 end of * central directory record. */ 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(self, &diskNumber, (OFStreamOffset)offset64, OFSeekSet); if ([_stream readLittleEndianInt32] != 0x06064B50) @throw [OFInvalidFormatException exception]; size = [_stream readLittleEndianInt64]; if (size < 44) @throw [OFInvalidFormatException exception]; /* version made by */ [_stream readLittleEndianInt16]; /* version needed to extract */ [_stream readLittleEndianInt16]; if ([_stream readLittleEndianInt32] != _diskNumber) @throw [OFInvalidFormatException exception]; _centralDirectoryDisk = [_stream readLittleEndianInt32]; _centralDirectoryEntriesInDisk = [_stream readLittleEndianInt64]; _centralDirectoryEntries = [_stream readLittleEndianInt64]; _centralDirectorySize = [_stream readLittleEndianInt64]; _centralDirectoryOffset = [_stream readLittleEndianInt64]; if (_centralDirectoryOffset < 0 || (OFStreamOffset)_centralDirectoryOffset != _centralDirectoryOffset) @throw [OFOutOfRangeException exception]; } objc_autoreleasePoolPop(pool); } - (void)of_readEntries { void *pool = objc_autoreleasePoolPush(); if (_centralDirectoryOffset < 0 || (OFStreamOffset)_centralDirectoryOffset != _centralDirectoryOffset) @throw [OFOutOfRangeException exception]; seekOrThrowInvalidFormat(self, &_centralDirectoryDisk, (OFStreamOffset)_centralDirectoryOffset, OFSeekSet); for (size_t i = 0; i < _centralDirectoryEntries; i++) { 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]; [_entries addObject: entry]; [_pathToEntryMap setObject: entry forKey: entry.fileName]; } objc_autoreleasePoolPop(pool); } - (OFArray *)entries { return [[_entries copy] autorelease]; } - (OFString *)archiveComment { return _archiveComment; } - (void)setArchiveComment: (OFString *)comment { void *pool = objc_autoreleasePoolPush(); OFString *old; if (comment.UTF8StringLength > UINT16_MAX) @throw [OFOutOfRangeException exception]; old = _archiveComment; _archiveComment = [comment copy]; [old release]; objc_autoreleasePoolPop(pool); } - (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]; if (_mode != modeRead) @throw [OFInvalidArgumentException exception]; if ((entry = [_pathToEntryMap objectForKey: path]) == nil) @throw [OFOpenItemFailedException exceptionWithPath: path mode: @"r" errNo: ENOENT]; @try { [_lastReturnedStream close]; } @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(self, &startDiskNumber, (OFStreamOffset)offset64, OFSeekSet); localFileHeader = [[[OFZIPArchiveLocalFileHeader alloc] initWithStream: _stream] autorelease]; if (![localFileHeader matchesEntry: entry]) @throw [OFInvalidFormatException exception]; if ((localFileHeader->_minVersionNeeded & 0xFF) > 45) { OFString *version = [OFString stringWithFormat: @"%u.%u", (localFileHeader->_minVersionNeeded & 0xFF) / 10, (localFileHeader->_minVersionNeeded & 0xFF) % 10]; @throw [OFUnsupportedVersionException exceptionWithVersion: version]; } objc_autoreleasePoolPop(pool); _lastReturnedStream = [[[OFZIPArchiveFileReadStream alloc] of_initWithArchive: self stream: _stream entry: entry] autorelease]; return _lastReturnedStream; } - (OFStream *)streamForWritingEntry: (OFZIPArchiveEntry *)entry_ { 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]; if (_mode != modeWrite && _mode != modeAppend) @throw [OFInvalidArgumentException exception]; pool = objc_autoreleasePoolPush(); entry = [[entry_ mutableCopy] autorelease]; if ([_pathToEntryMap objectForKey: entry.fileName] != nil) @throw [OFOpenItemFailedException exceptionWithPath: entry.fileName mode: @"w" errNo: EEXIST]; if (entry.compressionMethod != OFZIPArchiveEntryCompressionMethodNone) @throw [OFNotImplementedException exceptionWithSelector: _cmd object: self]; @try { [_lastReturnedStream close]; } @catch (OFNotOpenException *e) { /* Might have already been closed by the user - that's fine. */ } _lastReturnedStream = nil; fileName = entry.fileName; fileNameLength = fileName.UTF8StringLength; extraField = entry.extraField; 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 |= (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]; /* 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]; [_stream writeLittleEndianInt16: extraFieldLength + 20]; offsetAdd += 4 + (5 * 2) + (3 * 4) + (2 * 2); [_stream writeString: fileName]; offsetAdd += fileNameLength; [_stream writeLittleEndianInt16: OFZIPArchiveEntryExtraFieldTagZIP64]; [_stream writeLittleEndianInt16: 16]; /* 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) [_stream writeData: extraField]; offsetAdd += extraFieldLength; if (INT64_MAX - _offset < offsetAdd) @throw [OFOutOfRangeException exception]; _offset += offsetAdd; _lastReturnedStream = [[OFZIPArchiveFileWriteStream alloc] of_initWithArchive: self stream: _stream entry: entry CRC32Offset: CRC32Offset size64Offset: size64Offset]; objc_autoreleasePoolPop(pool); return [_lastReturnedStream autorelease]; } - (void)of_writeCentralDirectory { void *pool = objc_autoreleasePoolPush(); _centralDirectoryEntries = 0; _centralDirectoryEntriesInDisk = 0; _centralDirectorySize = 0; _centralDirectoryOffset = _offset; for (OFZIPArchiveEntry *entry in _entries) { _centralDirectorySize += [entry of_writeToStream: _stream]; _centralDirectoryEntries++; _centralDirectoryEntriesInDisk++; } /* ZIP64 end of central directory */ [_stream writeLittleEndianInt32: 0x06064B50]; [_stream writeLittleEndianInt64: 44]; /* Remaining size */ [_stream writeLittleEndianInt16: 45]; /* Version made by */ [_stream writeLittleEndianInt16: 45]; /* Version required */ [_stream writeLittleEndianInt32: _diskNumber]; [_stream writeLittleEndianInt32: _centralDirectoryDisk]; [_stream writeLittleEndianInt64: _centralDirectoryEntriesInDisk]; [_stream writeLittleEndianInt64: _centralDirectoryEntries]; [_stream writeLittleEndianInt64: _centralDirectorySize]; [_stream writeLittleEndianInt64: _centralDirectoryOffset]; /* ZIP64 end of central directory locator */ [_stream writeLittleEndianInt32: 0x07064B50]; [_stream writeLittleEndianInt32: _diskNumber]; [_stream writeLittleEndianInt64: _centralDirectoryOffset + _centralDirectorySize]; [_stream writeLittleEndianInt32: 1]; /* Total number of disks */ /* End of central directory */ [_stream writeLittleEndianInt32: 0x06054B50]; [_stream writeLittleEndianInt16: 0xFFFF]; /* Disk number */ [_stream writeLittleEndianInt16: 0xFFFF]; /* CD disk */ [_stream writeLittleEndianInt16: 0xFFFF]; /* CD entries in disk */ [_stream writeLittleEndianInt16: 0xFFFF]; /* CD entries */ [_stream writeLittleEndianInt32: 0xFFFFFFFF]; /* CD size */ [_stream writeLittleEndianInt32: 0xFFFFFFFF]; /* CD offset */ [_stream writeLittleEndianInt16: _archiveComment.UTF8StringLength]; if (_archiveComment != nil) [_stream writeString: _archiveComment]; objc_autoreleasePoolPop(pool); } - (void)close { if (_stream == nil) @throw [OFNotOpenException exceptionWithObject: self]; @try { [_lastReturnedStream close]; } @catch (OFNotOpenException *e) { /* Might have already been closed by the user - that's fine. */ } _lastReturnedStream = nil; if (_mode == modeWrite || _mode == modeAppend) [self of_writeCentralDirectory]; [_stream release]; _stream = nil; } @end @implementation OFZIPArchiveLocalFileHeader - (instancetype)initWithStream: (OFStream *)stream { self = [super init]; @try { void *pool = objc_autoreleasePoolPush(); OFMutableData *extraField = nil; uint16_t fileNameLength, extraFieldLength; OFStringEncoding encoding; size_t ZIP64Index; uint16_t ZIP64Size; if ([stream readLittleEndianInt32] != 0x04034B50) @throw [OFInvalidFormatException exception]; _minVersionNeeded = [stream readLittleEndianInt16]; _generalPurposeBitFlag = [stream readLittleEndianInt16]; _compressionMethod = [stream readLittleEndianInt16]; _lastModifiedFileTime = [stream readLittleEndianInt16]; _lastModifiedFileDate = [stream readLittleEndianInt16]; _CRC32 = [stream readLittleEndianInt32]; _compressedSize = [stream readLittleEndianInt32]; _uncompressedSize = [stream readLittleEndianInt32]; fileNameLength = [stream readLittleEndianInt16]; extraFieldLength = [stream readLittleEndianInt16]; encoding = (_generalPurposeBitFlag & (1u << 11) ? OFStringEncodingUTF8 : OFStringEncodingCodepage437); _fileName = [[stream readStringWithLength: fileNameLength encoding: encoding] copy]; if (extraFieldLength > 0) extraField = [[[stream readDataWithCount: extraFieldLength] mutableCopy] autorelease]; ZIP64Index = OFZIPArchiveEntryExtraFieldFind(extraField, OFZIPArchiveEntryExtraFieldTagZIP64, &ZIP64Size); if (ZIP64Index != OFNotFound) { const uint8_t *ZIP64 = [extraField itemAtIndex: ZIP64Index]; OFRange range = OFMakeRange(ZIP64Index - 4, ZIP64Size + 4); if (_uncompressedSize == 0xFFFFFFFF) _uncompressedSize = OFZIPArchiveReadField64( &ZIP64, &ZIP64Size); if (_compressedSize == 0xFFFFFFFF) _compressedSize = OFZIPArchiveReadField64( &ZIP64, &ZIP64Size); if (ZIP64Size > 0) @throw [OFInvalidFormatException exception]; [extraField removeItemsInRange: range]; } if (extraField.count > 0) { [extraField makeImmutable]; _extraField = [extraField copy]; } objc_autoreleasePoolPop(pool); } @catch (id e) { [self release]; @throw e; } return self; } - (void)dealloc { [_fileName release]; [_extraField release]; [super dealloc]; } - (bool)matchesEntry: (OFZIPArchiveEntry *)entry { if (_compressionMethod != entry.compressionMethod || _lastModifiedFileTime != entry.of_lastModifiedFileTime || _lastModifiedFileDate != entry.of_lastModifiedFileDate) return false; if (!(_generalPurposeBitFlag & (1u << 3))) if (_CRC32 != entry.CRC32 || _compressedSize != entry.compressedSize || _uncompressedSize != entry.uncompressedSize) return false; if (![_fileName isEqual: entry.fileName]) return false; return true; } @end @implementation OFZIPArchiveFileReadStream - (instancetype)of_initWithArchive: (OFZIPArchive *)archive stream: (OFStream *)stream entry: (OFZIPArchiveEntry *)entry { self = [super init]; @try { _archive = [archive retain]; _compressionMethod = entry.compressionMethod; switch (_compressionMethod) { case OFZIPArchiveEntryCompressionMethodNone: _decompressedStream = [_archive->_stream retain]; break; case OFZIPArchiveEntryCompressionMethodDeflate: _decompressedStream = [[OFInflateStream alloc] initWithStream: _archive->_stream]; break; case OFZIPArchiveEntryCompressionMethodDeflate64: _decompressedStream = [[OFInflate64Stream alloc] initWithStream: _archive->_stream]; break; default: @throw [OFNotImplementedException exceptionWithSelector: _cmd object: nil]; } _entry = [entry copy]; _toRead = entry.uncompressedSize; _CRC32 = ~0; } @catch (id e) { [self release]; @throw e; } return self; } - (void)dealloc { if (_decompressedStream != nil) [self close]; [_entry release]; if (_archive->_lastReturnedStream == self) _archive->_lastReturnedStream = nil; [_archive release]; [super dealloc]; } - (bool)lowlevelIsAtEndOfStream { if (_decompressedStream == nil) @throw [OFNotOpenException exceptionWithObject: self]; return _atEndOfStream; } - (size_t)lowlevelReadIntoBuffer: (void *)buffer length: (size_t)length { size_t ret; if (_decompressedStream == nil) @throw [OFNotOpenException exceptionWithObject: self]; if (_atEndOfStream) return 0; 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 if (length > _toRead) length = (size_t)_toRead; ret = [_decompressedStream readIntoBuffer: buffer length: length]; _toRead -= ret; _CRC32 = OFCRC32(_CRC32, buffer, ret); if (_toRead == 0) { _atEndOfStream = true; if (~_CRC32 != _entry.CRC32) { OFString *actualChecksum = [OFString stringWithFormat: @"%08" PRIX32, ~_CRC32]; OFString *expectedChecksum = [OFString stringWithFormat: @"%08" PRIX32, _entry.CRC32]; @throw [OFChecksumMismatchException exceptionWithActualChecksum: actualChecksum expectedChecksum: expectedChecksum]; } } return ret; } - (bool)lowlevelHasDataInReadBuffer { return ((OFStream *)_decompressedStream).hasDataInReadBuffer; } - (int)fileDescriptorForReading { return ((id <OFReadyForReadingObserving>)_decompressedStream) .fileDescriptorForReading; } - (void)close { if (_decompressedStream == nil) @throw [OFNotOpenException exceptionWithObject: self]; [_decompressedStream release]; _decompressedStream = nil; [super close]; } @end @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 { if (_stream != nil) [self close]; [_entry release]; if (_archive->_lastReturnedStream == self) _archive->_lastReturnedStream = nil; [_archive release]; [super dealloc]; } - (size_t)lowlevelWriteBuffer: (const void *)buffer length: (size_t)length { #if SIZE_MAX >= INT64_MAX if (length > INT64_MAX) @throw [OFOutOfRangeException exception]; #endif if (ULLONG_MAX - _bytesWritten < length) @throw [OFOutOfRangeException exception]; @try { [_stream writeBuffer: buffer length: length]; } @catch (OFWriteFailedException *e) { OFEnsure(e.bytesWritten <= length); _bytesWritten += (unsigned long long)e.bytesWritten; _CRC32 = OFCRC32(_CRC32, buffer, e.bytesWritten); if (e.errNo == EWOULDBLOCK || e.errNo == EAGAIN) return e.bytesWritten; @throw e; } _bytesWritten += (unsigned long long)length; _CRC32 = OFCRC32(_CRC32, buffer, length); return length; } - (void)close { bool seekable; if (_stream == nil) @throw [OFNotOpenException exceptionWithObject: self]; if (_bytesWritten > UINT64_MAX) @throw [OFOutOfRangeException exception]; 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]; if (!seekable) _bytesWritten += (2 * 4 + 2 * 8); [_archive->_entries addObject: _entry]; [_archive->_pathToEntryMap setObject: _entry forKey: _entry.fileName]; if (ULLONG_MAX - _archive->_offset < _bytesWritten) @throw [OFOutOfRangeException exception]; _archive->_offset += _bytesWritten; [super close]; } @end