/*
 * Copyright (c) 2008-2026 Jonathan Schleifer <js@nil.im>
 *
 * All rights reserved.
 *
 * This program is free software: you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License version 3.0 only,
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
 * version 3.0 for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * version 3.0 along with this program. If not, see
 * <https://www.gnu.org/licenses/>.
 */

#define OF_ZIP_ARCHIVE_M

#include "config.h"

#include <errno.h>

#import "OFZIPArchive.h"
#import "OFZIPArchive+Private.h"
#import "OFArchiveIRIHandler.h"
#import "OFArray.h"
#import "OFCRC32.h"
#import "OFData.h"
#import "OFDeflate64Stream.h"
#import "OFDeflateStream.h"
#import "OFDictionary.h"
#import "OFIRI.h"
#import "OFIRIHandler.h"
#import "OFSeekableStream.h"
#import "OFStream.h"
#import "OFZIPArchiveEntry.h"
#import "OFZIPArchiveEntry+Private.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 = objc_retain(stream);
		objc_release(oldStream);
	}

	@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 objc_autoreleaseReturnValue([[self alloc] initWithStream: stream
								   mode: mode]);
}

+ (instancetype)archiveWithIRI: (OFIRI *)IRI mode: (OFString *)mode
{
	return objc_autoreleaseReturnValue([[self alloc] initWithIRI: IRI
								mode: mode]);
}

+ (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 = objc_retain(stream);
		_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.
		 */
		objc_release(_stream);
		_stream = nil;

		objc_release(self);
		@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) {
		objc_release(self);
		@throw e;
	}

	self = [self initWithStream: stream mode: mode];

	objc_autoreleasePoolPop(pool);

	return self;
}

- (void)dealloc
{
	if (_stream != nil)
		[self close];

	objc_release(_stream);
	objc_release(_archiveComment);
	objc_release(_entries);
	objc_release(_pathToEntryMap);

	[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, numDisks;
		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];
		numDisks = [_stream readLittleEndianInt32];

		if (numDisks == 0)
			numDisks = 1;

		_diskNumber = _lastDiskNumber = numDisks - 1;

		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];

		diskNumber = [_stream readLittleEndianInt32];
		if (diskNumber != _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 = objc_retain(stream);
			objc_release(oldStream);
		}

		entry = objc_autorelease(
		    [[OFZIPArchiveEntry alloc] of_initWithStream: _stream]);

		if ([_pathToEntryMap objectForKey: entry.fileName] != nil)
			@throw [OFInvalidFormatException exception];

		[_entries addObject: entry];
		[_pathToEntryMap setObject: entry forKey: entry.fileName];
	}

	objc_autoreleasePoolPop(pool);
}

- (OFArray *)entries
{
	return objc_autoreleaseReturnValue([_entries copy]);
}

- (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];
	objc_release(old);

	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 = objc_autorelease(
	    [[OFZIPArchiveLocalFileHeader alloc] initWithStream: _stream]);

	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];

	return objc_autoreleaseReturnValue(_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 = objc_autorelease([entry_ mutableCopy]);

	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_startDiskNumber = _diskNumber;
	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 encoding: OFStringEncodingUTF8];
	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 objc_autoreleaseReturnValue(_lastReturnedStream);
}

- (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: 0];	/* 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
	     cStringLengthWithEncoding: OFStringEncodingCodepage437]];
	if (_archiveComment != nil)
		[_stream writeString: _archiveComment
			    encoding: OFStringEncodingCodepage437];

	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];

	objc_release(_stream);
	_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 = objc_autorelease([[stream
			    readDataWithCount: extraFieldLength] mutableCopy]);

		ZIP64Index = OFZIPArchiveEntryExtraFieldFind(extraField,
		    OFZIPArchiveEntryExtraFieldTagZIP64, &ZIP64Size);

		if (ZIP64Index != OFNotFound) {
			const uint8_t *ZIP64 =
			    [extraField itemAtIndex: ZIP64Index];
			OFRange range =
			    OFMakeRange(ZIP64Index - 4, ZIP64Size + 4);

			_uncompressedSize = _OFZIPArchiveReadField64(
			    &ZIP64, &ZIP64Size);
			_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) {
		objc_release(self);
		@throw e;
	}

	return self;
}

- (void)dealloc
{
	objc_release(_fileName);
	objc_release(_extraField);

	[super dealloc];
}

- (bool)matchesEntry: (OFZIPArchiveEntry *)entry
{
	if (_compressionMethod != entry.compressionMethod)
		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 = objc_retain(archive);
		_compressionMethod = entry.compressionMethod;

		switch (_compressionMethod) {
		case OFZIPArchiveEntryCompressionMethodNone:
			_decompressedStream = objc_retain(_archive->_stream);
			break;
		case OFZIPArchiveEntryCompressionMethodDeflate:
			_decompressedStream = [[OFDeflateStream alloc]
			    initWithStream: _archive->_stream
				      mode: @"r"];
			break;
		case OFZIPArchiveEntryCompressionMethodDeflate64:
			_decompressedStream = [[OFDeflate64Stream alloc]
			    initWithStream: _archive->_stream
				      mode: @"r"];
			break;
		default:
			@throw [OFNotImplementedException
			    exceptionWithSelector: _cmd
					   object: nil];
		}

		_entry = [entry copy];
		_toRead = entry.uncompressedSize;
		_CRC32 = ~0;
	} @catch (id e) {
		objc_release(self);
		@throw e;
	}

	return self;
}

- (void)dealloc
{
	if (_decompressedStream != nil)
		[self close];

	objc_release(_entry);

	if (_archive->_lastReturnedStream == self)
		_archive->_lastReturnedStream = nil;

	objc_release(_archive);

	[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 = objc_retain(stream);
		objc_release(oldStream);

		switch (_compressionMethod) {
		case OFZIPArchiveEntryCompressionMethodNone:
			oldDecompressedStream = _decompressedStream;
			_decompressedStream = objc_retain(_archive->_stream);
			objc_release(oldDecompressedStream);
			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];

	objc_release(_decompressedStream);
	_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 = objc_retain(archive);
	_stream = objc_retain(stream);
	_entry = objc_retain(entry);
	_CRC32 = ~0;
	_CRC32Offset = CRC32Offset;
	_size64Offset = size64Offset;

	return self;
}

- (void)dealloc
{
	if (_stream != nil)
		[self close];

	objc_release(_entry);

	if (_archive->_lastReturnedStream == self)
		_archive->_lastReturnedStream = nil;

	objc_release(_archive);

	[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];
	}

	objc_release(_stream);
	_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
