rizin/librz/diff/unified_diff.c

491 lines
13 KiB
C

// SPDX-FileCopyrightText: 2021 RizinOrg <info@rizin.re>
// SPDX-FileCopyrightText: 2021 deroad <wargio@libero.it>
// SPDX-License-Identifier: LGPL-3.0-only
#include <rz_cons.h>
#define Color_RANGE Color_BBLUE
#define Color_INSERT Color_BGREEN
#define Color_DELETE Color_BRED
#define Color_BGINSERT "\x1b[48;5;22m"
#define Color_BGDELETE "\x1b[48;5;52m"
#define RZ_DIFF_DEFAULT_N_GROUPS_BYTES 8
#define FAST_MOD2(x, y) ((x) & (y - 1))
#define FAST_MOD64(x) FAST_MOD2(x, 64)
#define DIFF_COLOR(prefix) (prefix == '+' ? Color_INSERT : (prefix == '-' ? Color_DELETE : ""))
#define DIFF_BGCOLOR(prefix) (prefix == '+' ? Color_BGINSERT : (prefix == '-' ? Color_BGDELETE : ""))
static inline ut32 count_newlines(RzDiff *diff, const void *array, st32 beg, st32 end) {
RzDiffMethodElemAt elem_at = diff->methods.elem_at;
RzDiffMethodStringify stringify = diff->methods.stringify;
int len = 0;
ut32 count = 0;
const char *p;
const void *elem;
RzStrBuf tmp;
for (st32 i = beg; i < end; ++i) {
rz_strbuf_init(&tmp);
elem = elem_at(array, i);
stringify(elem, &tmp);
len = rz_strbuf_length(&tmp);
p = rz_strbuf_get(&tmp);
if (len > 0 && p[len - 1] == '\n') {
count++;
}
rz_strbuf_fini(&tmp);
}
return count;
}
static inline void diff_unified_append_ranges(RzList /*<RzDiffOp *>*/ *opcodes, RzStrBuf *sb, bool color) {
const char *color_beg = color ? Color_RANGE : "";
const char *color_end = color ? Color_RESET : "";
RzDiffOp *first = rz_list_first_val(opcodes);
RzDiffOp *last = rz_list_last_val(opcodes);
st32 a_len = last->a_end - first->a_beg;
st32 b_len = last->b_end - first->b_beg;
rz_strbuf_appendf(sb, "%s@@ -%d,%d +%d,%d @@%s\n", color_beg, first->a_beg + 1, a_len, first->b_beg + 1, b_len, color_end);
}
static inline void diff_unified_json_ranges(RzList /*<RzDiffOp *>*/ *opcodes, PJ *pj) {
RzDiffOp *first = rz_list_first_val(opcodes);
RzDiffOp *last = rz_list_last_val(opcodes);
st32 a_len = last->a_end - first->a_beg;
st32 b_len = last->b_end - first->b_beg;
pj_ka(pj, "from");
pj_N(pj, first->a_beg + 1);
pj_N(pj, a_len);
pj_end(pj);
pj_ka(pj, "to");
pj_N(pj, first->b_beg + 1);
pj_N(pj, b_len);
pj_end(pj);
}
static inline void diff_unified_append_data(RzDiff *diff, const void *array, st32 beg, st32 end, RzStrBuf *sb, char prefix, bool color) {
RzDiffMethodElemAt elem_at = diff->methods.elem_at;
RzDiffMethodStringify stringify = diff->methods.stringify;
int len = 0;
ut32 count = 0;
const char *p;
const void *elem;
RzStrBuf tmp;
bool newline = false;
bool is_bytes = DIFF_IS_BYTES_METHOD(diff->methods);
if (beg < 0) {
beg = 0;
}
const char *bcol = color ? DIFF_COLOR(prefix) : "";
const char *ecol = color ? (Color_RESET) : "";
rz_strbuf_appendf(sb, "%s%c", bcol, prefix);
for (st32 i = beg; i < end; ++i) {
if (newline || (is_bytes && count > 0 && !FAST_MOD64(count))) {
rz_strbuf_appendf(sb, "%s\n%s%c", ecol, bcol, prefix);
newline = false;
}
rz_strbuf_init(&tmp);
elem = elem_at(array, i);
stringify(elem, &tmp);
len = rz_strbuf_length(&tmp);
p = rz_strbuf_get(&tmp);
count += len;
if (len > 0 && p[len - 1] == '\n') {
len--;
newline = true;
}
rz_strbuf_append_n(sb, p, len);
rz_strbuf_fini(&tmp);
}
rz_strbuf_appendf(sb, "%s\n", ecol);
}
// Assumes that color is true, diffing lines, op->type is RZ_DIFF_OP_REPLACE
// and that the number of inserted lines is equal to the number of deleted
// lines.
static inline void diff_unified_lines_hl(RzDiff *diff, RzDiffOp *op, RzStrBuf *sb, char del_prefix, char ins_prefix) {
RzDiffMethodElemAt elem_at = diff->methods.elem_at;
RzDiffMethodStringify stringify = diff->methods.stringify;
int len = 0;
const char *p;
const void *elem;
RzStrBuf tmp, tmp2;
const char *ecol = Color_RESET;
const char *ebgcol = Color_RESET_BG;
const void *a_array = diff->a;
st32 a_beg = op->a_beg;
if (a_beg < 0) {
a_beg = 0;
}
st32 a_end = op->a_end;
const void *b_array = diff->b;
st32 b_beg = op->b_beg;
if (b_beg < 0) {
b_beg = 0;
}
st32 b_end = op->b_end;
ut32 num_nl = count_newlines(diff, a_array, a_beg, a_end);
// + 1 just in case there's no nl at end
ut32 num_bounds = num_nl + 1;
st32 *char_bounds = malloc(sizeof(st32) * 2 * num_bounds);
if (!char_bounds) {
return;
}
for (ut32 i = 0; i < num_bounds; i++) {
char_bounds[i * 2] = char_bounds[i * 2 + 1] = -1;
}
// Fill char_bounds array
ut32 bounds_idx = 0;
bool newline = false;
st32 i = a_beg;
st32 j = b_beg;
for (; i < a_end; ++i) {
if (newline) {
bounds_idx++;
newline = false;
}
rz_strbuf_init(&tmp);
elem = elem_at(a_array, i);
stringify(elem, &tmp);
len = rz_strbuf_length(&tmp);
p = rz_strbuf_get(&tmp);
if (len > 0 && p[len - 1] == '\n') {
len--;
newline = true;
}
int len_b = 0;
const void *elem_b;
const char *p_b;
for (; j < b_end; ++j) {
rz_strbuf_init(&tmp2);
elem_b = elem_at(b_array, j);
stringify(elem_b, &tmp2);
len_b = rz_strbuf_length(&tmp2);
p_b = rz_strbuf_get(&tmp2);
if (len_b > 0 && p_b[len_b - 1] == '\n') {
len_b--;
}
if (len && len_b && (p[0] == p_b[0] || p[len - 1] == p_b[len_b - 1])) {
// Get left bound.
st32 left = 0;
for (; left < RZ_MIN(len, len_b) && p[left] == p_b[left]; left++)
;
char_bounds[bounds_idx * 2] = left;
// Get right bound (offset). "- left" chops off
// the left portion that has already matched.
st32 right = 0;
for (; right < RZ_MIN(len, len_b) - left && p[len - 1 - right] == p_b[len_b - 1 - right];
right++)
;
char_bounds[bounds_idx * 2 + 1] = right;
}
rz_strbuf_fini(&tmp2);
++j;
break;
}
rz_strbuf_fini(&tmp);
}
// Show deleted lines
char prefix = del_prefix;
const char *bcol = DIFF_COLOR(prefix);
const char *bbgcol = DIFF_BGCOLOR(prefix);
newline = false;
bounds_idx = 0;
rz_strbuf_appendf(sb, "%s%c", bcol, prefix);
for (st32 i = a_beg; i < a_end; ++i) {
if (newline) {
rz_strbuf_appendf(sb, "%s\n%s%c", ecol, bcol, prefix);
newline = false;
bounds_idx++;
}
rz_strbuf_init(&tmp);
elem = elem_at(a_array, i);
stringify(elem, &tmp);
len = rz_strbuf_length(&tmp);
p = rz_strbuf_get(&tmp);
if (len > 0 && p[len - 1] == '\n') {
len--;
newline = true;
}
st32 left = char_bounds[bounds_idx * 2];
st32 right = char_bounds[bounds_idx * 2 + 1];
if (left < 0 || right < 0 || len - right < left) {
rz_strbuf_append_n(sb, p, len);
} else {
rz_strbuf_append_n(sb, p, left);
rz_strbuf_append(sb, bbgcol);
rz_strbuf_append_n(sb, p + left, len - right - left);
rz_strbuf_append(sb, ebgcol);
rz_strbuf_append_n(sb, p + len - right, right);
}
rz_strbuf_fini(&tmp);
}
rz_strbuf_appendf(sb, "%s\n", ecol);
// Show inserted lines
prefix = ins_prefix;
bcol = DIFF_COLOR(prefix);
bbgcol = DIFF_BGCOLOR(prefix);
newline = false;
bounds_idx = 0;
rz_strbuf_appendf(sb, "%s%c", bcol, prefix);
for (st32 i = b_beg; i < b_end; ++i) {
if (newline) {
rz_strbuf_appendf(sb, "%s\n%s%c", ecol, bcol, prefix);
newline = false;
bounds_idx++;
}
rz_strbuf_init(&tmp);
elem = elem_at(b_array, i);
stringify(elem, &tmp);
len = rz_strbuf_length(&tmp);
p = rz_strbuf_get(&tmp);
if (len > 0 && p[len - 1] == '\n') {
len--;
newline = true;
}
st32 left = char_bounds[bounds_idx * 2];
st32 right = char_bounds[bounds_idx * 2 + 1];
if (left < 0 || right < 0 || len - right < left) {
rz_strbuf_append_n(sb, p, len);
} else {
rz_strbuf_append_n(sb, p, left);
rz_strbuf_append(sb, bbgcol);
rz_strbuf_append_n(sb, p + left, len - right - left);
rz_strbuf_append(sb, ebgcol);
rz_strbuf_append_n(sb, p + len - right, right);
}
rz_strbuf_fini(&tmp);
}
rz_strbuf_appendf(sb, "%s\n", ecol);
free(char_bounds);
}
static inline void diff_unified_json_data(RzDiff *diff, const void *array, st32 beg, st32 end, PJ *pj, const char *op) {
RzDiffMethodElemAt elem_at = diff->methods.elem_at;
RzDiffMethodStringify stringify = diff->methods.stringify;
int len = 0;
ut32 count = 0;
const char *p;
const void *elem;
RzStrBuf tmp;
bool newline = false;
bool is_bytes = DIFF_IS_BYTES_METHOD(diff->methods);
if (beg < 0) {
beg = 0;
}
pj_o(pj);
pj_ks(pj, "op", op);
rz_strbuf_init(&tmp);
for (st32 i = beg; i < end; ++i) {
if (newline || (is_bytes && count > 0 && !FAST_MOD64(count))) {
pj_ks(pj, "value", rz_strbuf_get(&tmp));
pj_end(pj);
rz_strbuf_fini(&tmp);
rz_strbuf_init(&tmp);
pj_o(pj);
pj_ks(pj, "op", op);
newline = false;
}
elem = elem_at(array, i);
stringify(elem, &tmp);
len = rz_strbuf_length(&tmp);
p = rz_strbuf_get(&tmp);
count += len;
if (len > 0 && p[len - 1] == '\n') {
newline = true;
}
}
pj_ks(pj, "value", rz_strbuf_get(&tmp));
pj_end(pj);
rz_strbuf_fini(&tmp);
}
/**
* \brief Produces a diff output with A and B inputs presented immediately adjacent to each other.
*
* Produces a diff output with A and B inputs presented immediately adjacent to each other.
* It begins with range information and is immediately followed with the line additions,
* line deletions, and any number of the contextual lines.
* */
RZ_API RZ_OWN char *rz_diff_unified_text(RZ_NONNULL RzDiff *diff, RZ_NULLABLE const char *from, RZ_NULLABLE const char *to, bool show_time, bool color) {
rz_return_val_if_fail(diff && diff->methods.elem_at && diff->methods.stringify, NULL);
RzStrBuf *sb = NULL;
RzList *groups = NULL;
RzList *opcodes = NULL;
RzDiffOp *op = NULL;
RzListIter *itg = NULL;
RzListIter *ito = NULL;
if (!from) {
from = "/original";
}
if (!to) {
to = "/modified";
}
sb = rz_strbuf_new("");
if (!sb) {
RZ_LOG_ERROR("rz_diff_unified: cannot allocate sb\n");
goto rz_diff_unified_text_fail;
}
if (show_time) {
char *time = rz_time_date_now_to_string();
rz_strbuf_appendf(sb, "--- %s %s\n+++ %s %s\n", from, (time ? time : ""), to, (time ? time : ""));
free(time);
} else {
rz_strbuf_appendf(sb, "--- %s\n+++ %s\n", from, to);
}
bool is_lines = DIFF_IS_LINES_METHOD(diff->methods);
bool is_bytes = DIFF_IS_BYTES_METHOD(diff->methods);
groups = rz_diff_opcodes_grouped_new(diff, is_bytes ? RZ_DIFF_DEFAULT_N_GROUPS_BYTES : RZ_DIFF_DEFAULT_N_GROUPS);
if (!groups) {
goto rz_diff_unified_text_fail;
}
rz_list_foreach (groups, itg, opcodes) {
if (rz_list_length(opcodes) < 1) {
continue;
}
diff_unified_append_ranges(opcodes, sb, color);
rz_list_foreach (opcodes, ito, op) {
switch (op->type) {
case RZ_DIFF_OP_DELETE:
diff_unified_append_data(diff, diff->a, op->a_beg, op->a_end, sb, '-', color);
break;
case RZ_DIFF_OP_EQUAL:
diff_unified_append_data(diff, diff->a, op->a_beg, op->a_end, sb, ' ', color);
break;
case RZ_DIFF_OP_INSERT:
diff_unified_append_data(diff, diff->b, op->b_beg, op->b_end, sb, '+', color);
break;
default: // RZ_DIFF_OP_REPLACE
if (!color || !is_lines ||
count_newlines(diff, diff->a, op->a_beg, op->a_end) !=
count_newlines(diff, diff->b, op->b_beg, op->b_end)) {
diff_unified_append_data(diff, diff->a, op->a_beg, op->a_end, sb, '-', color);
diff_unified_append_data(diff, diff->b, op->b_beg, op->b_end, sb, '+', color);
} else {
diff_unified_lines_hl(diff, op, sb, '-', '+');
}
break;
}
}
}
rz_list_free(groups);
return rz_strbuf_drain(sb);
rz_diff_unified_text_fail:
rz_strbuf_free(sb);
rz_list_free(groups);
return NULL;
}
/**
* \brief Produces a diff output to convert A in B in a JSON format.
*
* Produces a diff output with A and B inputs and contains the operations required
* to convert A in B and the values to remove, insert or keep.
* */
RZ_API RZ_OWN PJ *rz_diff_unified_json(RZ_NONNULL RzDiff *diff, RZ_NULLABLE const char *from, RZ_NULLABLE const char *to, bool show_time) {
rz_return_val_if_fail(diff && diff->methods.elem_at && diff->methods.stringify, NULL);
PJ *pj = NULL;
RzList *groups = NULL;
RzList *opcodes = NULL;
RzDiffOp *op = NULL;
RzListIter *itg = NULL;
RzListIter *ito = NULL;
if (!from) {
from = "/original";
}
if (!to) {
to = "/modified";
}
pj = pj_new();
if (!pj) {
RZ_LOG_ERROR("rz_diff_unified: failed to allocate json\n");
goto rz_diff_unified_json_fail;
}
pj_o(pj);
if (show_time) {
char *time = rz_time_date_now_to_string();
if (!time) {
RZ_LOG_ERROR("rz_diff_unified: failed to allocate timestamp\n");
goto rz_diff_unified_json_fail;
}
pj_ks(pj, "timestamp", time);
free(time);
}
pj_ks(pj, "from", from);
pj_ks(pj, "to", to);
groups = rz_diff_opcodes_grouped_new(diff, RZ_DIFF_DEFAULT_N_GROUPS);
if (!groups) {
goto rz_diff_unified_json_fail;
}
pj_ka(pj, "diff");
rz_list_foreach (groups, itg, opcodes) {
if (rz_list_length(opcodes) < 1) {
continue;
}
pj_o(pj);
diff_unified_json_ranges(opcodes, pj);
pj_ka(pj, "ops");
rz_list_foreach (opcodes, ito, op) {
if (op->type == RZ_DIFF_OP_EQUAL) {
diff_unified_json_data(diff, diff->a, op->a_beg, op->a_end, pj, "equal");
continue;
}
if (op->type == RZ_DIFF_OP_DELETE || op->type == RZ_DIFF_OP_REPLACE) {
diff_unified_json_data(diff, diff->a, op->a_beg, op->a_end, pj, "delete");
}
if (op->type == RZ_DIFF_OP_INSERT || op->type == RZ_DIFF_OP_REPLACE) {
diff_unified_json_data(diff, diff->b, op->b_beg, op->b_end, pj, "insert");
}
}
pj_end(pj);
pj_end(pj);
}
pj_end(pj);
pj_end(pj);
rz_list_free(groups);
return pj;
rz_diff_unified_json_fail:
pj_free(pj);
rz_list_free(groups);
return NULL;
}