Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions ios/CodePush.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@

/* Begin PBXBuildFile section */
08B8B3B8260E70B7ECA85451 /* bspatch_bridge.c in Sources */ = {isa = PBXBuildFile; fileRef = A430CBE260F09A3233110E28 /* bspatch_bridge.c */; };
BBC7F97A68E454FB38953FC4 /* CodePushDiffManifestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9645879798B46D35D8D824F8 /* CodePushDiffManifestTests.swift */; };
CED0A6936F6F8C354302F246 /* CodePushDiffManifest.m in Sources */ = {isa = PBXBuildFile; fileRef = 49166671F067D5F6429B262B /* CodePushDiffManifest.m */; };
CD32522E04D0F0E86DCC7BBE /* CodePushDiffManifest.m in Sources */ = {isa = PBXBuildFile; fileRef = 49166671F067D5F6429B262B /* CodePushDiffManifest.m */; };
0E9314F338AC505D2E33C1A3 /* CodePushDiffManifest.m in Sources */ = {isa = PBXBuildFile; fileRef = 49166671F067D5F6429B262B /* CodePushDiffManifest.m */; };
3F6A18E4C1B72D094F5E8A67 /* CodePushErrorUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 1B762E8F1C9A5E9A006EF800 /* CodePushErrorUtils.m */; };
C11A2DB29D3D814B1A7891DF /* CodePushDiffManifest.h in Headers */ = {isa = PBXBuildFile; fileRef = 2A092598DCE51279CA317823 /* CodePushDiffManifest.h */; };
4693408DCB9ACB8E02BF69B1 /* CodePushDiffManifest.h in Headers */ = {isa = PBXBuildFile; fileRef = 2A092598DCE51279CA317823 /* CodePushDiffManifest.h */; };
0ABCB5DEFE01A7A15552A498 /* file_for_patch.c in Sources */ = {isa = PBXBuildFile; fileRef = 70779807AB59EA4711737F4E /* file_for_patch.c */; };
0EC08441DDDD08C96DDE851C /* CodePushSha256Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23322E9E8EFBE01F1FACFB1 /* CodePushSha256Tests.swift */; };
13BE3DEE1AC21097009241FE /* CodePush.m in Sources */ = {isa = PBXBuildFile; fileRef = 13BE3DED1AC21097009241FE /* CodePush.m */; };
Expand Down Expand Up @@ -185,6 +192,9 @@
/* End PBXCopyFilesBuildPhase section */

