From 5d17d72621acad4aa216cf6a05c7f46b8206f284 Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Thu, 9 Jul 2015 19:22:28 -0500 Subject: [PATCH] patch parsing: parse binary patch files --- src/patch.c | 154 +++++++++++++++++++++++++++++++-- tests/apply/fromfile.c | 188 ++++++++++++++++++++++++++++------------- 2 files changed, 273 insertions(+), 69 deletions(-) diff --git a/src/patch.c b/src/patch.c index 9999fa24d..f6eceac2d 100644 --- a/src/patch.c +++ b/src/patch.c @@ -65,6 +65,15 @@ static int parse_advance_ws(patch_parse_ctx *ctx) return ret; } +static int parse_advance_nl(patch_parse_ctx *ctx) +{ + if (ctx->line_len != 1 || ctx->line[0] != '\n') + return -1; + + parse_advance_line(ctx); + return 0; +} + static int header_path_len(patch_parse_ctx *ctx) { bool inquote = 0; @@ -354,6 +363,7 @@ typedef struct { static const header_git_op header_git_ops[] = { { "@@ -", NULL }, + { "GIT binary patch", NULL }, { "--- ", parse_header_git_oldpath }, { "+++ ", parse_header_git_newpath }, { "index ", parse_header_git_index }, @@ -426,7 +436,7 @@ done: return error; } -static int parse_number(int *out, patch_parse_ctx *ctx) +static int parse_number(git_off_t *out, patch_parse_ctx *ctx) { const char *end; int64_t num; @@ -440,12 +450,23 @@ static int parse_number(int *out, patch_parse_ctx *ctx) if (num < 0) return -1; - *out = (int)num; + *out = num; parse_advance_chars(ctx, (end - ctx->line)); return 0; } +static int parse_int(int *out, patch_parse_ctx *ctx) +{ + git_off_t num; + + if (parse_number(&num, ctx) < 0 || !git__is_int(num)) + return -1; + + *out = (int)num; + return 0; +} + static int parse_hunk_header( diff_patch_hunk *hunk, patch_parse_ctx *ctx) @@ -456,22 +477,22 @@ static int parse_hunk_header( hunk->hunk.new_lines = 1; if (parse_advance_expected(ctx, "@@ -", 4) < 0 || - parse_number(&hunk->hunk.old_start, ctx) < 0) + parse_int(&hunk->hunk.old_start, ctx) < 0) goto fail; if (ctx->line_len > 0 && ctx->line[0] == ',') { if (parse_advance_expected(ctx, ",", 1) < 0 || - parse_number(&hunk->hunk.old_lines, ctx) < 0) + parse_int(&hunk->hunk.old_lines, ctx) < 0) goto fail; } if (parse_advance_expected(ctx, " +", 2) < 0 || - parse_number(&hunk->hunk.new_start, ctx) < 0) + parse_int(&hunk->hunk.new_start, ctx) < 0) goto fail; if (ctx->line_len > 0 && ctx->line[0] == ',') { if (parse_advance_expected(ctx, ",", 1) < 0 || - parse_number(&hunk->hunk.new_lines, ctx) < 0) + parse_int(&hunk->hunk.new_lines, ctx) < 0) goto fail; } @@ -672,7 +693,110 @@ done: return error; } -static int parse_patch_body( +static int parse_patch_binary_side( + git_diff_binary_file *binary, + patch_parse_ctx *ctx) +{ + git_diff_binary_t type = GIT_DIFF_BINARY_NONE; + git_buf base85 = GIT_BUF_INIT, decoded = GIT_BUF_INIT; + git_off_t len; + int error = 0; + + if (ctx->line_len >= 8 && memcmp(ctx->line, "literal ", 8) == 0) { + type = GIT_DIFF_BINARY_LITERAL; + parse_advance_chars(ctx, 8); + } else if (ctx->line_len >= 6 && memcmp(ctx->line, "delta ", 6) == 0) { + type = GIT_DIFF_BINARY_DELTA; + parse_advance_chars(ctx, 6); + } else { + error = parse_err("unknown binary delta type at line %d", ctx->line_num); + goto done; + } + + if (parse_number(&len, ctx) < 0 || parse_advance_nl(ctx) < 0 || len < 0) { + error = parse_err("invalid binary size at line %d", ctx->line_num); + goto done; + } + + while (ctx->line_len) { + char c = ctx->line[0]; + size_t encoded_len, decoded_len = 0, decoded_orig = decoded.size; + + if (c == '\n') + break; + else if (c >= 'A' && c <= 'Z') + decoded_len = c - 'A' + 1; + else if (c >= 'a' && c <= 'z') + decoded_len = c - 'a' + 26 + 1; + + if (!decoded_len) { + error = parse_err("invalid binary length at line %d", ctx->line_num); + goto done; + } + + parse_advance_chars(ctx, 1); + + encoded_len = ((decoded_len / 4) + !!(decoded_len % 4)) * 5; + + if (encoded_len > ctx->line_len - 1) { + error = parse_err("truncated binary data at line %d", ctx->line_num); + goto done; + } + + if ((error = git_buf_decode_base85( + &decoded, ctx->line, encoded_len, decoded_len)) < 0) + goto done; + + if (decoded.size - decoded_orig != decoded_len) { + error = parse_err("truncated binary data at line %d", ctx->line_num); + goto done; + } + + parse_advance_chars(ctx, encoded_len); + + if (parse_advance_nl(ctx) < 0) { + error = parse_err("trailing data at line %d", ctx->line_num); + goto done; + } + } + + binary->type = type; + binary->inflatedlen = (size_t)len; + binary->datalen = decoded.size; + binary->data = git_buf_detach(&decoded); + +done: + git_buf_free(&base85); + git_buf_free(&decoded); + return error; +} + +static int parse_patch_binary( + git_patch *patch, + patch_parse_ctx *ctx) +{ + int error; + + if (parse_advance_expected(ctx, "GIT binary patch", 16) < 0 || + parse_advance_nl(ctx) < 0) + return parse_err("corrupt git binary header at line %d", ctx->line_num); + + /* parse old->new binary diff */ + if ((error = parse_patch_binary_side(&patch->binary.new_file, ctx)) < 0) + return error; + + if (parse_advance_nl(ctx) < 0) + return parse_err("corrupt git binary separator at line %d", ctx->line_num); + + /* parse new->old binary diff */ + if ((error = parse_patch_binary_side(&patch->binary.old_file, ctx)) < 0) + return error; + + patch->delta->flags |= GIT_DIFF_FLAG_BINARY; + return 0; +} + +static int parse_patch_hunks( git_patch *patch, patch_parse_ctx *ctx) { @@ -698,6 +822,17 @@ done: return error; } +static int parse_patch_body(git_patch *patch, patch_parse_ctx *ctx) +{ + if (ctx->line_len >= 16 && memcmp(ctx->line, "GIT binary patch", 16) == 0) + return parse_patch_binary(patch, ctx); + + else if (ctx->line_len >= 4 && memcmp(ctx->line, "@@ -", 4) == 0) + return parse_patch_hunks(patch, ctx); + + return 0; +} + static int check_patch(git_patch *patch) { if (!patch->ofile.file->path && patch->delta->status != GIT_DELTA_ADDED) @@ -712,8 +847,9 @@ static int check_patch(git_patch *patch) } if (patch->delta->status == GIT_DELTA_MODIFIED && - patch->nfile.file->mode == patch->ofile.file->mode && - git_array_size(patch->hunks) == 0) + !(patch->delta->flags & GIT_DIFF_FLAG_BINARY) && + patch->nfile.file->mode == patch->ofile.file->mode && + git_array_size(patch->hunks) == 0) return parse_err("patch with no hunks"); return 0; diff --git a/tests/apply/fromfile.c b/tests/apply/fromfile.c index 75b318523..4b1bacddb 100644 --- a/tests/apply/fromfile.c +++ b/tests/apply/fromfile.c @@ -21,7 +21,9 @@ void test_apply_fromfile__cleanup(void) static int apply_patchfile( const char *old, + size_t old_len, const char *new, + size_t new_len, const char *patchfile, const char *filename_expected, unsigned int mode_expected) @@ -35,13 +37,11 @@ static int apply_patchfile( cl_git_pass(git_patch_from_patchfile(&patch, patchfile, strlen(patchfile))); - error = git_apply__patch(&result, &filename, &mode, old, old ? strlen(old) : 0, patch); + error = git_apply__patch(&result, &filename, &mode, old, old_len, patch); if (error == 0) { - if (new == NULL) - cl_assert_equal_i(0, result.size); - else - cl_assert_equal_s(new, result.ptr); + cl_assert_equal_i(new_len, result.size); + cl_assert(memcmp(new, result.ptr, new_len) == 0); cl_assert_equal_s(filename_expected, filename); cl_assert_equal_i(mode_expected, mode); @@ -57,7 +57,9 @@ static int apply_patchfile( static int validate_and_apply_patchfile( const char *old, + size_t old_len, const char *new, + size_t new_len, const char *patchfile, const git_diff_options *diff_opts, const char *filename_expected, @@ -68,14 +70,14 @@ static int validate_and_apply_patchfile( int error; cl_git_pass(git_patch_from_buffers(&patch_fromdiff, - old, old ? strlen(old) : 0, "file.txt", - new, new ? strlen(new) : 0, "file.txt", + old, old_len, "file.txt", + new, new_len, "file.txt", diff_opts)); cl_git_pass(git_patch_to_buf(&validated, patch_fromdiff)); cl_assert_equal_s(patchfile, validated.ptr); - error = apply_patchfile(old, new, patchfile, filename_expected, mode_expected); + error = apply_patchfile(old, old_len, new, new_len, patchfile, filename_expected, mode_expected); git_buf_free(&validated); git_patch_free(patch_fromdiff); @@ -85,8 +87,10 @@ static int validate_and_apply_patchfile( void test_apply_fromfile__change_middle(void) { - cl_git_pass(validate_and_apply_patchfile(FILE_ORIGINAL, - FILE_CHANGE_MIDDLE, PATCH_ORIGINAL_TO_CHANGE_MIDDLE, NULL, + cl_git_pass(validate_and_apply_patchfile( + FILE_ORIGINAL, strlen(FILE_ORIGINAL), + FILE_CHANGE_MIDDLE, strlen(FILE_CHANGE_MIDDLE), + PATCH_ORIGINAL_TO_CHANGE_MIDDLE, NULL, "b/file.txt", 0100644)); } @@ -95,28 +99,36 @@ void test_apply_fromfile__change_middle_nocontext(void) git_diff_options diff_opts = GIT_DIFF_OPTIONS_INIT; diff_opts.context_lines = 0; - cl_git_pass(validate_and_apply_patchfile(FILE_ORIGINAL, - FILE_CHANGE_MIDDLE, PATCH_ORIGINAL_TO_CHANGE_MIDDLE_NOCONTEXT, + cl_git_pass(validate_and_apply_patchfile( + FILE_ORIGINAL, strlen(FILE_ORIGINAL), + FILE_CHANGE_MIDDLE, strlen(FILE_CHANGE_MIDDLE), + PATCH_ORIGINAL_TO_CHANGE_MIDDLE_NOCONTEXT, &diff_opts, "b/file.txt", 0100644)); } void test_apply_fromfile__change_firstline(void) { - cl_git_pass(validate_and_apply_patchfile(FILE_ORIGINAL, - FILE_CHANGE_FIRSTLINE, PATCH_ORIGINAL_TO_CHANGE_FIRSTLINE, NULL, + cl_git_pass(validate_and_apply_patchfile( + FILE_ORIGINAL, strlen(FILE_ORIGINAL), + FILE_CHANGE_FIRSTLINE, strlen(FILE_CHANGE_FIRSTLINE), + PATCH_ORIGINAL_TO_CHANGE_FIRSTLINE, NULL, "b/file.txt", 0100644)); } void test_apply_fromfile__lastline(void) { - cl_git_pass(validate_and_apply_patchfile(FILE_ORIGINAL, - FILE_CHANGE_LASTLINE, PATCH_ORIGINAL_TO_CHANGE_LASTLINE, NULL, + cl_git_pass(validate_and_apply_patchfile( + FILE_ORIGINAL, strlen(FILE_ORIGINAL), + FILE_CHANGE_LASTLINE, strlen(FILE_CHANGE_LASTLINE), + PATCH_ORIGINAL_TO_CHANGE_LASTLINE, NULL, "b/file.txt", 0100644)); } void test_apply_fromfile__prepend(void) { - cl_git_pass(validate_and_apply_patchfile(FILE_ORIGINAL, FILE_PREPEND, + cl_git_pass(validate_and_apply_patchfile( + FILE_ORIGINAL, strlen(FILE_ORIGINAL), + FILE_PREPEND, strlen(FILE_PREPEND), PATCH_ORIGINAL_TO_PREPEND, NULL, "b/file.txt", 0100644)); } @@ -125,14 +137,18 @@ void test_apply_fromfile__prepend_nocontext(void) git_diff_options diff_opts = GIT_DIFF_OPTIONS_INIT; diff_opts.context_lines = 0; - cl_git_pass(validate_and_apply_patchfile(FILE_ORIGINAL, FILE_PREPEND, + cl_git_pass(validate_and_apply_patchfile( + FILE_ORIGINAL, strlen(FILE_ORIGINAL), + FILE_PREPEND, strlen(FILE_PREPEND), PATCH_ORIGINAL_TO_PREPEND_NOCONTEXT, &diff_opts, "b/file.txt", 0100644)); } void test_apply_fromfile__append(void) { - cl_git_pass(validate_and_apply_patchfile(FILE_ORIGINAL, FILE_APPEND, + cl_git_pass(validate_and_apply_patchfile( + FILE_ORIGINAL, strlen(FILE_ORIGINAL), + FILE_APPEND, strlen(FILE_APPEND), PATCH_ORIGINAL_TO_APPEND, NULL, "b/file.txt", 0100644)); } @@ -141,82 +157,108 @@ void test_apply_fromfile__append_nocontext(void) git_diff_options diff_opts = GIT_DIFF_OPTIONS_INIT; diff_opts.context_lines = 0; - cl_git_pass(validate_and_apply_patchfile(FILE_ORIGINAL, FILE_APPEND, + cl_git_pass(validate_and_apply_patchfile( + FILE_ORIGINAL, strlen(FILE_ORIGINAL), + FILE_APPEND, strlen(FILE_APPEND), PATCH_ORIGINAL_TO_APPEND_NOCONTEXT, &diff_opts, "b/file.txt", 0100644)); } void test_apply_fromfile__prepend_and_append(void) { - cl_git_pass(validate_and_apply_patchfile(FILE_ORIGINAL, - FILE_PREPEND_AND_APPEND, PATCH_ORIGINAL_TO_PREPEND_AND_APPEND, NULL, + cl_git_pass(validate_and_apply_patchfile( + FILE_ORIGINAL, strlen(FILE_ORIGINAL), + FILE_PREPEND_AND_APPEND, strlen(FILE_PREPEND_AND_APPEND), + PATCH_ORIGINAL_TO_PREPEND_AND_APPEND, NULL, "b/file.txt", 0100644)); } void test_apply_fromfile__to_empty_file(void) { - cl_git_pass(validate_and_apply_patchfile(FILE_ORIGINAL, "", + cl_git_pass(validate_and_apply_patchfile( + FILE_ORIGINAL, strlen(FILE_ORIGINAL), + "", 0, PATCH_ORIGINAL_TO_EMPTY_FILE, NULL, "b/file.txt", 0100644)); } void test_apply_fromfile__from_empty_file(void) { - cl_git_pass(validate_and_apply_patchfile("", FILE_ORIGINAL, + cl_git_pass(validate_and_apply_patchfile( + "", 0, + FILE_ORIGINAL, strlen(FILE_ORIGINAL), PATCH_EMPTY_FILE_TO_ORIGINAL, NULL, "b/file.txt", 0100644)); } void test_apply_fromfile__add(void) { - cl_git_pass(validate_and_apply_patchfile(NULL, FILE_ORIGINAL, + cl_git_pass(validate_and_apply_patchfile( + NULL, 0, + FILE_ORIGINAL, strlen(FILE_ORIGINAL), PATCH_ADD_ORIGINAL, NULL, "b/file.txt", 0100644)); } void test_apply_fromfile__delete(void) { - cl_git_pass(validate_and_apply_patchfile(FILE_ORIGINAL, NULL, + cl_git_pass(validate_and_apply_patchfile( + FILE_ORIGINAL, strlen(FILE_ORIGINAL), + NULL, 0, PATCH_DELETE_ORIGINAL, NULL, NULL, 0)); } void test_apply_fromfile__rename_exact(void) { - cl_git_pass(apply_patchfile(FILE_ORIGINAL, FILE_ORIGINAL, + cl_git_pass(apply_patchfile( + FILE_ORIGINAL, strlen(FILE_ORIGINAL), + FILE_ORIGINAL, strlen(FILE_ORIGINAL), PATCH_RENAME_EXACT, "b/newfile.txt", 0100644)); } void test_apply_fromfile__rename_similar(void) { - cl_git_pass(apply_patchfile(FILE_ORIGINAL, FILE_CHANGE_MIDDLE, + cl_git_pass(apply_patchfile( + FILE_ORIGINAL, strlen(FILE_ORIGINAL), + FILE_CHANGE_MIDDLE, strlen(FILE_CHANGE_MIDDLE), PATCH_RENAME_SIMILAR, "b/newfile.txt", 0100644)); } void test_apply_fromfile__rename_similar_quotedname(void) { - cl_git_pass(apply_patchfile(FILE_ORIGINAL, FILE_CHANGE_MIDDLE, + cl_git_pass(apply_patchfile( + FILE_ORIGINAL, strlen(FILE_ORIGINAL), + FILE_CHANGE_MIDDLE, strlen(FILE_CHANGE_MIDDLE), PATCH_RENAME_SIMILAR_QUOTEDNAME, "b/foo\"bar.txt", 0100644)); } void test_apply_fromfile__modechange(void) { - cl_git_pass(apply_patchfile(FILE_ORIGINAL, FILE_ORIGINAL, + cl_git_pass(apply_patchfile( + FILE_ORIGINAL, strlen(FILE_ORIGINAL), + FILE_ORIGINAL, strlen(FILE_ORIGINAL), PATCH_MODECHANGE_UNCHANGED, "b/file.txt", 0100755)); } void test_apply_fromfile__modechange_with_modification(void) { - cl_git_pass(apply_patchfile(FILE_ORIGINAL, FILE_CHANGE_MIDDLE, + cl_git_pass(apply_patchfile( + FILE_ORIGINAL, strlen(FILE_ORIGINAL), + FILE_CHANGE_MIDDLE, strlen(FILE_CHANGE_MIDDLE), PATCH_MODECHANGE_MODIFIED, "b/file.txt", 0100755)); } void test_apply_fromfile__noisy(void) { - cl_git_pass(apply_patchfile(FILE_ORIGINAL, FILE_CHANGE_MIDDLE, + cl_git_pass(apply_patchfile( + FILE_ORIGINAL, strlen(FILE_ORIGINAL), + FILE_CHANGE_MIDDLE, strlen(FILE_CHANGE_MIDDLE), PATCH_NOISY, "b/file.txt", 0100644)); } void test_apply_fromfile__noisy_nocontext(void) { - cl_git_pass(apply_patchfile(FILE_ORIGINAL, FILE_CHANGE_MIDDLE, + cl_git_pass(apply_patchfile( + FILE_ORIGINAL, strlen(FILE_ORIGINAL), + FILE_CHANGE_MIDDLE, strlen(FILE_CHANGE_MIDDLE), PATCH_NOISY_NOCONTEXT, "b/file.txt", 0100644)); } @@ -250,15 +292,19 @@ void test_apply_fromfile__fail_corrupt_githeader(void) void test_apply_fromfile__empty_context(void) { - cl_git_pass(apply_patchfile(FILE_EMPTY_CONTEXT_ORIGINAL, - FILE_EMPTY_CONTEXT_MODIFIED, PATCH_EMPTY_CONTEXT, + cl_git_pass(apply_patchfile( + FILE_EMPTY_CONTEXT_ORIGINAL, strlen(FILE_EMPTY_CONTEXT_ORIGINAL), + FILE_EMPTY_CONTEXT_MODIFIED, strlen(FILE_EMPTY_CONTEXT_MODIFIED), + PATCH_EMPTY_CONTEXT, "b/file.txt", 0100644)); } void test_apply_fromfile__append_no_nl(void) { cl_git_pass(validate_and_apply_patchfile( - FILE_ORIGINAL, FILE_APPEND_NO_NL, PATCH_APPEND_NO_NL, NULL, "b/file.txt", 0100644)); + FILE_ORIGINAL, strlen(FILE_ORIGINAL), + FILE_APPEND_NO_NL, strlen(FILE_APPEND_NO_NL), + PATCH_APPEND_NO_NL, NULL, "b/file.txt", 0100644)); } void test_apply_fromfile__fail_missing_new_file(void) @@ -300,29 +346,51 @@ void test_apply_fromfile__fail_not_a_patch(void) strlen(PATCH_NOT_A_PATCH))); } -/* -void test_apply_fromdiff__binary_change_must_be_reversible(void) +void test_apply_fromfile__binary_add(void) { - git_buf original = GIT_BUF_INIT, modified = GIT_BUF_INIT, - result = GIT_BUF_INIT; - char *filename; - unsigned int mode; - - original.ptr = FILE_BINARY_DELTA_ORIGINAL; - original.size = FILE_BINARY_DELTA_ORIGINAL_LEN; - - modified.ptr = FILE_BINARY_DELTA_MODIFIED; - modified.size = FILE_BINARY_DELTA_MODIFIED_LEN; - - cl_git_fail(git_apply__patch(&result, &filename, &mode, old ? old->ptr : NULL, old ? old->size : 0, patch); - - cl_git_fail(apply_gitbuf( - &original, "binary.bin", - &modified, "binary.bin", - PATCH_BINARY_NOT_REVERSIBLE, &binary_opts)); - cl_assert_equal_s("binary patch did not apply cleanly", giterr_last()->message); - - git_buf_free(&result); - git__free(filename); + cl_git_pass(apply_patchfile( + NULL, 0, + FILE_BINARY_DELTA_MODIFIED, FILE_BINARY_DELTA_MODIFIED_LEN, + PATCH_BINARY_ADD, "b/binary.bin", 0100644)); +} + +void test_apply_fromfile__binary_change_delta(void) +{ + cl_git_pass(apply_patchfile( + FILE_BINARY_DELTA_ORIGINAL, FILE_BINARY_DELTA_ORIGINAL_LEN, + FILE_BINARY_DELTA_MODIFIED, FILE_BINARY_DELTA_MODIFIED_LEN, + PATCH_BINARY_DELTA, "b/binary.bin", 0100644)); +} + +void test_apply_fromfile__binary_change_literal(void) +{ + cl_git_pass(apply_patchfile( + FILE_BINARY_LITERAL_ORIGINAL, FILE_BINARY_LITERAL_ORIGINAL_LEN, + FILE_BINARY_LITERAL_MODIFIED, FILE_BINARY_LITERAL_MODIFIED_LEN, + PATCH_BINARY_LITERAL, "b/binary.bin", 0100644)); +} + +void test_apply_fromfile__binary_delete(void) +{ + cl_git_pass(apply_patchfile( + FILE_BINARY_DELTA_MODIFIED, FILE_BINARY_DELTA_MODIFIED_LEN, + NULL, 0, + PATCH_BINARY_DELETE, NULL, 0)); +} + +void test_apply_fromfile__binary_change_does_not_apply(void) +{ + /* try to apply patch backwards, ensure it does not apply */ + cl_git_fail(apply_patchfile( + FILE_BINARY_DELTA_MODIFIED, FILE_BINARY_DELTA_MODIFIED_LEN, + FILE_BINARY_DELTA_ORIGINAL, FILE_BINARY_DELTA_ORIGINAL_LEN, + PATCH_BINARY_DELTA, "b/binary.bin", 0100644)); +} + +void test_apply_fromfile__binary_change_must_be_reversible(void) +{ + cl_git_fail(apply_patchfile( + FILE_BINARY_DELTA_MODIFIED, FILE_BINARY_DELTA_MODIFIED_LEN, + NULL, 0, + PATCH_BINARY_NOT_REVERSIBLE, NULL, 0)); } -*/