Skip to content

Commit

Permalink
Preserve bundle creation date when creating and applying delta updates (
Browse files Browse the repository at this point in the history
#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.
  • Loading branch information
zorgiepoo authored Sep 30, 2024
1 parent 8de8db0 commit 6848a88
Show file tree
Hide file tree
Showing 9 changed files with 119 additions and 22 deletions.
5 changes: 4 additions & 1 deletion Autoupdate/SPUDeltaArchive.m
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -104,6 +105,8 @@ - (instancetype)initWithCompression:(SPUDeltaCompressionMode)compression compres

memcpy(_beforeTreeHash, beforeTreeHash, sizeof(_beforeTreeHash));
memcpy(_afterTreeHash, afterTreeHash, sizeof(_afterTreeHash));

_bundleCreationDate = bundleCreationDate;
}
return self;
}
Expand Down
3 changes: 2 additions & 1 deletion Autoupdate/SPUDeltaArchiveProtocol.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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

Expand Down
24 changes: 22 additions & 2 deletions Autoupdate/SPUSparkleDeltaArchive.m
Original file line number Diff line number Diff line change
Expand Up @@ -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<NSString *> *)_readRelativeFilePaths SPU_OBJC_DIRECT
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<NSString *> *newClonedPathEntries = [NSMutableArray array];
for (SPUDeltaArchiveItem *item in writableItems) {
Expand Down
2 changes: 1 addition & 1 deletion Autoupdate/SPUXarDeltaArchive.m
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions Autoupdate/SUBinaryDeltaApply.m
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
7 changes: 4 additions & 3 deletions Autoupdate/SUBinaryDeltaCommon.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,17 @@ 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;
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."

Expand Down
4 changes: 3 additions & 1 deletion Autoupdate/SUBinaryDeltaCommon.m
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -115,6 +115,8 @@ uint16_t latestMinorVersionForMajorVersion(SUBinaryDeltaMajorVersion majorVersio
return 4;
case SUBinaryDeltaMajorVersion3:
return 1;
case SUBinaryDeltaMajorVersion4:
return 0;
}
return 0;
}
Expand Down
18 changes: 17 additions & 1 deletion Autoupdate/SUBinaryDeltaCreate.m
Original file line number Diff line number Diff line change
Expand Up @@ -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<NSFileAttributeKey, id> *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) {
Expand Down
68 changes: 56 additions & 12 deletions Tests/SUBinaryDeltaTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1410,7 +1413,7 @@ - (void)testFileSystemCompression
XCTFail(@"Second destination file is not compressed!");
}
}
} testingVersion2Delta:NO];
} testingVersion3Delta:YES testingVersion2Delta:NO];
}

- (void)testNoFileSystemCompression
Expand Down Expand Up @@ -1455,7 +1458,7 @@ - (void)testNoFileSystemCompression
XCTFail(@"Second destination file is compressed!");
}
}
} testingVersion2Delta:NO];
} testingVersion3Delta:YES testingVersion2Delta:NO];
}

- (void)testFrameworkVersionChanged
Expand Down Expand Up @@ -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<NSFileAttributeKey, id> *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

0 comments on commit 6848a88

Please sign in to comment.