Artifact c12f6e9a97a7b8b9e1788b7cc0b2fbf2105c4e30982e2213d2e95eb003b7eee6:
- File
src/OFZIPArchive.m
— part of check-in
[1de551cb5f]
at
2016-06-07 22:56:28
on branch trunk
— Add support for reusing OFStreams after close
Right now, this is only useful for OFTCPSocket, as this is the only
class so far not establishing the stream in the init method. However,
this adds the general infrastructure to allow reuse to all subclasses of
OFStream. (user: js, size: 13916) [annotate] [blame] [check-ins using]
/* * Copyright (c) 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 * Jonathan Schleifer <js@heap.zone> * * 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. */ #include "config.h" #include <errno.h> #import "OFZIPArchive.h" #import "OFZIPArchiveEntry.h" #import "OFZIPArchiveEntry+Private.h" #import "OFDataArray.h" #import "OFArray.h" #import "OFDictionary.h" #import "OFSeekableStream.h" #ifdef OF_HAVE_FILES # import "OFFile.h" #endif #import "OFDeflateStream.h" #import "OFDeflate64Stream.h" #import "crc32.h" #import "OFChecksumFailedException.h" #import "OFInvalidArgumentException.h" #import "OFInvalidFormatException.h" #import "OFNotImplementedException.h" #import "OFOpenItemFailedException.h" #import "OFOutOfRangeException.h" #import "OFReadFailedException.h" #import "OFSeekFailedException.h" #import "OFUnsupportedVersionException.h" /* * FIXME: Current limitations: * - Split archives are not supported. * - Write support is missing. * - Encrypted files cannot be read. */ @interface OFZIPArchive () - (void)OF_readZIPInfo; - (void)OF_readEntries; @end @interface OFZIPArchive_LocalFileHeader: OFObject { @public uint16_t _minVersionNeeded, _generalPurposeBitFlag, _compressionMethod; uint16_t _lastModifiedFileTime, _lastModifiedFileDate; uint32_t _CRC32; uint64_t _compressedSize, _uncompressedSize; OFString *_fileName; OFDataArray *_extraField; } - initWithStream: (OFStream*)stream; - (bool)matchesEntry: (OFZIPArchiveEntry*)entry; @end @interface OFZIPArchive_FileStream: OFStream { OFStream *_stream, *_decompressedStream; OFZIPArchive_LocalFileHeader *_localFileHeader; bool _hasDataDescriptor; uint64_t _size; uint32_t _CRC32; bool _atEndOfStream, _closed; } - initWithStream: (OFStream*)path localFileHeader: (OFZIPArchive_LocalFileHeader*)localFileHeader; @end uint32_t of_zip_archive_read_field32(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 of_zip_archive_read_field64(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; } static void seekOrThrowInvalidFormat(OFSeekableStream *stream, of_offset_t offset, int whence) { @try { [stream seekToOffset: offset whence: whence]; } @catch (OFSeekFailedException *e) { if ([e errNo] == EINVAL) @throw [OFInvalidFormatException exception]; @throw e; } } @implementation OFZIPArchive @synthesize archiveComment = _archiveComment; + (instancetype)archiveWithSeekableStream: (OFSeekableStream*)stream { return [[[self alloc] initWithSeekableStream: stream] autorelease]; } #ifdef OF_HAVE_FILES + (instancetype)archiveWithPath: (OFString*)path { return [[[self alloc] initWithPath: path] autorelease]; } #endif - init { OF_INVALID_INIT_METHOD } - initWithSeekableStream: (OFSeekableStream*)stream { self = [super init]; @try { _stream = [stream retain]; [self OF_readZIPInfo]; [self OF_readEntries]; } @catch (id e) { [self release]; @throw e; } return self; } #ifdef OF_HAVE_FILES - initWithPath: (OFString*)path { self = [super init]; @try { _stream = [[OFFile alloc] initWithPath: path mode: @"rb"]; [self OF_readZIPInfo]; [self OF_readEntries]; } @catch (id e) { [self release]; @throw e; } return self; } #endif - (void)dealloc { [_stream release]; [_archiveComment release]; [_entries release]; [_pathToEntryMap release]; [_lastReturnedStream release]; [super dealloc]; } - (void)OF_readZIPInfo { void *pool = objc_autoreleasePoolPush(); uint16_t commentLength; of_offset_t offset = -22; bool valid = false; do { seekOrThrowInvalidFormat(_stream, offset, SEEK_END); if ([_stream readLittleEndianInt32] == 0x06054B50) { valid = true; break; } } while (--offset >= -65557); if (!valid) @throw [OFInvalidFormatException exception]; _diskNumber = [_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: OF_STRING_ENCODING_CODEPAGE_437] copy]; if (_diskNumber == 0xFFFF || _centralDirectoryDisk == 0xFFFF || _centralDirectoryEntriesInDisk == 0xFFFF || _centralDirectoryEntries == 0xFFFF || _centralDirectorySize == 0xFFFFFFFF || _centralDirectoryOffset == -1) { int64_t offset64; uint64_t size; seekOrThrowInvalidFormat(_stream, offset - 20, SEEK_END); if ([_stream readLittleEndianInt32] != 0x07064B50) { objc_autoreleasePoolPop(pool); return; } /* * FIXME: Handle number of the disk containing ZIP64 end of * central directory record. */ [_stream readLittleEndianInt32]; offset64 = [_stream readLittleEndianInt64]; if (offset64 < 0 || (of_offset_t)offset64 != offset64) @throw [OFOutOfRangeException exception]; seekOrThrowInvalidFormat(_stream, (of_offset_t)offset64, SEEK_SET); 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]; _diskNumber = [_stream readLittleEndianInt32]; _centralDirectoryDisk = [_stream readLittleEndianInt32]; _centralDirectoryEntriesInDisk = [_stream readLittleEndianInt64]; _centralDirectoryEntries = [_stream readLittleEndianInt64]; _centralDirectorySize = [_stream readLittleEndianInt64]; _centralDirectoryOffset = [_stream readLittleEndianInt64]; if (_centralDirectoryOffset < 0 || (of_offset_t)_centralDirectoryOffset != _centralDirectoryOffset) @throw [OFOutOfRangeException exception]; } objc_autoreleasePoolPop(pool); } - (void)OF_readEntries { void *pool = objc_autoreleasePoolPush(); if (_centralDirectoryOffset < 0 || (of_offset_t)_centralDirectoryOffset != _centralDirectoryOffset) @throw [OFOutOfRangeException exception]; seekOrThrowInvalidFormat(_stream, (of_offset_t)_centralDirectoryOffset, SEEK_SET); _entries = [[OFMutableArray alloc] init]; _pathToEntryMap = [[OFMutableDictionary alloc] init]; for (size_t i = 0; i < _centralDirectoryEntries; i++) { OFZIPArchiveEntry *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]]; } [_entries makeImmutable]; [_pathToEntryMap makeImmutable]; objc_autoreleasePoolPop(pool); } - (OFArray*)entries { return [[_entries copy] autorelease]; } - (OFStream*)streamForReadingFile: (OFString*)path { void *pool = objc_autoreleasePoolPush(); OFZIPArchiveEntry *entry = [_pathToEntryMap objectForKey: path]; OFZIPArchive_LocalFileHeader *localFileHeader; int64_t offset64; if (entry == nil) @throw [OFOpenItemFailedException exceptionWithPath: path mode: @"rb" errNo: ENOENT]; [_lastReturnedStream close]; [_lastReturnedStream release]; _lastReturnedStream = nil; offset64 = [entry OF_localFileHeaderOffset]; if (offset64 < 0 || (of_offset_t)offset64 != offset64) @throw [OFOutOfRangeException exception]; seekOrThrowInvalidFormat(_stream, (of_offset_t)offset64, SEEK_SET); localFileHeader = [[[OFZIPArchive_LocalFileHeader 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]; } _lastReturnedStream = [[OFZIPArchive_FileStream alloc] initWithStream: _stream localFileHeader: localFileHeader]; objc_autoreleasePoolPop(pool); return [[_lastReturnedStream retain] autorelease]; } @end @implementation OFZIPArchive_LocalFileHeader - initWithStream: (OFStream*)stream { self = [super init]; @try { uint16_t fileNameLength, extraFieldLength; of_string_encoding_t encoding; uint8_t *ZIP64; 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 & (1 << 11) ? OF_STRING_ENCODING_UTF_8 : OF_STRING_ENCODING_CODEPAGE_437); _fileName = [[stream readStringWithLength: fileNameLength encoding: encoding] copy]; _extraField = [[stream readDataArrayWithCount: extraFieldLength] retain]; of_zip_archive_entry_extra_field_find(_extraField, OF_ZIP_ARCHIVE_ENTRY_EXTRA_FIELD_ZIP64, &ZIP64, &ZIP64Size); if (ZIP64 != NULL) { if (_uncompressedSize == 0xFFFFFFFF) _uncompressedSize = of_zip_archive_read_field64( &ZIP64, &ZIP64Size); if (_compressedSize == 0xFFFFFFFF) _compressedSize = of_zip_archive_read_field64( &ZIP64, &ZIP64Size); if (ZIP64Size > 0) @throw [OFInvalidFormatException exception]; } } @catch (id e) { [self release]; @throw e; } return self; } - (void)dealloc { [_fileName release]; [_extraField release]; [super dealloc]; } - (bool)matchesEntry: (OFZIPArchiveEntry*)entry { if (_minVersionNeeded != [entry minVersionNeeded] || _compressionMethod != [entry compressionMethod] || _lastModifiedFileTime != [entry OF_lastModifiedFileTime] || _lastModifiedFileDate != [entry OF_lastModifiedFileDate]) return false; if (!(_generalPurposeBitFlag & (1 << 3))) if (_CRC32 != [entry CRC32] || _compressedSize != [entry compressedSize] || _uncompressedSize != [entry uncompressedSize]) return false; if (![_fileName isEqual: [entry fileName]]) return false; return true; } @end @implementation OFZIPArchive_FileStream - initWithStream: (OFStream*)stream localFileHeader: (OFZIPArchive_LocalFileHeader*)localFileHeader { self = [super init]; @try { _stream = [stream retain]; switch (localFileHeader->_compressionMethod) { case OF_ZIP_ARCHIVE_ENTRY_COMPRESSION_METHOD_NONE: _decompressedStream = [stream retain]; break; case OF_ZIP_ARCHIVE_ENTRY_COMPRESSION_METHOD_DEFLATE: _decompressedStream = [[OFDeflateStream alloc] initWithStream: stream]; break; case OF_ZIP_ARCHIVE_ENTRY_COMPRESSION_METHOD_DEFLATE64: _decompressedStream = [[OFDeflate64Stream alloc] initWithStream: stream]; break; default: @throw [OFNotImplementedException exceptionWithSelector: _cmd object: self]; } _localFileHeader = [localFileHeader retain]; _hasDataDescriptor = (localFileHeader->_generalPurposeBitFlag & (1 << 3)); _size = localFileHeader->_uncompressedSize; _CRC32 = ~0; } @catch (id e) { [self release]; @throw e; } return self; } - (void)dealloc { [_stream release]; [_decompressedStream release]; [_localFileHeader release]; [super dealloc]; } - (bool)lowlevelIsAtEndOfStream { return _atEndOfStream; } - (size_t)lowlevelReadIntoBuffer: (void*)buffer length: (size_t)length { size_t min, ret; if (_atEndOfStream || _closed) @throw [OFReadFailedException exceptionWithObject: self requestedLength: length]; if (_hasDataDescriptor) { if ([_decompressedStream isAtEndOfStream]) { uint32_t CRC32; _atEndOfStream = true; CRC32 = [_stream readLittleEndianInt32]; if (CRC32 == 0x08074B50) CRC32 = [_stream readLittleEndianInt32]; if (~_CRC32 != CRC32) @throw [OFChecksumFailedException exception]; /* * FIXME: Check (un)compressed length! * (Note: Both are 64 bit if the entry uses ZIP64!) */ return 0; } ret = [_decompressedStream readIntoBuffer: buffer length: length]; } else { if (_size == 0) { _atEndOfStream = true; if (~_CRC32 != _localFileHeader->_CRC32) @throw [OFChecksumFailedException exception]; return 0; } min = (length < _size ? length : (size_t)_size); ret = [_decompressedStream readIntoBuffer: buffer length: min]; _size -= ret; } _CRC32 = of_crc32(_CRC32, buffer, ret); return ret; } - (void)close { _closed = true; [super close]; } @end