diff --git a/src/clusterfuzz/_internal/base/utils.py b/src/clusterfuzz/_internal/base/utils.py index 5cd25d55a49..bb59e5c8651 100644 --- a/src/clusterfuzz/_internal/base/utils.py +++ b/src/clusterfuzz/_internal/base/utils.py @@ -36,6 +36,7 @@ from clusterfuzz._internal.config import local_config from clusterfuzz._internal.metrics import logs from clusterfuzz._internal.system import environment +from clusterfuzz._internal.system import shell try: import psutil @@ -658,6 +659,17 @@ def read_data_from_file(file_path, eval_data=True, default=None): return None +def read_data_from_file_and_remove(file_path, eval_data=False, default=None): + """Reads file content and removes the file after read""" + if not file_path or not os.path.exists(file_path): + return default + + try: + return read_data_from_file(file_path, eval_data=eval_data, default=default) + finally: + shell.remove_file(file_path) + + def remove_prefix(string, prefix): """Strips the prefix from a string.""" if string.startswith(prefix): diff --git a/src/clusterfuzz/_internal/bot/testcase_manager.py b/src/clusterfuzz/_internal/bot/testcase_manager.py index 67ac58a428d..78eddd2c8cf 100644 --- a/src/clusterfuzz/_internal/bot/testcase_manager.py +++ b/src/clusterfuzz/_internal/bot/testcase_manager.py @@ -15,6 +15,7 @@ import base64 import collections +import dataclasses import datetime import os import re @@ -356,6 +357,65 @@ class Crash( fields.""" +@dataclasses.dataclass +class FuzzerRunOutputData: + """Output metadata for a single fuzzer run stored in memory or a temp file.""" + _output: str | bytes | None = None + _file_path: str | None = None + crash_path: str | None = None + return_code: int = 0 + log_time: datetime.datetime | None = None + + @classmethod + def from_file_path(cls, + file_path: str, + crash_path: str | None = None, + return_code: int = 0, + log_time: datetime.datetime | None = None): + return cls( + _file_path=file_path, + crash_path=crash_path, + return_code=return_code, + log_time=log_time) + + @classmethod + def from_memory(cls, + output: str | bytes, + crash_path: str | None = None, + return_code: int = 0, + log_time: datetime.datetime | None = None): + return cls( + _output=output, + crash_path=crash_path, + return_code=return_code, + log_time=log_time) + + def get_output(self) -> str | None: + """Reads or decodes the fuzzer output text.""" + if self._file_path: + if not os.path.exists(self._file_path): + return None + output = utils.read_data_from_file_and_remove( + self._file_path, eval_data=False) + return None if output is None else output.decode( + 'utf-8', errors='replace') + + if not self._output: + return None + + if isinstance(self._output, bytes): + return self._output.decode('utf-8', errors='replace') + + return self._output + + +@dataclasses.dataclass +class TestcaseRunResult: + """Result of a testcase execution queued for processing.""" + crash: Crash | None = None + fuzzer_run_output_data: FuzzerRunOutputData | None = None + + def get_resource_paths(output): """Read the urls from the output.""" resource_paths = set()