diff --git a/Autoupdate/SPUDeltaArchive.m b/Autoupdate/SPUDeltaArchive.m index ca7ade75a..e2f1e40b8 100644 --- a/Autoupdate/SPUDeltaArchive.m +++ b/Autoupdate/SPUDeltaArchive.m @@ -89,8 +89,9 @@ @implementation SPUDeltaArchiveHeader @synthesize fileSystemCompression = _fileSystemCompression; @synthesize majorVersion = _majorVersion; @synthesize minorVersion = _minorVersion; +@synthesize bundleCreationDate = _bundleCreationDate; -- (instancetype)initWithCompression:(SPUDeltaCompressionMode)compression compressionLevel:(uint8_t)compressionLevel fileSystemCompression:(bool)fileSystemCompression majorVersion:(uint16_t)majorVersion minorVersion:(uint16_t)minorVersion beforeTreeHash:(const unsigned char *)beforeTreeHash afterTreeHash:(const unsigned char *)afterTreeHash +- (instancetype)initWithCompression:(SPUDeltaCompressionMode)compression compressionLevel:(uint8_t)compressionLevel fileSystemCompression:(bool)fileSystemCompression majorVersion:(uint16_t)majorVersion minorVersion:(uint16_t)minorVersion beforeTreeHash:(const unsigned char *)beforeTreeHash afterTreeHash:(const unsigned char *)afterTreeHash bundleCreationDate:(nullable NSDate *)bundleCreationDate { self = [super init]; if (self != nil) @@ -104,6 +105,8 @@ - (instancetype)initWithCompression:(SPUDeltaCompressionMode)compression compres memcpy(_beforeTreeHash, beforeTreeHash, sizeof(_beforeTreeHash)); memcpy(_afterTreeHash, afterTreeHash, sizeof(_afterTreeHash)); + + _bundleCreationDate = bundleCreationDate; } return self; } diff --git a/Autoupdate/SPUDeltaArchiveProtocol.h b/Autoupdate/SPUDeltaArchiveProtocol.h index 606908c0e..f601266b1 100644 --- a/Autoupdate/SPUDeltaArchiveProtocol.h +++ b/Autoupdate/SPUDeltaArchiveProtocol.h @@ -26,7 +26,7 @@ typedef NS_ENUM(uint8_t, SPUDeltaItemCommands) { // Represents header for our archive SPU_OBJC_DIRECT_MEMBERS @interface SPUDeltaArchiveHeader : NSObject -- (instancetype)initWithCompression:(SPUDeltaCompressionMode)compression compressionLevel:(uint8_t)compressionLevel fileSystemCompression:(bool)fileSystemCompression majorVersion:(uint16_t)majorVersion minorVersion:(uint16_t)minorVersion beforeTreeHash:(const unsigned char *)beforeTreeHash afterTreeHash:(const unsigned char *)afterTreeHash; +- (instancetype)initWithCompression:(SPUDeltaCompressionMode)compression compressionLevel:(uint8_t)compressionLevel fileSystemCompression:(bool)fileSystemCompression majorVersion:(uint16_t)majorVersion minorVersion:(uint16_t)minorVersion beforeTreeHash:(const unsigned char *)beforeTreeHash afterTreeHash:(const unsigned char *)afterTreeHash bundleCreationDate:(nullable NSDate *)bundleCreationDate; @property (nonatomic, readonly) SPUDeltaCompressionMode compression; @property (nonatomic, readonly) uint8_t compressionLevel; @@ -35,6 +35,7 @@ SPU_OBJC_DIRECT_MEMBERS @interface SPUDeltaArchiveHeader : NSObject @property (nonatomic, readonly) uint16_t minorVersion; @property (nonatomic, readonly) unsigned char *beforeTreeHash; @property (nonatomic, readonly) unsigned char *afterTreeHash; +@property (nonatomic, readonly, nullable) NSDate *bundleCreationDate; @end diff --git a/Autoupdate/SPUSparkleDeltaArchive.m b/Autoupdate/SPUSparkleDeltaArchive.m index cb71c73c2..f85019843 100644 --- a/Autoupdate/SPUSparkleDeltaArchive.m +++ b/Autoupdate/SPUSparkleDeltaArchive.m @@ -348,7 +348,19 @@ - (nullable SPUDeltaArchiveHeader *)readHeader return nil; } - return [[SPUDeltaArchiveHeader alloc] initWithCompression:compression compressionLevel:metadata.compressionLevel fileSystemCompression:metadata.fileSystemCompression majorVersion:majorVersion minorVersion:minorVersion beforeTreeHash:beforeTreeHash afterTreeHash:afterTreeHash]; + NSDate *bundleCreationDate; + if (MAJOR_VERSION_IS_AT_LEAST(majorVersion, SUBinaryDeltaMajorVersion4)) { + double bundleCreationTimeInterval = 0; + if (![self _readBuffer:&bundleCreationTimeInterval length:sizeof(bundleCreationTimeInterval)]) { + return nil; + } + + bundleCreationDate = (bundleCreationTimeInterval != 0.0) ? [NSDate dateWithTimeIntervalSinceReferenceDate:bundleCreationTimeInterval] : nil; + } else { + bundleCreationDate = nil; + } + + return [[SPUDeltaArchiveHeader alloc] initWithCompression:compression compressionLevel:metadata.compressionLevel fileSystemCompression:metadata.fileSystemCompression majorVersion:majorVersion minorVersion:minorVersion beforeTreeHash:beforeTreeHash afterTreeHash:afterTreeHash bundleCreationDate:bundleCreationDate]; } - (NSArray *)_readRelativeFilePaths SPU_OBJC_DIRECT @@ -855,6 +867,14 @@ - (void)writeHeader:(SPUDeltaArchiveHeader *)header [self _writeBuffer:header.beforeTreeHash length:CC_SHA1_DIGEST_LENGTH]; [self _writeBuffer:header.afterTreeHash length:CC_SHA1_DIGEST_LENGTH]; + + if (MAJOR_VERSION_IS_AT_LEAST(majorVersion, SUBinaryDeltaMajorVersion4)) { + NSDate *bundleCreationDate = header.bundleCreationDate; + + // If bundleCreationDate == nil, we will write out a 0 time interval + double timeInterval = bundleCreationDate.timeIntervalSinceReferenceDate; + [self _writeBuffer:&timeInterval length:sizeof(timeInterval)]; + } } - (void)addItem:(SPUDeltaArchiveItem *)item @@ -883,7 +903,7 @@ - (void)finishEncodingItems // Clone commands reference relative file paths in this table but sometimes there may not // be an entry if extraction for an original item was skipped. Fill out any missing file path entries. - // For example, if A.app has Contents/A and B.app has Contents/A and Contents/A and Contents/B, + // For example, if A.app has Contents/A and B.app has Contents/A and Contents/B, // where A and B's contents are the same and A is the same in both apps, normally we would not record Contents/A because its extraction was skipped. However now B is a clone of A so we need a record for A. NSMutableArray *newClonedPathEntries = [NSMutableArray array]; for (SPUDeltaArchiveItem *item in writableItems) { diff --git a/Autoupdate/SPUXarDeltaArchive.m b/Autoupdate/SPUXarDeltaArchive.m index ab117230d..cb859c7ae 100644 --- a/Autoupdate/SPUXarDeltaArchive.m +++ b/Autoupdate/SPUXarDeltaArchive.m @@ -167,7 +167,7 @@ - (nullable SPUDeltaArchiveHeader *)readHeader // I wasn't able to figure out how to retrieve the compression options from xar, // so we will use default flags to indicate the info isn't available - return [[SPUDeltaArchiveHeader alloc] initWithCompression:SPUDeltaCompressionModeDefault compressionLevel:0 fileSystemCompression:false majorVersion:majorDiffVersion minorVersion:minorDiffVersion beforeTreeHash:rawExpectedBeforeHash afterTreeHash:rawExpectedAfterHash]; + return [[SPUDeltaArchiveHeader alloc] initWithCompression:SPUDeltaCompressionModeDefault compressionLevel:0 fileSystemCompression:false majorVersion:majorDiffVersion minorVersion:minorDiffVersion beforeTreeHash:rawExpectedBeforeHash afterTreeHash:rawExpectedAfterHash bundleCreationDate:nil]; } - (void)writeHeader:(SPUDeltaArchiveHeader *)header diff --git a/Autoupdate/SUBinaryDeltaApply.m b/Autoupdate/SUBinaryDeltaApply.m index 9e862f1ff..f3a456131 100644 --- a/Autoupdate/SUBinaryDeltaApply.m +++ b/Autoupdate/SUBinaryDeltaApply.m @@ -152,6 +152,16 @@ BOOL applyBinaryDelta(NSString *source, NSString *finalDestination, NSString *pa } return NO; } + + // Preserve file creation date only for the root item if the date is recorded + // (requires major version 4 or later) + NSDate *bundleCreationDate = header.bundleCreationDate; + if (bundleCreationDate != nil) { + NSError *setFileCreationDateError = nil; + if (![fileManager setAttributes:@{NSFileCreationDate: bundleCreationDate} ofItemAtPath:destination error:&setFileCreationDateError]) { + fprintf(stderr, "\nWarning: failed to set file creation date: %s", setFileCreationDateError.localizedDescription.UTF8String); + } + } progressCallback(4/7.0); diff --git a/Autoupdate/SUBinaryDeltaCommon.h b/Autoupdate/SUBinaryDeltaCommon.h index e70aa1ce4..e6187ccc8 100644 --- a/Autoupdate/SUBinaryDeltaCommon.h +++ b/Autoupdate/SUBinaryDeltaCommon.h @@ -44,7 +44,8 @@ typedef NS_ENUM(uint16_t, SUBinaryDeltaMajorVersion) // Note: support for creating or applying version 1 deltas have been removed SUBinaryDeltaMajorVersion1 = 1, SUBinaryDeltaMajorVersion2 = 2, - SUBinaryDeltaMajorVersion3 = 3 + SUBinaryDeltaMajorVersion3 = 3, + SUBinaryDeltaMajorVersion4 = 4, }; extern SUBinaryDeltaMajorVersion SUBinaryDeltaMajorVersionDefault; @@ -52,8 +53,8 @@ extern SUBinaryDeltaMajorVersion SUBinaryDeltaMajorVersionLatest; extern SUBinaryDeltaMajorVersion SUBinaryDeltaMajorVersionFirst; extern SUBinaryDeltaMajorVersion SUBinaryDeltaMajorVersionFirstSupported; -// Additional compression methods for version 3 patches that we have for debugging are zlib, bzip2, none -#define COMPRESSION_METHOD_ARGUMENT_DESCRIPTION @"The compression method to use for generating delta updates. Supported methods for version 3 delta files are 'lzma' (best compression, slowest), 'lzfse' (good compression, fast), 'lz4' (worse compression, fastest), and 'default'. Note that version 2 delta files only support 'bzip2', and 'default' so other methods will be ignored if version 2 files are being generated. The 'default' compression for version 3 delta files is currently lzma." +// Additional compression methods for version 3 or 4 patches that we have for debugging are zlib, bzip2, none +#define COMPRESSION_METHOD_ARGUMENT_DESCRIPTION @"The compression method to use for generating delta updates. Supported methods for version 3 delta files are 'lzma' (best compression, slowest), 'lzfse' (good compression, fast), 'lz4' (worse compression, fastest), and 'default'. Note that version 2 delta files only support 'bzip2', and 'default' so other methods will be ignored if version 2 files are being generated. The 'default' compression for version 3 or 4 delta files is currently lzma." //#define COMPRESSION_LEVEL_ARGUMENT_DESCRIPTION @"The compression level to use for generating delta updates. This only applies if the compression method used is bzip2 which accepts values from 1 - 9. A special value of 0 will use the default compression level." diff --git a/Autoupdate/SUBinaryDeltaCommon.m b/Autoupdate/SUBinaryDeltaCommon.m index d1c718c3f..0461b2d0f 100644 --- a/Autoupdate/SUBinaryDeltaCommon.m +++ b/Autoupdate/SUBinaryDeltaCommon.m @@ -22,7 +22,7 @@ // Note: the framework bundle version must be bumped, and generate_appcast must be updated to compare it, // when we add/change new major versions and defaults. Unit tests need to be updated to use new versions too. SUBinaryDeltaMajorVersion SUBinaryDeltaMajorVersionDefault = SUBinaryDeltaMajorVersion3; -SUBinaryDeltaMajorVersion SUBinaryDeltaMajorVersionLatest = SUBinaryDeltaMajorVersion3; +SUBinaryDeltaMajorVersion SUBinaryDeltaMajorVersionLatest = SUBinaryDeltaMajorVersion4; SUBinaryDeltaMajorVersion SUBinaryDeltaMajorVersionFirst = SUBinaryDeltaMajorVersion1; SUBinaryDeltaMajorVersion SUBinaryDeltaMajorVersionFirstSupported = SUBinaryDeltaMajorVersion2; @@ -115,6 +115,8 @@ uint16_t latestMinorVersionForMajorVersion(SUBinaryDeltaMajorVersion majorVersio return 4; case SUBinaryDeltaMajorVersion3: return 1; + case SUBinaryDeltaMajorVersion4: + return 0; } return 0; } diff --git a/Autoupdate/SUBinaryDeltaCreate.m b/Autoupdate/SUBinaryDeltaCreate.m index 925688c69..a94896334 100644 --- a/Autoupdate/SUBinaryDeltaCreate.m +++ b/Autoupdate/SUBinaryDeltaCreate.m @@ -723,7 +723,23 @@ BOOL createBinaryDelta(NSString *source, NSString *destination, NSString *patchF #endif } - SPUDeltaArchiveHeader *header = [[SPUDeltaArchiveHeader alloc] initWithCompression:compression compressionLevel:compressionLevel fileSystemCompression:foundFilesystemCompression majorVersion:majorVersion minorVersion:minorVersion beforeTreeHash:beforeHash afterTreeHash:afterHash]; + // Record creation date of root bundle item + NSDate *bundleCreationDate; + if (MAJOR_VERSION_IS_AT_LEAST(majorVersion, SUBinaryDeltaMajorVersion4)) { + NSError *fileAttributesError = nil; + NSDictionary *fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:destination error:&fileAttributesError]; + + if (fileAttributes != nil) { + bundleCreationDate = fileAttributes[NSFileCreationDate]; + } else { + bundleCreationDate = nil; + fprintf(stderr, "\nWarning: unable to retrieve file creation date of new bundle: %s", fileAttributesError.localizedDescription.UTF8String); + } + } else { + bundleCreationDate = nil; + } + + SPUDeltaArchiveHeader *header = [[SPUDeltaArchiveHeader alloc] initWithCompression:compression compressionLevel:compressionLevel fileSystemCompression:foundFilesystemCompression majorVersion:majorVersion minorVersion:minorVersion beforeTreeHash:beforeHash afterTreeHash:afterHash bundleCreationDate:bundleCreationDate]; [archive writeHeader:header]; if (archive.error != nil) { diff --git a/Tests/SUBinaryDeltaTest.m b/Tests/SUBinaryDeltaTest.m index ecf19705f..97cdc842a 100644 --- a/Tests/SUBinaryDeltaTest.m +++ b/Tests/SUBinaryDeltaTest.m @@ -101,29 +101,32 @@ - (BOOL)createAndApplyPatchWithBeforeDiffHandler:(SUDeltaHandler)beforeDiffHandl #else BOOL testingVersion2Delta = NO; #endif - return [self createAndApplyPatchWithBeforeDiffHandler:beforeDiffHandler afterDiffHandler:afterDiffHandler afterPatchHandler:afterPatchHandler testingVersion2Delta:testingVersion2Delta]; + return [self createAndApplyPatchWithBeforeDiffHandler:beforeDiffHandler afterDiffHandler:afterDiffHandler afterPatchHandler:afterPatchHandler testingVersion3Delta:YES testingVersion2Delta:testingVersion2Delta]; } -- (BOOL)createAndApplyPatchWithBeforeDiffHandler:(SUDeltaHandler)beforeDiffHandler afterDiffHandler:(SUDeltaHandler)afterDiffHandler afterPatchHandler:(SUDeltaHandler)afterPatchHandler testingVersion2Delta:(BOOL)testingVersion2Delta +- (BOOL)createAndApplyPatchWithBeforeDiffHandler:(SUDeltaHandler)beforeDiffHandler afterDiffHandler:(SUDeltaHandler)afterDiffHandler afterPatchHandler:(SUDeltaHandler)afterPatchHandler testingVersion3Delta:(BOOL)testingVersion3Delta testingVersion2Delta:(BOOL)testingVersion2Delta { - XCTAssertEqual(SUBinaryDeltaMajorVersion3, SUBinaryDeltaMajorVersionLatest); + XCTAssertEqual(SUBinaryDeltaMajorVersion4, SUBinaryDeltaMajorVersionLatest); - BOOL version3DeltaFormatWithLZMASuccess = [self createAndApplyPatchUsingVersion:SUBinaryDeltaMajorVersion3 compressionMode:SPUDeltaCompressionModeLZMA beforeDiffHandler:beforeDiffHandler afterDiffHandler:afterDiffHandler afterPatchHandler:afterPatchHandler]; + BOOL version4DeltaFormatWithLZMASuccess = [self createAndApplyPatchUsingVersion:SUBinaryDeltaMajorVersion4 compressionMode:SPUDeltaCompressionModeLZMA beforeDiffHandler:beforeDiffHandler afterDiffHandler:afterDiffHandler afterPatchHandler:afterPatchHandler]; #if SPARKLE_BUILD_BZIP2_DELTA_SUPPORT - BOOL version3DeltaFormatWithBZIP2Success = [self createAndApplyPatchUsingVersion:SUBinaryDeltaMajorVersion3 compressionMode:SPUDeltaCompressionModeBzip2 beforeDiffHandler:beforeDiffHandler afterDiffHandler:afterDiffHandler afterPatchHandler:afterPatchHandler]; + BOOL version4DeltaFormatWithBZIP2Success = [self createAndApplyPatchUsingVersion:SUBinaryDeltaMajorVersion4 compressionMode:SPUDeltaCompressionModeBzip2 beforeDiffHandler:beforeDiffHandler afterDiffHandler:afterDiffHandler afterPatchHandler:afterPatchHandler]; #endif - BOOL version3DeltaFormatWithZLIBSuccess = [self createAndApplyPatchUsingVersion:SUBinaryDeltaMajorVersion3 compressionMode:SPUDeltaCompressionModeZLIB beforeDiffHandler:beforeDiffHandler afterDiffHandler:afterDiffHandler afterPatchHandler:afterPatchHandler]; + BOOL version4DeltaFormatWithZLIBSuccess = [self createAndApplyPatchUsingVersion:SUBinaryDeltaMajorVersion4 compressionMode:SPUDeltaCompressionModeZLIB beforeDiffHandler:beforeDiffHandler afterDiffHandler:afterDiffHandler afterPatchHandler:afterPatchHandler]; + + BOOL version3DeltaFormatWithLZMASuccess = !testingVersion3Delta || [self createAndApplyPatchUsingVersion:SUBinaryDeltaMajorVersion3 compressionMode:SPUDeltaCompressionModeLZMA beforeDiffHandler:beforeDiffHandler afterDiffHandler:afterDiffHandler afterPatchHandler:afterPatchHandler]; BOOL version2FormatSuccess = !testingVersion2Delta || [self createAndApplyPatchUsingVersion:SUBinaryDeltaMajorVersion2 compressionMode:SPUDeltaCompressionModeDefault beforeDiffHandler:beforeDiffHandler afterDiffHandler:afterDiffHandler afterPatchHandler:afterPatchHandler]; return ( - version3DeltaFormatWithLZMASuccess && + version4DeltaFormatWithLZMASuccess && #if SPARKLE_BUILD_BZIP2_DELTA_SUPPORT - version3DeltaFormatWithBZIP2Success && + version4DeltaFormatWithBZIP2Success && #endif - version3DeltaFormatWithZLIBSuccess && + version4DeltaFormatWithZLIBSuccess && + version3DeltaFormatWithLZMASuccess && version2FormatSuccess ); } @@ -1001,7 +1004,7 @@ - (void)testAddingSymlinkWithWrongPermissions // Test that we only respect valid symlink permissions for >= version 3 deltas unsigned short permissions = permissionAttribute.unsignedShortValue & PERMISSION_FLAGS; XCTAssertEqual(permissions, VALID_SYMBOLIC_LINK_PERMISSIONS); - } testingVersion2Delta:NO]; + } testingVersion3Delta:YES testingVersion2Delta:NO]; } - (void)testSmallFilePermissionChangeWithNoContentChange @@ -1410,7 +1413,7 @@ - (void)testFileSystemCompression XCTFail(@"Second destination file is not compressed!"); } } - } testingVersion2Delta:NO]; + } testingVersion3Delta:YES testingVersion2Delta:NO]; } - (void)testNoFileSystemCompression @@ -1455,7 +1458,7 @@ - (void)testNoFileSystemCompression XCTFail(@"Second destination file is compressed!"); } } - } testingVersion2Delta:NO]; + } testingVersion3Delta:YES testingVersion2Delta:NO]; } - (void)testFrameworkVersionChanged @@ -2207,4 +2210,45 @@ - (void)testInvalidSparkleFrameworkInAfterTree XCTAssertFalse(success); } +- (void)testBundleCreationDate +{ + NSDate *sourceDate = [NSDate dateWithTimeIntervalSinceReferenceDate:420111117.0]; + NSDate *destinationDate = [NSDate dateWithTimeIntervalSinceReferenceDate:530112117.0]; + + BOOL success = [self createAndApplyPatchWithBeforeDiffHandler:^(NSFileManager *fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { + NSString *sourceFile = [sourceDirectory stringByAppendingPathComponent:@"A"]; + NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"A"]; + + XCTAssertTrue([[NSData data] writeToFile:sourceFile atomically:YES]); + XCTAssertTrue([[NSData dataWithBytes:"loltest" length:7] writeToFile:destinationFile atomically:YES]); + + { + NSError *setFileCreationDateError = nil; + if (![fileManager setAttributes:@{NSFileCreationDate: sourceDate} ofItemAtPath:sourceDirectory error:&setFileCreationDateError]) { + XCTFail(@"Failed to modify file creation date for source directory: %@", setFileCreationDateError.localizedDescription); + } + } + + { + NSError *setFileCreationDateError = nil; + if (![fileManager setAttributes:@{NSFileCreationDate: destinationDate} ofItemAtPath:destinationDirectory error:&setFileCreationDateError]) { + XCTFail(@"Failed to modify file creation date for destination directory: %@", setFileCreationDateError.localizedDescription); + } + } + } afterDiffHandler:nil afterPatchHandler:^(NSFileManager *fileManager, NSString * __unused sourceDirectory, NSString *destinationDirectory) { + NSError *fileAttributesError = nil; + NSDictionary *fileAttributes = [fileManager attributesOfItemAtPath:destinationDirectory error:&fileAttributesError]; + + if (fileAttributes == nil) { + XCTFail(@"Failed to retrieve file attributes from destination directory: %@", fileAttributesError.localizedDescription); + } + + NSDate *fileCreationDate = fileAttributes[NSFileCreationDate]; + XCTAssertNotNil(fileCreationDate); + + XCTAssertEqualObjects(destinationDate, fileCreationDate); + } testingVersion3Delta:NO testingVersion2Delta:NO]; + XCTAssertTrue(success); +} + @end