Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 78 additions & 53 deletions rust/src/transform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ use crate::data_buffer::DataBuffer;
use crate::metadata::Metadata;
use crate::rc::{Array, CoreArrayProvider, CoreArrayProviderInner, Ref, RefCountable};
use crate::settings::Settings;
use crate::string::{raw_to_string, strings_to_string_list, BnString, IntoCStr};
use crate::string::{raw_to_string, strings_to_string_list, BnString, IntoCStr, IntoJson};
use binaryninjacore_sys::*;
use std::borrow::Cow;
use std::collections::HashMap;
use std::ffi::{c_char, c_void, CStr};
use std::ffi::{c_char, c_void, CStr, CString};
use std::fmt::{Debug, Formatter};
use std::mem::ManuallyDrop;
use std::path::Path;
Expand Down Expand Up @@ -605,6 +605,15 @@ unsafe impl CoreArrayProviderInner for TransformContext {
}
}

/// Converts the options passed to a [`TransformSession`] constructor to the JSON string the core
/// expects, using an empty object when there are none.
fn session_options<O: IntoJson>(options: Option<O>) -> Option<CString> {
match options {
Some(options) => Some(options.get_json_string().ok()?.to_cstr().to_owned()),
None => Some(c"{}".to_owned()),
}
}

/// A [`TransformSession`] manages the lifetime of a set of [`TransformContext`]s, performing transformations
/// on said contexts, and extracting the results.
///
Expand Down Expand Up @@ -635,86 +644,102 @@ impl TransformSession {

/// Creates the new transform session for the given `file_path` on disk.
///
/// `options` are per-session setting overrides as a JSON object of setting keys and values, for
/// example `{"files.container.excludedTransforms": ["Zip"]}`. Pass `None` to use the settings as they are.
///
/// Returns `None` if `options` cannot be converted to JSON.
///
/// NOTE: The [`TransformSessionMode`] will be set to the default specified by the `files.container.mode`
/// setting in the Binary Ninja global settings (_not_ the settings passed into this function). To
/// setting in the Binary Ninja global settings (_not_ the options passed into this function). To
/// specify the mode, use [`TransformSession::new_with_mode`] instead.
pub fn new(file_path: &Path, settings: &Settings) -> Ref<TransformSession> {
///
/// ```no_run
/// use binaryninja::transform::TransformSession;
/// use std::path::Path;
///
/// let _headless_session = binaryninja::headless::Session::new().unwrap();
///
/// // Don't extract ZIP files in this session.
/// let options = r#"{"files.container.excludedTransforms": ["Zip"]}"#;
/// let session = TransformSession::new(Path::new("/tmp/two.zip"), Some(options))
/// .expect("Couldn't create the transform session");
/// session.process();
/// ```
pub fn new<O: IntoJson>(file_path: &Path, options: Option<O>) -> Option<Ref<TransformSession>> {
let file_path = file_path.to_cstr();
let settings = settings.serialize_schema().to_cstr();
let session = unsafe { BNCreateTransformSession(file_path.as_ptr(), settings.as_ptr()) };
assert!(
!session.is_null(),
"Transform session should always be valid"
);
unsafe { Self::ref_from_raw(session) }
let options = session_options(options)?;
let session = unsafe { BNCreateTransformSession(file_path.as_ptr(), options.as_ptr()) };
unsafe { Self::ref_from_raw_checked(session) }
}

/// Creates the new transform session for the given `file_path` on disk, with the specified `mode`.
pub fn new_with_mode(
///
/// See [`TransformSession::new`] for the format of `options`.
pub fn new_with_mode<O: IntoJson>(
file_path: &Path,
settings: &Settings,
options: Option<O>,
mode: TransformSessionMode,
) -> Ref<TransformSession> {
) -> Option<Ref<TransformSession>> {
let file_path = file_path.to_cstr();
let settings = settings.serialize_schema().to_cstr();
let session = unsafe {
BNCreateTransformSessionWithMode(file_path.as_ptr(), mode, settings.as_ptr())
};
assert!(
!session.is_null(),
"Transform session should always be valid"
);
unsafe { Self::ref_from_raw(session) }
let options = session_options(options)?;
let session =
unsafe { BNCreateTransformSessionWithMode(file_path.as_ptr(), mode, options.as_ptr()) };
unsafe { Self::ref_from_raw_checked(session) }
}

/// Creates the new transform session for the given `view`.
pub fn from_view(view: &BinaryView, settings: &Settings) -> Ref<TransformSession> {
let settings = settings.serialize_schema().to_cstr();
///
/// See [`TransformSession::new`] for the format of `options`.
pub fn from_view<O: IntoJson>(
view: &BinaryView,
options: Option<O>,
) -> Option<Ref<TransformSession>> {
let options = session_options(options)?;
let session =
unsafe { BNCreateTransformSessionFromBinaryView(view.handle, settings.as_ptr()) };
assert!(
!session.is_null(),
"Transform session should always be valid"
);
unsafe { Self::ref_from_raw(session) }
unsafe { BNCreateTransformSessionFromBinaryView(view.handle, options.as_ptr()) };
unsafe { Self::ref_from_raw_checked(session) }
}

/// Creates the new transform session for the given `view`.
pub fn from_view_with_mode(
/// Creates the new transform session for the given `view`, with the specified `mode`.
///
/// See [`TransformSession::new`] for the format of `options`.
pub fn from_view_with_mode<O: IntoJson>(
view: &BinaryView,
settings: &Settings,
options: Option<O>,
mode: TransformSessionMode,
) -> Ref<TransformSession> {
let settings = settings.serialize_schema().to_cstr();
) -> Option<Ref<TransformSession>> {
let options = session_options(options)?;
let session = unsafe {
BNCreateTransformSessionFromBinaryViewWithMode(view.handle, mode, settings.as_ptr())
BNCreateTransformSessionFromBinaryViewWithMode(view.handle, mode, options.as_ptr())
};
assert!(
!session.is_null(),
"Transform session should always be valid"
);
unsafe { Self::ref_from_raw(session) }
unsafe { Self::ref_from_raw_checked(session) }
}

/// Creates the new transform session for the given `context`.
pub fn from_context(
/// Creates the new transform session for the given `context`, with the specified `mode`.
///
/// See [`TransformSession::new`] for the format of `options`.
pub fn from_context<O: IntoJson>(
context: &TransformContext,
settings: &Settings,
options: Option<O>,
mode: TransformSessionMode,
) -> Ref<TransformSession> {
let settings = settings.serialize_schema().to_cstr();
) -> Option<Ref<TransformSession>> {
let options = session_options(options)?;
let session = unsafe {
BNCreateTransformSessionFromTransformContextWithMode(
context.handle,
mode,
settings.as_ptr(),
options.as_ptr(),
)
};
assert!(
!session.is_null(),
"Transform session should always be valid"
);
unsafe { Self::ref_from_raw(session) }
unsafe { Self::ref_from_raw_checked(session) }
}

unsafe fn ref_from_raw_checked(handle: *mut BNTransformSession) -> Option<Ref<Self>> {
if handle.is_null() {
None
} else {
Some(unsafe { Self::ref_from_raw(handle) })
}
}

pub fn root_context(&self) -> Option<Ref<TransformContext>> {
Expand Down
3 changes: 1 addition & 2 deletions rust/tests/transform.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
use binaryninja::binary_view::{BinaryReader, BinaryView, BinaryViewBase};
use binaryninja::data_buffer::DataBuffer;
use binaryninja::file_metadata::FileMetadata;
use binaryninja::settings::Settings;
use binaryninja::transform::{
register_transform, register_transform_with_context, register_transform_with_detection,
ContextTransform, CustomTransform, DetectionTransform, ProcessResult, Transform,
Expand Down Expand Up @@ -159,7 +158,7 @@ fn test_context_transform() {
assert!(transform.supports_context());
let view = BinaryView::from_data(&FileMetadata::new(), b"initial_data");
// Create session and grab root context
let session = TransformSession::from_view(&view, &Settings::global());
let session = TransformSession::from_view(&view, None::<&str>).expect("Should create session");
let root = session.root_context().expect("Should have root context");
// Tell the root context to explicitly use our context transform
root.set_transform_name("MyContextTransform");
Expand Down
95 changes: 95 additions & 0 deletions rust/tests/transform_session.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
//! Transform session tests.
//!
//! These are kept apart from `transform.rs`, which registers a detection transform that accepts any
//! input. Tests in the same process would apply it while processing a session.

use binaryninja::binary_view::BinaryView;
use binaryninja::file_metadata::FileMetadata;
use binaryninja::transform::{TransformSession, TransformSessionMode};

fn crc32(data: &[u8]) -> u32 {
let mut crc = !0u32;
for &byte in data {
crc ^= byte as u32;
for _ in 0..8 {
crc = (crc >> 1) ^ (0xEDB8_8320 & (crc & 1).wrapping_neg());
}
}
!crc
}

/// Builds a ZIP archive that stores (doesn't compress) the given files.
fn stored_zip(files: &[(&str, &[u8])]) -> Vec<u8> {
let mut archive = Vec::new();
let mut central_directory = Vec::new();
for (name, data) in files {
let offset = archive.len() as u32;
let mut fields = Vec::new();
fields.extend_from_slice(&20u16.to_le_bytes()); // Version needed to extract
fields.extend_from_slice(&0u16.to_le_bytes()); // Flags
fields.extend_from_slice(&0u16.to_le_bytes()); // Compression method: stored
fields.extend_from_slice(&0u16.to_le_bytes()); // Modification time
fields.extend_from_slice(&0x21u16.to_le_bytes()); // Modification date: 1980-01-01
fields.extend_from_slice(&crc32(data).to_le_bytes());
fields.extend_from_slice(&(data.len() as u32).to_le_bytes()); // Compressed size
fields.extend_from_slice(&(data.len() as u32).to_le_bytes()); // Uncompressed size
fields.extend_from_slice(&(name.len() as u16).to_le_bytes());
fields.extend_from_slice(&0u16.to_le_bytes()); // Extra field length

archive.extend_from_slice(&0x0403_4b50u32.to_le_bytes()); // Local file header
archive.extend_from_slice(&fields);
archive.extend_from_slice(name.as_bytes());
archive.extend_from_slice(data);

central_directory.extend_from_slice(&0x0201_4b50u32.to_le_bytes()); // Central directory header
central_directory.extend_from_slice(&20u16.to_le_bytes()); // Version made by
central_directory.extend_from_slice(&fields);
central_directory.extend_from_slice(&[0; 6]); // Comment length, disk number, internal attributes
central_directory.extend_from_slice(&0u32.to_le_bytes()); // External attributes
central_directory.extend_from_slice(&offset.to_le_bytes());
central_directory.extend_from_slice(name.as_bytes());
}
let central_directory_offset = archive.len() as u32;
archive.extend_from_slice(&central_directory);
archive.extend_from_slice(&0x0605_4b50u32.to_le_bytes()); // End of central directory record
archive.extend_from_slice(&[0; 4]); // Disk numbers
archive.extend_from_slice(&(files.len() as u16).to_le_bytes()); // Entries on this disk
archive.extend_from_slice(&(files.len() as u16).to_le_bytes()); // Total entries
archive.extend_from_slice(&(central_directory.len() as u32).to_le_bytes());
archive.extend_from_slice(&central_directory_offset.to_le_bytes());
archive.extend_from_slice(&0u16.to_le_bytes()); // Comment length
archive
}

/// Processes the session and returns the root context's transform name and child count.
fn process_root(session: &TransformSession) -> (String, usize) {
session.process();
let root = session.root_context().expect("Should have root context");
(root.transform_name(), root.children().len())
}

#[test]
fn test_session_options() {
let _session = binaryninja::headless::Session::new().expect("Failed to create session");
let zip = stored_zip(&[("a.bin", b"AAAA"), ("b.bin", b"BBBBBBBB")]);
let temp_dir = tempfile::tempdir().expect("Failed to create temporary directory");
let zip_path = temp_dir.path().join("two.zip");
std::fs::write(&zip_path, &zip).expect("Failed to write ZIP");
let view = BinaryView::from_data(&FileMetadata::new(), &zip);
let mode = TransformSessionMode::TransformSessionModeFull;
let unzipped = ("Zip".to_string(), 2);
let not_unzipped = (String::new(), 0);

// Without options the session uses the settings as they are, so the ZIP is extracted.
let session = TransformSession::new_with_mode(&zip_path, None::<&str>, mode).unwrap();
assert_eq!(process_root(&session), unzipped);
let session = TransformSession::from_view_with_mode(&view, None::<&str>, mode).unwrap();
assert_eq!(process_root(&session), unzipped);

// Excluding the Zip transform for the session leaves the ZIP as it is.
let options = r#"{"files.container.excludedTransforms": ["Zip"]}"#;
let session = TransformSession::new_with_mode(&zip_path, Some(options), mode).unwrap();
assert_eq!(process_root(&session), not_unzipped);
let session = TransformSession::from_view_with_mode(&view, Some(options), mode).unwrap();
assert_eq!(process_root(&session), not_unzipped);
}
Loading