Skip to content

Commit

Permalink
refactor: large refactoring of how compilation errors are handled (#180)
Browse files Browse the repository at this point in the history
The changes in this PR include:

* Redesign how the compilation errors are exposed in the Rust API. With this change users of the API have access to more details about the error, including the structure of error reports (report title, individual labels with their corresponding text and code spans, etc). These changes are backward-incompatible, though.

* The CLI now shows multiple compilation errors at a time, instead of showing only the first error found.

* All the error details exposed in the Rust API are also exposed in the C API and the Golang API.
  • Loading branch information
plusvic committed Aug 27, 2024
1 parent 8b27676 commit 7def597
Show file tree
Hide file tree
Showing 37 changed files with 2,248 additions and 1,380 deletions.
12 changes: 2 additions & 10 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions capi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ name = "yara_x_capi"
crate-type = ["staticlib", "cdylib"]

[dependencies]
serde_json = { workspace = true }
yara-x = { workspace = true }

[build-dependencies]
Expand Down
40 changes: 40 additions & 0 deletions capi/include/yara_x.h
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,46 @@ enum YRX_RESULT yrx_compiler_define_global_float(struct YRX_COMPILER *compiler,
const char *ident,
double value);

// Returns the errors encountered during the compilation in JSON format.
//
// In the address indicated by the `buf` pointer, the function will copy a
// `YRX_BUFFER*` pointer. The `YRX_BUFFER` structure represents a buffer
// that contains the JSON representation of the compilation errors.
//
// The JSON consists on an array of objects, each object representing a
// compilation error. The object has the following fields:
//
// * type: A string that describes the type of error.
// * code: Error code (e.g: "E009").
// * title: Error title (e.g: ""unknown identifier `foo`").
// * labels: Array of labels.
// * text: The full text of the error report, as shown by the command-line tool.
//
// Here is an example:
//
// ```json
// [
// {
// "type": "UnknownIdentifier",
// "code": "E009",
// "title": "unknown identifier `foo`",
// "labels": [
// {
// "level": "error",
// "code_origin": null,
// "span": {"start":25,"end":28},
// "text": "this identifier has not been declared"
// }
// ],
// "text": "... <full report here> ..."
// }
// ]
// ```
//
// The [`YRX_BUFFER`] must be destroyed with [`yrx_buffer_destroy`].
enum YRX_RESULT yrx_compiler_errors_json(struct YRX_COMPILER *compiler,
struct YRX_BUFFER **buf);

// Builds the source code previously added to the compiler.
//
// After calling this function the compiler is reset to its initial state,
Expand Down
84 changes: 77 additions & 7 deletions capi/src/compiler.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
use crate::{LAST_ERROR, YRX_RESULT, YRX_RULES};
use std::ffi::{c_char, CStr, CString};
use std::ffi::{c_char, CStr};
use std::mem;
use std::mem::ManuallyDrop;

use yara_x::errors::{CompileError, SerializationError, VariableError};

use crate::{_yrx_set_last_error, YRX_BUFFER, YRX_RESULT, YRX_RULES};

/// A compiler that takes YARA source code and produces compiled rules.
pub struct YRX_COMPILER<'a> {
Expand Down Expand Up @@ -83,11 +87,11 @@ pub unsafe extern "C" fn yrx_compiler_add_source(

match compiler.inner.add_source(src.to_bytes()) {
Ok(_) => {
LAST_ERROR.set(None);
_yrx_set_last_error::<CompileError>(None);
YRX_RESULT::SUCCESS
}
Err(err) => {
LAST_ERROR.set(Some(CString::new(err.to_string()).unwrap()));
_yrx_set_last_error(Some(err));
YRX_RESULT::SYNTAX_ERROR
}
}
Expand Down Expand Up @@ -158,7 +162,7 @@ pub unsafe extern "C" fn yrx_compiler_new_namespace(
/// scanning data, however each scanner can change the variable’s initial
/// value by calling `yrx_scanner_set_global`.
unsafe fn yrx_compiler_define_global<
T: TryInto<yara_x::Variable, Error = yara_x::VariableError>,
T: TryInto<yara_x::Variable, Error = yara_x::errors::VariableError>,
>(
compiler: *mut YRX_COMPILER,
ident: *const c_char,
Expand All @@ -178,11 +182,11 @@ unsafe fn yrx_compiler_define_global<

match compiler.inner.define_global(ident, value) {
Ok(_) => {
LAST_ERROR.set(None);
_yrx_set_last_error::<VariableError>(None);
YRX_RESULT::SUCCESS
}
Err(err) => {
LAST_ERROR.set(Some(CString::new(err.to_string()).unwrap()));
_yrx_set_last_error(Some(err));
YRX_RESULT::VARIABLE_ERROR
}
}
Expand Down Expand Up @@ -234,6 +238,72 @@ pub unsafe extern "C" fn yrx_compiler_define_global_float(
yrx_compiler_define_global(compiler, ident, value)
}

/// Returns the errors encountered during the compilation in JSON format.
///
/// In the address indicated by the `buf` pointer, the function will copy a
/// `YRX_BUFFER*` pointer. The `YRX_BUFFER` structure represents a buffer
/// that contains the JSON representation of the compilation errors.
///
/// The JSON consists on an array of objects, each object representing a
/// compilation error. The object has the following fields:
///
/// * type: A string that describes the type of error.
/// * code: Error code (e.g: "E009").
/// * title: Error title (e.g: ""unknown identifier `foo`").
/// * labels: Array of labels.
/// * text: The full text of the error report, as shown by the command-line tool.
///
/// Here is an example:
///
/// ```json
/// [
/// {
/// "type": "UnknownIdentifier",
/// "code": "E009",
/// "title": "unknown identifier `foo`",
/// "labels": [
/// {
/// "level": "error",
/// "code_origin": null,
/// "span": {"start":25,"end":28},
/// "text": "this identifier has not been declared"
/// }
/// ],
/// "text": "... <full report here> ..."
/// }
/// ]
/// ```
///
/// The [`YRX_BUFFER`] must be destroyed with [`yrx_buffer_destroy`].
#[no_mangle]
pub unsafe extern "C" fn yrx_compiler_errors_json(
compiler: *mut YRX_COMPILER,
buf: &mut *mut YRX_BUFFER,
) -> YRX_RESULT {
let compiler = if let Some(compiler) = compiler.as_mut() {
compiler
} else {
return YRX_RESULT::INVALID_ARGUMENT;
};

match serde_json::to_vec(compiler.inner.errors()) {
Ok(json) => {
let json = json.into_boxed_slice();
let mut json = ManuallyDrop::new(json);
*buf = Box::into_raw(Box::new(YRX_BUFFER {
data: json.as_mut_ptr(),
length: json.len(),
}));
_yrx_set_last_error::<SerializationError>(None);
YRX_RESULT::SUCCESS
}
Err(err) => {
_yrx_set_last_error(Some(err));
YRX_RESULT::SERIALIZATION_ERROR
}
}
}

/// Builds the source code previously added to the compiler.
///
/// After calling this function the compiler is reset to its initial state,
Expand Down
36 changes: 23 additions & 13 deletions capi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,19 +99,29 @@ use std::mem::ManuallyDrop;
use std::ptr::slice_from_raw_parts_mut;
use std::slice;

use yara_x::errors::{CompileError, SerializationError};

pub use scanner::*;

mod compiler;
mod scanner;

#[cfg(test)]
mod tests;

pub use scanner::*;

thread_local! {
static LAST_ERROR: RefCell<Option<CString>> = const { RefCell::new(None) };
}

fn _yrx_set_last_error<E>(err: Option<E>)
where
E: ToString,
{
LAST_ERROR.set(err.map(|err| CString::new(err.to_string()).unwrap()))
}

/// Error codes returned by functions in this API.
#[derive(PartialEq, Debug)]
#[repr(C)]
pub enum YRX_RESULT {
/// Everything was OK.
Expand Down Expand Up @@ -318,11 +328,11 @@ pub unsafe extern "C" fn yrx_compile(
match yara_x::compile(c_str.to_bytes()) {
Ok(r) => {
*rules = Box::into_raw(Box::new(YRX_RULES(r)));
LAST_ERROR.set(None);
_yrx_set_last_error::<CompileError>(None);
YRX_RESULT::SUCCESS
}
Err(err) => {
LAST_ERROR.set(Some(CString::new(err.to_string()).unwrap()));
_yrx_set_last_error(Some(err));
YRX_RESULT::SYNTAX_ERROR
}
}
Expand Down Expand Up @@ -350,11 +360,11 @@ pub unsafe extern "C" fn yrx_rules_serialize(
data: serialized.as_mut_ptr(),
length: serialized.len(),
}));
LAST_ERROR.set(None);
_yrx_set_last_error::<SerializationError>(None);
YRX_RESULT::SUCCESS
}
Err(err) => {
LAST_ERROR.set(Some(CString::new(err.to_string()).unwrap()));
_yrx_set_last_error(Some(err));
YRX_RESULT::SERIALIZATION_ERROR
}
}
Expand All @@ -375,11 +385,11 @@ pub unsafe extern "C" fn yrx_rules_deserialize(
match yara_x::Rules::deserialize(slice::from_raw_parts(data, len)) {
Ok(r) => {
*rules = Box::into_raw(Box::new(YRX_RULES(r)));
LAST_ERROR.set(None);
_yrx_set_last_error::<SerializationError>(None);
YRX_RESULT::SUCCESS
}
Err(err) => {
LAST_ERROR.set(Some(CString::new(err.to_string()).unwrap()));
_yrx_set_last_error(Some(err));
YRX_RESULT::SERIALIZATION_ERROR
}
}
Expand Down Expand Up @@ -408,7 +418,7 @@ pub unsafe extern "C" fn yrx_rule_identifier(
if let Some(rule) = rule.as_ref() {
*ident = rule.0.identifier().as_ptr();
*len = rule.0.identifier().len();
LAST_ERROR.set(None);
_yrx_set_last_error::<String>(None);
YRX_RESULT::SUCCESS
} else {
YRX_RESULT::INVALID_ARGUMENT
Expand All @@ -432,7 +442,7 @@ pub unsafe extern "C" fn yrx_rule_namespace(
if let Some(rule) = rule.as_ref() {
*ns = rule.0.namespace().as_ptr();
*len = rule.0.namespace().len();
LAST_ERROR.set(None);
_yrx_set_last_error::<String>(None);
YRX_RESULT::SUCCESS
} else {
YRX_RESULT::INVALID_ARGUMENT
Expand Down Expand Up @@ -599,9 +609,9 @@ pub unsafe extern "C" fn yrx_buffer_destroy(buf: *mut YRX_BUFFER) {
/// the most recent function was successfully.
#[no_mangle]
pub unsafe extern "C" fn yrx_last_error() -> *const c_char {
LAST_ERROR.with_borrow(|last_error| {
if let Some(last_error) = last_error {
last_error.as_ptr()
LAST_ERROR.with_borrow(|err| {
if let Some(err) = err {
err.as_ptr()
} else {
std::ptr::null()
}
Expand Down
Loading

0 comments on commit 7def597

Please sign in to comment.