mirror of
https://github.com/radareorg/r2xsql.git
synced 2026-11-07 06:36:02 -10:00
Fix stale Make objects and migrate regression tests to r2r
This commit is contained in:
1 parent
528b22f41d
commit
808f9bc83c
20 files changed
+608
-658
No files matched your search
@@ -60,6 +60,12 @@ jobs:
|
||||
-DRadare2_ROOT="$GITHUB_WORKSPACE/r2prefix"
|
||||
cmake --build build --config Release
|
||||
|
||||
- name: Run r2r regression tests
|
||||
run: |
|
||||
export PATH="$GITHUB_WORKSPACE/r2prefix/bin:$PATH"
|
||||
export LD_LIBRARY_PATH="$GITHUB_WORKSPACE/r2prefix/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
|
||||
make -C tests R2XSQL_PLUGIN="$GITHUB_WORKSPACE/build/bin/core_r2xsql.so"
|
||||
|
||||
- name: Stage artifacts
|
||||
shell: bash
|
||||
run: bash .github/scripts/stage_artifacts.sh
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
# make build build-make/core_r2xsql.$(R2_LIBEXT)
|
||||
# make user-install install into the per-user plugin dir (no root)
|
||||
# make install install system-wide (R2_LIBR_PLUGINS)
|
||||
# make test build, then load the plugin in r2 and run one SQL query
|
||||
# make test build, then run the r2r regression suite
|
||||
# make clean
|
||||
#
|
||||
# Requires: a C++20 compiler, radare2 with pkg-config (`pkg-config r_core`),
|
||||
@@ -22,6 +22,7 @@
|
||||
# The radare2 driver. Most installs expose both `r2` and `radare2`; override
|
||||
# with `make R2=radare2` if only the long name is on PATH.
|
||||
R2?=r2
|
||||
R2R?=r2r
|
||||
R2_LIBEXT=$(shell $(R2) -H R2_LIBEXT)
|
||||
R2_USER_PLUGINS=$(shell $(R2) -H R2_USER_PLUGINS)
|
||||
R2_LIBR_PLUGINS=$(shell $(R2) -H R2_LIBR_PLUGINS)
|
||||
@@ -83,6 +84,9 @@ OBJS=$(SQLITE_OBJ) $(XSQL_OBJS) $(R2XSQL_OBJS) $(PLUGIN_OBJ)
|
||||
all: $(PLUGIN)
|
||||
|
||||
-include $(BUILD)/libxsql.mk
|
||||
-include $(OBJS:.o=.d)
|
||||
|
||||
.DELETE_ON_ERROR:
|
||||
|
||||
# libxsql fetch (only if the sibling is absent and we redirected into build-make)
|
||||
$(LIBXSQL_DIR)/CMakeLists.txt:
|
||||
@@ -93,21 +97,22 @@ $(BUILD)/libxsql.mk: $(LIBXSQL_DIR)/CMakeLists.txt
|
||||
@mkdir -p $(dir $@)
|
||||
@touch $@
|
||||
|
||||
$(BUILD)/obj/sqlite/sqlite3.o: $(LIBXSQL_DIR)/external/sqlite/sqlite3.c | $(LIBXSQL_DIR)/CMakeLists.txt
|
||||
# Rebuild legacy objects without .d files when this Makefile is updated.
|
||||
$(BUILD)/obj/sqlite/sqlite3.o: $(LIBXSQL_DIR)/external/sqlite/sqlite3.c Makefile | $(LIBXSQL_DIR)/CMakeLists.txt
|
||||
@mkdir -p $(dir $@)
|
||||
$(CC) $(CFLAGS) -c $< -o $@
|
||||
$(CC) $(CFLAGS) -MMD -MP -c $< -o $@
|
||||
|
||||
$(BUILD)/obj/xsql/%.o: $(LIBXSQL_DIR)/src/%.cpp | $(LIBXSQL_DIR)/CMakeLists.txt
|
||||
$(BUILD)/obj/xsql/%.o: $(LIBXSQL_DIR)/src/%.cpp Makefile | $(LIBXSQL_DIR)/CMakeLists.txt
|
||||
@mkdir -p $(dir $@)
|
||||
$(CXX) $(CXXFLAGS) -c $< -o $@
|
||||
$(CXX) $(CXXFLAGS) -MMD -MP -c $< -o $@
|
||||
|
||||
$(BUILD)/obj/r2xsql/%.o: src/lib/src/%.cpp
|
||||
$(BUILD)/obj/r2xsql/%.o: src/lib/src/%.cpp Makefile
|
||||
@mkdir -p $(dir $@)
|
||||
$(CXX) $(CXXFLAGS) -c $< -o $@
|
||||
$(CXX) $(CXXFLAGS) -MMD -MP -c $< -o $@
|
||||
|
||||
$(BUILD)/obj/plugin/core_r2xsql.o: src/plugin/core_r2xsql.cpp
|
||||
$(BUILD)/obj/plugin/core_r2xsql.o: src/plugin/core_r2xsql.cpp Makefile
|
||||
@mkdir -p $(dir $@)
|
||||
$(CXX) $(CXXFLAGS) -c $< -o $@
|
||||
$(CXX) $(CXXFLAGS) -MMD -MP -c $< -o $@
|
||||
|
||||
$(PLUGIN): $(OBJS)
|
||||
$(CXX) -shared -o $@ $(OBJS) $(LDFLAGS)
|
||||
@@ -132,10 +137,9 @@ user-install:
|
||||
user-uninstall:
|
||||
$(MAKE) uninstall PLUGDIR="$(R2_USER_PLUGINS)"
|
||||
|
||||
# Smoke test: load the freshly built plugin and run one query.
|
||||
# Check output as well as process status: r2 can exit 0 after an SQL exception.
|
||||
test: $(PLUGIN)
|
||||
@echo "=== loading core_r2xsql and running a SQL query ==="
|
||||
$(R2) -i "$(CWD)/$(PLUGIN)" -qc 'aa; sql SELECT count(*) AS n FROM funcs' -- /bin/ls
|
||||
$(MAKE) -C tests R2="$(R2)" R2R="$(R2R)" R2XSQL_PLUGIN="$(abspath $(PLUGIN))"
|
||||
|
||||
clean:
|
||||
rm -rf $(BUILD)/obj $(PLUGIN)
|
||||
|
||||
@@ -215,18 +215,36 @@ configure the native server with r2's `http.*` settings instead.
|
||||
|
||||
## Tests
|
||||
|
||||
After building the plugin, with radare2 and a C compiler on `PATH`:
|
||||
Run the regression suite with radare2, `r2r`, a C compiler and Python 3 on
|
||||
`PATH` (Python's standard library is used only for the HTTP transport test):
|
||||
|
||||
```sh
|
||||
python3 -m pip install pytest
|
||||
python3 -m pytest -q tests
|
||||
make test
|
||||
```
|
||||
|
||||
The tests load the plugin directly into radare2 and cover the SQL surface and
|
||||
native HTTP integration (GET and POST). They find `core_r2xsql` in `build/`,
|
||||
`build-make/`, `build-meson/`, or `out/`. Set `R2XSQL_PLUGIN` for other layouts
|
||||
and `R2_BIN` to choose the matching radare2 executable. A missing plugin fails
|
||||
the test run instead of silently skipping coverage.
|
||||
This builds the plugin and runs the r2r database in `tests/db`. Each test loads
|
||||
the exact build explicitly, ignoring installed user plugins, and checks its
|
||||
output; SQL exceptions fail the suite even when radare2 exits successfully.
|
||||
The fixture is compiled locally, with no downloaded test binaries or pytest.
|
||||
|
||||
To test an existing CMake/Meson build, or select matching radare2 tools:
|
||||
|
||||
```sh
|
||||
make -C tests R2XSQL_PLUGIN=/absolute/path/core_r2xsql.so R2=/path/to/r2 R2R=/path/to/r2r
|
||||
# Only command tests (no Python needed):
|
||||
make -C tests TESTS=db/cmd
|
||||
```
|
||||
|
||||
`R2R_ARGS` accepts r2r options (default `-L`); for example, `R2R_ARGS=-i` enables
|
||||
interactive failure review. One small standard-library helper retains real
|
||||
HTTP GET/POST coverage, including server startup and shutdown; r2r checks its
|
||||
result alongside the command tests.
|
||||
|
||||
The Make build tracks header dependencies. This prevents stale adapters after
|
||||
C++ interface changes, which could previously cause `std::bad_alloc` during
|
||||
`make test`. After updating this Makefile, existing objects are rebuilt once
|
||||
to establish dependency files. For older checkouts, `make clean && make test`
|
||||
clears those stale objects.
|
||||
|
||||
## Backends (internal detail)
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# SPDX-License-Identifier: LicenseRef-Human-Origin-Source-1.0
|
||||
R2?=r2
|
||||
R2R?=r2r
|
||||
R2XSQL_PLUGIN?=../build-make/core_r2xsql.$(shell $(R2) -H R2_LIBEXT)
|
||||
R2R_ARGS?=-L
|
||||
TESTS?=db
|
||||
CC?=cc
|
||||
|
||||
all: test
|
||||
|
||||
build/fixture.bin: fixture.c Makefile
|
||||
@mkdir -p build
|
||||
$(CC) -O0 -g $< -o $@
|
||||
|
||||
# Explicit loading works with r2r's -NN and ignores installed user plugins.
|
||||
# Regenerate when switching build directories, even if the plugin is older.
|
||||
load:
|
||||
@test -f "$(R2XSQL_PLUGIN)" || { echo "Plugin not found: $(R2XSQL_PLUGIN)"; exit 1; }
|
||||
@mkdir -p build
|
||||
@printf 'L %s\n' "$(abspath $(R2XSQL_PLUGIN))" > build/load.r2
|
||||
|
||||
test: build/fixture.bin load
|
||||
R2R_RADARE2="$(R2)" R2XSQL_PLUGIN="$(abspath $(R2XSQL_PLUGIN))" $(R2R) -u $(R2R_ARGS) -C . $(TESTS)
|
||||
|
||||
clean:
|
||||
rm -rf build
|
||||
|
||||
.PHONY: all test load clean
|
||||
@@ -1,46 +0,0 @@
|
||||
# Copyright (c) 2024-2026 Elias Bachaalany
|
||||
# SPDX-License-Identifier: LicenseRef-Human-Origin-Source-1.0
|
||||
"""pytest fixtures for the r2xsql test-suite."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
|
||||
import pytest
|
||||
|
||||
from test_support import R2xsqlRunner, build_fixture, find_r2xsql_plugin
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def r2_bin() -> str:
|
||||
path = os.environ.get("R2_BIN") or shutil.which("radare2") or shutil.which("r2")
|
||||
path = shutil.which(path) if path else None
|
||||
if not path:
|
||||
pytest.fail("radare2 is required; put it on PATH or set R2_BIN", pytrace=False)
|
||||
return str(Path(path).resolve())
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def r2xsql_plugin() -> Path:
|
||||
path = find_r2xsql_plugin()
|
||||
if path is None:
|
||||
pytest.fail(
|
||||
"no core_r2xsql plugin found. Build it first or set R2XSQL_PLUGIN "
|
||||
"to its path.", pytrace=False,
|
||||
)
|
||||
return path
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def fixture_binary(tmp_path_factory: pytest.TempPathFactory) -> Path:
|
||||
"""The compiled, deterministic fixture binary (see fixture.c), built
|
||||
once per test session into a session-scoped temp directory."""
|
||||
out_dir = tmp_path_factory.mktemp("r2xsql_fixture")
|
||||
return build_fixture(out_dir)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def runner(r2_bin: str, r2xsql_plugin: Path, fixture_binary: Path) -> R2xsqlRunner:
|
||||
return R2xsqlRunner(r2_bin, r2xsql_plugin, fixture_binary)
|
||||
@@ -0,0 +1,254 @@
|
||||
# Command aliases preserve quoted SQL when applying native JSON filters.
|
||||
NAME=blocks: exact function start, interior and absent address
|
||||
FILE=malloc://1024
|
||||
ARGS=-e log.level=0 -e bin.relocs.apply=true -i build/load.r2 -a x86 -b 64
|
||||
CMDS=<<EOF
|
||||
wx 31c0ffc083f8037cf9c3
|
||||
af @ 0
|
||||
"sql,:quiet SELECT count(*) >= 1 FROM blocks WHERE func_addr=0"
|
||||
"$query=\"sqlj SELECT * FROM blocks WHERE func_addr=1\""
|
||||
$query~{results[0].rows}
|
||||
"$query=\"sqlj SELECT * FROM blocks WHERE func_addr=128\""
|
||||
$query~{results[0].rows}
|
||||
EOF
|
||||
EXPECT=<<EOF
|
||||
1
|
||||
[]
|
||||
[]
|
||||
EOF
|
||||
EXPECT_ERR=
|
||||
RUN
|
||||
|
||||
NAME=cfg_edges: exact function start, interior and absent address
|
||||
FILE=malloc://1024
|
||||
ARGS=-e log.level=0 -e bin.relocs.apply=true -i build/load.r2 -a x86 -b 64
|
||||
CMDS=<<EOF
|
||||
wx 31c0ffc083f8037cf9c3
|
||||
af @ 0
|
||||
"sql,:quiet SELECT count(*) >= 1 FROM cfg_edges WHERE func_addr=0"
|
||||
"$query=\"sqlj SELECT * FROM cfg_edges WHERE func_addr=1\""
|
||||
$query~{results[0].rows}
|
||||
"$query=\"sqlj SELECT * FROM cfg_edges WHERE func_addr=128\""
|
||||
$query~{results[0].rows}
|
||||
EOF
|
||||
EXPECT=<<EOF
|
||||
1
|
||||
[]
|
||||
[]
|
||||
EOF
|
||||
EXPECT_ERR=
|
||||
RUN
|
||||
|
||||
NAME=switch_tables: exact function start, interior and absent address
|
||||
FILE=malloc://1024
|
||||
ARGS=-e log.level=0 -e bin.relocs.apply=true -i build/load.r2 -a x86 -b 64
|
||||
CMDS=<<EOF
|
||||
wx 31c0ffc083f8037cf9c3
|
||||
af @ 0
|
||||
"sql,:quiet SELECT count(*) >= 0 FROM switch_tables WHERE func_addr=0"
|
||||
"$query=\"sqlj SELECT * FROM switch_tables WHERE func_addr=1\""
|
||||
$query~{results[0].rows}
|
||||
"$query=\"sqlj SELECT * FROM switch_tables WHERE func_addr=128\""
|
||||
$query~{results[0].rows}
|
||||
EOF
|
||||
EXPECT=<<EOF
|
||||
1
|
||||
[]
|
||||
[]
|
||||
EOF
|
||||
EXPECT_ERR=
|
||||
RUN
|
||||
|
||||
NAME=dominators: exact function start, interior and absent address
|
||||
FILE=malloc://1024
|
||||
ARGS=-e log.level=0 -e bin.relocs.apply=true -i build/load.r2 -a x86 -b 64
|
||||
CMDS=<<EOF
|
||||
wx 31c0ffc083f8037cf9c3
|
||||
af @ 0
|
||||
"sql,:quiet SELECT count(*) >= 1 FROM dominators WHERE func_addr=0"
|
||||
"$query=\"sqlj SELECT * FROM dominators WHERE func_addr=1\""
|
||||
$query~{results[0].rows}
|
||||
"$query=\"sqlj SELECT * FROM dominators WHERE func_addr=128\""
|
||||
$query~{results[0].rows}
|
||||
EOF
|
||||
EXPECT=<<EOF
|
||||
1
|
||||
[]
|
||||
[]
|
||||
EOF
|
||||
EXPECT_ERR=
|
||||
RUN
|
||||
|
||||
NAME=post_dominators: exact function start, interior and absent address
|
||||
FILE=malloc://1024
|
||||
ARGS=-e log.level=0 -e bin.relocs.apply=true -i build/load.r2 -a x86 -b 64
|
||||
CMDS=<<EOF
|
||||
wx 31c0ffc083f8037cf9c3
|
||||
af @ 0
|
||||
"sql,:quiet SELECT count(*) >= 1 FROM post_dominators WHERE func_addr=0"
|
||||
"$query=\"sqlj SELECT * FROM post_dominators WHERE func_addr=1\""
|
||||
$query~{results[0].rows}
|
||||
"$query=\"sqlj SELECT * FROM post_dominators WHERE func_addr=128\""
|
||||
$query~{results[0].rows}
|
||||
EOF
|
||||
EXPECT=<<EOF
|
||||
1
|
||||
[]
|
||||
[]
|
||||
EOF
|
||||
EXPECT_ERR=
|
||||
RUN
|
||||
|
||||
NAME=loops: exact function start, interior and absent address
|
||||
FILE=malloc://1024
|
||||
ARGS=-e log.level=0 -e bin.relocs.apply=true -i build/load.r2 -a x86 -b 64
|
||||
CMDS=<<EOF
|
||||
wx 31c0ffc083f8037cf9c3
|
||||
af @ 0
|
||||
"sql,:quiet SELECT count(*) >= 1 FROM loops WHERE func_addr=0"
|
||||
"$query=\"sqlj SELECT * FROM loops WHERE func_addr=1\""
|
||||
$query~{results[0].rows}
|
||||
"$query=\"sqlj SELECT * FROM loops WHERE func_addr=128\""
|
||||
$query~{results[0].rows}
|
||||
EOF
|
||||
EXPECT=<<EOF
|
||||
1
|
||||
[]
|
||||
[]
|
||||
EOF
|
||||
EXPECT_ERR=
|
||||
RUN
|
||||
|
||||
NAME=instructions: exact function start, interior and absent address
|
||||
FILE=malloc://1024
|
||||
ARGS=-e log.level=0 -e bin.relocs.apply=true -i build/load.r2 -a x86 -b 64
|
||||
CMDS=<<EOF
|
||||
wx 31c0ffc083f8037cf9c3
|
||||
af @ 0
|
||||
"sql,:quiet SELECT count(*) >= 1 FROM instructions WHERE func_addr=0"
|
||||
"$query=\"sqlj SELECT * FROM instructions WHERE func_addr=1\""
|
||||
$query~{results[0].rows}
|
||||
"$query=\"sqlj SELECT * FROM instructions WHERE func_addr=128\""
|
||||
$query~{results[0].rows}
|
||||
EOF
|
||||
EXPECT=<<EOF
|
||||
1
|
||||
[]
|
||||
[]
|
||||
EOF
|
||||
EXPECT_ERR=
|
||||
RUN
|
||||
|
||||
NAME=instruction_operands: exact function start, interior and absent address
|
||||
FILE=malloc://1024
|
||||
ARGS=-e log.level=0 -e bin.relocs.apply=true -i build/load.r2 -a x86 -b 64
|
||||
CMDS=<<EOF
|
||||
wx 31c0ffc083f8037cf9c3
|
||||
af @ 0
|
||||
"sql,:quiet SELECT count(*) >= 1 FROM instruction_operands WHERE func_addr=0"
|
||||
"$query=\"sqlj SELECT * FROM instruction_operands WHERE func_addr=1\""
|
||||
$query~{results[0].rows}
|
||||
"$query=\"sqlj SELECT * FROM instruction_operands WHERE func_addr=128\""
|
||||
$query~{results[0].rows}
|
||||
EOF
|
||||
EXPECT=<<EOF
|
||||
1
|
||||
[]
|
||||
[]
|
||||
EOF
|
||||
EXPECT_ERR=
|
||||
RUN
|
||||
|
||||
NAME=esil_ops: exact function start, interior and absent address
|
||||
FILE=malloc://1024
|
||||
ARGS=-e log.level=0 -e bin.relocs.apply=true -i build/load.r2 -a x86 -b 64
|
||||
CMDS=<<EOF
|
||||
wx 31c0ffc083f8037cf9c3
|
||||
af @ 0
|
||||
"sql,:quiet SELECT count(*) >= 1 FROM esil_ops WHERE func_addr=0"
|
||||
"$query=\"sqlj SELECT * FROM esil_ops WHERE func_addr=1\""
|
||||
$query~{results[0].rows}
|
||||
"$query=\"sqlj SELECT * FROM esil_ops WHERE func_addr=128\""
|
||||
$query~{results[0].rows}
|
||||
EOF
|
||||
EXPECT=<<EOF
|
||||
1
|
||||
[]
|
||||
[]
|
||||
EOF
|
||||
EXPECT_ERR=
|
||||
RUN
|
||||
|
||||
NAME=esil_operands: exact function start, interior and absent address
|
||||
FILE=malloc://1024
|
||||
ARGS=-e log.level=0 -e bin.relocs.apply=true -i build/load.r2 -a x86 -b 64
|
||||
CMDS=<<EOF
|
||||
wx 31c0ffc083f8037cf9c3
|
||||
af @ 0
|
||||
"sql,:quiet SELECT count(*) >= 1 FROM esil_operands WHERE func_addr=0"
|
||||
"$query=\"sqlj SELECT * FROM esil_operands WHERE func_addr=1\""
|
||||
$query~{results[0].rows}
|
||||
"$query=\"sqlj SELECT * FROM esil_operands WHERE func_addr=128\""
|
||||
$query~{results[0].rows}
|
||||
EOF
|
||||
EXPECT=<<EOF
|
||||
1
|
||||
[]
|
||||
[]
|
||||
EOF
|
||||
EXPECT_ERR=
|
||||
RUN
|
||||
|
||||
NAME=function_frames: exact function start, interior and absent address
|
||||
FILE=malloc://1024
|
||||
ARGS=-e log.level=0 -e bin.relocs.apply=true -i build/load.r2 -a x86 -b 64
|
||||
CMDS=<<EOF
|
||||
wx 31c0ffc083f8037cf9c3
|
||||
af @ 0
|
||||
"sql,:quiet SELECT count(*) >= 1 FROM function_frames WHERE func_addr=0"
|
||||
"$query=\"sqlj SELECT * FROM function_frames WHERE func_addr=1\""
|
||||
$query~{results[0].rows}
|
||||
"$query=\"sqlj SELECT * FROM function_frames WHERE func_addr=128\""
|
||||
$query~{results[0].rows}
|
||||
EOF
|
||||
EXPECT=<<EOF
|
||||
1
|
||||
[]
|
||||
[]
|
||||
EOF
|
||||
EXPECT_ERR=
|
||||
RUN
|
||||
|
||||
NAME=Native calling convention data
|
||||
FILE=malloc://1024
|
||||
ARGS=-e log.level=0 -e bin.relocs.apply=true -i build/load.r2 -a x86 -b 64
|
||||
CMDS=<<EOF
|
||||
wx c3
|
||||
af @ 0
|
||||
"sql,:quiet SELECT calltype FROM funcs WHERE addr=0"
|
||||
EOF
|
||||
EXPECT=<<EOF
|
||||
amd64
|
||||
EOF
|
||||
EXPECT_ERR=
|
||||
RUN
|
||||
|
||||
NAME=pseudocode: exact function start, interior and absent address
|
||||
FILE=malloc://1024
|
||||
ARGS=-e log.level=0 -e bin.relocs.apply=true -i build/load.r2 -a x86 -b 64
|
||||
CMDS=<<EOF
|
||||
wx 31c0ffc083f8037cf9c3
|
||||
af @ 0
|
||||
"sql,:quiet SELECT count(*) >= 1 FROM pseudocode WHERE func_addr=0"
|
||||
"$query=\"sqlj SELECT * FROM pseudocode WHERE func_addr=1\""
|
||||
$query~{results[0].rows}
|
||||
"$query=\"sqlj SELECT * FROM pseudocode WHERE func_addr=128\""
|
||||
$query~{results[0].rows}
|
||||
EOF
|
||||
EXPECT=<<EOF
|
||||
1
|
||||
[]
|
||||
[]
|
||||
EOF
|
||||
EXPECT_ERR=
|
||||
RUN
|
||||
@@ -0,0 +1,147 @@
|
||||
# Command aliases preserve quoted SQL when applying native JSON filters.
|
||||
NAME=Analyzed fixture tables and views
|
||||
FILE=build/fixture.bin
|
||||
ARGS=-e log.level=0 -e bin.relocs.apply=true -i build/load.r2
|
||||
CMDS=<<EOF
|
||||
aa
|
||||
"sql,:quiet SELECT count(*) > 0 FROM funcs"
|
||||
"sql,:quiet SELECT count(*) > 0 FROM funcs WHERE name LIKE '%main%'"
|
||||
"sql,:quiet SELECT count(*) > 0 FROM strings WHERE content LIKE '%r2xsql_test_marker_string%'"
|
||||
"sql,:quiet SELECT count(*) > 0 FROM xrefs"
|
||||
"sql,:quiet SELECT count(*) > 0 FROM sections WHERE name LIKE '%text%'"
|
||||
"sql,:quiet SELECT count(*) > 0 FROM (SELECT prototype FROM funcs WHERE name LIKE '%main%')"
|
||||
"sql,:quiet SELECT count(*) > 0 FROM funcs WHERE addr <= (SELECT addr FROM funcs WHERE name LIKE '%main%' LIMIT 1) AND end_addr > (SELECT addr FROM funcs WHERE name LIKE '%main%' LIMIT 1)"
|
||||
"sql,:quiet SELECT count(*) > 0 AND min(length(disasm)>0) FROM instructions WHERE func_addr=(SELECT addr FROM funcs WHERE name LIKE '%main%' LIMIT 1)"
|
||||
"sql,:quiet SELECT count(*) > 0 FROM names"
|
||||
"sql,:quiet SELECT count(*) = 2 FROM sqlite_master WHERE name IN ('funcs','callers')"
|
||||
"sql,:quiet SELECT count(*) > 0 FROM grep WHERE pattern='main'"
|
||||
EOF
|
||||
EXPECT=<<EOF
|
||||
1
|
||||
1
|
||||
1
|
||||
1
|
||||
1
|
||||
1
|
||||
1
|
||||
1
|
||||
1
|
||||
1
|
||||
1
|
||||
EOF
|
||||
EXPECT_ERR=
|
||||
RUN
|
||||
|
||||
NAME=Native types, removed wrappers and REGEXP
|
||||
FILE=malloc://1024
|
||||
ARGS=-e log.level=0 -e bin.relocs.apply=true -i build/load.r2
|
||||
CMDS=<<EOF
|
||||
"td struct R2xsqlPt { int a; int b; };"
|
||||
"sql,:quiet SELECT name,kind FROM types WHERE name='R2xsqlPt'"
|
||||
"sql,:quiet SELECT member_name FROM types_members WHERE type_name='R2xsqlPt' ORDER BY member_name"
|
||||
"sql,:quiet SELECT count(*) FROM pragma_function_list WHERE name IN ('r2xsql_project_save','r2xsql_project_open','r2xsql_type_define')"
|
||||
"sql,:quiet SELECT 'abc' REGEXP '^a'"
|
||||
"$query=\"sqlj INSERT INTO types(name,kind) VALUES('EmptyNative','struct'); DELETE FROM types WHERE name='EmptyNative'; SELECT name FROM types WHERE name='EmptyNative'\""
|
||||
$query~{results[2].rows}
|
||||
EOF
|
||||
EXPECT=<<EOF
|
||||
R2xsqlPt struct
|
||||
a
|
||||
b
|
||||
0
|
||||
1
|
||||
[]
|
||||
EOF
|
||||
EXPECT_ERR=
|
||||
RUN
|
||||
|
||||
NAME=Comments CRUD and command injection
|
||||
FILE=malloc://1024
|
||||
ARGS=-e log.level=0 -e bin.relocs.apply=true -i build/load.r2
|
||||
CMDS=<<EOF
|
||||
"$query=\"sqlj INSERT INTO comments(addr,text) VALUES(0,'x; f INJ_FLAG @ 0x1000'); SELECT text FROM comments WHERE addr=0\""
|
||||
$query~{results[1].rows}
|
||||
"sql,:quiet SELECT count(*) FROM flags WHERE name='INJ_FLAG'"
|
||||
"$query=\"sqlj UPDATE comments SET text='updated' WHERE addr=0; SELECT text FROM comments WHERE addr=0\""
|
||||
$query~{results[1].rows}
|
||||
"$query=\"sqlj DELETE FROM comments WHERE addr=0; SELECT text FROM comments WHERE addr=0\""
|
||||
$query~{results[1].rows}
|
||||
EOF
|
||||
EXPECT=<<EOF
|
||||
[["x; f INJ_FLAG @ 0x1000"]]
|
||||
0
|
||||
[["updated"]]
|
||||
[]
|
||||
EOF
|
||||
EXPECT_ERR=
|
||||
RUN
|
||||
|
||||
NAME=Flag self rename, rename and collision preserve state
|
||||
FILE=malloc://1024
|
||||
ARGS=-e log.level=0 -e bin.relocs.apply=true -i build/load.r2
|
||||
CMDS=<<EOF
|
||||
"$query=\"sqlj INSERT INTO flags(addr,name) VALUES(0,'flag_a'); INSERT INTO flags(addr,name) VALUES(4,'flag_b'); UPDATE flags SET name=name WHERE name='flag_a'; UPDATE flags SET name='flag_new' WHERE name='flag_a'; SELECT name FROM flags WHERE name IN ('flag_a','flag_new')\""
|
||||
$query~{results[4].rows}
|
||||
"$query=\"sqlj UPDATE flags SET name='flag_b' WHERE name='flag_new'\""
|
||||
$query~{success}
|
||||
"sql,:quiet SELECT name,addr FROM flags WHERE name IN ('flag_a','flag_new','flag_b') ORDER BY addr"
|
||||
EOF
|
||||
EXPECT=<<EOF
|
||||
[["flag_new"]]
|
||||
false
|
||||
flag_new 0
|
||||
flag_b 4
|
||||
EOF
|
||||
EXPECT_ERR=
|
||||
RUN
|
||||
|
||||
NAME=Live libraries, relocations and registers after target change
|
||||
FILE=build/fixture.bin
|
||||
ARGS=-e log.level=0 -e bin.relocs.apply=true -i build/load.r2 -a x86 -b 64
|
||||
CMDS=<<EOF
|
||||
"sql,:quiet SELECT count(*) > 0 FROM libs"
|
||||
"sql,:quiet SELECT count(*) > 0 FROM relocs"
|
||||
"sql,:quiet SELECT DISTINCT bits FROM registers"
|
||||
o-- malloc://1024
|
||||
e asm.bits=32
|
||||
"sql,:quiet SELECT count(*) FROM libs"
|
||||
"sql,:quiet SELECT count(*) FROM relocs"
|
||||
"sql,:quiet SELECT DISTINCT bits FROM registers"
|
||||
EOF
|
||||
EXPECT=<<EOF
|
||||
1
|
||||
1
|
||||
64
|
||||
0
|
||||
0
|
||||
32
|
||||
EOF
|
||||
EXPECT_ERR=
|
||||
RUN
|
||||
|
||||
NAME=Catalogs include and remove user tables and views
|
||||
FILE=malloc://1024
|
||||
ARGS=-e log.level=0 -e bin.relocs.apply=true -i build/load.r2
|
||||
CMDS=<<EOF
|
||||
"$query=\"sqlj CREATE TABLE catalog_table(n); CREATE VIEW catalog_view AS SELECT 1 AS n\""
|
||||
$query~{success}
|
||||
e cfg.table.format=tsv
|
||||
sql.tables~catalog_table
|
||||
sql.tables?~catalog_table
|
||||
sql.views~catalog_view
|
||||
sql.views?~catalog_view
|
||||
"$query=\"sqlj DROP VIEW catalog_view; DROP TABLE catalog_table\""
|
||||
$query~{success}
|
||||
sql.tables~catalog_table
|
||||
sql.views~catalog_view
|
||||
EOF
|
||||
EXPECT=<<EOF
|
||||
true
|
||||
catalog_table
|
||||
catalog_table
|
||||
catalog_view
|
||||
catalog_view
|
||||
true
|
||||
EOF
|
||||
EXPECT_ERR=
|
||||
RUN
|
||||
@@ -0,0 +1,62 @@
|
||||
# Command aliases preserve quoted SQL when applying native JSON filters.
|
||||
NAME=JSON NULLs, columns, numeric strings and statement count
|
||||
FILE=malloc://1024
|
||||
ARGS=-e log.level=0 -e bin.relocs.apply=true -i build/load.r2
|
||||
CMDS=<<EOF
|
||||
"$query=\"sqlj SELECT NULL AS missing, '' AS empty, 'NULL' AS literal; SELECT 7 > 3 AS n\""
|
||||
$query~{success}
|
||||
"$query=\"sqlj SELECT NULL AS missing, '' AS empty, 'NULL' AS literal\""
|
||||
$query~{results[0].columns}
|
||||
"$query=\"sqlj SELECT NULL AS missing, '' AS empty, 'NULL' AS literal\""
|
||||
$query~{results[0].rows}
|
||||
"$query=\"sqlj SELECT 7 > 3 AS n\""
|
||||
$query~{results[0].rows}
|
||||
"$query=\"sqlj SELECT 1; SELECT 2\""
|
||||
$query~{statement_count}
|
||||
EOF
|
||||
EXPECT=<<EOF
|
||||
true
|
||||
["missing","empty","literal"]
|
||||
[[null,"","NULL"]]
|
||||
[["1"]]
|
||||
2
|
||||
EOF
|
||||
EXPECT_ERR=
|
||||
RUN
|
||||
|
||||
NAME=JSON writes and literal semicolon
|
||||
FILE=malloc://1024
|
||||
ARGS=-e log.level=0 -e bin.relocs.apply=true -i build/load.r2
|
||||
CMDS=<<EOF
|
||||
"$query=\"sqlj INSERT INTO comments(addr,text) VALUES(0,'a;b|c'); SELECT text FROM comments WHERE addr=0\""
|
||||
$query~{results[1].rows}
|
||||
EOF
|
||||
EXPECT=<<EOF
|
||||
[["a;b|c"]]
|
||||
EOF
|
||||
EXPECT_ERR=
|
||||
RUN
|
||||
|
||||
NAME=JSON stops at first error and rejects malformed SQL
|
||||
FILE=malloc://1024
|
||||
ARGS=-e log.level=0 -e bin.relocs.apply=true -i build/load.r2
|
||||
CMDS=<<EOF
|
||||
"$query=\"sqlj SELECT 1; SELECT * FROM missing_table; INSERT INTO comments(addr,text) VALUES(0,'must not execute')\""
|
||||
$query~{success}
|
||||
"$query=\"sqlj SELECT 1; SELECT * FROM missing_table; INSERT INTO comments(addr,text) VALUES(0,'must not execute')\""
|
||||
$query~{results[1].error}
|
||||
"$query=\"sqlj SELECT 1; SELECT * FROM missing_table; INSERT INTO comments(addr,text) VALUES(0,'must not execute')\""
|
||||
$query~{statement_count}
|
||||
"sql,:quiet SELECT count(*) FROM comments WHERE addr=0"
|
||||
"$query=\"sqlj SELECT 'unterminated\""
|
||||
$query~{success}
|
||||
EOF
|
||||
EXPECT=<<EOF
|
||||
false
|
||||
no such table: missing_table
|
||||
3
|
||||
0
|
||||
false
|
||||
EOF
|
||||
EXPECT_ERR=
|
||||
RUN
|
||||
@@ -0,0 +1,44 @@
|
||||
NAME=Native tables, CSV and table queries
|
||||
FILE=malloc://1024
|
||||
ARGS=-e log.level=0 -e bin.relocs.apply=true -i build/load.r2
|
||||
CMDS=<<EOF
|
||||
"sql SELECT 1 AS n, 'abc' AS name"
|
||||
"sql,:csv SELECT 1 AS n, 'a,b' AS name"
|
||||
"sql,n/gt/1,n/sort/dec,:quiet SELECT 1 AS n UNION SELECT 2 UNION SELECT 3"
|
||||
e cfg.table.format=csv
|
||||
"sql SELECT 7 AS n"
|
||||
e cfg.table.format=
|
||||
"sql,:csv SELECT 1 AS n WHERE 0"
|
||||
EOF
|
||||
EXPECT=<<EOF
|
||||
n name
|
||||
------
|
||||
1 abc
|
||||
n,name
|
||||
1,"a,b"
|
||||
3
|
||||
2
|
||||
7
|
||||
n
|
||||
EOF
|
||||
EXPECT_ERR=
|
||||
RUN
|
||||
|
||||
NAME=Help and invalid commands
|
||||
FILE=malloc://1024
|
||||
ARGS=-e log.level=0 -e bin.relocs.apply=true -i build/load.r2
|
||||
CMDS=<<EOF
|
||||
sql,?~:csv[0]
|
||||
sql,:help~:csv[0]
|
||||
"sql,:csv SELECT * FROM missing_table"
|
||||
sql.SELECT 1
|
||||
EOF
|
||||
EXPECT=<<EOF
|
||||
:csv
|
||||
:csv
|
||||
ERROR: no such table: missing_table
|
||||
Unknown sql subcommand: sql.SELECT 1
|
||||
See `sql.?` for available subcommands.
|
||||
EOF
|
||||
EXPECT_ERR=
|
||||
RUN
|
||||
@@ -0,0 +1,11 @@
|
||||
NAME=Native HTTP GET and POST transport
|
||||
FILE=malloc://1024
|
||||
ARGS=-e log.level=0 -e bin.relocs.apply=true -i build/load.r2
|
||||
CMDS=<<EOF
|
||||
!python3 http_transport.py
|
||||
EOF
|
||||
EXPECT=<<EOF
|
||||
HTTP GET and POST: OK
|
||||
EOF
|
||||
EXPECT_ERR=
|
||||
RUN
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
* named functions (one calling another, so xrefs/callers/callees are
|
||||
* non-empty) plus a unique string marker, so the tests in this directory
|
||||
* have stable, predictable data to assert against. Portable C (no libc
|
||||
* beyond stdio/string), built at test time by test_support.py.
|
||||
* beyond stdio/string), built at test time by the tests Makefile.
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
"""Exercise the plugin through radare2's own HTTP command endpoint."""
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
import socket
|
||||
import subprocess
|
||||
import time
|
||||
from urllib.parse import quote
|
||||
from urllib.request import build_opener, ProxyHandler
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.parametrize("post", [False, True], ids=["GET", "POST"])
|
||||
def test_r2_http(r2_bin, r2xsql_plugin, fixture_binary, tmp_path, post):
|
||||
with socket.socket() as sock:
|
||||
sock.bind(("127.0.0.1", 0))
|
||||
@@ -29,9 +30,9 @@ def test_r2_http(r2_bin, r2xsql_plugin, fixture_binary, tmp_path, post):
|
||||
log_path = tmp_path / "r2-http.log"
|
||||
with log_path.open("w") as log:
|
||||
proc = subprocess.Popen([
|
||||
r2_bin, "-N", "-q", "-e", "http.bind=127.0.0.1", "-e", f"http.port={port}",
|
||||
r2_bin, "-NN", "-q", "-e", "http.bind=127.0.0.1", "-e", f"http.port={port}",
|
||||
"-e", "http.log=false", "-e", "scr.color=0",
|
||||
"-i", str(r2xsql_plugin), "-c", "aa", "-c", "=h", str(fixture_binary),
|
||||
"-c", f"L {r2xsql_plugin}", "-c", "aa", "-c", "=h", str(fixture_binary),
|
||||
], stdout=log, stderr=log, cwd=tmp_path)
|
||||
try:
|
||||
deadline = time.monotonic() + 15
|
||||
@@ -84,3 +85,11 @@ def test_r2_http(r2_bin, r2xsql_plugin, fixture_binary, tmp_path, post):
|
||||
except subprocess.TimeoutExpired:
|
||||
proc.kill()
|
||||
proc.wait()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
for post in (False, True):
|
||||
with TemporaryDirectory() as directory:
|
||||
test_r2_http(os.environ["R2R_RADARE2"], os.environ["R2XSQL_PLUGIN"],
|
||||
Path("build/fixture.bin").resolve(), Path(directory), post)
|
||||
print("HTTP GET and POST: OK")
|
||||
@@ -1,31 +0,0 @@
|
||||
"""Catalog commands follow the live schema, including user-created objects."""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
|
||||
|
||||
def test_catalogs(r2_bin, r2xsql_plugin):
|
||||
commands = ['"sql CREATE TABLE catalog_table(n);"',
|
||||
'"sql CREATE VIEW catalog_view AS SELECT 1 AS n;"',
|
||||
'e cfg.table.format=tsv']
|
||||
for kind, command in [("table", "sql.tables"), ("view", "sql.views")]:
|
||||
commands += [command, "?e SPLIT", command + "?", "?e SPLIT",
|
||||
f'"sqlj SELECT name FROM sqlite_master WHERE type=\'{kind}\' ORDER BY name"',
|
||||
"?e SPLIT"]
|
||||
commands += ['"sql DROP VIEW catalog_view"', '"sql DROP TABLE catalog_table"',
|
||||
'sql.tables', 'sql.views']
|
||||
args = [r2_bin, "-N", "-q", "-e", "scr.color=0", "-i", str(r2xsql_plugin)]
|
||||
for command in commands:
|
||||
args += ["-c", command]
|
||||
result = subprocess.run(args + ["malloc://1024"], capture_output=True, text=True, timeout=15)
|
||||
assert result.returncode == 0, result.stderr
|
||||
parts = result.stdout.split("SPLIT\n")
|
||||
assert len(parts) == 7, result.stdout
|
||||
for offset in (0, 3):
|
||||
envelope = json.loads(parts[offset + 2])
|
||||
assert envelope["success"], envelope
|
||||
names = [row[0] for row in envelope["results"][0]["rows"]]
|
||||
assert parts[offset].strip().splitlines() == names
|
||||
assert parts[offset + 1].strip() == parts[offset].strip()
|
||||
assert "catalog_table" in parts[0] and "catalog_view" in parts[3]
|
||||
assert "catalog_table" not in parts[6] and "catalog_view" not in parts[6]
|
||||
@@ -1,17 +0,0 @@
|
||||
"""A flag self-rename succeeds and a refused rename remains an SQL error."""
|
||||
|
||||
|
||||
def test_flag_renames(runner):
|
||||
setup = ["INSERT INTO flags(addr,name) VALUES(0,'r2xsql_flag_a')",
|
||||
"INSERT INTO flags(addr,name) VALUES(4,'r2xsql_flag_b')"]
|
||||
result = runner.script(setup + [
|
||||
"UPDATE flags SET name=name WHERE name='r2xsql_flag_a'",
|
||||
"UPDATE flags SET name='r2xsql_flag_new' WHERE name='r2xsql_flag_a'",
|
||||
"SELECT name FROM flags WHERE name IN ('r2xsql_flag_a','r2xsql_flag_new')"])
|
||||
assert result.ok, result.stderr
|
||||
assert result.column("name") == ["r2xsql_flag_new"]
|
||||
result = runner.script(setup + [
|
||||
"UPDATE flags SET name='r2xsql_flag_b' WHERE name='r2xsql_flag_a'",
|
||||
"SELECT name FROM flags WHERE name='r2xsql_flag_a'"])
|
||||
assert not result.ok
|
||||
assert "\nname\nr2xsql_flag_a\n" in result.stdout
|
||||
@@ -1,57 +0,0 @@
|
||||
"""A func_addr equality must not relabel a containing function's rows."""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
|
||||
|
||||
def test_function_addresses(r2_bin, r2xsql_plugin):
|
||||
tables = ["blocks", "cfg_edges", "switch_tables", "dominators", "post_dominators",
|
||||
"loops", "instructions", "instruction_operands", "esil_ops", "esil_operands",
|
||||
"function_frames", "pseudocode"]
|
||||
# x86-64: xor eax,eax; inc eax; cmp eax,3; jl loop; ret
|
||||
args = [r2_bin, "-N", "-q", "-a", "x86", "-b", "64", "-i", str(r2xsql_plugin),
|
||||
"-c", "wx 31c0ffc083f8037cf9c3", "-c", "af @ 0"]
|
||||
for table in tables:
|
||||
for address in (0, 1, 128):
|
||||
# SELECT * forces rendered instruction columns on the libr backend.
|
||||
args += ["-c", f'"sqlj SELECT * FROM {table} WHERE func_addr={address}"']
|
||||
proc = subprocess.run(args + ["malloc://1024"], capture_output=True, text=True, timeout=30)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
results = [json.loads(line) for line in proc.stdout.splitlines() if line.startswith("{")]
|
||||
assert len(results) == len(tables) * 3, proc.stdout
|
||||
for i, table in enumerate(tables):
|
||||
start, interior, missing = results[i * 3:i * 3 + 3]
|
||||
if table == "pseudocode" and not start["success"]:
|
||||
assert "no such table: pseudocode" in start["results"][0]["error"]
|
||||
continue
|
||||
for result in (start, interior, missing):
|
||||
assert result["success"], (table, result)
|
||||
assert interior["results"][0]["rows"] == [], table
|
||||
assert missing["results"][0]["rows"] == [], table
|
||||
if table != "switch_tables":
|
||||
assert start["results"][0]["rows"], table
|
||||
|
||||
|
||||
def test_function_start_vs_inside(r2_bin, r2xsql_plugin):
|
||||
"""Exact function starts match; interior and missing addresses do not."""
|
||||
args = [
|
||||
r2_bin, "-N", "-q", "-a", "x86", "-b", "64", "-i", str(r2xsql_plugin),
|
||||
"-c", "wx 31c0ffc083f8037cf9c3",
|
||||
"-c", "af @ 0",
|
||||
]
|
||||
queries = [
|
||||
("start", 0),
|
||||
("inside", 1),
|
||||
("missing", 128),
|
||||
]
|
||||
for _, address in queries:
|
||||
args += ["-c", f'"sqlj SELECT addr,size FROM blocks WHERE func_addr={address}"']
|
||||
proc = subprocess.run(args + ["malloc://1024"], capture_output=True, text=True, timeout=30)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
results = [json.loads(line) for line in proc.stdout.splitlines() if line.startswith("{")]
|
||||
assert len(results) == 3, proc.stdout
|
||||
|
||||
start, inside, missing = [r["results"][0]["rows"] for r in results]
|
||||
assert start, "function start must resolve"
|
||||
assert inside == [], "address inside a function must not match func_addr equality"
|
||||
assert missing == [], "address outside every function must not match"
|
||||
@@ -1,22 +0,0 @@
|
||||
"""Repeated queries observe a changed target and register configuration."""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
|
||||
|
||||
def test_live_cache(r2_bin, r2xsql_plugin, fixture_binary):
|
||||
query = '"sqlj SELECT name FROM libs; SELECT addr FROM relocs; SELECT DISTINCT bits FROM registers"'
|
||||
commands = [query, "o-- malloc://1024", "e asm.bits=32", query]
|
||||
args = [r2_bin, "-N", "-q", "-b", "64", "-i", str(r2xsql_plugin)]
|
||||
for command in commands:
|
||||
args += ["-c", command]
|
||||
proc = subprocess.run(args + [str(fixture_binary)], capture_output=True, text=True, timeout=20)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
results = [json.loads(line) for line in proc.stdout.splitlines() if line.startswith("{")]
|
||||
assert len(results) == 2, proc.stdout
|
||||
before, after = results
|
||||
assert before["success"] and after["success"], results
|
||||
assert before["results"][0]["rows"], "fixture must import a library"
|
||||
assert before["results"][1]["rows"], "fixture must have relocations"
|
||||
assert before["results"][2]["rows"] == [["64"]]
|
||||
assert [r["rows"] for r in after["results"]] == [[], [], [["32"]]]
|
||||
@@ -1,245 +0,0 @@
|
||||
# Copyright (c) 2024-2026 Elias Bachaalany
|
||||
# SPDX-License-Identifier: LicenseRef-Human-Origin-Source-1.0
|
||||
"""Public smoke coverage of the SQL surface.
|
||||
|
||||
A small, standalone sanity suite a consumer can run against a built
|
||||
`core_r2xsql` plugin with nothing but a C compiler and radare2 on
|
||||
PATH: it compiles a deterministic fixture, opens it, and checks that the core
|
||||
tables, views, the type system, and the writable comment surface behave. The
|
||||
exhaustive coverage lives in the project's private suite; this file is the
|
||||
portable, dependency-light layer.
|
||||
|
||||
Every assertion targets a surface that genuinely exists and is checked against
|
||||
the actual table/column names.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from test_support import R2xsqlRunner
|
||||
|
||||
|
||||
def _addr_of(runner: R2xsqlRunner, name_like: str) -> str:
|
||||
r = runner.query(f"SELECT addr FROM funcs WHERE name LIKE '%{name_like}%' LIMIT 1")
|
||||
assert r.ok, r.stderr
|
||||
assert r.rows, f"no function matching {name_like!r} found"
|
||||
return r.rows[0][0]
|
||||
|
||||
|
||||
# --- core tables ------------------------------------------------------------
|
||||
|
||||
|
||||
def test_funcs_is_nonempty(runner: R2xsqlRunner) -> None:
|
||||
r = runner.query("SELECT count(*) AS n FROM funcs")
|
||||
assert r.ok, r.stderr
|
||||
assert int(r.rows[0][0]) > 0
|
||||
|
||||
|
||||
def test_funcs_has_named_function(runner: R2xsqlRunner) -> None:
|
||||
# "main" rather than one of the fixture's own helper functions
|
||||
# (r2xsql_add/r2xsql_mul/r2xsql_compute): those get real names only from a
|
||||
# full local symtab, which ELF (`gcc -g`) embeds directly in the binary but
|
||||
# MSVC/PE keeps in a separate .pdb this fixture is built without (for
|
||||
# portability). "main" is reliably named on every platform: radare2
|
||||
# recognizes the standard CRT startup call into it even with no debug info.
|
||||
r = runner.query("SELECT name FROM funcs WHERE name LIKE '%main%'")
|
||||
assert r.ok, r.stderr
|
||||
assert any("main" in row[0] for row in r.rows)
|
||||
|
||||
|
||||
def test_strings_has_marker(runner: R2xsqlRunner) -> None:
|
||||
# The `strings` column is `content` (tables_strings.cpp).
|
||||
r = runner.query(
|
||||
"SELECT content FROM strings WHERE content LIKE '%r2xsql_test_marker_string%'"
|
||||
)
|
||||
assert r.ok, r.stderr
|
||||
assert r.rows, "marker string not found in strings table"
|
||||
|
||||
|
||||
def test_xrefs_is_nonempty(runner: R2xsqlRunner) -> None:
|
||||
# r2xsql_compute calls r2xsql_add and r2xsql_mul, so real CALL xrefs exist.
|
||||
r = runner.query("SELECT count(*) AS n FROM xrefs")
|
||||
assert r.ok, r.stderr
|
||||
assert int(r.rows[0][0]) > 0
|
||||
|
||||
|
||||
def test_sections_has_text_section(runner: R2xsqlRunner) -> None:
|
||||
r = runner.query("SELECT name FROM sections WHERE name LIKE '%text%'")
|
||||
assert r.ok, r.stderr
|
||||
assert r.rows, "no section with 'text' in its name (expected .text/__text)"
|
||||
|
||||
|
||||
# --- function signature / calling convention --------------------------------
|
||||
# The signature surface is `funcs.prototype` (read) and `funcs.calltype`
|
||||
# (read-only), both plain columns rather than scalar functions.
|
||||
|
||||
|
||||
def test_funcs_prototype_column_exists(runner: R2xsqlRunner) -> None:
|
||||
r = runner.query("SELECT prototype FROM funcs WHERE name LIKE '%main%'")
|
||||
assert r.ok, r.stderr
|
||||
assert r.columns == ["prototype"]
|
||||
assert r.rows, "no row for 'main'"
|
||||
|
||||
|
||||
def test_funcs_calltype_reads_correctly(runner: R2xsqlRunner) -> None:
|
||||
# `calltype` is read-only (tables_funcs.cpp declares it `.column_text`, not
|
||||
# `.column_text_rw`; the schema catalog's `funcs` write-bullet list never
|
||||
# included it) -- caught when a genuinely fresh build (not a stale one --
|
||||
# see the MSBuild-staleness note above) rejected the UPDATE with
|
||||
# "column \"calltype\" is read-only".
|
||||
#
|
||||
# What IS worth pinning: the READ must resolve to the correct native calling
|
||||
# convention for the target's ABI, NOT the "reg" fallback a degraded
|
||||
# calling-convention SDB reports -- the exact failure the calltype data-dir
|
||||
# dependency can cause (a real, confirmed radare2 data-dir dependency; see
|
||||
# deployment.md).
|
||||
#
|
||||
# The convention is ABI-specific, so the expectation is platform-aware: a PE
|
||||
# (Windows/MSVC) fixture reports "ms" (Microsoft x64); an ELF (Linux/macOS,
|
||||
# gcc/clang) fixture reports "amd64" (System V). The invariant on BOTH: it
|
||||
# must be the native convention and must NOT be "reg" (the cliff fallback).
|
||||
r = runner.query("SELECT lower(value) FROM binary WHERE key = 'bintype'")
|
||||
is_pe = bool(r.ok and r.rows and "pe" in r.rows[0][0])
|
||||
expected = "ms" if is_pe else "amd64"
|
||||
|
||||
addr = _addr_of(runner, "main")
|
||||
r = runner.query(f"SELECT calltype FROM funcs WHERE addr = {addr}")
|
||||
assert r.ok, r.stderr
|
||||
got = r.rows[0][0] if r.rows else None
|
||||
assert got != "reg", (
|
||||
f"calltype is 'reg' -- the calling-convention SDB degraded to its "
|
||||
f"data-dir-cliff fallback (expected the native {expected!r})"
|
||||
)
|
||||
assert got == expected, (
|
||||
f"expected the native calling convention {expected!r} for a "
|
||||
f"{'PE' if is_pe else 'ELF'} target, got {got!r}"
|
||||
)
|
||||
|
||||
|
||||
# --- function lookup by address / disassembly -------------------------------
|
||||
# Both are ordinary table reads (a range query on `funcs`, a scan of
|
||||
# `instructions`), not scalar functions.
|
||||
|
||||
|
||||
def test_func_lookup_by_address(runner: R2xsqlRunner) -> None:
|
||||
addr = _addr_of(runner, "main")
|
||||
r = runner.query(
|
||||
f"SELECT name FROM funcs WHERE addr <= {addr} AND end_addr > {addr}"
|
||||
)
|
||||
assert r.ok, r.stderr
|
||||
assert any("main" in row[0] for row in r.rows)
|
||||
|
||||
|
||||
def test_instructions_disasm_nonempty(runner: R2xsqlRunner) -> None:
|
||||
addr = _addr_of(runner, "main")
|
||||
r = runner.query(f"SELECT disasm FROM instructions WHERE func_addr = {addr}")
|
||||
assert r.ok, r.stderr
|
||||
assert r.rows, "no instructions decoded for main"
|
||||
assert all(row[0] for row in r.rows), "an instruction had an empty disasm"
|
||||
|
||||
|
||||
# --- views -------------------------------------------------------------------
|
||||
# Schema introspection uses `sqlite_master` -- the portable SQL way to list
|
||||
# tables and views -- rather than any tool-specific introspection command.
|
||||
|
||||
|
||||
def test_names_view_works(runner: R2xsqlRunner) -> None:
|
||||
r = runner.query("SELECT name FROM names LIMIT 1")
|
||||
assert r.ok, r.stderr
|
||||
assert r.rows
|
||||
|
||||
|
||||
def test_schema_lists_funcs_and_callers(runner: R2xsqlRunner) -> None:
|
||||
r = runner.query(
|
||||
"SELECT name FROM sqlite_master WHERE type IN ('table','view') "
|
||||
"AND name IN ('funcs','callers')"
|
||||
)
|
||||
assert r.ok, r.stderr
|
||||
names = {row[0] for row in r.rows}
|
||||
assert names == {"funcs", "callers"}
|
||||
|
||||
|
||||
# --- type system ---------------------------------------------------------
|
||||
# A type is defined via the native td command; its members live in
|
||||
# the `types_members` table (plural "types") -- confirmed against
|
||||
# tables_types_members.cpp.
|
||||
|
||||
|
||||
def test_type_define_and_read_back(runner: R2xsqlRunner) -> None:
|
||||
r = runner.script(
|
||||
[
|
||||
"SELECT name, kind FROM types WHERE name = 'R2xsqlPt'",
|
||||
],
|
||||
write=True,
|
||||
setup_commands=['"td struct R2xsqlPt { int x; char name[8]; };"'],
|
||||
)
|
||||
assert r.ok, r.stderr
|
||||
assert r.rows and r.rows[0][0] == "R2xsqlPt" and r.rows[0][1] == "struct"
|
||||
|
||||
|
||||
def test_type_members_populated(runner: R2xsqlRunner) -> None:
|
||||
r = runner.script(
|
||||
[
|
||||
"SELECT member_name FROM types_members WHERE type_name = 'R2xsqlPt2'",
|
||||
],
|
||||
write=True,
|
||||
setup_commands=['"td struct R2xsqlPt2 { int a; int b; };"'],
|
||||
)
|
||||
assert r.ok, r.stderr
|
||||
members = {row[0] for row in r.rows}
|
||||
assert members == {"a", "b"}
|
||||
|
||||
|
||||
# --- entity grep -------------------------------------------------------------
|
||||
# Named-entity search is the `grep` table's `pattern` pushdown column, not a
|
||||
# scalar function.
|
||||
|
||||
|
||||
def test_grep_finds_function(runner: R2xsqlRunner) -> None:
|
||||
r = runner.query("SELECT name FROM grep WHERE pattern = 'main'")
|
||||
assert r.ok, r.stderr
|
||||
assert any("main" in row[0] for row in r.rows)
|
||||
|
||||
|
||||
# --- live mutation + injection safety ---------------------------------------
|
||||
# The writable `comments` table passes its text as `base64:` specifically so a
|
||||
# value can never be interpreted as an r2 command (schema-catalog.md). The
|
||||
# second test proves that guard holds.
|
||||
|
||||
|
||||
def test_comment_write_is_live(runner: R2xsqlRunner) -> None:
|
||||
addr = _addr_of(runner, "main")
|
||||
r = runner.script(
|
||||
[
|
||||
f"INSERT INTO comments (addr, text) VALUES ({addr}, 'tagged')",
|
||||
f"SELECT text FROM comments WHERE addr = {addr}",
|
||||
],
|
||||
write=True,
|
||||
)
|
||||
assert r.ok, r.stderr
|
||||
assert any(row[0] == "tagged" for row in r.rows)
|
||||
|
||||
|
||||
def test_comment_text_cannot_inject_a_command(runner: R2xsqlRunner) -> None:
|
||||
addr = _addr_of(runner, "main")
|
||||
payload = "x; f INJ_FLAG @ 0x1000"
|
||||
r = runner.script(
|
||||
[
|
||||
f"INSERT INTO comments (addr, text) VALUES ({addr}, '{payload}')",
|
||||
"SELECT count(*) AS n FROM flags WHERE name = 'INJ_FLAG'",
|
||||
],
|
||||
write=True,
|
||||
)
|
||||
assert r.ok, r.stderr
|
||||
assert r.rows and r.rows[-1][0] == "0", "comment text executed as an r2 command"
|
||||
|
||||
|
||||
def test_native_commands_replace_sql_wrappers(runner):
|
||||
result = runner.query("SELECT name FROM pragma_function_list WHERE name IN "
|
||||
"('r2xsql_project_save','r2xsql_project_open','r2xsql_type_define')")
|
||||
assert result.ok and not result.rows, result.stderr
|
||||
result = runner.query("SELECT 'abc' REGEXP '^a' AS matches")
|
||||
assert result.ok and result.rows == [["1"]], result.stderr
|
||||
result = runner.script(["INSERT INTO types(name,kind) VALUES('EmptyNative','struct')",
|
||||
"DELETE FROM types WHERE name='EmptyNative'",
|
||||
"SELECT name FROM types WHERE name='EmptyNative'"])
|
||||
assert result.ok and not result.rows, result.stderr
|
||||
@@ -1,31 +0,0 @@
|
||||
"""Machine-readable SQL scripts preserve NULLs, errors, and session state."""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
|
||||
|
||||
def test_sqlj(r2_bin, r2xsql_plugin):
|
||||
def run(sql):
|
||||
command = '"sqlj ' + sql.replace('"', '\\"') + '"'
|
||||
proc = subprocess.run([r2_bin, "-N", "-q", "-i", str(r2xsql_plugin),
|
||||
"-c", command, "malloc://1024"],
|
||||
capture_output=True, text=True, timeout=15)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
result = run("SELECT NULL AS missing, '' AS empty, 'NULL' AS literal; SELECT 7 > 3 AS n")
|
||||
assert result["success"] and result["statement_count"] == 2
|
||||
first, second = result["results"]
|
||||
assert first["columns"] == ["missing", "empty", "literal"]
|
||||
assert first["rows"] == [[None, "", "NULL"]]
|
||||
assert second["rows"] == [["1"]]
|
||||
assert result["elapsed_ms_total"] >= 0
|
||||
|
||||
result = run("INSERT INTO comments(addr,text) VALUES(0,'a;b|c'); SELECT text FROM comments WHERE addr=0")
|
||||
assert result["success"]
|
||||
assert result["results"][1]["rows"] == [["a;b|c"]]
|
||||
result = run("SELECT 1; SELECT * FROM missing_table; SELECT 2")
|
||||
assert not result["success"] and len(result["results"]) == 2
|
||||
assert "missing_table" in result["results"][1]["error"]
|
||||
result = run("SELECT 'unterminated")
|
||||
assert not result["success"] and result["results"][0]["error"]
|
||||
@@ -1,159 +0,0 @@
|
||||
# Copyright (c) 2024-2026 Elias Bachaalany
|
||||
# SPDX-License-Identifier: LicenseRef-Human-Origin-Source-1.0
|
||||
"""Locate the built plugin, compile a deterministic fixture, and run SQL
|
||||
through radare2. Build r2xsql first; the fixture only needs a C compiler.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
|
||||
TESTS_DIR = Path(__file__).resolve().parent
|
||||
REPO_ROOT = TESTS_DIR.parent # r2xsql/
|
||||
|
||||
|
||||
def find_r2xsql_plugin() -> Path | None:
|
||||
"""Find the plugin from R2XSQL_PLUGIN or a CMake, Make, or Meson build."""
|
||||
env_path = os.environ.get("R2XSQL_PLUGIN")
|
||||
if env_path:
|
||||
path = Path(env_path).resolve()
|
||||
return path if path.is_file() else None
|
||||
for name in ("build", "build-make", "build-meson", "out"):
|
||||
for path in sorted((REPO_ROOT / name).rglob("core_r2xsql.*")):
|
||||
if path.is_file() and path.suffix in (".so", ".dylib", ".dll"):
|
||||
return path.resolve()
|
||||
return None
|
||||
|
||||
|
||||
def _find_c_compiler() -> list[str] | None:
|
||||
# On Windows, prefer cl.exe (MSVC) first and unconditionally: Visual
|
||||
# Studio ships its own clang.exe on PATH inside a Developer/vcvars shell,
|
||||
# and shutil.which("clang") happily finds it -- but that's plain
|
||||
# clang.exe (GNU-style driver), not clang-cl, so it silently mis-parses
|
||||
# the MSVC-style /Fe:/nologo flags below rather than erroring. Checking
|
||||
# cl first avoids ever reaching that trap on the platform this project
|
||||
# actually builds on.
|
||||
if sys.platform == "win32" and shutil.which("cl"):
|
||||
return ["cl", "/nologo"]
|
||||
for cc in ("cc", "gcc", "clang"):
|
||||
if shutil.which(cc):
|
||||
return [cc]
|
||||
return None
|
||||
|
||||
|
||||
def build_fixture(out_dir: Path) -> Path:
|
||||
"""Compile tests/fixture.c into a native executable under out_dir.
|
||||
|
||||
Raises RuntimeError with an actionable message if no compiler is found
|
||||
(a build environment problem, not a test failure to hide).
|
||||
"""
|
||||
src = TESTS_DIR / "fixture.c"
|
||||
exe = out_dir / ("r2xsql_fixture.exe" if sys.platform == "win32" else "r2xsql_fixture")
|
||||
cc = _find_c_compiler()
|
||||
if cc is None:
|
||||
raise RuntimeError(
|
||||
"no C compiler found on PATH (tried cc/gcc/clang, and cl on Windows) "
|
||||
"-- install one or run pytest from a shell with a compiler on PATH"
|
||||
)
|
||||
if cc[0] == "cl":
|
||||
# No /Zi: a plain PE carries no local function names at all regardless
|
||||
# (unlike an ELF built with `gcc -g`, which embeds a full local
|
||||
# symtab directly in the binary) -- radare2 recovers "entry0"/"main"
|
||||
# from its own CRT-startup heuristic either way, which is what every
|
||||
# by-name assertion in this suite keys off. /Zi was tried and
|
||||
# dropped: it forces MSVC's default incremental linking, which
|
||||
# produces a PE radare2 mis-reads at the entry point (0xFF filler
|
||||
# instead of the real prologue) even though the binary runs fine --
|
||||
# not worth the /link /INCREMENTAL:NO workaround for data this suite
|
||||
# doesn't otherwise need.
|
||||
cmd = cc + ["/Fe:" + str(exe), str(src)]
|
||||
else:
|
||||
cmd = cc + ["-O0", "-g", "-o", str(exe), str(src)]
|
||||
proc = subprocess.run(cmd, cwd=out_dir, capture_output=True, text=True)
|
||||
if proc.returncode != 0 or not exe.is_file():
|
||||
raise RuntimeError(
|
||||
f"failed to build fixture with {cc[0]}: rc={proc.returncode}\n"
|
||||
f"stdout: {proc.stdout}\nstderr: {proc.stderr}"
|
||||
)
|
||||
return exe
|
||||
|
||||
|
||||
@dataclass
|
||||
class QueryResult:
|
||||
"""A parsed plugin response, including SQL errors printed on stdout."""
|
||||
|
||||
returncode: int
|
||||
stdout: str
|
||||
stderr: str
|
||||
columns: list[str] = field(default_factory=list)
|
||||
rows: list[list[str]] = field(default_factory=list)
|
||||
|
||||
@property
|
||||
def ok(self) -> bool:
|
||||
return self.returncode == 0
|
||||
|
||||
def column(self, name: str) -> list[str]:
|
||||
if name not in self.columns:
|
||||
return []
|
||||
idx = self.columns.index(name)
|
||||
return [r[idx] for r in self.rows]
|
||||
|
||||
def contains(self, needle: str) -> bool:
|
||||
"""Substring search over the raw stdout, for assertions that only need
|
||||
presence, not structure."""
|
||||
return needle in self.stdout
|
||||
|
||||
|
||||
class R2xsqlRunner:
|
||||
"""Run SQL through the plugin against a fixed fixture binary."""
|
||||
|
||||
def __init__(self, r2_path: str, plugin_path: Path, fixture_path: Path):
|
||||
self.r2_path = r2_path
|
||||
self.plugin_path = plugin_path
|
||||
self.fixture_path = fixture_path
|
||||
|
||||
def query(self, sql: str, *, write: bool = False) -> QueryResult:
|
||||
return self.script([sql], write=write)
|
||||
|
||||
def script(self, statements: list[str], *, write: bool = False,
|
||||
setup_commands: list[str] | None = None) -> QueryResult:
|
||||
"""Run statements in one session and return the last table's rows."""
|
||||
cmd = [self.r2_path, "-N", "-q", "-e", "scr.color=0",
|
||||
"-i", str(self.plugin_path), "-c", "aaa"]
|
||||
if write:
|
||||
cmd.append("-w")
|
||||
for command in setup_commands or []:
|
||||
cmd.extend(["-c", command])
|
||||
for sql in statements:
|
||||
cmd.extend(["-c", "?e R2XSQL_RESULT_BEGIN",
|
||||
"-c", '"sql,:tsv ' + sql.replace('"', '\\"') + '"'])
|
||||
cmd.extend(["-c", "?e R2XSQL_RESULT_END"])
|
||||
cmd.append(str(self.fixture_path))
|
||||
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
|
||||
errors = [line for line in proc.stdout.splitlines()
|
||||
if line.startswith(("ERROR:", "EXCEPTION:"))]
|
||||
completed = proc.stdout.rstrip().endswith("R2XSQL_RESULT_END")
|
||||
columns, rows = _parse_tab_block(proc.stdout)
|
||||
return QueryResult(
|
||||
returncode=proc.returncode or int(bool(errors) or not completed),
|
||||
stdout=proc.stdout,
|
||||
stderr=proc.stderr + "\n".join(errors),
|
||||
columns=columns,
|
||||
rows=rows,
|
||||
)
|
||||
|
||||
|
||||
def _parse_tab_block(text: str) -> tuple[list[str], list[list[str]]]:
|
||||
# Result markers delimit the last TSV table, including an empty write result.
|
||||
_, found, table = text.rpartition("R2XSQL_RESULT_BEGIN\n")
|
||||
if not found:
|
||||
return [], []
|
||||
lines = table.removesuffix("R2XSQL_RESULT_END\n").strip("\r\n").splitlines()
|
||||
if not lines:
|
||||
return [], []
|
||||
return lines[0].split("\t"), [line.split("\t") for line in lines[1:]]
|
||||
@@ -1,25 +0,0 @@
|
||||
"""Native table formatting and suffix dispatch through the plugin."""
|
||||
|
||||
import subprocess
|
||||
|
||||
|
||||
def test_table_output(r2_bin, r2xsql_plugin):
|
||||
def run(*commands):
|
||||
args = [r2_bin, "-N", "-q", "-e", "scr.color=0", "-e", "scr.utf8=false",
|
||||
"-i", str(r2xsql_plugin)]
|
||||
for command in commands:
|
||||
args.extend(["-c", command])
|
||||
proc = subprocess.run(args + ["malloc://1024"], capture_output=True,
|
||||
text=True, timeout=15)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return proc.stdout
|
||||
|
||||
assert run('"sql SELECT 1 AS n, \'abc\' AS name"') == "n name\n------\n1 abc\n"
|
||||
assert run('"sql,:csv SELECT 1 AS n, \'a,b\' AS name"') == 'n,name\n1,"a,b"\n'
|
||||
assert run('"sql,n/gt/1,n/sort/dec,:quiet SELECT 1 AS n UNION SELECT 2 UNION SELECT 3"') == "3\n2\n"
|
||||
assert run("e cfg.table.format=csv", '"sql SELECT 7 AS n"') == "7\n"
|
||||
assert ":csv" in run("sql,?")
|
||||
assert ":csv" in run("sql,:help")
|
||||
assert run('"sql,:csv SELECT 1 AS n WHERE 0"') == "n\n"
|
||||
assert run('"sql,:csv SELECT * FROM missing_table"').startswith("ERROR:")
|
||||
assert "Unknown sql subcommand" in run("sql.SELECT 1")
|
||||
Reference in new issue
Block a user