/* Begin PBXFileReference section */
9645879798B46D35D8D824F8 /* CodePushDiffManifestTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CodePushDiffManifestTests.swift; sourceTree = "<group>"; };
2A092598DCE51279CA317823 /* CodePushDiffManifest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CodePushDiffManifest.h; path = CodePush/CodePushDiffManifest.h; sourceTree = "<group>"; };
49166671F067D5F6429B262B /* CodePushDiffManifest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = CodePushDiffManifest.m; path = CodePush/CodePushDiffManifest.m; sourceTree = "<group>"; };
0BF68F85125CF81D7EB65ABB /* bspatch_bridge.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = bspatch_bridge.h; sourceTree = "<group>"; };
0DC7989C75C72A774EF3685F /* bsdiff_wrapper/bspatch_wrapper.c */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.c; path = bsdiff_wrapper/bspatch_wrapper.c; sourceTree = "<group>"; };
134814201AA4EA6300B7C361 /* libCodePush.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libCodePush.a; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down Expand Up @@ -439,6 +449,8 @@
1B762E8F1C9A5E9A006EF800 /* CodePushErrorUtils.m */,
8C69F7C2081D7CEE96691674 /* CodePushErrorUtils.h */,
810D4E6C1B96935000B397E9 /* CodePushPackage.m */,
2A092598DCE51279CA317823 /* CodePushDiffManifest.h */,
49166671F067D5F6429B262B /* CodePushDiffManifest.m */,
5421FE301C58AD5A00986A55 /* CodePushTelemetryManager.m */,
540D20111C7684FE00D6EF41 /* CodePushUpdateUtils.m */,
1B23B9131BF9267B000BB2F0 /* RCTConvert+CodePushInstallMode.m */,
Expand Down Expand Up @@ -470,6 +482,7 @@
isa = PBXGroup;
children = (
95D6DD0EACAC8D095880DFD0 /* BSPatchTests.swift */,
9645879798B46D35D8D824F8 /* CodePushDiffManifestTests.swift */,
E9FA144425AE78B97AD6C870 /* CodePushTests-Bridging-Header.h */,
E74D80ECD8DD03C4C0772B7B /* Fixtures */,
D23322E9E8EFBE01F1FACFB1 /* CodePushSha256Tests.swift */,
Expand Down Expand Up @@ -668,6 +681,7 @@
3221E4762C8ABE1300268379 /* mz_strm_pkcrypt.h in Headers */,
3221E4642C8ABE1300268379 /* mz_strm_split.h in Headers */,
6463C8471EBA0D290095B8CD /* CodePush.h in Headers */,
4693408DCB9ACB8E02BF69B1 /* CodePushDiffManifest.h in Headers */,
3221E46A2C8ABE1300268379 /* mz_strm.h in Headers */,
3221E4782C8ABE1300268379 /* mz_zip_rw.h in Headers */,
3221E4802C8ABE1400268379 /* mz.h in Headers */,
Expand Down Expand Up @@ -728,6 +742,7 @@
3221E4632C8ABE1300268379 /* mz_strm_split.h in Headers */,
3221E4752C8ABE1300268379 /* mz_strm_pkcrypt.h in Headers */,
8482F84E1E24C66300F793DB /* CodePush.h in Headers */,
C11A2DB29D3D814B1A7891DF /* CodePushDiffManifest.h in Headers */,
F88664711F4AD1EE0036D01B /* JWTCoding+VersionTwo.h in Headers */,
F886646B1F4AD1EE0036D01B /* JWTCoding+ResultTypes.h in Headers */,
3221E4652C8ABE1300268379 /* mz_strm_buf.h in Headers */,
Expand Down Expand Up @@ -897,6 +912,7 @@
5498D8F61D21F14100B5EB43 /* CodePushUtils.m in Sources */,
3221E4612C8ABE1300268379 /* mz_zip_rw.c in Sources */,
810D4E6D1B96935000B397E9 /* CodePushPackage.m in Sources */,
CED0A6936F6F8C354302F246 /* CodePushDiffManifest.m in Sources */,
3221E4552C8ABE1300268379 /* mz_strm_pkcrypt.c in Sources */,
F88664531F4AD1EE0036D01B /* JWTAlgorithmESBase.m in Sources */,
3221E4532C8ABE1300268379 /* mz_strm_os_posix.c in Sources */,
Expand All @@ -921,6 +937,7 @@
6463C8311EBA0CFB0095B8CD /* CodePushErrorUtils.m in Sources */,
3221E46E2C8ABE1300268379 /* mz_crypt.c in Sources */,
6463C8321EBA0CFB0095B8CD /* CodePushPackage.m in Sources */,
CD32522E04D0F0E86DCC7BBE /* CodePushDiffManifest.m in Sources */,
6463C8331EBA0CFB0095B8CD /* CodePushTelemetryManager.m in Sources */,
6463C8341EBA0CFB0095B8CD /* CodePushUpdateUtils.m in Sources */,
3221E45C2C8ABE1300268379 /* mz_strm_zlib.c in Sources */,
Expand All @@ -943,7 +960,10 @@
buildActionMask = 2147483647;
files = (
B5B50F91FAAF80444988E284 /* BSPatchTests.swift in Sources */,
BBC7F97A68E454FB38953FC4 /* CodePushDiffManifestTests.swift in Sources */,
47F66D5AF3C3185E1A3E3B15 /* bspatch_bridge.c in Sources */,
0E9314F338AC505D2E33C1A3 /* CodePushDiffManifest.m in Sources */,
3F6A18E4C1B72D094F5E8A67 /* CodePushErrorUtils.m in Sources */,
DC983F1C71E0E7131BB343C5 /* libHDiffPatch/HPatch/patch.c in Sources */,
A88F11124A2120A8373A8B61 /* bsdiff_wrapper/bspatch_wrapper.c in Sources */,
0ABCB5DEFE01A7A15552A498 /* file_for_patch.c in Sources */,
Expand Down
55 changes: 55 additions & 0 deletions ios/CodePush/CodePushDiffManifest.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface CodePushPatchedFileEntry : NSObject

// The only value this client understands at the moment is "bsdiff".
@property (nonatomic, readonly, copy) NSString *algo;
// SHA-256 hex of the file's content in the currently installed package. Checked before patching.
@property (nonatomic, readonly, copy) NSString *baseHash;
// SHA-256 hex the patched output must match. Checked after patching.
@property (nonatomic, readonly, copy) NSString *targetHash;
// Zip-relative path to the patch file, under the reserved patches folder prefix.
@property (nonatomic, readonly, copy) NSString *patch;

- (instancetype)initWithAlgo:(NSString *)algo
baseHash:(NSString *)baseHash
targetHash:(NSString *)targetHash
patch:(NSString *)patch;

@end

@interface CodePushDiffManifest : NSObject

// No version field, or version 1: original format, file-by-file patching only.
// Version 2: adds support for binary diff patching.
@property (nonatomic, readonly, assign) NSInteger version;
// Relative paths, from the old package, to delete rather than carry over into the new one.
@property (nonatomic, readonly, copy) NSArray<NSString *> *deletedFiles;
// Key: file's relative path in the package being installed.
@property (nonatomic, readonly, copy) NSDictionary<NSString *, CodePushPatchedFileEntry *> *patchedFiles;

- (instancetype)initWithVersion:(NSInteger)version
deletedFiles:(NSArray<NSString *> *)deletedFiles
patchedFiles:(NSDictionary<NSString *, CodePushPatchedFileEntry *> *)patchedFiles;

// Parses a diff manifest from its already-deserialized JSON representation.
// Returns nil and sets *error if a required field is missing or malformed.
+ (nullable instancetype)manifestFromJSON:(NSDictionary *)json error:(NSError **)error NS_SWIFT_NAME(init(json:));

// Turns a relative path from a diff manifest into an absolute path under
// `folder`. Returns nil and sets *error if the path is malformed, `folder`
// is not a usable directory, or the path would resolve outside `folder`.
//
// Every path in a manifest is untrusted: the manifest and the files it refers
// to come from the downloaded update, which is unpacked before anything
// verifies it.
+ (nullable NSString *)resolvePath:(NSString *)relativePath
withinFolder:(NSString *)folder
error:(NSError * _Nullable * _Nullable)error
NS_SWIFT_NAME(resolvePath(_:withinFolder:));

@end

NS_ASSUME_NONNULL_END
239 changes: 239 additions & 0 deletions ios/CodePush/CodePushDiffManifest.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
#import "CodePushDiffManifest.h"

#import "CodePushErrorUtils.h"

#import <sys/stat.h>

static NSError *missingFieldError(NSString *fieldName, NSString *context)
{
return [CodePushErrorUtils errorWithMessage:[NSString stringWithFormat:@"Diff manifest %@ is missing required field \"%@\"", context, fieldName]];
}

static NSError *wrongTypedFieldError(NSString *fieldName, NSString *context, id value)
{
return [CodePushErrorUtils errorWithMessage:[NSString stringWithFormat:@"Diff manifest %@ field \"%@\" must be a string, but is %@", context, fieldName, NSStringFromClass([value class])]];
}

static BOOL isAbsent(id value)
{
return value == nil || [value isKindOfClass:[NSNull class]];
}

// Resolves every symlink in `path`. Returns nil if `path` does not exist or
// cannot be read.
static NSString *canonicalPathOfExistingItem(NSString *path)
{
char pathBuffer[PATH_MAX];
if (![path getFileSystemRepresentation:pathBuffer maxLength:sizeof(pathBuffer)]) {
return nil;
}

char resolvedBuffer[PATH_MAX];
if (realpath(pathBuffer, resolvedBuffer) == NULL) {
return nil;
}
return [[NSFileManager defaultManager] stringWithFileSystemRepresentation:resolvedBuffer length:strlen(resolvedBuffer)];
}

// Canonicalize the deepest path component that does exist, then re-append
// the components below it. Returns nil if one of those components is a dangling
// symlink: it would survive canonicalization as its own path, and a write to it
// would still follow the link out of the folder.
static NSString *canonicalPathAllowingMissingComponents(NSString *path)
{
NSMutableArray<NSString *> *missingComponents = [NSMutableArray array];
NSString *existingAncestor = path;
NSString *canonicalPath = nil;

while ((canonicalPath = canonicalPathOfExistingItem(existingAncestor)) == nil) {
NSString *parent = [existingAncestor stringByDeletingLastPathComponent];
if (parent.length == 0 || [parent isEqualToString:existingAncestor]) {
return nil;
}
[missingComponents insertObject:existingAncestor.lastPathComponent atIndex:0];
existingAncestor = parent;
}

for (NSString *component in missingComponents) {
// "." refers to the same directory, so appending it verbatim would
// leave the result unnormalized without changing what it points to.
if ([component isEqualToString:@"."]) {
continue;
}
canonicalPath = [canonicalPath stringByAppendingPathComponent:component];
Comment thread
ofalvai marked this conversation as resolved.

char componentBuffer[PATH_MAX];
if (![canonicalPath getFileSystemRepresentation:componentBuffer maxLength:sizeof(componentBuffer)]) {
return nil;
}

struct stat fileInfo;
if (lstat(componentBuffer, &fileInfo) == 0 && S_ISLNK(fileInfo.st_mode)) {
return nil;
}
}
return canonicalPath;
}

@implementation CodePushPatchedFileEntry

- (instancetype)initWithAlgo:(NSString *)algo
baseHash:(NSString *)baseHash
targetHash:(NSString *)targetHash
patch:(NSString *)patch
{
self = [super init];
if (self) {
_algo = [algo copy];
_baseHash = [baseHash copy];
_targetHash = [targetHash copy];
_patch = [patch copy];
}
return self;
}

@end

@implementation CodePushDiffManifest

- (instancetype)initWithVersion:(NSInteger)version
deletedFiles:(NSArray<NSString *> *)deletedFiles
patchedFiles:(NSDictionary<NSString *, CodePushPatchedFileEntry *> *)patchedFiles
{
self = [super init];
if (self) {
_version = version;
_deletedFiles = [deletedFiles copy];
_patchedFiles = [patchedFiles copy];
}
return self;
}

+ (nullable instancetype)manifestFromJSON:(NSDictionary *)json error:(NSError **)error
{
if (![json isKindOfClass:[NSDictionary class]]) {
if (error) *error = [CodePushErrorUtils errorWithMessage:[NSString stringWithFormat:@"Diff manifest must be a JSON object, but is %@", NSStringFromClass([json class])]];
return nil;
}

// A version we cannot read is a hard failure: silently treating it as 1
// would skip every patch and install the old bytes under the new hash.
id versionValue = json[@"version"];
NSInteger version = 1;
if (!isAbsent(versionValue)) {
BOOL isBoolean = versionValue == (id)kCFBooleanTrue || versionValue == (id)kCFBooleanFalse;
double versionDouble = [versionValue isKindOfClass:[NSNumber class]] ? [versionValue doubleValue] : 0;
if (![versionValue isKindOfClass:[NSNumber class]] || isBoolean || versionDouble != trunc(versionDouble)) {
if (error) *error = [CodePushErrorUtils errorWithMessage:[NSString stringWithFormat:@"Diff manifest field \"version\" must be an integer, but is \"%@\"", versionValue]];
return nil;
}
version = [versionValue integerValue];
}
Comment thread
ofalvai marked this conversation as resolved.

id deletedFilesJSON = json[@"deletedFiles"];
NSMutableArray<NSString *> *deletedFiles = [NSMutableArray array];
if (!isAbsent(deletedFilesJSON)) {
if (![deletedFilesJSON isKindOfClass:[NSArray class]]) {
if (error) *error = [CodePushErrorUtils errorWithMessage:[NSString stringWithFormat:@"Diff manifest field \"deletedFiles\" must be an array, but is %@", NSStringFromClass([deletedFilesJSON class])]];
return nil;
}
for (id deletedFileName in (NSArray *)deletedFilesJSON) {
if (![deletedFileName isKindOfClass:[NSString class]]) {
if (error) *error = [CodePushErrorUtils errorWithMessage:[NSString stringWithFormat:@"Diff manifest field \"deletedFiles\" must hold strings, but holds \"%@\"", deletedFileName]];
return nil;
}
[deletedFiles addObject:deletedFileName];
}
}

id patchedFilesJSON = json[@"patchedFiles"];
NSMutableDictionary<NSString *, CodePushPatchedFileEntry *> *patchedFiles = [NSMutableDictionary dictionary];
if (!isAbsent(patchedFilesJSON)) {
if (![patchedFilesJSON isKindOfClass:[NSDictionary class]]) {
if (error) *error = [CodePushErrorUtils errorWithMessage:[NSString stringWithFormat:@"Diff manifest field \"patchedFiles\" must be an object, but is %@", NSStringFromClass([patchedFilesJSON class])]];
return nil;
}
for (NSString *relativePath in (NSDictionary *)patchedFilesJSON) {
NSString *context = [NSString stringWithFormat:@"patchedFiles[\"%@\"]", relativePath];

id entryJSON = patchedFilesJSON[relativePath];
if (![entryJSON isKindOfClass:[NSDictionary class]]) {
if (error) *error = [CodePushErrorUtils errorWithMessage:[NSString stringWithFormat:@"Diff manifest %@ must be an object, but is %@", context, NSStringFromClass([entryJSON class])]];
return nil;
}

for (NSString *fieldName in @[@"algo", @"baseHash", @"targetHash", @"patch"]) {
id fieldValue = entryJSON[fieldName];
if (![fieldValue isKindOfClass:[NSString class]]) {
if (error) *error = isAbsent(fieldValue) ? missingFieldError(fieldName, context) : wrongTypedFieldError(fieldName, context, fieldValue);
return nil;
}
Comment thread
ofalvai marked this conversation as resolved.
}

patchedFiles[relativePath] = [[CodePushPatchedFileEntry alloc] initWithAlgo:entryJSON[@"algo"]
baseHash:entryJSON[@"baseHash"]
targetHash:entryJSON[@"targetHash"]
patch:entryJSON[@"patch"]];
}
}

// Only version 2 defines file patching.
// A manifest of any other version that lists patched files is malformed,
// and applying none of them would leave the old bytes behind.
if (version != 2 && patchedFiles.count > 0) {
if (error) *error = [CodePushErrorUtils errorWithMessage:[NSString stringWithFormat:@"Diff manifest declares version %ld but lists %lu patchedFiles, which require version 2", (long)version, (unsigned long)patchedFiles.count]];
return nil;
}

return [[CodePushDiffManifest alloc] initWithVersion:version
deletedFiles:deletedFiles
patchedFiles:patchedFiles];
}

+ (nullable NSString *)resolvePath:(NSString *)relativePath
withinFolder:(NSString *)folder
error:(NSError **)error
{
if (relativePath.length == 0) {
if (error) *error = [CodePushErrorUtils errorWithMessage:@"path is empty"];
return nil;
}
if (relativePath.isAbsolutePath) {
if (error) *error = [CodePushErrorUtils errorWithMessage:@"path escapes expected directory"];
return nil;
}
// A path the file system cannot represent - an embedded NUL, or one longer
// than PATH_MAX - never reaches a syscall from here.
char pathBuffer[PATH_MAX];
if (![relativePath getFileSystemRepresentation:pathBuffer maxLength:sizeof(pathBuffer)]) {
if (error) *error = [CodePushErrorUtils errorWithMessage:@"path cannot be represented in the file system"];
return nil;
}
Comment thread
miklosboros marked this conversation as resolved.
for (NSString *component in relativePath.pathComponents) {
if ([component isEqualToString:@".."]) {
if (error) *error = [CodePushErrorUtils errorWithMessage:@"path escapes expected directory"];
return nil;
}
}

NSString *canonicalFolder = canonicalPathOfExistingItem(folder);
if (canonicalFolder == nil) {
if (error) *error = [CodePushErrorUtils errorWithMessage:@"base folder does not exist or is not accessible"];
return nil;
}

NSString *resolved = canonicalPathAllowingMissingComponents([canonicalFolder stringByAppendingPathComponent:relativePath]);
if (resolved == nil) {
if (error) *error = [CodePushErrorUtils errorWithMessage:@"path escapes expected directory"];
return nil;
}
// Callers treat the result as a file inside the folder, so the folder
// itself (e.g. from "." or a symlink resolving back to it) must not pass.
if (![resolved hasPrefix:[canonicalFolder stringByAppendingString:@"/"]]) {
if (error) *error = [CodePushErrorUtils errorWithMessage:@"path escapes expected directory"];
return nil;
}
return resolved;
}

@end
Loading
Loading