From 6848a88abc341c61db305627e51f513af1006410 Mon Sep 17 00:00:00 2001 From: Zorg Date: Sun, 29 Sep 2024 18:25:38 -0700 Subject: [PATCH] Preserve bundle creation date when creating and applying delta updates (#2583) This will preserve the file creation date of the new app bundle, but not the file creation date of any of the files inside the new app bundle because tracking those changes is complex/undesirable. This bumps the major binary delta version to 4. A new test has been added for testing that the new bundle creation date is also preserved. --- Autoupdate/SPUDeltaArchive.m | 5 +- Autoupdate/SPUDeltaArchiveProtocol.h | 3 +- Autoupdate/SPUSparkleDeltaArchive.m | 24 +++++++++- Autoupdate/SPUXarDeltaArchive.m | 2 +- Autoupdate/SUBinaryDeltaApply.m | 10 ++++ Autoupdate/SUBinaryDeltaCommon.h | 7 +-- Autoupdate/SUBinaryDeltaCommon.m | 4 +- Autoupdate/SUBinaryDeltaCreate.m | 18 +++++++- Tests/SUBinaryDeltaTest.m | 68 +++++++++++++++++++++++----- 9 files changed, 119 insertions(+), 22 deletions(-) diff --git a/Autoupdate/SPUDeltaArchive.m b/Autoupdate/SPUDeltaArchive.m index ca7ade75ad..e2f1e40b80 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 606908c0ec..f601266b16 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 cb71c73c26..f850198433 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 ab117230d4..cb859c7aef 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 9e862f1ff2..f3a456131a 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 e70aa1ce40..e6187ccc8c 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 d1c718c3f1..0461b2d0f3 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 925688c69a..a94896334c 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 ecf19705fb..97cdc842ae 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