Skip to content
Draft
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Supports React Native 0.77 ~ 0.86.
- **React Native**: 0.77 or higher
- **iOS**: 15.5 or higher
- **Android**: API level 16 or higher
- **Android NDK and CMake**: the Android library compiles the applier that installs binary patch updates from C sources, so building an app that depends on it needs both. A React Native project already builds native code, and the Android Gradle Plugin installs the NDK and CMake versions it is missing, so this is usually nothing to set up.

## 🚗 Migration Guide

Expand Down
19 changes: 19 additions & 0 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,27 @@ android {
defaultConfig {
consumerProguardFiles 'proguard-rules.pro'
}

// Builds the binary patch applier. Its sources are the shared ones at the root of the
// repository, which the CMake file points at; nothing is compiled twice.
externalNativeBuild {
cmake {
path "src/main/cpp/CMakeLists.txt"
}
}

testOptions {
// The unit tests exercise plain logic, so the android framework classes they touch
// in passing (logging above all) are allowed to do nothing instead of throwing.
unitTests.returnDefaultValues = true
}
}

dependencies {
implementation "com.facebook.react:react-native:+"

testImplementation "junit:junit:4.13.2"
// org.json ships as stubs in the android framework jar, and the unit tests parse real
// manifests, so they need an implementation that actually parses.
testImplementation "org.json:json:20231013"
}
5 changes: 5 additions & 0 deletions android/app/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,8 @@

# Can't find referenced class org.bouncycastle.**
-dontwarn com.nimbusds.jose.**

# The binary patch applier is found by name from the native library.
-keepclasseswithmembernames class com.microsoft.codepush.react.HDiffPatchNative {
native <methods>;
}
38 changes: 38 additions & 0 deletions android/app/src/main/cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
cmake_minimum_required(VERSION 3.13)

project(codepush-binarypatch C)

# The applier sources live outside the platform directories because the host build and
# the other platform compile the very same files. Referencing them where they are is
# what keeps the appliers of the platforms from drifting apart, so they are deliberately
# not copied into this directory.
set(BINARY_PATCH_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../cpp/binarypatch)

add_library(codepush-binarypatch SHARED
${CMAKE_CURRENT_SOURCE_DIR}/binarypatch_jni.c
${BINARY_PATCH_DIR}/binarypatch_zstd_decompressor.c
${BINARY_PATCH_DIR}/vendor/HDiffPatch/libHDiffPatch/HPatch/patch.c
${BINARY_PATCH_DIR}/vendor/zstd/common/debug.c
${BINARY_PATCH_DIR}/vendor/zstd/common/entropy_common.c
${BINARY_PATCH_DIR}/vendor/zstd/common/error_private.c
${BINARY_PATCH_DIR}/vendor/zstd/common/fse_decompress.c
${BINARY_PATCH_DIR}/vendor/zstd/common/xxhash.c
${BINARY_PATCH_DIR}/vendor/zstd/common/zstd_common.c
${BINARY_PATCH_DIR}/vendor/zstd/decompress/huf_decompress.c
${BINARY_PATCH_DIR}/vendor/zstd/decompress/zstd_ddict.c
${BINARY_PATCH_DIR}/vendor/zstd/decompress/zstd_decompress.c
${BINARY_PATCH_DIR}/vendor/zstd/decompress/zstd_decompress_block.c)

target_include_directories(codepush-binarypatch PRIVATE
${BINARY_PATCH_DIR}
${BINARY_PATCH_DIR}/vendor/HDiffPatch
${BINARY_PATCH_DIR}/vendor/zstd)

# ZSTD_DISABLE_ASM: the assembly fast path is intentionally not vendored.
# _IS_USED_MULTITHREAD=0: patches are applied on the thread that downloads them.
target_compile_definitions(codepush-binarypatch PRIVATE
ZSTD_DISABLE_ASM=1
_IS_USED_MULTITHREAD=0)

find_library(log-lib log)
target_link_libraries(codepush-binarypatch ${log-lib})
181 changes: 181 additions & 0 deletions android/app/src/main/cpp/binarypatch_jni.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
/*
* JNI entry point of the CodePush binary patch applier.
*
* The applier itself is the shared C code one directory tree up, which the host build and
* the other platform compile as well; this file only moves data across the JNI boundary
* and turns a failure into the result code `HDiffPatchNative` hands back to Java.
*
* Memory contract, the same one the host build documents:
* - the base bundle is held whole, because the patches are produced with `hdiffz -m`,
* which patches with random access to the base data
* - the patch is held whole, being far smaller than either bundle
* - the restored bundle is written sequentially to a file, so a patch session never
* holds two bundles at once
*
* A patch carries no checksum of the base data and its zstd streams carry no content
* checksum, so a successful apply here is not proof of a correct result. The caller
* verifies the base and target hashes; this file cannot.
*/

