Index: src/OFLHAArchive.h ================================================================== --- src/OFLHAArchive.h +++ src/OFLHAArchive.h @@ -30,12 +30,16 @@ * @brief A class for accessing and manipulating LHA files. */ @interface OFLHAArchive: OFObject { OF_KINDOF(OFStream *) _stream; + enum { + OF_LHA_ARCHIVE_MODE_READ, + OF_LHA_ARCHIVE_MODE_WRITE, + OF_LHA_ARCHIVE_MODE_APPEND + } _mode; of_string_encoding_t _encoding; - OFLHAArchiveEntry *_Nullable _lastEntry; OF_KINDOF(OFStream *) _Nullable _lastReturnedStream; } /*! * @brief The encoding to use for the archive. Defaults to ISO 8859-1. @@ -126,12 +130,35 @@ * @return The next entry from the LHA archive or `nil` if all entries have * been read */ - (nullable OFLHAArchiveEntry *)nextEntry; +/*! + * @brief Returns a stream for writing the specified entry. + * + * @note This is only available in write and append mode. + * + * @note The uncompressed size, compressed size and CRC16 of the specified + * entry are ignored. + * + * @note The returned stream only conforms to @ref OFReadyForWritingObserving if + * the underlying stream does so, too. + * + * @warning Calling @ref nextEntry 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 + * @return A stream for writing the specified entry + */ +- (OFStream *) + streamForWritingEntry: (OFLHAArchiveEntry *)entry; + /*! * @brief Closes the OFLHAArchive. */ - (void)close; @end OF_ASSUME_NONNULL_END Index: src/OFLHAArchive.m ================================================================== --- src/OFLHAArchive.m +++ src/OFLHAArchive.m @@ -32,11 +32,13 @@ #import "OFChecksumMismatchException.h" #import "OFInvalidArgumentException.h" #import "OFNotImplementedException.h" #import "OFNotOpenException.h" +#import "OFOutOfRangeException.h" #import "OFTruncatedDataException.h" +#import "OFWriteFailedException.h" @interface OFLHAArchive_FileReadStream: OFStream { OF_KINDOF(OFStream *) _stream; OF_KINDOF(OFStream *) _decompressedStream; @@ -48,10 +50,25 @@ - (instancetype)of_initWithStream: (OF_KINDOF(OFStream *))stream entry: (OFLHAArchiveEntry *)entry; - (void)of_skip; @end + +@interface OFLHAArchive_FileWriteStream: OFStream +{ + OFMutableLHAArchiveEntry *_entry; + of_string_encoding_t _encoding; + OF_KINDOF(OFStream *) _stream; + of_offset_t _headerOffset; + uint32_t _bytesWritten; + uint16_t _CRC16; +} + +- (instancetype)of_initWithStream: (OF_KINDOF(OFStream *))stream + entry: (OFLHAArchiveEntry *)entry + encoding: (of_string_encoding_t)encoding; +@end @implementation OFLHAArchive @synthesize encoding = _encoding; + (instancetype)archiveWithStream: (OF_KINDOF(OFStream *))stream @@ -79,16 +96,30 @@ mode: (OFString *)mode { self = [super init]; @try { - if (![mode isEqual: @"r"]) - @throw [OFNotImplementedException - exceptionWithSelector: _cmd - object: self]; - _stream = [stream retain]; + + if ([mode isEqual: @"r"]) + _mode = OF_LHA_ARCHIVE_MODE_READ; + else if ([mode isEqual: @"w"]) + _mode = OF_LHA_ARCHIVE_MODE_WRITE; + else if ([mode isEqual: @"a"]) + _mode = OF_LHA_ARCHIVE_MODE_APPEND; + else + @throw [OFInvalidArgumentException exception]; + + if ((_mode == OF_LHA_ARCHIVE_MODE_WRITE || + _mode == OF_LHA_ARCHIVE_MODE_APPEND) && + ![_stream isKindOfClass: [OFSeekableStream class]]) + @throw [OFInvalidArgumentException exception]; + + if (_mode == OF_LHA_ARCHIVE_MODE_APPEND) + [_stream seekToOffset: 0 + whence: SEEK_END]; + _encoding = OF_STRING_ENCODING_ISO_8859_1; } @catch (id e) { [self release]; @throw e; } @@ -127,15 +158,16 @@ [super dealloc]; } - (OFLHAArchiveEntry *)nextEntry { + OFLHAArchiveEntry *entry; char header[21]; size_t headerLen; - [_lastEntry release]; - _lastEntry = nil; + if (_mode != OF_LHA_ARCHIVE_MODE_READ) + @throw [OFInvalidArgumentException exception]; [_lastReturnedStream of_skip]; [_lastReturnedStream close]; [_lastReturnedStream release]; _lastReturnedStream = nil; @@ -153,38 +185,61 @@ headerLen += [_stream readIntoBuffer: header + headerLen length: 21 - headerLen]; } - _lastEntry = [[OFLHAArchiveEntry alloc] + entry = [[[OFLHAArchiveEntry alloc] of_initWithHeader: header stream: _stream - encoding: _encoding]; + encoding: _encoding] autorelease]; _lastReturnedStream = [[OFLHAArchive_FileReadStream alloc] of_initWithStream: _stream - entry: _lastEntry]; + entry: entry]; - return [[_lastEntry copy] autorelease]; + return entry; } - (OFStream *)streamForReadingCurrentEntry { + if (_mode != OF_LHA_ARCHIVE_MODE_READ) + @throw [OFInvalidArgumentException exception]; + if (_lastReturnedStream == nil) @throw [OFInvalidArgumentException exception]; + + return [[_lastReturnedStream retain] autorelease]; +} + +- (OFStream *) + streamForWritingEntry: (OFLHAArchiveEntry *)entry +{ + if (_mode != OF_LHA_ARCHIVE_MODE_WRITE && + _mode != OF_LHA_ARCHIVE_MODE_APPEND) + @throw [OFInvalidArgumentException exception]; + + if (![[entry compressionMethod] isEqual: @"-lh0-"]) + @throw [OFNotImplementedException exceptionWithSelector: _cmd + object: self]; + + [_lastReturnedStream close]; + [_lastReturnedStream release]; + _lastReturnedStream = nil; + + _lastReturnedStream = [[OFLHAArchive_FileWriteStream alloc] + of_initWithStream: _stream + entry: entry + encoding: _encoding]; return [[_lastReturnedStream retain] autorelease]; } - (void)close { if (_stream == nil) return; - [_lastEntry release]; - _lastEntry = nil; - [_lastReturnedStream close]; [_lastReturnedStream release]; _lastReturnedStream = nil; [_stream release]; @@ -358,9 +413,114 @@ [_stream release]; _stream = nil; [_decompressedStream release]; _decompressedStream = nil; + + [super close]; +} +@end + +@implementation OFLHAArchive_FileWriteStream +- (instancetype)of_initWithStream: (OF_KINDOF(OFStream *))stream + entry: (OFLHAArchiveEntry *)entry + encoding: (of_string_encoding_t)encoding +{ + self = [super init]; + + @try { + _entry = [entry mutableCopy]; + _encoding = encoding; + + _headerOffset = [stream seekToOffset: 0 + whence: SEEK_CUR]; + [_entry of_writeToStream: stream + encoding: _encoding]; + + /* + * Retain stream last, so that -[close] called by -[dealloc] + * doesn't write in case of an error. + */ + _stream = [stream retain]; + } @catch (id e) { + [self release]; + @throw e; + } + + return self; +} + +- (void)dealloc +{ + [self close]; + + [_entry release]; + + [super dealloc]; +} + +- (size_t)lowlevelWriteBuffer: (const void *)buffer + length: (size_t)length +{ + uint32_t bytesWritten; + + if (_stream == nil) + @throw [OFNotOpenException exceptionWithObject: self]; + + if (UINT32_MAX - _bytesWritten < length) + @throw [OFOutOfRangeException exception]; + + @try { + bytesWritten = (uint32_t)[_stream writeBuffer: buffer + length: length]; + } @catch (OFWriteFailedException *e) { + _bytesWritten += [e bytesWritten]; + _CRC16 = of_crc16(_CRC16, buffer, [e bytesWritten]); + + @throw e; + } + + _bytesWritten += (uint32_t)bytesWritten; + _CRC16 = of_crc16(_CRC16, buffer, bytesWritten); + + return bytesWritten; +} + +- (bool)lowlevelIsAtEndOfStream +{ + if (_stream == nil) + @throw [OFNotOpenException exceptionWithObject: self]; + + return [_stream isAtEndOfStream]; +} + +- (int)fileDescriptorForWriting +{ + return [_stream fileDescriptorForWriting]; +} + +- (void)close +{ + of_offset_t offset; + + if (_stream == nil) + return; + + [_entry setUncompressedSize: _bytesWritten]; + [_entry setCompressedSize: _bytesWritten]; + [_entry setCRC16: _CRC16]; + + offset = [_stream seekToOffset: 0 + whence:SEEK_CUR]; + [_stream seekToOffset: _headerOffset + whence: SEEK_SET]; + [_entry of_writeToStream: _stream + encoding: _encoding]; + [_stream seekToOffset: offset + whence: SEEK_SET]; + + [_stream release]; + _stream = nil; [super close]; } @end Index: src/OFLHAArchiveEntry+Private.h ================================================================== --- src/OFLHAArchiveEntry+Private.h +++ src/OFLHAArchiveEntry+Private.h @@ -22,8 +22,10 @@ @interface OFLHAArchiveEntry () - (instancetype)of_initWithHeader: (char [_Nonnull 21])header stream: (OFStream *)stream encoding: (of_string_encoding_t)encoding OF_METHOD_FAMILY(init); +- (void)of_writeToStream: (OFStream *)stream + encoding: (of_string_encoding_t)encoding; @end OF_ASSUME_NONNULL_END Index: src/OFLHAArchiveEntry.m ================================================================== --- src/OFLHAArchiveEntry.m +++ src/OFLHAArchiveEntry.m @@ -16,10 +16,12 @@ */ #include "config.h" #define OF_LHA_ARCHIVE_ENTRY_M + +#include #import "OFLHAArchiveEntry.h" #import "OFLHAArchiveEntry+Private.h" #import "OFArray.h" #import "OFData.h" @@ -26,11 +28,15 @@ #import "OFDate.h" #import "OFNumber.h" #import "OFStream.h" #import "OFString.h" +#import "crc16.h" + +#import "OFInvalidArgumentException.h" #import "OFInvalidFormatException.h" +#import "OFOutOfRangeException.h" #import "OFUnsupportedVersionException.h" static OFDate * parseMSDOSDate(uint32_t MSDOSDate) { @@ -260,10 +266,37 @@ entry->_compressedSize -= size; } } } + +static void +getFileNameAndDirectoryNameForPath(OFString *path, + of_string_encoding_t encoding, + const char **fileName, size_t *fileNameLength, + const char **directoryName, size_t *directoryNameLength) +{ + /* We use OFMutableData to have an autoreleased buffer. */ + OFMutableData *data = [OFMutableData + dataWithItems: [path cStringWithEncoding: encoding] + count: [path cStringLengthWithEncoding: encoding]]; + char *cString = [data items]; + size_t length = [data count]; + size_t pos = 0; + + for (size_t i = 0; i < length; i++) { + if (cString[i] == '/' || cString[i] == '\\') { + cString[i] = '\xFF'; + pos = i + 1; + } + } + + *fileName = cString + pos; + *fileNameLength = length - pos; + *directoryName = cString; + *directoryNameLength = pos; +} @implementation OFLHAArchiveEntry + (instancetype)entryWithFileName: (OFString *)fileName { return [[[self alloc] initWithFileName: fileName] autorelease]; @@ -358,10 +391,13 @@ @throw [OFUnsupportedVersionException exceptionWithVersion: version]; } + if (_fileName == nil) + @throw [OFInvalidFormatException exception]; + [_extensions makeImmutable]; } @catch (id e) { [self release]; @throw e; } @@ -507,16 +543,201 @@ - (OFArray OF_GENERIC(OFData *) *)extensions { return _extensions; } + +- (void)of_writeToStream: (OFStream *)stream + encoding: (of_string_encoding_t)encoding +{ + void *pool = objc_autoreleasePoolPush(); + OFMutableData *data = [OFMutableData dataWithCapacity: 24]; + const char *fileName, *directoryName; + size_t fileNameLength, directoryNameLength; + uint16_t tmp16; + uint32_t tmp32; + size_t headerSize; + + if ([_compressionMethod cStringLengthWithEncoding: + OF_STRING_ENCODING_ASCII] != 5) + @throw [OFInvalidArgumentException exception]; + + getFileNameAndDirectoryNameForPath(_fileName, encoding, + &fileName, &fileNameLength, + &directoryName, &directoryNameLength); + + if (fileNameLength > UINT16_MAX - 3 || + directoryNameLength > UINT16_MAX - 3) + @throw [OFOutOfRangeException exception]; + + /* Length. Filled in after we're done. */ + [data increaseCountBy: 2]; + + [data addItems: [_compressionMethod + cStringWithEncoding: OF_STRING_ENCODING_ASCII] + count: 5]; + + tmp32 = OF_BSWAP32_IF_BE(_compressedSize); + [data addItems: &tmp32 + count: sizeof(tmp32)]; + + tmp32 = OF_BSWAP32_IF_BE(_uncompressedSize); + [data addItems: &tmp32 + count: sizeof(tmp32)]; + + tmp32 = OF_BSWAP32_IF_BE((uint32_t)[_date timeIntervalSince1970]); + [data addItems: &tmp32 + count: sizeof(tmp32)]; + + /* Reserved */ + [data increaseCountBy: 1]; + + /* Header level */ + [data addItem: "\x02"]; + + /* CRC16 */ + tmp16 = OF_BSWAP16_IF_BE(_CRC16); + [data addItems: &tmp16 + count: sizeof(tmp16)]; + + /* Operating system identifier */ + [data addItem: "U"]; + + /* Common header. Contains CRC16, which is written at the end. */ + tmp16 = OF_BSWAP16_IF_BE(5); + [data addItems: &tmp16 + count: sizeof(tmp16)]; + [data addItem: "\x00"]; + [data increaseCountBy: 2]; + + tmp16 = OF_BSWAP16_IF_BE((uint16_t)fileNameLength + 3); + [data addItems: &tmp16 + count: sizeof(tmp16)]; + [data addItem: "\x01"]; + [data addItems: fileName + count: fileNameLength]; + + if (directoryNameLength > 0) { + tmp16 = OF_BSWAP16_IF_BE((uint16_t)directoryNameLength + 3); + [data addItems: &tmp16 + count: sizeof(tmp16)]; + [data addItem: "\x02"]; + [data addItems: directoryName + count: directoryNameLength]; + } + + if (_fileComment != nil) { + size_t fileCommentLength = + [_fileComment cStringLengthWithEncoding: encoding]; + + if (fileCommentLength > UINT16_MAX - 3) + @throw [OFOutOfRangeException exception]; + + tmp16 = OF_BSWAP16_IF_BE((uint16_t)fileCommentLength + 3); + [data addItems: &tmp16 + count: sizeof(tmp16)]; + [data addItem: "\x3F"]; + [data addItems: [_fileComment cStringWithEncoding: encoding] + count: fileCommentLength]; + } + + if (_mode != nil) { + tmp16 = OF_BSWAP16_IF_BE(5); + [data addItems: &tmp16 + count: sizeof(tmp16)]; + [data addItem: "\x50"]; + + tmp16 = OF_BSWAP16_IF_BE([_mode uInt16Value]); + [data addItems: &tmp16 + count: sizeof(tmp16)]; + } + + if (_UID != nil || _GID != nil) { + if (_UID == nil || _GID == nil) + @throw [OFInvalidArgumentException exception]; + + tmp16 = OF_BSWAP16_IF_BE(7); + [data addItems: &tmp16 + count: sizeof(tmp16)]; + [data addItem: "\x51"]; + + tmp16 = OF_BSWAP16_IF_BE([_GID uInt16Value]); + [data addItems: &tmp16 + count: sizeof(tmp16)]; + + tmp16 = OF_BSWAP16_IF_BE([_UID uInt16Value]); + [data addItems: &tmp16 + count: sizeof(tmp16)]; + } + + if (_group != nil) { + size_t groupLength = + [_group cStringLengthWithEncoding: encoding]; + + if (groupLength > UINT16_MAX - 3) + @throw [OFOutOfRangeException exception]; + + tmp16 = OF_BSWAP16_IF_BE((uint16_t)groupLength + 3); + [data addItems: &tmp16 + count: sizeof(tmp16)]; + [data addItem: "\x52"]; + [data addItems: [_group cStringWithEncoding: encoding] + count: groupLength]; + } + + if (_owner != nil) { + size_t ownerLength = + [_owner cStringLengthWithEncoding: encoding]; + + if (ownerLength > UINT16_MAX - 3) + @throw [OFOutOfRangeException exception]; + + tmp16 = OF_BSWAP16_IF_BE((uint16_t)ownerLength + 3); + [data addItems: &tmp16 + count: sizeof(tmp16)]; + [data addItem: "\x53"]; + [data addItems: [_owner cStringWithEncoding: encoding] + count: ownerLength]; + } + + if (_modificationDate != nil) { + tmp16 = OF_BSWAP16_IF_BE(7); + [data addItems: &tmp16 + count: sizeof(tmp16)]; + [data addItem: "\x54"]; + + tmp32 = OF_BSWAP32_IF_BE( + (uint32_t)[_modificationDate timeIntervalSince1970]); + [data addItems: &tmp32 + count: sizeof(tmp32)]; + } + + /* Zero-length extension to terminate */ + [data increaseCountBy: 2]; + + headerSize = [data count]; + + if (headerSize > UINT16_MAX) + @throw [OFOutOfRangeException exception]; + + /* Now fill in the size and CRC16 for the entire header */ + tmp16 = OF_BSWAP16_IF_BE(headerSize); + memcpy([data itemAtIndex: 0], &tmp16, sizeof(tmp16)); + + tmp16 = OF_BSWAP16_IF_BE(of_crc16(0, [data items], [data count])); + memcpy([data itemAtIndex: 27], &tmp16, sizeof(tmp16)); + + [stream writeData: data]; + + objc_autoreleasePoolPop(pool); +} - (OFString *)description { void *pool = objc_autoreleasePoolPush(); OFString *mode = (_mode == nil - ? @"(nil)" + ? nil : [OFString stringWithFormat: @"%" PRIo16, [_mode uInt16Value]]); OFString *extensions = [[_extensions description] stringByReplacingOccurrencesOfString: @"\n" withString: @"\n\t"]; OFString *ret = [OFString stringWithFormat: Index: src/OFTarArchive.m ================================================================== --- src/OFTarArchive.m +++ src/OFTarArchive.m @@ -107,24 +107,24 @@ bool empty = true; if (![_stream isKindOfClass: [OFSeekableStream class]]) @throw [OFInvalidArgumentException exception]; - [stream seekToOffset: -1024 - whence: SEEK_END]; - [stream readIntoBuffer: buffer.c - exactLength: 1024]; + [_stream seekToOffset: -1024 + whence: SEEK_END]; + [_stream readIntoBuffer: buffer.c + exactLength: 1024]; for (size_t i = 0; i < 1024 / sizeof(uint32_t); i++) if (buffer.u32[i] != 0) empty = false; if (!empty) @throw [OFInvalidFormatException exception]; - [stream seekToOffset: -1024 - whence: SEEK_END]; + [_stream seekToOffset: -1024 + whence: SEEK_END]; } _encoding = OF_STRING_ENCODING_UTF_8; } @catch (id e) { [self release]; @@ -402,11 +402,11 @@ } } @end @implementation OFTarArchive_FileWriteStream -- (instancetype)of_initWithStream: (OFStream *)stream +- (instancetype)of_initWithStream: (OF_KINDOF(OFStream *))stream entry: (OFTarArchiveEntry *)entry { self = [super init]; @try { Index: src/crc16.h ================================================================== --- src/crc16.h +++ src/crc16.h @@ -25,10 +25,10 @@ #import "macros.h" #ifdef __cplusplus extern "C" { #endif -extern uint16_t of_crc16(uint16_t crc, const unsigned char *_Nonnull bytes, +extern uint16_t of_crc16(uint16_t crc, const void *_Nonnull bytes, size_t length); #ifdef __cplusplus } #endif Index: src/crc16.m ================================================================== --- src/crc16.m +++ src/crc16.m @@ -15,21 +15,23 @@ * file. */ #include "config.h" -#import "crc32.h" +#import "crc16.h" #define CRC16_MAGIC 0xA001 uint16_t -of_crc16(uint16_t crc, const unsigned char *bytes, size_t length) +of_crc16(uint16_t crc, const void *bytes_, size_t length) { + const unsigned char *bytes = bytes_; + for (size_t i = 0; i < length; i++) { crc ^= bytes[i]; for (uint8_t j = 0; j < 8; j++) crc = (crc >> 1) ^ (CRC16_MAGIC & (~(crc & 1) + 1)); } return crc; } Index: src/crc32.h ================================================================== --- src/crc32.h +++ src/crc32.h @@ -25,10 +25,10 @@ #import "macros.h" #ifdef __cplusplus extern "C" { #endif -extern uint32_t of_crc32(uint32_t crc, const unsigned char *_Nonnull bytes, +extern uint32_t of_crc32(uint32_t crc, const void *_Nonnull bytes, size_t length); #ifdef __cplusplus } #endif Index: src/crc32.m ================================================================== --- src/crc32.m +++ src/crc32.m @@ -20,16 +20,18 @@ #import "crc32.h" #define CRC32_MAGIC 0xEDB88320 uint32_t -of_crc32(uint32_t crc, const unsigned char *bytes, size_t length) +of_crc32(uint32_t crc, const void *bytes_, size_t length) { + const unsigned char *bytes = bytes_; + for (size_t i = 0; i < length; i++) { crc ^= bytes[i]; for (uint8_t j = 0; j < 8; j++) crc = (crc >> 1) ^ (CRC32_MAGIC & (~(crc & 1) + 1)); } return crc; } Index: utils/ofzip/LHAArchive.m ================================================================== --- utils/ofzip/LHAArchive.m +++ utils/ofzip/LHAArchive.m @@ -134,14 +134,18 @@ [of_stdout writeLine: OF_LOCALIZED(@"list_date", @"Date: %[date]", @"date", date)]; if ([entry mode] != nil) { + OFString *modeString = [OFString + stringWithFormat: + @"%" PRIo16, [[entry mode] uInt16Value]]; + [of_stdout writeString: @"\t"]; [of_stdout writeLine: OF_LOCALIZED(@"list_mode", @"Mode: %[mode]", - @"mode", [entry mode])]; + @"mode", modeString)]; } if ([entry UID] != nil) { [of_stdout writeString: @"\t"]; [of_stdout writeLine: OF_LOCALIZED(@"list_uid", @"UID: %[uid]",