Files
QZMusic/apkdiff/jni/apkpatch.c

300 lines
8.9 KiB
C

/*
* jni/apkpatch.c - Android JNI: apply APK patch
*
* Java class: love.qz.apkpatcher.Patcher
* Native method: applyPatch(String oldApkPath, String patchPath, String newApkPath)
*
* Reads the patch file, opens the old APK, and produces a new APK
* by applying per-file bsdiff patches, copying unchanged files,
* and adding new files.
*/
#include <jni.h>
#include <android/log.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "unzip.h"
#include "zip.h"
#include "bspatch.h"
#include "common/patch_format.h"
#include "common/zlib_stream.h"
#define LOG_TAG "ApkPatch"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
/* Read a file entry from a ZIP into a malloc'd buffer.
* Returns 0 on success, -1 on failure. Caller must free *out_data. */
static int read_zip_entry(unzFile zf, const char* path,
uint8_t** out_data, size_t* out_size)
{
if (unzLocateFile(zf, path, 1) != UNZ_OK)
return -1;
unz_file_info fi;
if (unzGetCurrentFileInfo(zf, &fi, NULL, 0, NULL, 0, NULL, 0) != UNZ_OK)
return -1;
if (unzOpenCurrentFile(zf) != UNZ_OK)
return -1;
uint8_t* data = NULL;
if (fi.uncompressed_size > 0) {
data = (uint8_t*)malloc(fi.uncompressed_size);
if (!data) {
unzCloseCurrentFile(zf);
return -1;
}
int rd = unzReadCurrentFile(zf, data, (unsigned)fi.uncompressed_size);
if (rd != (int)fi.uncompressed_size) {
free(data);
unzCloseCurrentFile(zf);
return -1;
}
}
unzCloseCurrentFile(zf);
*out_data = data;
*out_size = fi.uncompressed_size;
return 0;
}
/* Write data to a new ZIP entry using deflate compression.
* Returns 0 on success, -1 on failure. */
static int write_zip_entry(zipFile zf_out, const char* path,
const uint8_t* data, size_t size)
{
zip_fileinfo zi;
memset(&zi, 0, sizeof(zi));
int method = (size > 0) ? Z_DEFLATED : 0;
if (zipOpenNewFileInZip(zf_out, path, &zi,
NULL, 0, NULL, 0, NULL,
method, Z_DEFAULT_COMPRESSION) != ZIP_OK)
return -1;
if (size > 0) {
if (zipWriteInFileInZip(zf_out, data, (unsigned)size) != ZIP_OK) {
zipCloseFileInZip(zf_out);
return -1;
}
}
zipCloseFileInZip(zf_out);
return 0;
}
/* zlib decompress a buffer. Returns malloc'd buffer or NULL. */
static uint8_t* zlib_decompress_buffer(const uint8_t* comp_data, uint32_t comp_size,
int64_t orig_size)
{
uint8_t* out = (uint8_t*)malloc(orig_size + 1);
if (!out) return NULL;
zlib_read_ctx rctx;
if (zlib_read_init(&rctx, comp_data, comp_size) != 0) {
free(out);
return NULL;
}
rctx.zs.next_out = (Bytef*)out;
rctx.zs.avail_out = (uInt)orig_size;
int ret;
do {
ret = inflate(&rctx.zs, Z_NO_FLUSH);
if (ret != Z_OK && ret != Z_STREAM_END) {
zlib_read_cleanup(&rctx);
free(out);
return NULL;
}
} while (ret != Z_STREAM_END);
zlib_read_cleanup(&rctx);
return out;
}
/* Apply a single PATCH entry: pass compressed patch data to bspatch via zlib stream. */
static int apply_patch_entry(const uint8_t* old_data, size_t old_size,
const uint8_t* comp_patch, uint32_t comp_patch_size,
int64_t new_size,
uint8_t** out_new_data)
{
uint8_t* new_data = (uint8_t*)malloc(new_size + 1);
if (!new_data) return -1;
/* Pass compressed data directly to bspatch; zlib_read_cb handles decompression */
zlib_read_ctx rctx;
struct bspatch_stream stream;
zlib_setup_bspatch_stream(&stream, &rctx, comp_patch, comp_patch_size);
int bspatch_ret = bspatch(old_data, (int64_t)old_size,
new_data, new_size, &stream);
zlib_read_cleanup(&rctx);
if (bspatch_ret != 0) {
free(new_data);
return -1;
}
*out_new_data = new_data;
return 0;
}
/*
* JNI: com.apkdiff.Patcher.applyPatch
* Returns: 0 = success, -1 = error
*/
JNIEXPORT jint JNICALL
Java_love_qz_apkpatcher_Patcher_applyPatch(JNIEnv* env, jclass clazz,
jstring jOldApkPath,
jstring jPatchPath,
jstring jNewApkPath)
{
(void)clazz;
const char* old_apk_path = (*env)->GetStringUTFChars(env, jOldApkPath, NULL);
const char* patch_path = (*env)->GetStringUTFChars(env, jPatchPath, NULL);
const char* new_apk_path = (*env)->GetStringUTFChars(env, jNewApkPath, NULL);
int result = -1;
FILE* patch_fp = NULL;
unzFile zf_old = NULL;
zipFile zf_new = NULL;
LOGI("applyPatch: %s + %s -> %s", old_apk_path, patch_path, new_apk_path);
/* Open patch file */
patch_fp = fopen(patch_path, "rb");
if (!patch_fp) {
LOGE("Cannot open patch file: %s", patch_path);
goto done;
}
/* Read and validate patch header */
uint32_t entry_count = 0;
if (patch_read_header(patch_fp, &entry_count) != 0) {
LOGE("Invalid patch file format");
goto done;
}
LOGI("Patch: %u entries", entry_count);
/* Open old APK */
zf_old = unzOpen(old_apk_path);
if (!zf_old) {
LOGE("Cannot open old APK: %s", old_apk_path);
goto done;
}
/* Create new APK */
zf_new = zipOpen(new_apk_path, APPEND_STATUS_CREATE);
if (!zf_new) {
LOGE("Cannot create new APK: %s", new_apk_path);
goto done;
}
/* Process each entry */
for (uint32_t i = 0; i < entry_count; i++) {
char* path = NULL;
uint8_t type;
int64_t old_size, new_size;
uint32_t comp_size;
if (patch_read_entry_header(patch_fp, &path, &type,
&old_size, &new_size, &comp_size) != 0) {
LOGE("Error reading entry %u", i);
goto done;
}
uint8_t* comp_data = NULL;
if (comp_size > 0) {
comp_data = (uint8_t*)malloc(comp_size);
if (!comp_data || fread(comp_data, 1, comp_size, patch_fp) != comp_size) {
LOGE("Error reading compressed data for %s", path);
free(comp_data);
free(path);
goto done;
}
}
int entry_ok = 0;
switch (type) {
case ENTRY_TYPE_COPY: {
/* Copy unchanged file from old APK */
uint8_t* data = NULL;
size_t size = 0;
if (read_zip_entry(zf_old, path, &data, &size) == 0) {
entry_ok = (write_zip_entry(zf_new, path, data, size) == 0);
free(data);
} else {
LOGE("COPY: file not found in old APK: %s", path);
}
break;
}
case ENTRY_TYPE_NEW: {
/* Decompress and write new file */
uint8_t* data = zlib_decompress_buffer(comp_data, comp_size, new_size);
if (data) {
entry_ok = (write_zip_entry(zf_new, path, data, (size_t)new_size) == 0);
free(data);
} else {
LOGE("NEW: decompress failed for %s", path);
}
break;
}
case ENTRY_TYPE_PATCH: {
/* Read old file, apply patch, write new file */
uint8_t* old_data = NULL;
size_t old_file_size = 0;
if (read_zip_entry(zf_old, path, &old_data, &old_file_size) != 0) {
LOGE("PATCH: file not found in old APK: %s", path);
break;
}
uint8_t* new_data = NULL;
if (apply_patch_entry(old_data, old_file_size,
comp_data, comp_size,
new_size, &new_data) == 0) {
entry_ok = (write_zip_entry(zf_new, path, new_data, (size_t)new_size) == 0);
free(new_data);
} else {
LOGE("PATCH: bspatch failed for %s", path);
}
free(old_data);
break;
}
default:
LOGE("Unknown entry type %d for %s", type, path);
break;
}
free(comp_data);
free(path);
if (!entry_ok) {
LOGE("Failed to process entry %u", i);
goto done;
}
}
result = 0;
LOGI("Patch applied successfully -> %s", new_apk_path);
done:
if (zf_new) zipClose(zf_new, NULL);
if (zf_old) unzClose(zf_old);
if (patch_fp) fclose(patch_fp);
(*env)->ReleaseStringUTFChars(env, jOldApkPath, old_apk_path);
(*env)->ReleaseStringUTFChars(env, jPatchPath, patch_path);
(*env)->ReleaseStringUTFChars(env, jNewApkPath, new_apk_path);
return result;
}