diff --git a/rust/src/transform.rs b/rust/src/transform.rs index 05b81c94f8..335d1c3b2c 100644 --- a/rust/src/transform.rs +++ b/rust/src/transform.rs @@ -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; @@ -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(options: Option) -> Option { + 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. /// @@ -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 { + /// + /// ```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(file_path: &Path, options: Option) -> Option> { 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( file_path: &Path, - settings: &Settings, + options: Option, mode: TransformSessionMode, - ) -> Ref { + ) -> Option> { 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 { - let settings = settings.serialize_schema().to_cstr(); + /// + /// See [`TransformSession::new`] for the format of `options`. + pub fn from_view( + view: &BinaryView, + options: Option, + ) -> Option> { + 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( view: &BinaryView, - settings: &Settings, + options: Option, mode: TransformSessionMode, - ) -> Ref { - let settings = settings.serialize_schema().to_cstr(); + ) -> Option> { + 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( context: &TransformContext, - settings: &Settings, + options: Option, mode: TransformSessionMode, - ) -> Ref { - let settings = settings.serialize_schema().to_cstr(); + ) -> Option> { + 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> { + if handle.is_null() { + None + } else { + Some(unsafe { Self::ref_from_raw(handle) }) + } } pub fn root_context(&self) -> Option> { diff --git a/rust/tests/transform.rs b/rust/tests/transform.rs index c573f07209..285a97a239 100644 --- a/rust/tests/transform.rs +++ b/rust/tests/transform.rs @@ -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, @@ -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"); diff --git a/rust/tests/transform_session.rs b/rust/tests/transform_session.rs new file mode 100644 index 0000000000..ac6d346cac --- /dev/null +++ b/rust/tests/transform_session.rs @@ -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 { + 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(¢ral_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(¢ral_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); +}