#include <jni.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

#include <android/log.h>

#include "binarypatch_zstd_decompressor.h"
#include "libHDiffPatch/HPatch/patch.h"

#define LOG_TAG "ReactNative"
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)

/* Mirrored by CodePushBinaryPatch.PatchApplier; the two lists have to stay in step. */
#define RESULT_OK 0
#define RESULT_INVALID_ARGUMENT 1
#define RESULT_IO_ERROR 2
#define RESULT_INVALID_HEADER 3
#define RESULT_UNSUPPORTED_COMPRESSION 4
#define RESULT_SIZE_MISMATCH 5
#define RESULT_APPLY_FAILED 6

/* Scratch buffer handed to patch_decompress_with_cache() to reduce stream reads. */
#define APPLY_CACHE_SIZE (1024 * 1024)

typedef struct {
FILE* file;
hpatch_StreamPos_t writtenSize;
} TSequentialFileWriter;

static hpatch_BOOL _write_sequential(const hpatch_TStreamOutput* stream,
hpatch_StreamPos_t writeToPos,
const unsigned char* data,
const unsigned char* data_end) {
TSequentialFileWriter* self = (TSequentialFileWriter*)stream->streamImport;
const size_t length = (size_t)(data_end - data);
/* patch_decompress_with_cache() only ever appends; anything else is a bug. */
if (writeToPos != self->writtenSize) {
return hpatch_FALSE;
}
if (fwrite(data, 1, length, self->file) != length) {
return hpatch_FALSE;
}
self->writtenSize += length;
return hpatch_TRUE;
}

