rizin/test/unit/test_agraph.c
Giovanni 6131a0e187
Fix RZ_IPI usage on public headers & fix memory leak in the ROP code. (#6171)
* Fix RZ_IPI declaration on public api for rz_pdb

* Fix RZ_IPI declaration on public api for jemalloc/glibc

* Fix RZ_IPI declaration on public api for rz_io

* Remove functions from public header for rz_panels

* Fix public functions not following rizin nomenclature in rz_graph

* Fix public functions not following rizin nomenclature in rz_rop

* move resolve_fcn_name into rz_analysis

* Fix public functions not following rizin nomenclature in rz_ascii_table

* Fix leak in rop code

* Rewrite parser for rop

* Add doxygen comment
2026-04-08 13:37:17 +08:00

415 lines
13 KiB
C

// SPDX-FileCopyrightText: 2020 HoundThe <cgkajm@gmail.com>
// SPDX-FileCopyrightText: 2025 heersin <teablearcher@gmail.com>
// SPDX-License-Identifier: LGPL-3.0-only
#include <rz_core.h>
#include <rz_analysis.h>
#include <rz_agraph.h>
#include <rz_util.h>
#include "minunit.h"
bool test_graph_to_agraph() {
RzCore *core = rz_core_new();
rz_core_cmd0(core, "ac A");
rz_core_cmd0(core, "ac B");
rz_core_cmd0(core, "ac C");
rz_core_cmd0(core, "ac D");
rz_core_cmd0(core, "acb B A");
rz_core_cmd0(core, "acb C A");
rz_core_cmd0(core, "acb D B");
rz_core_cmd0(core, "acb D C");
RzGraph *graph = rz_analysis_class_get_inheritance_graph(core->analysis);
mu_assert_notnull(graph, "Couldn't create the graph");
mu_assert_eq(rz_graph_count_nodes(graph), 4, "Wrong node count");
RzAGraph *agraph = rz_core_create_agraph_from_graph(graph, false);
mu_assert_notnull(agraph, "Couldn't create the agraph");
mu_assert_eq(rz_graph_count_nodes(agraph->graph), 4, "Wrong agraph node count");
RzIterator *iter = rz_graph_get_nodes(agraph->graph);
mu_assert_notnull(iter, "get_nodes iterator");
RzGraphNode *node;
int i = 0;
rz_iterator_foreach(iter, node) {
const RzANode *info = rz_graph_node_get_data(node);
switch (i++) {
case 0:
mu_assert_streq(info->title, "A", "Wrong node name");
mu_assert_eq(rz_graph_out_degree(agraph->graph, node), 2, "Wrong node out-nodes");
{
RzIterator *out_iter = rz_graph_out_neighbors(agraph->graph, node);
mu_assert_notnull(out_iter, "out_neighbors iter A");
RzGraphNode *out_node;
int j = 0;
rz_iterator_foreach(out_iter, out_node) {
const RzANode *out_info = rz_graph_node_get_data(out_node);
switch (j++) {
case 0:
mu_assert_streq(out_info->title, "B", "Wrong node name");
break;
case 1:
mu_assert_streq(out_info->title, "C", "Wrong node name");
break;
}
}
rz_iterator_free(out_iter);
}
break;
case 1:
mu_assert_streq(info->title, "B", "Wrong node name");
mu_assert_eq(rz_graph_out_degree(agraph->graph, node), 1, "Wrong node out-nodes");
mu_assert_eq(rz_graph_in_degree(agraph->graph, node), 1, "Wrong node in-nodes");
{
RzIterator *out_iter = rz_graph_out_neighbors(agraph->graph, node);
mu_assert_notnull(out_iter, "out_neighbors iter B");
RzGraphNode *out_node;
int j = 0;
rz_iterator_foreach(out_iter, out_node) {
const RzANode *out_info = rz_graph_node_get_data(out_node);
switch (j++) {
case 0:
mu_assert_streq(out_info->title, "D", "Wrong node name");
break;
}
}
rz_iterator_free(out_iter);
}
break;
case 2:
mu_assert_streq(info->title, "C", "Wrong node name");
mu_assert_eq(rz_graph_out_degree(agraph->graph, node), 1, "Wrong node out-nodes");
mu_assert_eq(rz_graph_in_degree(agraph->graph, node), 1, "Wrong node in-nodes");
{
RzIterator *out_iter = rz_graph_out_neighbors(agraph->graph, node);
mu_assert_notnull(out_iter, "out_neighbors iter C");
RzGraphNode *out_node;
int j = 0;
rz_iterator_foreach(out_iter, out_node) {
const RzANode *out_info = rz_graph_node_get_data(out_node);
switch (j++) {
case 0:
mu_assert_streq(out_info->title, "D", "Wrong node name");
break;
}
}
rz_iterator_free(out_iter);
}
break;
case 3:
mu_assert_streq(info->title, "D", "Wrong node name");
mu_assert_eq(rz_graph_in_degree(agraph->graph, node), 2, "Wrong node in-nodes");
break;
default:
break;
}
}
rz_iterator_free(iter);
rz_core_free(core);
rz_graph_free(graph);
rz_agraph_free(agraph);
mu_end;
}
/* Helper: look up an RzANode by title inside an RzAGraph. */
static RzANode *find_anode(RzAGraph *ag, const char *title) {
return rz_agraph_get_node(ag, title);
}
/*
* Build a minimal RzAGraph (no canvas, no core) with the given nodes and edges.
* Nodes are added in the order of `titles`; edges are added in the order of `edges`.
* Returns the graph; caller must rz_agraph_free() it.
*/
/*
* build_agraph: allocate an RzAGraph with the given nodes and edges.
* The caller must keep a live RzCons (rz_cons_new) for the lifetime of the
* graph, because set_layout calls rz_cons_is_breaked() internally.
* The caller is responsible for rz_agraph_free() and rz_cons_free().
*/
static RzAGraph *build_agraph(const char **titles, int n_titles,
const char *edges[][2], int n_edges) {
RzAGraph *ag = rz_agraph_new(NULL);
if (!ag) {
return NULL;
}
for (int i = 0; i < n_titles; i++) {
rz_agraph_add_node(ag, titles[i], "");
}
for (int i = 0; i < n_edges; i++) {
RzANode *from = find_anode(ag, edges[i][0]);
RzANode *to = find_anode(ag, edges[i][1]);
if (from && to) {
rz_agraph_add_edge(ag, from, to);
}
}
return ag;
}
/*
* test_layout_back_edge_simple
*
* Graph (edges created in this order):
* A -> B (nth=-, order=0)
* B -> exit (nth=-, order=1)
* B -> C (nth=-, order=2)
* C -> A (nth=-, order=3)
*
* DFS traversal (edges sorted by creation order, LIFO stack):
* discover A(gray) -> push finish(A), neighbor B
* discover B(gray) -> push finish(B), neighbors [exit, C]
* pop (B,C): discover C(gray) -> push finish(C), neighbor A
* pop (C,A): A is GRAY -> back_edge(C->A)
* finish C
* pop (B,exit): discover exit(gray) -> finish exit
* finish B -> finish A
*
* Expected back_edge: C -> A
* After reversing C->A, DAG has: A->B, B->exit, B->C, A->C
* Expected layers: A=0, B=1, C=2, exit=2
* (exit and C both have max parent layer = 1, so layer = 2)
*
* Verified by checking that back_edge C->A is the only edge
* going from a higher layer to a lower layer in the original graph.
*/
bool test_layout_back_edge_simple() {
RzCons *cons = rz_cons_new();
mu_assert_notnull(cons, "rz_cons_new");
const char *titles[] = { "A", "B", "C", "exit" };
const char *edges[][2] = {
{ "A", "B" },
{ "B", "exit" },
{ "B", "C" },
{ "C", "A" },
};
RzAGraph *ag = build_agraph(titles, 4, edges, 4);
mu_assert_notnull(ag, "build_agraph");
rz_agraph_compute_layout(ag);
RzANode *nA = find_anode(ag, "A");
RzANode *nB = find_anode(ag, "B");
RzANode *nC = find_anode(ag, "C");
RzANode *nExit = find_anode(ag, "exit");
mu_assert_notnull(nA, "node A");
mu_assert_notnull(nB, "node B");
mu_assert_notnull(nC, "node C");
mu_assert_notnull(nExit, "node exit");
/* Layer assignment reflects the DAG after removing C->A as back_edge */
mu_assert_eq(nA->layer, 0, "layer A");
mu_assert_eq(nB->layer, 1, "layer B");
/* C and exit both have max incoming layer = 1, so layer = 2 */
mu_assert_eq(nC->layer, 2, "layer C");
mu_assert_eq(nExit->layer, 2, "layer exit");
/*
* Confirm which edge violates layer monotonicity in the original graph —
* that is the back_edge chosen by the DFS.
* C->A: layer(C)=2 > layer(A)=0 => back_edge ✓
* A->B: layer(A)=0 < layer(B)=1 => forward ✓
* B->C: layer(B)=1 < layer(C)=2 => forward ✓
* B->exit: layer(B)=1 < layer(exit)=2 => forward ✓
*/
mu_assert_true(nC->layer > nA->layer, "C->A is the back_edge (layer C > layer A)");
mu_assert_true(nA->layer < nB->layer, "A->B is a forward edge");
mu_assert_true(nB->layer < nC->layer, "B->C is a forward edge");
mu_assert_true(nB->layer < nExit->layer, "B->exit is a forward edge");
rz_agraph_free(ag);
rz_cons_free();
mu_end;
}
/*
* test_layout_back_edge_complex
*
* Graph (edges created in this order):
* A -> B (order=0)
* A -> C (order=1)
* B -> D (order=2)
* B -> A (order=3)
* C -> D (order=4)
* D -> A (order=5)
*
* DFS traversal (edges sorted by creation order, LIFO stack):
* discover A(gray) -> neighbors [B(0), C(1)], pushed as (A,B),(A,C)
* pop (A,C): discover C(gray) -> neighbor D(4) -> push (C,D)
* pop (C,D): discover D(gray) -> neighbor A(5) -> push (D,A)
* pop (D,A): A is GRAY -> back_edge(D->A)
* finish D; finish C
* pop (A,B): discover B(gray) -> neighbors [D(2), A(3)] -> push (B,D),(B,A)
* pop (B,A): A is GRAY -> back_edge(B->A)
* pop (B,D): D is BLACK -> fcross_edge (not a back_edge)
* finish B; finish A
*
* Expected back_edges: { D->A, B->A }
* After reversing both: DAG has A->B, A->C, A->D, C->D (B->D dropped, B->A reversed)
* Expected layers: A=0, B=1, C=1, D=2
*
* Verified by checking that both D->A and B->A go from higher to lower layer,
* while all other original edges go from lower to higher layer.
*/
bool test_layout_back_edge_complex() {
RzCons *cons = rz_cons_new();
mu_assert_notnull(cons, "rz_cons_new");
const char *titles[] = { "A", "B", "C", "D" };
const char *edges[][2] = {
{ "A", "B" },
{ "A", "C" },
{ "B", "D" },
{ "B", "A" },
{ "C", "D" },
{ "D", "A" },
};
RzAGraph *ag = build_agraph(titles, 4, edges, 6);
mu_assert_notnull(ag, "build_agraph");
rz_agraph_compute_layout(ag);
RzANode *nA = find_anode(ag, "A");
RzANode *nB = find_anode(ag, "B");
RzANode *nC = find_anode(ag, "C");
RzANode *nD = find_anode(ag, "D");
mu_assert_notnull(nA, "node A");
mu_assert_notnull(nB, "node B");
mu_assert_notnull(nC, "node C");
mu_assert_notnull(nD, "node D");
mu_assert_eq(nA->layer, 0, "layer A");
mu_assert_eq(nB->layer, 1, "layer B");
mu_assert_eq(nC->layer, 1, "layer C");
mu_assert_eq(nD->layer, 2, "layer D");
/* back_edges: D->A and B->A both go from higher to lower layer */
mu_assert_true(nD->layer > nA->layer, "D->A is a back_edge (layer D > layer A)");
mu_assert_true(nB->layer > nA->layer, "B->A is a back_edge (layer B > layer A)");
/* forward edges */
mu_assert_true(nA->layer < nB->layer, "A->B is a forward edge");
mu_assert_true(nA->layer < nC->layer, "A->C is a forward edge");
mu_assert_true(nB->layer < nD->layer, "B->D is a forward edge");
mu_assert_true(nC->layer < nD->layer, "C->D is a forward edge");
rz_agraph_free(ag);
rz_cons_free();
mu_end;
}
/*
* test_layout_topo_sort
*
* Diamond graph (no cycles):
* A -> B (order=0)
* A -> C (order=1)
* B -> D (order=2)
* C -> D (order=3)
*
* No back_edges. DFS topological finish order: D, B, C, A (or D, C, B, A).
* assign_layers sets layer = max(parent layers) + 1:
* A: no parents -> layer 0
* B: parent A(0) -> layer 1
* C: parent A(0) -> layer 1
* D: parents B(1), C(1) -> layer 2
*
* The test verifies the topological property:
* for every edge u->v in the graph, layer(u) < layer(v).
*/
bool test_layout_topo_sort_diamond() {
RzCons *cons = rz_cons_new();
mu_assert_notnull(cons, "rz_cons_new");
const char *titles[] = { "A", "B", "C", "D" };
const char *edges[][2] = {
{ "A", "B" },
{ "A", "C" },
{ "B", "D" },
{ "C", "D" },
};
RzAGraph *ag = build_agraph(titles, 4, edges, 4);
mu_assert_notnull(ag, "build_agraph");
rz_agraph_compute_layout(ag);
RzANode *nA = find_anode(ag, "A");
RzANode *nB = find_anode(ag, "B");
RzANode *nC = find_anode(ag, "C");
RzANode *nD = find_anode(ag, "D");
mu_assert_notnull(nA, "node A");
mu_assert_notnull(nB, "node B");
mu_assert_notnull(nC, "node C");
mu_assert_notnull(nD, "node D");
/* Exact layer values */
mu_assert_eq(nA->layer, 0, "layer A");
mu_assert_eq(nB->layer, 1, "layer B");
mu_assert_eq(nC->layer, 1, "layer C");
mu_assert_eq(nD->layer, 2, "layer D");
/* Topological property: every edge goes from lower to higher layer */
mu_assert_true(nA->layer < nB->layer, "topo: A < B");
mu_assert_true(nA->layer < nC->layer, "topo: A < C");
mu_assert_true(nB->layer < nD->layer, "topo: B < D");
mu_assert_true(nC->layer < nD->layer, "topo: C < D");
rz_agraph_free(ag);
rz_cons_free();
mu_end;
}
/*
* test_layout_topo_sort_chain
*
* Linear chain: A -> B -> C -> D -> E
*
* assign_layers produces strictly increasing layers:
* A=0, B=1, C=2, D=3, E=4
*
* This is the degenerate case where topo sort and layer assignment
* produce a single unique ordering.
*/
bool test_layout_topo_sort_chain() {
RzCons *cons = rz_cons_new();
mu_assert_notnull(cons, "rz_cons_new");
const char *titles[] = { "A", "B", "C", "D", "E" };
const char *edges[][2] = {
{ "A", "B" },
{ "B", "C" },
{ "C", "D" },
{ "D", "E" },
};
RzAGraph *ag = build_agraph(titles, 5, edges, 4);
mu_assert_notnull(ag, "build_agraph");
rz_agraph_compute_layout(ag);
RzANode *nA = find_anode(ag, "A");
RzANode *nB = find_anode(ag, "B");
RzANode *nC = find_anode(ag, "C");
RzANode *nD = find_anode(ag, "D");
RzANode *nE = find_anode(ag, "E");
mu_assert_notnull(nA, "node A");
mu_assert_notnull(nB, "node B");
mu_assert_notnull(nC, "node C");
mu_assert_notnull(nD, "node D");
mu_assert_notnull(nE, "node E");
mu_assert_eq(nA->layer, 0, "layer A");
mu_assert_eq(nB->layer, 1, "layer B");
mu_assert_eq(nC->layer, 2, "layer C");
mu_assert_eq(nD->layer, 3, "layer D");
mu_assert_eq(nE->layer, 4, "layer E");
rz_agraph_free(ag);
rz_cons_free();
mu_end;
}
int all_tests() {
mu_run_test(test_graph_to_agraph);
mu_run_test(test_layout_back_edge_simple);
mu_run_test(test_layout_back_edge_complex);
mu_run_test(test_layout_topo_sort_diamond);
mu_run_test(test_layout_topo_sort_chain);
return tests_passed != tests_run;
}
mu_main(all_tests)