From fc26ab767eb8c33b620de4d3a4c0117803905343 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliv=C3=A9r=20Falvai?= Date: Tue, 8 Sep 2026 14:57:25 +0200 Subject: [PATCH] iOS: add CodePushBinaryDiffPatcher to apply bsdiff patches --- ios/CodePush.xcodeproj/project.pbxproj | 18 ++ ios/CodePush/CodePushBinaryDiffPatcher.h | 25 ++ ios/CodePush/CodePushBinaryDiffPatcher.m | 113 ++++++++ .../CodePushBinaryDiffPatcherTests.swift | 248 ++++++++++++++++++ .../CodePushTests-Bridging-Header.h | 1 + 5 files changed, 405 insertions(+) create mode 100644 ios/CodePush/CodePushBinaryDiffPatcher.h create mode 100644 ios/CodePush/CodePushBinaryDiffPatcher.m create mode 100644 ios/CodePushTests/CodePushBinaryDiffPatcherTests.swift diff --git a/ios/CodePush.xcodeproj/project.pbxproj b/ios/CodePush.xcodeproj/project.pbxproj index 5f4f719a..61b371d7 100644 --- a/ios/CodePush.xcodeproj/project.pbxproj +++ b/ios/CodePush.xcodeproj/project.pbxproj @@ -15,6 +15,12 @@ 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 */; }; + 3643F3729205426163367671 /* CodePushBinaryDiffPatcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F42FA68BF21AED765F55E71A /* CodePushBinaryDiffPatcherTests.swift */; }; + 1993F82DF6330426AC5CBC2E /* CodePushBinaryDiffPatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 9414C770C70E1B1319F24B12 /* CodePushBinaryDiffPatcher.m */; }; + F41E2EFAD0D322F7D14D7125 /* CodePushBinaryDiffPatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 9414C770C70E1B1319F24B12 /* CodePushBinaryDiffPatcher.m */; }; + D5BC8B1BF7D2DA03BB888FCC /* CodePushBinaryDiffPatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 9414C770C70E1B1319F24B12 /* CodePushBinaryDiffPatcher.m */; }; + E5C712D353D9164F04A2F456 /* CodePushBinaryDiffPatcher.h in Headers */ = {isa = PBXBuildFile; fileRef = 0F588BE40F9B3092085C5B63 /* CodePushBinaryDiffPatcher.h */; }; + 469472C57F1D5FF7C0274268 /* CodePushBinaryDiffPatcher.h in Headers */ = {isa = PBXBuildFile; fileRef = 0F588BE40F9B3092085C5B63 /* CodePushBinaryDiffPatcher.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 */; }; @@ -195,6 +201,9 @@ 9645879798B46D35D8D824F8 /* CodePushDiffManifestTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CodePushDiffManifestTests.swift; sourceTree = ""; }; 2A092598DCE51279CA317823 /* CodePushDiffManifest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CodePushDiffManifest.h; path = CodePush/CodePushDiffManifest.h; sourceTree = ""; }; 49166671F067D5F6429B262B /* CodePushDiffManifest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = CodePushDiffManifest.m; path = CodePush/CodePushDiffManifest.m; sourceTree = ""; }; + F42FA68BF21AED765F55E71A /* CodePushBinaryDiffPatcherTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CodePushBinaryDiffPatcherTests.swift; sourceTree = ""; }; + 0F588BE40F9B3092085C5B63 /* CodePushBinaryDiffPatcher.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CodePushBinaryDiffPatcher.h; path = CodePush/CodePushBinaryDiffPatcher.h; sourceTree = ""; }; + 9414C770C70E1B1319F24B12 /* CodePushBinaryDiffPatcher.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = CodePushBinaryDiffPatcher.m; path = CodePush/CodePushBinaryDiffPatcher.m; sourceTree = ""; }; 0BF68F85125CF81D7EB65ABB /* bspatch_bridge.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = bspatch_bridge.h; sourceTree = ""; }; 0DC7989C75C72A774EF3685F /* bsdiff_wrapper/bspatch_wrapper.c */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.c; path = bsdiff_wrapper/bspatch_wrapper.c; sourceTree = ""; }; 134814201AA4EA6300B7C361 /* libCodePush.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libCodePush.a; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -451,6 +460,8 @@ 810D4E6C1B96935000B397E9 /* CodePushPackage.m */, 2A092598DCE51279CA317823 /* CodePushDiffManifest.h */, 49166671F067D5F6429B262B /* CodePushDiffManifest.m */, + 0F588BE40F9B3092085C5B63 /* CodePushBinaryDiffPatcher.h */, + 9414C770C70E1B1319F24B12 /* CodePushBinaryDiffPatcher.m */, 5421FE301C58AD5A00986A55 /* CodePushTelemetryManager.m */, 540D20111C7684FE00D6EF41 /* CodePushUpdateUtils.m */, 1B23B9131BF9267B000BB2F0 /* RCTConvert+CodePushInstallMode.m */, @@ -483,6 +494,7 @@ children = ( 95D6DD0EACAC8D095880DFD0 /* BSPatchTests.swift */, 9645879798B46D35D8D824F8 /* CodePushDiffManifestTests.swift */, + F42FA68BF21AED765F55E71A /* CodePushBinaryDiffPatcherTests.swift */, E9FA144425AE78B97AD6C870 /* CodePushTests-Bridging-Header.h */, E74D80ECD8DD03C4C0772B7B /* Fixtures */, D23322E9E8EFBE01F1FACFB1 /* CodePushSha256Tests.swift */, @@ -682,6 +694,7 @@ 3221E4642C8ABE1300268379 /* mz_strm_split.h in Headers */, 6463C8471EBA0D290095B8CD /* CodePush.h in Headers */, 4693408DCB9ACB8E02BF69B1 /* CodePushDiffManifest.h in Headers */, + 469472C57F1D5FF7C0274268 /* CodePushBinaryDiffPatcher.h in Headers */, 3221E46A2C8ABE1300268379 /* mz_strm.h in Headers */, 3221E4782C8ABE1300268379 /* mz_zip_rw.h in Headers */, 3221E4802C8ABE1400268379 /* mz.h in Headers */, @@ -743,6 +756,7 @@ 3221E4752C8ABE1300268379 /* mz_strm_pkcrypt.h in Headers */, 8482F84E1E24C66300F793DB /* CodePush.h in Headers */, C11A2DB29D3D814B1A7891DF /* CodePushDiffManifest.h in Headers */, + E5C712D353D9164F04A2F456 /* CodePushBinaryDiffPatcher.h in Headers */, F88664711F4AD1EE0036D01B /* JWTCoding+VersionTwo.h in Headers */, F886646B1F4AD1EE0036D01B /* JWTCoding+ResultTypes.h in Headers */, 3221E4652C8ABE1300268379 /* mz_strm_buf.h in Headers */, @@ -913,6 +927,7 @@ 3221E4612C8ABE1300268379 /* mz_zip_rw.c in Sources */, 810D4E6D1B96935000B397E9 /* CodePushPackage.m in Sources */, CED0A6936F6F8C354302F246 /* CodePushDiffManifest.m in Sources */, + 1993F82DF6330426AC5CBC2E /* CodePushBinaryDiffPatcher.m in Sources */, 3221E4552C8ABE1300268379 /* mz_strm_pkcrypt.c in Sources */, F88664531F4AD1EE0036D01B /* JWTAlgorithmESBase.m in Sources */, 3221E4532C8ABE1300268379 /* mz_strm_os_posix.c in Sources */, @@ -938,6 +953,7 @@ 3221E46E2C8ABE1300268379 /* mz_crypt.c in Sources */, 6463C8321EBA0CFB0095B8CD /* CodePushPackage.m in Sources */, CD32522E04D0F0E86DCC7BBE /* CodePushDiffManifest.m in Sources */, + F41E2EFAD0D322F7D14D7125 /* CodePushBinaryDiffPatcher.m in Sources */, 6463C8331EBA0CFB0095B8CD /* CodePushTelemetryManager.m in Sources */, 6463C8341EBA0CFB0095B8CD /* CodePushUpdateUtils.m in Sources */, 3221E45C2C8ABE1300268379 /* mz_strm_zlib.c in Sources */, @@ -961,9 +977,11 @@ files = ( B5B50F91FAAF80444988E284 /* BSPatchTests.swift in Sources */, BBC7F97A68E454FB38953FC4 /* CodePushDiffManifestTests.swift in Sources */, + 3643F3729205426163367671 /* CodePushBinaryDiffPatcherTests.swift in Sources */, 47F66D5AF3C3185E1A3E3B15 /* bspatch_bridge.c in Sources */, 0E9314F338AC505D2E33C1A3 /* CodePushDiffManifest.m in Sources */, 3F6A18E4C1B72D094F5E8A67 /* CodePushErrorUtils.m in Sources */, + D5BC8B1BF7D2DA03BB888FCC /* CodePushBinaryDiffPatcher.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 */, diff --git a/ios/CodePush/CodePushBinaryDiffPatcher.h b/ios/CodePush/CodePushBinaryDiffPatcher.h new file mode 100644 index 00000000..28416e20 --- /dev/null +++ b/ios/CodePush/CodePushBinaryDiffPatcher.h @@ -0,0 +1,25 @@ +#import +#import "CodePushDiffManifest.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface CodePushBinaryDiffPatcher : NSObject + +// Applies every entry in manifest.patchedFiles: verifies the pre-patch file +// against baseHash, applies the patch into newUpdateFolder, then verifies +// the result against targetHash. +// +// All three folder arguments must exist on disk. Files within them need not, +// the patch output and its parent directories are created as needed. +// +// Returns NO and sets *error on the first failure. ++ (BOOL)applyBinaryDiffPatchesFromManifest:(CodePushDiffManifest *)manifest + currentPackageFolder:(NSString *)currentPackageFolder + unzippedFolder:(NSString *)unzippedFolder + newUpdateFolder:(NSString *)newUpdateFolder + error:(NSError **)error + NS_SWIFT_NAME(applyBinaryDiffPatches(manifest:currentPackageFolder:unzippedFolder:newUpdateFolder:)); + +@end + +NS_ASSUME_NONNULL_END diff --git a/ios/CodePush/CodePushBinaryDiffPatcher.m b/ios/CodePush/CodePushBinaryDiffPatcher.m new file mode 100644 index 00000000..3d992948 --- /dev/null +++ b/ios/CodePush/CodePushBinaryDiffPatcher.m @@ -0,0 +1,113 @@ +#import "CodePushBinaryDiffPatcher.h" +#import "CodePushErrorUtils.h" +#import "CodePushSha256.h" +#import "bspatch_bridge.h" + +static NSError *patchApplyError(NSString *relativePath, NSString *reason) +{ + return [CodePushErrorUtils errorWithMessage: + [NSString stringWithFormat:@"Failed to apply binary diff patch for \"%@\": %@", relativePath, reason]]; +} + +// Keep in sync with shared/diffpatch/bspatch_bridge.h. +static NSString *describeBSPatchResult(CodePushBSPatchResult result) +{ + switch (result) { + case CODEPUSH_BSPATCH_OK: return @"OK"; + case CODEPUSH_BSPATCH_ERR_BAD_DIFF_HEADER: return @"BAD_DIFF_HEADER"; + case CODEPUSH_BSPATCH_ERR_OPEN_OLD: return @"OPEN_OLD_FAILED"; + case CODEPUSH_BSPATCH_ERR_OPEN_DIFF: return @"OPEN_DIFF_FAILED"; + case CODEPUSH_BSPATCH_ERR_OPEN_OUT: return @"OPEN_OUT_FAILED"; + case CODEPUSH_BSPATCH_ERR_OOM: return @"OUT_OF_MEMORY"; + case CODEPUSH_BSPATCH_ERR_PATCH_FAILED: return @"PATCH_FAILED"; + } + return [NSString stringWithFormat:@"UNKNOWN (%ld)", (long)result]; +} + +static NSString *resolveWithin(NSString *base, NSString *path, NSString *manifestEntry, NSError **error) +{ + NSError *resolveError = nil; + NSString *resolved = [CodePushDiffManifest resolvePath:path withinFolder:base error:&resolveError]; + if (resolved == nil && error) { + *error = patchApplyError(manifestEntry, resolveError.localizedDescription); + } + return resolved; +} + +@implementation CodePushBinaryDiffPatcher + ++ (BOOL)applyBinaryDiffPatchesFromManifest:(CodePushDiffManifest *)manifest + currentPackageFolder:(NSString *)currentPackageFolder + unzippedFolder:(NSString *)unzippedFolder + newUpdateFolder:(NSString *)newUpdateFolder + error:(NSError **)error +{ + NSDictionary *patchedFiles = manifest.patchedFiles; + + for (NSString *relativePath in patchedFiles) { + CodePushPatchedFileEntry *entry = patchedFiles[relativePath]; + if (![entry.algo isEqualToString:@"bsdiff"]) { + if (error) *error = patchApplyError(relativePath, [NSString stringWithFormat:@"unsupported patch algorithm: %@", entry.algo]); + return NO; + } + } + + NSFileManager *fileManager = [NSFileManager defaultManager]; + + for (NSString *relativePath in patchedFiles) { + CodePushPatchedFileEntry *entry = patchedFiles[relativePath]; + + NSString *oldFile = resolveWithin(currentPackageFolder, relativePath, relativePath, error); + if (!oldFile) return NO; + + NSError *oldFileHashError = nil; + NSString *oldFileHash = CodePushSha256HexForFile(oldFile, &oldFileHashError); + if (!oldFileHash) { + if (error) *error = patchApplyError(relativePath, [NSString stringWithFormat:@"could not hash the file to patch: %@", oldFileHashError.localizedDescription]); + return NO; + } + if (![oldFileHash isEqualToString:entry.baseHash]) { + if (error) *error = patchApplyError(relativePath, [NSString stringWithFormat:@"baseHash mismatch: expected %@, got %@", entry.baseHash, oldFileHash]); + return NO; + } + + NSString *diffFile = resolveWithin(unzippedFolder, entry.patch, relativePath, error); + if (!diffFile) return NO; + + NSString *newFile = resolveWithin(newUpdateFolder, relativePath, relativePath, error); + if (!newFile) return NO; + + NSError *createDirError = nil; + [fileManager createDirectoryAtPath:[newFile stringByDeletingLastPathComponent] + withIntermediateDirectories:YES + attributes:nil + error:&createDirError]; + if (createDirError) { + if (error) *error = patchApplyError(relativePath, createDirError.localizedDescription); + return NO; + } + + CodePushBSPatchResult result = codepush_bspatch_apply(oldFile.fileSystemRepresentation, + diffFile.fileSystemRepresentation, + newFile.fileSystemRepresentation); + if (result != CODEPUSH_BSPATCH_OK) { + if (error) *error = patchApplyError(relativePath, [NSString stringWithFormat:@"patch failed: %@", describeBSPatchResult(result)]); + return NO; + } + + NSError *newFileHashError = nil; + NSString *newFileHash = CodePushSha256HexForFile(newFile, &newFileHashError); + if (!newFileHash) { + if (error) *error = patchApplyError(relativePath, [NSString stringWithFormat:@"could not hash the patched file: %@", newFileHashError.localizedDescription]); + return NO; + } + if (![newFileHash isEqualToString:entry.targetHash]) { + if (error) *error = patchApplyError(relativePath, [NSString stringWithFormat:@"targetHash mismatch: expected %@, got %@", entry.targetHash, newFileHash]); + return NO; + } + } + + return YES; +} + +@end diff --git a/ios/CodePushTests/CodePushBinaryDiffPatcherTests.swift b/ios/CodePushTests/CodePushBinaryDiffPatcherTests.swift new file mode 100644 index 00000000..045c723f --- /dev/null +++ b/ios/CodePushTests/CodePushBinaryDiffPatcherTests.swift @@ -0,0 +1,248 @@ +import XCTest + +final class CodePushBinaryDiffPatcherTests: XCTestCase { + + private var tempDir: URL! + private var currentPackageFolder: URL! + private var unzippedFolder: URL! + private var newUpdateFolder: URL! + + private let relativePath = "main.jsbundle" + private let patchRelativePath = "__hcp_patches/main.jsbundle.bsdiff" + + override func setUpWithError() throws { + tempDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + currentPackageFolder = tempDir.appendingPathComponent("current") + unzippedFolder = tempDir.appendingPathComponent("unzipped") + newUpdateFolder = tempDir.appendingPathComponent("new") + + let scratchDirs: [URL] = [currentPackageFolder, unzippedFolder, newUpdateFolder] + for dir in scratchDirs { + try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) + } + try FileManager.default.createDirectory( + at: unzippedFolder.appendingPathComponent("__hcp_patches"), + withIntermediateDirectories: true) + } + + override func tearDownWithError() throws { + try? FileManager.default.removeItem(at: tempDir) + } + + private func fixtureURL(_ relativePath: String) -> URL { + let bundle = Bundle(for: type(of: self)) + guard let resourceURL = bundle.url(forResource: "Fixtures", withExtension: nil) else { + fatalError("Fixtures resource folder not found in test bundle") + } + return resourceURL.appendingPathComponent(relativePath) + } + + private func hashOfFixture(_ relativePath: String) throws -> String { + var error: NSError? + guard let hex = CodePushSha256HexForFile(fixtureURL(relativePath).path, &error) else { + throw error ?? NSError(domain: "test", code: 1) + } + return hex + } + + private func installBasicFixtures() throws { + try FileManager.default.copyItem( + at: fixtureURL("basic/old.dat"), + to: currentPackageFolder.appendingPathComponent(relativePath)) + try FileManager.default.copyItem( + at: fixtureURL("basic/patch.bsdiff"), + to: unzippedFolder.appendingPathComponent(patchRelativePath)) + } + + private func manifest(baseHash: String, targetHash: String, algo: String = "bsdiff", patchPath: String? = nil, relativePath: String? = nil) -> CodePushDiffManifest { + let entry = CodePushPatchedFileEntry( + algo: algo, + baseHash: baseHash, + targetHash: targetHash, + patch: patchPath ?? patchRelativePath) + return CodePushDiffManifest( + version: 2, + deletedFiles: [], + patchedFiles: [relativePath ?? self.relativePath: entry]) + } + + func testApply_happyPath_producesExpectedOutputFile() throws { + try installBasicFixtures() + let baseHash = try hashOfFixture("basic/old.dat") + let targetHash = try hashOfFixture("basic/new.dat") + + try CodePushBinaryDiffPatcher.applyBinaryDiffPatches( + manifest: manifest(baseHash: baseHash, targetHash: targetHash), + currentPackageFolder: currentPackageFolder.path, + unzippedFolder: unzippedFolder.path, + newUpdateFolder: newUpdateFolder.path) + + let producedData = try Data(contentsOf: newUpdateFolder.appendingPathComponent(relativePath)) + let expectedData = try Data(contentsOf: fixtureURL("basic/new.dat")) + XCTAssertEqual(producedData, expectedData) + } + + func testApply_baseHashMismatch_throws() throws { + try installBasicFixtures() + let targetHash = try hashOfFixture("basic/new.dat") + + XCTAssertThrowsError( + try CodePushBinaryDiffPatcher.applyBinaryDiffPatches( + manifest: manifest(baseHash: "not-the-real-hash", targetHash: targetHash), + currentPackageFolder: currentPackageFolder.path, + unzippedFolder: unzippedFolder.path, + newUpdateFolder: newUpdateFolder.path)) + } + + func testApply_targetHashMismatch_throws() throws { + try installBasicFixtures() + let baseHash = try hashOfFixture("basic/old.dat") + + XCTAssertThrowsError( + try CodePushBinaryDiffPatcher.applyBinaryDiffPatches( + manifest: manifest(baseHash: baseHash, targetHash: "not-the-real-hash"), + currentPackageFolder: currentPackageFolder.path, + unzippedFolder: unzippedFolder.path, + newUpdateFolder: newUpdateFolder.path)) + } + + func testApply_unsupportedAlgo_throwsWithoutTouchingFiles() { + // No fixtures installed: an unsupported algo must be rejected before any file I/O. + XCTAssertThrowsError( + try CodePushBinaryDiffPatcher.applyBinaryDiffPatches( + manifest: manifest(baseHash: "irrelevant", targetHash: "irrelevant", algo: "xdelta"), + currentPackageFolder: currentPackageFolder.path, + unzippedFolder: unzippedFolder.path, + newUpdateFolder: newUpdateFolder.path)) + } + + func testApply_pathTraversalInPatchedFilesKey_isRejected() throws { + try installBasicFixtures() + let baseHash = try hashOfFixture("basic/old.dat") + let targetHash = try hashOfFixture("basic/new.dat") + + XCTAssertThrowsError( + try CodePushBinaryDiffPatcher.applyBinaryDiffPatches( + manifest: manifest(baseHash: baseHash, targetHash: targetHash, relativePath: "../../etc/passwd"), + currentPackageFolder: currentPackageFolder.path, + unzippedFolder: unzippedFolder.path, + newUpdateFolder: newUpdateFolder.path)) { error in + let message = (error as NSError).localizedDescription + XCTAssertTrue(message.contains("escapes"), message) + } + } + + func testApply_emptyPatchedFilesKey_isRejectedAsMalformed() throws { + try installBasicFixtures() + let baseHash = try hashOfFixture("basic/old.dat") + let targetHash = try hashOfFixture("basic/new.dat") + + XCTAssertThrowsError( + try CodePushBinaryDiffPatcher.applyBinaryDiffPatches( + manifest: manifest(baseHash: baseHash, targetHash: targetHash, relativePath: ""), + currentPackageFolder: currentPackageFolder.path, + unzippedFolder: unzippedFolder.path, + newUpdateFolder: newUpdateFolder.path)) { error in + let message = (error as NSError).localizedDescription + XCTAssertTrue(message.contains("empty"), message) + XCTAssertFalse(message.contains("escapes"), message) + } + } + + func testApply_missingCurrentPackageFolder_reportsFolderUnusable() throws { + try FileManager.default.removeItem(at: currentPackageFolder) + + XCTAssertThrowsError( + try CodePushBinaryDiffPatcher.applyBinaryDiffPatches( + manifest: manifest(baseHash: "irrelevant-not-reached-before-folder-check", targetHash: "irrelevant-not-reached-before-folder-check"), + currentPackageFolder: currentPackageFolder.path, + unzippedFolder: unzippedFolder.path, + newUpdateFolder: newUpdateFolder.path)) { error in + let message = (error as NSError).localizedDescription + XCTAssertTrue(message.contains("does not exist"), message) + XCTAssertFalse(message.contains("escapes"), message) + } + } + + func testApply_pathTraversalInManifestPatchField_isRejected() throws { + try installBasicFixtures() + let baseHash = try hashOfFixture("basic/old.dat") + let targetHash = try hashOfFixture("basic/new.dat") + + XCTAssertThrowsError( + try CodePushBinaryDiffPatcher.applyBinaryDiffPatches( + manifest: manifest(baseHash: baseHash, targetHash: targetHash, patchPath: "../../../etc/passwd"), + currentPackageFolder: currentPackageFolder.path, + unzippedFolder: unzippedFolder.path, + newUpdateFolder: newUpdateFolder.path)) + } + + // The update zip is extracted before anything verifies it, so it can plant a + // symlink in the new update folder and patch through it. + func testApply_outputPathThroughSymlink_isRejectedAndWritesNothing() throws { + let nestedRelativePath = "escape/main.jsbundle" + try FileManager.default.createDirectory( + at: currentPackageFolder.appendingPathComponent("escape"), + withIntermediateDirectories: true) + try FileManager.default.copyItem( + at: fixtureURL("basic/old.dat"), + to: currentPackageFolder.appendingPathComponent(nestedRelativePath)) + try FileManager.default.copyItem( + at: fixtureURL("basic/patch.bsdiff"), + to: unzippedFolder.appendingPathComponent(patchRelativePath)) + + let outsideFolder = tempDir.appendingPathComponent("outside") + try FileManager.default.createDirectory(at: outsideFolder, withIntermediateDirectories: true) + try FileManager.default.createSymbolicLink( + at: newUpdateFolder.appendingPathComponent("escape"), + withDestinationURL: outsideFolder) + + let baseHash = try hashOfFixture("basic/old.dat") + let targetHash = try hashOfFixture("basic/new.dat") + + XCTAssertThrowsError( + try CodePushBinaryDiffPatcher.applyBinaryDiffPatches( + manifest: manifest(baseHash: baseHash, targetHash: targetHash, relativePath: nestedRelativePath), + currentPackageFolder: currentPackageFolder.path, + unzippedFolder: unzippedFolder.path, + newUpdateFolder: newUpdateFolder.path)) + XCTAssertFalse( + FileManager.default.fileExists(atPath: outsideFolder.appendingPathComponent("main.jsbundle").path)) + } + + func testApply_fileToPatchMissing_reportsTheReadFailure() throws { + // Only the patch is installed: the manifest names a file the current package does not have. + try FileManager.default.copyItem( + at: fixtureURL("basic/patch.bsdiff"), + to: unzippedFolder.appendingPathComponent(patchRelativePath)) + let baseHash = try hashOfFixture("basic/old.dat") + let targetHash = try hashOfFixture("basic/new.dat") + + XCTAssertThrowsError( + try CodePushBinaryDiffPatcher.applyBinaryDiffPatches( + manifest: manifest(baseHash: baseHash, targetHash: targetHash), + currentPackageFolder: currentPackageFolder.path, + unzippedFolder: unzippedFolder.path, + newUpdateFolder: newUpdateFolder.path)) { error in + let message = (error as NSError).localizedDescription + XCTAssertTrue(message.contains("Could not open file for reading"), message) + XCTAssertFalse(message.contains("mismatch"), message) + } + } + + func testApply_corruptPatchFile_throws() throws { + try FileManager.default.copyItem( + at: fixtureURL("basic/old.dat"), + to: currentPackageFolder.appendingPathComponent(relativePath)) + try Data([0x00, 0x01, 0x02, 0x03]).write( + to: unzippedFolder.appendingPathComponent(patchRelativePath)) + let baseHash = try hashOfFixture("basic/old.dat") + + XCTAssertThrowsError( + try CodePushBinaryDiffPatcher.applyBinaryDiffPatches( + manifest: manifest(baseHash: baseHash, targetHash: "irrelevant-not-reached-on-failure"), + currentPackageFolder: currentPackageFolder.path, + unzippedFolder: unzippedFolder.path, + newUpdateFolder: newUpdateFolder.path)) + } +} diff --git a/ios/CodePushTests/CodePushTests-Bridging-Header.h b/ios/CodePushTests/CodePushTests-Bridging-Header.h index 4ccab848..6aad74d6 100644 --- a/ios/CodePushTests/CodePushTests-Bridging-Header.h +++ b/ios/CodePushTests/CodePushTests-Bridging-Header.h @@ -1,3 +1,4 @@ #import "bspatch_bridge.h" #import "CodePushSha256.h" #import "CodePushDiffManifest.h" +#import "CodePushBinaryDiffPatcher.h"