/* * Copyright (c) 2008-2024 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_ZOO_ARCHIVE_M #include "config.h" #include <errno.h> #import "OFZooArchive.h" #import "OFZooArchiveEntry.h" #import "OFZooArchiveEntry+Private.h" #import "OFArchiveIRIHandler.h" #import "OFCRC16.h" #import "OFIRI.h" #import "OFIRIHandler.h" #import "OFKernelEventObserver.h" #import "OFLHADecompressingStream.h" #import "OFSeekableStream.h" #import "OFStream.h" #import "OFString.h" #import "OFChecksumMismatchException.h" #import "OFInvalidArgumentException.h" #import "OFInvalidFormatException.h" #import "OFNotImplementedException.h" #import "OFNotOpenException.h" #import "OFOutOfRangeException.h" #import "OFTruncatedDataException.h" #import "OFUnsupportedVersionException.h" #import "OFWriteFailedException.h" enum { modeRead, modeWrite }; OF_DIRECT_MEMBERS @interface OFZooArchive () - (void)of_readArchiveHeader; @end OF_DIRECT_MEMBERS @interface OFZooArchiveFileReadStream: OFStream <OFReadyForReadingObserving> { OFZooArchive *_archive; OF_KINDOF(OFStream *) _stream; OFStream *_decompressedStream; OFZooArchiveEntry *_entry; unsigned long long _toRead; uint16_t _CRC16; bool _atEndOfStream; } - (instancetype)of_initWithArchive: (OFZooArchive *)archive stream: (OFStream *)stream entry: (OFZooArchiveEntry *)entry; @end OF_DIRECT_MEMBERS @interface OFZooArchiveFileWriteStream: OFStream <OFReadyForWritingObserving> { OFZooArchive *_archive; OFMutableZooArchiveEntry *_entry; OFStringEncoding _encoding; OFSeekableStream *_stream; OFStreamOffset *_lastHeaderOffset; size_t *_lastHeaderLength; uint32_t _bytesWritten; uint16_t _CRC16; } - (instancetype)of_initWithArchive: (OFZooArchive *)archive stream: (OFSeekableStream *)stream entry: (OFZooArchiveEntry *)entry encoding: (OFStringEncoding)encoding lastHeaderOffset: (OFStreamOffset *)lastHeaderOffset lastHeaderLength: (size_t *)lastHeaderLength; @end @implementation OFZooArchive @synthesize encoding = _encoding; + (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(@"zoo", 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"]) @throw [OFNotImplementedException exceptionWithSelector: _cmd object: nil]; else @throw [OFInvalidArgumentException exception]; if (![stream isKindOfClass: [OFSeekableStream class]]) @throw [OFInvalidArgumentException exception]; _stream = [stream retain]; _encoding = OFStringEncodingUTF8; if (_mode == modeRead) [self of_readArchiveHeader]; } @catch (id e) { [self release]; @throw e; } return self; } - (instancetype)initWithIRI: (OFIRI *)IRI mode: (OFString *)mode { void *pool = objc_autoreleasePoolPush(); OFStream *stream; @try { if ([mode isEqual: @"w"]) stream = [OFIRIHandler openItemAtIRI: IRI mode: @"w+"]; 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]; [_archiveComment release]; [_currentEntry release]; [super dealloc]; } - (void)of_readArchiveHeader { char headerText[20]; uint32_t firstFileOffset, commentOffset; uint16_t commentLength; [_stream readIntoBuffer: headerText exactLength: 20]; if ([_stream readLittleEndianInt32] != 0xFDC4A7DC) @throw [OFInvalidFormatException exception]; firstFileOffset = [_stream readLittleEndianInt32]; if ([_stream readLittleEndianInt32] != ~(uint32_t)(firstFileOffset - 1)) @throw [OFInvalidFormatException exception]; if ((_minVersionNeeded = [_stream readBigEndianInt16]) > 0x201) @throw [OFUnsupportedVersionException exceptionWithVersion: [OFString stringWithFormat: @"%" PRIu8 @".%" PRIu8, _minVersionNeeded >> 8, _minVersionNeeded & 0xFF]]; if ((_headerType = [_stream readInt8]) > 1) @throw [OFUnsupportedVersionException exceptionWithVersion: [OFString stringWithFormat: @"%" PRIu8, _headerType]]; commentOffset = [_stream readLittleEndianInt32]; commentLength = [_stream readLittleEndianInt16]; if (commentOffset > 0) { [_stream seekToOffset: commentOffset whence: OFSeekSet]; _archiveComment = [_stream readStringWithLength: commentLength encoding: _encoding]; } [_stream seekToOffset: firstFileOffset whence: OFSeekSet]; } - (OFString *)archiveComment { return _archiveComment; } - (void)setArchiveComment: (OFString *)comment { void *pool = objc_autoreleasePoolPush(); OFString *old; if ([comment cStringLengthWithEncoding: _encoding] > UINT16_MAX) @throw [OFOutOfRangeException exception]; old = _archiveComment; _archiveComment = [comment copy]; [old release]; objc_autoreleasePoolPop(pool); } - (OFZooArchiveEntry *)nextEntry { if (_mode != modeRead) @throw [OFInvalidArgumentException exception]; if (_currentEntry != nil) [_stream seekToOffset: _currentEntry->_nextHeaderOffset whence: OFSeekSet]; [_currentEntry release]; _currentEntry = nil; @try { [_lastReturnedStream close]; } @catch (OFNotOpenException *e) { /* Might have already been closed by the user - that's fine. */ } _lastReturnedStream = nil; _currentEntry = [[OFZooArchiveEntry alloc] of_initWithStream: _stream encoding: _encoding]; return _currentEntry; } - (OFStream *)streamForReadingCurrentEntry { if (_mode != modeRead) @throw [OFInvalidArgumentException exception]; if (_currentEntry == nil) @throw [OFInvalidArgumentException exception]; _lastReturnedStream = [[[OFZooArchiveFileReadStream alloc] of_initWithArchive: self stream: _stream entry: _currentEntry] autorelease]; return _lastReturnedStream; } - (void)of_fixUpLastHeader { OFStreamOffset offset; unsigned char *buffer; if (_lastHeaderOffset == 0) return; offset = [_stream seekToOffset: 0 whence: OFSeekCurrent]; if (offset < 0 || (unsigned long long)offset > UINT32_MAX) @throw [OFOutOfRangeException exception]; OFEnsure(_lastHeaderLength >= 56); [_stream seekToOffset: _lastHeaderOffset whence: OFSeekSet]; buffer = OFAllocMemory(1, _lastHeaderLength); @try { uint16_t tmp16; uint32_t tmp32; [_stream readIntoBuffer: buffer exactLength: _lastHeaderLength]; tmp32 = OFToLittleEndian32((uint32_t)offset); memcpy(buffer + 6, &tmp32, 4); tmp16 = OFToLittleEndian16( _OFCRC16(0, buffer, _lastHeaderLength)); memcpy(buffer + 54, &tmp16, 2); [_stream seekToOffset: _lastHeaderOffset whence: OFSeekSet]; [_stream writeBuffer: buffer length: _lastHeaderLength]; [_stream seekToOffset: offset whence: OFSeekSet]; } @finally { OFFreeMemory(buffer); } } - (OFStream *)streamForWritingEntry: (OFZooArchiveEntry *)entry { if (_mode != modeWrite) @throw [OFInvalidArgumentException exception]; if (entry.compressionMethod != 0) @throw [OFNotImplementedException exceptionWithSelector: _cmd object: self]; if (_lastHeaderOffset == 0) { uint16_t commentLength = [_archiveComment cStringLengthWithEncoding: _encoding]; /* First file - write header. */ [_stream writeBuffer: "ObjFW Zoo Archive.\x1F" length: 20]; [_stream writeLittleEndianInt32: 0xFDC4A7DC]; [_stream writeLittleEndianInt32: 42 + commentLength]; [_stream writeLittleEndianInt32: -(42 + commentLength)]; /* TODO: Increase to 0x201 once we add compressed files. */ [_stream writeBigEndianInt16: 0x200]; /* Header type */ [_stream writeInt8: 1]; if (_archiveComment != nil) { [_stream writeLittleEndianInt32: 42]; [_stream writeLittleEndianInt16: commentLength]; } else { [_stream writeLittleEndianInt32: 0]; [_stream writeLittleEndianInt16: 0]; } /* Version flag */ [_stream writeInt8: 0]; if (_archiveComment != nil) [_stream writeString: _archiveComment encoding: _encoding]; } else [self of_fixUpLastHeader]; @try { [_lastReturnedStream close]; } @catch (OFNotOpenException *e) { /* Might have already been closed by the user - that's fine. */ } _lastReturnedStream = nil; _lastReturnedStream = [[[OFZooArchiveFileWriteStream alloc] of_initWithArchive: self stream: _stream entry: entry encoding: _encoding lastHeaderOffset: &_lastHeaderOffset lastHeaderLength: &_lastHeaderLength] autorelease]; return _lastReturnedStream; } - (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; /* * Zoo archives should be terminated with an entry that has a next * header offset of 0. */ if (_mode == modeWrite) { static const unsigned char header[56] = { 0xDC, 0xA7, 0xC4, 0xFD, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xFC, 0x83 }; [self of_fixUpLastHeader]; [_stream writeBuffer: header length: sizeof(header)]; } [_stream release]; _stream = nil; } @end @implementation OFZooArchiveFileReadStream - (instancetype)of_initWithArchive: (OFZooArchive *)archive stream: (OFStream *)stream entry: (OFZooArchiveEntry *)entry { self = [super init]; @try { _archive = [archive retain]; _stream = [stream retain]; switch (entry.compressionMethod) { case 0: _decompressedStream = [stream retain]; break; case 2: _decompressedStream = [[OFLHADecompressingStream alloc] of_initWithStream: stream distanceBits: 4 dictionaryBits: 14]; break; default: @throw [OFUnsupportedVersionException exceptionWithVersion: [OFString stringWithFormat: @"%u", entry.compressionMethod]]; } _entry = [entry copy]; _toRead = entry.uncompressedSize; [_stream seekToOffset: entry->_dataOffset whence: OFSeekSet]; } @catch (id e) { [self release]; @throw e; } return self; } - (void)dealloc { if (_stream != nil && _decompressedStream != nil) [self close]; [_entry release]; if (_archive->_lastReturnedStream == self) _archive->_lastReturnedStream = nil; [_archive release]; [super dealloc]; } - (bool)lowlevelIsAtEndOfStream { if (_stream == nil) @throw [OFNotOpenException exceptionWithObject: self]; return _atEndOfStream; } - (size_t)lowlevelReadIntoBuffer: (void *)buffer length: (size_t)length { size_t ret; if (_stream == nil) @throw [OFNotOpenException exceptionWithObject: self]; if (_atEndOfStream) return 0; if ([_stream isAtEndOfStream] && !_decompressedStream.hasDataInReadBuffer) @throw [OFTruncatedDataException exception]; if (length > _toRead) length = (size_t)_toRead; ret = [_decompressedStream readIntoBuffer: buffer length: length]; _toRead -= ret; _CRC16 = _OFCRC16(_CRC16, buffer, ret); if (_toRead == 0) { _atEndOfStream = true; if (_CRC16 != _entry.CRC16) { OFString *actualChecksum = [OFString stringWithFormat: @"%04" @PRIX16, _CRC16]; OFString *expectedChecksum = [OFString stringWithFormat: @"%04" @PRIX16, _entry.CRC16]; @throw [OFChecksumMismatchException exceptionWithActualChecksum: actualChecksum expectedChecksum: expectedChecksum]; } } return ret; } - (bool)hasDataInReadBuffer { return (super.hasDataInReadBuffer || _decompressedStream.hasDataInReadBuffer); } - (int)fileDescriptorForReading { return ((id <OFReadyForReadingObserving>)_decompressedStream) .fileDescriptorForReading; } - (void)close { if (_stream == nil || _decompressedStream == nil) @throw [OFNotOpenException exceptionWithObject: self]; [_stream release]; _stream = nil; [_decompressedStream release]; _decompressedStream = nil; [super close]; } @end @implementation OFZooArchiveFileWriteStream - (instancetype)of_initWithArchive: (OFZooArchive *)archive stream: (OFSeekableStream *)stream entry: (OFZooArchiveEntry *)entry encoding: (OFStringEncoding)encoding lastHeaderOffset: (OFStreamOffset *)lastHeaderOffset lastHeaderLength: (size_t *)lastHeaderLength { self = [super init]; @try { _archive = [archive retain]; _entry = [entry mutableCopy]; _encoding = encoding; _lastHeaderOffset = lastHeaderOffset; _lastHeaderLength = lastHeaderLength; *_lastHeaderOffset = [stream seekToOffset: 0 whence: OFSeekCurrent]; *_lastHeaderLength = [_entry of_writeToStream: stream encoding: _encoding]; /* * Retain stream last, so that -[close] called by -[dealloc] * doesn't write in case of error. */ _stream = [stream retain]; } @catch (id e) { [self release]; @throw e; } 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 (_stream == nil) @throw [OFNotOpenException exceptionWithObject: self]; if (UINT32_MAX - _bytesWritten < length) @throw [OFOutOfRangeException exception]; @try { [_stream writeBuffer: buffer length: length]; } @catch (OFWriteFailedException *e) { OFEnsure(e.bytesWritten <= length); _bytesWritten += (uint32_t)e.bytesWritten; _CRC16 = _OFCRC16(_CRC16, buffer, e.bytesWritten); if (e.errNo == EWOULDBLOCK || e.errNo == EAGAIN) return e.bytesWritten; @throw e; } _bytesWritten += (uint32_t)length; _CRC16 = _OFCRC16(_CRC16, buffer, length); return length; } - (bool)lowlevelIsAtEndOfStream { if (_stream == nil) @throw [OFNotOpenException exceptionWithObject: self]; return _stream.atEndOfStream; } - (int)fileDescriptorForWriting { return ((id <OFReadyForWritingObserving>)_stream) .fileDescriptorForWriting; } - (void)close { OFStreamOffset offset; if (_stream == nil) @throw [OFNotOpenException exceptionWithObject: self]; _entry.uncompressedSize = _bytesWritten; _entry.compressedSize = _bytesWritten; _entry.CRC16 = _CRC16; offset = [_stream seekToOffset: 0 whence: OFSeekCurrent]; if ((unsigned long long)offset > UINT32_MAX) @throw [OFOutOfRangeException exception]; [_stream seekToOffset: *_lastHeaderOffset whence: OFSeekSet]; _entry->_dataOffset = (uint32_t)offset; OFEnsure([_entry of_writeToStream: _stream encoding: _encoding] == *_lastHeaderLength); [_stream seekToOffset: offset whence: OFSeekSet]; [_stream release]; _stream = nil; [super close]; } @end