/* * gen/main.c - APK patch generation tool * * Usage: gen.exe * * Reads two APK (ZIP) files, compares their entries, and generates * a binary patch file that can be applied by the JNI bspatch module. */ #include #include #include #include "unzip.h" #include "bsdiff.h" #include "common/patch_format.h" #include "common/zlib_stream.h" /* ---- ZIP entry storage ---- */ typedef struct { char* path; uint8_t* data; size_t size; uLong crc; } file_entry; typedef struct { file_entry* entries; int count; int capacity; } file_set; static void file_set_init(file_set* set) { set->entries = NULL; set->count = 0; set->capacity = 0; } static void file_set_free(file_set* set) { for (int i = 0; i < set->count; i++) { free(set->entries[i].path); free(set->entries[i].data); } free(set->entries); set->entries = NULL; set->count = 0; } static void file_set_add(file_set* set, const char* path, uint8_t* data, size_t size, uLong crc) { if (set->count >= set->capacity) { set->capacity = set->capacity ? set->capacity * 2 : 256; set->entries = (file_entry*)realloc(set->entries, set->capacity * sizeof(file_entry)); } set->entries[set->count].path = strdup(path); set->entries[set->count].data = data; set->entries[set->count].size = size; set->entries[set->count].crc = crc; set->count++; } static int path_cmp(const void* a, const void* b) { return strcmp(((const file_entry*)a)->path, ((const file_entry*)b)->path); } static void file_set_sort(file_set* set) { if (set->count > 0) qsort(set->entries, set->count, sizeof(file_entry), path_cmp); } /* Binary search by path (set must be sorted) */ static file_entry* file_set_find(file_set* set, const char* path) { int lo = 0, hi = set->count - 1; while (lo <= hi) { int mid = (lo + hi) / 2; int cmp = strcmp(set->entries[mid].path, path); if (cmp < 0) lo = mid + 1; else if (cmp > 0) hi = mid - 1; else return &set->entries[mid]; } return NULL; } /* ---- Read all entries from a ZIP/APK into a file_set ---- */ static int read_zip_entries(const char* path, file_set* set) { unzFile zf = unzOpen(path); if (!zf) { fprintf(stderr, "Error: cannot open %s\n", path); return -1; } unz_global_info gi; if (unzGetGlobalInfo(zf, &gi) != UNZ_OK) { fprintf(stderr, "Error: unzGetGlobalInfo failed\n"); unzClose(zf); return -1; } if (unzGoToFirstFile(zf) != UNZ_OK) { unzClose(zf); return -1; } do { unz_file_info fi; char filename[1024]; if (unzGetCurrentFileInfo(zf, &fi, filename, sizeof(filename), NULL, 0, NULL, 0) != UNZ_OK) continue; /* Skip directories */ size_t len = strlen(filename); if (len > 0 && filename[len - 1] == '/') continue; if (unzOpenCurrentFile(zf) != UNZ_OK) continue; uint8_t* data = NULL; if (fi.uncompressed_size > 0) { data = (uint8_t*)malloc(fi.uncompressed_size + 1); if (!data) { unzCloseCurrentFile(zf); continue; } int read_bytes = unzReadCurrentFile(zf, data, (unsigned)fi.uncompressed_size); if (read_bytes != (int)fi.uncompressed_size) { fprintf(stderr, "Warning: short read on %s (%d/%lu)\n", filename, read_bytes, fi.uncompressed_size); free(data); unzCloseCurrentFile(zf); continue; } } unzCloseCurrentFile(zf); file_set_add(set, filename, data, fi.uncompressed_size, fi.crc); } while (unzGoToNextFile(zf) == UNZ_OK); unzClose(zf); file_set_sort(set); return 0; } /* ---- zlib compress a buffer, returns malloc'd buffer and sets *out_size ---- */ static uint8_t* zlib_compress_buffer(const uint8_t* data, size_t in_size, uint32_t* out_size) { uLongf dest_len = compressBound((uLong)in_size); uint8_t* dest = (uint8_t*)malloc(dest_len); if (!dest) return NULL; if (compress2(dest, &dest_len, data, (uLong)in_size, Z_BEST_COMPRESSION) != Z_OK) { free(dest); return NULL; } *out_size = (uint32_t)dest_len; return dest; } /* ---- Write a COPY entry (unchanged file) ---- */ static int write_copy_entry(FILE* patch, const char* path) { return patch_write_entry_header(patch, path, ENTRY_TYPE_COPY, 0, 0, 0); } /* ---- Write a NEW entry (file only in new APK) ---- */ static int write_new_entry(FILE* patch, const char* path, const uint8_t* data, size_t size) { uint32_t comp_size = 0; uint8_t* comp_data = zlib_compress_buffer(data, size, &comp_size); if (!comp_data) return -1; int ret = patch_write_entry_header(patch, path, ENTRY_TYPE_NEW, 0, (int64_t)size, comp_size); if (ret == 0) { if (fwrite(comp_data, 1, comp_size, patch) != comp_size) ret = -1; } free(comp_data); return ret; } /* ---- Write a PATCH entry (modified file, bsdiff + zlib) ---- */ static int write_patch_entry(FILE* patch, const char* path, const uint8_t* old_data, size_t old_size, const uint8_t* new_data, size_t new_size) { /* Use tmpfile to capture bsdiff output via zlib write callbacks */ FILE* tmp = tmpfile(); if (!tmp) { fprintf(stderr, "Error: tmpfile() failed\n"); return -1; } zlib_write_ctx wctx; struct bsdiff_stream stream; zlib_setup_bsdiff_stream(&stream, &wctx, tmp); int bsdiff_ret = bsdiff(old_data, (int64_t)old_size, new_data, (int64_t)new_size, &stream); zlib_write_finish(&wctx); if (bsdiff_ret != 0) { fprintf(stderr, "Error: bsdiff failed for %s\n", path); fclose(tmp); return -1; } /* Read compressed patch data from temp file */ long comp_size = ftell(tmp); if (comp_size < 0) { fclose(tmp); return -1; } fseek(tmp, 0, SEEK_SET); uint8_t* comp_data = (uint8_t*)malloc(comp_size); if (!comp_data) { fclose(tmp); return -1; } if ((long)fread(comp_data, 1, comp_size, tmp) != comp_size) { free(comp_data); fclose(tmp); return -1; } fclose(tmp); /* Write entry header + compressed patch data */ int ret = patch_write_entry_header(patch, path, ENTRY_TYPE_PATCH, (int64_t)old_size, (int64_t)new_size, (uint32_t)comp_size); if (ret == 0) { if (fwrite(comp_data, 1, comp_size, patch) != (size_t)comp_size) ret = -1; } free(comp_data); return ret; } /* ---- main ---- */ int main(int argc, char* argv[]) { if (argc != 4) { fprintf(stderr, "Usage: %s \n", argv[0]); return 1; } const char* old_path = argv[1]; const char* new_path = argv[2]; const char* out_path = argv[3]; /* Read both APKs */ file_set old_set, new_set; file_set_init(&old_set); file_set_init(&new_set); printf("Reading old APK: %s\n", old_path); if (read_zip_entries(old_path, &old_set) != 0) return 1; printf(" %d entries\n", old_set.count); printf("Reading new APK: %s\n", new_path); if (read_zip_entries(new_path, &new_set) != 0) { file_set_free(&old_set); return 1; } printf(" %d entries\n", new_set.count); /* Open output patch file */ FILE* patch = fopen(out_path, "wb"); if (!patch) { fprintf(stderr, "Error: cannot create %s\n", out_path); file_set_free(&old_set); file_set_free(&new_set); return 1; } /* Write placeholder header (entry_count will be filled later) */ patch_write_header(patch, 0); uint32_t entry_count = 0; int stats_copy = 0, stats_patch = 0, stats_new = 0; /* Process all entries in the new APK */ for (int i = 0; i < new_set.count; i++) { file_entry* ne = &new_set.entries[i]; file_entry* oe = file_set_find(&old_set, ne->path); if (!oe) { /* New file - only in new APK */ printf(" NEW %s (%zu bytes)\n", ne->path, ne->size); if (write_new_entry(patch, ne->path, ne->data, ne->size) != 0) { fprintf(stderr, "Error writing NEW entry: %s\n", ne->path); goto cleanup; } entry_count++; stats_new++; } else if (oe->size == ne->size && oe->crc == ne->crc) { /* Unchanged - mark as COPY */ if (write_copy_entry(patch, ne->path) != 0) { fprintf(stderr, "Error writing COPY entry: %s\n", ne->path); goto cleanup; } entry_count++; stats_copy++; } else { /* Modified - generate bsdiff patch */ printf(" PATCH %s (%zu -> %zu bytes)\n", ne->path, oe->size, ne->size); if (write_patch_entry(patch, ne->path, oe->data, oe->size, ne->data, ne->size) != 0) { fprintf(stderr, "Error writing PATCH entry: %s\n", ne->path); goto cleanup; } entry_count++; stats_patch++; } } /* Seek back and write actual entry count */ fseek(patch, (long)APKDIFF_MAGIC_SIZE, SEEK_SET); fwrite(&entry_count, sizeof(uint32_t), 1, patch); printf("\nDone! Patch: %s\n", out_path); printf(" Entries: %u total (%d copy, %d patch, %d new)\n", entry_count, stats_copy, stats_patch, stats_new); cleanup: fclose(patch); file_set_free(&old_set); file_set_free(&new_set); return 0; }