diff --git a/rust/src/collaboration/changeset.rs b/rust/src/collaboration/changeset.rs new file mode 100644 index 000000000..2b5a6fda9 --- /dev/null +++ b/rust/src/collaboration/changeset.rs @@ -0,0 +1,130 @@ +use core::{ffi, mem, ptr}; + +use binaryninjacore_sys::*; + +use super::{RemoteFile, User}; + +use crate::database::Database; +use crate::rc::{Array, CoreArrayProvider, CoreArrayProviderInner}; +use crate::string::{BnStrCompatible, BnString}; + +/// Class representing a collection of snapshots in a local database +#[repr(transparent)] +pub struct Changeset { + handle: ptr::NonNull, +} + +impl Drop for Changeset { + fn drop(&mut self) { + unsafe { BNFreeCollaborationChangeset(self.as_raw()) } + } +} + +impl Clone for Changeset { + fn clone(&self) -> Self { + unsafe { + Self::from_raw( + ptr::NonNull::new(BNNewCollaborationChangesetReference(self.as_raw())).unwrap(), + ) + } + } +} + +impl Changeset { + pub(crate) unsafe fn from_raw(handle: ptr::NonNull) -> Self { + Self { handle } + } + + pub(crate) unsafe fn ref_from_raw(handle: &*mut BNCollaborationChangeset) -> &Self { + assert!(!handle.is_null()); + mem::transmute(handle) + } + + #[allow(clippy::mut_from_ref)] + pub(crate) unsafe fn as_raw(&self) -> &mut BNCollaborationChangeset { + &mut *self.handle.as_ptr() + } + + /// Owning database for snapshots + pub fn database(&self) -> Result { + let result = unsafe { BNCollaborationChangesetGetDatabase(self.as_raw()) }; + let raw = ptr::NonNull::new(result).ok_or(())?; + Ok(unsafe { Database::from_raw(raw) }) + } + + /// Relevant remote File object + pub fn file(&self) -> Result { + let result = unsafe { BNCollaborationChangesetGetFile(self.as_raw()) }; + ptr::NonNull::new(result) + .map(|raw| unsafe { RemoteFile::from_raw(raw) }) + .ok_or(()) + } + + /// List of snapshot ids in the database + pub fn snapshot_ids(&self) -> Result, ()> { + let mut count = 0; + let result = unsafe { BNCollaborationChangesetGetSnapshotIds(self.as_raw(), &mut count) }; + (!result.is_null()) + .then(|| unsafe { Array::new(result, count, ()) }) + .ok_or(()) + } + + /// Relevant remote author User + pub fn author(&self) -> Result { + let result = unsafe { BNCollaborationChangesetGetAuthor(self.as_raw()) }; + ptr::NonNull::new(result) + .map(|raw| unsafe { User::from_raw(raw) }) + .ok_or(()) + } + + /// Changeset name + pub fn name(&self) -> BnString { + let result = unsafe { BNCollaborationChangesetGetName(self.as_raw()) }; + assert!(!result.is_null()); + unsafe { BnString::from_raw(result) } + } + + /// Set the name of the changeset, e.g. in a name changeset function. + pub fn set_name(&self, value: S) -> bool { + let value = value.into_bytes_with_nul(); + unsafe { + BNCollaborationChangesetSetName( + self.as_raw(), + value.as_ref().as_ptr() as *const ffi::c_char, + ) + } + } +} + +impl CoreArrayProvider for Changeset { + type Raw = *mut BNCollaborationChangeset; + type Context = (); + type Wrapped<'a> = &'a Self; +} + +unsafe impl CoreArrayProviderInner for Changeset { + unsafe fn free(raw: *mut Self::Raw, count: usize, _context: &Self::Context) { + BNFreeCollaborationChangesetList(raw, count) + } + + unsafe fn wrap_raw<'a>(raw: &'a Self::Raw, _context: &'a Self::Context) -> Self::Wrapped<'a> { + Self::ref_from_raw(raw) + } +} + +pub struct SnapshotId; +impl CoreArrayProvider for SnapshotId { + type Raw = i64; + type Context = (); + type Wrapped<'a> = i64; +} + +unsafe impl CoreArrayProviderInner for SnapshotId { + unsafe fn free(raw: *mut Self::Raw, count: usize, _context: &Self::Context) { + BNCollaborationFreeSnapshotIdList(raw, count) + } + + unsafe fn wrap_raw<'a>(raw: &'a Self::Raw, _context: &'a Self::Context) -> Self::Wrapped<'a> { + *raw + } +} diff --git a/rust/src/collaboration/databasesync.rs b/rust/src/collaboration/databasesync.rs new file mode 100644 index 000000000..d038c2495 --- /dev/null +++ b/rust/src/collaboration/databasesync.rs @@ -0,0 +1,708 @@ +use core::{ffi, mem, ptr}; + +use binaryninjacore_sys::*; + +use super::{ + Changeset, CollabSnapshot, MergeConflict, Remote, RemoteFile, RemoteFolder, RemoteProject, +}; + +use crate::binaryview::{BinaryView, BinaryViewExt}; +use crate::database::{Database, Snapshot}; +use crate::ffi::{ProgressCallback, ProgressCallbackNop}; +use crate::filemetadata::FileMetadata; +use crate::project::ProjectFile; +use crate::rc::Ref; +use crate::string::{BnStrCompatible, BnString}; +use crate::typearchive::{TypeArchive, TypeArchiveMergeConflict}; + +/// Get the default directory path for a remote Project. This is based off the Setting for +/// collaboration.directory, the project's id, and the project's remote's id. +pub fn default_project_path(project: &RemoteProject) -> Result { + let result = unsafe { BNCollaborationDefaultProjectPath(project.as_raw()) }; + let success = !result.is_null(); + success + .then(|| unsafe { BnString::from_raw(result) }) + .ok_or(()) +} + +// Get the default filepath for a remote File. This is based off the Setting for +// collaboration.directory, the file's id, the file's project's id, and the file's +// remote's id. +pub fn default_file_path(file: &RemoteFile) -> Result { + let result = unsafe { BNCollaborationDefaultFilePath(file.as_raw()) }; + let success = !result.is_null(); + success + .then(|| unsafe { BnString::from_raw(result) }) + .ok_or(()) +} + +/// Download a file from its remote, saving all snapshots to a database in the +/// specified location. Returns a FileContext for opening the file later. +/// +/// * `file` - Remote File to download and open +/// * `db_path` - File path for saved database +/// * `progress` - Function to call for progress updates +pub fn download_file( + file: &RemoteFile, + db_path: S, + mut progress: F, +) -> Result, ()> { + let db_path = db_path.into_bytes_with_nul(); + let result = unsafe { + BNCollaborationDownloadFile( + file.as_raw(), + db_path.as_ref().as_ptr() as *const ffi::c_char, + Some(F::cb_progress_callback), + &mut progress as *mut F as *mut ffi::c_void, + ) + }; + let success = !result.is_null(); + success + .then(|| unsafe { Ref::new(FileMetadata::from_raw(result)) }) + .ok_or(()) +} + +/// Upload a file, with database, to the remote under the given project +/// +/// * `metadata` - Local file with database +/// * `project` - Remote project under which to place the new file +/// * `parent_folder` - Optional parent folder in which to place this file +/// * `progress` - Function to call for progress updates +/// * `name_changeset` - Function to call for naming a pushed changeset, if necessary +pub fn upload_database( + metadata: &FileMetadata, + project: &RemoteProject, + parent_folder: Option<&RemoteFolder>, + mut progress: P, + mut name_changeset: N, +) -> Result { + let folder_raw = parent_folder.map_or(ptr::null_mut(), |h| unsafe { h.as_raw() } as *mut _); + let result = unsafe { + BNCollaborationUploadDatabase( + metadata.handle, + project.as_raw(), + folder_raw, + Some(P::cb_progress_callback), + &mut progress as *mut P as *mut ffi::c_void, + Some(N::cb_name_changeset), + &mut name_changeset as *mut N as *mut ffi::c_void, + ) + }; + ptr::NonNull::new(result) + .map(|raw| unsafe { RemoteFile::from_raw(raw) }) + .ok_or(()) +} + +/// Test if a database is valid for use in collaboration +pub fn is_collaboration_database(database: &Database) -> bool { + unsafe { BNCollaborationIsCollaborationDatabase(database.as_raw()) } +} + +/// Get the Remote for a Database +pub fn get_remote_for_local_database(database: &Database) -> Result, ()> { + let mut value = ptr::null_mut(); + let success = + unsafe { BNCollaborationGetRemoteForLocalDatabase(database.as_raw(), &mut value) }; + success + .then(|| ptr::NonNull::new(value).map(|handle| unsafe { Remote::from_raw(handle) })) + .ok_or(()) +} + +/// Get the Remote for a BinaryView +pub fn get_remote_for_binary_view(bv: &BinaryView) -> Result, ()> { + let Some(db) = bv.file().database() else { + return Ok(None); + }; + get_remote_for_local_database(&db) +} + +/// Get the Remote Project for a Database, returning the Remote project from one of the +/// connected remotes, or None if not found or if projects are not pulled +pub fn get_remote_project_for_local_database( + database: &Database, +) -> Result, ()> { + let mut value = ptr::null_mut(); + let success = + unsafe { BNCollaborationGetRemoteProjectForLocalDatabase(database.as_raw(), &mut value) }; + success + .then(|| ptr::NonNull::new(value).map(|handle| unsafe { RemoteProject::from_raw(handle) })) + .ok_or(()) +} + +/// Get the Remote File for a Database +pub fn get_remote_file_for_local_database(database: &Database) -> Result, ()> { + let mut value = ptr::null_mut(); + let success = + unsafe { BNCollaborationGetRemoteFileForLocalDatabase(database.as_raw(), &mut value) }; + success + .then(|| ptr::NonNull::new(value).map(|handle| unsafe { RemoteFile::from_raw(handle) })) + .ok_or(()) +} + +/// Add a snapshot to the id map in a database +pub fn assign_snapshot_map( + local_snapshot: &Snapshot, + remote_snapshot: &CollabSnapshot, +) -> Result<(), ()> { + let success = unsafe { + BNCollaborationAssignSnapshotMap(local_snapshot.as_raw(), remote_snapshot.as_raw()) + }; + success.then_some(()).ok_or(()) +} + +/// Get the remote snapshot associated with a local snapshot (if it exists) +pub fn get_remote_snapshot_from_local(snap: &Snapshot) -> Result, ()> { + let mut value = ptr::null_mut(); + let success = unsafe { BNCollaborationGetRemoteSnapshotFromLocal(snap.as_raw(), &mut value) }; + success + .then(|| ptr::NonNull::new(value).map(|handle| unsafe { CollabSnapshot::from_raw(handle) })) + .ok_or(()) +} + +/// Get the local snapshot associated with a remote snapshot (if it exists) +pub fn get_local_snapshot_for_remote( + snapshot: &CollabSnapshot, + database: &Database, +) -> Result, ()> { + let mut value = ptr::null_mut(); + let success = unsafe { + BNCollaborationGetLocalSnapshotFromRemote(snapshot.as_raw(), database.as_raw(), &mut value) + }; + success + .then(|| ptr::NonNull::new(value).map(|handle| unsafe { Snapshot::from_raw(handle) })) + .ok_or(()) +} + +/// Completely sync a database, pushing/pulling/merging/applying changes +/// +/// * `database` - Database to sync +/// * `file` - File to sync with +/// * `conflict_handler` - Function to call to resolve snapshot conflicts +/// * `progress` - Function to call for progress updates +/// * `name_changeset` - Function to call for naming a pushed changeset, if necessary +pub fn sync_database( + database: &Database, + file: &RemoteFile, + mut conflict_handler: C, + mut progress: P, + mut name_changeset: N, +) -> Result<(), ()> { + let success = unsafe { + BNCollaborationSyncDatabase( + database.as_raw(), + file.as_raw(), + Some(C::cb_handle_conflict), + &mut conflict_handler as *mut C as *mut ffi::c_void, + Some(P::cb_progress_callback), + &mut progress as *mut P as *mut ffi::c_void, + Some(N::cb_name_changeset), + &mut name_changeset as *mut N as *mut ffi::c_void, + ) + }; + success.then_some(()).ok_or(()) +} + +/// Pull updated snapshots from the remote. Merge local changes with remote changes and +/// potentially create a new snapshot for unsaved changes, named via name_changeset. +/// +/// * `database` - Database to pull +/// * `file` - Remote File to pull to +/// * `conflict_handler` - Function to call to resolve snapshot conflicts +/// * `progress` - Function to call for progress updates +/// * `name_changeset` - Function to call for naming a pushed changeset, if necessary +pub fn pull_database( + database: &Database, + file: &RemoteFile, + mut conflict_handler: C, + mut progress: P, + mut name_changeset: N, +) -> Result { + let mut count = 0; + let success = unsafe { + BNCollaborationPullDatabase( + database.as_raw(), + file.as_raw(), + &mut count, + Some(C::cb_handle_conflict), + &mut conflict_handler as *mut C as *mut ffi::c_void, + Some(P::cb_progress_callback), + &mut progress as *mut P as *mut ffi::c_void, + Some(N::cb_name_changeset), + &mut name_changeset as *mut N as *mut ffi::c_void, + ) + }; + success.then_some(count).ok_or(()) +} + +/// Merge all leaf snapshots in a database down to a single leaf snapshot. +/// +/// * `database` - Database to merge +/// * `conflict_handler` - Function to call for progress updates +/// * `progress` - Function to call to resolve snapshot conflicts +pub fn merge_database( + database: &Database, + mut conflict_handler: C, + mut progress: P, +) -> Result<(), ()> { + let success = unsafe { + BNCollaborationMergeDatabase( + database.as_raw(), + Some(C::cb_handle_conflict), + &mut conflict_handler as *mut C as *mut ffi::c_void, + Some(P::cb_progress_callback), + &mut progress as *mut P as *mut ffi::c_void, + ) + }; + success.then_some(()).ok_or(()) +} + +/// Push locally added snapshots to the remote +/// +/// * `database` - Database to push +/// * `file` - Remote File to push to +/// * `progress` - Function to call for progress updates +pub fn push_database( + database: &Database, + file: &RemoteFile, + mut progress: P, +) -> Result { + let mut count = 0; + let success = unsafe { + BNCollaborationPushDatabase( + database.as_raw(), + file.as_raw(), + &mut count, + Some(P::cb_progress_callback), + &mut progress as *mut P as *mut ffi::c_void, + ) + }; + success.then_some(count).ok_or(()) +} + +/// Print debug information about a database to stdout +pub fn dump_database(database: &Database) -> Result<(), ()> { + let success = unsafe { BNCollaborationDumpDatabase(database.as_raw()) }; + success.then_some(()).ok_or(()) +} + +/// Ignore a snapshot from database syncing operations +/// +/// * `database` - Parent database +/// * `snapshot` - Snapshot to ignore +pub fn ignore_snapshot(database: &Database, snapshot: &Snapshot) -> Result<(), ()> { + let success = unsafe { BNCollaborationIgnoreSnapshot(database.as_raw(), snapshot.as_raw()) }; + success.then_some(()).ok_or(()) +} + +/// Test if a snapshot is ignored from the database +/// +/// * `database` - Parent database +/// * `snapshot` - Snapshot to test +pub fn is_snapshot_ignored(database: &Database, snapshot: &Snapshot) -> bool { + unsafe { BNCollaborationIsSnapshotIgnored(database.as_raw(), snapshot.as_raw()) } +} + +/// Get the remote author of a local snapshot +/// +/// * `database` - Parent database +/// * `snapshot` - Snapshot to query +pub fn get_snapshot_author( + database: &Database, + snapshot: &Snapshot, +) -> Result, ()> { + let mut value = ptr::null_mut(); + let success = unsafe { + BNCollaborationGetSnapshotAuthor(database.as_raw(), snapshot.as_raw(), &mut value) + }; + success + .then(|| (!value.is_null()).then(|| unsafe { BnString::from_raw(value) })) + .ok_or(()) +} + +/// Set the remote author of a local snapshot (does not upload) +/// +/// * `database` - Parent database +/// * `snapshot` - Snapshot to edit +/// * `author` - Target author +pub fn set_snapshot_author( + database: &Database, + snapshot: &Snapshot, + author: S, +) -> Result<(), ()> { + let author = author.into_bytes_with_nul(); + let success = unsafe { + BNCollaborationSetSnapshotAuthor( + database.as_raw(), + snapshot.as_raw(), + author.as_ref().as_ptr() as *const ffi::c_char, + ) + }; + success.then_some(()).ok_or(()) +} + +pub(crate) fn pull_projects(database: &Database) -> Result { + let Some(remote) = get_remote_for_local_database(database)? else { + return Ok(false); + }; + if !remote.has_pulled_projects() { + remote.pull_projects(ProgressCallbackNop)?; + } + Ok(true) +} + +pub(crate) fn pull_files(database: &Database) -> Result { + if !pull_projects(database)? { + return Ok(false); + } + let Some(project) = get_remote_project_for_local_database(database)? else { + return Ok(false); + }; + if !project.has_pulled_files() { + project.pull_files(ProgressCallbackNop)?; + } + Ok(true) +} + +/// Completely sync a type archive, pushing/pulling/merging/applying changes +/// +/// * `type_archive` - TypeArchive to sync +/// * `file` - File to sync with +/// * `conflict_handler` - Function to call to resolve snapshot conflicts +/// * `progress` - Function to call for progress updates +pub fn sync_type_archive( + type_archive: &TypeArchive, + file: &RemoteFile, + mut conflict_handler: C, + mut progress: P, +) -> Result<(), ()> { + let success = unsafe { + BNCollaborationSyncTypeArchive( + type_archive.as_raw(), + file.as_raw(), + Some(C::cb_handle_conflict), + &mut conflict_handler as *mut C as *mut ffi::c_void, + Some(P::cb_progress_callback), + &mut progress as *mut P as *mut ffi::c_void, + ) + }; + success.then_some(()).ok_or(()) +} + +/// Push locally added snapshots to the remote +/// +/// * `type_archive` - TypeArchive to push +/// * `file` - Remote File to push to +/// * `progress` - Function to call for progress updates +pub fn push_type_archive( + type_archive: &TypeArchive, + file: &RemoteFile, + mut progress: P, +) -> Result { + let mut count = 0; + let success = unsafe { + BNCollaborationPushTypeArchive( + type_archive.as_raw(), + file.as_raw(), + &mut count, + Some(P::cb_progress_callback), + &mut progress as *mut P as *mut ffi::c_void, + ) + }; + success.then_some(count).ok_or(()) +} + +/// Pull updated snapshots from the remote. Merge local changes with remote changes and +/// potentially create a new snapshot for unsaved changes, named via name_changeset. +/// +/// * `type_archive` - TypeArchive to pull +/// * `file` - Remote File to pull to +/// * `conflict_handler` - Function to call to resolve snapshot conflicts +/// * `progress` - Function to call for progress updates +/// * `name_changeset` - Function to call for naming a pushed changeset, if necessary +pub fn pull_type_archive( + type_archive: &TypeArchive, + file: &RemoteFile, + mut conflict_handler: C, + mut progress: P, +) -> Result { + let mut count = 0; + let success = unsafe { + BNCollaborationPullTypeArchive( + type_archive.as_raw(), + file.as_raw(), + &mut count, + Some(C::cb_handle_conflict), + &mut conflict_handler as *mut C as *mut ffi::c_void, + Some(P::cb_progress_callback), + &mut progress as *mut P as *mut ffi::c_void, + ) + }; + success.then_some(count).ok_or(()) +} + +/// Test if a type archive is valid for use in collaboration +pub fn is_collaboration_type_archive(type_archive: &TypeArchive) -> bool { + unsafe { BNCollaborationIsCollaborationTypeArchive(type_archive.as_raw()) } +} + +/// Get the Remote for a Type Archive +pub fn get_remote_for_local_type_archive(type_archive: &TypeArchive) -> Option { + let value = unsafe { BNCollaborationGetRemoteForLocalTypeArchive(type_archive.as_raw()) }; + ptr::NonNull::new(value).map(|handle| unsafe { Remote::from_raw(handle) }) +} + +/// Get the Remote Project for a Type Archive +pub fn get_remote_project_for_local_type_archive(database: &TypeArchive) -> Option { + let value = unsafe { BNCollaborationGetRemoteProjectForLocalTypeArchive(database.as_raw()) }; + ptr::NonNull::new(value).map(|handle| unsafe { RemoteProject::from_raw(handle) }) +} + +/// Get the Remote File for a Type Archive +pub fn get_remote_file_for_local_type_archive(database: &TypeArchive) -> Option { + let value = unsafe { BNCollaborationGetRemoteFileForLocalTypeArchive(database.as_raw()) }; + ptr::NonNull::new(value).map(|handle| unsafe { RemoteFile::from_raw(handle) }) +} + +/// Get the remote snapshot associated with a local snapshot (if it exists) in a Type Archive +pub fn get_remote_snapshot_from_local_type_archive( + type_archive: &TypeArchive, + snapshot_id: S, +) -> Option { + let snapshot_id = snapshot_id.into_bytes_with_nul(); + let value = unsafe { + BNCollaborationGetRemoteSnapshotFromLocalTypeArchive( + type_archive.as_raw(), + snapshot_id.as_ref().as_ptr() as *const ffi::c_char, + ) + }; + ptr::NonNull::new(value).map(|handle| unsafe { CollabSnapshot::from_raw(handle) }) +} + +/// Get the local snapshot associated with a remote snapshot (if it exists) in a Type Archive +pub fn get_local_snapshot_from_remote_type_archive( + snapshot: &CollabSnapshot, + type_archive: &TypeArchive, +) -> Option { + let value = unsafe { + BNCollaborationGetLocalSnapshotFromRemoteTypeArchive( + snapshot.as_raw(), + type_archive.as_raw(), + ) + }; + (!value.is_null()).then(|| unsafe { BnString::from_raw(value) }) +} + +/// Test if a snapshot is ignored from the archive +pub fn is_type_archive_snapshot_ignored( + type_archive: &TypeArchive, + snapshot_id: S, +) -> bool { + let snapshot_id = snapshot_id.into_bytes_with_nul(); + unsafe { + BNCollaborationIsTypeArchiveSnapshotIgnored( + type_archive.as_raw(), + snapshot_id.as_ref().as_ptr() as *const ffi::c_char, + ) + } +} + +/// Download a type archive from its remote, saving all snapshots to an archive in the +/// specified `location`. Returns a [TypeArchive] for using later. +pub fn download_type_archive( + file: &RemoteFile, + location: S, + mut progress: F, +) -> Result, ()> { + let mut value = ptr::null_mut(); + let db_path = location.into_bytes_with_nul(); + let success = unsafe { + BNCollaborationDownloadTypeArchive( + file.as_raw(), + db_path.as_ref().as_ptr() as *const ffi::c_char, + Some(F::cb_progress_callback), + &mut progress as *mut F as *mut ffi::c_void, + &mut value, + ) + }; + success + .then(|| ptr::NonNull::new(value).map(|handle| unsafe { TypeArchive::from_raw(handle) })) + .ok_or(()) +} + +/// Upload a type archive +pub fn upload_type_archive( + archive: &TypeArchive, + project: &RemoteProject, + folder: &RemoteFolder, + mut progress: P, + core_file: &ProjectFile, +) -> Result { + let mut value = ptr::null_mut(); + let success = unsafe { + BNCollaborationUploadTypeArchive( + archive.as_raw(), + project.as_raw(), + folder.as_raw(), + Some(P::cb_progress_callback), + &mut progress as *const P as *mut ffi::c_void, + core_file.as_raw(), + &mut value, + ) + }; + success + .then(|| { + ptr::NonNull::new(value) + .map(|handle| unsafe { RemoteFile::from_raw(handle) }) + .unwrap() + }) + .ok_or(()) +} + +/// Merge a pair of snapshots and create a new snapshot with the result. +pub fn merge_snapshots( + first: &Snapshot, + second: &Snapshot, + mut conflict_handler: C, + mut progress: P, +) -> Result { + let value = unsafe { + BNCollaborationMergeSnapshots( + first.as_raw(), + second.as_raw(), + Some(C::cb_handle_conflict), + &mut conflict_handler as *mut C as *mut ffi::c_void, + Some(P::cb_progress_callback), + &mut progress as *mut P as *mut ffi::c_void, + ) + }; + ptr::NonNull::new(value) + .map(|handle| unsafe { Snapshot::from_raw(handle) }) + .ok_or(()) +} + +pub trait NameChangeset: Sized { + fn name_changeset(&mut self, changeset: &Changeset) -> bool; + unsafe extern "C" fn cb_name_changeset( + ctxt: *mut ::std::os::raw::c_void, + changeset: *mut BNCollaborationChangeset, + ) -> bool { + let ctxt: &mut Self = &mut *(ctxt as *mut Self); + ctxt.name_changeset(Changeset::ref_from_raw(&changeset)) + } +} + +impl NameChangeset for F +where + F: for<'a> FnMut(&'a Changeset) -> bool, +{ + fn name_changeset(&mut self, changeset: &Changeset) -> bool { + self(changeset) + } +} + +pub struct NameChangesetNop; +impl NameChangeset for NameChangesetNop { + fn name_changeset(&mut self, _changeset: &Changeset) -> bool { + unreachable!() + } + + unsafe extern "C" fn cb_name_changeset( + _ctxt: *mut std::os::raw::c_void, + _changeset: *mut BNCollaborationChangeset, + ) -> bool { + true + } +} + +/// Helper trait that resolves conflicts +pub trait DatabaseConflictHandler: Sized { + /// Handle any merge conflicts by calling their success() function with a merged value + /// + /// * `conflicts` - conflicts ids to conflicts structures + /// + /// Return true if all conflicts were successfully merged + fn handle_conflict(&mut self, keys: &str, conflicts: &MergeConflict) -> bool; + unsafe extern "C" fn cb_handle_conflict( + ctxt: *mut ffi::c_void, + keys: *mut *const ffi::c_char, + conflicts: *mut *mut BNAnalysisMergeConflict, + conflict_count: usize, + ) -> bool { + let ctxt: &mut Self = &mut *(ctxt as *mut Self); + let keys = core::slice::from_raw_parts(keys, conflict_count); + let conflicts = core::slice::from_raw_parts(conflicts, conflict_count); + keys.iter().zip(conflicts.iter()).all(|(key, conflict)| { + // NOTE this is a reference, not owned, so ManuallyDrop is required, or just implement `ref_from_raw` + let key = mem::ManuallyDrop::new(BnString::from_raw(*key as *mut _)); + let conflict = MergeConflict::ref_from_raw(conflict); + ctxt.handle_conflict(key.as_str(), conflict) + }) + } +} + +impl DatabaseConflictHandler for F +where + F: for<'a> FnMut(&'a str, &'a MergeConflict) -> bool, +{ + fn handle_conflict(&mut self, keys: &str, conflicts: &MergeConflict) -> bool { + self(keys, conflicts) + } +} + +pub struct DatabaseConflictHandlerFail; +impl DatabaseConflictHandler for DatabaseConflictHandlerFail { + fn handle_conflict(&mut self, _keys: &str, _conflicts: &MergeConflict) -> bool { + unreachable!() + } + + unsafe extern "C" fn cb_handle_conflict( + _ctxt: *mut ffi::c_void, + _keys: *mut *const ffi::c_char, + _conflicts: *mut *mut BNAnalysisMergeConflict, + _conflict_count: usize, + ) -> bool { + // TODO only fail if _conflict_count is greater then 0? + //_conflict_count > 0 + false + } +} + +pub trait TypeArchiveConflictHandler: Sized { + fn handle_conflict(&mut self, conflicts: &TypeArchiveMergeConflict) -> bool; + unsafe extern "C" fn cb_handle_conflict( + ctxt: *mut ::std::os::raw::c_void, + conflicts: *mut *mut BNTypeArchiveMergeConflict, + conflict_count: usize, + ) -> bool { + let slf: &mut Self = &mut *(ctxt as *mut Self); + core::slice::from_raw_parts(conflicts, conflict_count) + .iter() + .all(|conflict| slf.handle_conflict(TypeArchiveMergeConflict::ref_from_raw(conflict))) + } +} + +impl TypeArchiveConflictHandler for F +where + F: for<'a> FnMut(&'a TypeArchiveMergeConflict) -> bool, +{ + fn handle_conflict(&mut self, conflicts: &TypeArchiveMergeConflict) -> bool { + self(conflicts) + } +} + +pub struct TypeArchiveConflictHandlerFail; +impl TypeArchiveConflictHandler for TypeArchiveConflictHandlerFail { + fn handle_conflict(&mut self, _conflicts: &TypeArchiveMergeConflict) -> bool { + unreachable!() + } + + unsafe extern "C" fn cb_handle_conflict( + _ctxt: *mut ffi::c_void, + _conflicts: *mut *mut BNTypeArchiveMergeConflict, + _conflict_count: usize, + ) -> bool { + // TODO only fail if _conflict_count is greater then 0? + //_conflict_count > 0 + false + } +} diff --git a/rust/src/collaboration/file.rs b/rust/src/collaboration/file.rs new file mode 100644 index 000000000..fab5ed1d3 --- /dev/null +++ b/rust/src/collaboration/file.rs @@ -0,0 +1,537 @@ +use core::{ffi, mem, ptr}; + +use std::time::SystemTime; + +use binaryninjacore_sys::*; + +use super::{ + databasesync, CollabSnapshot, DatabaseConflictHandler, DatabaseConflictHandlerFail, + NameChangeset, NameChangesetNop, Remote, RemoteFolder, RemoteProject, +}; + +use crate::binaryview::{BinaryView, BinaryViewExt}; +use crate::database::Database; +use crate::ffi::{ProgressCallback, ProgressCallbackNop, SplitProgressBuilder}; +use crate::filemetadata::FileMetadata; +use crate::project::ProjectFile; +use crate::rc::{Array, CoreArrayProvider, CoreArrayProviderInner, Ref}; +use crate::string::{BnStrCompatible, BnString}; + +pub type RemoteFileType = BNRemoteFileType; + +/// Class representing a remote project file. It controls the various +/// snapshots and raw file contents associated with the analysis. +#[repr(transparent)] +pub struct RemoteFile { + handle: ptr::NonNull, +} + +impl Drop for RemoteFile { + fn drop(&mut self) { + unsafe { BNFreeRemoteFile(self.as_raw()) } + } +} + +impl PartialEq for RemoteFile { + fn eq(&self, other: &Self) -> bool { + self.id() == other.id() + } +} +impl Eq for RemoteFile {} + +impl Clone for RemoteFile { + fn clone(&self) -> Self { + unsafe { + Self::from_raw(ptr::NonNull::new(BNNewRemoteFileReference(self.as_raw())).unwrap()) + } + } +} + +impl RemoteFile { + pub(crate) unsafe fn from_raw(handle: ptr::NonNull) -> Self { + Self { handle } + } + + pub(crate) unsafe fn ref_from_raw(handle: &*mut BNRemoteFile) -> &Self { + assert!(!handle.is_null()); + mem::transmute(handle) + } + + #[allow(clippy::mut_from_ref)] + pub(crate) unsafe fn as_raw(&self) -> &mut BNRemoteFile { + &mut *self.handle.as_ptr() + } + + /// Look up the remote File for a local database, or None if there is no matching + /// remote File found. + /// See [RemoteFile::get_for_binary_view] to load from a [BinaryView]. + pub fn get_for_local_database(database: &Database) -> Result, ()> { + if !databasesync::pull_files(database)? { + return Ok(None); + } + databasesync::get_remote_file_for_local_database(database) + } + + /// Look up the remote File for a local BinaryView, or None if there is no matching + /// remote File found. + pub fn get_for_binary_view(bv: &BinaryView) -> Result, ()> { + let file = bv.file(); + let Some(database) = file.database() else { + return Ok(None); + }; + RemoteFile::get_for_local_database(&database) + } + + pub fn core_file(&self) -> Result { + let result = unsafe { BNRemoteFileGetCoreFile(self.as_raw()) }; + ptr::NonNull::new(result) + .map(|handle| unsafe { ProjectFile::from_raw(handle) }) + .ok_or(()) + } + + pub fn project(&self) -> Result { + let result = unsafe { BNRemoteFileGetProject(self.as_raw()) }; + ptr::NonNull::new(result) + .map(|handle| unsafe { RemoteProject::from_raw(handle) }) + .ok_or(()) + } + + pub fn remote(&self) -> Result { + let result = unsafe { BNRemoteFileGetRemote(self.as_raw()) }; + ptr::NonNull::new(result) + .map(|handle| unsafe { Remote::from_raw(handle) }) + .ok_or(()) + } + + /// Parent folder, if one exists. None if this is in the root of the project. + pub fn folder(&self) -> Result, ()> { + let project = self.project()?; + if !project.has_pulled_folders() { + project.pull_folders(ProgressCallbackNop)?; + } + let result = unsafe { BNRemoteFileGetFolder(self.as_raw()) }; + Ok(ptr::NonNull::new(result).map(|handle| unsafe { RemoteFolder::from_raw(handle) })) + } + + /// Set the parent folder of a file. + pub fn set_folder(&self, folder: Option<&RemoteFolder>) -> Result<(), ()> { + let folder_raw = folder.map_or(ptr::null_mut(), |folder| unsafe { folder.as_raw() } + as *mut _); + let success = unsafe { BNRemoteFileSetFolder(self.as_raw(), folder_raw) }; + success.then_some(()).ok_or(()) + } + + pub fn set_metadata(&self, folder: S) -> Result<(), ()> { + let folder_raw = folder.into_bytes_with_nul(); + let success = unsafe { + BNRemoteFileSetMetadata( + self.as_raw(), + folder_raw.as_ref().as_ptr() as *const ffi::c_char, + ) + }; + success.then_some(()).ok_or(()) + } + + /// Web API endpoint URL + pub fn url(&self) -> BnString { + let result = unsafe { BNRemoteFileGetUrl(self.as_raw()) }; + assert!(!result.is_null()); + unsafe { BnString::from_raw(result) } + } + + /// Chat log API endpoint URL + pub fn chat_log_url(&self) -> BnString { + let result = unsafe { BNRemoteFileGetChatLogUrl(self.as_raw()) }; + assert!(!result.is_null()); + unsafe { BnString::from_raw(result) } + } + + pub fn user_positions_url(&self) -> BnString { + let result = unsafe { BNRemoteFileGetUserPositionsUrl(self.as_raw()) }; + assert!(!result.is_null()); + unsafe { BnString::from_raw(result) } + } + + /// Unique ID + pub fn id(&self) -> BnString { + let result = unsafe { BNRemoteFileGetId(self.as_raw()) }; + assert!(!result.is_null()); + unsafe { BnString::from_raw(result) } + } + + /// All files share the same properties, but files with different types may make different + /// uses of those properties, or not use some of them at all. + pub fn file_type(&self) -> RemoteFileType { + unsafe { BNRemoteFileGetType(self.as_raw()) } + } + + /// Created date of the file + pub fn created(&self) -> SystemTime { + let result = unsafe { BNRemoteFileGetCreated(self.as_raw()) }; + crate::ffi::time_from_bn(result.try_into().unwrap()) + } + + pub fn created_by(&self) -> BnString { + let result = unsafe { BNRemoteFileGetCreatedBy(self.as_raw()) }; + assert!(!result.is_null()); + unsafe { BnString::from_raw(result) } + } + + /// Last modified of the file + pub fn last_modified(&self) -> SystemTime { + let result = unsafe { BNRemoteFileGetLastModified(self.as_raw()) }; + crate::ffi::time_from_bn(result.try_into().unwrap()) + } + + /// Date of last snapshot in the file + pub fn last_snapshot(&self) -> SystemTime { + let result = unsafe { BNRemoteFileGetLastSnapshot(self.as_raw()) }; + crate::ffi::time_from_bn(result.try_into().unwrap()) + } + + /// Username of user who pushed the last snapshot in the file + pub fn last_snapshot_by(&self) -> BnString { + let result = unsafe { BNRemoteFileGetLastSnapshotBy(self.as_raw()) }; + assert!(!result.is_null()); + unsafe { BnString::from_raw(result) } + } + + pub fn last_snapshot_name(&self) -> BnString { + let result = unsafe { BNRemoteFileGetLastSnapshotName(self.as_raw()) }; + assert!(!result.is_null()); + unsafe { BnString::from_raw(result) } + } + + /// Hash of file contents (no algorithm guaranteed) + pub fn hash(&self) -> BnString { + let result = unsafe { BNRemoteFileGetHash(self.as_raw()) }; + assert!(!result.is_null()); + unsafe { BnString::from_raw(result) } + } + + /// Displayed name of file + pub fn name(&self) -> BnString { + let result = unsafe { BNRemoteFileGetName(self.as_raw()) }; + assert!(!result.is_null()); + unsafe { BnString::from_raw(result) } + } + + /// Set the description of the file. You will need to push the file to update the remote version. + pub fn set_name(&self, name: S) -> Result<(), ()> { + let name = name.into_bytes_with_nul(); + let success = unsafe { + BNRemoteFileSetName(self.as_raw(), name.as_ref().as_ptr() as *const ffi::c_char) + }; + success.then_some(()).ok_or(()) + } + + /// Desciprtion of the file + pub fn description(&self) -> BnString { + let result = unsafe { BNRemoteFileGetDescription(self.as_raw()) }; + assert!(!result.is_null()); + unsafe { BnString::from_raw(result) } + } + + /// Set the description of the file. You will need to push the file to update the remote version. + pub fn set_description(&self, description: S) -> Result<(), ()> { + let description = description.into_bytes_with_nul(); + let success = unsafe { + BNRemoteFileSetDescription( + self.as_raw(), + description.as_ref().as_ptr() as *const ffi::c_char, + ) + }; + success.then_some(()).ok_or(()) + } + + pub fn metadata(&self) -> BnString { + let result = unsafe { BNRemoteFileGetMetadata(self.as_raw()) }; + assert!(!result.is_null()); + unsafe { BnString::from_raw(result) } + } + + /// Size of raw content of file, in bytes + pub fn size(&self) -> u64 { + unsafe { BNRemoteFileGetSize(self.as_raw()) } + } + + /// Get the default filepath for a remote File. This is based off the Setting for + /// collaboration.directory, the file's id, the file's project's id, and the file's + /// remote's id. + pub fn default_path(&self) -> BnString { + let result = unsafe { BNCollaborationDefaultFilePath(self.as_raw()) }; + assert!(!result.is_null()); + unsafe { BnString::from_raw(result) } + } + + /// If the file has pulled the snapshots yet + pub fn has_pulled_snapshots(&self) -> bool { + unsafe { BNRemoteFileHasPulledSnapshots(self.as_raw()) } + } + + /// Get the list of snapshots in this file. + /// + /// NOTE: If snapshots have not been pulled, they will be pulled upon calling this. + pub fn snapshots(&self) -> Result, ()> { + if !self.has_pulled_snapshots() { + self.pull_snapshots(ProgressCallbackNop)?; + } + let mut count = 0; + let result = unsafe { BNRemoteFileGetSnapshots(self.as_raw(), &mut count) }; + (!result.is_null()) + .then(|| unsafe { Array::new(result, count, ()) }) + .ok_or(()) + } + + /// Get a specific Snapshot in the File by its id + /// + /// NOTE: If snapshots have not been pulled, they will be pulled upon calling this. + pub fn snapshot_by_id(&self, id: S) -> Result, ()> { + if !self.has_pulled_snapshots() { + self.pull_snapshots(ProgressCallbackNop)?; + } + let id = id.into_bytes_with_nul(); + let result = unsafe { + BNRemoteFileGetSnapshotById(self.as_raw(), id.as_ref().as_ptr() as *const ffi::c_char) + }; + Ok(ptr::NonNull::new(result).map(|handle| unsafe { CollabSnapshot::from_raw(handle) })) + } + + /// Pull the list of Snapshots from the Remote. + pub fn pull_snapshots(&self, mut progress: P) -> Result<(), ()> { + let success = unsafe { + BNRemoteFilePullSnapshots( + self.as_raw(), + Some(P::cb_progress_callback), + &mut progress as *mut P as *mut ffi::c_void, + ) + }; + success.then_some(()).ok_or(()) + } + + /// Create a new snapshot on the remote (and pull it) + /// + /// * `name` - Snapshot name + /// * `contents` - Snapshot contents + /// * `analysis_cache_contents` - Contents of analysis cache of snapshot + /// * `file` - New file contents (if contents changed) + /// * `parent_ids` - List of ids of parent snapshots (or empty if this is a root snapshot) + /// * `progress` - Function to call on progress updates + pub fn create_snapshot( + &self, + name: S, + contents: &mut [u8], + analysis_cache_contexts: &mut [u8], + file: &mut [u8], + parent_ids: I, + mut progress: P, + ) -> Result + where + S: BnStrCompatible, + P: ProgressCallback, + I: IntoIterator, + I::Item: BnStrCompatible, + { + let name = name.into_bytes_with_nul(); + let parent_ids: Vec<_> = parent_ids + .into_iter() + .map(|id| id.into_bytes_with_nul()) + .collect(); + let mut parent_ids_raw: Vec<_> = parent_ids + .iter() + .map(|x| x.as_ref().as_ptr() as *const ffi::c_char) + .collect(); + let result = unsafe { + BNRemoteFileCreateSnapshot( + self.as_raw(), + name.as_ref().as_ptr() as *const ffi::c_char, + contents.as_mut_ptr(), + contents.len(), + analysis_cache_contexts.as_mut_ptr(), + analysis_cache_contexts.len(), + file.as_mut_ptr(), + file.len(), + parent_ids_raw.as_mut_ptr(), + parent_ids_raw.len(), + Some(P::cb_progress_callback), + &mut progress as *mut P as *mut ffi::c_void, + ) + }; + let handle = ptr::NonNull::new(result).ok_or(())?; + Ok(unsafe { CollabSnapshot::from_raw(handle) }) + } + + // Delete a snapshot from the remote + pub fn delete_snapshot(&self, snapshot: &CollabSnapshot) -> Result<(), ()> { + let success = unsafe { BNRemoteFileDeleteSnapshot(self.as_raw(), snapshot.as_raw()) }; + success.then_some(()).ok_or(()) + } + + // TODO - This passes and returns a c++ `std::vector`. A BnData can be implement in rust, but the + // coreAPI need to include a `FreeData` function, similar to `BNFreeString` does. + // The C++ API just assumes that both use the same allocator, and the python API seems to just leak this + // memory, never droping it. + //pub fn download_file(&self, mut progress_function: F) -> BnData + //where + // S: BnStrCompatible, + // F: ProgressCallback, + //{ + // let mut data = ptr::null_mut(); + // let mut data_len = 0; + // let result = unsafe { + // BNRemoteFileDownload( + // self.as_raw(), + // Some(F::cb_progress_callback), + // &mut progress_function as *mut _ as *mut ffi::c_void, + // &mut data, + // &mut data_len, + // ) + // }; + // todo!() + //} + + pub fn request_user_positions(&self) -> BnString { + let result = unsafe { BNRemoteFileRequestUserPositions(self.as_raw()) }; + assert!(!result.is_null()); + unsafe { BnString::from_raw(result) } + } + + pub fn request_chat_log(&self) -> BnString { + let result = unsafe { BNRemoteFileRequestChatLog(self.as_raw()) }; + assert!(!result.is_null()); + unsafe { BnString::from_raw(result) } + } + + /// Download a file from its remote, saving all snapshots to a database in the + /// specified location. Returns a FileContext for opening the file later. + /// + /// * `db_path` - File path for saved database + /// * `progress_function` - Function to call for progress updates + pub fn download(&self, db_path: S, mut progress_function: F) -> Ref + where + S: BnStrCompatible, + F: ProgressCallback, + { + let db_path = db_path.into_bytes_with_nul(); + let result = unsafe { + BNCollaborationDownloadFile( + self.as_raw(), + db_path.as_ref().as_ptr() as *const ffi::c_char, + Some(F::cb_progress_callback), + &mut progress_function as *mut _ as *mut ffi::c_void, + ) + }; + assert!(!result.is_null()); + unsafe { Ref::new(FileMetadata::from_raw(result)) } + } + + pub fn download_data_for_file( + &self, + db_path: S, + force: bool, + mut progress_function: F, + ) -> Result<(), ()> + where + S: BnStrCompatible, + F: ProgressCallback, + { + let db_path = db_path.into_bytes_with_nul(); + let success = unsafe { + BNCollaborationDownloadDatabaseForFile( + self.as_raw(), + db_path.as_ref().as_ptr() as *const ffi::c_char, + force, + Some(F::cb_progress_callback), + &mut progress_function as *mut _ as *mut ffi::c_void, + ) + }; + success.then_some(()).ok_or(()) + } + + /// Download a remote file and save it to a bndb at the given path. + /// This calls databasesync.download_file and self.sync to fully prepare the bndb. + pub fn download_to_bndb( + &self, + path: Option, + progress: P, + ) -> Result, ()> { + let path = path + .map(|x| BnString::new(x)) + .unwrap_or_else(|| self.default_path()); + let mut progress = progress.split(&[50, 50]); + let file = databasesync::download_file(self, path, progress.next_subpart().unwrap())?; + let database = file.database().ok_or(())?; + self.sync( + &database, + DatabaseConflictHandlerFail, + progress.next_subpart().unwrap(), + NameChangesetNop, + )?; + Ok(file) + } + + /// Completely sync a file, pushing/pulling/merging/applying changes + /// + /// * `bv_or_db` - Binary view or database to sync with + /// * `conflict_handler` - Function to call to resolve snapshot conflicts + /// * `name_changeset` - Function to call for naming a pushed changeset, if necessary + /// * `progress` - Function to call for progress updates + pub fn sync( + &self, + database: &Database, + conflict_handler: C, + progress: P, + name_changeset: N, + ) -> Result<(), ()> { + databasesync::sync_database(database, self, conflict_handler, progress, name_changeset) + } + + /// Pull updated snapshots from the remote. Merge local changes with remote changes and + /// potentially create a new snapshot for unsaved changes, named via name_changeset. + /// + /// * `bv_or_db` - Binary view or database to sync with + /// * `conflict_handler` - Function to call to resolve snapshot conflicts + /// * `name_changeset` - Function to call for naming a pushed changeset, if necessary + /// * `progress` - Function to call for progress updates + pub fn pull( + &self, + database: &Database, + conflict_handler: C, + progress: P, + name_changeset: N, + ) -> Result + where + C: DatabaseConflictHandler, + P: ProgressCallback, + N: NameChangeset, + { + databasesync::pull_database(database, self, conflict_handler, progress, name_changeset) + } + + /// Push locally added snapshots to the remote + /// + /// * `bv_or_db` - Binary view or database to sync with + /// * `progress` - Function to call for progress updates + pub fn push

