binaryninja/collaboration/
sync.rs

1use super::{
2    Changeset, MergeConflict, Remote, RemoteFile, RemoteFolder, RemoteProject, RemoteSnapshot,
3};
4use binaryninjacore_sys::*;
5use std::ffi::{c_char, c_void};
6use std::path::{Path, PathBuf};
7use std::ptr::NonNull;
8
9use crate::binary_view::{BinaryView, BinaryViewExt};
10use crate::database::{snapshot::Snapshot, Database};
11use crate::file_metadata::FileMetadata;
12use crate::progress::{NoProgressCallback, ProgressCallback};
13use crate::project::file::ProjectFile;
14use crate::rc::Ref;
15use crate::string::{raw_to_string, BnString, IntoCStr};
16use crate::type_archive::{TypeArchive, TypeArchiveMergeConflict};
17
18/// Get the default directory path for a remote Project. This is based off the Setting for
19/// collaboration.directory, the project's id, and the project's remote's id.
20pub fn default_project_path(project: &RemoteProject) -> Result<PathBuf, ()> {
21    let result = unsafe { BNCollaborationDefaultProjectPath(project.handle.as_ptr()) };
22    let success = !result.is_null();
23    success
24        .then(|| PathBuf::from(unsafe { BnString::into_string(result) }))
25        .ok_or(())
26}
27
28// Get the default filepath for a remote File. This is based off the Setting for
29// collaboration.directory, the file's id, the file's project's id, and the file's
30// remote's id.
31pub fn default_file_path(file: &RemoteFile) -> Result<PathBuf, ()> {
32    let result = unsafe { BNCollaborationDefaultFilePath(file.handle.as_ptr()) };
33    let success = !result.is_null();
34    success
35        .then(|| PathBuf::from(unsafe { BnString::into_string(result) }))
36        .ok_or(())
37}
38
39/// Download a file from its remote, saving all snapshots to a database in the
40/// specified location. Returns a FileContext for opening the file later.
41///
42/// * `file` - Remote File to download and open
43/// * `db_path` - File path for saved database
44pub fn download_file(file: &RemoteFile, db_path: &Path) -> Result<Ref<FileMetadata>, ()> {
45    download_file_with_progress(file, db_path, NoProgressCallback)
46}
47
48/// Download a file from its remote, saving all snapshots to a database in the
49/// specified location. Returns a FileContext for opening the file later.
50///
51/// * `file` - Remote File to download and open
52/// * `db_path` - File path for saved database
53/// * `progress` - Function to call for progress updates
54pub fn download_file_with_progress<F: ProgressCallback>(
55    file: &RemoteFile,
56    db_path: &Path,
57    mut progress: F,
58) -> Result<Ref<FileMetadata>, ()> {
59    let db_path = db_path.to_cstr();
60    let result = unsafe {
61        BNCollaborationDownloadFile(
62            file.handle.as_ptr(),
63            db_path.as_ptr(),
64            Some(F::cb_progress_callback),
65            &mut progress as *mut F as *mut c_void,
66        )
67    };
68    let success = !result.is_null();
69    success
70        .then(|| unsafe { Ref::new(FileMetadata::from_raw(result)) })
71        .ok_or(())
72}
73
74/// Upload a file, with database, to the remote under the given project
75///
76/// * `project` - Remote project under which to place the new file
77/// * `parent_folder` - Optional parent folder in which to place this file
78/// * `metadata` - Local file with database
79/// * `name_changeset` - Function to call for naming a pushed changeset, if necessary
80pub fn upload_database<N: NameChangeset>(
81    project: &RemoteProject,
82    parent_folder: Option<&RemoteFolder>,
83    metadata: &FileMetadata,
84    name_changeset: N,
85) -> Result<Ref<RemoteFile>, ()> {
86    upload_database_with_progress(
87        project,
88        parent_folder,
89        metadata,
90        name_changeset,
91        NoProgressCallback,
92    )
93}
94
95/// Upload a file, with database, to the remote under the given project
96///
97/// * `metadata` - Local file with database
98/// * `project` - Remote project under which to place the new file
99/// * `parent_folder` - Optional parent folder in which to place this file
100/// * `name_changeset` - Function to call for naming a pushed changeset, if necessary
101/// * `progress` - Function to call for progress updates
102pub fn upload_database_with_progress<P: ProgressCallback, N: NameChangeset>(
103    project: &RemoteProject,
104    parent_folder: Option<&RemoteFolder>,
105    metadata: &FileMetadata,
106    mut name_changeset: N,
107    mut progress: P,
108) -> Result<Ref<RemoteFile>, ()> {
109    let folder_raw = parent_folder.map_or(std::ptr::null_mut(), |h| h.handle.as_ptr());
110    let result = unsafe {
111        BNCollaborationUploadDatabase(
112            metadata.handle,
113            project.handle.as_ptr(),
114            folder_raw,
115            Some(P::cb_progress_callback),
116            &mut progress as *mut P as *mut c_void,
117            Some(N::cb_name_changeset),
118            &mut name_changeset as *mut N as *mut c_void,
119        )
120    };
121    NonNull::new(result)
122        .map(|raw| unsafe { RemoteFile::ref_from_raw(raw) })
123        .ok_or(())
124}
125
126/// Test if a database is valid for use in collaboration
127pub fn is_collaboration_database(database: &Database) -> bool {
128    unsafe { BNCollaborationIsCollaborationDatabase(database.handle.as_ptr()) }
129}
130
131/// Get the Remote for a Database
132pub fn get_remote_for_local_database(database: &Database) -> Result<Option<Ref<Remote>>, ()> {
133    let mut value = std::ptr::null_mut();
134    let success =
135        unsafe { BNCollaborationGetRemoteForLocalDatabase(database.handle.as_ptr(), &mut value) };
136    success
137        .then(|| NonNull::new(value).map(|handle| unsafe { Remote::ref_from_raw(handle) }))
138        .ok_or(())
139}
140
141/// Get the Remote for a BinaryView
142pub fn get_remote_for_binary_view(bv: &BinaryView) -> Result<Option<Ref<Remote>>, ()> {
143    let Some(db) = bv.file().database() else {
144        return Ok(None);
145    };
146    get_remote_for_local_database(&db)
147}
148
149/// Get the Remote Project for a Database, returning the Remote project from one of the
150/// connected remotes, or None if not found or if projects are not pulled
151pub fn get_remote_project_for_local_database(
152    database: &Database,
153) -> Result<Option<Ref<RemoteProject>>, ()> {
154    let mut value = std::ptr::null_mut();
155    let success = unsafe {
156        BNCollaborationGetRemoteProjectForLocalDatabase(database.handle.as_ptr(), &mut value)
157    };
158    success
159        .then(|| NonNull::new(value).map(|handle| unsafe { RemoteProject::ref_from_raw(handle) }))
160        .ok_or(())
161}
162
163/// Get the Remote File for a Database
164pub fn get_remote_file_for_local_database(
165    database: &Database,
166) -> Result<Option<Ref<RemoteFile>>, ()> {
167    let mut value = std::ptr::null_mut();
168    let success = unsafe {
169        BNCollaborationGetRemoteFileForLocalDatabase(database.handle.as_ptr(), &mut value)
170    };
171    success
172        .then(|| NonNull::new(value).map(|handle| unsafe { RemoteFile::ref_from_raw(handle) }))
173        .ok_or(())
174}
175
176/// Add a snapshot to the id map in a database
177pub fn assign_snapshot_map(
178    local_snapshot: &Snapshot,
179    remote_snapshot: &RemoteSnapshot,
180) -> Result<(), ()> {
181    let success = unsafe {
182        BNCollaborationAssignSnapshotMap(
183            local_snapshot.handle.as_ptr(),
184            remote_snapshot.handle.as_ptr(),
185        )
186    };
187    success.then_some(()).ok_or(())
188}
189
190/// Get the remote snapshot associated with a local snapshot (if it exists)
191pub fn get_remote_snapshot_from_local(snap: &Snapshot) -> Result<Option<Ref<RemoteSnapshot>>, ()> {
192    let mut value = std::ptr::null_mut();
193    let success =
194        unsafe { BNCollaborationGetRemoteSnapshotFromLocal(snap.handle.as_ptr(), &mut value) };
195    success
196        .then(|| NonNull::new(value).map(|handle| unsafe { RemoteSnapshot::ref_from_raw(handle) }))
197        .ok_or(())
198}
199
200/// Get the local snapshot associated with a remote snapshot (if it exists)
201pub fn get_local_snapshot_for_remote(
202    snapshot: &RemoteSnapshot,
203    database: &Database,
204) -> Result<Option<Ref<Snapshot>>, ()> {
205    let mut value = std::ptr::null_mut();
206    let success = unsafe {
207        BNCollaborationGetLocalSnapshotFromRemote(
208            snapshot.handle.as_ptr(),
209            database.handle.as_ptr(),
210            &mut value,
211        )
212    };
213    success
214        .then(|| NonNull::new(value).map(|handle| unsafe { Snapshot::ref_from_raw(handle) }))
215        .ok_or(())
216}
217
218pub fn download_database<S>(file: &RemoteFile, location: &Path, force: bool) -> Result<(), ()> {
219    download_database_with_progress(file, location, force, NoProgressCallback)
220}
221
222pub fn download_database_with_progress<PC>(
223    file: &RemoteFile,
224    location: &Path,
225    force: bool,
226    mut progress: PC,
227) -> Result<(), ()>
228where
229    PC: ProgressCallback,
230{
231    let db_path = location.to_cstr();
232    let success = unsafe {
233        BNCollaborationDownloadDatabaseForFile(
234            file.handle.as_ptr(),
235            db_path.as_ptr(),
236            force,
237            Some(PC::cb_progress_callback),
238            &mut progress as *mut PC as *mut c_void,
239        )
240    };
241    success.then_some(()).ok_or(())
242}
243
244/// Completely sync a database, pushing/pulling/merging/applying changes
245///
246/// * `database` - Database to sync
247/// * `file` - File to sync with
248/// * `conflict_handler` - Function to call to resolve snapshot conflicts
249/// * `name_changeset` - Function to call for naming a pushed changeset, if necessary
250pub fn sync_database<C: DatabaseConflictHandler, N: NameChangeset>(
251    database: &Database,
252    file: &RemoteFile,
253    conflict_handler: C,
254    name_changeset: N,
255) -> Result<(), ()> {
256    sync_database_with_progress(
257        database,
258        file,
259        conflict_handler,
260        name_changeset,
261        NoProgressCallback,
262    )
263}
264
265/// Completely sync a database, pushing/pulling/merging/applying changes
266///
267/// * `database` - Database to sync
268/// * `file` - File to sync with
269/// * `conflict_handler` - Function to call to resolve snapshot conflicts
270/// * `name_changeset` - Function to call for naming a pushed changeset, if necessary
271/// * `progress` - Function to call for progress updates
272pub fn sync_database_with_progress<
273    C: DatabaseConflictHandler,
274    P: ProgressCallback,
275    N: NameChangeset,
276>(
277    database: &Database,
278    file: &RemoteFile,
279    mut conflict_handler: C,
280    mut name_changeset: N,
281    mut progress: P,
282) -> Result<(), ()> {
283    let success = unsafe {
284        BNCollaborationSyncDatabase(
285            database.handle.as_ptr(),
286            file.handle.as_ptr(),
287            Some(C::cb_handle_conflict),
288            &mut conflict_handler as *mut C as *mut c_void,
289            Some(P::cb_progress_callback),
290            &mut progress as *mut P as *mut c_void,
291            Some(N::cb_name_changeset),
292            &mut name_changeset as *mut N as *mut c_void,
293        )
294    };
295    success.then_some(()).ok_or(())
296}
297
298/// Pull updated snapshots from the remote. Merge local changes with remote changes and
299/// potentially create a new snapshot for unsaved changes, named via name_changeset.
300///
301/// * `database` - Database to pull
302/// * `file` - Remote File to pull to
303/// * `conflict_handler` - Function to call to resolve snapshot conflicts
304/// * `name_changeset` - Function to call for naming a pushed changeset, if necessary
305pub fn pull_database<C: DatabaseConflictHandler, N: NameChangeset>(
306    database: &Database,
307    file: &RemoteFile,
308    conflict_handler: C,
309    name_changeset: N,
310) -> Result<usize, ()> {
311    pull_database_with_progress(
312        database,
313        file,
314        conflict_handler,
315        name_changeset,
316        NoProgressCallback,
317    )
318}
319
320/// Pull updated snapshots from the remote. Merge local changes with remote changes and
321/// potentially create a new snapshot for unsaved changes, named via name_changeset.
322///
323/// * `database` - Database to pull
324/// * `file` - Remote File to pull to
325/// * `conflict_handler` - Function to call to resolve snapshot conflicts
326/// * `name_changeset` - Function to call for naming a pushed changeset, if necessary
327/// * `progress` - Function to call for progress updates
328pub fn pull_database_with_progress<
329    C: DatabaseConflictHandler,
330    P: ProgressCallback,
331    N: NameChangeset,
332>(
333    database: &Database,
334    file: &RemoteFile,
335    mut conflict_handler: C,
336    mut name_changeset: N,
337    mut progress: P,
338) -> Result<usize, ()> {
339    let mut count = 0;
340    let success = unsafe {
341        BNCollaborationPullDatabase(
342            database.handle.as_ptr(),
343            file.handle.as_ptr(),
344            &mut count,
345            Some(C::cb_handle_conflict),
346            &mut conflict_handler as *mut C as *mut c_void,
347            Some(P::cb_progress_callback),
348            &mut progress as *mut P as *mut c_void,
349            Some(N::cb_name_changeset),
350            &mut name_changeset as *mut N as *mut c_void,
351        )
352    };
353    success.then_some(count).ok_or(())
354}
355
356/// Merge all leaf snapshots in a database down to a single leaf snapshot.
357///
358/// * `database` - Database to merge
359/// * `conflict_handler` - Function to call for progress updates
360pub fn merge_database<C: DatabaseConflictHandler>(
361    database: &Database,
362    conflict_handler: C,
363) -> Result<(), ()> {
364    merge_database_with_progress(database, conflict_handler, NoProgressCallback)
365}
366
367/// Merge all leaf snapshots in a database down to a single leaf snapshot.
368///
369/// * `database` - Database to merge
370/// * `conflict_handler` - Function to call for progress updates
371/// * `progress` - Function to call to resolve snapshot conflicts
372pub fn merge_database_with_progress<C: DatabaseConflictHandler, P: ProgressCallback>(
373    database: &Database,
374    mut conflict_handler: C,
375    mut progress: P,
376) -> Result<(), ()> {
377    let success = unsafe {
378        BNCollaborationMergeDatabase(
379            database.handle.as_ptr(),
380            Some(C::cb_handle_conflict),
381            &mut conflict_handler as *mut C as *mut c_void,
382            Some(P::cb_progress_callback),
383            &mut progress as *mut P as *mut c_void,
384        )
385    };
386    success.then_some(()).ok_or(())
387}
388
389/// Push locally added snapshots to the remote
390///
391/// * `database` - Database to push
392/// * `file` - Remote File to push to
393pub fn push_database(database: &Database, file: &RemoteFile) -> Result<usize, ()> {
394    push_database_with_progress(database, file, NoProgressCallback)
395}
396
397/// Push locally added snapshots to the remote
398///
399/// * `database` - Database to push
400/// * `file` - Remote File to push to
401/// * `progress` - Function to call for progress updates
402pub fn push_database_with_progress<P: ProgressCallback>(
403    database: &Database,
404    file: &RemoteFile,
405    mut progress: P,
406) -> Result<usize, ()> {
407    let mut count = 0;
408    let success = unsafe {
409        BNCollaborationPushDatabase(
410            database.handle.as_ptr(),
411            file.handle.as_ptr(),
412            &mut count,
413            Some(P::cb_progress_callback),
414            &mut progress as *mut P as *mut c_void,
415        )
416    };
417    success.then_some(count).ok_or(())
418}
419
420/// Print debug information about a database to stdout
421pub fn dump_database(database: &Database) -> Result<(), ()> {
422    let success = unsafe { BNCollaborationDumpDatabase(database.handle.as_ptr()) };
423    success.then_some(()).ok_or(())
424}
425
426/// Ignore a snapshot from database syncing operations
427///
428/// * `database` - Parent database
429/// * `snapshot` - Snapshot to ignore
430pub fn ignore_snapshot(database: &Database, snapshot: &Snapshot) -> Result<(), ()> {
431    let success = unsafe {
432        BNCollaborationIgnoreSnapshot(database.handle.as_ptr(), snapshot.handle.as_ptr())
433    };
434    success.then_some(()).ok_or(())
435}
436
437/// Test if a snapshot is ignored from the database
438///
439/// * `database` - Parent database
440/// * `snapshot` - Snapshot to test
441pub fn is_snapshot_ignored(database: &Database, snapshot: &Snapshot) -> bool {
442    unsafe { BNCollaborationIsSnapshotIgnored(database.handle.as_ptr(), snapshot.handle.as_ptr()) }
443}
444
445/// Get the remote author of a local snapshot
446///
447/// * `database` - Parent database
448/// * `snapshot` - Snapshot to query
449pub fn get_snapshot_author(
450    database: &Database,
451    snapshot: &Snapshot,
452) -> Result<Option<BnString>, ()> {
453    let mut value = std::ptr::null_mut();
454    let success = unsafe {
455        BNCollaborationGetSnapshotAuthor(
456            database.handle.as_ptr(),
457            snapshot.handle.as_ptr(),
458            &mut value,
459        )
460    };
461    success
462        .then(|| (!value.is_null()).then(|| unsafe { BnString::from_raw(value) }))
463        .ok_or(())
464}
465
466/// Set the remote author of a local snapshot (does not upload)
467///
468/// * `database` - Parent database
469/// * `snapshot` - Snapshot to edit
470/// * `author` - Target author
471pub fn set_snapshot_author(
472    database: &Database,
473    snapshot: &Snapshot,
474    author: &str,
475) -> Result<(), ()> {
476    let author = author.to_cstr();
477    let success = unsafe {
478        BNCollaborationSetSnapshotAuthor(
479            database.handle.as_ptr(),
480            snapshot.handle.as_ptr(),
481            author.as_ptr(),
482        )
483    };
484    success.then_some(()).ok_or(())
485}
486
487// TODO: this needs to be removed imo
488pub(crate) fn pull_projects(database: &Database) -> Result<bool, ()> {
489    let Some(remote) = get_remote_for_local_database(database)? else {
490        return Ok(false);
491    };
492    remote.pull_projects()?;
493    Ok(true)
494}
495
496// TODO: This needs to be removed imo
497pub(crate) fn pull_files(database: &Database) -> Result<bool, ()> {
498    if !pull_projects(database)? {
499        return Ok(false);
500    }
501    let Some(project) = get_remote_project_for_local_database(database)? else {
502        return Ok(false);
503    };
504    project.pull_files()?;
505    Ok(true)
506}
507
508/// Completely sync a type archive, pushing/pulling/merging/applying changes
509///
510/// * `type_archive` - TypeArchive to sync
511/// * `file` - File to sync with
512/// * `conflict_handler` - Function to call to resolve snapshot conflicts
513pub fn sync_type_archive<C: TypeArchiveConflictHandler>(
514    type_archive: &TypeArchive,
515    file: &RemoteFile,
516    conflict_handler: C,
517) -> Result<(), ()> {
518    sync_type_archive_with_progress(type_archive, file, conflict_handler, NoProgressCallback)
519}
520
521/// Completely sync a type archive, pushing/pulling/merging/applying changes
522///
523/// * `type_archive` - TypeArchive to sync
524/// * `file` - File to sync with
525/// * `conflict_handler` - Function to call to resolve snapshot conflicts
526/// * `progress` - Function to call for progress updates
527pub fn sync_type_archive_with_progress<C: TypeArchiveConflictHandler, P: ProgressCallback>(
528    type_archive: &TypeArchive,
529    file: &RemoteFile,
530    mut conflict_handler: C,
531    mut progress: P,
532) -> Result<(), ()> {
533    let success = unsafe {
534        BNCollaborationSyncTypeArchive(
535            type_archive.handle.as_ptr(),
536            file.handle.as_ptr(),
537            Some(C::cb_handle_conflict),
538            &mut conflict_handler as *mut C as *mut c_void,
539            Some(P::cb_progress_callback),
540            &mut progress as *mut P as *mut c_void,
541        )
542    };
543    success.then_some(()).ok_or(())
544}
545
546/// Push locally added snapshots to the remote
547///
548/// * `type_archive` - TypeArchive to push
549/// * `file` - Remote File to push to
550pub fn push_type_archive(type_archive: &TypeArchive, file: &RemoteFile) -> Result<usize, ()> {
551    push_type_archive_with_progress(type_archive, file, NoProgressCallback)
552}
553
554/// Push locally added snapshots to the remote
555///
556/// * `type_archive` - TypeArchive to push
557/// * `file` - Remote File to push to
558/// * `progress` - Function to call for progress updates
559pub fn push_type_archive_with_progress<P: ProgressCallback>(
560    type_archive: &TypeArchive,
561    file: &RemoteFile,
562    mut progress: P,
563) -> Result<usize, ()> {
564    let mut count = 0;
565    let success = unsafe {
566        BNCollaborationPushTypeArchive(
567            type_archive.handle.as_ptr(),
568            file.handle.as_ptr(),
569            &mut count,
570            Some(P::cb_progress_callback),
571            &mut progress as *mut P as *mut c_void,
572        )
573    };
574    success.then_some(count).ok_or(())
575}
576
577/// Pull updated type archives from the remote.
578///
579/// * `type_archive` - TypeArchive to pull
580/// * `file` - Remote File to pull to
581/// * `conflict_handler` - Function to call to resolve snapshot conflicts
582pub fn pull_type_archive<C: TypeArchiveConflictHandler>(
583    type_archive: &TypeArchive,
584    file: &RemoteFile,
585    conflict_handler: C,
586) -> Result<usize, ()> {
587    pull_type_archive_with_progress(type_archive, file, conflict_handler, NoProgressCallback)
588}
589
590/// Pull updated type archives from the remote.
591///
592/// * `type_archive` - TypeArchive to pull
593/// * `file` - Remote File to pull to
594/// * `conflict_handler` - Function to call to resolve snapshot conflicts
595/// * `progress` - Function to call for progress updates
596pub fn pull_type_archive_with_progress<C: TypeArchiveConflictHandler, P: ProgressCallback>(
597    type_archive: &TypeArchive,
598    file: &RemoteFile,
599    mut conflict_handler: C,
600    mut progress: P,
601) -> Result<usize, ()> {
602    let mut count = 0;
603    let success = unsafe {
604        BNCollaborationPullTypeArchive(
605            type_archive.handle.as_ptr(),
606            file.handle.as_ptr(),
607            &mut count,
608            Some(C::cb_handle_conflict),
609            &mut conflict_handler as *mut C as *mut c_void,
610            Some(P::cb_progress_callback),
611            &mut progress as *mut P as *mut c_void,
612        )
613    };
614    success.then_some(count).ok_or(())
615}
616
617/// Test if a type archive is valid for use in collaboration
618pub fn is_collaboration_type_archive(type_archive: &TypeArchive) -> bool {
619    unsafe { BNCollaborationIsCollaborationTypeArchive(type_archive.handle.as_ptr()) }
620}
621
622/// Get the Remote for a Type Archive
623pub fn get_remote_for_local_type_archive(type_archive: &TypeArchive) -> Option<Ref<Remote>> {
624    let value =
625        unsafe { BNCollaborationGetRemoteForLocalTypeArchive(type_archive.handle.as_ptr()) };
626    NonNull::new(value).map(|handle| unsafe { Remote::ref_from_raw(handle) })
627}
628
629/// Get the Remote Project for a Type Archive
630pub fn get_remote_project_for_local_type_archive(
631    database: &TypeArchive,
632) -> Option<Ref<RemoteProject>> {
633    let value =
634        unsafe { BNCollaborationGetRemoteProjectForLocalTypeArchive(database.handle.as_ptr()) };
635    NonNull::new(value).map(|handle| unsafe { RemoteProject::ref_from_raw(handle) })
636}
637
638/// Get the Remote File for a Type Archive
639pub fn get_remote_file_for_local_type_archive(database: &TypeArchive) -> Option<Ref<RemoteFile>> {
640    let value =
641        unsafe { BNCollaborationGetRemoteFileForLocalTypeArchive(database.handle.as_ptr()) };
642    NonNull::new(value).map(|handle| unsafe { RemoteFile::ref_from_raw(handle) })
643}
644
645/// Get the remote snapshot associated with a local snapshot (if it exists) in a Type Archive
646pub fn get_remote_snapshot_from_local_type_archive(
647    type_archive: &TypeArchive,
648    snapshot_id: &str,
649) -> Option<Ref<RemoteSnapshot>> {
650    let snapshot_id = snapshot_id.to_cstr();
651    let value = unsafe {
652        BNCollaborationGetRemoteSnapshotFromLocalTypeArchive(
653            type_archive.handle.as_ptr(),
654            snapshot_id.as_ptr(),
655        )
656    };
657    NonNull::new(value).map(|handle| unsafe { RemoteSnapshot::ref_from_raw(handle) })
658}
659
660/// Get the local snapshot associated with a remote snapshot (if it exists) in a Type Archive
661pub fn get_local_snapshot_from_remote_type_archive(
662    snapshot: &RemoteSnapshot,
663    type_archive: &TypeArchive,
664) -> Option<BnString> {
665    let value = unsafe {
666        BNCollaborationGetLocalSnapshotFromRemoteTypeArchive(
667            snapshot.handle.as_ptr(),
668            type_archive.handle.as_ptr(),
669        )
670    };
671    (!value.is_null()).then(|| unsafe { BnString::from_raw(value) })
672}
673
674/// Test if a snapshot is ignored from the archive
675pub fn is_type_archive_snapshot_ignored(type_archive: &TypeArchive, snapshot_id: &str) -> bool {
676    let snapshot_id = snapshot_id.to_cstr();
677    unsafe {
678        BNCollaborationIsTypeArchiveSnapshotIgnored(
679            type_archive.handle.as_ptr(),
680            snapshot_id.as_ptr(),
681        )
682    }
683}
684
685/// Download a type archive from its remote, saving all snapshots to an archive in the
686/// specified `location`. Returns a [`TypeArchive`] for using later.
687pub fn download_type_archive(
688    file: &RemoteFile,
689    location: &Path,
690) -> Result<Option<Ref<TypeArchive>>, ()> {
691    download_type_archive_with_progress(file, location, NoProgressCallback)
692}
693
694/// Download a type archive from its remote, saving all snapshots to an archive in the
695/// specified `location`. Returns a [`TypeArchive`] for using later.
696pub fn download_type_archive_with_progress<PC: ProgressCallback>(
697    file: &RemoteFile,
698    location: &Path,
699    mut progress: PC,
700) -> Result<Option<Ref<TypeArchive>>, ()> {
701    let mut value = std::ptr::null_mut();
702    let db_path = location.to_cstr();
703    let success = unsafe {
704        BNCollaborationDownloadTypeArchive(
705            file.handle.as_ptr(),
706            db_path.as_ptr(),
707            Some(PC::cb_progress_callback),
708            &mut progress as *mut PC as *mut c_void,
709            &mut value,
710        )
711    };
712    success
713        .then(|| NonNull::new(value).map(|handle| unsafe { TypeArchive::ref_from_raw(handle) }))
714        .ok_or(())
715}
716
717/// Upload a type archive
718pub fn upload_type_archive(
719    archive: &TypeArchive,
720    project: &RemoteProject,
721    // TODO: Is this required?
722    folder: &RemoteFolder,
723    core_file: &ProjectFile,
724) -> Result<Ref<RemoteFile>, ()> {
725    upload_type_archive_with_progress(archive, project, folder, core_file, NoProgressCallback)
726}
727
728/// Upload a type archive
729pub fn upload_type_archive_with_progress<P: ProgressCallback>(
730    archive: &TypeArchive,
731    project: &RemoteProject,
732    // TODO: Is this required?
733    folder: &RemoteFolder,
734    // TODO: I dislike the word "core" just say local?
735    core_file: &ProjectFile,
736    mut progress: P,
737) -> Result<Ref<RemoteFile>, ()> {
738    let mut value = std::ptr::null_mut();
739    let success = unsafe {
740        BNCollaborationUploadTypeArchive(
741            archive.handle.as_ptr(),
742            project.handle.as_ptr(),
743            folder.handle.as_ptr(),
744            Some(P::cb_progress_callback),
745            &mut progress as *const P as *mut c_void,
746            core_file.handle.as_ptr(),
747            &mut value,
748        )
749    };
750    success
751        .then(|| {
752            NonNull::new(value)
753                .map(|handle| unsafe { RemoteFile::ref_from_raw(handle) })
754                .unwrap()
755        })
756        .ok_or(())
757}
758
759/// Merge a pair of snapshots and create a new snapshot with the result.
760pub fn merge_snapshots<C: DatabaseConflictHandler>(
761    first: &Snapshot,
762    second: &Snapshot,
763    conflict_handler: C,
764) -> Result<Snapshot, ()> {
765    merge_snapshots_with_progress(first, second, conflict_handler, NoProgressCallback)
766}
767
768/// Merge a pair of snapshots and create a new snapshot with the result.
769pub fn merge_snapshots_with_progress<C: DatabaseConflictHandler, P: ProgressCallback>(
770    first: &Snapshot,
771    second: &Snapshot,
772    mut conflict_handler: C,
773    mut progress: P,
774) -> Result<Snapshot, ()> {
775    let value = unsafe {
776        BNCollaborationMergeSnapshots(
777            first.handle.as_ptr(),
778            second.handle.as_ptr(),
779            Some(C::cb_handle_conflict),
780            &mut conflict_handler as *mut C as *mut c_void,
781            Some(P::cb_progress_callback),
782            &mut progress as *mut P as *mut c_void,
783        )
784    };
785    NonNull::new(value)
786        .map(|handle| unsafe { Snapshot::from_raw(handle) })
787        .ok_or(())
788}
789
790pub trait NameChangeset: Sized {
791    fn name_changeset(&mut self, changeset: &Changeset) -> bool;
792
793    unsafe extern "C" fn cb_name_changeset(
794        ctxt: *mut ::std::os::raw::c_void,
795        changeset: *mut BNCollaborationChangeset,
796    ) -> bool {
797        let ctxt: &mut Self = &mut *(ctxt as *mut Self);
798        let raw_changeset_ptr = NonNull::new(changeset).unwrap();
799        // TODO: Do we take ownership with a ref here or not?
800        let changeset = Changeset::from_raw(raw_changeset_ptr);
801        ctxt.name_changeset(&changeset)
802    }
803}
804
805impl<F> NameChangeset for F
806where
807    F: for<'a> FnMut(&'a Changeset) -> bool,
808{
809    fn name_changeset(&mut self, changeset: &Changeset) -> bool {
810        self(changeset)
811    }
812}
813
814pub struct NoNameChangeset;
815
816impl NameChangeset for NoNameChangeset {
817    fn name_changeset(&mut self, _changeset: &Changeset) -> bool {
818        unreachable!()
819    }
820
821    unsafe extern "C" fn cb_name_changeset(
822        _ctxt: *mut std::os::raw::c_void,
823        _changeset: *mut BNCollaborationChangeset,
824    ) -> bool {
825        true
826    }
827}
828
829/// Helper trait that resolves conflicts
830pub trait DatabaseConflictHandler: Sized {
831    /// Handle any merge conflicts by calling their success() function with a merged value
832    ///
833    /// * `conflicts` - conflicts ids to conflicts structures
834    ///
835    /// Return true if all conflicts were successfully merged
836    fn handle_conflict(&mut self, keys: &str, conflicts: &MergeConflict) -> bool;
837
838    unsafe extern "C" fn cb_handle_conflict(
839        ctxt: *mut c_void,
840        keys: *mut *const c_char,
841        conflicts: *mut *mut BNAnalysisMergeConflict,
842        conflict_count: usize,
843    ) -> bool {
844        let ctxt: &mut Self = &mut *(ctxt as *mut Self);
845        let keys = core::slice::from_raw_parts(keys, conflict_count);
846        let conflicts = core::slice::from_raw_parts(conflicts, conflict_count);
847        keys.iter().zip(conflicts.iter()).all(|(key, conflict)| {
848            let key = raw_to_string(*key).unwrap();
849            // TODO I guess dont drop here?
850            let raw_ptr = NonNull::new(*conflict).unwrap();
851            let conflict = MergeConflict::from_raw(raw_ptr);
852            ctxt.handle_conflict(&key, &conflict)
853        })
854    }
855}
856
857impl<F> DatabaseConflictHandler for F
858where
859    F: for<'a> FnMut(&'a str, &'a MergeConflict) -> bool,
860{
861    fn handle_conflict(&mut self, keys: &str, conflicts: &MergeConflict) -> bool {
862        self(keys, conflicts)
863    }
864}
865
866pub struct DatabaseConflictHandlerFail;
867impl DatabaseConflictHandler for DatabaseConflictHandlerFail {
868    fn handle_conflict(&mut self, _keys: &str, _conflicts: &MergeConflict) -> bool {
869        unreachable!()
870    }
871
872    unsafe extern "C" fn cb_handle_conflict(
873        _ctxt: *mut c_void,
874        _keys: *mut *const c_char,
875        _conflicts: *mut *mut BNAnalysisMergeConflict,
876        conflict_count: usize,
877    ) -> bool {
878        // Fail if we have any conflicts.
879        conflict_count > 0
880    }
881}
882
883pub trait TypeArchiveConflictHandler: Sized {
884    fn handle_conflict(&mut self, conflicts: &TypeArchiveMergeConflict) -> bool;
885    unsafe extern "C" fn cb_handle_conflict(
886        ctxt: *mut ::std::os::raw::c_void,
887        conflicts: *mut *mut BNTypeArchiveMergeConflict,
888        conflict_count: usize,
889    ) -> bool {
890        let ctx: &mut Self = &mut *(ctxt as *mut Self);
891        // TODO: Verify that we dont own the merge conflict, or this list passed to us.
892        let conflicts_raw = core::slice::from_raw_parts(conflicts, conflict_count);
893        conflicts_raw
894            .iter()
895            .map(|t| NonNull::new_unchecked(*t))
896            .map(|t| TypeArchiveMergeConflict::from_raw(t))
897            .all(|conflict| ctx.handle_conflict(&conflict))
898    }
899}
900
901impl<F> TypeArchiveConflictHandler for F
902where
903    F: for<'a> FnMut(&'a TypeArchiveMergeConflict) -> bool,
904{
905    fn handle_conflict(&mut self, conflicts: &TypeArchiveMergeConflict) -> bool {
906        self(conflicts)
907    }
908}
909
910pub struct TypeArchiveConflictHandlerFail;
911impl TypeArchiveConflictHandler for TypeArchiveConflictHandlerFail {
912    fn handle_conflict(&mut self, _conflicts: &TypeArchiveMergeConflict) -> bool {
913        unreachable!()
914    }
915
916    unsafe extern "C" fn cb_handle_conflict(
917        _ctxt: *mut c_void,
918        _conflicts: *mut *mut BNTypeArchiveMergeConflict,
919        _conflict_count: usize,
920    ) -> bool {
921        // TODO only fail if _conflict_count is greater then 0?
922        //_conflict_count > 0
923        false
924    }
925}