summaryrefslogtreecommitdiff
path: root/rust/src/collaboration/sync.rs
diff options
context:
space:
mode:
Diffstat (limited to 'rust/src/collaboration/sync.rs')
-rw-r--r--rust/src/collaboration/sync.rs941
1 files changed, 941 insertions, 0 deletions
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
+ }
+}