/*
* Copyright (c) 2008-2021 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.
*/
#include "config.h"
#import "OFGZIPStream.h"
#import "OFInflateStream.h"
#import "OFDate.h"
#import "crc32.h"
#import "OFChecksumMismatchException.h"
#import "OFInvalidFormatException.h"
#import "OFNotImplementedException.h"
#import "OFNotOpenException.h"
#import "OFTruncatedDataException.h"
@implementation OFGZIPStream
@synthesize operatingSystemMadeOn = _operatingSystemMadeOn;
@synthesize modificationDate = _modificationDate;
+ (instancetype)streamWithStream: (OFStream *)stream
mode: (OFString *)mode
{
return [[[self alloc] initWithStream: stream
mode: mode] autorelease];
}
- (instancetype)init
{
OF_INVALID_INIT_METHOD
}
- (instancetype)initWithStream: (OFStream *)stream
mode: (OFString *)mode
{
self = [super init];
@try {
if (![mode isEqual: @"r"])
@throw [OFNotImplementedException
exceptionWithSelector: _cmd
object: nil];
_stream = [stream retain];
_operatingSystemMadeOn =
OF_GZIP_STREAM_OPERATING_SYSTEM_UNKNOWN;
_CRC32 = ~0;
} @catch (id e) {
[self release];
@throw e;
}
return self;
}
- (void)dealloc
{
if (_stream != nil)
[self close];
[_inflateStream release];
[_modificationDate release];
[super dealloc];
}
- (size_t)lowlevelReadIntoBuffer: (void *)buffer
length: (size_t)length
{
if (_stream == nil)
@throw [OFNotOpenException exceptionWithObject: self];
for (;;) {
uint8_t byte;
uint32_t CRC32, uncompressedSize;
if (_stream.atEndOfStream) {
if (_state != OF_GZIP_STREAM_ID1)
@throw [OFTruncatedDataException exception];
return 0;
}
switch (_state) {
case OF_GZIP_STREAM_ID1:
case OF_GZIP_STREAM_ID2:
case OF_GZIP_STREAM_COMPRESSION_METHOD:
if ([_stream readIntoBuffer: &byte
length: 1] < 1)
return 0;
if ((_state == OF_GZIP_STREAM_ID1 && byte != 0x1F) ||
(_state == OF_GZIP_STREAM_ID2 && byte != 0x8B) ||
(_state == OF_GZIP_STREAM_COMPRESSION_METHOD &&
byte != 8))
@throw [OFInvalidFormatException exception];
_state++;
break;
case OF_GZIP_STREAM_FLAGS:
if ([_stream readIntoBuffer: &byte
length: 1] < 1)
return 0;
_flags = byte;
_state++;
break;
case OF_GZIP_STREAM_MODIFICATION_TIME:
_bytesRead += [_stream
readIntoBuffer: _buffer + _bytesRead
length: 4 - _bytesRead];
if (_bytesRead < 4)
return 0;
[_modificationDate release];
_modificationDate = nil;
_modificationDate = [[OFDate alloc]
initWithTimeIntervalSince1970:
(_buffer[3] << 24) | (_buffer[2] << 16) |
(_buffer[1] << 8) | _buffer[0]];
_bytesRead = 0;
_state++;
break;
case OF_GZIP_STREAM_EXTRA_FLAGS:
if ([_stream readIntoBuffer: &byte
length: 1] < 1)
return 0;
_extraFlags = byte;
_state++;
break;
case OF_GZIP_STREAM_OPERATING_SYSTEM:
if ([_stream readIntoBuffer: &byte
length: 1] < 1)
return 0;
_operatingSystemMadeOn = byte;
_state++;
break;
case OF_GZIP_STREAM_EXTRA_LENGTH:
if (!(_flags & OF_GZIP_STREAM_FLAG_EXTRA)) {
_state += 2;
break;
}
_bytesRead += [_stream
readIntoBuffer: _buffer + _bytesRead
length: 2 - _bytesRead];
if (_bytesRead < 2)
return 0;
_extraLength = (_buffer[1] << 8) | _buffer[0];
_bytesRead = 0;
_state++;
break;
case OF_GZIP_STREAM_EXTRA:
{
char tmp[512];
size_t toRead = _extraLength - _bytesRead;
if (toRead > 512)
toRead = 512;
_bytesRead += [_stream readIntoBuffer: tmp
length: toRead];
}
if (_bytesRead < _extraLength)
return 0;
_bytesRead = 0;
_state++;
break;
case OF_GZIP_STREAM_NAME:
if (!(_flags & OF_GZIP_STREAM_FLAG_NAME)) {
_state++;
break;
}
do {
if ([_stream readIntoBuffer: &byte
length: 1] < 1)
return 0;
} while (byte != 0);
_state++;
break;
case OF_GZIP_STREAM_COMMENT:
if (!(_flags & OF_GZIP_STREAM_FLAG_COMMENT)) {
_state++;
break;
}
do {
if ([_stream readIntoBuffer: &byte
length: 1] < 1)
return 0;
} while (byte != 0);
_state++;
break;
case OF_GZIP_STREAM_HEADER_CRC16:
if (!(_flags & OF_GZIP_STREAM_FLAG_HEADER_CRC16)) {
_state++;
break;
}
_bytesRead += [_stream
readIntoBuffer: _buffer + _bytesRead
length: 2 - _bytesRead];
if (_bytesRead < 2)
return 0;
/*
* Header CRC16 is not checked, as I could not find a
* single file in the wild that actually has a header
* CRC16 - and thus no file to test against.
*/
_bytesRead = 0;
_state++;
break;
case OF_GZIP_STREAM_DATA:
if (_inflateStream == nil)
_inflateStream = [[OFInflateStream alloc]
initWithStream: _stream];
if (!_inflateStream.atEndOfStream) {
size_t bytesRead = [_inflateStream
readIntoBuffer: buffer
length: length];
_CRC32 = of_crc32(_CRC32, buffer, bytesRead);
_uncompressedSize += bytesRead;
return bytesRead;
}
[_inflateStream release];
_inflateStream = nil;
_state++;
break;
case OF_GZIP_STREAM_CRC32:
_bytesRead += [_stream readIntoBuffer: _buffer
length: 4 - _bytesRead];
if (_bytesRead < 4)
return 0;
CRC32 = ((uint32_t)_buffer[3] << 24) |
(_buffer[2] << 16) | (_buffer[1] << 8) | _buffer[0];
if (~_CRC32 != CRC32) {
OFString *actual = [OFString stringWithFormat:
@"%08" PRIX32, ~_CRC32];
OFString *expected = [OFString stringWithFormat:
@"%08" PRIX32, CRC32];
@throw [OFChecksumMismatchException
exceptionWithActualChecksum: actual
expectedChecksum: expected];
}
_bytesRead = 0;
_CRC32 = ~0;
_state++;
break;
case OF_GZIP_STREAM_UNCOMPRESSED_SIZE:
_bytesRead += [_stream readIntoBuffer: _buffer
length: 4 - _bytesRead];
uncompressedSize = ((uint32_t)_buffer[3] << 24) |
(_buffer[2] << 16) | (_buffer[1] << 8) | _buffer[0];
if (_uncompressedSize != uncompressedSize) {
OFString *actual = [OFString stringWithFormat:
@"%" PRIu32, _uncompressedSize];
OFString *expected = [OFString stringWithFormat:
@"%" PRIu32, uncompressedSize];
@throw [OFChecksumMismatchException
exceptionWithActualChecksum: actual
expectedChecksum: expected];
}
_bytesRead = 0;
_uncompressedSize = 0;
_state = OF_GZIP_STREAM_ID1;
break;
}
}
}
- (bool)lowlevelIsAtEndOfStream
{
if (_stream == nil)
@throw [OFNotOpenException exceptionWithObject: self];
return _stream.atEndOfStream;
}
- (bool)hasDataInReadBuffer
{
if (_state == OF_GZIP_STREAM_DATA)
return (super.hasDataInReadBuffer ||
_inflateStream.hasDataInReadBuffer);
return (super.hasDataInReadBuffer || _stream.hasDataInReadBuffer);
}
- (void)close
{
if (_stream == nil)
@throw [OFNotOpenException exceptionWithObject: self];
[_stream release];
_stream = nil;
[super close];
}
@end