Index: src/OFLHAArchive.m ================================================================== --- src/OFLHAArchive.m +++ src/OFLHAArchive.m @@ -296,11 +296,11 @@ @throw [OFNotOpenException exceptionWithObject: self]; @try { [_lastReturnedStream close]; } @catch (OFNotOpenException *e) { - /* Might have already been closed by the user - that's fine */ + /* Might have already been closed by the user - that's fine. */ } /* LHA archives should be terminated with a header of size 0 */ if (_hasWritten) [_stream writeBuffer: "" length: 1]; Index: src/OFMutableZooArchiveEntry.m ================================================================== --- src/OFMutableZooArchiveEntry.m +++ src/OFMutableZooArchiveEntry.m @@ -37,11 +37,11 @@ self = [super of_init]; @try { void *pool = objc_autoreleasePoolPush(); - _fileName = [fileName copy]; + self.fileName = fileName; self.modificationDate = [OFDate date]; objc_autoreleasePoolPop(pool); } @catch (id e) { [self release]; @@ -127,16 +127,33 @@ [old release]; } - (void)setFileName: (OFString *)fileName { - OFString *old = _fileName; - _fileName = [fileName copy]; - [old release]; + void *pool = objc_autoreleasePoolPush(); + OFString *oldFileName = _fileName, *oldDirectoryName = _directoryName; + size_t lastSlash; + + lastSlash = [fileName rangeOfString: @"/" + options: OFStringSearchBackwards].location; + if (lastSlash != OFNotFound) { + _fileName = [[fileName substringWithRange: OFMakeRange( + lastSlash + 1, fileName.length - lastSlash - 1)] copy]; + [oldFileName release]; + + _directoryName = [[fileName substringWithRange: + OFMakeRange(0, lastSlash)] copy]; + [oldDirectoryName release]; + } else { + _fileName = [fileName copy]; + [oldFileName release]; + + [_directoryName release]; + _directoryName = nil; + } - [_directoryName release]; - _directoryName = nil; + objc_autoreleasePoolPop(pool); } - (void)setOperatingSystemIdentifier: (uint16_t)operatingSystemIdentifier { _operatingSystemIdentifier = operatingSystemIdentifier; Index: src/OFZooArchive.h ================================================================== --- src/OFZooArchive.h +++ src/OFZooArchive.h @@ -12,17 +12,16 @@ * LICENSE.GPLv2 or LICENSE.GPLv3 respectively included in the packaging of this * file. */ #import "OFObject.h" +#import "OFSeekableStream.h" #import "OFString.h" #import "OFZooArchiveEntry.h" OF_ASSUME_NONNULL_BEGIN -@class OFStream; - /** * @class OFZooArchive OFZooArchive.h ObjFW/OFZooArchive.h * * @brief A class for accessing and manipulating Zoo files. */ @@ -30,15 +29,20 @@ @interface OFZooArchive: OFObject { OF_KINDOF(OFStream *) _stream; uint_least8_t _mode; OFStringEncoding _encoding; + uint16_t _minVersionNeeded; + uint8_t _headerType; OFZooArchiveEntry *_Nullable _currentEntry; #ifdef OF_ZOO_ARCHIVE_M @public #endif OFStream *_Nullable _lastReturnedStream; +@protected + OFStreamOffset _lastHeaderOffset; + size_t _lastHeaderLength; } /** * @brief The encoding to use for the archive. Defaults to UTF-8. */ @@ -47,22 +51,22 @@ /** * @brief Creates a new OFZooArchive object with the specified stream. * * @param stream A stream from which the Zoo archive will be read. * For read mode, this needs to be an OFSeekableStream. - * @param mode The mode for the Zoo file. The only valid mode is "r" for - * reading. + * @param mode The mode for the Zoo file. Valid modes are "r" for reading and + * "w" for creating a new file. * @return A new, autoreleased OFZooArchive */ + (instancetype)archiveWithStream: (OFStream *)stream mode: (OFString *)mode; /** * @brief Creates a new OFZooArchive object with the specified file. * * @param IRI The IRI to the Zoo file - * @param mode The mode for the Zoo file. The only valid mode is "r" for - * reading. + * @param mode The mode for the Zoo file. Valid modes are "r" for reading and + * "w" for creating a new file. * @return A new, autoreleased OFZooArchive */ + (instancetype)archiveWithIRI: (OFIRI *)IRI mode: (OFString *)mode; - (instancetype)init OF_UNAVAILABLE; @@ -71,12 +75,12 @@ * @brief Initializes an already allocated OFZooArchive object with the * specified stream. * * @param stream A stream from which the Zoo archive will be read. * For read mode, this needs to be an OFSeekableStream. - * @param mode The mode for the Zoo file. The only valid mode is "r" for - * reading. + * @param mode The mode for the Zoo file. Valid modes are "r" for reading and + * "w" for creating a new file. * @return An initialized OFZooArchive */ - (instancetype)initWithStream: (OFStream *)stream mode: (OFString *)mode OF_DESIGNATED_INITIALIZER; @@ -83,12 +87,12 @@ /** * @brief Initializes an already allocated OFZooArchive object with the * specified file. * * @param IRI The IRI to the Zoo file - * @param mode The mode for the Zoo file. The only valid mode is "r" for - * reading. + * @param mode The mode for the Zoo file. Valid modes is "r" for reading and + * "w" for creating a new file. * @return An initialized OFZooArchive */ - (instancetype)initWithIRI: (OFIRI *)IRI mode: (OFString *)mode; /** @@ -96,16 +100,20 @@ * have been read. * * @note This is only available in read mode. * * @warning Calling @ref nextEntry will invalidate all streams returned by - * @ref streamForReadingCurrentEntry! Reading from an invalidated - * stream will throw an @ref OFReadFailedException! + * @ref streamForReadingCurrentEntry or + * @ref streamForWritingEntry:! Reading from or writing to an + * invalidated stream will throw an @ref OFReadFailedException or + * @ref OFWriteFailedException! * * @return The next entry from the Zoo archive or `nil` if all entries have * been read * @throw OFInvalidFormatException The archive's format is invalid + * @throw OFUnsupportedVersionException The archive's format is of an + * unsupported version * @throw OFTruncatedDataException The archive was truncated */ - (nullable OFZooArchiveEntry *)nextEntry; /** @@ -119,14 +127,39 @@ * @return A stream for reading the current entry * @throw OFSeekFailedException Seeking to the data in the archive failed */ - (OFStream *)streamForReadingCurrentEntry; +/** + * @brief Returns a stream for writing the specified entry. + * + * @note This is only available in write and append mode. + * + * @note The returned stream conforms to @ref OFReadyForWritingObserving if the + * underlying stream does so, too. + * + * @warning Calling @ref streamForWritingEntry: will invalidate all streams + * returned by @ref streamForReadingCurrentEntry or + * @ref streamForWritingEntry:! Reading from or writing to an + * invalidated stream will throw an @ref OFReadFailedException or + * @ref OFWriteFailedException! + * + * @param entry The entry for which a stream for writing should be returned.@n + * The following parts of the specified entry will be ignored: + * * The header type. + * * The minimum version needed. + * * The compressed size. + * * The uncompressed size. + * * The CRC16. + * @return A stream for writing the specified entry + */ +- (OFStream *)streamForWritingEntry: (OFZooArchiveEntry *)entry; + /** * @brief Closes the OFZooArchive. * * @throw OFNotOpenException The archive is not open */ - (void)close; @end OF_ASSUME_NONNULL_END Index: src/OFZooArchive.m ================================================================== --- src/OFZooArchive.m +++ src/OFZooArchive.m @@ -14,10 +14,12 @@ */ #define OF_ZOO_ARCHIVE_M #include "config.h" + +#include #import "OFZooArchive.h" #import "OFZooArchiveEntry.h" #import "OFZooArchiveEntry+Private.h" #import "OFCRC16.h" @@ -32,15 +34,18 @@ #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 + modeRead, + modeWrite }; OF_DIRECT_MEMBERS @interface OFZooArchive () - (void)of_readArchiveHeader; @@ -60,10 +65,31 @@ - (instancetype)of_initWithArchive: (OFZooArchive *)archive stream: (OFStream *)stream entry: (OFZooArchiveEntry *)entry; @end + +OF_DIRECT_MEMBERS +@interface OFZooArchiveFileWriteStream: OFStream +{ + 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 @@ -86,26 +112,27 @@ self = [super init]; @try { if ([mode isEqual: @"r"]) _mode = modeRead; - else if ([mode isEqual: @"w"] || [mode isEqual: @"a"]) + 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) { - if (![stream isKindOfClass: [OFSeekableStream class]]) - @throw [OFInvalidArgumentException exception]; - + if (_mode == modeRead) [self of_readArchiveHeader]; - } } @catch (id e) { [self release]; @throw e; } @@ -116,12 +143,12 @@ { void *pool = objc_autoreleasePoolPush(); OFStream *stream; @try { - if ([mode isEqual: @"a"]) - stream = [OFIRIHandler openItemAtIRI: IRI mode: @"r+"]; + if ([mode isEqual: @"w"]) + stream = [OFIRIHandler openItemAtIRI: IRI mode: @"w+"]; else stream = [OFIRIHandler openItemAtIRI: IRI mode: mode]; } @catch (id e) { [self release]; @throw e; @@ -157,12 +184,19 @@ firstFileOffset = [_stream readLittleEndianInt32]; if ([_stream readLittleEndianInt32] != ~(uint32_t)(firstFileOffset - 1)) @throw [OFInvalidFormatException exception]; - /* Version */ - [_stream readBigEndianInt16]; + 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]]; [_stream seekToOffset: firstFileOffset whence: OFSeekSet]; } - (OFZooArchiveEntry *)nextEntry @@ -204,10 +238,93 @@ 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 || offset > UINT32_MAX || _lastHeaderLength < 56) + @throw [OFOutOfRangeException exception]; + + [_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) { + /* First file - write header. */ + [_stream writeBuffer: "ObjFW Zoo Archive.\x1F" length: 20]; + [_stream writeLittleEndianInt32: 0xFDC4A7DC]; + [_stream writeLittleEndianInt32: 42]; + [_stream writeLittleEndianInt32: -42]; + /* TODO: Increase to 0x201 once we add compressed files. */ + [_stream writeBigEndianInt16: 0x200]; + /* Header type */ + [_stream writeInt8: 1]; + /* Archive comment offset */ + [_stream writeLittleEndianInt32: 0]; + /* Archive comment length */ + [_stream writeLittleEndianInt16: 0]; + /* Version flag */ + [_stream writeInt8: 0]; + } 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]; @@ -215,12 +332,28 @@ @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 @@ -347,9 +480,131 @@ [_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 )_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 (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 Index: src/OFZooArchiveEntry+Private.h ================================================================== --- src/OFZooArchiveEntry+Private.h +++ src/OFZooArchiveEntry+Private.h @@ -12,16 +12,19 @@ * LICENSE.GPLv2 or LICENSE.GPLv3 respectively included in the packaging of this * file. */ #import "OFZooArchive.h" +#import "OFSeekableStream.h" OF_ASSUME_NONNULL_BEGIN @interface OFZooArchiveEntry () - (instancetype)of_init OF_METHOD_FAMILY(init); -- (nullable instancetype)of_initWithStream: (OF_KINDOF(OFStream *))stream +- (nullable instancetype)of_initWithStream: (OFSeekableStream *)stream encoding: (OFStringEncoding)encoding OF_METHOD_FAMILY(init) OF_DIRECT; +- (size_t)of_writeToStream: (OFSeekableStream *)stream + encoding: (OFStringEncoding)encoding; @end OF_ASSUME_NONNULL_END Index: src/OFZooArchiveEntry.m ================================================================== --- src/OFZooArchiveEntry.m +++ src/OFZooArchiveEntry.m @@ -15,17 +15,18 @@ #include "config.h" #import "OFZooArchiveEntry.h" #import "OFZooArchiveEntry+Private.h" +#import "OFData.h" #import "OFDate.h" #import "OFNumber.h" #import "OFSeekableStream.h" -#import "OFStream.h" #import "OFString.h" #import "OFInvalidFormatException.h" +#import "OFOutOfRangeException.h" #import "OFUnsupportedVersionException.h" @implementation OFZooArchiveEntry - (instancetype)init { @@ -45,11 +46,11 @@ } return self; } -- (instancetype)of_initWithStream: (OF_KINDOF(OFStream *))stream +- (instancetype)of_initWithStream: (OFSeekableStream *)stream encoding: (OFStringEncoding)encoding { self = [super init]; @try { @@ -62,11 +63,11 @@ @throw [OFInvalidFormatException exception]; if ((_headerType = [stream readInt8]) > 2) @throw [OFUnsupportedVersionException exceptionWithVersion: [OFString - stringWithFormat: @"%u", _headerType]]; + stringWithFormat: @"%" PRIu8, _headerType]]; _compressionMethod = [stream readInt8]; _nextHeaderOffset = [stream readLittleEndianInt32]; if (_nextHeaderOffset == 0) { @@ -78,11 +79,17 @@ _lastModifiedFileDate = [stream readLittleEndianInt16]; _lastModifiedFileTime = [stream readLittleEndianInt16]; _CRC16 = [stream readLittleEndianInt16]; _uncompressedSize = [stream readLittleEndianInt32]; _compressedSize = [stream readLittleEndianInt32]; - _minVersionNeeded = [stream readBigEndianInt16]; + + if ((_minVersionNeeded = [stream readBigEndianInt16]) > 0x201) + @throw [OFUnsupportedVersionException + exceptionWithVersion: [OFString stringWithFormat: + @"%" PRIu8 @".%" PRIu8, + _minVersionNeeded >> 8, _minVersionNeeded & 0xFF]]; + _deleted = [stream readInt8]; /* * File structure, whatever that is meant to be. Seems to * always be 0. */ @@ -94,22 +101,21 @@ if (fileNameBuffer[12] != '\0') fileNameBuffer[12] = '\0'; if (_headerType >= 2) { uint16_t extraLength = [stream readLittleEndianInt16]; - uint8_t fileNameLength, directoryNameLength; - - if (extraLength < 10) - @throw [OFInvalidFormatException exception]; + uint8_t fileNameLength = 0, directoryNameLength = 0; _timeZone = [stream readInt8]; /* CRC16 of the header */ [stream readLittleEndianInt16]; - fileNameLength = [stream readInt8]; - directoryNameLength = [stream readInt8]; - extraLength -= 2; + if (extraLength >= 2) { + fileNameLength = [stream readInt8]; + directoryNameLength = [stream readInt8]; + extraLength -= 2; + } if (fileNameLength > 0) { if (extraLength < fileNameLength) @throw [OFInvalidFormatException exception]; @@ -195,11 +201,11 @@ } - (id)mutableCopy { OFZooArchiveEntry *copy = [[OFMutableZooArchiveEntry alloc] - initWithFileName: _fileName]; + initWithFileName: self.fileName]; @try { copy->_headerType = _headerType; copy->_compressionMethod = _compressionMethod; copy->_nextHeaderOffset = _nextHeaderOffset; @@ -210,20 +216,19 @@ copy->_uncompressedSize = _uncompressedSize; copy->_compressedSize = _compressedSize; copy->_minVersionNeeded = _minVersionNeeded; copy->_deleted = _deleted; copy->_fileComment = [_fileComment copy]; - copy->_directoryName = [_directoryName copy]; copy->_operatingSystemIdentifier = _operatingSystemIdentifier; copy->_POSIXPermissions = [_POSIXPermissions retain]; copy->_timeZone = _timeZone; } @catch (id e) { [self release]; @throw e; } - return self; + return copy; } - (uint8_t)headerType { return _headerType; @@ -319,10 +324,146 @@ if (_timeZone == 0x7F) return nil; return [OFNumber numberWithFloat: -(float)_timeZone / 4]; } + +- (size_t)of_writeToStream: (OFSeekableStream *)stream + encoding: (OFStringEncoding)encoding +{ + void *pool = objc_autoreleasePoolPush(); + OFMutableData *data = [OFMutableData dataWithCapacity: 56]; + OFStreamOffset offset = [stream seekToOffset: 0 whence: OFSeekCurrent]; + size_t dataOffsetIndex, commentOffsetIndex; + char fileNameBuffer[13] = { 0 }; + uint8_t tmp8; + uint16_t tmp16; + uint32_t tmp32; + size_t commentLength, fileNameLength, directoryNameLength, length; + + if (_uncompressedSize > UINT32_MAX || _compressedSize > UINT32_MAX) + @throw [OFOutOfRangeException exception]; + + commentLength = [_fileComment cStringLengthWithEncoding: encoding]; + if (commentLength > UINT16_MAX) + @throw [OFOutOfRangeException exception]; + + fileNameLength = [_fileName cStringLengthWithEncoding: encoding]; + if (fileNameLength > UINT8_MAX) + @throw [OFOutOfRangeException exception]; + + directoryNameLength = + [_directoryName cStringLengthWithEncoding: encoding]; + if (directoryNameLength > UINT8_MAX) + @throw [OFOutOfRangeException exception]; + + [data addItems: "\xDC\xA7\xC4\xFD" count: 4]; + /* Header type */ + [data addItem: "\x02"]; + [data addItem: &_compressionMethod]; + + /* Next header offset filled when writing the next header */ + [data increaseCountBy: 4]; + /* Data offset is filled after generating the header */ + dataOffsetIndex = data.count; + [data increaseCountBy: 4]; + + tmp16 = OFToLittleEndian16(_lastModifiedFileDate); + [data addItems: &tmp16 count: 2]; + tmp16 = OFToLittleEndian16(_lastModifiedFileTime); + [data addItems: &tmp16 count: 2]; + tmp16 = OFToLittleEndian16(_CRC16); + [data addItems: &tmp16 count: 2]; + + tmp32 = OFToLittleEndian32((uint32_t)_uncompressedSize); + [data addItems: &tmp32 count: 4]; + tmp32 = OFToLittleEndian32((uint32_t)_compressedSize); + [data addItems: &tmp32 count: 4]; + + /* Min version needed */ + /* TODO: Increase to 2.1 once we add compression. */ + [data addItems: "\x02\x00" count: 2]; + + [data addItem: (_deleted ? "\x01" : "")]; + + /* + * File structure, whatever that is meant to be. + * Seems to always be 0. + */ + [data addItem: ""]; + + /* Comment offset is filled after generating the header */ + commentOffsetIndex = data.count; + [data increaseCountBy: 4]; + tmp16 = OFToLittleEndian16((uint16_t)commentLength); + [data addItems: &tmp16 count: 2]; + + strncpy(fileNameBuffer, [_fileName cStringWithEncoding: encoding], 12); + [data addItems: fileNameBuffer count: 13]; + + /* Variable length. */ + tmp16 = OFToLittleEndian16(fileNameLength + directoryNameLength + 4 + + (_POSIXPermissions != nil ? 3 : 0)); + [data addItems: &tmp16 count: 2]; + + [data addItem: &_timeZone]; + + /* + * CRC16 is filled when writing the next header, as the CRC needs to + * include the next header offset. + */ + [data increaseCountBy: 2]; + + tmp8 = (uint8_t)fileNameLength; + [data addItem: &tmp8]; + tmp8 = (uint8_t)directoryNameLength; + [data addItem: &tmp8]; + + [data addItems: [_fileName cStringWithEncoding: encoding] + count: fileNameLength]; + [data addItems: [_directoryName cStringWithEncoding: encoding] + count: directoryNameLength]; + + tmp16 = OFToLittleEndian16((uint16_t)_operatingSystemIdentifier); + [data addItems: &tmp16 count: 2]; + + if (_POSIXPermissions != nil) { + unsigned short mode = _POSIXPermissions.unsignedShortValue; + uint8_t attributes[3]; + + attributes[0] = mode & 0xFF; + attributes[1] = mode >> 8; + attributes[2] = (1 << 6); + + [data addItems: attributes count: sizeof(attributes)]; + } + + /* Now that we have the entire header, we know where the data starts. */ + if (SIZE_MAX - data.count < commentLength) + @throw [OFOutOfRangeException exception]; + + if (offset < 0 || UINT32_MAX - (unsigned long long)offset < + data.count + commentLength) + @throw [OFOutOfRangeException exception]; + + tmp32 = OFToLittleEndian32( + (uint32_t)offset + (uint32_t)data.count + (uint32_t)commentLength); + memcpy([data mutableItemAtIndex: dataOffsetIndex], &tmp32, 4); + + tmp32 = OFToLittleEndian32((uint32_t)offset + (uint32_t)data.count); + memcpy([data mutableItemAtIndex: commentOffsetIndex], &tmp32, 4); + + [stream writeData: data]; + length = data.count; + + if (commentLength > 0) + [stream writeString: _fileComment encoding: encoding]; + + objc_autoreleasePoolPop(pool); + + return length; +} - (OFString *)description { void *pool = objc_autoreleasePoolPush(); OFString *ret = [OFString stringWithFormat: