diff options
Diffstat (limited to 'rust/src/collaboration')
| -rw-r--r-- | rust/src/collaboration/changeset.rs | 116 | ||||
| -rw-r--r-- | rust/src/collaboration/file.rs | 647 | ||||
| -rw-r--r-- | rust/src/collaboration/folder.rs | 180 | ||||
| -rw-r--r-- | rust/src/collaboration/group.rs | 196 | ||||
| -rw-r--r-- | rust/src/collaboration/merge.rs | 204 | ||||
| -rw-r--r-- | rust/src/collaboration/permission.rs | 159 | ||||
| -rw-r--r-- | rust/src/collaboration/project.rs | 1001 | ||||
| -rw-r--r-- | rust/src/collaboration/remote.rs | 959 | ||||
| -rw-r--r-- | rust/src/collaboration/snapshot.rs | 368 | ||||
| -rw-r--r-- | rust/src/collaboration/sync.rs | 941 | ||||
| -rw-r--r-- | rust/src/collaboration/undo.rs | 166 | ||||
| -rw-r--r-- | rust/src/collaboration/user.rs | 154 |
12 files changed, 5091 insertions, 0 deletions
diff --git a/rust/src/collaboration/changeset.rs b/rust/src/collaboration/changeset.rs new file mode 100644 index 00000000..9d7cdb7c --- /dev/null +++ b/rust/src/collaboration/changeset.rs @@ -0,0 +1,116 @@ +use binaryninjacore_sys::*; +use std::ffi::c_char; +use std::ptr::NonNull; + +use super::{RemoteFile, RemoteUser}; + +use crate::database::snapshot::SnapshotId; +use crate::database::Database; +use crate::rc::{Array, CoreArrayProvider, CoreArrayProviderInner, Guard, Ref, RefCountable}; +use crate::string::{BnStrCompatible, BnString}; + +/// A collection of snapshots in a local database +#[repr(transparent)] +pub struct Changeset { + handle: NonNull<BNCollaborationChangeset>, +} + +impl Changeset { + pub(crate) unsafe fn from_raw(handle: NonNull<BNCollaborationChangeset>) -> Self { + Self { handle } + } + + #[allow(unused)] + pub(crate) unsafe fn ref_from_raw(handle: NonNull<BNCollaborationChangeset>) -> Ref<Self> { + Ref::new(Self { handle }) + } + + /// Owning database for snapshots + pub fn database(&self) -> Result<Database, ()> { + let result = unsafe { BNCollaborationChangesetGetDatabase(self.handle.as_ptr()) }; + let raw = NonNull::new(result).ok_or(())?; + Ok(unsafe { Database::from_raw(raw) }) + } + + /// Relevant remote File object + pub fn file(&self) -> Result<Ref<RemoteFile>, ()> { + let result = unsafe { BNCollaborationChangesetGetFile(self.handle.as_ptr()) }; + NonNull::new(result) + .map(|raw| unsafe { RemoteFile::ref_from_raw(raw) }) + .ok_or(()) + } + + /// List of snapshot ids in the database + pub fn snapshot_ids(&self) -> Result<Array<SnapshotId>, ()> { + let mut count = 0; + let result = + unsafe { BNCollaborationChangesetGetSnapshotIds(self.handle.as_ptr(), &mut count) }; + (!result.is_null()) + .then(|| unsafe { Array::new(result, count, ()) }) + .ok_or(()) + } + + /// Relevant remote author User + pub fn author(&self) -> Result<Ref<RemoteUser>, ()> { + let result = unsafe { BNCollaborationChangesetGetAuthor(self.handle.as_ptr()) }; + NonNull::new(result) + .map(|raw| unsafe { RemoteUser::ref_from_raw(raw) }) + .ok_or(()) + } + + /// Changeset name + pub fn name(&self) -> BnString { + let result = unsafe { BNCollaborationChangesetGetName(self.handle.as_ptr()) }; + 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<S: BnStrCompatible>(&self, value: S) -> bool { + let value = value.into_bytes_with_nul(); + unsafe { + BNCollaborationChangesetSetName( + self.handle.as_ptr(), + value.as_ref().as_ptr() as *const c_char, + ) + } + } +} + +impl ToOwned for Changeset { + type Owned = Ref<Self>; + + fn to_owned(&self) -> Self::Owned { + unsafe { RefCountable::inc_ref(self) } + } +} + +unsafe impl RefCountable for Changeset { + unsafe fn inc_ref(handle: &Self) -> Ref<Self> { + Ref::new(Self { + handle: NonNull::new(BNNewCollaborationChangesetReference(handle.handle.as_ptr())) + .unwrap(), + }) + } + + unsafe fn dec_ref(handle: &Self) { + BNFreeCollaborationChangeset(handle.handle.as_ptr()); + } +} + +impl CoreArrayProvider for Changeset { + type Raw = *mut BNCollaborationChangeset; + type Context = (); + type Wrapped<'a> = Guard<'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> { + let raw_ptr = NonNull::new(*raw).unwrap(); + Guard::new(Self::from_raw(raw_ptr), context) + } +} diff --git a/rust/src/collaboration/file.rs b/rust/src/collaboration/file.rs new file mode 100644 index 00000000..2651d3c7 --- /dev/null +++ b/rust/src/collaboration/file.rs @@ -0,0 +1,647 @@ +use std::ffi::{c_char, c_void}; +use std::fmt::{Debug, Formatter}; +use std::ptr::NonNull; +use std::time::SystemTime; + +use binaryninjacore_sys::*; + +use super::{ + sync, DatabaseConflictHandler, DatabaseConflictHandlerFail, NameChangeset, NoNameChangeset, + Remote, RemoteFolder, RemoteProject, RemoteSnapshot, +}; + +use crate::binary_view::{BinaryView, BinaryViewExt}; +use crate::database::Database; +use crate::file_metadata::FileMetadata; +use crate::progress::{NoProgressCallback, ProgressCallback, SplitProgressBuilder}; +use crate::project::file::ProjectFile; +use crate::rc::{Array, CoreArrayProvider, CoreArrayProviderInner, Guard, Ref, RefCountable}; +use crate::string::{BnStrCompatible, BnString}; + +pub type RemoteFileType = BNRemoteFileType; + +/// A remote project file. It controls the various snapshots and raw file contents associated with the analysis. +#[repr(transparent)] +pub struct RemoteFile { + pub(crate) handle: NonNull<BNRemoteFile>, +} + +impl RemoteFile { + pub(crate) unsafe fn from_raw(handle: NonNull<BNRemoteFile>) -> Self { + Self { handle } + } + + pub(crate) unsafe fn ref_from_raw(handle: NonNull<BNRemoteFile>) -> Ref<Self> { + Ref::new(Self { handle }) + } + + /// 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<Option<Ref<RemoteFile>>, ()> { + // TODO: This sync should be removed? + if !sync::pull_files(database)? { + return Ok(None); + } + sync::get_remote_file_for_local_database(database) + } + + /// Look up the [`RemoteFile`] for a local [`BinaryView`], or None if there is no matching + /// remote File found. + pub fn get_for_binary_view(bv: &BinaryView) -> Result<Option<Ref<RemoteFile>>, ()> { + 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<ProjectFile, ()> { + let result = unsafe { BNRemoteFileGetCoreFile(self.handle.as_ptr()) }; + NonNull::new(result) + .map(|handle| unsafe { ProjectFile::from_raw(handle) }) + .ok_or(()) + } + + pub fn project(&self) -> Result<Ref<RemoteProject>, ()> { + let result = unsafe { BNRemoteFileGetProject(self.handle.as_ptr()) }; + NonNull::new(result) + .map(|handle| unsafe { RemoteProject::ref_from_raw(handle) }) + .ok_or(()) + } + + pub fn remote(&self) -> Result<Ref<Remote>, ()> { + let result = unsafe { BNRemoteFileGetRemote(self.handle.as_ptr()) }; + NonNull::new(result) + .map(|handle| unsafe { Remote::ref_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<Option<Ref<RemoteFolder>>, ()> { + let project = self.project()?; + if !project.has_pulled_folders() { + project.pull_folders()?; + } + let result = unsafe { BNRemoteFileGetFolder(self.handle.as_ptr()) }; + Ok(NonNull::new(result).map(|handle| unsafe { RemoteFolder::ref_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(std::ptr::null_mut(), |f| f.handle.as_ptr()); + let success = unsafe { BNRemoteFileSetFolder(self.handle.as_ptr(), folder_raw) }; + success.then_some(()).ok_or(()) + } + + pub fn set_metadata<S: BnStrCompatible>(&self, folder: S) -> Result<(), ()> { + let folder_raw = folder.into_bytes_with_nul(); + let success = unsafe { + BNRemoteFileSetMetadata( + self.handle.as_ptr(), + folder_raw.as_ref().as_ptr() as *const c_char, + ) + }; + success.then_some(()).ok_or(()) + } + + /// Web API endpoint URL + pub fn url(&self) -> BnString { + let result = unsafe { BNRemoteFileGetUrl(self.handle.as_ptr()) }; + 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.handle.as_ptr()) }; + assert!(!result.is_null()); + unsafe { BnString::from_raw(result) } + } + + pub fn user_positions_url(&self) -> BnString { + let result = unsafe { BNRemoteFileGetUserPositionsUrl(self.handle.as_ptr()) }; + assert!(!result.is_null()); + unsafe { BnString::from_raw(result) } + } + + /// Unique ID + pub fn id(&self) -> BnString { + let result = unsafe { BNRemoteFileGetId(self.handle.as_ptr()) }; + 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.handle.as_ptr()) } + } + + /// Created date of the file + pub fn created(&self) -> SystemTime { + let result = unsafe { BNRemoteFileGetCreated(self.handle.as_ptr()) }; + crate::ffi::time_from_bn(result.try_into().unwrap()) + } + + pub fn created_by(&self) -> BnString { + let result = unsafe { BNRemoteFileGetCreatedBy(self.handle.as_ptr()) }; + 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.handle.as_ptr()) }; + 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.handle.as_ptr()) }; + 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.handle.as_ptr()) }; + assert!(!result.is_null()); + unsafe { BnString::from_raw(result) } + } + + pub fn last_snapshot_name(&self) -> BnString { + let result = unsafe { BNRemoteFileGetLastSnapshotName(self.handle.as_ptr()) }; + 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.handle.as_ptr()) }; + assert!(!result.is_null()); + unsafe { BnString::from_raw(result) } + } + + /// Displayed name of file + pub fn name(&self) -> BnString { + let result = unsafe { BNRemoteFileGetName(self.handle.as_ptr()) }; + 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<S: BnStrCompatible>(&self, name: S) -> Result<(), ()> { + let name = name.into_bytes_with_nul(); + let success = unsafe { + BNRemoteFileSetName( + self.handle.as_ptr(), + name.as_ref().as_ptr() as *const c_char, + ) + }; + success.then_some(()).ok_or(()) + } + + /// Desciprtion of the file + pub fn description(&self) -> BnString { + let result = unsafe { BNRemoteFileGetDescription(self.handle.as_ptr()) }; + 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<S: BnStrCompatible>(&self, description: S) -> Result<(), ()> { + let description = description.into_bytes_with_nul(); + let success = unsafe { + BNRemoteFileSetDescription( + self.handle.as_ptr(), + description.as_ref().as_ptr() as *const c_char, + ) + }; + success.then_some(()).ok_or(()) + } + + pub fn metadata(&self) -> BnString { + let result = unsafe { BNRemoteFileGetMetadata(self.handle.as_ptr()) }; + 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.handle.as_ptr()) } + } + + /// 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.handle.as_ptr()) }; + 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.handle.as_ptr()) } + } + + /// 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<Array<RemoteSnapshot>, ()> { + // TODO: This sync should be removed? + if !self.has_pulled_snapshots() { + self.pull_snapshots()?; + } + let mut count = 0; + let result = unsafe { BNRemoteFileGetSnapshots(self.handle.as_ptr(), &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<S: BnStrCompatible>( + &self, + id: S, + ) -> Result<Option<Ref<RemoteSnapshot>>, ()> { + // TODO: This sync should be removed? + if !self.has_pulled_snapshots() { + self.pull_snapshots()?; + } + let id = id.into_bytes_with_nul(); + let result = unsafe { + BNRemoteFileGetSnapshotById(self.handle.as_ptr(), id.as_ref().as_ptr() as *const c_char) + }; + Ok(NonNull::new(result).map(|handle| unsafe { RemoteSnapshot::ref_from_raw(handle) })) + } + + /// Pull the list of Snapshots from the Remote. + pub fn pull_snapshots(&self) -> Result<(), ()> { + self.pull_snapshots_with_progress(NoProgressCallback) + } + + /// Pull the list of Snapshots from the Remote. + pub fn pull_snapshots_with_progress<P: ProgressCallback>( + &self, + mut progress: P, + ) -> Result<(), ()> { + let success = unsafe { + BNRemoteFilePullSnapshots( + self.handle.as_ptr(), + Some(P::cb_progress_callback), + &mut progress as *mut P as *mut 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) + pub fn create_snapshot<S, I>( + &self, + name: S, + contents: &mut [u8], + analysis_cache_contexts: &mut [u8], + file: &mut [u8], + parent_ids: I, + ) -> Result<Ref<RemoteSnapshot>, ()> + where + S: BnStrCompatible, + I: IntoIterator, + I::Item: BnStrCompatible, + { + self.create_snapshot_with_progress( + name, + contents, + analysis_cache_contexts, + file, + parent_ids, + NoProgressCallback, + ) + } + + /// 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_with_progress<S, I, P>( + &self, + name: S, + contents: &mut [u8], + analysis_cache_contexts: &mut [u8], + file: &mut [u8], + parent_ids: I, + mut progress: P, + ) -> Result<Ref<RemoteSnapshot>, ()> + 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 c_char) + .collect(); + let result = unsafe { + BNRemoteFileCreateSnapshot( + self.handle.as_ptr(), + name.as_ref().as_ptr() as *const 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 c_void, + ) + }; + let handle = NonNull::new(result).ok_or(())?; + Ok(unsafe { RemoteSnapshot::ref_from_raw(handle) }) + } + + // Delete a snapshot from the remote + pub fn delete_snapshot(&self, snapshot: &RemoteSnapshot) -> Result<(), ()> { + let success = + unsafe { BNRemoteFileDeleteSnapshot(self.handle.as_ptr(), snapshot.handle.as_ptr()) }; + success.then_some(()).ok_or(()) + } + + // TODO - This passes and returns a c++ `std::vector<T>`. 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 dropping it. + //pub fn download_file<S, F>(&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.handle.as_ptr(), + // Some(F::cb_progress_callback), + // &mut progress_function as *mut _ as *mut c_void, + // &mut data, + // &mut data_len, + // ) + // }; + // todo!() + //} + + pub fn request_user_positions(&self) -> BnString { + let result = unsafe { BNRemoteFileRequestUserPositions(self.handle.as_ptr()) }; + assert!(!result.is_null()); + unsafe { BnString::from_raw(result) } + } + + pub fn request_chat_log(&self) -> BnString { + let result = unsafe { BNRemoteFileRequestChatLog(self.handle.as_ptr()) }; + assert!(!result.is_null()); + unsafe { BnString::from_raw(result) } + } + + // TODO: AsRef<Path> + /// 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<S>(&self, db_path: S) -> Result<Ref<FileMetadata>, ()> + where + S: BnStrCompatible, + { + sync::download_file(self, db_path) + } + + // TODO: AsRef<Path> + /// 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_with_progress<S, F>( + &self, + db_path: S, + progress_function: F, + ) -> Result<Ref<FileMetadata>, ()> + where + S: BnStrCompatible, + F: ProgressCallback, + { + sync::download_file_with_progress(self, db_path, progress_function) + } + + /// Download a remote file and save it to a BNDB at the given `path`, returning the associated [`FileMetadata`]. + pub fn download_database<S: BnStrCompatible>(&self, path: S) -> Result<Ref<FileMetadata>, ()> { + let file = self.download(path)?; + let database = file.database().ok_or(())?; + self.sync(&database, DatabaseConflictHandlerFail, NoNameChangeset)?; + Ok(file) + } + + // TODO: This might be a bad helper... maybe remove... + // TODO: AsRef<Path> + /// Download a remote file and save it to a BNDB at the given `path`. + pub fn download_database_with_progress<S: BnStrCompatible>( + &self, + path: S, + progress: impl ProgressCallback, + ) -> Result<Ref<FileMetadata>, ()> { + let mut progress = progress.split(&[50, 50]); + let file = self.download_with_progress(path, progress.next_subpart().unwrap())?; + let database = file.database().ok_or(())?; + self.sync_with_progress( + &database, + DatabaseConflictHandlerFail, + NoNameChangeset, + progress.next_subpart().unwrap(), + )?; + 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 + pub fn sync<C: DatabaseConflictHandler, N: NameChangeset>( + &self, + database: &Database, + conflict_handler: C, + name_changeset: N, + ) -> Result<(), ()> { + sync::sync_database(database, self, conflict_handler, name_changeset) + } + + /// 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_with_progress<C: DatabaseConflictHandler, P: ProgressCallback, N: NameChangeset>( + &self, + database: &Database, + conflict_handler: C, + name_changeset: N, + progress: P, + ) -> Result<(), ()> { + sync::sync_database_with_progress( + database, + self, + conflict_handler, + name_changeset, + progress, + ) + } + + /// 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 + pub fn pull<C, N>( + &self, + database: &Database, + conflict_handler: C, + name_changeset: N, + ) -> Result<usize, ()> + where + C: DatabaseConflictHandler, + N: NameChangeset, + { + sync::pull_database(database, self, conflict_handler, 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_with_progress<C, P, N>( + &self, + database: &Database, + conflict_handler: C, + name_changeset: N, + progress: P, + ) -> Result<usize, ()> + where + C: DatabaseConflictHandler, + P: ProgressCallback, + N: NameChangeset, + { + sync::pull_database_with_progress( + database, + self, + conflict_handler, + name_changeset, + progress, + ) + } + + /// Push locally added snapshots to the remote. + /// + /// * `bv_or_db` - Binary view or database to sync with + pub fn push<P>(&self, database: &Database) -> Result<usize, ()> + where + P: ProgressCallback, + { + sync::push_database(database, self) + } + + /// 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_with_progress<P>(&self, database: &Database, progress: P) -> Result<usize, ()> + where + P: ProgressCallback, + { + sync::push_database_with_progress(database, self, progress) + } +} + +impl Debug for RemoteFile { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("RemoteFile") + .field("id", &self.id()) + .field("name", &self.name()) + .field("description", &self.description()) + .field("metadata", &self.metadata()) + .field("size", &self.size()) + .field( + "snapshot_count", + &self.snapshots().map(|s| s.len()).unwrap_or(0), + ) + .finish() + } +} + +impl PartialEq for RemoteFile { + fn eq(&self, other: &Self) -> bool { + self.id() == other.id() + } +} +impl Eq for RemoteFile {} + +impl ToOwned for RemoteFile { + type Owned = Ref<Self>; + + fn to_owned(&self) -> Self::Owned { + unsafe { RefCountable::inc_ref(self) } + } +} + +unsafe impl RefCountable for RemoteFile { + unsafe fn inc_ref(handle: &Self) -> Ref<Self> { + Ref::new(Self { + handle: NonNull::new(BNNewRemoteFileReference(handle.handle.as_ptr())).unwrap(), + }) + } + + unsafe fn dec_ref(handle: &Self) { + BNFreeRemoteFile(handle.handle.as_ptr()); + } +} + +impl CoreArrayProvider for RemoteFile { + type Raw = *mut BNRemoteFile; + type Context = (); + type Wrapped<'a> = Guard<'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> { + let raw_ptr = NonNull::new(*raw).unwrap(); + Guard::new(Self::from_raw(raw_ptr), context) + } +} diff --git a/rust/src/collaboration/folder.rs b/rust/src/collaboration/folder.rs new file mode 100644 index 00000000..90a85f1c --- /dev/null +++ b/rust/src/collaboration/folder.rs @@ -0,0 +1,180 @@ +use super::{Remote, RemoteProject}; +use binaryninjacore_sys::*; +use std::ffi::c_char; +use std::ptr::NonNull; + +use crate::project::folder::ProjectFolder; +use crate::rc::{CoreArrayProvider, CoreArrayProviderInner, Guard, Ref, RefCountable}; +use crate::string::{BnStrCompatible, BnString}; + +#[repr(transparent)] +pub struct RemoteFolder { + pub(crate) handle: NonNull<BNRemoteFolder>, +} + +impl RemoteFolder { + pub(crate) unsafe fn from_raw(handle: NonNull<BNRemoteFolder>) -> Self { + Self { handle } + } + + pub(crate) unsafe fn ref_from_raw(handle: NonNull<BNRemoteFolder>) -> Ref<Self> { + Ref::new(Self { handle }) + } + + // TODO: Rename to local folder? + // TODO: Bump this to an option + /// Get the core folder associated with this remote folder. + pub fn core_folder(&self) -> Result<Ref<ProjectFolder>, ()> { + let result = unsafe { BNRemoteFolderGetCoreFolder(self.handle.as_ptr()) }; + NonNull::new(result) + .map(|handle| unsafe { ProjectFolder::ref_from_raw(handle) }) + .ok_or(()) + } + + // TODO: Bump this to an option + /// Get the owning project of this folder. + pub fn project(&self) -> Result<Ref<RemoteProject>, ()> { + let result = unsafe { BNRemoteFolderGetProject(self.handle.as_ptr()) }; + NonNull::new(result) + .map(|handle| unsafe { RemoteProject::ref_from_raw(handle) }) + .ok_or(()) + } + + // TODO: Bump this to an option + /// Get the owning remote of this folder. + pub fn remote(&self) -> Result<Ref<Remote>, ()> { + let result = unsafe { BNRemoteFolderGetRemote(self.handle.as_ptr()) }; + NonNull::new(result) + .map(|handle| unsafe { Remote::ref_from_raw(handle) }) + .ok_or(()) + } + + // TODO: Should this pull folders? + // TODO: If it does we keep the result? + /// Get the parent folder, if available. + pub fn parent(&self) -> Result<Option<Ref<RemoteFolder>>, ()> { + let project = self.project()?; + // TODO: This sync should be removed? + if !project.has_pulled_folders() { + project.pull_folders()?; + } + let mut parent_handle = std::ptr::null_mut(); + let success = unsafe { BNRemoteFolderGetParent(self.handle.as_ptr(), &mut parent_handle) }; + success + .then(|| { + NonNull::new(parent_handle) + .map(|handle| unsafe { RemoteFolder::ref_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(std::ptr::null_mut(), |p| p.handle.as_ptr()); + let success = unsafe { BNRemoteFolderSetParent(self.handle.as_ptr(), parent_handle) }; + success.then_some(()).ok_or(()) + } + + /// Get web API endpoint URL. + pub fn url(&self) -> BnString { + let result = unsafe { BNRemoteFolderGetUrl(self.handle.as_ptr()) }; + assert!(!result.is_null()); + unsafe { BnString::from_raw(result) } + } + + /// Get unique ID. + pub fn id(&self) -> BnString { + let result = unsafe { BNRemoteFolderGetId(self.handle.as_ptr()) }; + 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<BnString> { + let mut parent_id = std::ptr::null_mut(); + let have = unsafe { BNRemoteFolderGetParentId(self.handle.as_ptr(), &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.handle.as_ptr()) }; + 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<S: BnStrCompatible>(&self, name: S) -> Result<(), ()> { + let name = name.into_bytes_with_nul(); + let success = unsafe { + BNRemoteFolderSetName( + self.handle.as_ptr(), + name.as_ref().as_ptr() as *const c_char, + ) + }; + success.then_some(()).ok_or(()) + } + + /// Description of the folder + pub fn description(&self) -> BnString { + let result = unsafe { BNRemoteFolderGetDescription(self.handle.as_ptr()) }; + 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<S: BnStrCompatible>(&self, description: S) -> Result<(), ()> { + let description = description.into_bytes_with_nul(); + let success = unsafe { + BNRemoteFolderSetDescription( + self.handle.as_ptr(), + description.as_ref().as_ptr() as *const c_char, + ) + }; + success.then_some(()).ok_or(()) + } +} + +impl PartialEq for RemoteFolder { + fn eq(&self, other: &Self) -> bool { + self.id() == other.id() + } +} +impl Eq for RemoteFolder {} + +impl ToOwned for RemoteFolder { + type Owned = Ref<Self>; + + fn to_owned(&self) -> Self::Owned { + unsafe { RefCountable::inc_ref(self) } + } +} + +unsafe impl RefCountable for RemoteFolder { + unsafe fn inc_ref(handle: &Self) -> Ref<Self> { + Ref::new(Self { + handle: NonNull::new(BNNewRemoteFolderReference(handle.handle.as_ptr())).unwrap(), + }) + } + + unsafe fn dec_ref(handle: &Self) { + BNFreeRemoteFolder(handle.handle.as_ptr()); + } +} + +impl CoreArrayProvider for RemoteFolder { + type Raw = *mut BNRemoteFolder; + type Context = (); + type Wrapped<'a> = Guard<'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> { + let raw_ptr = NonNull::new(*raw).unwrap(); + Guard::new(Self::from_raw(raw_ptr), context) + } +} diff --git a/rust/src/collaboration/group.rs b/rust/src/collaboration/group.rs new file mode 100644 index 00000000..bad09d6c --- /dev/null +++ b/rust/src/collaboration/group.rs @@ -0,0 +1,196 @@ +use super::Remote; +use crate::rc::{Array, CoreArrayProvider, CoreArrayProviderInner, Guard, Ref, RefCountable}; +use crate::string::{BnStrCompatible, BnString}; +use binaryninjacore_sys::*; +use std::ffi::c_char; +use std::fmt; +use std::fmt::{Display, Formatter}; +use std::ptr::NonNull; + +#[repr(transparent)] +pub struct RemoteGroup { + pub(crate) handle: NonNull<BNCollaborationGroup>, +} + +impl RemoteGroup { + pub(crate) unsafe fn from_raw(handle: NonNull<BNCollaborationGroup>) -> Self { + Self { handle } + } + + pub(crate) unsafe fn ref_from_raw(handle: NonNull<BNCollaborationGroup>) -> Ref<Self> { + Ref::new(Self { handle }) + } + + /// Owning Remote + pub fn remote(&self) -> Result<Ref<Remote>, ()> { + let value = unsafe { BNCollaborationGroupGetRemote(self.handle.as_ptr()) }; + NonNull::new(value) + .map(|handle| unsafe { Remote::ref_from_raw(handle) }) + .ok_or(()) + } + + /// Web api endpoint url + pub fn url(&self) -> BnString { + let value = unsafe { BNCollaborationGroupGetUrl(self.handle.as_ptr()) }; + assert!(!value.is_null()); + unsafe { BnString::from_raw(value) } + } + + /// Unique id + pub fn id(&self) -> GroupId { + GroupId(unsafe { BNCollaborationGroupGetId(self.handle.as_ptr()) }) + } + + /// Group name + pub fn name(&self) -> BnString { + let value = unsafe { BNCollaborationGroupGetName(self.handle.as_ptr()) }; + 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<U: BnStrCompatible>(&self, name: U) { + let name = name.into_bytes_with_nul(); + unsafe { + BNCollaborationGroupSetName( + self.handle.as_ptr(), + name.as_ref().as_ptr() as *const c_char, + ) + } + } + + /// Get list of users in the group + pub fn users(&self) -> Result<(Array<BnString>, Array<BnString>), ()> { + let mut usernames = std::ptr::null_mut(); + let mut user_ids = std::ptr::null_mut(); + let mut count = 0; + // TODO: This should only fail if collaboration is not supported. + // TODO: Because you should not have a RemoteGroup at that point we can ignore? + let success = unsafe { + BNCollaborationGroupGetUsers( + self.handle.as_ptr(), + &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(()) + } + + // TODO: Are any permissions required to the set the remote group users? + /// 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_users<I>(&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 c_char) + .collect(); + // TODO: This should only fail if collaboration is not supported. + // TODO: Because you should not have a RemoteGroup at that point we can ignore? + // TODO: Do you need any permissions to do this? + let success = unsafe { + BNCollaborationGroupSetUsernames( + self.handle.as_ptr(), + 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<U: BnStrCompatible>(&self, username: U) -> bool { + let username = username.into_bytes_with_nul(); + unsafe { + BNCollaborationGroupContainsUser( + self.handle.as_ptr(), + username.as_ref().as_ptr() as *const c_char, + ) + } + } +} + +impl PartialEq for RemoteGroup { + fn eq(&self, other: &Self) -> bool { + self.id() == other.id() + } +} +impl Eq for RemoteGroup {} + +impl ToOwned for RemoteGroup { + type Owned = Ref<Self>; + + fn to_owned(&self) -> Self::Owned { + unsafe { RefCountable::inc_ref(self) } + } +} + +unsafe impl RefCountable for RemoteGroup { + unsafe fn inc_ref(handle: &Self) -> Ref<Self> { + Ref::new(Self { + handle: NonNull::new(BNNewCollaborationGroupReference(handle.handle.as_ptr())).unwrap(), + }) + } + + unsafe fn dec_ref(handle: &Self) { + BNFreeCollaborationGroup(handle.handle.as_ptr()); + } +} + +impl CoreArrayProvider for RemoteGroup { + type Raw = *mut BNCollaborationGroup; + type Context = (); + type Wrapped<'a> = Guard<'a, Self>; +} + +unsafe impl CoreArrayProviderInner for RemoteGroup { + 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> { + let raw_ptr = NonNull::new(*raw).unwrap(); + Guard::new(Self::from_raw(raw_ptr), context) + } +} + +#[repr(transparent)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct GroupId(pub u64); + +impl Display for GroupId { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.write_fmt(format_args!("{}", self.0)) + } +} + +impl CoreArrayProvider for GroupId { + type Raw = u64; + type Context = (); + type Wrapped<'a> = GroupId; +} + +unsafe impl CoreArrayProviderInner for GroupId { + 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> { + GroupId(*raw) + } +} diff --git a/rust/src/collaboration/merge.rs b/rust/src/collaboration/merge.rs new file mode 100644 index 00000000..2d28725c --- /dev/null +++ b/rust/src/collaboration/merge.rs @@ -0,0 +1,204 @@ +use binaryninjacore_sys::*; +use std::ffi::c_char; +use std::ptr::NonNull; + +use crate::database::{snapshot::Snapshot, Database}; +use crate::file_metadata::FileMetadata; +use crate::rc::{CoreArrayProvider, CoreArrayProviderInner, Guard, Ref, RefCountable}; +use crate::string::{BnStrCompatible, BnString}; + +pub type MergeConflictDataType = BNMergeConflictDataType; + +/// Structure representing an individual merge conflict +#[repr(transparent)] +pub struct MergeConflict { + handle: NonNull<BNAnalysisMergeConflict>, +} + +impl MergeConflict { + pub(crate) unsafe fn from_raw(handle: NonNull<BNAnalysisMergeConflict>) -> Self { + Self { handle } + } + + #[allow(unused)] + pub(crate) unsafe fn ref_from_raw(handle: NonNull<BNAnalysisMergeConflict>) -> Ref<Self> { + Ref::new(Self { handle }) + } + + /// Database backing all snapshots in the merge conflict + pub fn database(&self) -> Database { + let result = unsafe { BNAnalysisMergeConflictGetDatabase(self.handle.as_ptr()) }; + unsafe { Database::from_raw(NonNull::new(result).unwrap()) } + } + + /// Snapshot which is the parent of the two being merged + pub fn base_snapshot(&self) -> Option<Snapshot> { + let result = unsafe { BNAnalysisMergeConflictGetBaseSnapshot(self.handle.as_ptr()) }; + NonNull::new(result).map(|handle| unsafe { Snapshot::from_raw(handle) }) + } + + /// First snapshot being merged + pub fn first_snapshot(&self) -> Option<Snapshot> { + let result = unsafe { BNAnalysisMergeConflictGetFirstSnapshot(self.handle.as_ptr()) }; + NonNull::new(result).map(|handle| unsafe { Snapshot::from_raw(handle) }) + } + + /// Second snapshot being merged + pub fn second_snapshot(&self) -> Option<Snapshot> { + let result = unsafe { BNAnalysisMergeConflictGetSecondSnapshot(self.handle.as_ptr()) }; + NonNull::new(result).map(|handle| unsafe { Snapshot::from_raw(handle) }) + } + + pub fn path_item_string<S: BnStrCompatible>(&self, path: S) -> Result<BnString, ()> { + let path = path.into_bytes_with_nul(); + let result = unsafe { + BNAnalysisMergeConflictGetPathItemString( + self.handle.as_ptr(), + path.as_ref().as_ptr() as *const 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<Ref<FileMetadata>> { + let result = unsafe { BNAnalysisMergeConflictGetBaseFile(self.handle.as_ptr()) }; + (!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<Ref<FileMetadata>> { + let result = unsafe { BNAnalysisMergeConflictGetFirstFile(self.handle.as_ptr()) }; + (!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<Ref<FileMetadata>> { + let result = unsafe { BNAnalysisMergeConflictGetSecondFile(self.handle.as_ptr()) }; + (!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<BnString> { + let result = unsafe { BNAnalysisMergeConflictGetBase(self.handle.as_ptr()) }; + (!result.is_null()).then(|| unsafe { BnString::from_raw(result) }) + } + + /// Json object for conflicting data in the base snapshot + pub fn first(&self) -> Option<BnString> { + let result = unsafe { BNAnalysisMergeConflictGetFirst(self.handle.as_ptr()) }; + (!result.is_null()).then(|| unsafe { BnString::from_raw(result) }) + } + + /// Json object for conflicting data in the second snapshot + pub fn second(&self) -> Option<BnString> { + let result = unsafe { BNAnalysisMergeConflictGetSecond(self.handle.as_ptr()) }; + (!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.handle.as_ptr()) } + } + + /// 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.handle.as_ptr()) }; + 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.handle.as_ptr()) }; + assert!(!result.is_null()); + unsafe { BnString::from_raw(result) } + } + + /// Call this when you've resolved the conflict to save the result + pub fn success<S: BnStrCompatible>(&self, value: S) -> Result<(), ()> { + let value = value.into_bytes_with_nul(); + let success = unsafe { + BNAnalysisMergeConflictSuccess( + self.handle.as_ptr(), + value.as_ref().as_ptr() as *const c_char, + ) + }; + success.then_some(()).ok_or(()) + } + + // TODO: Make a safe version of this that checks the path and if it holds a number + pub unsafe fn get_path_item_number<S: BnStrCompatible>(&self, path_key: S) -> Option<u64> { + let path_key = path_key.into_bytes_with_nul(); + let value = unsafe { + BNAnalysisMergeConflictGetPathItem( + self.handle.as_ptr(), + path_key.as_ref().as_ptr() as *const c_char, + ) + }; + match value.is_null() { + // SAFETY: The path must be a number. + false => Some(value as u64), + true => None, + } + } + + pub unsafe fn get_path_item_string<S: BnStrCompatible>(&self, path_key: S) -> Option<BnString> { + let path_key = path_key.into_bytes_with_nul(); + let value = unsafe { + BNAnalysisMergeConflictGetPathItemString( + self.handle.as_ptr(), + path_key.as_ref().as_ptr() as *const c_char, + ) + }; + match value.is_null() { + false => Some(unsafe { BnString::from_raw(value) }), + true => None, + } + } +} + +impl ToOwned for MergeConflict { + type Owned = Ref<Self>; + + fn to_owned(&self) -> Self::Owned { + unsafe { RefCountable::inc_ref(self) } + } +} + +unsafe impl RefCountable for MergeConflict { + unsafe fn inc_ref(handle: &Self) -> Ref<Self> { + Ref::new(Self { + handle: NonNull::new(BNNewAnalysisMergeConflictReference(handle.handle.as_ptr())) + .unwrap(), + }) + } + + unsafe fn dec_ref(handle: &Self) { + BNFreeAnalysisMergeConflict(handle.handle.as_ptr()); + } +} + +impl CoreArrayProvider for MergeConflict { + type Raw = *mut BNAnalysisMergeConflict; + type Context = (); + type Wrapped<'a> = Guard<'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> { + let raw_ptr = NonNull::new(*raw).unwrap(); + Guard::new(Self::from_raw(raw_ptr), context) + } +} diff --git a/rust/src/collaboration/permission.rs b/rust/src/collaboration/permission.rs new file mode 100644 index 00000000..99a7a4f2 --- /dev/null +++ b/rust/src/collaboration/permission.rs @@ -0,0 +1,159 @@ +use super::{GroupId, Remote, RemoteProject}; +use binaryninjacore_sys::*; +use std::ptr::NonNull; + +use crate::rc::{CoreArrayProvider, CoreArrayProviderInner, Guard, Ref, RefCountable}; +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 { + pub(crate) handle: NonNull<BNCollaborationPermission>, +} + +impl Permission { + pub(crate) unsafe fn from_raw(handle: NonNull<BNCollaborationPermission>) -> Self { + Self { handle } + } + + pub(crate) unsafe fn ref_from_raw(handle: NonNull<BNCollaborationPermission>) -> Ref<Self> { + Ref::new(Self { handle }) + } + + pub fn remote(&self) -> Result<Ref<Remote>, ()> { + let result = unsafe { BNCollaborationPermissionGetRemote(self.handle.as_ptr()) }; + NonNull::new(result) + .map(|handle| unsafe { Remote::ref_from_raw(handle) }) + .ok_or(()) + } + + pub fn project(&self) -> Result<Ref<RemoteProject>, ()> { + let result = unsafe { BNCollaborationPermissionGetProject(self.handle.as_ptr()) }; + NonNull::new(result) + .map(|handle| unsafe { RemoteProject::ref_from_raw(handle) }) + .ok_or(()) + } + + /// Web api endpoint url + pub fn url(&self) -> BnString { + let value = unsafe { BNCollaborationPermissionGetUrl(self.handle.as_ptr()) }; + assert!(!value.is_null()); + unsafe { BnString::from_raw(value) } + } + + /// unique id + pub fn id(&self) -> BnString { + let value = unsafe { BNCollaborationPermissionGetId(self.handle.as_ptr()) }; + assert!(!value.is_null()); + unsafe { BnString::from_raw(value) } + } + + /// Level of permission + pub fn level(&self) -> CollaborationPermissionLevel { + unsafe { BNCollaborationPermissionGetLevel(self.handle.as_ptr()) } + } + + /// 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.handle.as_ptr(), level) } + } + + /// Id of affected group + pub fn group_id(&self) -> Option<GroupId> { + let value = unsafe { BNCollaborationPermissionGetGroupId(self.handle.as_ptr()) }; + if value != 0 { + Some(GroupId(value)) + } else { + None + } + } + + /// Name of affected group + pub fn group_name(&self) -> Option<BnString> { + let value = unsafe { BNCollaborationPermissionGetGroupName(self.handle.as_ptr()) }; + 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<BnString> { + let value = unsafe { BNCollaborationPermissionGetUserId(self.handle.as_ptr()) }; + 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<BnString> { + let value = unsafe { BNCollaborationPermissionGetUsername(self.handle.as_ptr()) }; + 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.handle.as_ptr()) } + } + + /// 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.handle.as_ptr()) } + } + + /// If the permission grants the affect user/group the ability to administer the project + pub fn can_admin(&self) -> bool { + unsafe { BNCollaborationPermissionCanAdmin(self.handle.as_ptr()) } + } +} + +impl PartialEq for Permission { + fn eq(&self, other: &Self) -> bool { + self.id() == other.id() + } +} +impl Eq for Permission {} + +impl ToOwned for Permission { + type Owned = Ref<Self>; + + fn to_owned(&self) -> Self::Owned { + unsafe { RefCountable::inc_ref(self) } + } +} + +unsafe impl RefCountable for Permission { + unsafe fn inc_ref(handle: &Self) -> Ref<Self> { + Ref::new(Self { + handle: NonNull::new(BNNewCollaborationPermissionReference( + handle.handle.as_ptr(), + )) + .unwrap(), + }) + } + + unsafe fn dec_ref(handle: &Self) { + BNFreeCollaborationPermission(handle.handle.as_ptr()); + } +} + +impl CoreArrayProvider for Permission { + type Raw = *mut BNCollaborationPermission; + type Context = (); + type Wrapped<'a> = Guard<'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> { + let raw_ptr = NonNull::new(*raw).unwrap(); + Guard::new(Self::from_raw(raw_ptr), context) + } +} diff --git a/rust/src/collaboration/project.rs b/rust/src/collaboration/project.rs new file mode 100644 index 00000000..1455f6c3 --- /dev/null +++ b/rust/src/collaboration/project.rs @@ -0,0 +1,1001 @@ +use std::ffi::{c_char, c_void}; +use std::ptr::NonNull; +use std::time::SystemTime; + +use binaryninjacore_sys::*; + +use super::{ + sync, CollaborationPermissionLevel, NameChangeset, Permission, Remote, RemoteFile, + RemoteFileType, RemoteFolder, +}; + +use crate::binary_view::{BinaryView, BinaryViewExt}; +use crate::database::Database; +use crate::file_metadata::FileMetadata; +use crate::progress::{NoProgressCallback, ProgressCallback}; +use crate::project::Project; +use crate::rc::{Array, CoreArrayProvider, CoreArrayProviderInner, Guard, Ref, RefCountable}; +use crate::string::{BnStrCompatible, BnString}; + +#[repr(transparent)] +pub struct RemoteProject { + pub(crate) handle: NonNull<BNRemoteProject>, +} + +impl RemoteProject { + pub(crate) unsafe fn from_raw(handle: NonNull<BNRemoteProject>) -> Self { + Self { handle } + } + + pub(crate) unsafe fn ref_from_raw(handle: NonNull<BNRemoteProject>) -> Ref<Self> { + Ref::new(Self { handle }) + } + + /// 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.handle.as_ptr()) } + } + + /// Open the project, allowing various file and folder based apis to work, as well as + /// connecting a core Project + pub fn open(&self) -> Result<(), ()> { + self.open_with_progress(NoProgressCallback) + } + + /// Open the project, allowing various file and folder based apis to work, as well as + /// connecting a core Project + pub fn open_with_progress<F: ProgressCallback>(&self, mut progress: F) -> Result<(), ()> { + if self.is_open() { + return Ok(()); + } + let success = unsafe { + BNRemoteProjectOpen( + self.handle.as_ptr(), + Some(F::cb_progress_callback), + &mut progress as *mut F as *mut 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.handle.as_ptr()) } + } + + /// Get the Remote Project for a Database + pub fn get_for_local_database(database: &Database) -> Result<Option<Ref<Self>>, ()> { + // TODO: This sync should be removed? + if sync::pull_projects(database)? { + return Ok(None); + } + sync::get_remote_project_for_local_database(database) + } + + /// Get the Remote Project for a BinaryView + pub fn get_for_binaryview(bv: &BinaryView) -> Result<Option<Ref<Self>>, ()> { + 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<Ref<Project>, ()> { + // TODO: This sync should be removed? + self.open()?; + + let value = unsafe { BNRemoteProjectGetCoreProject(self.handle.as_ptr()) }; + NonNull::new(value) + .map(|handle| unsafe { Project::ref_from_raw(handle) }) + .ok_or(()) + } + + /// Get the owning remote + pub fn remote(&self) -> Result<Ref<Remote>, ()> { + let value = unsafe { BNRemoteProjectGetRemote(self.handle.as_ptr()) }; + NonNull::new(value) + .map(|handle| unsafe { Remote::ref_from_raw(handle) }) + .ok_or(()) + } + + /// Get the URL of the project + pub fn url(&self) -> BnString { + let result = unsafe { BNRemoteProjectGetUrl(self.handle.as_ptr()) }; + 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.handle.as_ptr()) }; + assert!(!result.is_null()); + unsafe { BnString::from_raw(result) } + } + + /// Created date of the project + pub fn created(&self) -> SystemTime { + let result = unsafe { BNRemoteProjectGetCreated(self.handle.as_ptr()) }; + 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.handle.as_ptr()) }; + crate::ffi::time_from_bn(result.try_into().unwrap()) + } + + /// Displayed name of file + pub fn name(&self) -> BnString { + let result = unsafe { BNRemoteProjectGetName(self.handle.as_ptr()) }; + 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<S: BnStrCompatible>(&self, name: S) -> Result<(), ()> { + let name = name.into_bytes_with_nul(); + let success = unsafe { + BNRemoteProjectSetName( + self.handle.as_ptr(), + name.as_ref().as_ptr() as *const c_char, + ) + }; + success.then_some(()).ok_or(()) + } + + /// Desciprtion of the file + pub fn description(&self) -> BnString { + let result = unsafe { BNRemoteProjectGetDescription(self.handle.as_ptr()) }; + 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<S: BnStrCompatible>(&self, description: S) -> Result<(), ()> { + let description = description.into_bytes_with_nul(); + let success = unsafe { + BNRemoteProjectSetDescription( + self.handle.as_ptr(), + description.as_ref().as_ptr() as *const 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.handle.as_ptr()) } + } + + /// Get the number of folders in a project (without needing to pull them first) + pub fn received_folder_count(&self) -> u64 { + unsafe { BNRemoteProjectGetReceivedFolderCount(self.handle.as_ptr()) } + } + + /// 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<BnString, ()> { + sync::default_project_path(self) + } + + /// If the project has pulled the folders yet + pub fn has_pulled_files(&self) -> bool { + unsafe { BNRemoteProjectHasPulledFiles(self.handle.as_ptr()) } + } + + /// If the project has pulled the folders yet + pub fn has_pulled_folders(&self) -> bool { + unsafe { BNRemoteProjectHasPulledFolders(self.handle.as_ptr()) } + } + + /// If the project has pulled the group permissions yet + pub fn has_pulled_group_permissions(&self) -> bool { + unsafe { BNRemoteProjectHasPulledGroupPermissions(self.handle.as_ptr()) } + } + + /// If the project has pulled the user permissions yet + pub fn has_pulled_user_permissions(&self) -> bool { + unsafe { BNRemoteProjectHasPulledUserPermissions(self.handle.as_ptr()) } + } + + /// 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.handle.as_ptr()) } + } + + /// 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<Array<RemoteFile>, ()> { + // TODO: This sync should be removed? + if !self.has_pulled_files() { + self.pull_files()?; + } + + let mut count = 0; + let result = unsafe { BNRemoteProjectGetFiles(self.handle.as_ptr(), &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<S: BnStrCompatible>(&self, id: S) -> Result<Option<Ref<RemoteFile>>, ()> { + // TODO: This sync should be removed? + if !self.has_pulled_files() { + self.pull_files()?; + } + let id = id.into_bytes_with_nul(); + let result = unsafe { + BNRemoteProjectGetFileById(self.handle.as_ptr(), id.as_ref().as_ptr() as *const c_char) + }; + Ok(NonNull::new(result).map(|handle| unsafe { RemoteFile::ref_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<S: BnStrCompatible>( + &self, + name: S, + ) -> Result<Option<Ref<RemoteFile>>, ()> { + // TODO: This sync should be removed? + if !self.has_pulled_files() { + self.pull_files()?; + } + let id = name.into_bytes_with_nul(); + let result = unsafe { + BNRemoteProjectGetFileByName( + self.handle.as_ptr(), + id.as_ref().as_ptr() as *const c_char, + ) + }; + Ok(NonNull::new(result).map(|handle| unsafe { RemoteFile::ref_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) -> Result<(), ()> { + self.pull_files_with_progress(NoProgressCallback) + } + + /// 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_with_progress<P: ProgressCallback>(&self, mut progress: P) -> Result<(), ()> { + // TODO: This sync should be removed? + if !self.has_pulled_folders() { + self.pull_folders()?; + } + let success = unsafe { + BNRemoteProjectPullFiles( + self.handle.as_ptr(), + Some(P::cb_progress_callback), + &mut progress as *mut P as *mut 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 + pub fn create_file<F, N, D>( + &self, + filename: F, + contents: &[u8], + name: N, + description: D, + parent_folder: Option<&RemoteFolder>, + file_type: RemoteFileType, + ) -> Result<Ref<RemoteFile>, ()> + where + F: BnStrCompatible, + N: BnStrCompatible, + D: BnStrCompatible, + { + self.create_file_with_progress( + filename, + contents, + name, + description, + parent_folder, + file_type, + NoProgressCallback, + ) + } + + /// 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_with_progress<F, N, D, P>( + &self, + filename: F, + contents: &[u8], + name: N, + description: D, + parent_folder: Option<&RemoteFolder>, + file_type: RemoteFileType, + mut progress: P, + ) -> Result<Ref<RemoteFile>, ()> + where + F: BnStrCompatible, + N: BnStrCompatible, + D: BnStrCompatible, + P: ProgressCallback, + { + // TODO: This sync should be removed? + self.open()?; + + 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(std::ptr::null_mut(), |f| f.handle.as_ptr()); + let file_ptr = unsafe { + BNRemoteProjectCreateFile( + self.handle.as_ptr(), + filename.as_ref().as_ptr() as *const c_char, + contents.as_ptr() as *mut _, + contents.len(), + name.as_ref().as_ptr() as *const c_char, + description.as_ref().as_ptr() as *const c_char, + folder_handle, + file_type, + Some(P::cb_progress_callback), + &mut progress as *mut P as *mut c_void, + ) + }; + + NonNull::new(file_ptr) + .map(|handle| unsafe { RemoteFile::ref_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<I, K, V>(&self, file: &RemoteFile, extra_fields: I) -> Result<(), ()> + where + I: Iterator<Item = (K, V)>, + K: BnStrCompatible, + V: BnStrCompatible, + { + // TODO: This sync should be removed? + self.open()?; + + 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 c_char) + .collect::<Vec<_>>(); + let mut values_raw = values + .iter() + .map(|s| s.as_ref().as_ptr() as *const c_char) + .collect::<Vec<_>>(); + let success = unsafe { + BNRemoteProjectPushFile( + self.handle.as_ptr(), + file.handle.as_ptr(), + 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<(), ()> { + // TODO: This sync should be removed? + self.open()?; + + let success = + unsafe { BNRemoteProjectDeleteFile(self.handle.as_ptr(), file.handle.as_ptr()) }; + 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<Array<RemoteFolder>, ()> { + // TODO: This sync should be removed? + if !self.has_pulled_folders() { + self.pull_folders()?; + } + let mut count = 0; + let result = unsafe { BNRemoteProjectGetFolders(self.handle.as_ptr(), &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<S: BnStrCompatible>( + &self, + id: S, + ) -> Result<Option<Ref<RemoteFolder>>, ()> { + // TODO: This sync should be removed? + if !self.has_pulled_folders() { + self.pull_folders()?; + } + let id = id.into_bytes_with_nul(); + let result = unsafe { + BNRemoteProjectGetFolderById( + self.handle.as_ptr(), + id.as_ref().as_ptr() as *const c_char, + ) + }; + Ok(NonNull::new(result).map(|handle| unsafe { RemoteFolder::ref_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) -> Result<(), ()> { + self.pull_folders_with_progress(NoProgressCallback) + } + + /// 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_with_progress<P: ProgressCallback>( + &self, + mut progress: P, + ) -> Result<(), ()> { + // TODO: This sync should be removed? + self.open()?; + + let success = unsafe { + BNRemoteProjectPullFolders( + self.handle.as_ptr(), + Some(P::cb_progress_callback), + &mut progress as *mut P as *mut 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) + pub fn create_folder<N, D>( + &self, + name: N, + description: D, + parent_folder: Option<&RemoteFolder>, + ) -> Result<Ref<RemoteFolder>, ()> + where + N: BnStrCompatible, + D: BnStrCompatible, + { + self.create_folder_with_progress(name, description, parent_folder, NoProgressCallback) + } + + /// 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_folder_with_progress<N, D, P>( + &self, + name: N, + description: D, + parent_folder: Option<&RemoteFolder>, + mut progress: P, + ) -> Result<Ref<RemoteFolder>, ()> + where + N: BnStrCompatible, + D: BnStrCompatible, + P: ProgressCallback, + { + // TODO: This sync should be removed? + self.open()?; + + let name = name.into_bytes_with_nul(); + let description = description.into_bytes_with_nul(); + let folder_handle = parent_folder.map_or(std::ptr::null_mut(), |f| f.handle.as_ptr()); + let file_ptr = unsafe { + BNRemoteProjectCreateFolder( + self.handle.as_ptr(), + name.as_ref().as_ptr() as *const c_char, + description.as_ref().as_ptr() as *const c_char, + folder_handle, + Some(P::cb_progress_callback), + &mut progress as *mut P as *mut c_void, + ) + }; + + NonNull::new(file_ptr) + .map(|handle| unsafe { RemoteFolder::ref_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<I, K, V>(&self, folder: &RemoteFolder, extra_fields: I) -> Result<(), ()> + where + I: Iterator<Item = (K, V)>, + K: BnStrCompatible, + V: BnStrCompatible, + { + // TODO: This sync should be removed? + self.open()?; + + 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 c_char) + .collect::<Vec<_>>(); + let mut values_raw = values + .iter() + .map(|s| s.as_ref().as_ptr() as *const c_char) + .collect::<Vec<_>>(); + let success = unsafe { + BNRemoteProjectPushFolder( + self.handle.as_ptr(), + folder.handle.as_ptr(), + 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, folder: &RemoteFolder) -> Result<(), ()> { + // TODO: This sync should be removed? + self.open()?; + + let success = + unsafe { BNRemoteProjectDeleteFolder(self.handle.as_ptr(), folder.handle.as_ptr()) }; + 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<Array<Permission>, ()> { + // TODO: This sync should be removed? + if !self.has_pulled_group_permissions() { + self.pull_group_permissions()?; + } + + 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<Array<Permission>, ()> { + // TODO: This sync should be removed? + if !self.has_pulled_user_permissions() { + self.pull_user_permissions()?; + } + + 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<S: BnStrCompatible>( + &self, + id: S, + ) -> Result<Option<Ref<Permission>>, ()> { + // TODO: This sync should be removed? + if !self.has_pulled_user_permissions() { + self.pull_user_permissions()?; + } + // TODO: This sync should be removed? + if !self.has_pulled_group_permissions() { + self.pull_group_permissions()?; + } + + let id = id.into_bytes_with_nul(); + let value = unsafe { + BNRemoteProjectGetPermissionById(self.handle.as_ptr(), id.as_ref().as_ptr() as *const _) + }; + Ok(NonNull::new(value).map(|v| unsafe { Permission::ref_from_raw(v) })) + } + + /// Pull the list of group permissions from the Remote. + pub fn pull_group_permissions(&self) -> Result<(), ()> { + self.pull_group_permissions_with_progress(NoProgressCallback) + } + + /// Pull the list of group permissions from the Remote. + pub fn pull_group_permissions_with_progress<F: ProgressCallback>( + &self, + mut progress: F, + ) -> Result<(), ()> { + let success = unsafe { + BNRemoteProjectPullGroupPermissions( + self.handle.as_ptr(), + Some(F::cb_progress_callback), + &mut progress as *mut F as *mut c_void, + ) + }; + success.then_some(()).ok_or(()) + } + + /// Pull the list of user permissions from the Remote. + pub fn pull_user_permissions(&self) -> Result<(), ()> { + self.pull_user_permissions_with_progress(NoProgressCallback) + } + + /// Pull the list of user permissions from the Remote. + pub fn pull_user_permissions_with_progress<F: ProgressCallback>( + &self, + mut progress: F, + ) -> Result<(), ()> { + let success = unsafe { + BNRemoteProjectPullUserPermissions( + self.handle.as_ptr(), + Some(F::cb_progress_callback), + &mut progress as *mut F as *mut 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 + pub fn create_group_permission( + &self, + group_id: i64, + level: CollaborationPermissionLevel, + ) -> Result<Ref<Permission>, ()> { + self.create_group_permission_with_progress(group_id, level, NoProgressCallback) + } + + /// 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_with_progress<F: ProgressCallback>( + &self, + group_id: i64, + level: CollaborationPermissionLevel, + mut progress: F, + ) -> Result<Ref<Permission>, ()> { + let value = unsafe { + BNRemoteProjectCreateGroupPermission( + self.handle.as_ptr(), + group_id, + level, + Some(F::cb_progress_callback), + &mut progress as *mut F as *mut c_void, + ) + }; + + NonNull::new(value) + .map(|v| unsafe { Permission::ref_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<S: BnStrCompatible>( + &self, + user_id: S, + level: CollaborationPermissionLevel, + ) -> Result<Ref<Permission>, ()> { + self.create_user_permission_with_progress(user_id, level, NoProgressCallback) + } + + /// Create a new user permission on the remote (and pull it). + /// + /// # Arguments + /// + /// * `user_id` - User id + /// * `level` - Permission level + /// * `progress` - The progress callback to call + pub fn create_user_permission_with_progress<S: BnStrCompatible, F: ProgressCallback>( + &self, + user_id: S, + level: CollaborationPermissionLevel, + mut progress: F, + ) -> Result<Ref<Permission>, ()> { + let user_id = user_id.into_bytes_with_nul(); + let value = unsafe { + BNRemoteProjectCreateUserPermission( + self.handle.as_ptr(), + user_id.as_ref().as_ptr() as *const c_char, + level, + Some(F::cb_progress_callback), + &mut progress as *mut F as *mut c_void, + ) + }; + + NonNull::new(value) + .map(|v| unsafe { Permission::ref_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<I, K, V>( + &self, + permission: &Permission, + extra_fields: I, + ) -> Result<(), ()> + where + I: Iterator<Item = (K, V)>, + 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 c_char) + .collect::<Vec<_>>(); + let mut values_raw = values + .iter() + .map(|s| s.as_ref().as_ptr() as *const c_char) + .collect::<Vec<_>>(); + + let success = unsafe { + BNRemoteProjectPushPermission( + self.handle.as_ptr(), + permission.handle.as_ptr(), + 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.handle.as_ptr(), permission.handle.as_ptr()) + }; + 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<S: BnStrCompatible>(&self, username: S) -> bool { + let username = username.into_bytes_with_nul(); + unsafe { + BNRemoteProjectCanUserView( + self.handle.as_ptr(), + username.as_ref().as_ptr() as *const 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<S: BnStrCompatible>(&self, username: S) -> bool { + let username = username.into_bytes_with_nul(); + unsafe { + BNRemoteProjectCanUserEdit( + self.handle.as_ptr(), + username.as_ref().as_ptr() as *const c_char, + ) + } + } + + /// Determine if a user is in the admin group. + /// + /// # Arguments + /// + /// * `username` - Username of user to check + pub fn can_user_admin<S: BnStrCompatible>(&self, username: S) -> bool { + let username = username.into_bytes_with_nul(); + unsafe { + BNRemoteProjectCanUserAdmin( + self.handle.as_ptr(), + username.as_ref().as_ptr() as *const 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.handle.as_ptr()) }; + unsafe { BnString::from_raw(result) } + } + + /// Upload a file, with database, to the remote under the given project + /// + /// * `metadata` - Local file with database + /// * `parent_folder` - Optional parent folder in which to place this file + /// * `name_changeset` - Function to call for naming a pushed changeset, if necessary + pub fn upload_database<C>( + &self, + metadata: &FileMetadata, + parent_folder: Option<&RemoteFolder>, + name_changeset: C, + ) -> Result<Ref<RemoteFile>, ()> + where + C: NameChangeset, + { + // TODO: Do we want this? + // TODO: If you have not yet pulled files you will have never filled the map you will be placing your + // TODO: New file in. + if !self.has_pulled_files() { + self.pull_files()?; + } + sync::upload_database(self, parent_folder, metadata, name_changeset) + } + + /// Upload a file, with database, to the remote under the given project + /// + /// * `metadata` - Local file with database + /// * `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_with_progress<C>( + &self, + metadata: &FileMetadata, + parent_folder: Option<&RemoteFolder>, + name_changeset: C, + progress_function: impl ProgressCallback, + ) -> Result<Ref<RemoteFile>, ()> + where + C: NameChangeset, + { + sync::upload_database_with_progress( + self, + parent_folder, + metadata, + name_changeset, + progress_function, + ) + } + + // 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<S: BnStrCompatible, P: ProgressCallback>( + // &self, + // target: S, + // parent_folder: Option<&RemoteFolder>, + // progress: P, + // open_view_options: u32, + //) -> Result<(), ()> { + // if !self.open(NoProgressCallback)? { + // return Err(()); + // } + // let target = target.into_bytes_with_nul(); + // todo!(); + //} +} + +impl PartialEq for RemoteProject { + fn eq(&self, other: &Self) -> bool { + self.id() == other.id() + } +} +impl Eq for RemoteProject {} + +impl ToOwned for RemoteProject { + type Owned = Ref<Self>; + + fn to_owned(&self) -> Self::Owned { + unsafe { RefCountable::inc_ref(self) } + } +} + +unsafe impl RefCountable for RemoteProject { + unsafe fn inc_ref(handle: &Self) -> Ref<Self> { + Ref::new(Self { + handle: NonNull::new(BNNewRemoteProjectReference(handle.handle.as_ptr())).unwrap(), + }) + } + + unsafe fn dec_ref(handle: &Self) { + BNFreeRemoteProject(handle.handle.as_ptr()); + } +} + +impl CoreArrayProvider for RemoteProject { + type Raw = *mut BNRemoteProject; + type Context = (); + type Wrapped<'a> = Guard<'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> { + let raw_ptr = NonNull::new(*raw).unwrap(); + Guard::new(Self::from_raw(raw_ptr), context) + } +} diff --git a/rust/src/collaboration/remote.rs b/rust/src/collaboration/remote.rs new file mode 100644 index 00000000..baf2ce80 --- /dev/null +++ b/rust/src/collaboration/remote.rs @@ -0,0 +1,959 @@ +use binaryninjacore_sys::*; +use std::ffi::{c_char, c_void}; +use std::ptr::NonNull; + +use super::{sync, GroupId, RemoteGroup, RemoteProject, RemoteUser}; + +use crate::binary_view::BinaryView; +use crate::database::Database; +use crate::enterprise; +use crate::progress::{NoProgressCallback, ProgressCallback}; +use crate::project::Project; +use crate::rc::{Array, CoreArrayProvider, CoreArrayProviderInner, Guard, Ref, RefCountable}; +use crate::string::{BnStrCompatible, BnString}; + +#[repr(transparent)] +pub struct Remote { + pub(crate) handle: NonNull<BNRemote>, +} + +impl Remote { + pub(crate) unsafe fn from_raw(handle: NonNull<BNRemote>) -> Self { + Self { handle } + } + + pub(crate) unsafe fn ref_from_raw(handle: NonNull<BNRemote>) -> Ref<Self> { + Ref::new(Self { handle }) + } + + /// Create a Remote and add it to the list of known remotes (saved to Settings) + pub fn new<N: BnStrCompatible, A: BnStrCompatible>(name: N, address: A) -> Ref<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 c_char, + address.as_ref().as_ptr() as *const c_char, + ) + }; + unsafe { Self::ref_from_raw(NonNull::new(result).unwrap()) } + } + + /// Get the Remote for a Database + pub fn get_for_local_database(database: &Database) -> Result<Option<Ref<Remote>>, ()> { + sync::get_remote_for_local_database(database) + } + + /// Get the Remote for a Binary View + pub fn get_for_binary_view(bv: &BinaryView) -> Result<Option<Ref<Remote>>, ()> { + sync::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.handle.as_ptr()) } + } + + /// Gets the unique id. If metadata has not been pulled, it will be pulled upon calling this. + pub fn unique_id(&self) -> Result<BnString, ()> { + if !self.has_loaded_metadata() { + self.load_metadata()?; + } + let result = unsafe { BNRemoteGetUniqueId(self.handle.as_ptr()) }; + 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.handle.as_ptr()) }; + 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.handle.as_ptr()) }; + assert!(!result.is_null()); + unsafe { BnString::from_raw(result) } + } + + /// Checks if the remote is connected. + pub fn is_connected(&self) -> bool { + unsafe { BNRemoteIsConnected(self.handle.as_ptr()) } + } + + /// Gets the username used to connect to the remote. + pub fn username(&self) -> BnString { + let result = unsafe { BNRemoteGetUsername(self.handle.as_ptr()) }; + 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.handle.as_ptr()) }; + 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<i32, ()> { + if !self.has_loaded_metadata() { + self.load_metadata()?; + } + Ok(unsafe { BNRemoteGetServerVersion(self.handle.as_ptr()) }) + } + + /// 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<BnString, ()> { + if !self.has_loaded_metadata() { + self.load_metadata()?; + } + unsafe { + Ok(BnString::from_raw(BNRemoteGetServerBuildId( + self.handle.as_ptr(), + ))) + } + } + + /// 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<BnString>, Array<BnString>), ()> { + if !self.has_loaded_metadata() { + self.load_metadata()?; + } + + let mut backend_ids = std::ptr::null_mut(); + let mut backend_names = std::ptr::null_mut(); + let mut count = 0; + let success = unsafe { + BNRemoteGetAuthBackends( + self.handle.as_ptr(), + &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<bool, ()> { + if !self.has_pulled_users() { + self.pull_users()?; + } + Ok(unsafe { BNRemoteIsAdmin(self.handle.as_ptr()) }) + } + + /// Checks if the remote is the same as the Enterprise License server. + pub fn is_enterprise(&self) -> Result<bool, ()> { + if !self.has_loaded_metadata() { + self.load_metadata()?; + } + Ok(unsafe { BNRemoteIsEnterprise(self.handle.as_ptr()) }) + } + + /// Loads metadata from the remote, including unique id and versions. + pub fn load_metadata(&self) -> Result<(), ()> { + let success = unsafe { BNRemoteLoadMetadata(self.handle.as_ptr()) }; + success.then_some(()).ok_or(()) + } + + /// Requests an authentication token using a username and password. + pub fn request_authentication_token<U: BnStrCompatible, P: BnStrCompatible>( + &self, + username: U, + password: P, + ) -> Option<BnString> { + let username = username.into_bytes_with_nul(); + let password = password.into_bytes_with_nul(); + let token = unsafe { + BNRemoteRequestAuthenticationToken( + self.handle.as_ptr(), + username.as_ref().as_ptr() as *const c_char, + password.as_ref().as_ptr() as *const c_char, + ) + }; + if token.is_null() { + None + } else { + Some(unsafe { BnString::from_raw(token) }) + } + } + + /// Connects to the Remote, loading metadata and optionally acquiring a token. + /// + /// Use [Remote::connect_with_opts] if you cannot otherwise automatically connect using enterprise. + pub fn connect(&self) -> Result<(), ()> { + // TODO: implement SecretsProvider + if self.is_enterprise()? && enterprise::is_server_authenticated() { + self.connect_with_opts(ConnectionOptions::from_enterprise()?) + } else { + // TODO: Make this error instead. + let username = + std::env::var("BN_ENTERPRISE_USERNAME").expect("No username for connection!"); + let password = + std::env::var("BN_ENTERPRISE_PASSWORD").expect("No password for connection!"); + let connection_opts = ConnectionOptions::new_with_password(username, password); + self.connect_with_opts(connection_opts) + } + } + + // TODO: This needs docs and proper error. + pub fn connect_with_opts(&self, options: ConnectionOptions) -> Result<(), ()> { + // TODO: Should we make used load metadata first? + if !self.has_loaded_metadata() { + self.load_metadata()?; + } + let token = match options.token { + Some(token) => token, + None => { + // TODO: If password not defined than error saying no token or password + let password = options + .password + .expect("No password or token for connection!"); + let token = self.request_authentication_token(&options.username, password); + // TODO: Error if None. + token.unwrap().to_string() + } + }; + let username = options.username.into_bytes_with_nul(); + let username_ptr = username.as_ptr() as *const c_char; + let token = token.into_bytes_with_nul(); + let token_ptr = token.as_ptr() as *const c_char; + let success = unsafe { BNRemoteConnect(self.handle.as_ptr(), username_ptr, token_ptr) }; + success.then_some(()).ok_or(()) + } + + /// Disconnects from the remote. + pub fn disconnect(&self) -> Result<(), ()> { + let success = unsafe { BNRemoteDisconnect(self.handle.as_ptr()) }; + success.then_some(()).ok_or(()) + } + + /// Checks if the project has pulled the projects yet. + pub fn has_pulled_projects(&self) -> bool { + unsafe { BNRemoteHasPulledProjects(self.handle.as_ptr()) } + } + + /// Checks if the project has pulled the groups yet. + pub fn has_pulled_groups(&self) -> bool { + unsafe { BNRemoteHasPulledGroups(self.handle.as_ptr()) } + } + + /// Checks if the project has pulled the users yet. + pub fn has_pulled_users(&self) -> bool { + unsafe { BNRemoteHasPulledUsers(self.handle.as_ptr()) } + } + + /// 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<Array<RemoteProject>, ()> { + if !self.has_pulled_projects() { + self.pull_projects()?; + } + + let mut count = 0; + let value = unsafe { BNRemoteGetProjects(self.handle.as_ptr(), &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<S: BnStrCompatible>( + &self, + id: S, + ) -> Result<Option<Ref<RemoteProject>>, ()> { + if !self.has_pulled_projects() { + self.pull_projects()?; + } + + let id = id.into_bytes_with_nul(); + let value = unsafe { + BNRemoteGetProjectById(self.handle.as_ptr(), id.as_ref().as_ptr() as *const c_char) + }; + Ok(NonNull::new(value).map(|handle| unsafe { RemoteProject::ref_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<S: BnStrCompatible>( + &self, + name: S, + ) -> Result<Option<Ref<RemoteProject>>, ()> { + if !self.has_pulled_projects() { + self.pull_projects()?; + } + + let name = name.into_bytes_with_nul(); + let value = unsafe { + BNRemoteGetProjectByName( + self.handle.as_ptr(), + name.as_ref().as_ptr() as *const c_char, + ) + }; + Ok(NonNull::new(value).map(|handle| unsafe { RemoteProject::ref_from_raw(handle) })) + } + + /// Pulls the list of projects from the Remote. + pub fn pull_projects(&self) -> Result<(), ()> { + self.pull_projects_with_progress(NoProgressCallback) + } + + /// Pulls the list of projects from the Remote. + /// + /// # Arguments + /// + /// * `progress` - Function to call for progress updates + pub fn pull_projects_with_progress<F: ProgressCallback>( + &self, + mut progress: F, + ) -> Result<(), ()> { + let success = unsafe { + BNRemotePullProjects( + self.handle.as_ptr(), + Some(F::cb_progress_callback), + &mut progress as *mut F as *mut 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<N: BnStrCompatible, D: BnStrCompatible>( + &self, + name: N, + description: D, + ) -> Result<Ref<RemoteProject>, ()> { + // TODO: Do we want this? + // TODO: If you have not yet pulled projects you will have never filled the map you will be placing your + // TODO: New project in. + if !self.has_pulled_projects() { + self.pull_projects()?; + } + let name = name.into_bytes_with_nul(); + let description = description.into_bytes_with_nul(); + let value = unsafe { + BNRemoteCreateProject( + self.handle.as_ptr(), + name.as_ref().as_ptr() as *const c_char, + description.as_ref().as_ptr() as *const c_char, + ) + }; + NonNull::new(value) + .map(|handle| unsafe { RemoteProject::ref_from_raw(handle) }) + .ok_or(()) + } + + /// Create a new project on the remote from a local project. + pub fn import_local_project(&self, project: &Project) -> Option<Ref<RemoteProject>> { + self.import_local_project_with_progress(project, NoProgressCallback) + } + + /// Create a new project on the remote from a local project. + pub fn import_local_project_with_progress<P: ProgressCallback>( + &self, + project: &Project, + mut progress: P, + ) -> Option<Ref<RemoteProject>> { + let value = unsafe { + BNRemoteImportLocalProject( + self.handle.as_ptr(), + project.handle.as_ptr(), + Some(P::cb_progress_callback), + &mut progress as *mut P as *mut c_void, + ) + }; + NonNull::new(value).map(|handle| unsafe { RemoteProject::ref_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<I, K, V>(&self, project: &RemoteProject, extra_fields: I) -> Result<(), ()> + where + I: Iterator<Item = (K, V)>, + 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 c_char) + .collect::<Vec<_>>(); + let mut values_raw = values + .iter() + .map(|s| s.as_ref().as_ptr() as *const c_char) + .collect::<Vec<_>>(); + + let success = unsafe { + BNRemotePushProject( + self.handle.as_ptr(), + project.handle.as_ptr(), + 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.handle.as_ptr(), project.handle.as_ptr()) }; + 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<Array<RemoteGroup>, ()> { + if !self.has_pulled_groups() { + self.pull_groups()?; + } + + let mut count = 0; + let value = unsafe { BNRemoteGetGroups(self.handle.as_ptr(), &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: GroupId) -> Result<Option<Ref<RemoteGroup>>, ()> { + if !self.has_pulled_groups() { + self.pull_groups()?; + } + + let value = unsafe { BNRemoteGetGroupById(self.handle.as_ptr(), id.0) }; + Ok(NonNull::new(value).map(|handle| unsafe { RemoteGroup::ref_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<S: BnStrCompatible>( + &self, + name: S, + ) -> Result<Option<Ref<RemoteGroup>>, ()> { + if !self.has_pulled_groups() { + self.pull_groups()?; + } + + let name = name.into_bytes_with_nul(); + let value = unsafe { + BNRemoteGetGroupByName( + self.handle.as_ptr(), + name.as_ref().as_ptr() as *const c_char, + ) + }; + + Ok(NonNull::new(value).map(|handle| unsafe { RemoteGroup::ref_from_raw(handle) })) + } + + /// Searches for groups in the Remote with a given prefix. + /// + /// # Arguments + /// + /// * `prefix` - Prefix of name for groups + pub fn search_groups<S: BnStrCompatible>( + &self, + prefix: S, + ) -> Result<(Array<GroupId>, Array<BnString>), ()> { + let prefix = prefix.into_bytes_with_nul(); + let mut count = 0; + let mut group_ids = std::ptr::null_mut(); + let mut group_names = std::ptr::null_mut(); + + let success = unsafe { + BNRemoteSearchGroups( + self.handle.as_ptr(), + prefix.as_ref().as_ptr() as *const 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. + pub fn pull_groups(&self) -> Result<(), ()> { + self.pull_groups_with_progress(NoProgressCallback) + } + + /// 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_with_progress<F: ProgressCallback>( + &self, + mut progress: F, + ) -> Result<(), ()> { + let success = unsafe { + BNRemotePullGroups( + self.handle.as_ptr(), + Some(F::cb_progress_callback), + &mut progress as *mut F as *mut 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<N, I>(&self, name: N, usernames: I) -> Result<Ref<RemoteGroup>, ()> + 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 c_char) + .collect(); + + let value = unsafe { + BNRemoteCreateGroup( + self.handle.as_ptr(), + name.as_ref().as_ptr() as *const c_char, + username_ptrs.as_mut_ptr(), + username_ptrs.len(), + ) + }; + NonNull::new(value) + .map(|handle| unsafe { RemoteGroup::ref_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<I, K, V>(&self, group: &RemoteGroup, extra_fields: I) -> Result<(), ()> + where + I: IntoIterator<Item = (K, V)>, + 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 c_char) + .collect(); + let mut values_raw: Vec<_> = values + .iter() + .map(|s| s.as_ref().as_ptr() as *const c_char) + .collect(); + + let success = unsafe { + BNRemotePushGroup( + self.handle.as_ptr(), + group.handle.as_ptr(), + 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: &RemoteGroup) -> Result<(), ()> { + let success = unsafe { BNRemoteDeleteGroup(self.handle.as_ptr(), group.handle.as_ptr()) }; + 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<Array<RemoteUser>, ()> { + if !self.has_pulled_users() { + self.pull_users()?; + } + 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<S: BnStrCompatible>(&self, id: S) -> Result<Option<Ref<RemoteUser>>, ()> { + if !self.has_pulled_users() { + self.pull_users()?; + } + let id = id.into_bytes_with_nul(); + let value = unsafe { + BNRemoteGetUserById(self.handle.as_ptr(), id.as_ref().as_ptr() as *const c_char) + }; + Ok(NonNull::new(value).map(|handle| unsafe { RemoteUser::ref_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<S: BnStrCompatible>( + &self, + username: S, + ) -> Result<Option<Ref<RemoteUser>>, ()> { + if !self.has_pulled_users() { + self.pull_users()?; + } + let username = username.into_bytes_with_nul(); + let value = unsafe { + BNRemoteGetUserByUsername( + self.handle.as_ptr(), + username.as_ref().as_ptr() as *const c_char, + ) + }; + Ok(NonNull::new(value).map(|handle| unsafe { RemoteUser::ref_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<Option<Ref<RemoteUser>>, ()> { + if !self.has_pulled_users() { + self.pull_users()?; + } + let value = unsafe { BNRemoteGetCurrentUser(self.handle.as_ptr()) }; + Ok(NonNull::new(value).map(|handle| unsafe { RemoteUser::ref_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<S: BnStrCompatible>( + &self, + prefix: S, + ) -> Result<(Array<BnString>, Array<BnString>), ()> { + let prefix = prefix.into_bytes_with_nul(); + let mut count = 0; + let mut user_ids = std::ptr::null_mut(); + let mut usernames = std::ptr::null_mut(); + let success = unsafe { + BNRemoteSearchUsers( + self.handle.as_ptr(), + prefix.as_ref().as_ptr() as *const 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. + pub fn pull_users(&self) -> Result<(), ()> { + self.pull_users_with_progress(NoProgressCallback) + } + + /// 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_with_progress<P: ProgressCallback>(&self, mut progress: P) -> Result<(), ()> { + let success = unsafe { + BNRemotePullUsers( + self.handle.as_ptr(), + Some(P::cb_progress_callback), + &mut progress as *mut P as *mut 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<U: BnStrCompatible, E: BnStrCompatible, P: BnStrCompatible>( + &self, + username: U, + email: E, + is_active: bool, + password: P, + group_ids: &[u64], + user_permission_ids: &[u64], + ) -> Result<Ref<RemoteUser>, ()> { + 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.handle.as_ptr(), + username.as_ref().as_ptr() as *const c_char, + email.as_ref().as_ptr() as *const c_char, + is_active, + password.as_ref().as_ptr() as *const c_char, + group_ids.as_ptr(), + group_ids.len(), + user_permission_ids.as_ptr(), + user_permission_ids.len(), + ) + }; + NonNull::new(value) + .map(|handle| unsafe { RemoteUser::ref_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<I, K, V>(&self, user: &RemoteUser, extra_fields: I) -> Result<(), ()> + where + I: Iterator<Item = (K, V)>, + 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 c_char) + .collect(); + let mut values_raw: Vec<_> = values + .iter() + .map(|s| s.as_ref().as_ptr() as *const c_char) + .collect(); + let success = unsafe { + BNRemotePushUser( + self.handle.as_ptr(), + user.handle.as_ptr(), + 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.handle.as_ptr(), todo!(), todo!()) } + //} +} + +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 ToOwned for Remote { + type Owned = Ref<Self>; + + fn to_owned(&self) -> Self::Owned { + unsafe { RefCountable::inc_ref(self) } + } +} + +unsafe impl RefCountable for Remote { + unsafe fn inc_ref(handle: &Self) -> Ref<Self> { + Ref::new(Self { + handle: NonNull::new(BNNewRemoteReference(handle.handle.as_ptr())).unwrap(), + }) + } + + unsafe fn dec_ref(handle: &Self) { + BNFreeRemote(handle.handle.as_ptr()); + } +} + +impl CoreArrayProvider for Remote { + type Raw = *mut BNRemote; + type Context = (); + type Wrapped<'a> = Guard<'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> { + let raw_ptr = NonNull::new(*raw).unwrap(); + Guard::new(Self::from_raw(raw_ptr), context) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct ConnectionOptions { + pub username: String, + /// Provide this if you want to authenticate with a password. + pub password: Option<String>, + /// Provide this if you want to authenticate with a token. + /// + /// If you do not have a token you can use [ConnectionOptions::self]. + pub token: Option<String>, +} + +impl ConnectionOptions { + pub fn new_with_token(username: String, token: String) -> Self { + Self { + username, + token: Some(token), + password: None, + } + } + + pub fn new_with_password(username: String, password: String) -> Self { + Self { + username, + token: None, + password: Some(password), + } + } + + pub fn with_token(self, token: String) -> Self { + Self { + token: Some(token), + ..self + } + } + + pub fn with_password(self, token: String) -> Self { + Self { + token: Some(token), + ..self + } + } + + pub fn from_enterprise() -> Result<Self, ()> { + // TODO: Check if enterprise is initialized and error if not. + let username = enterprise::server_username(); + let token = enterprise::server_token(); + Ok(Self::new_with_token( + username.to_string(), + token.to_string(), + )) + } + + // TODO: from_secrets_provider +} diff --git a/rust/src/collaboration/snapshot.rs b/rust/src/collaboration/snapshot.rs new file mode 100644 index 00000000..9f8f3693 --- /dev/null +++ b/rust/src/collaboration/snapshot.rs @@ -0,0 +1,368 @@ +use std::ffi::{c_char, c_void}; +use std::ptr::NonNull; +use std::time::SystemTime; + +use super::{sync, Remote, RemoteFile, RemoteProject}; +use crate::binary_view::{BinaryView, BinaryViewExt}; +use crate::collaboration::undo::{RemoteUndoEntry, RemoteUndoEntryId}; +use crate::database::snapshot::Snapshot; +use crate::progress::{NoProgressCallback, ProgressCallback}; +use crate::rc::{Array, CoreArrayProvider, CoreArrayProviderInner, Guard, Ref, RefCountable}; +use crate::string::{BnStrCompatible, BnString}; +use binaryninjacore_sys::*; + +// TODO: RemoteSnapshotId ? + +#[repr(transparent)] +pub struct RemoteSnapshot { + pub(crate) handle: NonNull<BNCollaborationSnapshot>, +} + +impl RemoteSnapshot { + pub(crate) unsafe fn from_raw(handle: NonNull<BNCollaborationSnapshot>) -> Self { + Self { handle } + } + + pub(crate) unsafe fn ref_from_raw(handle: NonNull<BNCollaborationSnapshot>) -> Ref<Self> { + Ref::new(Self { handle }) + } + + /// Get the remote snapshot associated with a local snapshot (if it exists) + pub fn get_for_local_snapshot(snapshot: &Snapshot) -> Result<Option<Ref<RemoteSnapshot>>, ()> { + sync::get_remote_snapshot_from_local(snapshot) + } + + /// Owning File + pub fn file(&self) -> Result<Ref<RemoteFile>, ()> { + let result = unsafe { BNCollaborationSnapshotGetFile(self.handle.as_ptr()) }; + let raw = NonNull::new(result).ok_or(())?; + Ok(unsafe { RemoteFile::ref_from_raw(raw) }) + } + + /// Owning Project + pub fn project(&self) -> Result<Ref<RemoteProject>, ()> { + let result = unsafe { BNCollaborationSnapshotGetProject(self.handle.as_ptr()) }; + let raw = NonNull::new(result).ok_or(())?; + Ok(unsafe { RemoteProject::ref_from_raw(raw) }) + } + + /// Owning Remote + pub fn remote(&self) -> Result<Ref<Remote>, ()> { + let result = unsafe { BNCollaborationSnapshotGetRemote(self.handle.as_ptr()) }; + let raw = NonNull::new(result).ok_or(())?; + Ok(unsafe { Remote::ref_from_raw(raw) }) + } + + /// Web api endpoint url + pub fn url(&self) -> BnString { + let value = unsafe { BNCollaborationSnapshotGetUrl(self.handle.as_ptr()) }; + assert!(!value.is_null()); + unsafe { BnString::from_raw(value) } + } + + /// Unique id + pub fn id(&self) -> BnString { + let value = unsafe { BNCollaborationSnapshotGetId(self.handle.as_ptr()) }; + assert!(!value.is_null()); + unsafe { BnString::from_raw(value) } + } + + /// Name of snapshot + pub fn name(&self) -> BnString { + let value = unsafe { BNCollaborationSnapshotGetName(self.handle.as_ptr()) }; + 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.handle.as_ptr()) }; + 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.handle.as_ptr()) }; + 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.handle.as_ptr()) }; + 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.handle.as_ptr()) }; + assert!(!value.is_null()); + unsafe { BnString::from_raw(value) } + } + + /// Created date of Snapshot + pub fn created(&self) -> SystemTime { + let timestamp = unsafe { BNCollaborationSnapshotGetCreated(self.handle.as_ptr()) }; + 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.handle.as_ptr()) }; + 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.handle.as_ptr()) }; + 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.handle.as_ptr()) }; + assert!(!value.is_null()); + unsafe { BnString::from_raw(value) } + } + + /// If the snapshot has pulled undo entries yet + pub fn has_pulled_undo_entries(&self) -> bool { + unsafe { BNCollaborationSnapshotHasPulledUndoEntries(self.handle.as_ptr()) } + } + + /// If the snapshot has been finalized on the server and is no longer editable + pub fn is_finalized(&self) -> bool { + unsafe { BNCollaborationSnapshotIsFinalized(self.handle.as_ptr()) } + } + + /// List of ids of all remote parent Snapshots + pub fn parent_ids(&self) -> Result<Array<BnString>, ()> { + let mut count = 0; + let raw = unsafe { BNCollaborationSnapshotGetParentIds(self.handle.as_ptr(), &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<Array<BnString>, ()> { + let mut count = 0; + let raw = unsafe { BNCollaborationSnapshotGetChildIds(self.handle.as_ptr(), &mut count) }; + (!raw.is_null()) + .then(|| unsafe { Array::new(raw, count, ()) }) + .ok_or(()) + } + + /// List of all parent Snapshot objects + pub fn parents(&self) -> Result<Array<RemoteSnapshot>, ()> { + let mut count = 0; + let raw = unsafe { BNCollaborationSnapshotGetParents(self.handle.as_ptr(), &mut count) }; + (!raw.is_null()) + .then(|| unsafe { Array::new(raw, count, ()) }) + .ok_or(()) + } + + /// List of all child Snapshot objects + pub fn children(&self) -> Result<Array<RemoteSnapshot>, ()> { + let mut count = 0; + let raw = unsafe { BNCollaborationSnapshotGetChildren(self.handle.as_ptr(), &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<Array<RemoteUndoEntry>, ()> { + if !self.has_pulled_undo_entries() { + self.pull_undo_entries()?; + } + let mut count = 0; + let raw = + unsafe { BNCollaborationSnapshotGetUndoEntries(self.handle.as_ptr(), &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: RemoteUndoEntryId, + ) -> Result<Option<Ref<RemoteUndoEntry>>, ()> { + if !self.has_pulled_undo_entries() { + self.pull_undo_entries()?; + } + let raw = unsafe { BNCollaborationSnapshotGetUndoEntryById(self.handle.as_ptr(), id.0) }; + Ok(NonNull::new(raw).map(|handle| unsafe { RemoteUndoEntry::ref_from_raw(handle) })) + } + + /// Pull the list of Undo Entries from the Remote. + pub fn pull_undo_entries(&self) -> Result<(), ()> { + self.pull_undo_entries_with_progress(NoProgressCallback) + } + + /// Pull the list of Undo Entries from the Remote. + pub fn pull_undo_entries_with_progress<P: ProgressCallback>( + &self, + mut progress: P, + ) -> Result<(), ()> { + let success = unsafe { + BNCollaborationSnapshotPullUndoEntries( + self.handle.as_ptr(), + Some(P::cb_progress_callback), + &mut progress as *mut P as *mut c_void, + ) + }; + success.then_some(()).ok_or(()) + } + + /// Create a new Undo Entry in this snapshot. + pub fn create_undo_entry<S: BnStrCompatible>( + &self, + parent: Option<u64>, + data: S, + ) -> Result<Ref<RemoteUndoEntry>, ()> { + let data = data.into_bytes_with_nul(); + let value = unsafe { + BNCollaborationSnapshotCreateUndoEntry( + self.handle.as_ptr(), + parent.is_some(), + parent.unwrap_or(0), + data.as_ref().as_ptr() as *const c_char, + ) + }; + let handle = NonNull::new(value).ok_or(())?; + Ok(unsafe { RemoteUndoEntry::ref_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.handle.as_ptr()) }; + 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<P: ProgressCallback>( + // &self, + // mut progress: P, + //) -> Result<BnData, ()> { + // let mut data = ptr::null_mut(); + // let mut count = 0; + // let success = unsafe { + // BNCollaborationSnapshotDownloadSnapshotFile( + // self.handle.as_ptr(), + // 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<P: ProgressCallback>( + // &self, + // mut progress: P, + //) -> Result<BnData, ()> { + // let mut data = ptr::null_mut(); + // let mut count = 0; + // let success = unsafe { + // BNCollaborationSnapshotDownload( + // self.handle.as_ptr(), + // 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<P: ProgressCallback>( + // &self, + // mut progress: P, + //) -> Result<BnData, ()> { + // let mut data = ptr::null_mut(); + // let mut count = 0; + // let success = unsafe { + // BNCollaborationSnapshotDownloadAnalysisCache( + // self.handle.as_ptr(), + // 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<Option<Ref<Snapshot>>, ()> { + let Some(db) = bv.file().database() else { + return Ok(None); + }; + sync::get_local_snapshot_for_remote(self, &db) + } + + pub fn analysis_cache_build_id(&self) -> u64 { + unsafe { BNCollaborationSnapshotGetAnalysisCacheBuildId(self.handle.as_ptr()) } + } +} + +impl PartialEq for RemoteSnapshot { + fn eq(&self, other: &Self) -> bool { + self.id() == other.id() + } +} +impl Eq for RemoteSnapshot {} + +impl ToOwned for RemoteSnapshot { + type Owned = Ref<Self>; + + fn to_owned(&self) -> Self::Owned { + unsafe { RefCountable::inc_ref(self) } + } +} + +unsafe impl RefCountable for RemoteSnapshot { + unsafe fn inc_ref(handle: &Self) -> Ref<Self> { + Ref::new(Self { + handle: NonNull::new(BNNewCollaborationSnapshotReference(handle.handle.as_ptr())) + .unwrap(), + }) + } + + unsafe fn dec_ref(handle: &Self) { + BNFreeCollaborationSnapshot(handle.handle.as_ptr()); + } +} + +impl CoreArrayProvider for RemoteSnapshot { + type Raw = *mut BNCollaborationSnapshot; + type Context = (); + type Wrapped<'a> = Guard<'a, RemoteSnapshot>; +} + +unsafe impl CoreArrayProviderInner for RemoteSnapshot { + 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> { + let raw_ptr = NonNull::new(*raw).unwrap(); + Guard::new(Self::from_raw(raw_ptr), context) + } +} diff --git a/rust/src/collaboration/sync.rs b/rust/src/collaboration/sync.rs new file mode 100644 index 00000000..14e18856 --- /dev/null +++ b/rust/src/collaboration/sync.rs @@ -0,0 +1,941 @@ +use super::{ + Changeset, MergeConflict, Remote, RemoteFile, RemoteFolder, RemoteProject, RemoteSnapshot, +}; +use binaryninjacore_sys::*; +use std::ffi::{c_char, c_void}; +use std::mem::ManuallyDrop; +use std::ptr::NonNull; + +use crate::binary_view::{BinaryView, BinaryViewExt}; +use crate::database::{snapshot::Snapshot, Database}; +use crate::file_metadata::FileMetadata; +use crate::progress::{NoProgressCallback, ProgressCallback}; +use crate::project::file::ProjectFile; +use crate::rc::Ref; +use crate::string::{BnStrCompatible, BnString}; +use crate::type_archive::{TypeArchive, TypeArchiveMergeConflict}; + +// TODO: PathBuf +/// 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<BnString, ()> { + let result = unsafe { BNCollaborationDefaultProjectPath(project.handle.as_ptr()) }; + let success = !result.is_null(); + success + .then(|| unsafe { BnString::from_raw(result) }) + .ok_or(()) +} + +// TODO: PathBuf +// 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<BnString, ()> { + let result = unsafe { BNCollaborationDefaultFilePath(file.handle.as_ptr()) }; + let success = !result.is_null(); + success + .then(|| unsafe { BnString::from_raw(result) }) + .ok_or(()) +} + +// TODO: AsRef<Path> +/// 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 +pub fn download_file<S: BnStrCompatible>( + file: &RemoteFile, + db_path: S, +) -> Result<Ref<FileMetadata>, ()> { + download_file_with_progress(file, db_path, NoProgressCallback) +} + +// TODO: AsRef<Path> +/// 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_with_progress<S: BnStrCompatible, F: ProgressCallback>( + file: &RemoteFile, + db_path: S, + mut progress: F, +) -> Result<Ref<FileMetadata>, ()> { + let db_path = db_path.into_bytes_with_nul(); + let result = unsafe { + BNCollaborationDownloadFile( + file.handle.as_ptr(), + db_path.as_ref().as_ptr() as *const c_char, + Some(F::cb_progress_callback), + &mut progress as *mut F as *mut 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 +/// +/// * `project` - Remote project under which to place the new file +/// * `parent_folder` - Optional parent folder in which to place this file +/// * `metadata` - Local file with database +/// * `name_changeset` - Function to call for naming a pushed changeset, if necessary +pub fn upload_database<N: NameChangeset>( + project: &RemoteProject, + parent_folder: Option<&RemoteFolder>, + metadata: &FileMetadata, + name_changeset: N, +) -> Result<Ref<RemoteFile>, ()> { + upload_database_with_progress( + project, + parent_folder, + metadata, + name_changeset, + NoProgressCallback, + ) +} + +/// 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 +/// * `name_changeset` - Function to call for naming a pushed changeset, if necessary +/// * `progress` - Function to call for progress updates +pub fn upload_database_with_progress<P: ProgressCallback, N: NameChangeset>( + project: &RemoteProject, + parent_folder: Option<&RemoteFolder>, + metadata: &FileMetadata, + mut name_changeset: N, + mut progress: P, +) -> Result<Ref<RemoteFile>, ()> { + let folder_raw = parent_folder.map_or(std::ptr::null_mut(), |h| h.handle.as_ptr()); + let result = unsafe { + BNCollaborationUploadDatabase( + metadata.handle, + project.handle.as_ptr(), + folder_raw, + Some(P::cb_progress_callback), + &mut progress as *mut P as *mut c_void, + Some(N::cb_name_changeset), + &mut name_changeset as *mut N as *mut c_void, + ) + }; + NonNull::new(result) + .map(|raw| unsafe { RemoteFile::ref_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.handle.as_ptr()) } +} + +/// Get the Remote for a Database +pub fn get_remote_for_local_database(database: &Database) -> Result<Option<Ref<Remote>>, ()> { + let mut value = std::ptr::null_mut(); + let success = + unsafe { BNCollaborationGetRemoteForLocalDatabase(database.handle.as_ptr(), &mut value) }; + success + .then(|| NonNull::new(value).map(|handle| unsafe { Remote::ref_from_raw(handle) })) + .ok_or(()) +} + +/// Get the Remote for a BinaryView +pub fn get_remote_for_binary_view(bv: &BinaryView) -> Result<Option<Ref<Remote>>, ()> { + 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<Option<Ref<RemoteProject>>, ()> { + let mut value = std::ptr::null_mut(); + let success = unsafe { + BNCollaborationGetRemoteProjectForLocalDatabase(database.handle.as_ptr(), &mut value) + }; + success + .then(|| NonNull::new(value).map(|handle| unsafe { RemoteProject::ref_from_raw(handle) })) + .ok_or(()) +} + +/// Get the Remote File for a Database +pub fn get_remote_file_for_local_database( + database: &Database, +) -> Result<Option<Ref<RemoteFile>>, ()> { + let mut value = std::ptr::null_mut(); + let success = unsafe { + BNCollaborationGetRemoteFileForLocalDatabase(database.handle.as_ptr(), &mut value) + }; + success + .then(|| NonNull::new(value).map(|handle| unsafe { RemoteFile::ref_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: &RemoteSnapshot, +) -> Result<(), ()> { + let success = unsafe { + BNCollaborationAssignSnapshotMap( + local_snapshot.handle.as_ptr(), + remote_snapshot.handle.as_ptr(), + ) + }; + 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<Option<Ref<RemoteSnapshot>>, ()> { + let mut value = std::ptr::null_mut(); + let success = + unsafe { BNCollaborationGetRemoteSnapshotFromLocal(snap.handle.as_ptr(), &mut value) }; + success + .then(|| NonNull::new(value).map(|handle| unsafe { RemoteSnapshot::ref_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: &RemoteSnapshot, + database: &Database, +) -> Result<Option<Ref<Snapshot>>, ()> { + let mut value = std::ptr::null_mut(); + let success = unsafe { + BNCollaborationGetLocalSnapshotFromRemote( + snapshot.handle.as_ptr(), + database.handle.as_ptr(), + &mut value, + ) + }; + success + .then(|| NonNull::new(value).map(|handle| unsafe { Snapshot::ref_from_raw(handle) })) + .ok_or(()) +} + +pub fn download_database<S>(file: &RemoteFile, location: S, force: bool) -> Result<(), ()> +where + S: BnStrCompatible, +{ + download_database_with_progress(file, location, force, NoProgressCallback) +} + +pub fn download_database_with_progress<S, F>( + file: &RemoteFile, + location: S, + force: bool, + mut progress: F, +) -> Result<(), ()> +where + S: BnStrCompatible, + F: ProgressCallback, +{ + let db_path = location.into_bytes_with_nul(); + let success = unsafe { + BNCollaborationDownloadDatabaseForFile( + file.handle.as_ptr(), + db_path.as_ref().as_ptr() as *const c_char, + force, + Some(F::cb_progress_callback), + &mut progress as *mut _ as *mut c_void, + ) + }; + success.then_some(()).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 +/// * `name_changeset` - Function to call for naming a pushed changeset, if necessary +pub fn sync_database<C: DatabaseConflictHandler, N: NameChangeset>( + database: &Database, + file: &RemoteFile, + conflict_handler: C, + name_changeset: N, +) -> Result<(), ()> { + sync_database_with_progress( + database, + file, + conflict_handler, + name_changeset, + NoProgressCallback, + ) +} + +/// 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 +/// * `name_changeset` - Function to call for naming a pushed changeset, if necessary +/// * `progress` - Function to call for progress updates +pub fn sync_database_with_progress< + C: DatabaseConflictHandler, + P: ProgressCallback, + N: NameChangeset, +>( + database: &Database, + file: &RemoteFile, + mut conflict_handler: C, + mut name_changeset: N, + mut progress: P, +) -> Result<(), ()> { + let success = unsafe { + BNCollaborationSyncDatabase( + database.handle.as_ptr(), + file.handle.as_ptr(), + Some(C::cb_handle_conflict), + &mut conflict_handler as *mut C as *mut c_void, + Some(P::cb_progress_callback), + &mut progress as *mut P as *mut c_void, + Some(N::cb_name_changeset), + &mut name_changeset as *mut N as *mut 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 +/// * `name_changeset` - Function to call for naming a pushed changeset, if necessary +pub fn pull_database<C: DatabaseConflictHandler, N: NameChangeset>( + database: &Database, + file: &RemoteFile, + conflict_handler: C, + name_changeset: N, +) -> Result<usize, ()> { + pull_database_with_progress( + database, + file, + conflict_handler, + name_changeset, + NoProgressCallback, + ) +} + +/// 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 +/// * `name_changeset` - Function to call for naming a pushed changeset, if necessary +/// * `progress` - Function to call for progress updates +pub fn pull_database_with_progress< + C: DatabaseConflictHandler, + P: ProgressCallback, + N: NameChangeset, +>( + database: &Database, + file: &RemoteFile, + mut conflict_handler: C, + mut name_changeset: N, + mut progress: P, +) -> Result<usize, ()> { + let mut count = 0; + let success = unsafe { + BNCollaborationPullDatabase( + database.handle.as_ptr(), + file.handle.as_ptr(), + &mut count, + Some(C::cb_handle_conflict), + &mut conflict_handler as *mut C as *mut c_void, + Some(P::cb_progress_callback), + &mut progress as *mut P as *mut c_void, + Some(N::cb_name_changeset), + &mut name_changeset as *mut N as *mut 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 +pub fn merge_database<C: DatabaseConflictHandler>( + database: &Database, + conflict_handler: C, +) -> Result<(), ()> { + merge_database_with_progress(database, conflict_handler, NoProgressCallback) +} + +/// 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_with_progress<C: DatabaseConflictHandler, P: ProgressCallback>( + database: &Database, + mut conflict_handler: C, + mut progress: P, +) -> Result<(), ()> { + let success = unsafe { + BNCollaborationMergeDatabase( + database.handle.as_ptr(), + Some(C::cb_handle_conflict), + &mut conflict_handler as *mut C as *mut c_void, + Some(P::cb_progress_callback), + &mut progress as *mut P as *mut c_void, + ) + }; + success.then_some(()).ok_or(()) +} + +/// Push locally added snapshots to the remote +/// +/// * `database` - Database to push +/// * `file` - Remote File to push to +pub fn push_database(database: &Database, file: &RemoteFile) -> Result<usize, ()> { + push_database_with_progress(database, file, NoProgressCallback) +} + +/// 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_with_progress<P: ProgressCallback>( + database: &Database, + file: &RemoteFile, + mut progress: P, +) -> Result<usize, ()> { + let mut count = 0; + let success = unsafe { + BNCollaborationPushDatabase( + database.handle.as_ptr(), + file.handle.as_ptr(), + &mut count, + Some(P::cb_progress_callback), + &mut progress as *mut P as *mut 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.handle.as_ptr()) }; + 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.handle.as_ptr(), snapshot.handle.as_ptr()) + }; + 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.handle.as_ptr(), snapshot.handle.as_ptr()) } +} + +/// 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<Option<BnString>, ()> { + let mut value = std::ptr::null_mut(); + let success = unsafe { + BNCollaborationGetSnapshotAuthor( + database.handle.as_ptr(), + snapshot.handle.as_ptr(), + &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<S: BnStrCompatible>( + database: &Database, + snapshot: &Snapshot, + author: S, +) -> Result<(), ()> { + let author = author.into_bytes_with_nul(); + let success = unsafe { + BNCollaborationSetSnapshotAuthor( + database.handle.as_ptr(), + snapshot.handle.as_ptr(), + author.as_ref().as_ptr() as *const c_char, + ) + }; + success.then_some(()).ok_or(()) +} + +// TODO: this needs to be removed imo +pub(crate) fn pull_projects(database: &Database) -> Result<bool, ()> { + let Some(remote) = get_remote_for_local_database(database)? else { + return Ok(false); + }; + remote.pull_projects()?; + Ok(true) +} + +// TODO: This needs to be removed imo +pub(crate) fn pull_files(database: &Database) -> Result<bool, ()> { + if !pull_projects(database)? { + return Ok(false); + } + let Some(project) = get_remote_project_for_local_database(database)? else { + return Ok(false); + }; + project.pull_files()?; + 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 +pub fn sync_type_archive<C: TypeArchiveConflictHandler>( + type_archive: &TypeArchive, + file: &RemoteFile, + conflict_handler: C, +) -> Result<(), ()> { + sync_type_archive_with_progress(type_archive, file, conflict_handler, NoProgressCallback) +} + +/// 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_with_progress<C: TypeArchiveConflictHandler, P: ProgressCallback>( + type_archive: &TypeArchive, + file: &RemoteFile, + mut conflict_handler: C, + mut progress: P, +) -> Result<(), ()> { + let success = unsafe { + BNCollaborationSyncTypeArchive( + type_archive.handle.as_ptr(), + file.handle.as_ptr(), + Some(C::cb_handle_conflict), + &mut conflict_handler as *mut C as *mut c_void, + Some(P::cb_progress_callback), + &mut progress as *mut P as *mut 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 +pub fn push_type_archive(type_archive: &TypeArchive, file: &RemoteFile) -> Result<usize, ()> { + push_type_archive_with_progress(type_archive, file, NoProgressCallback) +} + +/// 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_with_progress<P: ProgressCallback>( + type_archive: &TypeArchive, + file: &RemoteFile, + mut progress: P, +) -> Result<usize, ()> { + let mut count = 0; + let success = unsafe { + BNCollaborationPushTypeArchive( + type_archive.handle.as_ptr(), + file.handle.as_ptr(), + &mut count, + Some(P::cb_progress_callback), + &mut progress as *mut P as *mut c_void, + ) + }; + success.then_some(count).ok_or(()) +} + +/// Pull updated type archives from the remote. +/// +/// * `type_archive` - TypeArchive to pull +/// * `file` - Remote File to pull to +/// * `conflict_handler` - Function to call to resolve snapshot conflicts +pub fn pull_type_archive<C: TypeArchiveConflictHandler>( + type_archive: &TypeArchive, + file: &RemoteFile, + conflict_handler: C, +) -> Result<usize, ()> { + pull_type_archive_with_progress(type_archive, file, conflict_handler, NoProgressCallback) +} + +/// Pull updated type archives from the remote. +/// +/// * `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 +pub fn pull_type_archive_with_progress<C: TypeArchiveConflictHandler, P: ProgressCallback>( + type_archive: &TypeArchive, + file: &RemoteFile, + mut conflict_handler: C, + mut progress: P, +) -> Result<usize, ()> { + let mut count = 0; + let success = unsafe { + BNCollaborationPullTypeArchive( + type_archive.handle.as_ptr(), + file.handle.as_ptr(), + &mut count, + Some(C::cb_handle_conflict), + &mut conflict_handler as *mut C as *mut c_void, + Some(P::cb_progress_callback), + &mut progress as *mut P as *mut 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.handle.as_ptr()) } +} + +/// Get the Remote for a Type Archive +pub fn get_remote_for_local_type_archive(type_archive: &TypeArchive) -> Option<Ref<Remote>> { + let value = + unsafe { BNCollaborationGetRemoteForLocalTypeArchive(type_archive.handle.as_ptr()) }; + NonNull::new(value).map(|handle| unsafe { Remote::ref_from_raw(handle) }) +} + +/// Get the Remote Project for a Type Archive +pub fn get_remote_project_for_local_type_archive( + database: &TypeArchive, +) -> Option<Ref<RemoteProject>> { + let value = + unsafe { BNCollaborationGetRemoteProjectForLocalTypeArchive(database.handle.as_ptr()) }; + NonNull::new(value).map(|handle| unsafe { RemoteProject::ref_from_raw(handle) }) +} + +/// Get the Remote File for a Type Archive +pub fn get_remote_file_for_local_type_archive(database: &TypeArchive) -> Option<Ref<RemoteFile>> { + let value = + unsafe { BNCollaborationGetRemoteFileForLocalTypeArchive(database.handle.as_ptr()) }; + NonNull::new(value).map(|handle| unsafe { RemoteFile::ref_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<S: BnStrCompatible>( + type_archive: &TypeArchive, + snapshot_id: S, +) -> Option<Ref<RemoteSnapshot>> { + let snapshot_id = snapshot_id.into_bytes_with_nul(); + let value = unsafe { + BNCollaborationGetRemoteSnapshotFromLocalTypeArchive( + type_archive.handle.as_ptr(), + snapshot_id.as_ref().as_ptr() as *const c_char, + ) + }; + NonNull::new(value).map(|handle| unsafe { RemoteSnapshot::ref_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: &RemoteSnapshot, + type_archive: &TypeArchive, +) -> Option<BnString> { + let value = unsafe { + BNCollaborationGetLocalSnapshotFromRemoteTypeArchive( + snapshot.handle.as_ptr(), + type_archive.handle.as_ptr(), + ) + }; + (!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<S: BnStrCompatible>( + type_archive: &TypeArchive, + snapshot_id: S, +) -> bool { + let snapshot_id = snapshot_id.into_bytes_with_nul(); + unsafe { + BNCollaborationIsTypeArchiveSnapshotIgnored( + type_archive.handle.as_ptr(), + snapshot_id.as_ref().as_ptr() as *const 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<S: BnStrCompatible>( + file: &RemoteFile, + location: S, +) -> Result<Option<TypeArchive>, ()> { + download_type_archive_with_progress(file, location, NoProgressCallback) +} + +/// 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_with_progress<S: BnStrCompatible, F: ProgressCallback>( + file: &RemoteFile, + location: S, + mut progress: F, +) -> Result<Option<TypeArchive>, ()> { + let mut value = std::ptr::null_mut(); + let db_path = location.into_bytes_with_nul(); + let success = unsafe { + BNCollaborationDownloadTypeArchive( + file.handle.as_ptr(), + db_path.as_ref().as_ptr() as *const c_char, + Some(F::cb_progress_callback), + &mut progress as *mut F as *mut c_void, + &mut value, + ) + }; + success + .then(|| 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, + // TODO: Is this required? + folder: &RemoteFolder, + core_file: &ProjectFile, +) -> Result<Ref<RemoteFile>, ()> { + upload_type_archive_with_progress(archive, project, folder, core_file, NoProgressCallback) +} + +/// Upload a type archive +pub fn upload_type_archive_with_progress<P: ProgressCallback>( + archive: &TypeArchive, + project: &RemoteProject, + // TODO: Is this required? + folder: &RemoteFolder, + // TODO: I dislike the word "core" just say local? + core_file: &ProjectFile, + mut progress: P, +) -> Result<Ref<RemoteFile>, ()> { + let mut value = std::ptr::null_mut(); + let success = unsafe { + BNCollaborationUploadTypeArchive( + archive.handle.as_ptr(), + project.handle.as_ptr(), + folder.handle.as_ptr(), + Some(P::cb_progress_callback), + &mut progress as *const P as *mut c_void, + core_file.handle.as_ptr(), + &mut value, + ) + }; + success + .then(|| { + NonNull::new(value) + .map(|handle| unsafe { RemoteFile::ref_from_raw(handle) }) + .unwrap() + }) + .ok_or(()) +} + +/// Merge a pair of snapshots and create a new snapshot with the result. +pub fn merge_snapshots<C: DatabaseConflictHandler>( + first: &Snapshot, + second: &Snapshot, + conflict_handler: C, +) -> Result<Snapshot, ()> { + merge_snapshots_with_progress(first, second, conflict_handler, NoProgressCallback) +} + +/// Merge a pair of snapshots and create a new snapshot with the result. +pub fn merge_snapshots_with_progress<C: DatabaseConflictHandler, P: ProgressCallback>( + first: &Snapshot, + second: &Snapshot, + mut conflict_handler: C, + mut progress: P, +) -> Result<Snapshot, ()> { + let value = unsafe { + BNCollaborationMergeSnapshots( + first.handle.as_ptr(), + second.handle.as_ptr(), + Some(C::cb_handle_conflict), + &mut conflict_handler as *mut C as *mut c_void, + Some(P::cb_progress_callback), + &mut progress as *mut P as *mut c_void, + ) + }; + 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); + let raw_changeset_ptr = NonNull::new(changeset).unwrap(); + // TODO: Do we take ownership with a ref here or not? + let changeset = Changeset::from_raw(raw_changeset_ptr); + ctxt.name_changeset(&changeset) + } +} + +impl<F> NameChangeset for F +where + F: for<'a> FnMut(&'a Changeset) -> bool, +{ + fn name_changeset(&mut self, changeset: &Changeset) -> bool { + self(changeset) + } +} + +pub struct NoNameChangeset; + +impl NameChangeset for NoNameChangeset { + 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 c_void, + keys: *mut *const 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` + // TODO: Replace with raw_to_string + let key = ManuallyDrop::new(BnString::from_raw(*key as *mut _)); + // TODO I guess dont drop here? + let raw_ptr = NonNull::new(*conflict).unwrap(); + let conflict = MergeConflict::from_raw(raw_ptr); + ctxt.handle_conflict(key.as_str(), &conflict) + }) + } +} + +impl<F> 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 c_void, + _keys: *mut *const c_char, + _conflicts: *mut *mut BNAnalysisMergeConflict, + conflict_count: usize, + ) -> bool { + // Fail if we have any conflicts. + conflict_count > 0 + } +} + +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 ctx: &mut Self = &mut *(ctxt as *mut Self); + // TODO: Verify that we dont own the merge conflict, or this list passed to us. + let conflicts_raw = core::slice::from_raw_parts(conflicts, conflict_count); + conflicts_raw + .iter() + .map(|t| NonNull::new_unchecked(*t)) + .map(|t| TypeArchiveMergeConflict::from_raw(t)) + .all(|conflict| ctx.handle_conflict(&conflict)) + } +} + +impl<F> 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 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/undo.rs b/rust/src/collaboration/undo.rs new file mode 100644 index 00000000..9f1cc5f0 --- /dev/null +++ b/rust/src/collaboration/undo.rs @@ -0,0 +1,166 @@ +use crate::collaboration::{Remote, RemoteFile, RemoteProject, RemoteSnapshot}; +use crate::rc::{CoreArrayProvider, CoreArrayProviderInner, Guard, Ref, RefCountable}; +use crate::string::BnString; +use binaryninjacore_sys::{ + BNCollaborationFreeIdList, BNCollaborationUndoEntry, BNCollaborationUndoEntryGetData, + BNCollaborationUndoEntryGetFile, BNCollaborationUndoEntryGetId, + BNCollaborationUndoEntryGetParent, BNCollaborationUndoEntryGetParentId, + BNCollaborationUndoEntryGetProject, BNCollaborationUndoEntryGetRemote, + BNCollaborationUndoEntryGetSnapshot, BNCollaborationUndoEntryGetUrl, + BNFreeCollaborationUndoEntry, BNFreeCollaborationUndoEntryList, + BNNewCollaborationUndoEntryReference, +}; +use std::fmt; +use std::fmt::{Display, Formatter}; +use std::ptr::NonNull; + +#[repr(transparent)] +pub struct RemoteUndoEntry { + handle: NonNull<BNCollaborationUndoEntry>, +} + +impl RemoteUndoEntry { + pub(crate) unsafe fn from_raw(handle: NonNull<BNCollaborationUndoEntry>) -> Self { + Self { handle } + } + + pub(crate) unsafe fn ref_from_raw(handle: NonNull<BNCollaborationUndoEntry>) -> Ref<Self> { + Ref::new(Self { handle }) + } + + /// Owning Snapshot + pub fn snapshot(&self) -> Result<Ref<RemoteSnapshot>, ()> { + let value = unsafe { BNCollaborationUndoEntryGetSnapshot(self.handle.as_ptr()) }; + let handle = NonNull::new(value).ok_or(())?; + Ok(unsafe { RemoteSnapshot::ref_from_raw(handle) }) + } + + /// Owning File + pub fn file(&self) -> Result<Ref<RemoteFile>, ()> { + let value = unsafe { BNCollaborationUndoEntryGetFile(self.handle.as_ptr()) }; + let handle = NonNull::new(value).ok_or(())?; + Ok(unsafe { RemoteFile::ref_from_raw(handle) }) + } + + /// Owning Project + pub fn project(&self) -> Result<Ref<RemoteProject>, ()> { + let value = unsafe { BNCollaborationUndoEntryGetProject(self.handle.as_ptr()) }; + let handle = NonNull::new(value).ok_or(())?; + Ok(unsafe { RemoteProject::ref_from_raw(handle) }) + } + + /// Owning Remote + pub fn remote(&self) -> Result<Ref<Remote>, ()> { + let value = unsafe { BNCollaborationUndoEntryGetRemote(self.handle.as_ptr()) }; + let handle = NonNull::new(value).ok_or(())?; + Ok(unsafe { Remote::ref_from_raw(handle) }) + } + + /// Web api endpoint url + pub fn url(&self) -> BnString { + let value = unsafe { BNCollaborationUndoEntryGetUrl(self.handle.as_ptr()) }; + assert!(!value.is_null()); + unsafe { BnString::from_raw(value) } + } + + /// Unique id + pub fn id(&self) -> RemoteUndoEntryId { + RemoteUndoEntryId(unsafe { BNCollaborationUndoEntryGetId(self.handle.as_ptr()) }) + } + + /// Id of parent undo entry + pub fn parent_id(&self) -> Option<RemoteUndoEntryId> { + let mut value = 0; + let success = + unsafe { BNCollaborationUndoEntryGetParentId(self.handle.as_ptr(), &mut value) }; + success.then_some(RemoteUndoEntryId(value)) + } + + /// Undo entry contents data + pub fn data(&self) -> Result<BnString, ()> { + let mut value = std::ptr::null_mut(); + let success = unsafe { BNCollaborationUndoEntryGetData(self.handle.as_ptr(), &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<Ref<RemoteUndoEntry>> { + let value = unsafe { BNCollaborationUndoEntryGetParent(self.handle.as_ptr()) }; + NonNull::new(value).map(|handle| unsafe { RemoteUndoEntry::ref_from_raw(handle) }) + } +} + +impl PartialEq for RemoteUndoEntry { + fn eq(&self, other: &Self) -> bool { + self.id() == other.id() + } +} +impl Eq for RemoteUndoEntry {} + +impl ToOwned for RemoteUndoEntry { + type Owned = Ref<Self>; + + fn to_owned(&self) -> Self::Owned { + unsafe { RefCountable::inc_ref(self) } + } +} + +unsafe impl RefCountable for RemoteUndoEntry { + unsafe fn inc_ref(handle: &Self) -> Ref<Self> { + Ref::new(Self { + handle: NonNull::new(BNNewCollaborationUndoEntryReference(handle.handle.as_ptr())) + .unwrap(), + }) + } + + unsafe fn dec_ref(handle: &Self) { + BNFreeCollaborationUndoEntry(handle.handle.as_ptr()); + } +} + +impl CoreArrayProvider for RemoteUndoEntry { + type Raw = *mut BNCollaborationUndoEntry; + type Context = (); + type Wrapped<'a> = Guard<'a, Self>; +} + +unsafe impl CoreArrayProviderInner for RemoteUndoEntry { + 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> { + let raw_ptr = NonNull::new(*raw).unwrap(); + Guard::new(Self::from_raw(raw_ptr), context) + } +} + +#[repr(transparent)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct RemoteUndoEntryId(pub u64); + +impl Display for RemoteUndoEntryId { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.write_fmt(format_args!("{}", self.0)) + } +} + +impl CoreArrayProvider for RemoteUndoEntryId { + type Raw = u64; + type Context = (); + type Wrapped<'a> = RemoteUndoEntryId; +} + +unsafe impl CoreArrayProviderInner for RemoteUndoEntryId { + 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> { + RemoteUndoEntryId(*raw) + } +} diff --git a/rust/src/collaboration/user.rs b/rust/src/collaboration/user.rs new file mode 100644 index 00000000..0e3433d3 --- /dev/null +++ b/rust/src/collaboration/user.rs @@ -0,0 +1,154 @@ +use super::Remote; +use binaryninjacore_sys::*; +use std::ffi::c_char; +use std::ptr::NonNull; + +use crate::rc::{CoreArrayProvider, CoreArrayProviderInner, Guard, Ref, RefCountable}; +use crate::string::{BnStrCompatible, BnString}; + +#[repr(transparent)] +pub struct RemoteUser { + pub(crate) handle: NonNull<BNCollaborationUser>, +} + +impl RemoteUser { + pub(crate) unsafe fn from_raw(handle: NonNull<BNCollaborationUser>) -> Self { + Self { handle } + } + + pub(crate) unsafe fn ref_from_raw(handle: NonNull<BNCollaborationUser>) -> Ref<Self> { + Ref::new(Self { handle }) + } + + /// Owning Remote + pub fn remote(&self) -> Result<Ref<Remote>, ()> { + let value = unsafe { BNCollaborationUserGetRemote(self.handle.as_ptr()) }; + let handle = NonNull::new(value).ok_or(())?; + Ok(unsafe { Remote::ref_from_raw(handle) }) + } + + /// Web api endpoint url + pub fn url(&self) -> BnString { + let value = unsafe { BNCollaborationUserGetUrl(self.handle.as_ptr()) }; + assert!(!value.is_null()); + unsafe { BnString::from_raw(value) } + } + + /// Unique id + pub fn id(&self) -> BnString { + let value = unsafe { BNCollaborationUserGetId(self.handle.as_ptr()) }; + assert!(!value.is_null()); + unsafe { BnString::from_raw(value) } + } + + /// User's login username + pub fn username(&self) -> BnString { + let value = unsafe { BNCollaborationUserGetUsername(self.handle.as_ptr()) }; + 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<U: BnStrCompatible>(&self, username: U) -> Result<(), ()> { + let username = username.into_bytes_with_nul(); + let result = unsafe { + BNCollaborationUserSetUsername( + self.handle.as_ptr(), + username.as_ref().as_ptr() as *const c_char, + ) + }; + if result { + Ok(()) + } else { + Err(()) + } + } + + /// User's email address + pub fn email(&self) -> BnString { + let value = unsafe { BNCollaborationUserGetEmail(self.handle.as_ptr()) }; + 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<U: BnStrCompatible>(&self, email: U) -> Result<(), ()> { + let username = email.into_bytes_with_nul(); + let result = unsafe { + BNCollaborationUserSetEmail( + self.handle.as_ptr(), + username.as_ref().as_ptr() as *const 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.handle.as_ptr()) }; + 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.handle.as_ptr()) } + } + + /// 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.handle.as_ptr(), value) } { + Ok(()) + } else { + Err(()) + } + } +} + +impl PartialEq for RemoteUser { + fn eq(&self, other: &Self) -> bool { + self.id() == other.id() + } +} +impl Eq for RemoteUser {} + +impl ToOwned for RemoteUser { + type Owned = Ref<Self>; + + fn to_owned(&self) -> Self::Owned { + unsafe { RefCountable::inc_ref(self) } + } +} + +unsafe impl RefCountable for RemoteUser { + unsafe fn inc_ref(handle: &Self) -> Ref<Self> { + Ref::new(Self { + handle: NonNull::new(BNNewCollaborationUserReference(handle.handle.as_ptr())).unwrap(), + }) + } + + unsafe fn dec_ref(handle: &Self) { + BNFreeCollaborationUser(handle.handle.as_ptr()); + } +} + +impl CoreArrayProvider for RemoteUser { + type Raw = *mut BNCollaborationUser; + type Context = (); + type Wrapped<'a> = Guard<'a, RemoteUser>; +} + +unsafe impl CoreArrayProviderInner for RemoteUser { + 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> { + let raw_ptr = NonNull::new(*raw).unwrap(); + Guard::new(Self::from_raw(raw_ptr), context) + } +} |
