Index: src/OFTarArchive.h ================================================================== --- src/OFTarArchive.h +++ src/OFTarArchive.h @@ -19,11 +19,10 @@ OF_ASSUME_NONNULL_BEGIN @class OFString; @class OFStream; -@class OFTarArchive_FileReadStream; /*! * @class OFTarArchive OFTarArchive.h ObjFW/OFTarArchive.h * * @brief A class for accessing and manipulating tar archives. @@ -34,20 +33,21 @@ enum { OF_TAR_ARCHIVE_MODE_READ, OF_TAR_ARCHIVE_MODE_WRITE, OF_TAR_ARCHIVE_MODE_APPEND } _mode; - OFTarArchive_FileReadStream *_lastReturnedStream; + OF_KINDOF(OFStream *) _lastReturnedStream; } /*! * @brief Creates a new OFTarArchive object with the specified stream. * - * @param stream A stream from which the tar archive will be read + * @param stream A stream from which the tar archive will be read. + * For append mode, this needs to be a seekable stream. * @param mode The mode for the tar file. Valid modes are "r" for reading, * "w" for creating a new file and "a" for appending to an existing - * file. + * archive. * @return A new, autoreleased OFTarArchive */ + (instancetype)archiveWithStream: (OFStream *)stream mode: (OFString *)mode; @@ -56,11 +56,11 @@ * @brief Creates a new OFTarArchive object with the specified file. * * @param path The path to the tar archive * @param mode The mode for the tar file. Valid modes are "r" for reading, * "w" for creating a new file and "a" for appending to an existing - * file. + * archive. * @return A new, autoreleased OFTarArchive */ + (instancetype)archiveWithPath: (OFString *)path mode: (OFString *)mode; #endif @@ -67,14 +67,15 @@ /*! * @brief Initializes an already allocated OFTarArchive object with the * specified stream. * - * @param stream A stream from which the tar archive will be read + * @param stream A stream from which the tar archive will be read. + * For append mode, this needs to be a seekable stream. * @param mode The mode for the tar file. Valid modes are "r" for reading, * "w" for creating a new file and "a" for appending to an existing - * file. + * archive. * @return An initialized OFTarArchive */ - initWithStream: (OFStream *)stream mode: (OFString *)mode OF_DESIGNATED_INITIALIZER; @@ -84,11 +85,11 @@ * specified file. * * @param path The path to the tar archive * @param mode The mode for the tar file. Valid modes are "r" for reading, * "w" for creating a new file and "a" for appending to an existing - * file. + * archive. * @return An initialized OFTarArchive */ - initWithPath: (OFString *)path mode: (OFString *)mode; #endif @@ -98,12 +99,14 @@ * have been read. * * This is only available in read mode. * * @warning Calling @ref nextEntry will invalidate all streams returned by - * @ref streamForReadingCurrentEntry entry! 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 tar archive or `nil` if all entries have * been read */ - (OFTarArchiveEntry *)nextEntry; @@ -112,8 +115,27 @@ * @brief Returns a stream for reading the current entry. * * @return A stream for reading the current entry */ - (OFStream *)streamForReadingCurrentEntry; + +/*! + * @brief Returns a stream for writing the specified entry. + * + * @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: (OFTarArchiveEntry *)entry; + +/*! + * @brief Closes the OFTarArchive. + */ +- (void)close; @end OF_ASSUME_NONNULL_END Index: src/OFTarArchive.m ================================================================== --- src/OFTarArchive.m +++ src/OFTarArchive.m @@ -13,35 +13,66 @@ * LICENSE.GPLv2 or LICENSE.GPLv3 respectively included in the packaging of this * file. */ #include "config.h" + +#include #import "OFTarArchive.h" #import "OFTarArchiveEntry.h" #import "OFTarArchiveEntry+Private.h" #import "OFStream.h" +#import "OFDate.h" #ifdef OF_HAVE_FILES # import "OFFile.h" #endif #import "OFInvalidArgumentException.h" #import "OFInvalidFormatException.h" #import "OFNotOpenException.h" +#import "OFOutOfRangeException.h" +#import "OFTruncatedDataException.h" +#import "OFWriteFailedException.h" @interface OFTarArchive_FileReadStream: OFStream { - OFStream *_stream; OFTarArchiveEntry *_entry; - size_t _toRead; + OFStream *_stream; + uint64_t _toRead; bool _atEndOfStream; } - initWithEntry: (OFTarArchiveEntry *)entry stream: (OFStream *)stream; - (void)of_skip; @end + +@interface OFTarArchive_FileWriteStream: OFStream +{ + OFTarArchiveEntry *_entry; + OFStream *_stream; + uint64_t _toWrite; +} + +- initWithEntry: (OFTarArchiveEntry *)entry + stream: (OFStream *)stream; +@end + +static void +stringToBuffer(unsigned char *buffer, OFString *string, size_t length) +{ + size_t UTF8StringLength = [string UTF8StringLength]; + + if (UTF8StringLength > length) + @throw [OFOutOfRangeException exception]; + + memcpy(buffer, [string UTF8String], UTF8StringLength); + + for (size_t i = UTF8StringLength; i < length; i++) + buffer[i] = '\0'; +} @implementation OFTarArchive: OFObject + (instancetype)archiveWithStream: (OFStream *)stream mode: (OFString *)mode { @@ -66,12 +97,42 @@ @try { _stream = [stream retain]; if ([mode isEqual: @"r"]) _mode = OF_TAR_ARCHIVE_MODE_READ; + else if ([mode isEqual: @"w"]) + _mode = OF_TAR_ARCHIVE_MODE_WRITE; + else if ([mode isEqual: @"a"]) + _mode = OF_TAR_ARCHIVE_MODE_APPEND; else @throw [OFInvalidArgumentException exception]; + + if (_mode == OF_TAR_ARCHIVE_MODE_APPEND) { + union { + char c[1024]; + uint32_t u32[1024 / sizeof(uint32_t)]; + } buffer; + bool empty = true; + + if (![_stream isKindOfClass: [OFSeekableStream class]]) + @throw [OFInvalidArgumentException exception]; + + [(OFSeekableStream *)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]; + + [(OFSeekableStream *)stream seekToOffset: -1024 + whence: SEEK_END]; + } } @catch (id e) { [self release]; @throw e; } @@ -80,12 +141,19 @@ #ifdef OF_HAVE_FILES - initWithPath: (OFString *)path mode: (OFString *)mode { - OFFile *file = [[OFFile alloc] initWithPath: path + OFFile *file; + + if ([mode isEqual: @"a"]) + file = [[OFFile alloc] initWithPath: path + mode: @"r+"]; + else + file = [[OFFile alloc] initWithPath: path mode: mode]; + @try { self = [self initWithStream: file mode: mode]; } @finally { [file release]; @@ -95,12 +163,11 @@ } #endif - (void)dealloc { - [_stream release]; - [_lastReturnedStream release]; + [self close]; [super dealloc]; } - (OFTarArchiveEntry *)nextEntry @@ -151,12 +218,100 @@ return entry; } - (OFStream *)streamForReadingCurrentEntry { + if (_mode != OF_TAR_ARCHIVE_MODE_READ) + @throw [OFInvalidArgumentException exception]; + + return [[_lastReturnedStream retain] autorelease]; +} + +- (OFStream *)streamForWritingEntry: (OFTarArchiveEntry *)entry +{ + void *pool; + uint64_t modificationDate; + unsigned char buffer[512]; + uint16_t checksum = 0; + + if (_mode != OF_TAR_ARCHIVE_MODE_WRITE && + _mode != OF_TAR_ARCHIVE_MODE_APPEND) + @throw [OFInvalidArgumentException exception]; + + pool = objc_autoreleasePoolPush(); + + [_lastReturnedStream close]; + [_lastReturnedStream release]; + _lastReturnedStream = nil; + + stringToBuffer(buffer, [entry fileName], 100); + stringToBuffer(buffer + 100, + [OFString stringWithFormat: @"%06" PRIo32 " ", [entry mode]], 8); + memcpy(buffer + 108, "000000 \0" "000000 \0", 16); + stringToBuffer(buffer + 124, + [OFString stringWithFormat: @"%011" PRIo64 " ", [entry size]], 12); + modificationDate = [[entry modificationDate] timeIntervalSince1970]; + stringToBuffer(buffer + 136, + [OFString stringWithFormat: @"%011" PRIo64 " ", modificationDate], + 12); + + /* + * During checksumming, the checksum field is expected to be set to 8 + * spaces. + */ + memset(buffer + 148, ' ', 8); + + buffer[156] = [entry type]; + stringToBuffer(buffer + 157, [entry targetFileName], 100); + + /* ustar */ + memcpy(buffer + 257, "ustar\0" "00", 8); + stringToBuffer(buffer + 265, [entry owner], 32); + stringToBuffer(buffer + 297, [entry group], 32); + stringToBuffer(buffer + 329, + [OFString stringWithFormat: @"%06" PRIo32 " ", [entry deviceMajor]], + 8); + stringToBuffer(buffer + 337, + [OFString stringWithFormat: @"%06" PRIo32 " ", [entry deviceMinor]], + 8); + memset(buffer + 345, '\0', 155 + 12); + + /* Fill in the checksum */ + for (size_t i = 0; i < 500; i++) + checksum += buffer[i]; + stringToBuffer(buffer + 148, + [OFString stringWithFormat: @"%06" PRIo16, checksum], 7); + + [_stream writeBuffer: buffer + length: sizeof(buffer)]; + + _lastReturnedStream = [[OFTarArchive_FileWriteStream alloc] + initWithEntry: entry + stream: _stream]; + + objc_autoreleasePoolPop(pool); + return [[_lastReturnedStream retain] autorelease]; } + +- (void)close +{ + [_lastReturnedStream close]; + [_lastReturnedStream release]; + _lastReturnedStream = nil; + + if (_mode == OF_TAR_ARCHIVE_MODE_WRITE || + _mode == OF_TAR_ARCHIVE_MODE_APPEND) { + char buffer[1024]; + memset(buffer, '\0', 1024); + [_stream writeBuffer: buffer + length: 1024]; + } + + [_stream release]; + _stream = nil; +} @end @implementation OFTarArchive_FileReadStream - initWithEntry: (OFTarArchiveEntry *)entry stream: (OFStream *)stream @@ -219,10 +374,15 @@ - (bool)hasDataInReadBuffer { return ([super hasDataInReadBuffer] || [_stream hasDataInReadBuffer]); } + +- (int)fileDescriptorForReading +{ + return [_stream fileDescriptorForReading]; +} - (void)close { [_stream release]; _stream = nil; @@ -252,5 +412,91 @@ if (size % 512 != 0) [_stream readIntoBuffer: buffer exactLength: 512 - ((size_t)size % 512)]; } @end + +@implementation OFTarArchive_FileWriteStream +- initWithEntry: (OFTarArchiveEntry *)entry + stream: (OFStream *)stream +{ + self = [super init]; + + @try { + _entry = [entry copy]; + _stream = [stream retain]; + _toWrite = [entry size]; + } @catch (id e) { + [self release]; + @throw e; + } + + return self; +} + +- (void)dealloc +{ + [self close]; + + [_entry release]; + + [super dealloc]; +} + +- (void)lowlevelWriteBuffer: (const void *)buffer + length: (size_t)length +{ + if (_stream == nil) + @throw [OFNotOpenException exceptionWithObject: self]; + + if ((uint64_t)length > _toWrite) + @throw [OFOutOfRangeException exception]; + + @try { + [_stream writeBuffer: buffer + length: length]; + } @catch (OFWriteFailedException *e) { + _toWrite -= [e bytesWritten]; + @throw e; + } + + _toWrite -= length; +} + +- (bool)lowlevelIsAtEndOfStream +{ + if (_stream == nil) + @throw [OFNotOpenException exceptionWithObject: self]; + + return (_toWrite == 0); +} + +- (int)fileDescriptorForWriting +{ + return [_stream fileDescriptorForWriting]; +} + +- (void)close +{ + uint64_t remainder = 512 - [_entry size] % 512; + + if (_toWrite > 0) + @throw [OFTruncatedDataException exception]; + + if (remainder != 512) { + bool wasWriteBuffered = [_stream isWriteBuffered]; + + [_stream setWriteBuffered: true]; + + while (remainder--) + [_stream writeInt8: 0]; + + [_stream flushWriteBuffer]; + [_stream setWriteBuffered: wasWriteBuffered]; + } + + [_stream release]; + _stream = nil; + + [super close]; +} +@end Index: src/OFTarArchiveEntry.m ================================================================== --- src/OFTarArchiveEntry.m +++ src/OFTarArchiveEntry.m @@ -114,10 +114,12 @@ { self = [super init]; @try { _fileName = [fileName copy]; + _type = OF_TAR_ARCHIVE_ENTRY_TYPE_FILE; + _mode = 0644; } @catch (id e) { [self release]; @throw e; } Index: src/OFZIPArchive.h ================================================================== --- src/OFZIPArchive.h +++ src/OFZIPArchive.h @@ -59,11 +59,11 @@ * @brief Creates a new OFZIPArchive object with the specified seekable stream. * * @param stream A seekable stream from which the ZIP archive will be read * @param mode The mode for the ZIP file. Valid modes are "r" for reading, * "w" for creating a new file and "a" for appending to an existing - * file. + * archive. * @return A new, autoreleased OFZIPArchive */ + (instancetype)archiveWithSeekableStream: (OFSeekableStream *)stream mode: (OFString *)mode; @@ -72,11 +72,11 @@ * @brief Creates a new OFZIPArchive object with the specified file. * * @param path The path to the ZIP file * @param mode The mode for the ZIP file. Valid modes are "r" for reading, * "w" for creating a new file and "a" for appending to an existing - * file. + * archive. * @return A new, autoreleased OFZIPArchive */ + (instancetype)archiveWithPath: (OFString *)path mode: (OFString *)mode; #endif @@ -88,11 +88,11 @@ * specified seekable stream. * * @param stream A seekable stream from which the ZIP archive will be read * @param mode The mode for the ZIP file. Valid modes are "r" for reading, * "w" for creating a new file and "a" for appending to an existing - * file. + * archive. * @return An initialized OFZIPArchive */ - initWithSeekableStream: (OFSeekableStream *)stream mode: (OFString *)mode OF_DESIGNATED_INITIALIZER; @@ -102,11 +102,11 @@ * specified file. * * @param path The path to the ZIP file * @param mode The mode for the ZIP file. Valid modes are "r" for reading, * "w" for creating a new file and "a" for appending to an existing - * file. + * archive. * @return An initialized OFZIPArchive */ - initWithPath: (OFString *)path mode: (OFString *)mode; #endif