Permalink
Browse files

Improve error handling for writing (ref #9)

* Replace writeable entries with -[ZZMutableArchive updateEntries:error:] method.

* Stream, data and data consumer blocks now should report errors.

* Revise error codes to be consistent with write errors.

* Internal API now report file manager, POSIX and stream errors.

* Unit tests for creating or inserting erroneous entries.
  • Loading branch information...
1 parent b257d81 commit 74daa74a8c02f7cd0a76ab3526eb498986adb35c @pixelglow committed Feb 2, 2013
View
@@ -50,7 +50,7 @@
/**
* The array of <ZZArchiveEntry> entries within this archive.
*/
-@property (readonly, copy, nonatomic) NSArray* entries;
+@property (readonly, nonatomic) NSArray* entries;
/**
* Creates a new archive with the zip file at the given file URL.
@@ -111,10 +111,15 @@
@interface ZZMutableArchive : ZZArchive
/**
- * The array of <ZZArchiveEntry> entries within this archive.
- * To write new entries in the zip file, set this property to a different array of <ZZArchiveEntry> entries.
- * When you set this property, any old entries should be considered invalid.
+ * Updates the entries and writes them to the source.
+ *
+ * @param newEntries The entries to update to, may contain some or all existing entries.
+ * @param error The error information when an error occurs. Pass in *nil* if you do not want error information.
+ * @return Whether the update was successful or not.
+ *
+ * @remarks If the write fails and the entries are completely new, the existing zip file will be untouched. Instead, if the write fails and the entries contain some or all existing entries, the zip file may be corrupted. In this case, the error information will report the ZZReplaceWriteErrorCode error code.
*/
-@property (copy, nonatomic) NSArray* entries;
+- (BOOL)updateEntries:(NSArray*)newEntries
+ error:(NSError**)error;
@end
View
@@ -94,7 +94,7 @@ - (BOOL)load:(NSError**)error
NSError* __autoreleasing readError;
NSData* contents = [_channel openInput:&readError];
if (!contents)
- return ZZRaiseError(error, ZZReadErrorCode, @{NSUnderlyingErrorKey : readError});
+ return ZZRaiseError(error, ZZOpenReadErrorCode, @{NSUnderlyingErrorKey : readError});
@mikeabdullah
mikeabdullah Feb 2, 2013 Contributor

Is there any advantage to reporting your own error code here? Cocoa provides perfectly decent errors itself

@pixelglow
pixelglow Feb 2, 2013 Owner

Having your own errors is useful to report what stage the read is in, or even the failed index in particular cases. In any case the underlying Cocoa or POSIX error is also reported.

// search for the end of directory signature in last 64K of file
const uint8_t* beginContent = (const uint8_t*)contents.bytes;
@@ -125,7 +125,7 @@ - (BOOL)load:(NSError**)error
// end of central directory occurs at actual end of the zip
|| endContent
!= endOfCentralDirectory + sizeof(ZZEndOfCentralDirectory) + endOfCentralDirectoryRecord->zipFileCommentLength)
- return ZZRaiseError(error, ZZBadEndOfCentralDirectoryErrorCode, nil);
+ return ZZRaiseError(error, ZZEndOfCentralDirectoryReadErrorCode, nil);
// add an entry for each central header in the sequence
ZZCentralFileHeader* nextCentralFileHeader = (ZZCentralFileHeader*)(beginContent
@@ -142,7 +142,7 @@ - (BOOL)load:(NSError**)error
// local file occurs before first central file header, and has enough minimal space for at least local file
|| nextCentralFileHeader->relativeOffsetOfLocalHeader + sizeof(ZZLocalFileHeader)
> endOfCentralDirectoryRecord->offsetOfStartOfCentralDirectoryWithRespectToTheStartingDiskNumber)
- return ZZRaiseError(error, ZZBadCentralFileErrorCode, @{ZZEntryIndexKey : @(index)});
+ return ZZRaiseError(error, ZZCentralFileHeaderReadErrorCode, @{ZZEntryIndexKey : @(index)});
ZZLocalFileHeader* nextLocalFileHeader = (ZZLocalFileHeader*)(beginContent
+ nextCentralFileHeader->relativeOffsetOfLocalHeader);
@@ -164,7 +164,8 @@ - (BOOL)load:(NSError**)error
@implementation ZZMutableArchive
-- (void)setEntries:(NSArray*)newEntries
+- (BOOL)updateEntries:(NSArray*)newEntries
+ error:(NSError**)error
{
// NOTE: we want to avoid loading at all when entries are being overwritten, even in the face of lazy loading:
// consider that nil _contents implies that no valid entries have been loaded, and newEntries cannot possibly contain any of our old entries
@@ -182,10 +183,9 @@ - (void)setEntries:(NSArray*)newEntries
// get an entry writer for each new entry
NSMutableArray* newEntryWriters = [NSMutableArray array];
- [newEntries enumerateObjectsUsingBlock:^(ZZArchiveEntry *anEntry, NSUInteger idx, BOOL *stop)
+ [newEntries enumerateObjectsUsingBlock:^(ZZArchiveEntry *anEntry, NSUInteger index, BOOL* stop)
{
-
- [newEntryWriters addObject:[anEntry writerCanSkipLocalFile:(idx < skipIndex)]];
+ [newEntryWriters addObject:[anEntry writerCanSkipLocalFile:index < skipIndex]];
}];
// clear entries + content
@@ -195,59 +195,98 @@ - (void)setEntries:(NSArray*)newEntries
// skip the initial matching entries
uint32_t initialSkip = skipIndex > 0 ? [[newEntryWriters objectAtIndex:skipIndex - 1] offsetToLocalFileEnd] : 0;
+ NSError* __autoreleasing underlyingError;
+
// create a temp channel for all output
- id<ZZChannel> temporaryChannel = [_channel temporaryChannel];
- id<ZZChannelOutput> temporaryChannelOutput = [temporaryChannel openOutputWithOffsetBias:initialSkip];
-
-
- // write out local files, recording which are valid
- NSMutableIndexSet* goodEntries = [NSMutableIndexSet indexSetWithIndexesInRange:NSMakeRange(0, newEntries.count)];
-
- [newEntryWriters enumerateObjectsUsingBlock:^(id <ZZArchiveEntryWriter> entryWriter, NSUInteger idx, BOOL *stop)
- {
- if (idx >= skipIndex && ![entryWriter writeLocalFileToChannelOutput:temporaryChannelOutput])
- [goodEntries removeIndex:idx];
- }];
-
- ZZEndOfCentralDirectory endOfCentralDirectory;
- endOfCentralDirectory.signature = ZZEndOfCentralDirectory::sign;
- endOfCentralDirectory.numberOfThisDisk
- = endOfCentralDirectory.numberOfTheDiskWithTheStartOfTheCentralDirectory
- = 0;
- endOfCentralDirectory.totalNumberOfEntriesInTheCentralDirectoryOnThisDisk
- = endOfCentralDirectory.totalNumberOfEntriesInTheCentralDirectory
- = goodEntries.count;
- endOfCentralDirectory.offsetOfStartOfCentralDirectoryWithRespectToTheStartingDiskNumber = temporaryChannelOutput.offset;
-
- // write out central file headers
- [newEntryWriters enumerateObjectsAtIndexes:goodEntries options:0 usingBlock:^(id <ZZArchiveEntryWriter> anEntryWriter, NSUInteger idx, BOOL *stop)
- {
- [anEntryWriter writeCentralFileHeaderToChannelOutput:temporaryChannelOutput];
- }];
-
- endOfCentralDirectory.sizeOfTheCentralDirectory = temporaryChannelOutput.offset
- - endOfCentralDirectory.offsetOfStartOfCentralDirectoryWithRespectToTheStartingDiskNumber;
- endOfCentralDirectory.zipFileCommentLength = 0;
+ id<ZZChannel> temporaryChannel = [_channel temporaryChannel:&underlyingError];
+ if (!temporaryChannel)
+ return ZZRaiseError(error, ZZOpenWriteErrorCode, @{NSUnderlyingErrorKey : underlyingError});
- // write out the end of central directory
- [temporaryChannelOutput write:[NSData dataWithBytesNoCopy:&endOfCentralDirectory
- length:sizeof(endOfCentralDirectory)
- freeWhenDone:NO]];
- [temporaryChannelOutput close];
-
- if (initialSkip)
+ @try
{
- // something skipped, append the temporary channel contents at the skipped offset
- id<ZZChannelOutput> channelOutput = [_channel openOutputWithOffsetBias:0];
- channelOutput.offset = initialSkip;
- [channelOutput write:[temporaryChannel openInput:nil]];
- [channelOutput close];
+ // open the channel
+ id<ZZChannelOutput> temporaryChannelOutput = [temporaryChannel openOutputWithOffsetBias:initialSkip
+ error:&underlyingError];
+ if (!temporaryChannelOutput)
+ return ZZRaiseError(error, ZZOpenWriteErrorCode, @{NSUnderlyingErrorKey : underlyingError});
+
+ @try
+ {
+ // write out local files
+ for (NSUInteger index = skipIndex; index < newEntriesCount; ++index)
+ if (![[newEntryWriters objectAtIndex:index] writeLocalFileToChannelOutput:temporaryChannelOutput
+ error:&underlyingError])
+ return ZZRaiseError(error, ZZLocalFileWriteErrorCode, @{NSUnderlyingErrorKey : underlyingError, ZZEntryIndexKey : @(index)});
+
+ ZZEndOfCentralDirectory endOfCentralDirectory;
+ endOfCentralDirectory.signature = ZZEndOfCentralDirectory::sign;
+ endOfCentralDirectory.numberOfThisDisk
+ = endOfCentralDirectory.numberOfTheDiskWithTheStartOfTheCentralDirectory
+ = 0;
+ endOfCentralDirectory.totalNumberOfEntriesInTheCentralDirectoryOnThisDisk
+ = endOfCentralDirectory.totalNumberOfEntriesInTheCentralDirectory
+ = newEntriesCount;
+ endOfCentralDirectory.offsetOfStartOfCentralDirectoryWithRespectToTheStartingDiskNumber = [temporaryChannelOutput offset];
+
+ // write out central file headers
+ for (NSUInteger index = 0; index < newEntriesCount; ++index)
+ if (![[newEntryWriters objectAtIndex:index] writeCentralFileHeaderToChannelOutput:temporaryChannelOutput
+ error:&underlyingError])
+ return ZZRaiseError(error, ZZCentralFileHeaderWriteErrorCode, @{NSUnderlyingErrorKey : underlyingError, ZZEntryIndexKey : @(index)});
+
+ endOfCentralDirectory.sizeOfTheCentralDirectory = [temporaryChannelOutput offset]
+ - endOfCentralDirectory.offsetOfStartOfCentralDirectoryWithRespectToTheStartingDiskNumber;
+ endOfCentralDirectory.zipFileCommentLength = 0;
+
+ // write out the end of central directory
+ if (![temporaryChannelOutput writeData:[NSData dataWithBytesNoCopy:&endOfCentralDirectory
+ length:sizeof(endOfCentralDirectory)
+ freeWhenDone:NO]
+ error:&underlyingError])
+ return ZZRaiseError(error, ZZEndOfCentralDirectoryWriteErrorCode, @{NSUnderlyingErrorKey : underlyingError});
+ }
+ @finally
+ {
+ [temporaryChannelOutput close];
+ }
+
+ if (initialSkip)
+ {
+ // something skipped, append the temporary channel contents at the skipped offset
+ id<ZZChannelOutput> channelOutput = [_channel openOutputWithOffsetBias:0
+ error:&underlyingError];
+ if (!channelOutput)
+ return ZZRaiseError(error, ZZReplaceWriteErrorCode, @{NSUnderlyingErrorKey : underlyingError});
+
+ @try
+ {
+ NSData* channelInput = [temporaryChannel openInput:&underlyingError];
+ if (!channelInput
+ || ![channelOutput seekToOffset:initialSkip
+ error:&underlyingError]
+ || ![channelOutput writeData:channelInput
+ error:&underlyingError]
+ || ![channelOutput truncateAtOffset:[channelOutput offset]
+ error:&underlyingError])
+ return ZZRaiseError(error, ZZReplaceWriteErrorCode, @{NSUnderlyingErrorKey : underlyingError});
+ }
+ @finally
+ {
+ [channelOutput close];
+ }
+ }
+ else
+ // nothing skipped, temporary channel is entire contents: simply replace the original
+ if (![_channel replaceWithChannel:temporaryChannel
+ error:&underlyingError])
+ return ZZRaiseError(error, ZZReplaceWriteErrorCode, @{NSUnderlyingErrorKey : underlyingError});
+ }
+ @finally
+ {
+ [_channel removeTemporaries];
}
- else
- // nothing skipped, temporary channel is entire contents: simply replace the original
- [_channel replaceWithChannel:temporaryChannel];
- [_channel removeTemporaries];
+ return YES;
}
@end
@@ -90,7 +90,7 @@
*/
+ (instancetype)archiveEntryWithFileName:(NSString*)fileName
compress:(BOOL)compress
- streamBlock:(BOOL(^)(NSOutputStream* stream))streamBlock;
+ streamBlock:(BOOL(^)(NSOutputStream* stream, NSError** error))streamBlock;
/**
* Creates a new file entry from a data callback.
@@ -102,7 +102,7 @@
*/
+ (instancetype)archiveEntryWithFileName:(NSString*)fileName
compress:(BOOL)compress
- dataBlock:(NSData*(^)())dataBlock;
+ dataBlock:(NSData*(^)(NSError** error))dataBlock;
/**
* Creates a new file entry from a data-consuming callback.
@@ -114,7 +114,7 @@
*/
+ (instancetype)archiveEntryWithFileName:(NSString*)fileName
compress:(BOOL)compress
- dataConsumerBlock:(BOOL(^)(CGDataConsumerRef dataConsumer))dataConsumerBlock;
+ dataConsumerBlock:(BOOL(^)(CGDataConsumerRef dataConsumer, NSError** error))dataConsumerBlock;
/**
* Creates a new directory entry.
@@ -142,9 +142,9 @@
fileMode:(mode_t)fileMode
lastModified:(NSDate*)lastModified
compressionLevel:(NSInteger)compressionLevel
- dataBlock:(NSData*(^)())dataBlock
- streamBlock:(BOOL(^)(NSOutputStream* stream))streamBlock
- dataConsumerBlock:(BOOL(^)(CGDataConsumerRef dataConsumer))dataConsumerBlock;
+ dataBlock:(NSData*(^)(NSError** error))dataBlock
+ streamBlock:(BOOL(^)(NSOutputStream* stream, NSError** error))streamBlock
+ dataConsumerBlock:(BOOL(^)(CGDataConsumerRef dataConsumer, NSError** error))dataConsumerBlock;
/**
* Checks whether the entry file is consistent.
@@ -15,7 +15,7 @@ @implementation ZZArchiveEntry
+ (instancetype)archiveEntryWithFileName:(NSString*)fileName
compress:(BOOL)compress
- dataBlock:(NSData*(^)())dataBlock
+ dataBlock:(NSData*(^)(NSError** error))dataBlock
{
return [self archiveEntryWithFileName:fileName
fileMode:S_IFREG | S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH
@@ -28,7 +28,7 @@ + (instancetype)archiveEntryWithFileName:(NSString*)fileName
+ (instancetype)archiveEntryWithFileName:(NSString*)fileName
compress:(BOOL)compress
- streamBlock:(BOOL(^)(NSOutputStream* stream))streamBlock
+ streamBlock:(BOOL(^)(NSOutputStream* stream, NSError** error))streamBlock
{
return [self archiveEntryWithFileName:fileName
fileMode:S_IFREG | S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH
@@ -41,7 +41,7 @@ + (instancetype)archiveEntryWithFileName:(NSString*)fileName
+ (instancetype)archiveEntryWithFileName:(NSString*)fileName
compress:(BOOL)compress
- dataConsumerBlock:(BOOL(^)(CGDataConsumerRef dataConsumer))dataConsumerBlock
+ dataConsumerBlock:(BOOL(^)(CGDataConsumerRef dataConsumer, NSError** error))dataConsumerBlock
{
return [self archiveEntryWithFileName:fileName
fileMode:S_IFREG | S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH
@@ -67,9 +67,9 @@ + (instancetype)archiveEntryWithFileName:(NSString*)fileName
fileMode:(mode_t)fileMode
lastModified:(NSDate*)lastModified
compressionLevel:(NSInteger)compressionLevel
- dataBlock:(NSData*(^)())dataBlock
- streamBlock:(BOOL(^)(NSOutputStream* stream))streamBlock
- dataConsumerBlock:(BOOL(^)(CGDataConsumerRef dataConsumer))dataConsumerBlock
+ dataBlock:(NSData*(^)(NSError** error))dataBlock
+ streamBlock:(BOOL(^)(NSOutputStream* stream, NSError** error))streamBlock
+ dataConsumerBlock:(BOOL(^)(CGDataConsumerRef dataConsumer, NSError** error))dataConsumerBlock
{
return [[ZZNewArchiveEntry alloc] initWithFileName:fileName
fileMode:fileMode
@@ -13,7 +13,8 @@
@protocol ZZArchiveEntryWriter
- (uint32_t)offsetToLocalFileEnd;
-- (BOOL)writeLocalFileToChannelOutput:(id<ZZChannelOutput>)channelOutput;
-- (void)writeCentralFileHeaderToChannelOutput:(id<ZZChannelOutput>)channelOutput;
-
+- (BOOL)writeLocalFileToChannelOutput:(id<ZZChannelOutput>)channelOutput
+ error:(NSError**)error;
+- (BOOL)writeCentralFileHeaderToChannelOutput:(id<ZZChannelOutput>)channelOutput
+ error:(NSError**)error;
@end
View
@@ -14,11 +14,13 @@
@property (readonly, nonatomic) NSURL* URL;
-- (instancetype)temporaryChannel;
-- (BOOL)replaceWithChannel:(id<ZZChannel>)channel;
+- (instancetype)temporaryChannel:(NSError**)error;
+- (BOOL)replaceWithChannel:(id<ZZChannel>)channel
+ error:(NSError**)error;
- (void)removeTemporaries;
- (NSData*)openInput:(NSError**)error;
-- (id<ZZChannelOutput>)openOutputWithOffsetBias:(uint32_t)offsetBias;
+- (id<ZZChannelOutput>)openOutputWithOffsetBias:(uint32_t)offsetBias
+ error:(NSError**)error;
@end
@@ -10,9 +10,14 @@
@protocol ZZChannelOutput
-@property (nonatomic) uint32_t offset;
+- (uint32_t)offset;
+- (BOOL)seekToOffset:(uint32_t)offset
+ error:(NSError**)error;
-- (void)write:(NSData*)data;
+- (BOOL)writeData:(NSData*)data
+ error:(NSError**)error;
+- (BOOL)truncateAtOffset:(uint32_t)offset
+ error:(NSError**)error;
- (void)close;
@end
@@ -16,11 +16,13 @@
- (id)initWithData:(NSData*)data;
-- (instancetype)temporaryChannel;
-- (BOOL)replaceWithChannel:(id<ZZChannel>)channel;
+- (instancetype)temporaryChannel:(NSError**)error;
+- (BOOL)replaceWithChannel:(id<ZZChannel>)channel
+ error:(NSError**)error;
- (void)removeTemporaries;
- (NSData*)openInput:(NSError**)error;
-- (id<ZZChannelOutput>)openOutputWithOffsetBias:(uint32_t)offsetBias;
+- (id<ZZChannelOutput>)openOutputWithOffsetBias:(uint32_t)offsetBias
+ error:(NSError**)error;
@end
@@ -26,12 +26,13 @@ - (NSURL*)URL
return nil;
}
-- (instancetype)temporaryChannel
+- (instancetype)temporaryChannel:(NSError**)error
{
return [[ZZDataChannel alloc] initWithData:[NSMutableData data]];
}
- (BOOL)replaceWithChannel:(id<ZZChannel>)channel
+ error:(NSError**)error
{
[(NSMutableData*)_allData setData:((ZZDataChannel*)channel)->_allData];
return YES;
@@ -47,6 +48,7 @@ - (NSData*)openInput:(NSError**)error
}
- (id<ZZChannelOutput>)openOutputWithOffsetBias:(uint32_t)offsetBias
+ error:(NSError**)error
{
return [[ZZDataChannelOutput alloc] initWithData:(NSMutableData*)_allData
offsetBias:offsetBias];
Oops, something went wrong.

0 comments on commit 74daa74

Please sign in to comment.