rizin/test/unit/test_diff.c

1198 lines
38 KiB
C

// SPDX-FileCopyrightText: 2021 RizinOrg <info@rizin.re>
// SPDX-FileCopyrightText: 2021 deroad <wargio@libero.it>
// SPDX-License-Identifier: LGPL-3.0-only
#include <math.h>
#include <rz_diff.h>
#include "minunit.h"
#define R(a, b, c, d) \
{ (const ut8 *)a, (const ut8 *)b, c, d }
static struct {
const ut8 *a;
const ut8 *b;
ut32 myers;
ut32 levenshtein;
} tests[] = {
R("", "zzz", 3, 3),
R("meow", "", 4, 4),
R("a", "b", 2, 1),
R("aaa", "aaa", 0, 0),
R("aaaaa", "aabaa", 2, 1),
R("aaaa", "aabaa", 1, 1),
R("aaba", "babca", 3, 2),
R("foo", "foobar", 3, 3),
R("wallaby", "wallet", 5, 3),
R("identity", "identity", 0, 0),
{ NULL, NULL, 0, 0 }
};
bool test_rz_diff_distances(void) {
ut32 distance;
bool boolean;
for (ut32 i = 0; tests[i].a; i++) {
size_t la = strlen((const char *)tests[i].a);
size_t lb = strlen((const char *)tests[i].b);
boolean = rz_diff_levenshtein_distance(tests[i].a, la, tests[i].b, lb, &distance, NULL);
mu_assert_true(boolean, "rz_diff_levenshtein_distance");
mu_assert_eq(distance, tests[i].levenshtein, "levenshtein distance");
boolean = rz_diff_myers_distance(tests[i].a, la, tests[i].b, lb, &distance, NULL);
mu_assert_true(boolean, "rz_diff_myers_distance");
mu_assert_eq(distance, tests[i].myers, "myers distance");
}
mu_end;
}
bool test_rz_diff_unified_lines(void) {
RzDiff *diff = NULL;
char *result = NULL;
// clang-format off
const char *a = ""
"This part of the\n"
"document has stayed the\n"
"same from version to\n"
"version. It shouldn't\n"
"be shown if it doesn't\n"
"change. Otherwise, that\n"
"would not be helping to\n"
"compress the size of the\n"
"changes.\n"
"\n"
"This paragraph contains\n"
"text that is outdated.\n"
"It will be deleted in the\n"
"near future.\n"
"\n"
"It is important to spell\n"
"check this dokument. On\n"
"the other hand, a\n"
"misspelled word isn't\n"
"the end of the world.\n"
"Nothing in the rest of\n"
"this paragraph needs to\n"
"be changed. Things can\n"
"be added after it.";
const char *b = ""
"This is an important\n"
"notice! It should\n"
"therefore be located at\n"
"the beginning of this\n"
"document!\n"
"\n"
"This part of the\n"
"document has stayed the\n"
"same from version to\n"
"version. It shouldn't\n"
"be shown if it doesn't\n"
"change. Otherwise, that\n"
"would not be helping to\n"
"compress the size of the\n"
"changes.\n"
"\n"
"It is important to spell\n"
"check this document. On\n"
"the other hand, a\n"
"misspelled word isn't\n"
"the end of the world.\n"
"Nothing in the rest of\n"
"this paragraph needs to\n"
"be changed. Things can\n"
"be added after it.\n"
"\n"
"This paragraph contains\n"
"important new additions\n"
"to this document.";
const char *expected = ""
"--- /original\n"
"+++ /modified\n"
"@@ -1,3 +1,9 @@\n"
"+This is an important\n"
"+notice! It should\n"
"+therefore be located at\n"
"+the beginning of this\n"
"+document!\n"
"+\n"
" This part of the\n"
" document has stayed the\n"
" same from version to\n"
"@@ -8,17 +14,16 @@\n"
" compress the size of the\n"
" changes.\n"
" \n"
"-This paragraph contains\n"
"-text that is outdated.\n"
"-It will be deleted in the\n"
"-near future.\n"
"-\n"
" It is important to spell\n"
"-check this dokument. On\n"
"+check this document. On\n"
" the other hand, a\n"
" misspelled word isn't\n"
" the end of the world.\n"
" Nothing in the rest of\n"
" this paragraph needs to\n"
" be changed. Things can\n"
"-be added after it.\n"
"+be added after it.\n"
"+\n"
"+This paragraph contains\n"
"+important new additions\n"
"+to this document.\n";
// clang-format on
diff = rz_diff_lines_new(a, b, NULL);
result = rz_diff_unified_text(diff, NULL, NULL, false, false);
rz_diff_free(diff);
mu_assert_notnull(result, "rz_diff_unified result not null");
mu_assert_streq(result, expected, "rz_diff_unified on lines");
free(result);
mu_end;
}
bool test_rz_diff_unified_bytes(void) {
RzDiff *diff = NULL;
char *result = NULL;
// clang-format off
const char *a = ""
"This part of the\n"
"document has stayed the\n"
"same from version to\n"
"version. It shouldn't\n"
"be shown if it doesn't\n"
"change. Otherwise, that\n"
"would not be helping to\n"
"compress the size of the\n"
"changes.\n"
"\n"
"This paragraph contains\n"
"text that is outdated.\n"
"It will be deleted in the\n"
"near future.\n"
"\n"
"It is important to spell\n"
"check this dokument. On\n"
"the other hand, a\n"
"misspelled word isn't\n"
"the end of the world.\n"
"Nothing in the rest of\n"
"this paragraph needs to\n"
"be changed. Things can\n"
"be added after it.";
const char *b = ""
"This is an important\n"
"notice! It should\n"
"therefore be located at\n"
"the beginning of this\n"
"document!\n"
"\n"
"This part of the\n"
"document has stayed the\n"
"same from version to\n"
"version. It shouldn't\n"
"be shown if it doesn't\n"
"change. Otherwise, that\n"
"would not be helping to\n"
"compress the size of the\n"
"changes.\n"
"\n"
"It is important to spell\n"
"check this document. On\n"
"the other hand, a\n"
"misspelled word isn't\n"
"the end of the world.\n"
"Nothing in the rest of\n"
"this paragraph needs to\n"
"be changed. Things can\n"
"be added after it.\n"
"\n"
"This paragraph contains\n"
"important new additions\n"
"to this document.";
const char *expected = ""
"--- /original\n"
"+++ /modified\n"
"@@ --2,287 +-2,296 @@\n"
" 5468697320\n"
"-70617274206f66207468650a646f63756d656e74206861732073746179656420\n"
"-7468650a73616d652066726f6d2076657273696f6e20746f0a76657273696f6e\n"
"-2e20204974207368\n"
"+697320616e20696d706f7274616e740a6e6f74696365212049742073686f756c\n"
"+640a7468657265666f7265206265206c6f63617465642061740a746865206265\n"
"+67696e6e696e6720\n"
" 6f\n"
"-756c646e2774\n"
"+662074686973\n"
" 0a\n"
"-62652073686f776e20696620697420646f65736e27740a6368616e67652e2020\n"
"-4f746865727769\n"
"+646f63756d656e74210a0a546869732070617274206f66207468650a646f6375\n"
"+6d656e74206861\n"
" 73\n"
"-652c20746861740a776f756c64206e6f742062652068656c70696e6720746f0a\n"
"-636f6d7072657373207468652073697a65206f66207468650a6368616e676573\n"
"-2e0a0a54686973207061726167726170\n"
"+20737461796564207468650a73616d652066726f6d2076657273696f6e20746f\n"
"+0a76657273696f6e2e202049742073686f756c646e27740a62652073686f776e\n"
"+20696620697420646f65736e27740a63\n"
" 68\n"
"-20636f6e7461696e730a7465787420746861\n"
"+616e67652e20204f74686572776973652c20\n"
" 74\n"
"-206973206f757464617465642e0a4974\n"
"+6861740a776f756c64206e6f74206265\n"
" 20\n"
"-7769\n"
"+6865\n"
" 6c\n"
"-6c206265\n"
"+70696e67\n"
" 20\n"
"-64656c6574656420696e207468650a6e656172206675747572\n"
"+746f0a636f6d7072657373207468652073697a65206f66207468\n"
" 65\n"
"+0a6368616e676573\n"
" 2e0a0a4974206973\n"
"@@ -310,17 +319,17 @@\n"
" 207468697320646f\n"
"-6b\n"
"+63\n"
" 756d656e742e204f\n"
"@@ -471,8 +480,75 @@\n"
" 667465722069742e\n"
"+0a0a546869732070617261677261706820636f6e7461696e730a696d706f7274\n"
"+616e74206e6577206164646974696f6e730a746f207468697320646f63756d65\n"
"+6e742e\n";
// clang-format on
diff = rz_diff_bytes_new((const ut8 *)a, strlen(a), (const ut8 *)b, strlen(b));
result = rz_diff_unified_text(diff, NULL, NULL, false, false);
rz_diff_free(diff);
mu_assert_notnull(result, "rz_diff_unified result not null");
mu_assert_streq(result, expected, "rz_diff_unified on bytes");
free(result);
mu_end;
}
// ------------------------------------------------------------
// Additional tests covering the rest of the RzDiff public API.
//
// These tests exercise the matches, opcodes, opcodes-grouped,
// ratio, sizes-ratio, generic-methods and JSON output paths,
// plus edge cases (empty, identical, single byte, NULL guard)
// and the ignore-callback hooks.
// ------------------------------------------------------------
// ------------------------------------------------------------
// Helpers
// ------------------------------------------------------------
static bool ignore_blank_line(const char *line) {
if (!line) {
return true;
}
for (const char *p = line; *p; p++) {
if (*p != ' ' && *p != '\t' && *p != '\n' && *p != '\r') {
return false;
}
}
return true;
}
// ------------------------------------------------------------
// rz_diff_hash_data
// ------------------------------------------------------------
bool test_rz_diff_hash_data(void) {
// Empty / NULL input must return the documented seed (5381).
mu_assert_eq(rz_diff_hash_data(NULL, 0), 5381u, "hash on NULL returns seed");
mu_assert_eq(rz_diff_hash_data((const ut8 *)"", 0), 5381u, "hash on size 0 returns seed");
// Determinism: equal inputs must hash to equal values.
const ut8 *a = (const ut8 *)"rizin";
const ut8 *b = (const ut8 *)"rizin";
mu_assert_eq(rz_diff_hash_data(a, 5), rz_diff_hash_data(b, 5), "deterministic hash");
// Different inputs of the same size should generally differ (probabilistic,
// but these two clearly do under the DJB-XOR mix used here).
const ut8 *c = (const ut8 *)"radar";
mu_assert_neq(rz_diff_hash_data(a, 5), rz_diff_hash_data(c, 5), "different inputs hash differently");
mu_end;
}
// ------------------------------------------------------------
// Construction / accessors / NULL guards
// ------------------------------------------------------------
bool test_rz_diff_get_a_b(void) {
const ut8 *a = (const ut8 *)"AAAA";
const ut8 *b = (const ut8 *)"BBBB";
RzDiff *d = rz_diff_bytes_new(a, 4, b, 4);
mu_assert_notnull(d, "diff created");
mu_assert_ptreq(rz_diff_get_a(d), a, "get_a returns the original A pointer");
mu_assert_ptreq(rz_diff_get_b(d), b, "get_b returns the original B pointer");
rz_diff_free(d);
mu_end;
}
bool test_rz_diff_null_inputs(void) {
// rz_diff_free is documented RZ_NULLABLE -- it must accept NULL as a
// no-op. This is the only NULL-input case we can portably test: the
// `_new` constructors are RZ_NONNULL on their data pointers, so passing
// NULL there is undefined behavior by contract and aborts when the
// build is configured with RZ_CHECKS_LEVEL=3 (used by the codecov CI
// job, see .github/workflows/ci.yml: `-Dchecks_level=3`).
rz_diff_free(NULL);
mu_end;
}
bool test_rz_diff_empty_buffers(void) {
// Diff of two empty buffers: should succeed, ratio = 1.0, no opcodes,
// no matches besides the synthetic end sentinel.
const ut8 empty[1] = { 0 };
RzDiff *d = rz_diff_bytes_new(empty, 0, empty, 0);
mu_assert_notnull(d, "diff on (empty, empty) created");
double r = -1.0;
mu_assert_true(rz_diff_ratio(d, &r), "ratio on empty returns true");
mu_assert_eqf(r, 1.0, "ratio on (empty, empty) is 1.0");
double sr = -1.0;
mu_assert_true(rz_diff_sizes_ratio(d, &sr), "sizes_ratio on empty returns true");
mu_assert_eqf(sr, 1.0, "sizes_ratio on (empty, empty) is 1.0");
RzList *ops = rz_diff_opcodes_new(d);
mu_assert_notnull(ops, "opcodes_new on empty returns a list");
mu_assert_eq(rz_list_length(ops), 0, "no opcodes between two empty buffers");
rz_list_free(ops);
rz_diff_free(d);
mu_end;
}
bool test_rz_diff_identical_buffers(void) {
// For identical inputs we expect a single EQUAL opcode spanning the whole
// buffer, ratio == 1.0, and exactly one non-sentinel match.
const ut8 *s = (const ut8 *)"abcdefghij";
RzDiff *d = rz_diff_bytes_new(s, 10, s, 10);
mu_assert_notnull(d, "diff on identical created");
double r = 0.0;
mu_assert_true(rz_diff_ratio(d, &r), "ratio returns true");
mu_assert_eqf(r, 1.0, "ratio on identical == 1.0");
RzList *ops = rz_diff_opcodes_new(d);
mu_assert_notnull(ops, "opcodes_new on identical not null");
mu_assert_eq(rz_list_length(ops), 1, "exactly one opcode on identical");
RzDiffOp *op = rz_list_first_val(ops);
mu_assert_notnull(op, "first opcode not null");
mu_assert_eq((int)op->type, (int)RZ_DIFF_OP_EQUAL, "opcode type is EQUAL");
mu_assert_eq(op->a_beg, 0, "EQUAL spans from 0");
mu_assert_eq(op->a_end, 10, "EQUAL spans up to a_size");
mu_assert_eq(op->b_beg, 0, "EQUAL spans from 0 (b)");
mu_assert_eq(op->b_end, 10, "EQUAL spans up to b_size");
rz_list_free(ops);
rz_diff_free(d);
mu_end;
}
bool test_rz_diff_completely_different(void) {
// Two strings that share no characters at all -> no equal matches,
// ratio should be 0.0, single REPLACE opcode covering the full ranges.
const ut8 *a = (const ut8 *)"AAAAA";
const ut8 *b = (const ut8 *)"BBBBB";
RzDiff *d = rz_diff_bytes_new(a, 5, b, 5);
mu_assert_notnull(d, "diff created");
double r = -1.0;
mu_assert_true(rz_diff_ratio(d, &r), "ratio call ok");
mu_assert_eqf(r, 0.0, "ratio is 0.0 for disjoint alphabets");
double sr = -1.0;
mu_assert_true(rz_diff_sizes_ratio(d, &sr), "sizes_ratio call ok");
mu_assert_eqf(sr, 1.0, "sizes ratio is 1.0 for equal lengths");
RzList *ops = rz_diff_opcodes_new(d);
mu_assert_notnull(ops, "opcodes not null");
mu_assert_eq(rz_list_length(ops), 1, "one REPLACE opcode");
RzDiffOp *op = rz_list_first_val(ops);
mu_assert_eq((int)op->type, (int)RZ_DIFF_OP_REPLACE, "single REPLACE opcode");
mu_assert_eq(RZ_DIFF_OP_SIZE_A(op), 5, "REPLACE covers all of A");
mu_assert_eq(RZ_DIFF_OP_SIZE_B(op), 5, "REPLACE covers all of B");
rz_list_free(ops);
rz_diff_free(d);
mu_end;
}
bool test_rz_diff_pure_insert_and_delete(void) {
// Pure insert: A is empty, B has content -> single INSERT, ratio 0.
{
const ut8 *a = (const ut8 *)"";
const ut8 *b = (const ut8 *)"hello";
RzDiff *d = rz_diff_bytes_new(a, 0, b, 5);
mu_assert_notnull(d, "diff(empty A) created");
RzList *ops = rz_diff_opcodes_new(d);
mu_assert_notnull(ops, "ops not null");
mu_assert_eq(rz_list_length(ops), 1, "one INSERT opcode");
RzDiffOp *op = rz_list_first_val(ops);
mu_assert_eq((int)op->type, (int)RZ_DIFF_OP_INSERT, "type INSERT");
mu_assert_eq(op->a_beg, 0, "a_beg 0");
mu_assert_eq(op->a_end, 0, "a_end 0");
mu_assert_eq(op->b_beg, 0, "b_beg 0");
mu_assert_eq(op->b_end, 5, "b_end 5");
rz_list_free(ops);
double r = -1.0;
mu_assert_true(rz_diff_ratio(d, &r), "ratio ok");
mu_assert_eqf(r, 0.0, "ratio 0 for pure insert");
rz_diff_free(d);
}
// Pure delete: A has content, B is empty -> single DELETE.
{
const ut8 *a = (const ut8 *)"hello";
const ut8 *b = (const ut8 *)"";
RzDiff *d = rz_diff_bytes_new(a, 5, b, 0);
mu_assert_notnull(d, "diff(empty B) created");
RzList *ops = rz_diff_opcodes_new(d);
mu_assert_notnull(ops, "ops not null");
mu_assert_eq(rz_list_length(ops), 1, "one DELETE opcode");
RzDiffOp *op = rz_list_first_val(ops);
mu_assert_eq((int)op->type, (int)RZ_DIFF_OP_DELETE, "type DELETE");
mu_assert_eq(op->a_beg, 0, "a_beg 0");
mu_assert_eq(op->a_end, 5, "a_end 5");
mu_assert_eq(op->b_beg, 0, "b_beg 0");
mu_assert_eq(op->b_end, 0, "b_end 0");
rz_list_free(ops);
rz_diff_free(d);
}
mu_end;
}
// ------------------------------------------------------------
// rz_diff_matches_new
// ------------------------------------------------------------
bool test_rz_diff_matches_basic(void) {
// Inputs reproducing the README example "ABCDEFGHI" vs "YZBCDLZNHI":
// matches should be (B..D, H..I) plus the synthetic end sentinel.
// clang-format off
const char *a = ""
"It is important to spell\n"
"check this document. On\n"
"the other hand, a\n"
"misspelled word isn't\n"
"the end of the world.\n"
"Nothing in the rest of\n"
"this paragraph needs to\n"
"be changed. Things can\n"
"be added after it.\n";
const char *b = ""
"It is important to spell\n"
"cheeeck this document. On\n"
"the other hand, a\n"
"misspelled word isn't\n"
"the end of the world.\n"
"Nothhhhing in the rest of\n"
"this paragraph needs to\n"
"be changed. Things can\n"
"be added after it.\n"
"\n"
"This paragraph contains\n"
"important new additions\n"
"to this document.";
// clang-format on
RzDiff *d = rz_diff_bytes_new((const ut8 *)a, strlen(a), (const ut8 *)b, strlen(b));
mu_assert_notnull(d, "diff created");
RzList *matches = rz_diff_matches_new(d);
mu_assert_notnull(matches, "matches not null");
// At least 2 real matches plus the (a_size, b_size, 0) sentinel.
mu_assert_eq(rz_list_length(matches), 4, "at least two real matches + sentinel");
// All non-sentinel matches must point to byte-identical sub-strings of A and B.
RzListIter *it = NULL;
RzDiffMatch *m = NULL;
rz_list_foreach (matches, it, m) {
if (m->size == 0) {
continue;
}
mu_assert_eq(memcmp(a + m->a, b + m->b, m->size), 0, "match content equal in A and B");
}
// The very last entry is the synthetic end sentinel at (a_size, b_size, 0).
RzDiffMatch *last = rz_list_last_val(matches);
mu_assert_eq(last->size, 0u, "last match is sentinel with size 0");
mu_assert_eq(last->a, 200u, "sentinel a == a_size");
mu_assert_eq(last->b, 271u, "sentinel b == b_size");
rz_list_free(matches);
rz_diff_free(d);
mu_end;
}
// ------------------------------------------------------------
// rz_diff_opcodes_grouped_new
// ------------------------------------------------------------
bool test_rz_diff_opcodes_grouped_identical(void) {
// Identical inputs: documented behavior is that a single trivial EQUAL
// opcode group is produced.
const ut8 *s = (const ut8 *)"identical content";
ut32 n = (ut32)strlen((const char *)s);
RzDiff *d = rz_diff_bytes_new(s, n, s, n);
mu_assert_notnull(d, "diff created");
RzList *groups = rz_diff_opcodes_grouped_new(d, RZ_DIFF_DEFAULT_N_GROUPS);
mu_assert_notnull(groups, "groups not null");
// Per the source, when the only opcode is a single EQUAL the empty group
// is dropped, so we may get zero groups; either way we must not crash.
rz_list_free(groups);
rz_diff_free(d);
mu_end;
}
bool test_rz_diff_opcodes_grouped_replace(void) {
const ut8 *a = (const ut8 *)"AAAAAAAA";
const ut8 *b = (const ut8 *)"BBBBBBBB";
RzDiff *d = rz_diff_bytes_new(a, 8, b, 8);
mu_assert_notnull(d, "diff created");
RzList *groups = rz_diff_opcodes_grouped_new(d, RZ_DIFF_DEFAULT_N_GROUPS);
mu_assert_notnull(groups, "groups not null");
mu_assert_true(rz_list_length(groups) >= 1, "at least one group for non-trivial diff");
// Every group must contain at least one op, and every op's ranges must be
// in-bounds.
RzListIter *itg = NULL;
RzList *group = NULL;
rz_list_foreach (groups, itg, group) {
mu_assert_true(rz_list_length(group) >= 1, "non-empty group");
RzListIter *ito = NULL;
RzDiffOp *op = NULL;
rz_list_foreach (group, ito, op) {
mu_assert_true(op->a_beg >= 0 && op->a_end <= 8, "a-range in bounds");
mu_assert_true(op->b_beg >= 0 && op->b_end <= 8, "b-range in bounds");
mu_assert_true(op->a_end >= op->a_beg, "a-range valid");
mu_assert_true(op->b_end >= op->b_beg, "b-range valid");
}
}
rz_list_free(groups);
rz_diff_free(d);
mu_end;
}
// ------------------------------------------------------------
// rz_diff_ratio / rz_diff_sizes_ratio
// ------------------------------------------------------------
bool test_rz_diff_ratio_bounds(void) {
const ut8 *a = (const ut8 *)"the quick brown fox";
const ut8 *b = (const ut8 *)"the quirky brown ox";
RzDiff *d = rz_diff_bytes_new(a, (ut32)strlen((const char *)a),
b, (ut32)strlen((const char *)b));
mu_assert_notnull(d, "diff created");
double r = -1.0;
mu_assert_true(rz_diff_ratio(d, &r), "ratio call ok");
mu_assert_true(r > 0.0 && r < 1.0, "ratio strictly in (0, 1) for partial overlap");
double sr = -1.0;
mu_assert_true(rz_diff_sizes_ratio(d, &sr), "sizes_ratio call ok");
mu_assert_true(sr > 0.0 && sr <= 1.0, "sizes_ratio in (0, 1]");
rz_diff_free(d);
mu_end;
}
bool test_rz_diff_sizes_ratio_skewed(void) {
// A is 4x the size of B: sizes ratio = 2*min/(a+b) = 2*1/(4+1) = 0.4
const ut8 *a = (const ut8 *)"aaaa";
const ut8 *b = (const ut8 *)"a";
RzDiff *d = rz_diff_bytes_new(a, 4, b, 1);
mu_assert_notnull(d, "diff created");
double sr = 0.0;
mu_assert_true(rz_diff_sizes_ratio(d, &sr), "sizes_ratio call ok");
mu_assert_true(fabs(sr - 0.4) < 1e-9, "sizes_ratio is 2*min/(a+b)");
rz_diff_free(d);
mu_end;
}
// ------------------------------------------------------------
// Ignore callbacks
// ------------------------------------------------------------
bool test_rz_diff_ignore_line(void) {
const char *a =
"one\n"
"\n"
"two\n";
const char *b =
"one\n"
" \n"
"two\n";
RzDiff *plain = rz_diff_lines_new(a, b, NULL);
mu_assert_notnull(plain, "plain lines diff");
double r_plain = -1.0;
mu_assert_true(rz_diff_ratio(plain, &r_plain), "plain ratio ok");
rz_diff_free(plain);
RzDiff *ign = rz_diff_lines_new(a, b, ignore_blank_line);
mu_assert_notnull(ign, "ignore-blank lines diff");
double r_ign = -1.0;
mu_assert_true(rz_diff_ratio(ign, &r_ign), "ignore-blank ratio ok");
rz_diff_free(ign);
mu_assert_true(r_ign >= r_plain, "ignoring blank lines can only raise ratio");
mu_end;
}
// ------------------------------------------------------------
// rz_diff_generic_new -- exercise the user-defined methods path on an
// array of integers.
// ------------------------------------------------------------
static const void *int_elem_at(const void *array, ut32 index) {
const ut32 *arr = array;
return &arr[index];
}
static ut32 int_elem_hash(const void *elem) {
const ut32 *v = elem;
return rz_diff_hash_data((const ut8 *)v, sizeof(ut32));
}
static int int_compare(const void *a_elem, const void *b_elem) {
const ut32 *a = a_elem;
const ut32 *b = b_elem;
return (*a == *b) ? 0 : 1;
}
static void int_stringify(const void *elem, RzStrBuf *sb) {
const ut32 *v = elem;
rz_strbuf_setf(sb, "%u", *v);
}
bool test_rz_diff_generic_ints(void) {
const ut32 a[] = { 1, 2, 3, 4, 5, 6 };
const ut32 b[] = { 1, 2, 3, 4, 5, 6 };
RzDiffMethods methods = {
.elem_at = int_elem_at,
.elem_hash = int_elem_hash,
.compare = int_compare,
.stringify = int_stringify,
.ignore = NULL,
};
RzDiff *d = rz_diff_generic_new(a, 6, b, 6, &methods);
mu_assert_notnull(d, "generic diff created");
double r = 0.0;
mu_assert_true(rz_diff_ratio(d, &r), "ratio call ok");
mu_assert_eqf(r, 1.0, "ratio of identical int arrays is 1.0");
RzList *ops = rz_diff_opcodes_new(d);
mu_assert_notnull(ops, "opcodes not null");
mu_assert_eq(rz_list_length(ops), 1, "single EQUAL on identical");
rz_list_free(ops);
rz_diff_free(d);
mu_end;
}
bool test_rz_diff_generic_ints_modified(void) {
const ut32 a[] = { 10, 20, 30, 40, 50 };
const ut32 b[] = { 10, 99, 30, 40, 50 };
RzDiffMethods methods = {
.elem_at = int_elem_at,
.elem_hash = int_elem_hash,
.compare = int_compare,
.stringify = int_stringify,
.ignore = NULL,
};
RzDiff *d = rz_diff_generic_new(a, 5, b, 5, &methods);
mu_assert_notnull(d, "generic diff created");
RzList *ops = rz_diff_opcodes_new(d);
mu_assert_notnull(ops, "ops not null");
mu_assert_true(rz_list_length(ops) >= 2, "at least EQUAL+REPLACE+EQUAL");
// Look for the REPLACE op at index 1 of A (where 20 -> 99).
bool found_replace = false;
RzListIter *it = NULL;
RzDiffOp *op = NULL;
rz_list_foreach (ops, it, op) {
if (op->type == RZ_DIFF_OP_REPLACE && op->a_beg == 1 && op->a_end == 2 && op->b_beg == 1 && op->b_end == 2) {
found_replace = true;
break;
}
}
mu_assert_true(found_replace, "REPLACE at position 1 found");
rz_list_free(ops);
rz_diff_free(d);
mu_end;
}
// Note: there's no test for `rz_diff_generic_new(..., NULL)` or for a
// RzDiffMethods with missing required callbacks -- those inputs are
// RZ_NONNULL by contract (see librz/include/rz_diff.h), and passing NULL
// triggers an `assert()` under RZ_CHECKS_LEVEL=3 (used by the codecov CI
// job). The successful-creation paths above already exercise every
// callback field.
// ------------------------------------------------------------
// rz_diff_unified_json -- sanity-check the JSON output skeleton.
// ------------------------------------------------------------
bool test_rz_diff_unified_json_smoke(void) {
const char *a = "alpha\nbeta\ngamma\n";
const char *b = "alpha\nBETA\ngamma\n";
RzDiff *d = rz_diff_lines_new(a, b, NULL);
mu_assert_notnull(d, "lines diff created");
PJ *pj = rz_diff_unified_json(d, "a.txt", "b.txt", false);
mu_assert_notnull(pj, "unified_json returned a PJ");
const char *out = pj_string(pj);
mu_assert_notnull(out, "pj_string returned non-null");
// JSON must contain the file labels and the "diff" array key.
mu_assert_strcontains(out, "a.txt", "from label present");
mu_assert_strcontains(out, "b.txt", "to label present");
mu_assert_strcontains(out, "\"diff\":", "diff key present");
pj_free(pj);
rz_diff_free(d);
mu_end;
}
// ------------------------------------------------------------
// Equivalence test: same content via bytes-diff and via lines-diff should
// produce the same ratio (within float epsilon) when there are no line
// boundary effects.
// ------------------------------------------------------------
bool test_rz_diff_ratio_byte_line_agree_on_identical(void) {
const char *s = "alpha\nbeta\ngamma\n";
RzDiff *db = rz_diff_bytes_new((const ut8 *)s, (ut32)strlen(s),
(const ut8 *)s, (ut32)strlen(s));
RzDiff *dl = rz_diff_lines_new(s, s, NULL);
mu_assert_notnull(db, "bytes diff created");
mu_assert_notnull(dl, "lines diff created");
double rb = 0.0, rl = 0.0;
mu_assert_true(rz_diff_ratio(db, &rb), "bytes ratio ok");
mu_assert_true(rz_diff_ratio(dl, &rl), "lines ratio ok");
mu_assert_eqf(rb, 1.0, "bytes ratio == 1.0 on identical");
mu_assert_eqf(rl, 1.0, "lines ratio == 1.0 on identical");
rz_diff_free(db);
rz_diff_free(dl);
mu_end;
}
// ------------------------------------------------------------
// Single-byte / single-line buffers.
// ------------------------------------------------------------
bool test_rz_diff_single_element(void) {
// Single-byte identical.
{
const ut8 *a = (const ut8 *)"x";
RzDiff *d = rz_diff_bytes_new(a, 1, a, 1);
mu_assert_notnull(d, "diff 1-byte identical");
double r = 0.0;
mu_assert_true(rz_diff_ratio(d, &r), "ratio ok");
mu_assert_eqf(r, 1.0, "1-byte identical ratio 1.0");
rz_diff_free(d);
}
// Single-byte different.
{
const ut8 *a = (const ut8 *)"x";
const ut8 *b = (const ut8 *)"y";
RzDiff *d = rz_diff_bytes_new(a, 1, b, 1);
mu_assert_notnull(d, "diff 1-byte different");
double r = 1.0;
mu_assert_true(rz_diff_ratio(d, &r), "ratio ok");
mu_assert_eqf(r, 0.0, "1-byte different ratio 0.0");
RzList *ops = rz_diff_opcodes_new(d);
mu_assert_notnull(ops, "ops not null");
mu_assert_eq(rz_list_length(ops), 1, "single REPLACE");
RzDiffOp *op = rz_list_first_val(ops);
mu_assert_eq((int)op->type, (int)RZ_DIFF_OP_REPLACE, "REPLACE");
rz_list_free(ops);
rz_diff_free(d);
}
mu_end;
}
// ------------------------------------------------------------
// Stress / regression smoke test: a larger pseudo-random pair.
//
// This isn't a benchmark, but it does ensure that the full pipeline
// (matches -> opcodes -> grouped -> unified) survives a realistic size
// without errors. It uses a deterministic LCG so failures reproduce.
// ------------------------------------------------------------
bool test_rz_diff_stress_medium(void) {
const ut32 size = 2048;
ut8 *a = malloc(size);
ut8 *b = malloc(size);
mu_assert_notnull(a, "alloc a");
mu_assert_notnull(b, "alloc b");
ut32 s = 0x12345678u;
for (ut32 i = 0; i < size; i++) {
s = s * 1103515245u + 12345u;
a[i] = 0x20 + (s % (0x7f - 0x20));
}
memcpy(b, a, size);
// Flip every 64th byte.
for (ut32 i = 0; i < size; i += 64) {
b[i] ^= 0xff;
}
RzDiff *d = rz_diff_bytes_new(a, size, b, size);
mu_assert_notnull(d, "stress diff created");
RzList *matches = rz_diff_matches_new(d);
mu_assert_notnull(matches, "stress matches not null");
mu_assert_true(rz_list_length(matches) > 1, "stress: more than one match");
rz_list_free(matches);
RzList *ops = rz_diff_opcodes_new(d);
mu_assert_notnull(ops, "stress ops not null");
mu_assert_true(rz_list_length(ops) > 1, "stress: more than one op");
rz_list_free(ops);
char *u = rz_diff_unified_text(d, NULL, NULL, false, false);
mu_assert_notnull(u, "stress unified_text not null");
free(u);
rz_diff_free(d);
free(a);
free(b);
mu_end;
}
// ------------------------------------------------------------
// Structural edge cases that mirror the "torture" benchmark
// scenarios in test/bench/bench_diff.c. Each one checks the
// shape of the produced opcodes for a recognizable real-world
// binary pattern.
// ------------------------------------------------------------
bool test_rz_diff_all_zeros_pathological(void) {
// Pathological hash distribution: every byte in B hashes to the same
// bucket. The diff must still succeed and the ratio must reflect that
// the two buffers are almost equal (one byte changed).
const ut32 size = 512;
ut8 *a = malloc(size);
ut8 *b = malloc(size);
mu_assert_notnull(a, "alloc a");
mu_assert_notnull(b, "alloc b");
memset(a, 0, size);
memset(b, 0, size);
b[size / 2] = 0x01;
RzDiff *d = rz_diff_bytes_new(a, size, b, size);
mu_assert_notnull(d, "all-zeros diff created");
double r = 0.0;
mu_assert_true(rz_diff_ratio(d, &r), "ratio ok");
mu_assert_true(r > 0.99, "all-zeros: ratio nearly 1.0");
RzList *ops = rz_diff_opcodes_new(d);
mu_assert_notnull(ops, "ops not null");
// The exact opcode shape depends on which length-256 match the matcher
// picks first -- on this input there are many length-256 matches with
// the same score (every (a_pos, b_pos) where a_pos == b_pos +/- the
// flipped byte's offset works), and the algorithm is free to pick any.
// We've observed two valid encodings of "the two buffers differ by one
// byte":
// - a single REPLACE op (one byte differs at the same position), or
// - one INSERT plus one DELETE (B has one extra byte at some
// position, A has one extra byte at the end -- equivalent).
// Both have total non-EQUAL byte coverage of at most one byte in each
// of A and B, which is the real invariant we want to lock down.
RzListIter *it = NULL;
RzDiffOp *op = NULL;
int seen_diff = 0;
int seen_equal = 0;
ut32 total_a_changed = 0;
ut32 total_b_changed = 0;
rz_list_foreach (ops, it, op) {
if (op->type == RZ_DIFF_OP_EQUAL) {
seen_equal++;
} else {
seen_diff++;
ut32 a_sz = RZ_DIFF_OP_SIZE_A(op);
ut32 b_sz = RZ_DIFF_OP_SIZE_B(op);
total_a_changed += a_sz;
total_b_changed += b_sz;
}
}
mu_assert_true(seen_diff >= 1 && seen_diff <= 2, "1 or 2 non-EQUAL ops");
mu_assert_eq(total_a_changed, 1, "exactly one byte of A is in non-EQUAL ops");
mu_assert_eq(total_b_changed, 1, "exactly one byte of B is in non-EQUAL ops");
mu_assert_true(seen_equal >= 1, "at least one EQUAL surrounding the diff");
rz_list_free(ops);
rz_diff_free(d);
free(a);
free(b);
mu_end;
}
bool test_rz_diff_repeating_pattern(void) {
// Short repeating pattern: every elem_hash bucket accumulates a lot of
// candidates. The algorithm should still terminate quickly and produce
// the right shape -- which is "almost entirely equal" with point
// disturbances.
const ut32 size = 1024;
ut8 *a = malloc(size);
ut8 *b = malloc(size);
mu_assert_notnull(a, "alloc a");
mu_assert_notnull(b, "alloc b");
for (ut32 i = 0; i < size; i++) {
a[i] = (ut8)('A' + (i & 3));
}
memcpy(b, a, size);
b[size / 4] = 'Z';
b[size / 2] = 'Z';
b[size * 3 / 4] = 'Z';
RzDiff *d = rz_diff_bytes_new(a, size, b, size);
mu_assert_notnull(d, "repeating-pattern diff");
double r = 0.0;
mu_assert_true(rz_diff_ratio(d, &r), "ratio ok");
mu_assert_true(r > 0.99, "repeating-pattern: ratio nearly 1.0");
rz_diff_free(d);
free(a);
free(b);
mu_end;
}
bool test_rz_diff_firmware_like_structure(void) {
// Miniature firmware: identical bootloader prefix (32 B), changed
// middle (16 B), identical tail (32 B). The opcode sequence must
// reflect that prefix and suffix are EQUAL and only the middle
// differs as a REPLACE.
const ut32 prefix = 32, mid = 16, suffix = 32;
const ut32 size = prefix + mid + suffix;
ut8 *v1 = malloc(size);
ut8 *v2 = malloc(size);
mu_assert_notnull(v1, "alloc v1");
mu_assert_notnull(v2, "alloc v2");
for (ut32 i = 0; i < size; i++) {
v1[i] = (ut8)(i ^ 0x5a);
}
memcpy(v2, v1, size);
// Re-write the middle 16 bytes in v2.
for (ut32 i = 0; i < mid; i++) {
v2[prefix + i] = (ut8)(0xff - i);
}
RzDiff *d = rz_diff_bytes_new(v1, size, v2, size);
mu_assert_notnull(d, "firmware-like diff");
RzList *ops = rz_diff_opcodes_new(d);
mu_assert_notnull(ops, "ops not null");
mu_assert_eq(rz_list_length(ops), 3, "EQUAL, REPLACE, EQUAL");
RzDiffOp *first = rz_list_first_val(ops);
RzDiffOp *last = rz_list_last_val(ops);
mu_assert_eq((int)first->type, (int)RZ_DIFF_OP_EQUAL, "first op EQUAL");
mu_assert_eq(first->a_beg, 0, "first op starts at 0");
mu_assert_eq(first->a_end, (st32)prefix, "first op ends at prefix");
mu_assert_eq((int)last->type, (int)RZ_DIFF_OP_EQUAL, "last op EQUAL");
mu_assert_eq(last->a_end, (st32)size, "last op ends at size");
rz_list_free(ops);
rz_diff_free(d);
free(v1);
free(v2);
mu_end;
}
bool test_rz_diff_block_shift(void) {
// A and B are byte-identical except a 64-byte block has been moved
// from one position to another. Ratio should be high (most content
// equal), and the opcode sequence should not be "single REPLACE on
// the full range" -- the matcher must find the shifted block.
const ut32 size = 512;
ut8 *a = malloc(size);
ut8 *b = malloc(size);
mu_assert_notnull(a, "alloc a");
mu_assert_notnull(b, "alloc b");
for (ut32 i = 0; i < size; i++) {
a[i] = (ut8)('a' + (i % 26));
}
// Make the moved block distinctive so it's actually findable.
for (ut32 i = 0; i < 64; i++) {
a[100 + i] = (ut8)(0x80 + i);
}
// In B, the same distinctive block lives at offset 300 and the
// region between is shifted left.
memcpy(b, a, 100);
memcpy(b + 100, a + 100 + 64, 300 - 100 - 64);
memcpy(b + 300 - 64, a + 100, 64);
memcpy(b + 300, a + 300, size - 300);
RzDiff *d = rz_diff_bytes_new(a, size, b, size);
mu_assert_notnull(d, "block-shift diff");
double r = 0.0;
mu_assert_true(rz_diff_ratio(d, &r), "ratio ok");
mu_assert_true(r > 0.5, "block-shift: most content matches");
RzList *ops = rz_diff_opcodes_new(d);
mu_assert_notnull(ops, "ops not null");
// More than one EQUAL means the matcher found more than just the
// common prefix or suffix.
int equal_ops = 0;
RzListIter *it = NULL;
RzDiffOp *op = NULL;
rz_list_foreach (ops, it, op) {
if (op->type == RZ_DIFF_OP_EQUAL) {
equal_ops++;
}
}
mu_assert_true(equal_ops >= 2, "block-shift produced multiple EQUAL ops");
rz_list_free(ops);
rz_diff_free(d);
free(a);
free(b);
mu_end;
}
bool test_rz_diff_growing_buffer(void) {
// B is a strict superset of A with content inserted in the middle --
// the "string got longer" pattern that produces a slide of the const
// pool in real binaries. We expect a single INSERT opcode.
const ut8 *a = (const ut8 *)"prefix_middle_suffix";
const ut8 *b = (const ut8 *)"prefix_middle_INSERTED_suffix";
RzDiff *d = rz_diff_bytes_new(a, (ut32)strlen((const char *)a),
b, (ut32)strlen((const char *)b));
mu_assert_notnull(d, "growing-buffer diff");
RzList *ops = rz_diff_opcodes_new(d);
mu_assert_notnull(ops, "ops not null");
bool found_insert = false;
RzListIter *it = NULL;
RzDiffOp *op = NULL;
rz_list_foreach (ops, it, op) {
if (op->type == RZ_DIFF_OP_INSERT) {
found_insert = true;
mu_assert_eq(RZ_DIFF_OP_SIZE_A(op), 0, "INSERT has zero A-range");
mu_assert_true(RZ_DIFF_OP_SIZE_B(op) > 0, "INSERT has positive B-range");
}
}
mu_assert_true(found_insert, "growing-buffer produces an INSERT op");
rz_list_free(ops);
rz_diff_free(d);
mu_end;
}
bool test_rz_diff_long_common_prefix(void) {
// Long common prefix, then total divergence. After the matcher peels
// off the prefix as EQUAL, the rest must come out as a single
// REPLACE / DELETE / INSERT depending on lengths.
const ut32 prefix_len = 256;
const ut32 tail_len = 64;
ut8 *a = malloc(prefix_len + tail_len);
ut8 *b = malloc(prefix_len + tail_len);
mu_assert_notnull(a, "alloc a");
mu_assert_notnull(b, "alloc b");
for (ut32 i = 0; i < prefix_len; i++) {
a[i] = b[i] = (ut8)('A' + (i % 26));
}
for (ut32 i = 0; i < tail_len; i++) {
a[prefix_len + i] = 0x10;
b[prefix_len + i] = 0x20;
}
RzDiff *d = rz_diff_bytes_new(a, prefix_len + tail_len, b, prefix_len + tail_len);
mu_assert_notnull(d, "long-prefix diff");
RzList *ops = rz_diff_opcodes_new(d);
mu_assert_notnull(ops, "ops not null");
// First op must be EQUAL and must cover the entire prefix.
RzDiffOp *first = rz_list_first_val(ops);
mu_assert_eq((int)first->type, (int)RZ_DIFF_OP_EQUAL, "first op EQUAL");
mu_assert_eq(first->a_beg, 0, "EQUAL starts at 0");
mu_assert_eq(first->a_end, (st32)prefix_len, "EQUAL covers full prefix");
rz_list_free(ops);
rz_diff_free(d);
free(a);
free(b);
mu_end;
}
int all_tests() {
mu_run_test(test_rz_diff_distances);
mu_run_test(test_rz_diff_unified_lines);
mu_run_test(test_rz_diff_unified_bytes);
mu_run_test(test_rz_diff_hash_data);
mu_run_test(test_rz_diff_get_a_b);
mu_run_test(test_rz_diff_null_inputs);
mu_run_test(test_rz_diff_empty_buffers);
mu_run_test(test_rz_diff_identical_buffers);
mu_run_test(test_rz_diff_completely_different);
mu_run_test(test_rz_diff_pure_insert_and_delete);
mu_run_test(test_rz_diff_matches_basic);
mu_run_test(test_rz_diff_opcodes_grouped_identical);
mu_run_test(test_rz_diff_opcodes_grouped_replace);
mu_run_test(test_rz_diff_ratio_bounds);
mu_run_test(test_rz_diff_sizes_ratio_skewed);
mu_run_test(test_rz_diff_ignore_line);
mu_run_test(test_rz_diff_generic_ints);
mu_run_test(test_rz_diff_generic_ints_modified);
mu_run_test(test_rz_diff_unified_json_smoke);
mu_run_test(test_rz_diff_ratio_byte_line_agree_on_identical);
mu_run_test(test_rz_diff_single_element);
mu_run_test(test_rz_diff_stress_medium);
mu_run_test(test_rz_diff_all_zeros_pathological);
mu_run_test(test_rz_diff_repeating_pattern);
mu_run_test(test_rz_diff_firmware_like_structure);
mu_run_test(test_rz_diff_block_shift);
mu_run_test(test_rz_diff_growing_buffer);
mu_run_test(test_rz_diff_long_common_prefix);
return tests_passed != tests_run;
}
mu_main(all_tests)