(&self, database: &Database, progress: P) -> Result + where + P: ProgressCallback, + { + databasesync::push_database(database, self, progress) + } +} + +impl CoreArrayProvider for RemoteFile { + type Raw = *mut BNRemoteFile; + type Context = (); + type Wrapped<'a> = &'a Self; +} + +unsafe impl CoreArrayProviderInner for RemoteFile { + unsafe fn free(raw: *mut Self::Raw, count: usize, _context: &Self::Context) { + BNFreeRemoteFileList(raw, count) + } + + unsafe fn wrap_raw<'a>(raw: &'a Self::Raw, _context: &'a Self::Context) -> Self::Wrapped<'a> { + Self::ref_from_raw(raw) + } +} diff --git a/rust/src/collaboration/folder.rs b/rust/src/collaboration/folder.rs new file mode 100644 index 000000000..0977afee9 --- /dev/null +++ b/rust/src/collaboration/folder.rs @@ -0,0 +1,172 @@ +use core::{ffi, mem, ptr}; + +use binaryninjacore_sys::*; + +use super::{Remote, RemoteProject}; + +use crate::ffi::ProgressCallbackNop; +use crate::project::ProjectFolder; +use crate::rc::{CoreArrayProvider, CoreArrayProviderInner}; +use crate::string::{BnStrCompatible, BnString}; + +/// Struct representing a remote folder in a project. +#[repr(transparent)] +pub struct RemoteFolder { + handle: ptr::NonNull, +} + +impl Drop for RemoteFolder { + fn drop(&mut self) { + unsafe { BNFreeRemoteFolder(self.as_raw()) } + } +} + +impl PartialEq for RemoteFolder { + fn eq(&self, other: &Self) -> bool { + self.id() == other.id() + } +} +impl Eq for RemoteFolder {} + +impl Clone for RemoteFolder { + fn clone(&self) -> Self { + unsafe { + Self::from_raw(ptr::NonNull::new(BNNewRemoteFolderReference(self.as_raw())).unwrap()) + } + } +} + +impl RemoteFolder { + pub(crate) unsafe fn from_raw(handle: ptr::NonNull) -> Self { + Self { handle } + } + + pub(crate) unsafe fn ref_from_raw(handle: &*mut BNRemoteFolder) -> &Self { + assert!(!handle.is_null()); + mem::transmute(handle) + } + + #[allow(clippy::mut_from_ref)] + pub(crate) unsafe fn as_raw(&self) -> &mut BNRemoteFolder { + &mut *self.handle.as_ptr() + } + + /// Get the core folder associated with this remote folder. + pub fn core_folder(&self) -> Result { + let result = unsafe { BNRemoteFolderGetCoreFolder(self.as_raw()) }; + ptr::NonNull::new(result) + .map(|handle| unsafe { ProjectFolder::from_raw(handle) }) + .ok_or(()) + } + + /// Get the owning project of this folder. + pub fn project(&self) -> Result { + let result = unsafe { BNRemoteFolderGetProject(self.as_raw()) }; + ptr::NonNull::new(result) + .map(|handle| unsafe { RemoteProject::from_raw(handle) }) + .ok_or(()) + } + + /// Get the owning remote of this folder. + pub fn remote(&self) -> Result { + let result = unsafe { BNRemoteFolderGetRemote(self.as_raw()) }; + ptr::NonNull::new(result) + .map(|handle| unsafe { Remote::from_raw(handle) }) + .ok_or(()) + } + + /// Get the parent folder, if available. + pub fn parent(&self) -> Result, ()> { + let project = self.project()?; + if !project.has_pulled_folders() { + project.pull_folders(ProgressCallbackNop)?; + } + let mut parent_handle = ptr::null_mut(); + let success = unsafe { BNRemoteFolderGetParent(self.as_raw(), &mut parent_handle) }; + success + .then(|| { + ptr::NonNull::new(parent_handle) + .map(|handle| unsafe { RemoteFolder::from_raw(handle) }) + }) + .ok_or(()) + } + + /// Set the parent folder. You will need to push the folder to update the remote version. + pub fn set_parent(&self, parent: Option<&RemoteFolder>) -> Result<(), ()> { + let parent_handle = parent.map_or(ptr::null_mut(), |p| unsafe { p.as_raw() } as *mut _); + let success = unsafe { BNRemoteFolderSetParent(self.as_raw(), parent_handle) }; + success.then_some(()).ok_or(()) + } + + /// Get web API endpoint URL. + pub fn url(&self) -> BnString { + let result = unsafe { BNRemoteFolderGetUrl(self.as_raw()) }; + assert!(!result.is_null()); + unsafe { BnString::from_raw(result) } + } + + /// Get unique ID. + pub fn id(&self) -> BnString { + let result = unsafe { BNRemoteFolderGetId(self.as_raw()) }; + assert!(!result.is_null()); + unsafe { BnString::from_raw(result) } + } + + /// Unique id of parent folder, if there is a parent. None, otherwise + pub fn parent_id(&self) -> Option { + let mut parent_id = ptr::null_mut(); + let have = unsafe { BNRemoteFolderGetParentId(self.as_raw(), &mut parent_id) }; + have.then(|| unsafe { BnString::from_raw(parent_id) }) + } + + /// Displayed name of folder + pub fn name(&self) -> BnString { + let result = unsafe { BNRemoteFolderGetName(self.as_raw()) }; + assert!(!result.is_null()); + unsafe { BnString::from_raw(result) } + } + + /// Set the display name of the folder. You will need to push the folder to update the remote version. + pub fn set_name(&self, name: S) -> Result<(), ()> { + let name = name.into_bytes_with_nul(); + let success = unsafe { + BNRemoteFolderSetName(self.as_raw(), name.as_ref().as_ptr() as *const ffi::c_char) + }; + success.then_some(()).ok_or(()) + } + + /// Description of the folder + pub fn description(&self) -> BnString { + let result = unsafe { BNRemoteFolderGetDescription(self.as_raw()) }; + assert!(!result.is_null()); + unsafe { BnString::from_raw(result) } + } + + /// Set the description of the folder. You will need to push the folder to update the remote version. + pub fn set_description(&self, description: S) -> Result<(), ()> { + let description = description.into_bytes_with_nul(); + let success = unsafe { + BNRemoteFolderSetDescription( + self.as_raw(), + description.as_ref().as_ptr() as *const ffi::c_char, + ) + }; + success.then_some(()).ok_or(()) + } +} + +impl CoreArrayProvider for RemoteFolder { + type Raw = *mut BNRemoteFolder; + type Context = (); + type Wrapped<'a> = &'a Self; +} + +unsafe impl CoreArrayProviderInner for RemoteFolder { + unsafe fn free(raw: *mut Self::Raw, count: usize, _context: &Self::Context) { + BNFreeRemoteFolderList(raw, count) + } + + unsafe fn wrap_raw<'a>(raw: &'a Self::Raw, _context: &'a Self::Context) -> Self::Wrapped<'a> { + Self::ref_from_raw(raw) + } +} diff --git a/rust/src/collaboration/group.rs b/rust/src/collaboration/group.rs new file mode 100644 index 000000000..2fa2fffeb --- /dev/null +++ b/rust/src/collaboration/group.rs @@ -0,0 +1,180 @@ +use core::{ffi, mem, ptr}; + +use binaryninjacore_sys::*; + +use super::Remote; + +use crate::rc::{Array, CoreArrayProvider, CoreArrayProviderInner}; +use crate::string::{BnStrCompatible, BnString}; + +/// Struct representing a remote Group +#[repr(transparent)] +pub struct Group { + handle: ptr::NonNull, +} + +impl Drop for Group { + fn drop(&mut self) { + unsafe { BNFreeCollaborationGroup(self.as_raw()) } + } +} + +impl PartialEq for Group { + fn eq(&self, other: &Self) -> bool { + self.id() == other.id() + } +} +impl Eq for Group {} + +impl Clone for Group { + fn clone(&self) -> Self { + unsafe { + Self::from_raw( + ptr::NonNull::new(BNNewCollaborationGroupReference(self.as_raw())).unwrap(), + ) + } + } +} + +impl Group { + pub(crate) unsafe fn from_raw(handle: ptr::NonNull) -> Self { + Self { handle } + } + + pub(crate) unsafe fn ref_from_raw(handle: &*mut BNCollaborationGroup) -> &Self { + assert!(!handle.is_null()); + mem::transmute(handle) + } + + #[allow(clippy::mut_from_ref)] + pub(crate) unsafe fn as_raw(&self) -> &mut BNCollaborationGroup { + &mut *self.handle.as_ptr() + } + + /// Owning Remote + pub fn remote(&self) -> Result { + let value = unsafe { BNCollaborationGroupGetRemote(self.as_raw()) }; + ptr::NonNull::new(value) + .map(|handle| unsafe { Remote::from_raw(handle) }) + .ok_or(()) + } + + /// Web api endpoint url + pub fn url(&self) -> BnString { + let value = unsafe { BNCollaborationGroupGetUrl(self.as_raw()) }; + assert!(!value.is_null()); + unsafe { BnString::from_raw(value) } + } + + /// Unique id + pub fn id(&self) -> u64 { + unsafe { BNCollaborationGroupGetId(self.as_raw()) } + } + + /// Group name + pub fn name(&self) -> BnString { + let value = unsafe { BNCollaborationGroupGetName(self.as_raw()) }; + assert!(!value.is_null()); + unsafe { BnString::from_raw(value) } + } + + /// Set group name + /// You will need to push the group to update the Remote. + pub fn set_name(&self, name: U) { + let name = name.into_bytes_with_nul(); + unsafe { + BNCollaborationGroupSetName(self.as_raw(), name.as_ref().as_ptr() as *const ffi::c_char) + } + } + + /// Get list of users in the group + pub fn users(&self) -> Result<(Array, Array), ()> { + let mut usernames = ptr::null_mut(); + let mut user_ids = ptr::null_mut(); + let mut count = 0; + let success = unsafe { + BNCollaborationGroupGetUsers(self.as_raw(), &mut user_ids, &mut usernames, &mut count) + }; + success + .then(|| unsafe { + let ids = Array::new(user_ids, count, ()); + let users = Array::new(usernames, count, ()); + (ids, users) + }) + .ok_or(()) + } + + /// Set the list of users in a group by their usernames. + /// You will need to push the group to update the Remote. + pub fn set_user(&self, usernames: I) -> Result<(), ()> + where + I: IntoIterator, + I::Item: BnStrCompatible, + { + let usernames: Vec<_> = usernames + .into_iter() + .map(|u| u.into_bytes_with_nul()) + .collect(); + let mut usernames_raw: Vec<_> = usernames + .iter() + .map(|s| s.as_ref().as_ptr() as *const ffi::c_char) + .collect(); + let success = unsafe { + BNCollaborationGroupSetUsernames( + self.as_raw(), + usernames_raw.as_mut_ptr(), + usernames_raw.len(), + ) + }; + success.then_some(()).ok_or(()) + } + + /// Test if a group has a user with the given username + pub fn contains_user(&self, username: U) -> bool { + let username = username.into_bytes_with_nul(); + unsafe { + BNCollaborationGroupContainsUser( + self.as_raw(), + username.as_ref().as_ptr() as *const ffi::c_char, + ) + } + } +} + +impl CoreArrayProvider for Group { + type Raw = *mut BNCollaborationGroup; + type Context = (); + type Wrapped<'a> = &'a Self; +} + +unsafe impl CoreArrayProviderInner for Group { + unsafe fn free(raw: *mut Self::Raw, count: usize, _context: &Self::Context) { + BNFreeCollaborationGroupList(raw, count) + } + + unsafe fn wrap_raw<'a>(raw: &'a Self::Raw, _context: &'a Self::Context) -> Self::Wrapped<'a> { + Self::ref_from_raw(raw) + } +} + +pub struct Id(pub u64); +impl Id { + pub fn as_raw(&self) -> u64 { + self.0 + } +} +impl CoreArrayProvider for Id { + type Raw = u64; + type Context = (); + type Wrapped<'a> = Self; +} + +unsafe impl CoreArrayProviderInner for Id { + unsafe fn free(raw: *mut Self::Raw, count: usize, _context: &Self::Context) { + BNCollaborationFreeIdList(raw, count) + } + + unsafe fn wrap_raw<'a>(raw: &'a Self::Raw, _context: &'a Self::Context) -> Self::Wrapped<'a> { + Id(*raw) + } +} diff --git a/rust/src/collaboration/merge.rs b/rust/src/collaboration/merge.rs new file mode 100644 index 000000000..52fad0c72 --- /dev/null +++ b/rust/src/collaboration/merge.rs @@ -0,0 +1,199 @@ +use core::{ffi, mem, ptr}; + +use binaryninjacore_sys::*; + +use crate::database::{Database, Snapshot}; +use crate::filemetadata::FileMetadata; +use crate::rc::{CoreArrayProvider, CoreArrayProviderInner, Ref}; +use crate::string::{BnStrCompatible, BnString}; + +pub type MergeConflictDataType = BNMergeConflictDataType; + +/// Structure representing an individual merge conflict +#[repr(transparent)] +pub struct MergeConflict { + handle: ptr::NonNull, +} + +impl Drop for MergeConflict { + fn drop(&mut self) { + unsafe { BNFreeAnalysisMergeConflict(self.as_raw()) } + } +} + +impl Clone for MergeConflict { + fn clone(&self) -> Self { + unsafe { + Self::from_raw( + ptr::NonNull::new(BNNewAnalysisMergeConflictReference(self.as_raw())).unwrap(), + ) + } + } +} + +impl MergeConflict { + pub(crate) unsafe fn from_raw(handle: ptr::NonNull) -> Self { + Self { handle } + } + + pub(crate) unsafe fn ref_from_raw(handle: &*mut BNAnalysisMergeConflict) -> &Self { + assert!(!handle.is_null()); + mem::transmute(handle) + } + + #[allow(clippy::mut_from_ref)] + pub(crate) unsafe fn as_raw(&self) -> &mut BNAnalysisMergeConflict { + &mut *self.handle.as_ptr() + } + + /// Database backing all snapshots in the merge conflict + pub fn database(&self) -> Database { + let result = unsafe { BNAnalysisMergeConflictGetDatabase(self.as_raw()) }; + unsafe { Database::from_raw(ptr::NonNull::new(result).unwrap()) } + } + + /// Snapshot which is the parent of the two being merged + pub fn base_snapshot(&self) -> Option { + let result = unsafe { BNAnalysisMergeConflictGetBaseSnapshot(self.as_raw()) }; + ptr::NonNull::new(result).map(|handle| unsafe { Snapshot::from_raw(handle) }) + } + + /// First snapshot being merged + pub fn first_snapshot(&self) -> Option { + let result = unsafe { BNAnalysisMergeConflictGetFirstSnapshot(self.as_raw()) }; + ptr::NonNull::new(result).map(|handle| unsafe { Snapshot::from_raw(handle) }) + } + + /// Second snapshot being merged + pub fn second_snapshot(&self) -> Option { + let result = unsafe { BNAnalysisMergeConflictGetSecondSnapshot(self.as_raw()) }; + ptr::NonNull::new(result).map(|handle| unsafe { Snapshot::from_raw(handle) }) + } + + pub fn path_item_string(&self, path: S) -> Result { + let path = path.into_bytes_with_nul(); + let result = unsafe { + BNAnalysisMergeConflictGetPathItemString( + self.as_raw(), + path.as_ref().as_ptr() as *const ffi::c_char, + ) + }; + (!result.is_null()) + .then(|| unsafe { BnString::from_raw(result) }) + .ok_or(()) + } + + /// FileMetadata with contents of file for base snapshot + /// This function is slow! Only use it if you really need it. + pub fn base_file(&self) -> Option> { + let result = unsafe { BNAnalysisMergeConflictGetBaseFile(self.as_raw()) }; + (!result.is_null()).then(|| unsafe { Ref::new(FileMetadata::from_raw(result)) }) + } + + /// FileMetadata with contents of file for first snapshot + /// This function is slow! Only use it if you really need it. + pub fn first_file(&self) -> Option> { + let result = unsafe { BNAnalysisMergeConflictGetFirstFile(self.as_raw()) }; + (!result.is_null()).then(|| unsafe { Ref::new(FileMetadata::from_raw(result)) }) + } + + /// FileMetadata with contents of file for second snapshot + /// This function is slow! Only use it if you really need it. + pub fn second_file(&self) -> Option> { + let result = unsafe { BNAnalysisMergeConflictGetSecondFile(self.as_raw()) }; + (!result.is_null()).then(|| unsafe { Ref::new(FileMetadata::from_raw(result)) }) + } + + /// Json String for conflicting data in the base snapshot + pub fn base(&self) -> Option { + let result = unsafe { BNAnalysisMergeConflictGetBase(self.as_raw()) }; + (!result.is_null()).then(|| unsafe { BnString::from_raw(result) }) + } + + /// Json object for conflicting data in the base snapshot + pub fn first(&self) -> Option { + let result = unsafe { BNAnalysisMergeConflictGetFirst(self.as_raw()) }; + (!result.is_null()).then(|| unsafe { BnString::from_raw(result) }) + } + + /// Json object for conflicting data in the second snapshot + pub fn second(&self) -> Option { + let result = unsafe { BNAnalysisMergeConflictGetSecond(self.as_raw()) }; + (!result.is_null()).then(|| unsafe { BnString::from_raw(result) }) + } + + /// Type of data in the conflict, Text/Json/Binary + pub fn data_type(&self) -> MergeConflictDataType { + unsafe { BNAnalysisMergeConflictGetDataType(self.as_raw()) } + } + + /// String representing the type name of the data, not the same as data_type. + /// This is like "typeName" or "tag" depending on what object the conflict represents. + pub fn conflict_type(&self) -> BnString { + let result = unsafe { BNAnalysisMergeConflictGetType(self.as_raw()) }; + assert!(!result.is_null()); + unsafe { BnString::from_raw(result) } + } + + /// Lookup key for the merge conflict, ideally a tree path that contains the name of the conflict + /// and all the recursive children leading up to this conflict. + pub fn key(&self) -> BnString { + let result = unsafe { BNAnalysisMergeConflictGetKey(self.as_raw()) }; + assert!(!result.is_null()); + unsafe { BnString::from_raw(result) } + } + + /// Call this when you've resolved the conflict to save the result + pub fn success(&self, value: S) -> Result<(), ()> { + let value = value.into_bytes_with_nul(); + let success = unsafe { + BNAnalysisMergeConflictSuccess( + self.as_raw(), + value.as_ref().as_ptr() as *const ffi::c_char, + ) + }; + success.then_some(()).ok_or(()) + } + + fn get_path_item_inner(&self, path_key: S) -> *mut ffi::c_void { + let path_key = path_key.into_bytes_with_nul(); + unsafe { + BNAnalysisMergeConflictGetPathItem( + self.as_raw(), + path_key.as_ref().as_ptr() as *const ffi::c_char, + ) + } + } + + // TODO - How to downcast into usize/u64? How to free the original pointer? It's unclear how to handle + // this correctly + //pub fn get_path_item_number(&self, path_key: S) -> Result { + // Ok(self.get_path_item_inner(path_key) as usize) + //} + + pub unsafe fn get_path_item_string( + &self, + path_key: S, + ) -> Result { + let value = self.get_path_item_inner(path_key); + (!value.is_null()) + .then(|| unsafe { BnString::from_raw(value as *mut ffi::c_char) }) + .ok_or(()) + } +} + +impl CoreArrayProvider for MergeConflict { + type Raw = *mut BNAnalysisMergeConflict; + type Context = (); + type Wrapped<'a> = &'a Self; +} + +unsafe impl CoreArrayProviderInner for MergeConflict { + unsafe fn free(raw: *mut Self::Raw, count: usize, _context: &Self::Context) { + BNFreeAnalysisMergeConflictList(raw, count) + } + + unsafe fn wrap_raw<'a>(raw: &'a Self::Raw, _context: &'a Self::Context) -> Self::Wrapped<'a> { + Self::ref_from_raw(raw) + } +} diff --git a/rust/src/collaboration/mod.rs b/rust/src/collaboration/mod.rs new file mode 100644 index 000000000..bcab202cb --- /dev/null +++ b/rust/src/collaboration/mod.rs @@ -0,0 +1,151 @@ +mod changeset; +mod databasesync; +mod file; +mod folder; +mod group; +mod merge; +mod permission; +mod project; +mod remote; +mod snapshot; +mod user; + +pub use changeset::*; +pub use databasesync::*; +pub use file::*; +pub use folder::*; +pub use group::*; +pub use merge::*; +pub use permission::*; +pub use project::*; +pub use remote::*; +pub use snapshot::*; +pub use user::*; + +use core::{ffi, ptr}; + +use binaryninjacore_sys::*; + +use crate::rc::Array; +use crate::string::{BnStrCompatible, BnString}; + +// TODO it's unclear where should preventivelly call things like `open`, `pull_files`, `pull_folders`, etc +// and where should let the user do it. + +/// Get the single actively connected Remote (for ux simplification), if any +pub fn active_remote() -> Option { + let value = unsafe { BNCollaborationGetActiveRemote() }; + ptr::NonNull::new(value).map(|h| unsafe { Remote::from_raw(h) }) +} + +/// Set the single actively connected Remote +pub fn set_active_remote(remote: Option<&Remote>) { + let remote_ptr = remote.map_or(ptr::null_mut(), |r| unsafe { r.as_raw() } as *mut _); + unsafe { BNCollaborationSetActiveRemote(remote_ptr) } +} + +pub fn store_data_in_keychain(key: K, data: I) -> bool +where + K: BnStrCompatible, + I: IntoIterator, + DK: BnStrCompatible, + DV: BnStrCompatible, +{ + let key = key.into_bytes_with_nul(); + let (data_keys, data_values): (Vec, Vec) = data + .into_iter() + .map(|(k, v)| (k.into_bytes_with_nul(), v.into_bytes_with_nul())) + .unzip(); + let data_keys_ptr: Box<[*const ffi::c_char]> = data_keys + .iter() + .map(|k| k.as_ref().as_ptr() as *const ffi::c_char) + .collect(); + let data_values_ptr: Box<[*const ffi::c_char]> = data_values + .iter() + .map(|v| v.as_ref().as_ptr() as *const ffi::c_char) + .collect(); + unsafe { + BNCollaborationStoreDataInKeychain( + key.as_ref().as_ptr() as *const ffi::c_char, + data_keys_ptr.as_ptr() as *mut _, + data_values_ptr.as_ptr() as *mut _, + data_keys.len(), + ) + } +} + +pub fn has_data_in_keychain(key: K) -> bool { + let key = key.into_bytes_with_nul(); + unsafe { BNCollaborationHasDataInKeychain(key.as_ref().as_ptr() as *const ffi::c_char) } +} + +pub fn get_data_from_keychain( + key: K, +) -> Option<(Array, Array)> { + let key = key.into_bytes_with_nul(); + let mut keys = ptr::null_mut(); + let mut values = ptr::null_mut(); + let count = unsafe { + BNCollaborationGetDataFromKeychain( + key.as_ref().as_ptr() as *const ffi::c_char, + &mut keys, + &mut values, + ) + }; + let keys = (!keys.is_null()).then(|| unsafe { Array::new(keys, count, ()) }); + let values = (!values.is_null()).then(|| unsafe { Array::new(values, count, ()) }); + keys.zip(values) +} + +pub fn delete_data_from_keychain(key: K) -> bool { + let key = key.into_bytes_with_nul(); + unsafe { BNCollaborationDeleteDataFromKeychain(key.as_ref().as_ptr() as *const ffi::c_char) } +} + +/// Load the list of known Remotes from local Settings +pub fn load_remotes() -> Result<(), ()> { + let success = unsafe { BNCollaborationLoadRemotes() }; + success.then_some(()).ok_or(()) +} + +/// List of known/connected Remotes +pub fn known_remotes() -> Array { + let mut count = 0; + let value = unsafe { BNCollaborationGetRemotes(&mut count) }; + assert!(!value.is_null()); + unsafe { Array::new(value, count, ()) } +} + +/// Get Remote by unique `id` +pub fn get_remote_by_id(id: S) -> Option { + let id = id.into_bytes_with_nul(); + let value = unsafe { BNCollaborationGetRemoteById(id.as_ref().as_ptr() as *const ffi::c_char) }; + ptr::NonNull::new(value).map(|h| unsafe { Remote::from_raw(h) }) +} + +/// Get Remote by `address` +pub fn get_remote_by_address(address: S) -> Option { + let address = address.into_bytes_with_nul(); + let value = unsafe { + BNCollaborationGetRemoteByAddress(address.as_ref().as_ptr() as *const ffi::c_char) + }; + ptr::NonNull::new(value).map(|h| unsafe { Remote::from_raw(h) }) +} + +/// Get Remote by `name` +pub fn get_remote_by_name(name: S) -> Option { + let name = name.into_bytes_with_nul(); + let value = + unsafe { BNCollaborationGetRemoteByName(name.as_ref().as_ptr() as *const ffi::c_char) }; + ptr::NonNull::new(value).map(|h| unsafe { Remote::from_raw(h) }) +} + +/// Remove a Remote from the list of known remotes (saved to Settings) +pub fn remove_known_remote(remote: &Remote) { + unsafe { BNCollaborationRemoveRemote(remote.as_raw()) } +} + +/// Save the list of known Remotes to local Settings +pub fn save_remotes() { + unsafe { BNCollaborationSaveRemotes() } +} diff --git a/rust/src/collaboration/permission.rs b/rust/src/collaboration/permission.rs new file mode 100644 index 000000000..0b27fe382 --- /dev/null +++ b/rust/src/collaboration/permission.rs @@ -0,0 +1,157 @@ +use core::{mem, ptr}; + +use std::num::NonZeroU64; + +use binaryninjacore_sys::*; + +use super::{Remote, RemoteProject}; + +use crate::rc::{CoreArrayProvider, CoreArrayProviderInner}; +use crate::string::BnString; + +pub type CollaborationPermissionLevel = BNCollaborationPermissionLevel; + +/// Struct representing a permission grant for a user or group on a project. +#[repr(transparent)] +pub struct Permission { + handle: ptr::NonNull, +} + +impl Drop for Permission { + fn drop(&mut self) { + unsafe { BNFreeCollaborationPermission(self.as_raw()) } + } +} + +impl PartialEq for Permission { + fn eq(&self, other: &Self) -> bool { + self.id() == other.id() + } +} +impl Eq for Permission {} + +impl Clone for Permission { + fn clone(&self) -> Self { + unsafe { + Self::from_raw( + ptr::NonNull::new(BNNewCollaborationPermissionReference(self.as_raw())).unwrap(), + ) + } + } +} + +impl Permission { + pub(crate) unsafe fn from_raw(handle: ptr::NonNull) -> Self { + Self { handle } + } + + pub(crate) unsafe fn ref_from_raw(handle: &*mut BNCollaborationPermission) -> &Self { + assert!(!handle.is_null()); + mem::transmute(handle) + } + + #[allow(clippy::mut_from_ref)] + pub(crate) unsafe fn as_raw(&self) -> &mut BNCollaborationPermission { + &mut *self.handle.as_ptr() + } + + pub fn remote(&self) -> Result { + let result = unsafe { BNCollaborationPermissionGetRemote(self.as_raw()) }; + ptr::NonNull::new(result) + .map(|handle| unsafe { Remote::from_raw(handle) }) + .ok_or(()) + } + + pub fn project(&self) -> Result { + let result = unsafe { BNCollaborationPermissionGetProject(self.as_raw()) }; + ptr::NonNull::new(result) + .map(|handle| unsafe { RemoteProject::from_raw(handle) }) + .ok_or(()) + } + + /// Web api endpoint url + pub fn url(&self) -> BnString { + let value = unsafe { BNCollaborationPermissionGetUrl(self.as_raw()) }; + assert!(!value.is_null()); + unsafe { BnString::from_raw(value) } + } + + /// unique id + pub fn id(&self) -> BnString { + let value = unsafe { BNCollaborationPermissionGetId(self.as_raw()) }; + assert!(!value.is_null()); + unsafe { BnString::from_raw(value) } + } + + /// Level of permission + pub fn level(&self) -> CollaborationPermissionLevel { + unsafe { BNCollaborationPermissionGetLevel(self.as_raw()) } + } + + /// Change the level of the permission + /// You will need to push the group to update the Remote. + pub fn set_level(&self, level: CollaborationPermissionLevel) { + unsafe { BNCollaborationPermissionSetLevel(self.as_raw(), level) } + } + + /// Id of affected group + pub fn group_id(&self) -> Option { + let value = unsafe { BNCollaborationPermissionGetGroupId(self.as_raw()) }; + NonZeroU64::new(value) + } + + /// Name of affected group + pub fn group_name(&self) -> Option { + let value = unsafe { BNCollaborationPermissionGetGroupName(self.as_raw()) }; + assert!(!value.is_null()); + let result = unsafe { BnString::from_raw(value) }; + (!result.is_empty()).then_some(result) + } + + /// Id of affected user + pub fn user_id(&self) -> Option { + let value = unsafe { BNCollaborationPermissionGetUserId(self.as_raw()) }; + assert!(!value.is_null()); + let result = unsafe { BnString::from_raw(value) }; + (!result.is_empty()).then_some(result) + } + + /// Name of affected user + pub fn username(&self) -> Option { + let value = unsafe { BNCollaborationPermissionGetUsername(self.as_raw()) }; + assert!(!value.is_null()); + let result = unsafe { BnString::from_raw(value) }; + (!result.is_empty()).then_some(result) + } + + /// If the permission grants the affect user/group the ability to read files in the project + pub fn can_view(&self) -> bool { + unsafe { BNCollaborationPermissionCanView(self.as_raw()) } + } + + /// If the permission grants the affect user/group the ability to edit files in the project + pub fn can_edit(&self) -> bool { + unsafe { BNCollaborationPermissionCanEdit(self.as_raw()) } + } + + /// If the permission grants the affect user/group the ability to administer the project + pub fn can_admin(&self) -> bool { + unsafe { BNCollaborationPermissionCanAdmin(self.as_raw()) } + } +} + +impl CoreArrayProvider for Permission { + type Raw = *mut BNCollaborationPermission; + type Context = (); + type Wrapped<'a> = &'a Self; +} + +unsafe impl CoreArrayProviderInner for Permission { + unsafe fn free(raw: *mut Self::Raw, count: usize, _context: &Self::Context) { + BNFreeCollaborationPermissionList(raw, count) + } + + unsafe fn wrap_raw<'a>(raw: &'a Self::Raw, _context: &'a Self::Context) -> Self::Wrapped<'a> { + Self::ref_from_raw(raw) + } +} diff --git a/rust/src/collaboration/project.rs b/rust/src/collaboration/project.rs new file mode 100644 index 000000000..c839c0716 --- /dev/null +++ b/rust/src/collaboration/project.rs @@ -0,0 +1,822 @@ +use core::{ffi, mem, ptr}; + +use std::time::SystemTime; + +use binaryninjacore_sys::*; + +use super::{ + databasesync, CollaborationPermissionLevel, NameChangeset, Permission, Remote, RemoteFile, + RemoteFileType, RemoteFolder, +}; + +use crate::binaryview::{BinaryView, BinaryViewExt}; +use crate::database::Database; +use crate::ffi::{ProgressCallback, ProgressCallbackNop}; +use crate::filemetadata::FileMetadata; +use crate::project::Project; +use crate::rc::{Array, CoreArrayProvider, CoreArrayProviderInner}; +use crate::string::{BnStrCompatible, BnString}; + +/// Struct representing a remote project +#[repr(transparent)] +pub struct RemoteProject { + handle: ptr::NonNull, +} + +impl Drop for RemoteProject { + fn drop(&mut self) { + unsafe { BNFreeRemoteProject(self.as_raw()) } + } +} + +impl PartialEq for RemoteProject { + fn eq(&self, other: &Self) -> bool { + self.id() == other.id() + } +} +impl Eq for RemoteProject {} + +impl Clone for RemoteProject { + fn clone(&self) -> Self { + unsafe { + Self::from_raw(ptr::NonNull::new(BNNewRemoteProjectReference(self.as_raw())).unwrap()) + } + } +} + +impl RemoteProject { + pub(crate) unsafe fn from_raw(handle: ptr::NonNull) -> Self { + Self { handle } + } + + pub(crate) unsafe fn ref_from_raw(handle: &*mut BNRemoteProject) -> &Self { + assert!(!handle.is_null()); + mem::transmute(handle) + } + + #[allow(clippy::mut_from_ref)] + pub(crate) unsafe fn as_raw(&self) -> &mut BNRemoteProject { + &mut *self.handle.as_ptr() + } + + /// Determine if the project is open (it needs to be opened before you can access its files) + pub fn is_open(&self) -> bool { + unsafe { BNRemoteProjectIsOpen(self.as_raw()) } + } + + /// Open the project, allowing various file and folder based apis to work, as well as + /// connecting a core Project + pub fn open(&self, mut progress: F) -> Result<(), ()> { + if self.is_open() { + return Ok(()); + } + let success = unsafe { + BNRemoteProjectOpen( + self.as_raw(), + Some(F::cb_progress_callback), + &mut progress as *mut F as *mut ffi::c_void, + ) + }; + success.then_some(()).ok_or(()) + } + + /// Close the project and stop all background operations (e.g. file uploads) + pub fn close(&self) { + unsafe { BNRemoteProjectClose(self.as_raw()) } + } + + /// Get the Remote Project for a Database + pub fn get_for_local_database(database: &Database) -> Result, ()> { + if databasesync::pull_projects(database)? { + return Ok(None); + } + databasesync::get_remote_project_for_local_database(database) + } + + /// Get the Remote Project for a BinaryView + pub fn get_for_binaryview(bv: &BinaryView) -> Result, ()> { + let file = bv.file(); + let Some(database) = file.database() else { + return Ok(None); + }; + Self::get_for_local_database(&database) + } + + /// Get the core [Project] for the remote project. + /// + /// NOTE: If the project has not been opened, it will be opened upon calling this. + pub fn core_project(&self) -> Result { + self.open(ProgressCallbackNop)?; + + let value = unsafe { BNRemoteProjectGetCoreProject(self.as_raw()) }; + ptr::NonNull::new(value) + .map(|handle| unsafe { Project::from_raw(handle) }) + .ok_or(()) + } + + /// Get the owning remote + pub fn remote(&self) -> Result { + let value = unsafe { BNRemoteProjectGetRemote(self.as_raw()) }; + ptr::NonNull::new(value) + .map(|handle| unsafe { Remote::from_raw(handle) }) + .ok_or(()) + } + + /// Get the URL of the project + pub fn url(&self) -> BnString { + let result = unsafe { BNRemoteProjectGetUrl(self.as_raw()) }; + assert!(!result.is_null()); + unsafe { BnString::from_raw(result) } + } + + /// Get the unique ID of the project + pub fn id(&self) -> BnString { + let result = unsafe { BNRemoteProjectGetId(self.as_raw()) }; + assert!(!result.is_null()); + unsafe { BnString::from_raw(result) } + } + + /// Created date of the project + pub fn created(&self) -> SystemTime { + let result = unsafe { BNRemoteProjectGetCreated(self.as_raw()) }; + crate::ffi::time_from_bn(result.try_into().unwrap()) + } + + /// Last modification of the project + pub fn last_modified(&self) -> SystemTime { + let result = unsafe { BNRemoteProjectGetLastModified(self.as_raw()) }; + crate::ffi::time_from_bn(result.try_into().unwrap()) + } + + /// Displayed name of file + pub fn name(&self) -> BnString { + let result = unsafe { BNRemoteProjectGetName(self.as_raw()) }; + assert!(!result.is_null()); + unsafe { BnString::from_raw(result) } + } + + /// Set the description of the file. You will need to push the file to update the remote version. + pub fn set_name(&self, name: S) -> Result<(), ()> { + let name = name.into_bytes_with_nul(); + let success = unsafe { + BNRemoteProjectSetName(self.as_raw(), name.as_ref().as_ptr() as *const ffi::c_char) + }; + success.then_some(()).ok_or(()) + } + + /// Desciprtion of the file + pub fn description(&self) -> BnString { + let result = unsafe { BNRemoteProjectGetDescription(self.as_raw()) }; + assert!(!result.is_null()); + unsafe { BnString::from_raw(result) } + } + + /// Set the description of the file. You will need to push the file to update the remote version. + pub fn set_description(&self, description: S) -> Result<(), ()> { + let description = description.into_bytes_with_nul(); + let success = unsafe { + BNRemoteProjectSetDescription( + self.as_raw(), + description.as_ref().as_ptr() as *const ffi::c_char, + ) + }; + success.then_some(()).ok_or(()) + } + + /// Get the number of files in a project (without needing to pull them first) + pub fn received_file_count(&self) -> u64 { + unsafe { BNRemoteProjectGetReceivedFileCount(self.as_raw()) } + } + + /// Get the number of folders in a project (without needing to pull them first) + pub fn received_folder_count(&self) -> u64 { + unsafe { BNRemoteProjectGetReceivedFolderCount(self.as_raw()) } + } + + /// Get the default directory path for a remote Project. This is based off the Setting for + /// collaboration.directory, the project's id, and the project's remote's id. + pub fn default_path(&self) -> Result { + databasesync::default_project_path(self) + } + + /// If the project has pulled the folders yet + pub fn has_pulled_files(&self) -> bool { + unsafe { BNRemoteProjectHasPulledFiles(self.as_raw()) } + } + + /// If the project has pulled the folders yet + pub fn has_pulled_folders(&self) -> bool { + unsafe { BNRemoteProjectHasPulledFolders(self.as_raw()) } + } + + /// If the project has pulled the group permissions yet + pub fn has_pulled_group_permissions(&self) -> bool { + unsafe { BNRemoteProjectHasPulledGroupPermissions(self.as_raw()) } + } + + /// If the project has pulled the user permissions yet + pub fn has_pulled_user_permissions(&self) -> bool { + unsafe { BNRemoteProjectHasPulledUserPermissions(self.as_raw()) } + } + + /// If the currently logged in user is an administrator of the project (and can edit + /// permissions and such for the project). + pub fn is_admin(&self) -> bool { + unsafe { BNRemoteProjectIsAdmin(self.as_raw()) } + } + + /// Get the list of files in this project. + /// + /// NOTE: If the project has not been opened, it will be opened upon calling this. + /// NOTE: If folders have not been pulled, they will be pulled upon calling this. + /// NOTE: If files have not been pulled, they will be pulled upon calling this. + pub fn files(&self) -> Result, ()> { + if !self.has_pulled_files() { + self.pull_files(ProgressCallbackNop)?; + } + + let mut count = 0; + let result = unsafe { BNRemoteProjectGetFiles(self.as_raw(), &mut count) }; + (!result.is_null()) + .then(|| unsafe { Array::new(result, count, ()) }) + .ok_or(()) + } + + /// Get a specific File in the Project by its id + /// + /// NOTE: If the project has not been opened, it will be opened upon calling this. + /// NOTE: If files have not been pulled, they will be pulled upon calling this. + pub fn get_file_by_id(&self, id: S) -> Result, ()> { + if !self.has_pulled_files() { + self.pull_files(ProgressCallbackNop)?; + } + let id = id.into_bytes_with_nul(); + let result = unsafe { + BNRemoteProjectGetFileById(self.as_raw(), id.as_ref().as_ptr() as *const ffi::c_char) + }; + Ok(ptr::NonNull::new(result).map(|handle| unsafe { RemoteFile::from_raw(handle) })) + } + + /// Get a specific File in the Project by its name + /// + /// NOTE: If the project has not been opened, it will be opened upon calling this. + /// NOTE: If files have not been pulled, they will be pulled upon calling this. + pub fn get_file_by_name(&self, name: S) -> Result, ()> { + if !self.has_pulled_files() { + self.pull_files(ProgressCallbackNop)?; + } + let id = name.into_bytes_with_nul(); + let result = unsafe { + BNRemoteProjectGetFileByName(self.as_raw(), id.as_ref().as_ptr() as *const ffi::c_char) + }; + Ok(ptr::NonNull::new(result).map(|handle| unsafe { RemoteFile::from_raw(handle) })) + } + + /// Pull the list of files from the Remote. + /// + /// NOTE: If the project has not been opened, it will be opened upon calling this. + /// NOTE: If folders have not been pulled, they will be pulled upon calling this. + pub fn pull_files(&self, mut progress: P) -> Result<(), ()> { + if !self.has_pulled_folders() { + self.pull_folders(ProgressCallbackNop)?; + } + let success = unsafe { + BNRemoteProjectPullFiles( + self.as_raw(), + Some(P::cb_progress_callback), + &mut progress as *mut P as *mut ffi::c_void, + ) + }; + success.then_some(()).ok_or(()) + } + + /// Create a new file on the remote and return a reference to the created file + /// + /// NOTE: If the project has not been opened, it will be opened upon calling this. + /// + /// * `filename` - File name + /// * `contents` - File contents + /// * `name` - Displayed file name + /// * `description` - File description + /// * `parent_folder` - Folder that will contain the file + /// * `file_type` - Type of File to create + /// * `progress` - Function to call on upload progress updates + pub fn create_file( + &self, + filename: F, + contents: &[u8], + name: N, + description: D, + parent_folder: Option<&RemoteFolder>, + file_type: RemoteFileType, + mut progress: P, + ) -> Result + where + F: BnStrCompatible, + N: BnStrCompatible, + D: BnStrCompatible, + P: ProgressCallback, + { + self.open(ProgressCallbackNop)?; + + let filename = filename.into_bytes_with_nul(); + let name = name.into_bytes_with_nul(); + let description = description.into_bytes_with_nul(); + let folder_handle = + parent_folder.map_or(ptr::null_mut(), |f| unsafe { f.as_raw() } as *mut _); + let file_ptr = unsafe { + BNRemoteProjectCreateFile( + self.as_raw(), + filename.as_ref().as_ptr() as *const ffi::c_char, + contents.as_ptr() as *mut _, + contents.len(), + name.as_ref().as_ptr() as *const ffi::c_char, + description.as_ref().as_ptr() as *const ffi::c_char, + folder_handle, + file_type, + Some(P::cb_progress_callback), + &mut progress as *mut P as *mut ffi::c_void, + ) + }; + + ptr::NonNull::new(file_ptr) + .map(|handle| unsafe { RemoteFile::from_raw(handle) }) + .ok_or(()) + } + + /// Push an updated File object to the Remote + /// + /// NOTE: If the project has not been opened, it will be opened upon calling this. + pub fn push_file(&self, file: &RemoteFile, extra_fields: I) -> Result<(), ()> + where + I: Iterator, + K: BnStrCompatible, + V: BnStrCompatible, + { + self.open(ProgressCallbackNop)?; + + let (keys, values): (Vec<_>, Vec<_>) = extra_fields + .into_iter() + .map(|(k, v)| (k.into_bytes_with_nul(), v.into_bytes_with_nul())) + .unzip(); + let mut keys_raw = keys + .iter() + .map(|s| s.as_ref().as_ptr() as *const ffi::c_char) + .collect::>(); + let mut values_raw = values + .iter() + .map(|s| s.as_ref().as_ptr() as *const ffi::c_char) + .collect::>(); + let success = unsafe { + BNRemoteProjectPushFile( + self.as_raw(), + file.as_raw(), + keys_raw.as_mut_ptr(), + values_raw.as_mut_ptr(), + keys_raw.len(), + ) + }; + success.then_some(()).ok_or(()) + } + + pub fn delete_file(&self, file: &RemoteFile) -> Result<(), ()> { + self.open(ProgressCallbackNop)?; + + let success = unsafe { BNRemoteProjectDeleteFile(self.as_raw(), file.as_raw()) }; + success.then_some(()).ok_or(()) + } + + /// Get the list of folders in this project. + /// + /// NOTE: If the project has not been opened, it will be opened upon calling this. + /// NOTE: If folders have not been pulled, they will be pulled upon calling this. + pub fn folders(&self) -> Result, ()> { + if !self.has_pulled_folders() { + self.pull_folders(ProgressCallbackNop)?; + } + let mut count = 0; + let result = unsafe { BNRemoteProjectGetFolders(self.as_raw(), &mut count) }; + if result.is_null() { + return Err(()); + } + Ok(unsafe { Array::new(result, count, ()) }) + } + + /// Get a specific Folder in the Project by its id + /// + /// NOTE: If the project has not been opened, it will be opened upon calling this. + /// NOTE: If folders have not been pulled, they will be pulled upon calling this. + pub fn get_folder_by_id(&self, id: S) -> Result, ()> { + if !self.has_pulled_folders() { + self.pull_folders(ProgressCallbackNop)?; + } + let id = id.into_bytes_with_nul(); + let result = unsafe { + BNRemoteProjectGetFolderById(self.as_raw(), id.as_ref().as_ptr() as *const ffi::c_char) + }; + Ok(ptr::NonNull::new(result).map(|handle| unsafe { RemoteFolder::from_raw(handle) })) + } + + /// Pull the list of folders from the Remote. + /// + /// NOTE: If the project has not been opened, it will be opened upon calling this. + pub fn pull_folders(&self, mut progress: P) -> Result<(), ()> { + self.open(ProgressCallbackNop)?; + + let success = unsafe { + BNRemoteProjectPullFolders( + self.as_raw(), + Some(P::cb_progress_callback), + &mut progress as *mut P as *mut ffi::c_void, + ) + }; + success.then_some(()).ok_or(()) + } + + /// Create a new folder on the remote (and pull it) + /// + /// NOTE: If the project has not been opened, it will be opened upon calling this. + /// + /// * `name` - Displayed folder name + /// * `description` - Folder description + /// * `parent` - Parent folder (optional) + /// * `progress` - Function to call on upload progress updates + pub fn create_folders( + &self, + name: N, + description: D, + parent_folder: Option<&RemoteFolder>, + mut progress: P, + ) -> Result + where + N: BnStrCompatible, + D: BnStrCompatible, + P: ProgressCallback, + { + self.open(ProgressCallbackNop)?; + + let name = name.into_bytes_with_nul(); + let description = description.into_bytes_with_nul(); + let folder_handle = + parent_folder.map_or(ptr::null_mut(), |f| unsafe { f.as_raw() } as *mut _); + let file_ptr = unsafe { + BNRemoteProjectCreateFolder( + self.as_raw(), + name.as_ref().as_ptr() as *const ffi::c_char, + description.as_ref().as_ptr() as *const ffi::c_char, + folder_handle, + Some(P::cb_progress_callback), + &mut progress as *mut P as *mut ffi::c_void, + ) + }; + + ptr::NonNull::new(file_ptr) + .map(|handle| unsafe { RemoteFolder::from_raw(handle) }) + .ok_or(()) + } + + /// Push an updated Folder object to the Remote + /// + /// NOTE: If the project has not been opened, it will be opened upon calling this. + /// + /// * `folder` - Folder object which has been updated + /// * `extra_fields` - Extra HTTP fields to send with the update + pub fn push_folder(&self, folder: &RemoteFolder, extra_fields: I) -> Result<(), ()> + where + I: Iterator, + K: BnStrCompatible, + V: BnStrCompatible, + { + self.open(ProgressCallbackNop)?; + + let (keys, values): (Vec<_>, Vec<_>) = extra_fields + .into_iter() + .map(|(k, v)| (k.into_bytes_with_nul(), v.into_bytes_with_nul())) + .unzip(); + let mut keys_raw = keys + .iter() + .map(|s| s.as_ref().as_ptr() as *const ffi::c_char) + .collect::>(); + let mut values_raw = values + .iter() + .map(|s| s.as_ref().as_ptr() as *const ffi::c_char) + .collect::>(); + let success = unsafe { + BNRemoteProjectPushFolder( + self.as_raw(), + folder.as_raw(), + keys_raw.as_mut_ptr(), + values_raw.as_mut_ptr(), + keys_raw.len(), + ) + }; + success.then_some(()).ok_or(()) + } + + /// Delete a folder from the remote + /// + /// NOTE: If the project has not been opened, it will be opened upon calling this. + pub fn delete_folder(&self, file: &RemoteFolder) -> Result<(), ()> { + self.open(ProgressCallbackNop)?; + + let success = unsafe { BNRemoteProjectDeleteFolder(self.as_raw(), file.as_raw()) }; + success.then_some(()).ok_or(()) + } + + /// Get the list of group permissions in this project. + /// + /// NOTE: If group permissions have not been pulled, they will be pulled upon calling this. + pub fn group_permissions(&self) -> Result, ()> { + if !self.has_pulled_group_permissions() { + self.pull_group_permissions(ProgressCallbackNop)?; + } + + let mut count: usize = 0; + let value = unsafe { BNRemoteProjectGetGroupPermissions(self.handle.as_ptr(), &mut count) }; + assert!(!value.is_null()); + Ok(unsafe { Array::new(value, count, ()) }) + } + + /// Get the list of user permissions in this project. + /// + /// NOTE: If user permissions have not been pulled, they will be pulled upon calling this. + pub fn user_permissions(&self) -> Result, ()> { + if !self.has_pulled_user_permissions() { + self.pull_user_permissions(ProgressCallbackNop)?; + } + + let mut count: usize = 0; + let value = unsafe { BNRemoteProjectGetUserPermissions(self.handle.as_ptr(), &mut count) }; + assert!(!value.is_null()); + Ok(unsafe { Array::new(value, count, ()) }) + } + + /// Get a specific permission in the Project by its id. + /// + /// NOTE: If group or user permissions have not been pulled, they will be pulled upon calling this. + pub fn get_permission_by_id( + &self, + id: S, + ) -> Result, ()> { + if !self.has_pulled_user_permissions() { + self.pull_user_permissions(ProgressCallbackNop)?; + } + + if !self.has_pulled_group_permissions() { + self.pull_group_permissions(ProgressCallbackNop)?; + } + + let id = id.into_bytes_with_nul(); + let value = unsafe { + BNRemoteProjectGetPermissionById(self.as_raw(), id.as_ref().as_ptr() as *const _) + }; + Ok(ptr::NonNull::new(value).map(|v| unsafe { Permission::from_raw(v) })) + } + + /// Pull the list of group permissions from the Remote. + pub fn pull_group_permissions(&self, mut progress: F) -> Result<(), ()> { + let success = unsafe { + BNRemoteProjectPullGroupPermissions( + self.as_raw(), + Some(F::cb_progress_callback), + &mut progress as *mut F as *mut ffi::c_void, + ) + }; + success.then_some(()).ok_or(()) + } + + /// Pull the list of user permissions from the Remote. + pub fn pull_user_permissions(&self, mut progress: F) -> Result<(), ()> { + let success = unsafe { + BNRemoteProjectPullUserPermissions( + self.as_raw(), + Some(F::cb_progress_callback), + &mut progress as *mut F as *mut ffi::c_void, + ) + }; + success.then_some(()).ok_or(()) + } + + /// Create a new group permission on the remote (and pull it). + /// + /// # Arguments + /// + /// * `group_id` - Group id + /// * `level` - Permission level + /// * `progress` - Function to call for upload progress updates + pub fn create_group_permission( + &self, + group_id: i64, + level: CollaborationPermissionLevel, + mut progress: F, + ) -> Result { + let value = unsafe { + BNRemoteProjectCreateGroupPermission( + self.as_raw(), + group_id, + level, + Some(F::cb_progress_callback), + &mut progress as *mut F as *mut ffi::c_void, + ) + }; + + ptr::NonNull::new(value) + .map(|v| unsafe { Permission::from_raw(v) }) + .ok_or(()) + } + + /// Create a new user permission on the remote (and pull it). + /// + /// # Arguments + /// + /// * `user_id` - User id + /// * `level` - Permission level + pub fn create_user_permission( + &self, + user_id: S, + level: CollaborationPermissionLevel, + mut progress: F, + ) -> Result { + let user_id = user_id.into_bytes_with_nul(); + let value = unsafe { + BNRemoteProjectCreateUserPermission( + self.as_raw(), + user_id.as_ref().as_ptr() as *const ffi::c_char, + level, + Some(F::cb_progress_callback), + &mut progress as *mut F as *mut ffi::c_void, + ) + }; + + ptr::NonNull::new(value) + .map(|v| unsafe { Permission::from_raw(v) }) + .ok_or(()) + } + + /// Push project permissions to the remote. + /// + /// # Arguments + /// + /// * `permission` - Permission object which has been updated + /// * `extra_fields` - Extra HTTP fields to send with the update + pub fn push_permission( + &self, + permission: &Permission, + extra_fields: I, + ) -> Result<(), ()> + where + I: Iterator, + K: BnStrCompatible, + V: BnStrCompatible, + { + let (keys, values): (Vec<_>, Vec<_>) = extra_fields + .into_iter() + .map(|(k, v)| (k.into_bytes_with_nul(), v.into_bytes_with_nul())) + .unzip(); + let mut keys_raw = keys + .iter() + .map(|s| s.as_ref().as_ptr() as *const ffi::c_char) + .collect::>(); + let mut values_raw = values + .iter() + .map(|s| s.as_ref().as_ptr() as *const ffi::c_char) + .collect::>(); + + let success = unsafe { + BNRemoteProjectPushPermission( + self.as_raw(), + permission.as_raw(), + keys_raw.as_mut_ptr(), + values_raw.as_mut_ptr(), + keys_raw.len(), + ) + }; + success.then_some(()).ok_or(()) + } + + /// Delete a permission from the remote. + pub fn delete_permission(&self, permission: &Permission) -> Result<(), ()> { + let success = + unsafe { BNRemoteProjectDeletePermission(self.as_raw(), permission.as_raw()) }; + success.then_some(()).ok_or(()) + } + + /// Determine if a user is in any of the view/edit/admin groups. + /// + /// # Arguments + /// + /// * `username` - Username of user to check + pub fn can_user_view(&self, username: S) -> bool { + let username = username.into_bytes_with_nul(); + unsafe { + BNRemoteProjectCanUserView( + self.as_raw(), + username.as_ref().as_ptr() as *const ffi::c_char, + ) + } + } + + /// Determine if a user is in any of the edit/admin groups. + /// + /// # Arguments + /// + /// * `username` - Username of user to check + pub fn can_user_edit(&self, username: S) -> bool { + let username = username.into_bytes_with_nul(); + unsafe { + BNRemoteProjectCanUserEdit( + self.as_raw(), + username.as_ref().as_ptr() as *const ffi::c_char, + ) + } + } + + /// Determine if a user is in the admin group. + /// + /// # Arguments + /// + /// * `username` - Username of user to check + pub fn can_user_admin(&self, username: S) -> bool { + let username = username.into_bytes_with_nul(); + unsafe { + BNRemoteProjectCanUserAdmin( + self.as_raw(), + username.as_ref().as_ptr() as *const ffi::c_char, + ) + } + } + + /// Get the default directory path for a remote Project. This is based off + /// the Setting for collaboration.directory, the project's id, and the + /// project's remote's id. + pub fn default_project_path(&self) -> BnString { + let result = unsafe { BNCollaborationDefaultProjectPath(self.as_raw()) }; + unsafe { BnString::from_raw(result) } + } + + /// Upload a file, with database, to the remote under the given project + /// + /// * `metadata` - Local file with database + /// * `progress` -: Function to call for progress updates + /// * `name_changeset` - Function to call for naming a pushed changeset, if necessary + /// * `parent_folder` - Optional parent folder in which to place this file + pub fn upload_database( + &self, + metadata: &FileMetadata, + parent_folder: Option<&RemoteFolder>, + progress_function: P, + name_changeset: C, + ) -> Result + where + S: BnStrCompatible, + P: ProgressCallback, + C: NameChangeset, + { + databasesync::upload_database( + metadata, + self, + parent_folder, + progress_function, + name_changeset, + ) + } + + // TODO: check remotebrowser.cpp for implementation + ///// Upload a file to the project, creating a new File and pulling it + ///// + ///// NOTE: If the project has not been opened, it will be opened upon calling this. + ///// + ///// * `target` - Path to file on disk or BinaryView/FileMetadata object of + ///// already-opened file + ///// * `parent_folder` - Parent folder to place the uploaded file in + ///// * `progress` - Function to call for progress updates + //pub fn upload_new_file( + // &self, + // target: S, + // parent_folder: Option<&RemoteFolder>, + // progress: P, + // open_view_options: u32, + //) -> Result<(), ()> { + // if !self.open(ProgressCallbackNop)? { + // return Err(()); + // } + // let target = target.into_bytes_with_nul(); + // todo!(); + //} +} + +impl CoreArrayProvider for RemoteProject { + type Raw = *mut BNRemoteProject; + type Context = (); + type Wrapped<'a> = &'a Self; +} + +unsafe impl CoreArrayProviderInner for RemoteProject { + unsafe fn free(raw: *mut Self::Raw, count: usize, _context: &Self::Context) { + BNFreeRemoteProjectList(raw, count) + } + + unsafe fn wrap_raw<'a>(raw: &'a Self::Raw, _context: &'a Self::Context) -> Self::Wrapped<'a> { + Self::ref_from_raw(raw) + } +} diff --git a/rust/src/collaboration/remote.rs b/rust/src/collaboration/remote.rs new file mode 100644 index 000000000..4510bf3d9 --- /dev/null +++ b/rust/src/collaboration/remote.rs @@ -0,0 +1,880 @@ +use core::{ffi, mem, ptr}; + +use binaryninjacore_sys::*; + +use super::{databasesync, Group, Id, RemoteProject, User}; + +use crate::binaryview::BinaryView; +use crate::database::Database; +use crate::ffi::{ProgressCallback, ProgressCallbackNop}; +use crate::project::Project; +use crate::rc::{Array, CoreArrayProvider, CoreArrayProviderInner}; +use crate::string::{BnStrCompatible, BnString}; + +#[repr(transparent)] +pub struct Remote { + handle: ptr::NonNull, +} + +impl Drop for Remote { + fn drop(&mut self) { + unsafe { BNFreeRemote(self.as_raw()) } + } +} + +impl PartialEq for Remote { + fn eq(&self, other: &Self) -> bool { + // don't pull metadata if we hand't yet + if !self.has_loaded_metadata() || other.has_loaded_metadata() { + self.address() == other.address() + } else if let Some((slf, oth)) = self.unique_id().ok().zip(other.unique_id().ok()) { + slf == oth + } else { + // falback to comparing address + self.address() == other.address() + } + } +} +impl Eq for Remote {} + +impl Clone for Remote { + fn clone(&self) -> Self { + unsafe { Self::from_raw(ptr::NonNull::new(BNNewRemoteReference(self.as_raw())).unwrap()) } + } +} + +impl Remote { + pub(crate) unsafe fn from_raw(handle: ptr::NonNull) -> Self { + Self { handle } + } + + pub(crate) unsafe fn ref_from_raw(handle: &*mut BNRemote) -> &Self { + assert!(!handle.is_null()); + mem::transmute(handle) + } + + #[allow(clippy::mut_from_ref)] + pub(crate) unsafe fn as_raw(&self) -> &mut BNRemote { + &mut *self.handle.as_ptr() + } + + pub fn new(name: N, address: A) -> Self { + let name = name.into_bytes_with_nul(); + let address = address.into_bytes_with_nul(); + let result = unsafe { + BNCollaborationCreateRemote( + name.as_ref().as_ptr() as *const ffi::c_char, + address.as_ref().as_ptr() as *const ffi::c_char, + ) + }; + unsafe { Self::from_raw(ptr::NonNull::new(result).unwrap()) } + } + + /// Get the Remote for a Database + pub fn get_for_local_database(database: &Database) -> Result, ()> { + databasesync::get_remote_for_local_database(database) + } + + /// Get the Remote for a Binary View + pub fn get_for_binary_view(bv: &BinaryView) -> Result, ()> { + databasesync::get_remote_for_binary_view(bv) + } + + /// Checks if the remote has pulled metadata like its id, etc. + pub fn has_loaded_metadata(&self) -> bool { + unsafe { BNRemoteHasLoadedMetadata(self.as_raw()) } + } + + /// Gets the unique id. If metadata has not been pulled, it will be pulled upon calling this. + pub fn unique_id(&self) -> Result { + if !self.has_loaded_metadata() { + self.load_metadata()?; + } + let result = unsafe { BNRemoteGetUniqueId(self.as_raw()) }; + assert!(!result.is_null()); + Ok(unsafe { BnString::from_raw(result) }) + } + + /// Gets the name of the remote. + pub fn name(&self) -> BnString { + let result = unsafe { BNRemoteGetName(self.as_raw()) }; + assert!(!result.is_null()); + unsafe { BnString::from_raw(result) } + } + + /// Gets the address of the remote. + pub fn address(&self) -> BnString { + let result = unsafe { BNRemoteGetAddress(self.as_raw()) }; + assert!(!result.is_null()); + unsafe { BnString::from_raw(result) } + } + + /// Checks if the remote is connected. + pub fn is_connected(&self) -> bool { + unsafe { BNRemoteIsConnected(self.as_raw()) } + } + + /// Gets the username used to connect to the remote. + pub fn username(&self) -> BnString { + let result = unsafe { BNRemoteGetUsername(self.as_raw()) }; + assert!(!result.is_null()); + unsafe { BnString::from_raw(result) } + } + + /// Gets the token used to connect to the remote. + pub fn token(&self) -> BnString { + let result = unsafe { BNRemoteGetToken(self.as_raw()) }; + assert!(!result.is_null()); + unsafe { BnString::from_raw(result) } + } + + /// Gets the server version. If metadata has not been pulled, it will be pulled upon calling this. + pub fn server_version(&self) -> Result { + if !self.has_loaded_metadata() { + self.load_metadata()?; + } + Ok(unsafe { BNRemoteGetServerVersion(self.as_raw()) }) + } + + /// Gets the server build id. If metadata has not been pulled, it will be pulled upon calling this. + pub fn server_build_id(&self) -> Result { + if !self.has_loaded_metadata() { + self.load_metadata()?; + } + unsafe { Ok(BnString::from_raw(BNRemoteGetServerBuildId(self.as_raw()))) } + } + + /// Gets the list of supported authentication backends on the server. + /// If metadata has not been pulled, it will be pulled upon calling this. + pub fn auth_backends(&self) -> Result<(Array, Array), ()> { + if !self.has_loaded_metadata() { + self.load_metadata()?; + } + + let mut backend_ids = ptr::null_mut(); + let mut backend_names = ptr::null_mut(); + let mut count = 0; + let success = unsafe { + BNRemoteGetAuthBackends( + self.as_raw(), + &mut backend_ids, + &mut backend_names, + &mut count, + ) + }; + success + .then(|| unsafe { + ( + Array::new(backend_ids, count, ()), + Array::new(backend_names, count, ()), + ) + }) + .ok_or(()) + } + + /// Checks if the current user is an administrator. + pub fn is_admin(&self) -> Result { + if !self.has_pulled_users() { + self.pull_users(ProgressCallbackNop)?; + } + Ok(unsafe { BNRemoteIsAdmin(self.as_raw()) }) + } + + /// Checks if the remote is the same as the Enterprise License server. + pub fn is_enterprise(&self) -> Result { + if !self.has_loaded_metadata() { + self.load_metadata()?; + } + Ok(unsafe { BNRemoteIsEnterprise(self.as_raw()) }) + } + + /// Loads metadata from the remote, including unique id and versions. + pub fn load_metadata(&self) -> Result<(), ()> { + let success = unsafe { BNRemoteLoadMetadata(self.as_raw()) }; + success.then_some(()).ok_or(()) + } + + /// Requests an authentication token using a username and password. + pub fn request_authentication_token( + &self, + username: U, + password: P, + ) -> Option { + let username = username.into_bytes_with_nul(); + let password = password.into_bytes_with_nul(); + let token = unsafe { + BNRemoteRequestAuthenticationToken( + self.as_raw(), + username.as_ref().as_ptr() as *const ffi::c_char, + password.as_ref().as_ptr() as *const ffi::c_char, + ) + }; + if token.is_null() { + None + } else { + Some(unsafe { BnString::from_raw(token) }) + } + } + + // TODO: implement enterprise and SecretsProvider + /// Connects to the Remote, loading metadata and optionally acquiring a token. + /// + /// NOTE: If no username or token are provided, they will be looked up from the keychain, \ + /// likely saved there by Enterprise authentication. + pub fn connect( + &self, + username_and_token: Option<(U, T)>, + ) -> Result<(), ()> { + if !self.has_loaded_metadata() { + self.load_metadata()?; + } + + let success = if let Some((username, token)) = username_and_token { + return self.connect_with_username_and_token(username, token); + // TODO: implement enterprise + //} else if self.is_enterprise()? && enterprise::is_authenticated() { + // // try with the enterprise + // let username = enterprise::username(); + // let token = enterprise::token(); + + // unsafe { BNRemoteConnect(self.as_raw(), username.as_ptr(), token.as_ptr()) } + } else { + // TODO: implement SecretsProvider + //let secrets_prov_name = crate::settings::Settings::new(c"default").get_string( + // c"enterprise.secretsProvider", + // None, + // None, + //); + //let secrets_prov = secrets::SecretsProvider::by_name(secrets_prov_name); + //let secrets_proc_creds = secrets_prov.get_data(self.address()); + let secrets_proc_creds: Option = None; + if let Some(_creds_json) = secrets_proc_creds { + // TODO: implement/use a json_decode + // try loggin from the secrets provider + //let crefs = json_decode::decode(creds_json.as_str()); + //let username = creds.get("username"); + //let token = creds.get("token"); + //unsafe { BNRemoteConnect(self.as_raw(), username.as_ptr(), token.as_ptr()) } + unreachable!(); + } else { + // try loggin in with creds in the env + let username = std::env::var("BN_ENTERPRISE_USERNAME").ok(); + let password = std::env::var("BN_ENTERPRISE_PASSWORD").ok(); + let token = username.as_ref().zip(password).map(|(username, password)| { + self.request_authentication_token(username, password) + }); + + if let Some(Some(token)) = token { + let username_ptr = username.as_ref().unwrap().as_ptr() as *const ffi::c_char; + + unsafe { BNRemoteConnect(self.as_raw(), username_ptr, token.as_ptr()) } + } else { + // unable to find valid creds + return Err(()); + } + } + }; + success.then_some(()).ok_or(()) + } + + pub fn connect_with_username_and_token( + &self, + username: U, + token: T, + ) -> Result<(), ()> { + let username = username.into_bytes_with_nul(); + let token = token.into_bytes_with_nul(); + let username_ptr = username.as_ref().as_ptr() as *const ffi::c_char; + let token_ptr = token.as_ref().as_ptr() as *const ffi::c_char; + + let success = unsafe { BNRemoteConnect(self.as_raw(), username_ptr, token_ptr) }; + success.then_some(()).ok_or(()) + } + + /// Disconnects from the remote. + pub fn disconnect(&self) -> Result<(), ()> { + let success = unsafe { BNRemoteDisconnect(self.as_raw()) }; + success.then_some(()).ok_or(()) + } + + /// Checks if the project has pulled the projects yet. + pub fn has_pulled_projects(&self) -> bool { + unsafe { BNRemoteHasPulledProjects(self.as_raw()) } + } + + /// Checks if the project has pulled the groups yet. + pub fn has_pulled_groups(&self) -> bool { + unsafe { BNRemoteHasPulledGroups(self.as_raw()) } + } + + /// Checks if the project has pulled the users yet. + pub fn has_pulled_users(&self) -> bool { + unsafe { BNRemoteHasPulledUsers(self.as_raw()) } + } + + /// Gets the list of projects in this project. + /// + /// NOTE: If projects have not been pulled, they will be pulled upon calling this. + pub fn projects(&self) -> Result, ()> { + if !self.has_pulled_projects() { + self.pull_projects(ProgressCallbackNop)?; + } + + let mut count = 0; + let value = unsafe { BNRemoteGetProjects(self.as_raw(), &mut count) }; + if value.is_null() { + return Err(()); + } + Ok(unsafe { Array::new(value, count, ()) }) + } + + /// Gets a specific project in the Remote by its id. + /// + /// NOTE: If projects have not been pulled, they will be pulled upon calling this. + pub fn get_project_by_id( + &self, + id: S, + ) -> Result, ()> { + if !self.has_pulled_projects() { + self.pull_projects(ProgressCallbackNop)?; + } + + let id = id.into_bytes_with_nul(); + let value = unsafe { + BNRemoteGetProjectById(self.as_raw(), id.as_ref().as_ptr() as *const ffi::c_char) + }; + Ok(ptr::NonNull::new(value).map(|handle| unsafe { RemoteProject::from_raw(handle) })) + } + + /// Gets a specific project in the Remote by its name. + /// + /// NOTE: If projects have not been pulled, they will be pulled upon calling this. + pub fn get_project_by_name( + &self, + name: S, + ) -> Result, ()> { + if !self.has_pulled_projects() { + self.pull_projects(ProgressCallbackNop)?; + } + + let name = name.into_bytes_with_nul(); + let value = unsafe { + BNRemoteGetProjectByName(self.as_raw(), name.as_ref().as_ptr() as *const ffi::c_char) + }; + Ok(ptr::NonNull::new(value).map(|handle| unsafe { RemoteProject::from_raw(handle) })) + } + + /// Pulls the list of projects from the Remote. + /// + /// # Arguments + /// + /// * `progress` - Function to call for progress updates + pub fn pull_projects(&self, mut progress: F) -> Result<(), ()> { + let success = unsafe { + BNRemotePullProjects( + self.as_raw(), + Some(F::cb_progress_callback), + &mut progress as *mut F as *mut ffi::c_void, + ) + }; + success.then_some(()).ok_or(()) + } + + /// Creates a new project on the remote (and pull it). + /// + /// # Arguments + /// + /// * `name` - Project name + /// * `description` - Project description + pub fn create_project( + &self, + name: N, + description: D, + ) -> Result { + let name = name.into_bytes_with_nul(); + let description = description.into_bytes_with_nul(); + let value = unsafe { + BNRemoteCreateProject( + self.as_raw(), + name.as_ref().as_ptr() as *const ffi::c_char, + description.as_ref().as_ptr() as *const ffi::c_char, + ) + }; + ptr::NonNull::new(value) + .map(|handle| unsafe { RemoteProject::from_raw(handle) }) + .ok_or(()) + } + + /// Create a new project on the remote from a local project. + pub fn import_local_project( + &self, + project: &Project, + mut progress: P, + ) -> Option { + let value = unsafe { + BNRemoteImportLocalProject( + self.as_raw(), + project.as_raw(), + Some(P::cb_progress_callback), + &mut progress as *mut P as *mut ffi::c_void, + ) + }; + ptr::NonNull::new(value).map(|handle| unsafe { RemoteProject::from_raw(handle) }) + } + + /// Pushes an updated Project object to the Remote. + /// + /// # Arguments + /// + /// * `project` - Project object which has been updated + /// * `extra_fields` - Extra HTTP fields to send with the update + pub fn push_project(&self, project: &RemoteProject, extra_fields: I) -> Result<(), ()> + where + I: Iterator, + K: BnStrCompatible, + V: BnStrCompatible, + { + let (keys, values): (Vec<_>, Vec<_>) = extra_fields + .into_iter() + .map(|(k, v)| (k.into_bytes_with_nul(), v.into_bytes_with_nul())) + .unzip(); + let mut keys_raw = keys + .iter() + .map(|s| s.as_ref().as_ptr() as *const ffi::c_char) + .collect::>(); + let mut values_raw = values + .iter() + .map(|s| s.as_ref().as_ptr() as *const ffi::c_char) + .collect::>(); + + let success = unsafe { + BNRemotePushProject( + self.as_raw(), + project.as_raw(), + keys_raw.as_mut_ptr(), + values_raw.as_mut_ptr(), + keys_raw.len(), + ) + }; + success.then_some(()).ok_or(()) + } + + /// Deletes a project from the remote. + pub fn delete_project(&self, project: &RemoteProject) -> Result<(), ()> { + let success = unsafe { BNRemoteDeleteProject(self.as_raw(), project.as_raw()) }; + success.then_some(()).ok_or(()) + } + + /// Gets the list of groups in this project. + /// + /// If groups have not been pulled, they will be pulled upon calling this. + /// This function is only available to accounts with admin status on the Remote. + pub fn groups(&self) -> Result, ()> { + if !self.has_pulled_groups() { + self.pull_groups(ProgressCallbackNop)?; + } + + let mut count = 0; + let value = unsafe { BNRemoteGetGroups(self.as_raw(), &mut count) }; + if value.is_null() { + return Err(()); + } + Ok(unsafe { Array::new(value, count, ()) }) + } + + /// Gets a specific group in the Remote by its id. + /// + /// If groups have not been pulled, they will be pulled upon calling this. + /// This function is only available to accounts with admin status on the Remote. + pub fn get_group_by_id(&self, id: u64) -> Result, ()> { + if !self.has_pulled_groups() { + self.pull_groups(ProgressCallbackNop)?; + } + + let value = unsafe { BNRemoteGetGroupById(self.as_raw(), id) }; + Ok(ptr::NonNull::new(value).map(|handle| unsafe { Group::from_raw(handle) })) + } + + /// Gets a specific group in the Remote by its name. + /// + /// If groups have not been pulled, they will be pulled upon calling this. + /// This function is only available to accounts with admin status on the Remote. + pub fn get_group_by_name(&self, name: S) -> Result, ()> { + if !self.has_pulled_groups() { + self.pull_groups(ProgressCallbackNop)?; + } + + let name = name.into_bytes_with_nul(); + let value = unsafe { + BNRemoteGetGroupByName(self.as_raw(), name.as_ref().as_ptr() as *const ffi::c_char) + }; + + Ok(ptr::NonNull::new(value).map(|handle| unsafe { Group::from_raw(handle) })) + } + + /// Searches for groups in the Remote with a given prefix. + /// + /// # Arguments + /// + /// * `prefix` - Prefix of name for groups + pub fn search_groups( + &self, + prefix: S, + ) -> Result<(Array, Array), ()> { + let prefix = prefix.into_bytes_with_nul(); + let mut count = 0; + let mut group_ids = ptr::null_mut(); + let mut group_names = ptr::null_mut(); + + let success = unsafe { + BNRemoteSearchGroups( + self.as_raw(), + prefix.as_ref().as_ptr() as *const ffi::c_char, + &mut group_ids, + &mut group_names, + &mut count, + ) + }; + if !success { + return Err(()); + } + Ok(unsafe { + ( + Array::new(group_ids, count, ()), + Array::new(group_names, count, ()), + ) + }) + } + + /// Pulls the list of groups from the Remote. + /// This function is only available to accounts with admin status on the Remote. + /// + /// # Arguments + /// + /// * `progress` - Function to call for progress updates + pub fn pull_groups(&self, mut progress: F) -> Result<(), ()> { + let success = unsafe { + BNRemotePullGroups( + self.as_raw(), + Some(F::cb_progress_callback), + &mut progress as *mut F as *mut ffi::c_void, + ) + }; + success.then_some(()).ok_or(()) + } + + /// Creates a new group on the remote (and pull it). + /// This function is only available to accounts with admin status on the Remote. + /// + /// # Arguments + /// + /// * `name` - Group name + /// * `usernames` - List of usernames of users in the group + pub fn create_group(&self, name: N, usernames: I) -> Result + where + N: BnStrCompatible, + I: IntoIterator, + I::Item: BnStrCompatible, + { + let name = name.into_bytes_with_nul(); + let usernames: Vec<_> = usernames + .into_iter() + .map(|s| s.into_bytes_with_nul()) + .collect(); + let mut username_ptrs: Vec<_> = usernames + .iter() + .map(|s| s.as_ref().as_ptr() as *const ffi::c_char) + .collect(); + + let value = unsafe { + BNRemoteCreateGroup( + self.as_raw(), + name.as_ref().as_ptr() as *const ffi::c_char, + username_ptrs.as_mut_ptr(), + username_ptrs.len(), + ) + }; + ptr::NonNull::new(value) + .map(|handle| unsafe { Group::from_raw(handle) }) + .ok_or(()) + } + + /// Pushes an updated Group object to the Remote. + /// This function is only available to accounts with admin status on the Remote. + /// + /// # Arguments + /// + /// * `group` - Group object which has been updated + /// * `extra_fields` - Extra HTTP fields to send with the update + pub fn push_group(&self, group: &Group, extra_fields: I) -> Result<(), ()> + where + I: IntoIterator, + K: BnStrCompatible, + V: BnStrCompatible, + { + let (keys, values): (Vec<_>, Vec<_>) = extra_fields + .into_iter() + .map(|(k, v)| (k.into_bytes_with_nul(), v.into_bytes_with_nul())) + .unzip(); + let mut keys_raw: Vec<_> = keys + .iter() + .map(|s| s.as_ref().as_ptr() as *const ffi::c_char) + .collect(); + let mut values_raw: Vec<_> = values + .iter() + .map(|s| s.as_ref().as_ptr() as *const ffi::c_char) + .collect(); + + let success = unsafe { + BNRemotePushGroup( + self.as_raw(), + group.as_raw(), + keys_raw.as_mut_ptr(), + values_raw.as_mut_ptr(), + keys.len(), + ) + }; + success.then_some(()).ok_or(()) + } + + /// Deletes the specified group from the remote. + + /// NOTE: This function is only available to accounts with admin status on the Remote + /// + /// # Arguments + /// + /// * `group` - Reference to the group to delete. + pub fn delete_group(&self, group: &Group) -> Result<(), ()> { + let success = unsafe { BNRemoteDeleteGroup(self.as_raw(), group.as_raw()) }; + success.then_some(()).ok_or(()) + } + + /// Retrieves the list of users in the project. + /// + /// NOTE: If users have not been pulled, they will be pulled upon calling this. + /// + /// NOTE: This function is only available to accounts with admin status on the Remote + pub fn users(&self) -> Result, ()> { + if !self.has_pulled_users() { + self.pull_users(ProgressCallbackNop)?; + } + let mut count = 0; + let value = unsafe { BNRemoteGetUsers(self.handle.as_ptr(), &mut count) }; + if value.is_null() { + return Err(()); + } + Ok(unsafe { Array::new(value, count, ()) }) + } + + /// Retrieves a specific user in the project by their ID. + /// + /// NOTE: If users have not been pulled, they will be pulled upon calling this. + /// + /// NOTE: This function is only available to accounts with admin status on the Remote + /// + /// # Arguments + /// + /// * `id` - The identifier of the user to retrieve. + pub fn get_user_by_id(&self, id: S) -> Result, ()> { + if !self.has_pulled_users() { + self.pull_users(ProgressCallbackNop)?; + } + let id = id.into_bytes_with_nul(); + let value = unsafe { + BNRemoteGetUserById(self.as_raw(), id.as_ref().as_ptr() as *const ffi::c_char) + }; + Ok(ptr::NonNull::new(value).map(|handle| unsafe { User::from_raw(handle) })) + } + + /// Retrieves a specific user in the project by their username. + /// + /// NOTE: If users have not been pulled, they will be pulled upon calling this. + /// + /// NOTE: This function is only available to accounts with admin status on the Remote + /// + /// # Arguments + /// + /// * `username` - The username of the user to retrieve. + pub fn get_user_by_username( + &self, + username: S, + ) -> Result, ()> { + if !self.has_pulled_users() { + self.pull_users(ProgressCallbackNop)?; + } + let username = username.into_bytes_with_nul(); + let value = unsafe { + BNRemoteGetUserByUsername( + self.as_raw(), + username.as_ref().as_ptr() as *const ffi::c_char, + ) + }; + Ok(ptr::NonNull::new(value).map(|handle| unsafe { User::from_raw(handle) })) + } + + /// Retrieves the user object for the currently connected user. + /// + /// NOTE: If users have not been pulled, they will be pulled upon calling this. + /// + /// NOTE: This function is only available to accounts with admin status on the Remote + pub fn current_user(&self) -> Result, ()> { + if !self.has_pulled_users() { + self.pull_users(ProgressCallbackNop)?; + } + let value = unsafe { BNRemoteGetCurrentUser(self.handle.as_ptr()) }; + Ok(ptr::NonNull::new(value).map(|handle| unsafe { User::from_raw(handle) })) + } + + /// Searches for users in the project with a given prefix. + /// + /// # Arguments + /// + /// * `prefix` - The prefix to search for in usernames. + pub fn search_users( + &self, + prefix: S, + ) -> Result<(Array, Array), ()> { + let prefix = prefix.into_bytes_with_nul(); + let mut count = 0; + let mut user_ids = ptr::null_mut(); + let mut usernames = ptr::null_mut(); + let success = unsafe { + BNRemoteSearchUsers( + self.as_raw(), + prefix.as_ref().as_ptr() as *const ffi::c_char, + &mut user_ids, + &mut usernames, + &mut count, + ) + }; + + if !success { + return Err(()); + } + assert!(!user_ids.is_null()); + assert!(!usernames.is_null()); + Ok(unsafe { + ( + Array::new(user_ids, count, ()), + Array::new(usernames, count, ()), + ) + }) + } + + /// Pulls the list of users from the remote. + /// + /// NOTE: This function is only available to accounts with admin status on the Remote. + /// Non-admin accounts attempting to call this function will pull an empty list of users. + /// + /// # Arguments + /// + /// * `progress` - Closure called to report progress. Takes current and total progress counts. + pub fn pull_users(&self, mut progress: P) -> Result<(), ()> { + let success = unsafe { + BNRemotePullUsers( + self.as_raw(), + Some(P::cb_progress_callback), + &mut progress as *mut P as *mut ffi::c_void, + ) + }; + success.then_some(()).ok_or(()) + } + + /// Creates a new user on the remote and returns a reference to the created user. + /// + /// NOTE: This function is only available to accounts with admin status on the Remote + /// + /// # Arguments + /// + /// * Various details about the new user to be created. + pub fn create_user( + &self, + username: U, + email: E, + is_active: bool, + password: P, + group_ids: &[u64], + user_permission_ids: &[u64], + ) -> Result { + let username = username.into_bytes_with_nul(); + let email = email.into_bytes_with_nul(); + let password = password.into_bytes_with_nul(); + + let value = unsafe { + BNRemoteCreateUser( + self.as_raw(), + username.as_ref().as_ptr() as *const ffi::c_char, + email.as_ref().as_ptr() as *const ffi::c_char, + is_active, + password.as_ref().as_ptr() as *const ffi::c_char, + group_ids.as_ptr(), + group_ids.len(), + user_permission_ids.as_ptr(), + user_permission_ids.len(), + ) + }; + ptr::NonNull::new(value) + .map(|handle| unsafe { User::from_raw(handle) }) + .ok_or(()) + } + + /// Pushes updates to the specified user on the remote. + /// + /// NOTE: This function is only available to accounts with admin status on the Remote + /// + /// # Arguments + /// + /// * `user` - Reference to the `RemoteUser` object to push. + /// * `extra_fields` - Optional extra fields to send with the update. + pub fn push_user(&self, user: &User, extra_fields: I) -> Result<(), ()> + where + I: Iterator, + K: BnStrCompatible, + V: BnStrCompatible, + { + let (keys, values): (Vec<_>, Vec<_>) = extra_fields + .into_iter() + .map(|(k, v)| (k.into_bytes_with_nul(), v.into_bytes_with_nul())) + .unzip(); + let mut keys_raw: Vec<_> = keys + .iter() + .map(|s| s.as_ref().as_ptr() as *const ffi::c_char) + .collect(); + let mut values_raw: Vec<_> = values + .iter() + .map(|s| s.as_ref().as_ptr() as *const ffi::c_char) + .collect(); + let success = unsafe { + BNRemotePushUser( + self.as_raw(), + user.as_raw(), + keys_raw.as_mut_ptr(), + values_raw.as_mut_ptr(), + keys_raw.len(), + ) + }; + success.then_some(()).ok_or(()) + } + + // TODO identify the request and ret type of this function, it seems to use a C++ implementation of + // HTTP requests, composed mostly of `std:vector`. + //pub fn request(&self) { + // unsafe { BNRemoteRequest(self.as_raw(), todo!(), todo!()) } + //} +} + +impl CoreArrayProvider for Remote { + type Raw = *mut BNRemote; + type Context = (); + type Wrapped<'a> = &'a Self; +} + +unsafe impl CoreArrayProviderInner for Remote { + unsafe fn free(raw: *mut Self::Raw, count: usize, _context: &Self::Context) { + BNFreeRemoteList(raw, count) + } + + unsafe fn wrap_raw<'a>(raw: &'a Self::Raw, _context: &'a Self::Context) -> Self::Wrapped<'a> { + Self::ref_from_raw(raw) + } +} diff --git a/rust/src/collaboration/snapshot.rs b/rust/src/collaboration/snapshot.rs new file mode 100644 index 000000000..227565087 --- /dev/null +++ b/rust/src/collaboration/snapshot.rs @@ -0,0 +1,480 @@ +use core::{ffi, mem, ptr}; + +use std::time::SystemTime; + +use binaryninjacore_sys::*; + +use super::{databasesync, Remote, RemoteFile, RemoteProject}; + +use crate::binaryview::{BinaryView, BinaryViewExt}; +use crate::database::Snapshot; +use crate::ffi::{ProgressCallback, ProgressCallbackNop}; +use crate::rc::{Array, CoreArrayProvider, CoreArrayProviderInner}; +use crate::string::{BnStrCompatible, BnString}; + +/// Class representing a remote Snapshot +#[repr(transparent)] +pub struct CollabSnapshot { + handle: ptr::NonNull, +} + +impl Drop for CollabSnapshot { + fn drop(&mut self) { + unsafe { BNFreeCollaborationSnapshot(self.as_raw()) } + } +} + +impl PartialEq for CollabSnapshot { + fn eq(&self, other: &Self) -> bool { + self.id() == other.id() + } +} +impl Eq for CollabSnapshot {} + +impl Clone for CollabSnapshot { + fn clone(&self) -> Self { + unsafe { + Self::from_raw( + ptr::NonNull::new(BNNewCollaborationSnapshotReference(self.as_raw())).unwrap(), + ) + } + } +} + +impl CollabSnapshot { + pub(crate) unsafe fn from_raw(handle: ptr::NonNull) -> Self { + Self { handle } + } + + pub(crate) unsafe fn ref_from_raw(handle: &*mut BNCollaborationSnapshot) -> &Self { + assert!(!handle.is_null()); + mem::transmute(handle) + } + + #[allow(clippy::mut_from_ref)] + pub(crate) unsafe fn as_raw(&self) -> &mut BNCollaborationSnapshot { + &mut *self.handle.as_ptr() + } + + /// Get the remote snapshot associated with a local snapshot (if it exists) + pub fn get_for_local_snapshot(snapshot: &Snapshot) -> Result, ()> { + databasesync::get_remote_snapshot_from_local(snapshot) + } + + /// Owning File + pub fn file(&self) -> Result { + let result = unsafe { BNCollaborationSnapshotGetFile(self.as_raw()) }; + let raw = ptr::NonNull::new(result).ok_or(())?; + Ok(unsafe { RemoteFile::from_raw(raw) }) + } + + /// Owning Project + pub fn project(&self) -> Result { + let result = unsafe { BNCollaborationSnapshotGetProject(self.as_raw()) }; + let raw = ptr::NonNull::new(result).ok_or(())?; + Ok(unsafe { RemoteProject::from_raw(raw) }) + } + + /// Owning Remote + pub fn remote(&self) -> Result { + let result = unsafe { BNCollaborationSnapshotGetRemote(self.as_raw()) }; + let raw = ptr::NonNull::new(result).ok_or(())?; + Ok(unsafe { Remote::from_raw(raw) }) + } + + /// Web api endpoint url + pub fn url(&self) -> BnString { + let value = unsafe { BNCollaborationSnapshotGetUrl(self.as_raw()) }; + assert!(!value.is_null()); + unsafe { BnString::from_raw(value) } + } + + /// Unique id + pub fn id(&self) -> BnString { + let value = unsafe { BNCollaborationSnapshotGetId(self.as_raw()) }; + assert!(!value.is_null()); + unsafe { BnString::from_raw(value) } + } + + /// Name of snapshot + pub fn name(&self) -> BnString { + let value = unsafe { BNCollaborationSnapshotGetName(self.as_raw()) }; + assert!(!value.is_null()); + unsafe { BnString::from_raw(value) } + } + + /// Get the title of a snapshot: the first line of its name + pub fn title(&self) -> BnString { + let value = unsafe { BNCollaborationSnapshotGetTitle(self.as_raw()) }; + assert!(!value.is_null()); + unsafe { BnString::from_raw(value) } + } + + /// Get the description of a snapshot: the lines of its name after the first line + pub fn description(&self) -> BnString { + let value = unsafe { BNCollaborationSnapshotGetDescription(self.as_raw()) }; + assert!(!value.is_null()); + unsafe { BnString::from_raw(value) } + } + + /// Get the user id of the author of a snapshot + pub fn author(&self) -> BnString { + let value = unsafe { BNCollaborationSnapshotGetAuthor(self.as_raw()) }; + assert!(!value.is_null()); + unsafe { BnString::from_raw(value) } + } + + /// Get the username of the author of a snapshot, if possible (vs author which is user id) + pub fn author_username(&self) -> BnString { + let value = unsafe { BNCollaborationSnapshotGetAuthorUsername(self.as_raw()) }; + assert!(!value.is_null()); + unsafe { BnString::from_raw(value) } + } + + /// Created date of Snapshot + pub fn created(&self) -> SystemTime { + let timestamp = unsafe { BNCollaborationSnapshotGetCreated(self.as_raw()) }; + crate::ffi::time_from_bn(timestamp.try_into().unwrap()) + } + + /// Date of last modification to the snapshot + pub fn last_modified(&self) -> SystemTime { + let timestamp = unsafe { BNCollaborationSnapshotGetLastModified(self.as_raw()) }; + crate::ffi::time_from_bn(timestamp.try_into().unwrap()) + } + + /// Hash of snapshot data (analysis and markup, etc) + /// No specific hash algorithm is guaranteed + pub fn hash(&self) -> BnString { + let value = unsafe { BNCollaborationSnapshotGetHash(self.as_raw()) }; + assert!(!value.is_null()); + unsafe { BnString::from_raw(value) } + } + + /// Hash of file contents in snapshot + /// No specific hash algorithm is guaranteed + pub fn snapshot_file_hash(&self) -> BnString { + let value = unsafe { BNCollaborationSnapshotGetSnapshotFileHash(self.as_raw()) }; + assert!(!value.is_null()); + unsafe { BnString::from_raw(value) } + } + + /// If the snapshot has pulled undo entries yet + pub fn has_pulled_undo_entires(&self) -> bool { + unsafe { BNCollaborationSnapshotHasPulledUndoEntries(self.as_raw()) } + } + + /// If the snapshot has been finalized on the server and is no longer editable + pub fn is_finalized(&self) -> bool { + unsafe { BNCollaborationSnapshotIsFinalized(self.as_raw()) } + } + + /// List of ids of all remote parent Snapshots + pub fn parent_ids(&self) -> Result, ()> { + let mut count = 0; + let raw = unsafe { BNCollaborationSnapshotGetParentIds(self.as_raw(), &mut count) }; + (!raw.is_null()) + .then(|| unsafe { Array::new(raw, count, ()) }) + .ok_or(()) + } + + /// List of ids of all remote child Snapshots + pub fn child_ids(&self) -> Result, ()> { + let mut count = 0; + let raw = unsafe { BNCollaborationSnapshotGetChildIds(self.as_raw(), &mut count) }; + (!raw.is_null()) + .then(|| unsafe { Array::new(raw, count, ()) }) + .ok_or(()) + } + + /// List of all parent Snapshot objects + pub fn parents(&self) -> Result, ()> { + let mut count = 0; + let raw = unsafe { BNCollaborationSnapshotGetParents(self.as_raw(), &mut count) }; + (!raw.is_null()) + .then(|| unsafe { Array::new(raw, count, ()) }) + .ok_or(()) + } + + /// List of all child Snapshot objects + pub fn children(&self) -> Result, ()> { + let mut count = 0; + let raw = unsafe { BNCollaborationSnapshotGetChildren(self.as_raw(), &mut count) }; + (!raw.is_null()) + .then(|| unsafe { Array::new(raw, count, ()) }) + .ok_or(()) + } + + /// Get the list of undo entries stored in this snapshot. + /// + /// NOTE: If undo entries have not been pulled, they will be pulled upon calling this. + pub fn undo_entries(&self) -> Result, ()> { + if !self.has_pulled_undo_entires() { + self.pull_undo_entries(ProgressCallbackNop)?; + } + let mut count = 0; + let raw = unsafe { BNCollaborationSnapshotGetUndoEntries(self.as_raw(), &mut count) }; + (!raw.is_null()) + .then(|| unsafe { Array::new(raw, count, ()) }) + .ok_or(()) + } + + /// Get a specific Undo Entry in the Snapshot by its id + /// + /// NOTE: If undo entries have not been pulled, they will be pulled upon calling this. + pub fn get_undo_entry_by_id(&self, id: u64) -> Result, ()> { + if !self.has_pulled_undo_entires() { + self.pull_undo_entries(ProgressCallbackNop)?; + } + let raw = unsafe { BNCollaborationSnapshotGetUndoEntryById(self.as_raw(), id) }; + Ok(ptr::NonNull::new(raw).map(|handle| unsafe { UndoEntry::from_raw(handle) })) + } + + /// Pull the list of Undo Entries from the Remote. + pub fn pull_undo_entries(&self, mut progress: P) -> Result<(), ()> { + let success = unsafe { + BNCollaborationSnapshotPullUndoEntries( + self.as_raw(), + Some(P::cb_progress_callback), + &mut progress as *mut P as *mut ffi::c_void, + ) + }; + success.then_some(()).ok_or(()) + } + + /// Create a new Undo Entry in this snapshot. + pub fn create_undo_entry( + &self, + parent: Option, + data: S, + ) -> Result { + let data = data.into_bytes_with_nul(); + let value = unsafe { + BNCollaborationSnapshotCreateUndoEntry( + self.as_raw(), + parent.is_some(), + parent.unwrap_or(0), + data.as_ref().as_ptr() as *const ffi::c_char, + ) + }; + let handle = ptr::NonNull::new(value).ok_or(())?; + Ok(unsafe { UndoEntry::from_raw(handle) }) + } + + /// Mark a snapshot as Finalized, committing it to the Remote, preventing future updates, + /// and allowing snapshots to be children of it. + pub fn finalize(&self) -> Result<(), ()> { + let success = unsafe { BNCollaborationSnapshotFinalize(self.as_raw()) }; + success.then_some(()).ok_or(()) + } + + // TODO what kind of struct is this and how to free it? + ///// Download the contents of the file in the Snapshot. + //pub fn download_snapshot_file( + // &self, + // mut progress: P, + //) -> Result { + // let mut data = ptr::null_mut(); + // let mut count = 0; + // let success = unsafe { + // BNCollaborationSnapshotDownloadSnapshotFile( + // self.as_raw(), + // Some(P::cb_progress_callback), + // &mut progress as *mut P as *mut ffi::c_void, + // &mut data, + // &mut count, + // ) + // }; + // todo!(); + //} + // + ///// Download the snapshot fields blob, compatible with KeyValueStore. + //pub fn download( + // &self, + // mut progress: P, + //) -> Result { + // let mut data = ptr::null_mut(); + // let mut count = 0; + // let success = unsafe { + // BNCollaborationSnapshotDownload( + // self.as_raw(), + // Some(P::cb_progress_callback), + // &mut progress as *mut P as *mut ffi::c_void, + // &mut data, + // &mut count, + // ) + // }; + // todo!(); + //} + // + ///// Download the analysis cache fields blob, compatible with KeyValueStore. + //pub fn download_analysis_cache( + // &self, + // mut progress: P, + //) -> Result { + // let mut data = ptr::null_mut(); + // let mut count = 0; + // let success = unsafe { + // BNCollaborationSnapshotDownloadAnalysisCache( + // self.as_raw(), + // Some(P::cb_progress_callback), + // &mut progress as *mut P as *mut ffi::c_void, + // &mut data, + // &mut count, + // ) + // }; + // todo!(); + //} + + /// Get the local snapshot associated with a remote snapshot (if it exists) + pub fn get_local_snapshot(&self, bv: &BinaryView) -> Result, ()> { + let Some(db) = bv.file().database() else { + return Ok(None); + }; + databasesync::get_local_snapshot_for_remote(self, &db) + } + + pub fn analysis_cache_build_id(&self) -> u64 { + unsafe { BNCollaborationSnapshotGetAnalysisCacheBuildId(self.as_raw()) } + } +} + +impl CoreArrayProvider for CollabSnapshot { + type Raw = *mut BNCollaborationSnapshot; + type Context = (); + type Wrapped<'a> = &'a CollabSnapshot; +} + +unsafe impl CoreArrayProviderInner for CollabSnapshot { + unsafe fn free(raw: *mut Self::Raw, count: usize, _context: &Self::Context) { + BNFreeCollaborationSnapshotList(raw, count) + } + + unsafe fn wrap_raw<'a>(raw: &'a Self::Raw, _context: &'a Self::Context) -> Self::Wrapped<'a> { + Self::ref_from_raw(raw) + } +} + +#[repr(transparent)] +pub struct UndoEntry { + handle: ptr::NonNull, +} + +impl Drop for UndoEntry { + fn drop(&mut self) { + unsafe { BNFreeCollaborationUndoEntry(self.as_raw()) } + } +} + +impl PartialEq for UndoEntry { + fn eq(&self, other: &Self) -> bool { + self.id() == other.id() + } +} +impl Eq for UndoEntry {} + +impl Clone for UndoEntry { + fn clone(&self) -> Self { + unsafe { + Self::from_raw( + ptr::NonNull::new(BNNewCollaborationUndoEntryReference(self.as_raw())).unwrap(), + ) + } + } +} + +impl UndoEntry { + pub(crate) unsafe fn from_raw(handle: ptr::NonNull) -> Self { + Self { handle } + } + + pub(crate) unsafe fn ref_from_raw(handle: &*mut BNCollaborationUndoEntry) -> &Self { + assert!(!handle.is_null()); + mem::transmute(handle) + } + + #[allow(clippy::mut_from_ref)] + pub(crate) unsafe fn as_raw(&self) -> &mut BNCollaborationUndoEntry { + &mut *self.handle.as_ptr() + } + + /// Owning Snapshot + pub fn snapshot(&self) -> Result { + let value = unsafe { BNCollaborationUndoEntryGetSnapshot(self.as_raw()) }; + let handle = ptr::NonNull::new(value).ok_or(())?; + Ok(unsafe { CollabSnapshot::from_raw(handle) }) + } + + /// Owning File + pub fn file(&self) -> Result { + let value = unsafe { BNCollaborationUndoEntryGetFile(self.as_raw()) }; + let handle = ptr::NonNull::new(value).ok_or(())?; + Ok(unsafe { RemoteFile::from_raw(handle) }) + } + + /// Owning Project + pub fn project(&self) -> Result { + let value = unsafe { BNCollaborationUndoEntryGetProject(self.as_raw()) }; + let handle = ptr::NonNull::new(value).ok_or(())?; + Ok(unsafe { RemoteProject::from_raw(handle) }) + } + + /// Owning Remote + pub fn remote(&self) -> Result { + let value = unsafe { BNCollaborationUndoEntryGetRemote(self.as_raw()) }; + let handle = ptr::NonNull::new(value).ok_or(())?; + Ok(unsafe { Remote::from_raw(handle) }) + } + + /// Web api endpoint url + pub fn url(&self) -> BnString { + let value = unsafe { BNCollaborationUndoEntryGetUrl(self.as_raw()) }; + assert!(!value.is_null()); + unsafe { BnString::from_raw(value) } + } + + /// Unique id + pub fn id(&self) -> u64 { + unsafe { BNCollaborationUndoEntryGetId(self.as_raw()) } + } + + /// Id of parent undo entry + pub fn parent_id(&self) -> Option { + let mut value = 0; + let success = unsafe { BNCollaborationUndoEntryGetParentId(self.as_raw(), &mut value) }; + success.then_some(value) + } + + /// Undo entry contents data + pub fn data(&self) -> Result { + let mut value = ptr::null_mut(); + let success = unsafe { BNCollaborationUndoEntryGetData(self.as_raw(), &mut value) }; + if !success { + return Err(()); + } + assert!(!value.is_null()); + Ok(unsafe { BnString::from_raw(value) }) + } + + /// Parent Undo Entry object + pub fn parent(&self) -> Option { + let value = unsafe { BNCollaborationUndoEntryGetParent(self.as_raw()) }; + ptr::NonNull::new(value).map(|handle| unsafe { UndoEntry::from_raw(handle) }) + } +} + +impl CoreArrayProvider for UndoEntry { + type Raw = *mut BNCollaborationUndoEntry; + type Context = (); + type Wrapped<'a> = &'a Self; +} + +unsafe impl CoreArrayProviderInner for UndoEntry { + unsafe fn free(raw: *mut Self::Raw, count: usize, _context: &Self::Context) { + BNFreeCollaborationUndoEntryList(raw, count) + } + + unsafe fn wrap_raw<'a>(raw: &'a Self::Raw, _context: &'a Self::Context) -> Self::Wrapped<'a> { + Self::ref_from_raw(raw) + } +} diff --git a/rust/src/collaboration/user.rs b/rust/src/collaboration/user.rs new file mode 100644 index 000000000..e8370ba6b --- /dev/null +++ b/rust/src/collaboration/user.rs @@ -0,0 +1,157 @@ +use core::{ffi, mem, ptr}; + +use binaryninjacore_sys::*; + +use super::Remote; + +use crate::rc::{CoreArrayProvider, CoreArrayProviderInner}; +use crate::string::{BnStrCompatible, BnString}; + +/// Class representing a remote User +#[repr(transparent)] +pub struct User { + handle: ptr::NonNull, +} + +impl Drop for User { + fn drop(&mut self) { + unsafe { BNFreeCollaborationUser(self.as_raw()) } + } +} + +impl PartialEq for User { + fn eq(&self, other: &Self) -> bool { + self.id() == other.id() + } +} +impl Eq for User {} + +impl Clone for User { + fn clone(&self) -> Self { + unsafe { + Self::from_raw( + ptr::NonNull::new(BNNewCollaborationUserReference(self.as_raw())).unwrap(), + ) + } + } +} + +impl User { + pub(crate) unsafe fn from_raw(handle: ptr::NonNull) -> Self { + Self { handle } + } + + pub(crate) unsafe fn ref_from_raw(handle: &*mut BNCollaborationUser) -> &Self { + assert!(!handle.is_null()); + mem::transmute(handle) + } + + #[allow(clippy::mut_from_ref)] + pub(crate) unsafe fn as_raw(&self) -> &mut BNCollaborationUser { + &mut *self.handle.as_ptr() + } + + /// Owning Remote + pub fn remote(&self) -> Result { + let value = unsafe { BNCollaborationUserGetRemote(self.as_raw()) }; + let handle = ptr::NonNull::new(value).ok_or(())?; + Ok(unsafe { Remote::from_raw(handle) }) + } + + /// Web api endpoint url + pub fn url(&self) -> BnString { + let value = unsafe { BNCollaborationUserGetUrl(self.as_raw()) }; + assert!(!value.is_null()); + unsafe { BnString::from_raw(value) } + } + + /// Unique id + pub fn id(&self) -> BnString { + let value = unsafe { BNCollaborationUserGetId(self.as_raw()) }; + assert!(!value.is_null()); + unsafe { BnString::from_raw(value) } + } + + /// User's login username + pub fn username(&self) -> BnString { + let value = unsafe { BNCollaborationUserGetUsername(self.as_raw()) }; + assert!(!value.is_null()); + unsafe { BnString::from_raw(value) } + } + + /// Set user's username. You will need to push the user to update the Remote + pub fn set_username(&self, username: U) -> Result<(), ()> { + let username = username.into_bytes_with_nul(); + let result = unsafe { + BNCollaborationUserSetUsername( + self.as_raw(), + username.as_ref().as_ptr() as *const ffi::c_char, + ) + }; + if result { + Ok(()) + } else { + Err(()) + } + } + + /// User's email address + pub fn email(&self) -> BnString { + let value = unsafe { BNCollaborationUserGetEmail(self.as_raw()) }; + assert!(!value.is_null()); + unsafe { BnString::from_raw(value) } + } + + /// Set user's email. You will need to push the user to update the Remote + pub fn set_email(&self, email: U) -> Result<(), ()> { + let username = email.into_bytes_with_nul(); + let result = unsafe { + BNCollaborationUserSetEmail( + self.as_raw(), + username.as_ref().as_ptr() as *const ffi::c_char, + ) + }; + if result { + Ok(()) + } else { + Err(()) + } + } + + /// String representing the last date the user logged in + pub fn last_login(&self) -> BnString { + let value = unsafe { BNCollaborationUserGetLastLogin(self.as_raw()) }; + assert!(!value.is_null()); + unsafe { BnString::from_raw(value) } + } + + /// If the user account is active and can log in + pub fn is_active(&self) -> bool { + unsafe { BNCollaborationUserIsActive(self.as_raw()) } + } + + /// Enable/disable a user account. You will need to push the user to update the Remote + pub fn set_is_active(&self, value: bool) -> Result<(), ()> { + if unsafe { BNCollaborationUserSetIsActive(self.as_raw(), value) } { + Ok(()) + } else { + Err(()) + } + } +} + +impl CoreArrayProvider for User { + type Raw = *mut BNCollaborationUser; + type Context = (); + type Wrapped<'a> = &'a User; +} + +unsafe impl CoreArrayProviderInner for User { + unsafe fn free(raw: *mut Self::Raw, count: usize, _context: &Self::Context) { + BNFreeCollaborationUserList(raw, count) + } + + unsafe fn wrap_raw<'a>(raw: &'a Self::Raw, _context: &'a Self::Context) -> Self::Wrapped<'a> { + Self::ref_from_raw(raw) + } +} diff --git a/rust/src/ffi.rs b/rust/src/ffi.rs index eb5848280..6c02ce2bc 100644 --- a/rust/src/ffi.rs +++ b/rust/src/ffi.rs @@ -12,6 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. +use core::ffi; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + macro_rules! ffi_wrap { ($n:expr, $b:expr) => {{ use std::panic; @@ -23,3 +26,267 @@ macro_rules! ffi_wrap { }) }}; } + +pub trait ProgressCallback: Sized { + type SplitProgressType: SplitProgressBuilder; + fn progress(&mut self, progress: usize, total: usize) -> bool; + + unsafe extern "C" fn cb_progress_callback( + ctxt: *mut ffi::c_void, + progress: usize, + total: usize, + ) -> bool { + let ctxt: &mut Self = &mut *(ctxt as *mut Self); + ctxt.progress(progress, total) + } + /// Split a single progress function into proportionally sized subparts. + /// This function takes the original progress function and returns a new function whose signature + /// is the same but whose output is shortened to correspond to the specified subparts. + /// + /// The length of a subpart is proportional to the sum of all the weights. + /// E.g. with `subpart_weights = &[ 25, 50, 25 ]`, this will return a function that calls + /// progress_func and maps its progress to the ranges `[0..=25, 25..=75, 75..=100]` + /// + /// Weights of subparts, described above + /// + /// * `progress_func` - Original progress function (usually updates a UI) + /// * `subpart_weights` - Weights of subparts, described above + fn split(self, subpart_weights: &'static [usize]) -> Self::SplitProgressType; +} + +pub trait SplitProgressBuilder { + type Progress<'a>: ProgressCallback + where + Self: 'a; + fn next_subpart<'a>(&'a mut self) -> Option>; +} + +impl ProgressCallback for F +where + F: FnMut(usize, usize) -> bool, +{ + type SplitProgressType = SplitProgress; + + fn progress(&mut self, progress: usize, total: usize) -> bool { + self(progress, total) + } + + fn split(self, subpart_weights: &'static [usize]) -> Self::SplitProgressType { + SplitProgress::new(self, subpart_weights) + } +} + +pub struct ProgressCallbackNop; +impl ProgressCallback for ProgressCallbackNop { + type SplitProgressType = SplitProgressNop; + + fn progress(&mut self, _progress: usize, _total: usize) -> bool { + unreachable!() + } + + unsafe extern "C" fn cb_progress_callback( + _ctxt: *mut ffi::c_void, + _progress: usize, + _total: usize, + ) -> bool { + true + } + + fn split(self, subpart_weights: &'static [usize]) -> Self::SplitProgressType { + SplitProgressNop(subpart_weights.len()) + } +} + +pub struct SplitProgressNop(usize); +impl SplitProgressBuilder for SplitProgressNop { + type Progress<'a> = ProgressCallbackNop; + + fn next_subpart(&mut self) -> Option> { + if self.0 == 0 { + return None; + } + self.0 -= 1; + Some(ProgressCallbackNop) + } +} + +pub struct SplitProgress

