summaryrefslogtreecommitdiff
path: root/rust/src/collaboration
diff options
context:
space:
mode:
Diffstat (limited to 'rust/src/collaboration')
-rw-r--r--rust/src/collaboration/changeset.rs116
-rw-r--r--rust/src/collaboration/file.rs647
-rw-r--r--rust/src/collaboration/folder.rs180
-rw-r--r--rust/src/collaboration/group.rs196
-rw-r--r--rust/src/collaboration/merge.rs204
-rw-r--r--rust/src/collaboration/permission.rs159
-rw-r--r--rust/src/collaboration/project.rs1001
-rw-r--r--rust/src/collaboration/remote.rs959
-rw-r--r--rust/src/collaboration/snapshot.rs368
-rw-r--r--rust/src/collaboration/sync.rs941
-rw-r--r--rust/src/collaboration/undo.rs166
-rw-r--r--rust/src/collaboration/user.rs154
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)
+ }
+}