JNIEXPORT jint JNICALL
Java_com_microsoft_codepush_react_HDiffPatchNative_applyPatch(JNIEnv* env,
jclass clazz,
jbyteArray base,
jbyteArray patch,
jstring outputPath,
jlong expectedTargetSize) {
jbyte* baseData = NULL;
jbyte* patchData = NULL;
const char* outputPathChars = NULL;
unsigned char* cache = NULL;
jsize baseSize = 0;
jsize patchSize = 0;
hpatch_compressedDiffInfo diffInfo;
hpatch_TDecompress decompressor;
hpatch_TStreamInput baseStream;
hpatch_TStreamInput patchStream;
hpatch_TStreamOutput targetStream;
TSequentialFileWriter writer;
hpatch_BOOL applied;
int result = RESULT_OK;

(void)clazz;

if ((base == NULL) || (patch == NULL) || (outputPath == NULL) || (expectedTargetSize <= 0)) {
return RESULT_INVALID_ARGUMENT;
}

baseSize = (*env)->GetArrayLength(env, base);
patchSize = (*env)->GetArrayLength(env, patch);
baseData = (*env)->GetByteArrayElements(env, base, NULL);
patchData = (*env)->GetByteArrayElements(env, patch, NULL);
outputPathChars = (*env)->GetStringUTFChars(env, outputPath, NULL);
if ((baseData == NULL) || (patchData == NULL) || (outputPathChars == NULL)) {
LOGE("[CodePush] out of memory while reading the binary patch inputs");
result = RESULT_IO_ERROR;
goto cleanup;
}

binarypatch_zstd_decompressor_init(&decompressor);

if (!getCompressedDiffInfo_mem(&diffInfo, (const unsigned char*)patchData,
(const unsigned char*)patchData + patchSize)) {
LOGE("[CodePush] the binary patch header could not be read");
result = RESULT_INVALID_HEADER;
goto cleanup;
}
if ((strlen(diffInfo.compressType) > 0) && !decompressor.is_can_open(diffInfo.compressType)) {
LOGE("[CodePush] the binary patch uses an unsupported codec: %s", diffInfo.compressType);
result = RESULT_UNSUPPORTED_COMPRESSION;
goto cleanup;
}
if (diffInfo.oldDataSize != (hpatch_StreamPos_t)baseSize) {
LOGE("[CodePush] the binary patch expects a %llu byte base bundle, this one is %llu bytes",
(unsigned long long)diffInfo.oldDataSize, (unsigned long long)baseSize);
result = RESULT_SIZE_MISMATCH;
goto cleanup;
}
if (diffInfo.newDataSize != (hpatch_StreamPos_t)expectedTargetSize) {
LOGE("[CodePush] the binary patch produces %llu bytes, the manifest promises %llu",
(unsigned long long)diffInfo.newDataSize, (unsigned long long)expectedTargetSize);
result = RESULT_SIZE_MISMATCH;
goto cleanup;
}

cache = (unsigned char*)malloc(APPLY_CACHE_SIZE);
if (cache == NULL) {
LOGE("[CodePush] out of memory while allocating the binary patch cache");
result = RESULT_IO_ERROR;
goto cleanup;
}

writer.file = fopen(outputPathChars, "wb");
writer.writtenSize = 0;
if (writer.file == NULL) {
LOGE("[CodePush] the restored bundle could not be opened for writing");
result = RESULT_IO_ERROR;
goto cleanup;
}

mem_as_hStreamInput(&baseStream, (const unsigned char*)baseData, (const unsigned char*)baseData + baseSize);
mem_as_hStreamInput(&patchStream, (const unsigned char*)patchData, (const unsigned char*)patchData + patchSize);
memset(&targetStream, 0, sizeof(targetStream));
targetStream.streamImport = &writer;
targetStream.streamSize = diffInfo.newDataSize;
targetStream.write = _write_sequential;

applied = patch_decompress_with_cache(&targetStream, &baseStream, &patchStream, &decompressor,
cache, cache + APPLY_CACHE_SIZE);
if (fclose(writer.file) != 0) {
LOGE("[CodePush] the restored bundle could not be flushed to disk");
result = RESULT_IO_ERROR;
goto cleanup;
}
if (!applied) {
LOGE("[CodePush] applying the binary patch failed (decError=%d)", (int)decompressor.decError);
result = RESULT_APPLY_FAILED;
goto cleanup;
}

cleanup:
free(cache);
if (outputPathChars != NULL) {
(*env)->ReleaseStringUTFChars(env, outputPath, outputPathChars);
}
/* JNI_ABORT: neither array is written to, so nothing has to be copied back. */
if (patchData != NULL) {
(*env)->ReleaseByteArrayElements(env, patch, patchData, JNI_ABORT);
}
if (baseData != NULL) {
(*env)->ReleaseByteArrayElements(env, base, baseData, JNI_ABORT);
}

return (jint)result;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.microsoft.codepush.react;

/**
* Outcome of restoring an update from its binary patch archive.
*
* A failed result is not an error the user hears about: it is the signal to download the
* update's full archive instead. The reason strings are the vocabulary the appliers of
* every platform report, and the logs a rollout is judged from are read for exactly these
* words, so they must not be reworded.
*/
public class BinaryPatchResult {

/** The bundle inside the app binary could not be opened or read. */
public static final String REASON_BASE_BUNDLE_UNAVAILABLE = "base_bundle_unavailable";

/** The bundle inside the app binary is not the one the patch was computed against. */
public static final String REASON_BASE_HASH_MISMATCH = "base_hash_mismatch";

/** The manifest is missing, malformed, points outside the archive, or asks for too much. */
public static final String REASON_INVALID_MANIFEST = "invalid_manifest";

/** The patch was produced by a format or a codec this client cannot apply. */
public static final String REASON_UNSUPPORTED_FORMAT = "unsupported_format";

/** The applier refused the patch, or the restored bundle could not be written. */
public static final String REASON_PATCH_APPLY_FAILED = "patch_apply_failed";

/** The restored bundle is not the one the manifest promised. */
public static final String REASON_TARGET_VERIFICATION_FAILED = "target_verification_failed";

private final boolean mSucceeded;
private final String mFailureReason;

private BinaryPatchResult(boolean succeeded, String failureReason) {
mSucceeded = succeeded;
mFailureReason = failureReason;
}

public static BinaryPatchResult success() {
return new BinaryPatchResult(true, null);
}

public static BinaryPatchResult failure(String failureReason) {
return new BinaryPatchResult(false, failureReason);
}

public boolean succeeded() {
return mSucceeded;
}

/** Why the full archive has to be downloaded instead, or null when the patch was applied. */
public String getFailureReason() {
return mFailureReason;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public static synchronized CodePush getInstance(Context context, boolean isDebug
private CodePush(Context context, boolean isDebugMode) {
mContext = context.getApplicationContext();

mUpdateManager = new CodePushUpdateManager(context.getFilesDir().getAbsolutePath());
mUpdateManager = new CodePushUpdateManager(context.getFilesDir().getAbsolutePath(), mContext);
mTelemetryManager = new CodePushTelemetryManager(mContext);
mDeploymentKey = "deprecated_deployment_key";
mIsDebugMode = isDebugMode;
Expand Down
Loading
Loading