{ + callback: P, + subpart_weights: &'static [usize], + total: usize, + progress: usize, +} + +impl SplitProgress

{ + pub fn new(callback: P, subpart_weights: &'static [usize]) -> Self { + let total = subpart_weights.iter().sum(); + Self { + callback, + subpart_weights, + total, + progress: 0, + } + } + + pub fn next_subpart(&mut self) -> Option> { + if self.subpart_weights.is_empty() { + return None; + } + Some(SplitProgressInstance { progress: self }) + } +} + +impl SplitProgressBuilder for SplitProgress

{ + type Progress<'a> = SplitProgressInstance<'a, P> where Self: 'a; + fn next_subpart<'a>(&'a mut self) -> Option> { + self.next_subpart() + } +} + +pub struct SplitProgressInstance<'a, P: ProgressCallback> { + progress: &'a mut SplitProgress

, +} + +impl<'a, P: ProgressCallback> Drop for SplitProgressInstance<'a, P> { + fn drop(&mut self) { + self.progress.progress += self.progress.subpart_weights[0]; + self.progress.subpart_weights = &self.progress.subpart_weights[1..]; + } +} + +impl<'a, P: ProgressCallback> ProgressCallback for SplitProgressInstance<'a, P> { + type SplitProgressType = SplitProgress; + + fn progress(&mut self, progress: usize, total: usize) -> bool { + let subpart_progress = (self.progress.subpart_weights[0] * progress) / total; + let progress = self.progress.progress + subpart_progress; + self.progress + .callback + .progress(progress, self.progress.total) + } + + fn split(self, subpart_weights: &'static [usize]) -> Self::SplitProgressType { + SplitProgress::new(self, subpart_weights) + } +} + +pub(crate) fn time_from_bn(timestamp: u64) -> SystemTime { + let m = Duration::from_secs(timestamp); + UNIX_EPOCH + m +} + +#[cfg(test)] +mod test { + use std::cell::Cell; + + use super::*; + + #[test] + fn progress_simple() { + let progress = Cell::new(0); + let mut callback = |p, _| { + progress.set(p); + true + }; + callback.progress(0, 100); + assert_eq!(progress.get(), 0); + callback.progress(1, 100); + assert_eq!(progress.get(), 1); + callback.progress(50, 100); + assert_eq!(progress.get(), 50); + callback.progress(99, 100); + assert_eq!(progress.get(), 99); + callback.progress(100, 100); + assert_eq!(progress.get(), 100); + } + + #[test] + fn progress_simple_split() { + let progress = Cell::new(0); + let callback = |p, _| { + progress.set(p); + true + }; + let mut split = callback.split(&[25, 50, 25]); + // 0..=25 + let mut split_instance = split.next_subpart().unwrap(); + split_instance.progress(0, 100); + assert_eq!(progress.get(), 0); + split_instance.progress(100, 100); + assert_eq!(progress.get(), 25); + drop(split_instance); + + // 25..=75 + let mut split_instance = split.next_subpart().unwrap(); + split_instance.progress(0, 100); + assert_eq!(progress.get(), 25); + split_instance.progress(25, 100); + // there is no way to check for exact values, it depends on how the calculation is done, + // at the time or writing of this test is always round down, but we just check a range because this + // could change + assert!((36..=37).contains(&progress.get())); + split_instance.progress(50, 100); + assert_eq!(progress.get(), 50); + split_instance.progress(100, 100); + assert_eq!(progress.get(), 75); + drop(split_instance); + + // 75..=100 + let mut split_instance = split.next_subpart().unwrap(); + split_instance.progress(0, 100); + assert_eq!(progress.get(), 75); + split_instance.progress(100, 100); + assert_eq!(progress.get(), 100); + drop(split_instance); + + assert!(split.next_subpart().is_none()); + } + + #[test] + fn progress_recursive_split() { + let progress = Cell::new(0); + let callback = |p, _| { + progress.set(p); + true + }; + let mut split = callback.split(&[25, 50, 25]); + // 0..=25 + let mut split_instance = split.next_subpart().unwrap(); + split_instance.progress(0, 100); + assert_eq!(progress.get(), 0); + split_instance.progress(100, 100); + assert_eq!(progress.get(), 25); + drop(split_instance); + + // 25..=75, will get split into two parts: 25..=50 and 50..=75 + { + let split_instance = split.next_subpart().unwrap(); + let mut sub_split = split_instance.split(&[50, 50]); + // 25..=50 + let mut sub_split_instance = sub_split.next_subpart().unwrap(); + sub_split_instance.progress(0, 100); + assert_eq!(progress.get(), 25); + sub_split_instance.progress(100, 100); + assert_eq!(progress.get(), 50); + drop(sub_split_instance); + + // 50..=75 + let mut sub_split_instance = sub_split.next_subpart().unwrap(); + sub_split_instance.progress(0, 100); + assert_eq!(progress.get(), 50); + sub_split_instance.progress(100, 100); + assert_eq!(progress.get(), 75); + drop(sub_split_instance); + } + + // 75..=100 + let mut split_instance = split.next_subpart().unwrap(); + split_instance.progress(0, 100); + assert_eq!(progress.get(), 75); + split_instance.progress(100, 100); + assert_eq!(progress.get(), 100); + drop(split_instance); + + assert!(split.next_subpart().is_none()); + } +} diff --git a/rust/src/filemetadata.rs b/rust/src/filemetadata.rs index 7d385a4c8..08cffecfb 100644 --- a/rust/src/filemetadata.rs +++ b/rust/src/filemetadata.rs @@ -22,12 +22,12 @@ use binaryninjacore_sys::{ BNFreeFileMetadata, BNGetCurrentOffset, BNGetCurrentView, + //BNSetFileMetadataNavigationHandler, + BNGetFileMetadataDatabase, BNGetFileViewOfType, BNGetFilename, BNIsAnalysisChanged, BNIsBackedByDatabase, - //BNSetFileMetadataNavigationHandler, - BNGetFileMetadataDatabase, BNIsFileModified, BNMarkFileModified, BNMarkFileSaved, diff --git a/rust/src/lib.rs b/rust/src/lib.rs index c086478d9..71a429ec1 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -131,14 +131,15 @@ pub mod binaryreader; pub mod binaryview; pub mod binarywriter; pub mod callingconvention; +pub mod collaboration; pub mod command; +pub mod component; pub mod custombinaryview; pub mod database; pub mod databuffer; pub mod debuginfo; pub mod demangle; pub mod disassembly; -pub mod component; pub mod downloadprovider; pub mod externallibrary; pub mod fileaccessor; @@ -166,8 +167,8 @@ pub mod string; pub mod symbol; pub mod tags; pub mod templatesimplifier; -pub mod typelibrary; pub mod typearchive; +pub mod typelibrary; pub mod types; pub mod update; diff --git a/rust/src/typearchive.rs b/rust/src/typearchive.rs index 71a7058e9..577e46490 100644 --- a/rust/src/typearchive.rs +++ b/rust/src/typearchive.rs @@ -945,3 +945,95 @@ unsafe extern "C" fn cb_type_deleted( &Type { handle: definition }, ) } + +#[repr(transparent)] +pub struct TypeArchiveMergeConflict { + handle: ptr::NonNull, +} + +impl Drop for TypeArchiveMergeConflict { + fn drop(&mut self) { + unsafe { BNFreeTypeArchiveMergeConflict(self.as_raw()) } + } +} + +impl Clone for TypeArchiveMergeConflict { + fn clone(&self) -> Self { + unsafe { + Self::from_raw( + ptr::NonNull::new(BNNewTypeArchiveMergeConflictReference(self.as_raw())).unwrap(), + ) + } + } +} + +impl TypeArchiveMergeConflict { + pub(crate) unsafe fn from_raw(handle: ptr::NonNull) -> Self { + Self { handle } + } + + pub(crate) unsafe fn ref_from_raw(handle: &*mut BNTypeArchiveMergeConflict) -> &Self { + assert!(!handle.is_null()); + mem::transmute(handle) + } + + #[allow(clippy::mut_from_ref)] + pub(crate) unsafe fn as_raw(&self) -> &mut BNTypeArchiveMergeConflict { + &mut *self.handle.as_ptr() + } + + pub fn get_type_archive(&self) -> Option { + let value = unsafe { BNTypeArchiveMergeConflictGetTypeArchive(self.as_raw()) }; + ptr::NonNull::new(value).map(|handle| unsafe { TypeArchive::from_raw(handle) }) + } + + pub fn type_id(&self) -> BnString { + let value = unsafe { BNTypeArchiveMergeConflictGetTypeId(self.as_raw()) }; + assert!(!value.is_null()); + unsafe { BnString::from_raw(value) } + } + + pub fn base_snapshot_id(&self) -> BnString { + let value = unsafe { BNTypeArchiveMergeConflictGetBaseSnapshotId(self.as_raw()) }; + assert!(!value.is_null()); + unsafe { BnString::from_raw(value) } + } + + pub fn first_snapshot_id(&self) -> BnString { + let value = unsafe { BNTypeArchiveMergeConflictGetFirstSnapshotId(self.as_raw()) }; + assert!(!value.is_null()); + unsafe { BnString::from_raw(value) } + } + + pub fn second_snapshot_id(&self) -> BnString { + let value = unsafe { BNTypeArchiveMergeConflictGetSecondSnapshotId(self.as_raw()) }; + assert!(!value.is_null()); + unsafe { BnString::from_raw(value) } + } + + pub fn success(&self, value: S) -> bool { + let value = value.into_bytes_with_nul(); + unsafe { + BNTypeArchiveMergeConflictSuccess( + self.as_raw(), + value.as_ref().as_ptr() as *const ffi::c_char, + ) + } + } +} + +impl CoreArrayProvider for TypeArchiveMergeConflict { + type Raw = *mut BNTypeArchiveMergeConflict; + type Context = (); + type Wrapped<'a> = &'a Self; +} + +unsafe impl CoreArrayProviderInner for TypeArchiveMergeConflict { + unsafe fn free(raw: *mut Self::Raw, count: usize, _context: &Self::Context) { + BNFreeTypeArchiveMergeConflictList(raw, count) + } + + unsafe fn wrap_raw<'a>(raw: &'a Self::Raw, _context: &'a Self::Context) -> Self::Wrapped<'a> { + Self::ref_from_raw(raw) + } +}