Files
trailofbits-buttercup/program-model/tests/test_tree_sitter.py
T
Boyan MILANOV bef12fa3f5 Program model: Java type resolution (#613)
* Add basic get_type case

* Add support for dotted expression

* Add tests for method return type resolution

* Add more callees tests

* Lint and format

* Remove residual debug prints

* Commit tree_sitter file too

* Fix test issues from merging main

---------

Co-authored-by: Boyan MILANOV <boyanmilanov@coder-boyanmilanov-aixcc-boyan.c.production-1-405717.internal>
2025-05-19 16:07:34 -04:00

454 lines
13 KiB
Python

import pytest
from buttercup.common.challenge_task import ChallengeTask
from buttercup.common.task_meta import TaskMeta
from buttercup.program_model.api.tree_sitter import CodeTS, TypeDefinitionType
from pathlib import Path
from dataclasses import dataclass
@dataclass(frozen=True)
class FunctionInfo:
num_bodies: int
body_excerpts: list[str]
@pytest.fixture
def task_dir(tmp_path: Path) -> Path:
"""Create a mock challenge task directory structure."""
# Create the main directories
base_path = tmp_path / "task_rw"
oss_fuzz = base_path / "fuzz-tooling" / "fuzz-tooling"
source = base_path / "src" / "example_project"
diffs = base_path / "diff" / "my-diff"
oss_fuzz.mkdir(parents=True, exist_ok=True)
source.mkdir(parents=True, exist_ok=True)
diffs.mkdir(parents=True, exist_ok=True)
# Create mock project.yaml file
project_yaml_path = oss_fuzz / "projects" / "example_project" / "project.yaml"
project_yaml_path.parent.mkdir(parents=True, exist_ok=True)
project_yaml_path.write_text(
"language: c\n"
"sanitizers:\n"
" - address\n"
" - memory\n"
" - undefined\n"
"architectures:\n"
" - x86_64\n"
"fuzzing_engines:\n"
" - afl\n"
" - honggfuzz\n"
" - libfuzzer\n"
)
# Create some mock patch files
(diffs / "patch1.diff").write_text("mock patch 1")
(diffs / "patch2.diff").write_text("mock patch 2")
# Create a mock helper.py file
helper_path = oss_fuzz / "infra" / "helper.py"
helper_path.parent.mkdir(parents=True, exist_ok=True)
helper_path.write_text("import sys;\nsys.exit(0)\n")
# Create a mock test.txt file
(source / "test.txt").write_text("mock test content")
# Create a test C file with two functions
test_c_content = """#include <stdio.h>
// Forward declarations - these should not be matched
struct forward_struct;
union forward_union;
enum forward_enum;
// Preprocessor type definitions
#define MY_TYPE my_struct_t
#define ANOTHER_TYPE struct my_struct
struct struct_name {
int a;
int b;
};
int add(int a, int b) {
return a + b;
}
void print_hello(void) {
printf("Hello, World!\\n");
}
"""
(source / "test.c").write_text(test_c_content)
test2_c_content = """#include <stdio.h>
#ifdef TEST
int add(int a, int b) {
return a + b;
}
#else
double add(double a, double b) {
return a + b;
}
#endif
"""
(source / "test2.c").write_text(test2_c_content)
# Create task metadata
TaskMeta(
project_name="example_project",
focus="example_project",
task_id="task-id-tree-sitter",
metadata={
"task_id": "task-id-tree-sitter",
"round_id": "testing",
"team_id": "tob",
},
).save(base_path)
return base_path
@pytest.fixture
def challenge_task_readonly(task_dir: Path) -> ChallengeTask:
"""Create a mock challenge task for testing."""
return ChallengeTask(
read_only_task_dir=task_dir,
)
@pytest.fixture
def java_task_dir(tmp_path: Path) -> Path:
"""Create a mock challenge task directory structure."""
# Create the main directories
base_path = tmp_path / "task_rw"
oss_fuzz = base_path / "fuzz-tooling" / "fuzz-tooling"
source = base_path / "src" / "example_project"
diffs = base_path / "diff" / "my-diff"
oss_fuzz.mkdir(parents=True, exist_ok=True)
source.mkdir(parents=True, exist_ok=True)
diffs.mkdir(parents=True, exist_ok=True)
# Create mock project.yaml file
project_yaml_path = oss_fuzz / "projects" / "example_project" / "project.yaml"
project_yaml_path.parent.mkdir(parents=True, exist_ok=True)
project_yaml_path.write_text("language: java\n")
# Create a mock helper.py file
helper_path = oss_fuzz / "infra" / "helper.py"
helper_path.parent.mkdir(parents=True, exist_ok=True)
helper_path.write_text("import sys;\nsys.exit(0)\n")
# Create a mock test.txt file
(source / "test.txt").write_text("mock test content")
# Create task metadata
TaskMeta(
project_name="example_project",
focus="example_project",
task_id="task-id-tree-sitter",
metadata={
"task_id": "task-id-tree-sitter",
"round_id": "testing",
"team_id": "tob",
},
).save(base_path)
return base_path
@pytest.fixture
def java_challenge_task_readonly(java_task_dir: Path) -> ChallengeTask:
"""Create a mock challenge task for testing."""
return ChallengeTask(
read_only_task_dir=java_task_dir,
)
def test_get_functions_code_c(challenge_task_readonly: ChallengeTask):
"""Test getting function code from a C file."""
code_ts = CodeTS(challenge_task_readonly)
functions = code_ts.get_functions(Path("src/example_project/test.c"))
assert "add" in functions
assert "print_hello" in functions
add_function = functions["add"]
assert len(add_function.bodies) == 1
assert "int add(int a, int b)" in add_function.bodies[0].body
assert "return a + b;" in add_function.bodies[0].body
print_hello_function = functions["print_hello"]
assert len(print_hello_function.bodies) == 1
assert "void print_hello(void)" in print_hello_function.bodies[0].body
assert 'printf("Hello, World!\\n");' in print_hello_function.bodies[0].body
def test_get_function_c(challenge_task_readonly: ChallengeTask):
"""Test getting a function from a C file."""
code_ts = CodeTS(challenge_task_readonly)
function = code_ts.get_function("add", Path("src/example_project/test.c"))
assert function is not None
assert function.name == "add"
assert function.file_path == Path("src/example_project/test.c")
assert len(function.bodies) == 1
assert "int add(int a, int b)" in function.bodies[0].body
assert "return a + b;" in function.bodies[0].body
assert function.bodies[0].start_line == 17
assert function.bodies[0].end_line == 19
def test_get_function_multiple_definitions_c(challenge_task_readonly: ChallengeTask):
"""Test getting a function from a C file with multiple definitions."""
code_ts = CodeTS(challenge_task_readonly)
function = code_ts.get_function("add", Path("src/example_project/test2.c"))
assert function is not None
assert function.name == "add"
assert function.file_path == Path("src/example_project/test2.c")
assert len(function.bodies) == 2
assert "#ifdef TEST" in function.bodies[0].body
assert "int add(int a, int b)" in function.bodies[0].body
assert "double add(double a, double b)" in function.bodies[1].body
assert "#else" in function.bodies[1].body
assert function.bodies[0].start_line == 3
assert function.bodies[0].end_line == 6
assert function.bodies[1].start_line == 7
assert function.bodies[1].end_line == 10
def test_get_type_definition_types(challenge_task_readonly: ChallengeTask):
"""Test getting different types of definitions."""
code_ts = CodeTS(challenge_task_readonly)
types = code_ts.parse_types_in_code(Path("src/example_project/test.c"))
# Test preprocessor type definitions
type_def = types["MY_TYPE"]
assert type_def is not None
assert type_def.type == TypeDefinitionType.PREPROC_TYPE
assert "#define MY_TYPE my_struct_t" in type_def.definition
type_def = types["ANOTHER_TYPE"]
assert type_def is not None
assert type_def.type == TypeDefinitionType.PREPROC_TYPE
assert "#define ANOTHER_TYPE struct my_struct" in type_def.definition
@pytest.mark.parametrize(
"function_name,file_path,function_info",
[
(
"png_icc_check_length",
"src/libpng/png.c",
FunctionInfo(
num_bodies=1,
body_excerpts=[
"""int /* PRIVATE */
png_icc_check_length(png_const_structrp png_ptr, png_const_charp name,
png_uint_32 profile_length)
{
if (!icc_check_length(png_ptr, name, profile_length))
return 0;
"""
],
),
),
(
"have_chromaticities",
"src/libpng/png.c",
FunctionInfo(
num_bodies=1,
body_excerpts=[
"""static int
have_chromaticities(png_const_structrp png_ptr)
{
/* Handle new PNGv3 chunks and the precedence rules to determine whether
* png_struct::chromaticities must be processed. Only required for RGB to
""",
],
),
),
(
"png_pow10",
"src/libpng/png.c",
FunctionInfo(
num_bodies=1,
body_excerpts=[
"""/* Utility used below - a simple accurate power of ten from an integral
* exponent.
*/
static double
png_pow10(int power)
{
int recip = 0;
double d = 1;
""",
],
),
),
(
"png_check_IHDR",
"src/libpng/png.c",
FunctionInfo(
num_bodies=1,
body_excerpts=[
"""
#ifdef PNG_SET_USER_LIMITS_SUPPORTED
if (width > png_ptr->user_width_max)
#else
if (width > PNG_USER_WIDTH_MAX)
#endif
{
png_warning(png_ptr, "Image width exceeds user limit in IHDR");
error = 1;
}
if (height == 0)
{
png_warning(png_ptr, "Image height is zero in IHDR");
error = 1;
}
if (height > PNG_UINT_31_MAX)
{
png_warning(png_ptr, "Invalid image height in IHDR");
error = 1;
}
#ifdef PNG_SET_USER_LIMITS_SUPPORTED
if (height > png_ptr->user_height_max)
#else
if (height > PNG_USER_HEIGHT_MAX)
#endif
{
png_warning(png_ptr, "Image height exceeds user limit in IHDR");
error = 1;
}"""
],
),
),
],
)
@pytest.mark.integration
def test_libpng_indexing(
libpng_oss_fuzz_task: ChallengeTask,
function_name: str,
file_path: str,
function_info: FunctionInfo,
):
"""Test that we can parse libpng code using tree-sitter."""
code_ts = CodeTS(libpng_oss_fuzz_task)
function = code_ts.get_function(function_name, Path(file_path))
assert function is not None
assert len(function.bodies) == function_info.num_bodies
for body in function_info.body_excerpts:
assert any([body in x.body for x in function.bodies])
def test_get_field_type(java_challenge_task_readonly: ChallengeTask):
"""Test getting the type of a field of a type definition."""
code_ts = CodeTS(java_challenge_task_readonly)
typedef = b"""class Person {
age = 30;
String something;
public String child() {
return this.child.toString();
}
Person2 child;
}
"""
type_name = code_ts.get_field_type_name(typedef, "child")
assert type_name == "Person2"
def test_get_method_return_type(java_challenge_task_readonly: ChallengeTask):
"""Test getting the return type of a method of a type definition."""
code_ts = CodeTS(java_challenge_task_readonly)
typedef = b"""class Person {
int getName = 40;
public SuperClass getname() {
int getName = 1;
return new SuperClass(getName);
}
public String getName() {
return "John";
}
}
"""
type_name = code_ts.get_method_return_type_name(typedef, "getName")
assert type_name == "String"
typedef = b"""public interface LoggerRepository {
/**
* Add a {@link HierarchyEventListener} event to the repository.
*
* @param listener The listener
*/
void addHierarchyEventListener(HierarchyEventListener listener);
/**
* Returns whether this repository is disabled for a given
* level. The answer depends on the repository threshold and the
* <code>level</code> parameter. See also {@link #setThreshold}
* method.
*
* @param level The level
* @return whether this repository is disabled.
*/
boolean isDisabled(int level);
/**
* Set the repository-wide threshold. All logging requests below the
* threshold are immediately dropped. By default, the threshold is
* set to <code>Level.ALL</code> which has the lowest possible rank.
*
* @param level The level
*/
void setThreshold(Level level);
/**
* Another form of {@link #setThreshold(Level)} accepting a string
* parameter instead of a <code>Level</code>.
*
* @param val The threshold value
*/
void setThreshold(String val);
void emitNoAppenderWarning(Category cat);
/**
* Get the repository-wide threshold. See {@link #setThreshold(Level)} for an explanation.
*
* @return the level.
*/
Level getThreshold();
Logger getLogger(String name);
Logger getLogger(String name, LoggerFactory factory);
Logger getRootLogger();
Logger exists(String name);
void shutdown();
@SuppressWarnings("rawtypes")
Enumeration getCurrentLoggers();
/**
* Deprecated. Please use {@link #getCurrentLoggers} instead.
*
* @return an enumeration of loggers.
*/
@SuppressWarnings("rawtypes")
Enumeration getCurrentCategories();
void fireAddAppenderEvent(Category logger, Appender appender);
void resetConfiguration();
}"""
type_name = code_ts.get_method_return_type_name(typedef, "getLogger")
assert type_name == "Logger"