|
| 1 | +// |
| 2 | +// SUBinaryDeltaCreate.m |
| 3 | +// Sparkle |
| 4 | +// |
| 5 | +// Created by Mayur Pawashe on 4/9/15. |
| 6 | +// Copyright (c) 2015 Sparkle Project. All rights reserved. |
| 7 | +// |
| 8 | + |
| 9 | +#define _DARWIN_NO_64_BIT_INODE 1 |
| 10 | + |
| 11 | +#import "SUBinaryDeltaCreate.h" |
| 12 | +#import <Foundation/Foundation.h> |
| 13 | +#include "SUBinaryDeltaCommon.h" |
| 14 | +#import <CommonCrypto/CommonDigest.h> |
| 15 | +#include <fcntl.h> |
| 16 | +#include <fts.h> |
| 17 | +#include <libgen.h> |
| 18 | +#include <stdio.h> |
| 19 | +#include <sys/mman.h> |
| 20 | +#include <sys/param.h> |
| 21 | +#include <sys/stat.h> |
| 22 | +#include <unistd.h> |
| 23 | +#include <xar/xar.h> |
| 24 | + |
| 25 | +extern int bsdiff(int argc, const char **argv); |
| 26 | + |
| 27 | +@interface CreateBinaryDeltaOperation : NSOperation |
| 28 | +@property (copy) NSString *relativePath; |
| 29 | +@property (strong) NSString *resultPath; |
| 30 | +@property (strong) NSString *_fromPath; |
| 31 | +@property (strong) NSString *_toPath; |
| 32 | +- (id)initWithRelativePath:(NSString *)relativePath oldTree:(NSString *)oldTree newTree:(NSString *)newTree; |
| 33 | +@end |
| 34 | + |
| 35 | +@implementation CreateBinaryDeltaOperation |
| 36 | +@synthesize relativePath = _relativePath; |
| 37 | +@synthesize resultPath = _resultPath; |
| 38 | +@synthesize _fromPath = _fromPath; |
| 39 | +@synthesize _toPath = _toPath; |
| 40 | + |
| 41 | +- (id)initWithRelativePath:(NSString *)relativePath oldTree:(NSString *)oldTree newTree:(NSString *)newTree |
| 42 | +{ |
| 43 | + if ((self = [super init])) { |
| 44 | + self.relativePath = relativePath; |
| 45 | + self._fromPath = [oldTree stringByAppendingPathComponent:relativePath]; |
| 46 | + self._toPath = [newTree stringByAppendingPathComponent:relativePath]; |
| 47 | + } |
| 48 | + return self; |
| 49 | +} |
| 50 | + |
| 51 | +- (void)main |
| 52 | +{ |
| 53 | + NSString *temporaryFile = temporaryFilename(@"BinaryDelta"); |
| 54 | + const char *argv[] = {"/usr/bin/bsdiff", [self._fromPath fileSystemRepresentation], [self._toPath fileSystemRepresentation], [temporaryFile fileSystemRepresentation]}; |
| 55 | + int result = bsdiff(4, argv); |
| 56 | + if (!result) |
| 57 | + self.resultPath = temporaryFile; |
| 58 | +} |
| 59 | + |
| 60 | +@end |
| 61 | + |
| 62 | +static NSDictionary *infoForFile(FTSENT *ent) |
| 63 | +{ |
| 64 | + NSData *hash = hashOfFile(ent); |
| 65 | + NSNumber *size = @0; |
| 66 | + if (ent->fts_info != FTS_D) { |
| 67 | + size = @(ent->fts_statp->st_size); |
| 68 | + } |
| 69 | + return @{@"hash": hash, @"type": @(ent->fts_info), @"size": size}; |
| 70 | +} |
| 71 | + |
| 72 | +static NSString *absolutePath(NSString *path) |
| 73 | +{ |
| 74 | + NSURL *url = [[NSURL alloc] initFileURLWithPath:path]; |
| 75 | + return [[url absoluteURL] path]; |
| 76 | +} |
| 77 | + |
| 78 | +static NSString *temporaryPatchFile(NSString *patchFile) |
| 79 | +{ |
| 80 | + NSString *path = absolutePath(patchFile); |
| 81 | + NSString *directory = [path stringByDeletingLastPathComponent]; |
| 82 | + NSString *file = [path lastPathComponent]; |
| 83 | + return [NSString stringWithFormat:@"%@/.%@.tmp", directory, file]; |
| 84 | +} |
| 85 | + |
| 86 | +static BOOL shouldSkipDeltaCompression(NSString * __unused key, NSDictionary* originalInfo, NSDictionary *newInfo) |
| 87 | +{ |
| 88 | + unsigned long long fileSize = [newInfo[@"size"] unsignedLongLongValue]; |
| 89 | + if (fileSize < 4096) { |
| 90 | + return YES; |
| 91 | + } |
| 92 | + |
| 93 | + if (!originalInfo) { |
| 94 | + return YES; |
| 95 | + } |
| 96 | + |
| 97 | + if ([originalInfo[@"type"] unsignedShortValue] != [newInfo[@"type"] unsignedShortValue]) { |
| 98 | + return YES; |
| 99 | + } |
| 100 | + |
| 101 | + return NO; |
| 102 | +} |
| 103 | + |
| 104 | +static BOOL shouldDeleteThenExtract(NSString * __unused key, NSDictionary* originalInfo, NSDictionary *newInfo) |
| 105 | +{ |
| 106 | + if (!originalInfo) { |
| 107 | + return NO; |
| 108 | + } |
| 109 | + |
| 110 | + if ([originalInfo[@"type"] unsignedShortValue] != [newInfo[@"type"] unsignedShortValue]) { |
| 111 | + return YES; |
| 112 | + } |
| 113 | + |
| 114 | + return NO; |
| 115 | +} |
| 116 | + |
| 117 | +int createBinaryDelta(NSString *source, NSString *destination, NSString *patchFile) |
| 118 | +{ |
| 119 | + NSMutableDictionary *originalTreeState = [NSMutableDictionary dictionary]; |
| 120 | + |
| 121 | + const char *sourcePaths[] = {[source fileSystemRepresentation], 0}; |
| 122 | + FTS *fts = fts_open((char* const*)sourcePaths, FTS_PHYSICAL | FTS_NOCHDIR, compareFiles); |
| 123 | + if (!fts) { |
| 124 | + perror("fts_open"); |
| 125 | + return 1; |
| 126 | + } |
| 127 | + |
| 128 | + fprintf(stdout, "Processing %s...", [source UTF8String]); |
| 129 | + FTSENT *ent = 0; |
| 130 | + while ((ent = fts_read(fts))) { |
| 131 | + if (ent->fts_info != FTS_F && ent->fts_info != FTS_SL && ent->fts_info != FTS_D) { |
| 132 | + continue; |
| 133 | + } |
| 134 | + |
| 135 | + NSString *key = pathRelativeToDirectory(source, stringWithFileSystemRepresentation(ent->fts_path)); |
| 136 | + if (![key length]) { |
| 137 | + continue; |
| 138 | + } |
| 139 | + |
| 140 | + NSDictionary *info = infoForFile(ent); |
| 141 | + originalTreeState[key] = info; |
| 142 | + } |
| 143 | + fts_close(fts); |
| 144 | + |
| 145 | + NSString *beforeHash = hashOfTree(source); |
| 146 | + |
| 147 | + NSMutableDictionary *newTreeState = [NSMutableDictionary dictionary]; |
| 148 | + for (NSString *key in originalTreeState) |
| 149 | + { |
| 150 | + newTreeState[key] = [NSNull null]; |
| 151 | + } |
| 152 | + |
| 153 | + fprintf(stdout, "\nProcessing %s... ", [destination UTF8String]); |
| 154 | + sourcePaths[0] = [destination fileSystemRepresentation]; |
| 155 | + fts = fts_open((char* const*)sourcePaths, FTS_PHYSICAL | FTS_NOCHDIR, compareFiles); |
| 156 | + if (!fts) { |
| 157 | + perror("fts_open"); |
| 158 | + return 1; |
| 159 | + } |
| 160 | + |
| 161 | + |
| 162 | + while ((ent = fts_read(fts))) { |
| 163 | + if (ent->fts_info != FTS_F && ent->fts_info != FTS_SL && ent->fts_info != FTS_D) { |
| 164 | + continue; |
| 165 | + } |
| 166 | + |
| 167 | + NSString *key = pathRelativeToDirectory(destination, stringWithFileSystemRepresentation(ent->fts_path)); |
| 168 | + if (![key length]) { |
| 169 | + continue; |
| 170 | + } |
| 171 | + |
| 172 | + NSDictionary *info = infoForFile(ent); |
| 173 | + NSDictionary *oldInfo = originalTreeState[key]; |
| 174 | + |
| 175 | + if ([info isEqual:oldInfo]) { |
| 176 | + [newTreeState removeObjectForKey:key]; |
| 177 | + } else { |
| 178 | + newTreeState[key] = info; |
| 179 | + |
| 180 | + if (oldInfo && [oldInfo[@"type"] unsignedShortValue] == FTS_D && [info[@"type"] unsignedShortValue] != FTS_D) { |
| 181 | + NSArray *parentPathComponents = key.pathComponents; |
| 182 | + |
| 183 | + for (NSString *childPath in originalTreeState) { |
| 184 | + NSArray *childPathComponents = childPath.pathComponents; |
| 185 | + if (childPathComponents.count > parentPathComponents.count && |
| 186 | + [parentPathComponents isEqualToArray:[childPathComponents subarrayWithRange:NSMakeRange(0, parentPathComponents.count)]]) { |
| 187 | + [newTreeState removeObjectForKey:childPath]; |
| 188 | + } |
| 189 | + } |
| 190 | + } |
| 191 | + } |
| 192 | + } |
| 193 | + fts_close(fts); |
| 194 | + |
| 195 | + NSString *afterHash = hashOfTree(destination); |
| 196 | + |
| 197 | + fprintf(stdout, "\nGenerating delta... "); |
| 198 | + |
| 199 | + NSString *temporaryFile = temporaryPatchFile(patchFile); |
| 200 | + xar_t x = xar_open([temporaryFile fileSystemRepresentation], WRITE); |
| 201 | + xar_opt_set(x, XAR_OPT_COMPRESSION, "bzip2"); |
| 202 | + xar_subdoc_t attributes = xar_subdoc_new(x, "binary-delta-attributes"); |
| 203 | + xar_subdoc_prop_set(attributes, "before-sha1", [beforeHash UTF8String]); |
| 204 | + xar_subdoc_prop_set(attributes, "after-sha1", [afterHash UTF8String]); |
| 205 | + |
| 206 | + NSOperationQueue *deltaQueue = [[NSOperationQueue alloc] init]; |
| 207 | + NSMutableArray *deltaOperations = [NSMutableArray array]; |
| 208 | + |
| 209 | + // Sort the keys by preferring the ones from the original tree to appear first |
| 210 | + // We want to enforce deleting before extracting in the case paths differ only by case |
| 211 | + NSArray *keys = [[newTreeState allKeys] sortedArrayUsingComparator:^NSComparisonResult(NSString *key1, NSString *key2) { |
| 212 | + NSComparisonResult insensitiveCompareResult = [key1 caseInsensitiveCompare:key2]; |
| 213 | + if (insensitiveCompareResult != NSOrderedSame) { |
| 214 | + return insensitiveCompareResult; |
| 215 | + } |
| 216 | + |
| 217 | + return originalTreeState[key1] ? NSOrderedAscending : NSOrderedDescending; |
| 218 | + }]; |
| 219 | + for (NSString* key in keys) { |
| 220 | + id value = [newTreeState valueForKey:key]; |
| 221 | + |
| 222 | + if ([value isEqual:[NSNull null]]) { |
| 223 | + xar_file_t newFile = xar_add_frombuffer(x, 0, [key fileSystemRepresentation], (char *)"", 1); |
| 224 | + assert(newFile); |
| 225 | + xar_prop_set(newFile, "delete", "true"); |
| 226 | + continue; |
| 227 | + } |
| 228 | + |
| 229 | + NSDictionary *originalInfo = originalTreeState[key]; |
| 230 | + NSDictionary *newInfo = newTreeState[key]; |
| 231 | + if (shouldSkipDeltaCompression(key, originalInfo, newInfo)) { |
| 232 | + NSString *path = [destination stringByAppendingPathComponent:key]; |
| 233 | + xar_file_t newFile = xar_add_frompath(x, 0, [key fileSystemRepresentation], [path fileSystemRepresentation]); |
| 234 | + assert(newFile); |
| 235 | + if (shouldDeleteThenExtract(key, originalInfo, newInfo)) { |
| 236 | + xar_prop_set(newFile, "delete-then-extract", "true"); |
| 237 | + } |
| 238 | + } else { |
| 239 | + CreateBinaryDeltaOperation *operation = [[CreateBinaryDeltaOperation alloc] initWithRelativePath:key oldTree:source newTree:destination]; |
| 240 | + [deltaQueue addOperation:operation]; |
| 241 | + [deltaOperations addObject:operation]; |
| 242 | + } |
| 243 | + } |
| 244 | + |
| 245 | + [deltaQueue waitUntilAllOperationsAreFinished]; |
| 246 | + |
| 247 | + for (CreateBinaryDeltaOperation *operation in deltaOperations) { |
| 248 | + NSString *resultPath = [operation resultPath]; |
| 249 | + xar_file_t newFile = xar_add_frompath(x, 0, [[operation relativePath] fileSystemRepresentation], [resultPath fileSystemRepresentation]); |
| 250 | + assert(newFile); |
| 251 | + xar_prop_set(newFile, "binary-delta", "true"); |
| 252 | + unlink([resultPath fileSystemRepresentation]); |
| 253 | + } |
| 254 | + |
| 255 | + xar_close(x); |
| 256 | + |
| 257 | + unlink([patchFile fileSystemRepresentation]); |
| 258 | + link([temporaryFile fileSystemRepresentation], [patchFile fileSystemRepresentation]); |
| 259 | + unlink([temporaryFile fileSystemRepresentation]); |
| 260 | + fprintf(stdout, "Done!\n"); |
| 261 | + |
| 262 | + return 0; |
| 263 | +} |
0 commit comments