binaryninja/collaboration/
project.rs

1use std::ffi::c_void;
2use std::fmt::Debug;
3use std::path::PathBuf;
4use std::ptr::NonNull;
5use std::time::SystemTime;
6
7use binaryninjacore_sys::*;
8
9use super::{
10    sync, CollaborationPermissionLevel, NameChangeset, Permission, Remote, RemoteFile,
11    RemoteFileType, RemoteFolder,
12};
13
14use crate::binary_view::{BinaryView, BinaryViewExt};
15use crate::database::Database;
16use crate::file_metadata::FileMetadata;
17use crate::progress::{NoProgressCallback, ProgressCallback};
18use crate::project::Project;
19use crate::rc::{Array, CoreArrayProvider, CoreArrayProviderInner, Guard, Ref, RefCountable};
20use crate::string::{BnString, IntoCStr};
21
22#[repr(transparent)]
23pub struct RemoteProject {
24    pub(crate) handle: NonNull<BNRemoteProject>,
25}
26
27impl RemoteProject {
28    pub(crate) unsafe fn from_raw(handle: NonNull<BNRemoteProject>) -> Self {
29        Self { handle }
30    }
31
32    pub(crate) unsafe fn ref_from_raw(handle: NonNull<BNRemoteProject>) -> Ref<Self> {
33        Ref::new(Self { handle })
34    }
35
36    /// Determine if the project is open (it needs to be opened before you can access its files)
37    pub fn is_open(&self) -> bool {
38        unsafe { BNRemoteProjectIsOpen(self.handle.as_ptr()) }
39    }
40
41    /// Open the project, allowing various file and folder based apis to work, as well as
42    /// connecting a core Project
43    pub fn open(&self) -> Result<(), ()> {
44        self.open_with_progress(NoProgressCallback)
45    }
46
47    /// Open the project, allowing various file and folder based apis to work, as well as
48    /// connecting a core Project
49    pub fn open_with_progress<F: ProgressCallback>(&self, mut progress: F) -> Result<(), ()> {
50        if self.is_open() {
51            return Ok(());
52        }
53        let success = unsafe {
54            BNRemoteProjectOpen(
55                self.handle.as_ptr(),
56                Some(F::cb_progress_callback),
57                &mut progress as *mut F as *mut c_void,
58            )
59        };
60        success.then_some(()).ok_or(())
61    }
62
63    /// Close the project and stop all background operations (e.g. file uploads)
64    pub fn close(&self) {
65        unsafe { BNRemoteProjectClose(self.handle.as_ptr()) }
66    }
67
68    /// Get the Remote Project for a Database
69    pub fn get_for_local_database(database: &Database) -> Result<Option<Ref<Self>>, ()> {
70        // TODO: This sync should be removed?
71        if sync::pull_projects(database)? {
72            return Ok(None);
73        }
74        sync::get_remote_project_for_local_database(database)
75    }
76
77    /// Get the Remote Project for a BinaryView
78    pub fn get_for_binaryview(bv: &BinaryView) -> Result<Option<Ref<Self>>, ()> {
79        let file = bv.file();
80        let Some(database) = file.database() else {
81            return Ok(None);
82        };
83        Self::get_for_local_database(&database)
84    }
85
86    /// Get the core [`Project`] for the remote project.
87    ///
88    /// NOTE: If the project has not been opened, it will be opened upon calling this.
89    pub fn core_project(&self) -> Result<Ref<Project>, ()> {
90        // TODO: This sync should be removed?
91        self.open()?;
92
93        let value = unsafe { BNRemoteProjectGetCoreProject(self.handle.as_ptr()) };
94        NonNull::new(value)
95            .map(|handle| unsafe { Project::ref_from_raw(handle) })
96            .ok_or(())
97    }
98
99    /// Get the owning remote
100    pub fn remote(&self) -> Result<Ref<Remote>, ()> {
101        let value = unsafe { BNRemoteProjectGetRemote(self.handle.as_ptr()) };
102        NonNull::new(value)
103            .map(|handle| unsafe { Remote::ref_from_raw(handle) })
104            .ok_or(())
105    }
106
107    /// Get the URL of the project
108    pub fn url(&self) -> String {
109        let result = unsafe { BNRemoteProjectGetUrl(self.handle.as_ptr()) };
110        assert!(!result.is_null());
111        unsafe { BnString::into_string(result) }
112    }
113
114    /// Get the unique ID of the project
115    pub fn id(&self) -> String {
116        let result = unsafe { BNRemoteProjectGetId(self.handle.as_ptr()) };
117        assert!(!result.is_null());
118        unsafe { BnString::into_string(result) }
119    }
120
121    /// Created date of the project
122    pub fn created(&self) -> SystemTime {
123        let result = unsafe { BNRemoteProjectGetCreated(self.handle.as_ptr()) };
124        crate::ffi::time_from_bn(result.try_into().unwrap())
125    }
126
127    /// Last modification of the project
128    pub fn last_modified(&self) -> SystemTime {
129        let result = unsafe { BNRemoteProjectGetLastModified(self.handle.as_ptr()) };
130        crate::ffi::time_from_bn(result.try_into().unwrap())
131    }
132
133    /// Displayed name of file
134    pub fn name(&self) -> String {
135        let result = unsafe { BNRemoteProjectGetName(self.handle.as_ptr()) };
136        assert!(!result.is_null());
137        unsafe { BnString::into_string(result) }
138    }
139
140    /// Set the description of the file. You will need to push the file to update the remote version.
141    pub fn set_name(&self, name: &str) -> Result<(), ()> {
142        let name = name.to_cstr();
143        let success = unsafe { BNRemoteProjectSetName(self.handle.as_ptr(), name.as_ptr()) };
144        success.then_some(()).ok_or(())
145    }
146
147    /// Desciprtion of the file
148    pub fn description(&self) -> String {
149        let result = unsafe { BNRemoteProjectGetDescription(self.handle.as_ptr()) };
150        assert!(!result.is_null());
151        unsafe { BnString::into_string(result) }
152    }
153
154    /// Set the description of the file. You will need to push the file to update the remote version.
155    pub fn set_description(&self, description: &str) -> Result<(), ()> {
156        let description = description.to_cstr();
157        let success =
158            unsafe { BNRemoteProjectSetDescription(self.handle.as_ptr(), description.as_ptr()) };
159        success.then_some(()).ok_or(())
160    }
161
162    /// Get the number of files in a project (without needing to pull them first)
163    pub fn received_file_count(&self) -> u64 {
164        unsafe { BNRemoteProjectGetReceivedFileCount(self.handle.as_ptr()) }
165    }
166
167    /// Get the number of folders in a project (without needing to pull them first)
168    pub fn received_folder_count(&self) -> u64 {
169        unsafe { BNRemoteProjectGetReceivedFolderCount(self.handle.as_ptr()) }
170    }
171
172    /// Get the default directory path for a remote Project. This is based off the Setting for
173    /// collaboration.directory, the project's id, and the project's remote's id.
174    pub fn default_path(&self) -> Result<PathBuf, ()> {
175        sync::default_project_path(self)
176    }
177
178    /// If the project has pulled the folders yet
179    pub fn has_pulled_files(&self) -> bool {
180        unsafe { BNRemoteProjectHasPulledFiles(self.handle.as_ptr()) }
181    }
182
183    /// If the project has pulled the folders yet
184    pub fn has_pulled_folders(&self) -> bool {
185        unsafe { BNRemoteProjectHasPulledFolders(self.handle.as_ptr()) }
186    }
187
188    /// If the project has pulled the group permissions yet
189    pub fn has_pulled_group_permissions(&self) -> bool {
190        unsafe { BNRemoteProjectHasPulledGroupPermissions(self.handle.as_ptr()) }
191    }
192
193    /// If the project has pulled the user permissions yet
194    pub fn has_pulled_user_permissions(&self) -> bool {
195        unsafe { BNRemoteProjectHasPulledUserPermissions(self.handle.as_ptr()) }
196    }
197
198    /// If the currently logged in user is an administrator of the project (and can edit
199    /// permissions and such for the project).
200    pub fn is_admin(&self) -> bool {
201        unsafe { BNRemoteProjectIsAdmin(self.handle.as_ptr()) }
202    }
203
204    /// Get the list of files in this project.
205    ///
206    /// NOTE: If the project has not been opened, it will be opened upon calling this.
207    /// NOTE: If folders have not been pulled, they will be pulled upon calling this.
208    /// NOTE: If files have not been pulled, they will be pulled upon calling this.
209    pub fn files(&self) -> Result<Array<RemoteFile>, ()> {
210        // TODO: This sync should be removed?
211        if !self.has_pulled_files() {
212            self.pull_files()?;
213        }
214
215        let mut count = 0;
216        let result = unsafe { BNRemoteProjectGetFiles(self.handle.as_ptr(), &mut count) };
217        (!result.is_null())
218            .then(|| unsafe { Array::new(result, count, ()) })
219            .ok_or(())
220    }
221
222    /// Get a specific File in the Project by its id
223    ///
224    /// NOTE: If the project has not been opened, it will be opened upon calling this.
225    /// NOTE: If files have not been pulled, they will be pulled upon calling this.
226    pub fn get_file_by_id(&self, id: &str) -> Result<Option<Ref<RemoteFile>>, ()> {
227        // TODO: This sync should be removed?
228        if !self.has_pulled_files() {
229            self.pull_files()?;
230        }
231        let id = id.to_cstr();
232        let result = unsafe { BNRemoteProjectGetFileById(self.handle.as_ptr(), id.as_ptr()) };
233        Ok(NonNull::new(result).map(|handle| unsafe { RemoteFile::ref_from_raw(handle) }))
234    }
235
236    /// Get a specific File in the Project by its name
237    ///
238    /// NOTE: If the project has not been opened, it will be opened upon calling this.
239    /// NOTE: If files have not been pulled, they will be pulled upon calling this.
240    pub fn get_file_by_name(&self, name: &str) -> Result<Option<Ref<RemoteFile>>, ()> {
241        // TODO: This sync should be removed?
242        if !self.has_pulled_files() {
243            self.pull_files()?;
244        }
245        let id = name.to_cstr();
246        let result = unsafe { BNRemoteProjectGetFileByName(self.handle.as_ptr(), id.as_ptr()) };
247        Ok(NonNull::new(result).map(|handle| unsafe { RemoteFile::ref_from_raw(handle) }))
248    }
249
250    /// Pull the list of files from the Remote.
251    ///
252    /// NOTE: If the project has not been opened, it will be opened upon calling this.
253    /// NOTE: If folders have not been pulled, they will be pulled upon calling this.
254    pub fn pull_files(&self) -> Result<(), ()> {
255        self.pull_files_with_progress(NoProgressCallback)
256    }
257
258    /// Pull the list of files from the Remote.
259    ///
260    /// NOTE: If the project has not been opened, it will be opened upon calling this.
261    /// NOTE: If folders have not been pulled, they will be pulled upon calling this.
262    pub fn pull_files_with_progress<P: ProgressCallback>(&self, mut progress: P) -> Result<(), ()> {
263        // TODO: This sync should be removed?
264        if !self.has_pulled_folders() {
265            self.pull_folders()?;
266        }
267        let success = unsafe {
268            BNRemoteProjectPullFiles(
269                self.handle.as_ptr(),
270                Some(P::cb_progress_callback),
271                &mut progress as *mut P as *mut c_void,
272            )
273        };
274        success.then_some(()).ok_or(())
275    }
276
277    /// Create a new file on the remote and return a reference to the created file
278    ///
279    /// NOTE: If the project has not been opened, it will be opened upon calling this.
280    ///
281    /// * `filename` - File name
282    /// * `contents` - File contents
283    /// * `name` - Displayed file name
284    /// * `description` - File description
285    /// * `parent_folder` - Folder that will contain the file
286    /// * `file_type` - Type of File to create
287    pub fn create_file(
288        &self,
289        filename: &str,
290        contents: &[u8],
291        name: &str,
292        description: &str,
293        parent_folder: Option<&RemoteFolder>,
294        file_type: RemoteFileType,
295    ) -> Result<Ref<RemoteFile>, ()> {
296        self.create_file_with_progress(
297            filename,
298            contents,
299            name,
300            description,
301            parent_folder,
302            file_type,
303            NoProgressCallback,
304        )
305    }
306
307    /// Create a new file on the remote and return a reference to the created file
308    ///
309    /// NOTE: If the project has not been opened, it will be opened upon calling this.
310    ///
311    /// * `filename` - File name
312    /// * `contents` - File contents
313    /// * `name` - Displayed file name
314    /// * `description` - File description
315    /// * `parent_folder` - Folder that will contain the file
316    /// * `file_type` - Type of File to create
317    /// * `progress` - Function to call on upload progress updates
318    pub fn create_file_with_progress<P>(
319        &self,
320        filename: &str,
321        contents: &[u8],
322        name: &str,
323        description: &str,
324        parent_folder: Option<&RemoteFolder>,
325        file_type: RemoteFileType,
326        mut progress: P,
327    ) -> Result<Ref<RemoteFile>, ()>
328    where
329        P: ProgressCallback,
330    {
331        // TODO: This sync should be removed?
332        self.open()?;
333
334        let filename = filename.to_cstr();
335        let name = name.to_cstr();
336        let description = description.to_cstr();
337        let folder_handle = parent_folder.map_or(std::ptr::null_mut(), |f| f.handle.as_ptr());
338        let file_ptr = unsafe {
339            BNRemoteProjectCreateFile(
340                self.handle.as_ptr(),
341                filename.as_ptr(),
342                contents.as_ptr() as *mut _,
343                contents.len(),
344                name.as_ptr(),
345                description.as_ptr(),
346                folder_handle,
347                file_type,
348                Some(P::cb_progress_callback),
349                &mut progress as *mut P as *mut c_void,
350            )
351        };
352
353        NonNull::new(file_ptr)
354            .map(|handle| unsafe { RemoteFile::ref_from_raw(handle) })
355            .ok_or(())
356    }
357
358    /// Push an updated File object to the Remote
359    ///
360    /// NOTE: If the project has not been opened, it will be opened upon calling this.
361    pub fn push_file<I>(&self, file: &RemoteFile, extra_fields: I) -> Result<(), ()>
362    where
363        I: IntoIterator<Item = (String, String)>,
364    {
365        // TODO: This sync should be removed?
366        self.open()?;
367
368        let (keys, values): (Vec<_>, Vec<_>) = extra_fields
369            .into_iter()
370            .map(|(k, v)| (k.to_cstr(), v.to_cstr()))
371            .unzip();
372        let mut keys_raw = keys.iter().map(|s| s.as_ptr()).collect::<Vec<_>>();
373        let mut values_raw = values.iter().map(|s| s.as_ptr()).collect::<Vec<_>>();
374        let success = unsafe {
375            BNRemoteProjectPushFile(
376                self.handle.as_ptr(),
377                file.handle.as_ptr(),
378                keys_raw.as_mut_ptr(),
379                values_raw.as_mut_ptr(),
380                keys_raw.len(),
381            )
382        };
383        success.then_some(()).ok_or(())
384    }
385
386    pub fn delete_file(&self, file: &RemoteFile) -> Result<(), ()> {
387        // TODO: This sync should be removed?
388        self.open()?;
389
390        let success =
391            unsafe { BNRemoteProjectDeleteFile(self.handle.as_ptr(), file.handle.as_ptr()) };
392        success.then_some(()).ok_or(())
393    }
394
395    /// Get the list of folders in this project.
396    ///
397    /// NOTE: If the project has not been opened, it will be opened upon calling this.
398    /// NOTE: If folders have not been pulled, they will be pulled upon calling this.
399    pub fn folders(&self) -> Result<Array<RemoteFolder>, ()> {
400        // TODO: This sync should be removed?
401        if !self.has_pulled_folders() {
402            self.pull_folders()?;
403        }
404        let mut count = 0;
405        let result = unsafe { BNRemoteProjectGetFolders(self.handle.as_ptr(), &mut count) };
406        if result.is_null() {
407            return Err(());
408        }
409        Ok(unsafe { Array::new(result, count, ()) })
410    }
411
412    /// Get a specific Folder in the Project by its id
413    ///
414    /// NOTE: If the project has not been opened, it will be opened upon calling this.
415    /// NOTE: If folders have not been pulled, they will be pulled upon calling this.
416    pub fn get_folder_by_id(&self, id: &str) -> Result<Option<Ref<RemoteFolder>>, ()> {
417        // TODO: This sync should be removed?
418        if !self.has_pulled_folders() {
419            self.pull_folders()?;
420        }
421        let id = id.to_cstr();
422        let result = unsafe { BNRemoteProjectGetFolderById(self.handle.as_ptr(), id.as_ptr()) };
423        Ok(NonNull::new(result).map(|handle| unsafe { RemoteFolder::ref_from_raw(handle) }))
424    }
425
426    /// Pull the list of folders from the Remote.
427    ///
428    /// NOTE: If the project has not been opened, it will be opened upon calling this.
429    pub fn pull_folders(&self) -> Result<(), ()> {
430        self.pull_folders_with_progress(NoProgressCallback)
431    }
432
433    /// Pull the list of folders from the Remote.
434    ///
435    /// NOTE: If the project has not been opened, it will be opened upon calling this.
436    pub fn pull_folders_with_progress<P: ProgressCallback>(
437        &self,
438        mut progress: P,
439    ) -> Result<(), ()> {
440        // TODO: This sync should be removed?
441        self.open()?;
442
443        let success = unsafe {
444            BNRemoteProjectPullFolders(
445                self.handle.as_ptr(),
446                Some(P::cb_progress_callback),
447                &mut progress as *mut P as *mut c_void,
448            )
449        };
450        success.then_some(()).ok_or(())
451    }
452
453    /// Create a new folder on the remote (and pull it)
454    ///
455    /// NOTE: If the project has not been opened, it will be opened upon calling this.
456    ///
457    /// * `name` - Displayed folder name
458    /// * `description` - Folder description
459    /// * `parent` - Parent folder (optional)
460    pub fn create_folder(
461        &self,
462        name: &str,
463        description: &str,
464        parent_folder: Option<&RemoteFolder>,
465    ) -> Result<Ref<RemoteFolder>, ()> {
466        self.create_folder_with_progress(name, description, parent_folder, NoProgressCallback)
467    }
468
469    /// Create a new folder on the remote (and pull it)
470    ///
471    /// NOTE: If the project has not been opened, it will be opened upon calling this.
472    ///
473    /// * `name` - Displayed folder name
474    /// * `description` - Folder description
475    /// * `parent` - Parent folder (optional)
476    /// * `progress` - Function to call on upload progress updates
477    pub fn create_folder_with_progress<P>(
478        &self,
479        name: &str,
480        description: &str,
481        parent_folder: Option<&RemoteFolder>,
482        mut progress: P,
483    ) -> Result<Ref<RemoteFolder>, ()>
484    where
485        P: ProgressCallback,
486    {
487        // TODO: This sync should be removed?
488        self.open()?;
489
490        let name = name.to_cstr();
491        let description = description.to_cstr();
492        let folder_handle = parent_folder.map_or(std::ptr::null_mut(), |f| f.handle.as_ptr());
493        let file_ptr = unsafe {
494            BNRemoteProjectCreateFolder(
495                self.handle.as_ptr(),
496                name.as_ptr(),
497                description.as_ptr(),
498                folder_handle,
499                Some(P::cb_progress_callback),
500                &mut progress as *mut P as *mut c_void,
501            )
502        };
503
504        NonNull::new(file_ptr)
505            .map(|handle| unsafe { RemoteFolder::ref_from_raw(handle) })
506            .ok_or(())
507    }
508
509    /// Push an updated Folder object to the Remote
510    ///
511    /// NOTE: If the project has not been opened, it will be opened upon calling this.
512    ///
513    /// * `folder` - Folder object which has been updated
514    /// * `extra_fields` - Extra HTTP fields to send with the update
515    pub fn push_folder<I>(&self, folder: &RemoteFolder, extra_fields: I) -> Result<(), ()>
516    where
517        I: IntoIterator<Item = (String, String)>,
518    {
519        // TODO: This sync should be removed?
520        self.open()?;
521
522        let (keys, values): (Vec<_>, Vec<_>) = extra_fields
523            .into_iter()
524            .map(|(k, v)| (k.to_cstr(), v.to_cstr()))
525            .unzip();
526        let mut keys_raw = keys.iter().map(|s| s.as_ptr()).collect::<Vec<_>>();
527        let mut values_raw = values.iter().map(|s| s.as_ptr()).collect::<Vec<_>>();
528        let success = unsafe {
529            BNRemoteProjectPushFolder(
530                self.handle.as_ptr(),
531                folder.handle.as_ptr(),
532                keys_raw.as_mut_ptr(),
533                values_raw.as_mut_ptr(),
534                keys_raw.len(),
535            )
536        };
537        success.then_some(()).ok_or(())
538    }
539
540    /// Delete a folder from the remote
541    ///
542    /// NOTE: If the project has not been opened, it will be opened upon calling this.
543    pub fn delete_folder(&self, folder: &RemoteFolder) -> Result<(), ()> {
544        // TODO: This sync should be removed?
545        self.open()?;
546
547        let success =
548            unsafe { BNRemoteProjectDeleteFolder(self.handle.as_ptr(), folder.handle.as_ptr()) };
549        success.then_some(()).ok_or(())
550    }
551
552    /// Get the list of group permissions in this project.
553    ///
554    /// NOTE: If group permissions have not been pulled, they will be pulled upon calling this.
555    pub fn group_permissions(&self) -> Result<Array<Permission>, ()> {
556        // TODO: This sync should be removed?
557        if !self.has_pulled_group_permissions() {
558            self.pull_group_permissions()?;
559        }
560
561        let mut count: usize = 0;
562        let value = unsafe { BNRemoteProjectGetGroupPermissions(self.handle.as_ptr(), &mut count) };
563        assert!(!value.is_null());
564        Ok(unsafe { Array::new(value, count, ()) })
565    }
566
567    /// Get the list of user permissions in this project.
568    ///
569    /// NOTE: If user permissions have not been pulled, they will be pulled upon calling this.
570    pub fn user_permissions(&self) -> Result<Array<Permission>, ()> {
571        // TODO: This sync should be removed?
572        if !self.has_pulled_user_permissions() {
573            self.pull_user_permissions()?;
574        }
575
576        let mut count: usize = 0;
577        let value = unsafe { BNRemoteProjectGetUserPermissions(self.handle.as_ptr(), &mut count) };
578        assert!(!value.is_null());
579        Ok(unsafe { Array::new(value, count, ()) })
580    }
581
582    /// Get a specific permission in the Project by its id.
583    ///
584    /// NOTE: If group or user permissions have not been pulled, they will be pulled upon calling this.
585    pub fn get_permission_by_id(&self, id: &str) -> Result<Option<Ref<Permission>>, ()> {
586        // TODO: This sync should be removed?
587        if !self.has_pulled_user_permissions() {
588            self.pull_user_permissions()?;
589        }
590        // TODO: This sync should be removed?
591        if !self.has_pulled_group_permissions() {
592            self.pull_group_permissions()?;
593        }
594
595        let id = id.to_cstr();
596        let value = unsafe {
597            BNRemoteProjectGetPermissionById(self.handle.as_ptr(), id.as_ref().as_ptr() as *const _)
598        };
599        Ok(NonNull::new(value).map(|v| unsafe { Permission::ref_from_raw(v) }))
600    }
601
602    /// Pull the list of group permissions from the Remote.
603    pub fn pull_group_permissions(&self) -> Result<(), ()> {
604        self.pull_group_permissions_with_progress(NoProgressCallback)
605    }
606
607    /// Pull the list of group permissions from the Remote.
608    pub fn pull_group_permissions_with_progress<F: ProgressCallback>(
609        &self,
610        mut progress: F,
611    ) -> Result<(), ()> {
612        let success = unsafe {
613            BNRemoteProjectPullGroupPermissions(
614                self.handle.as_ptr(),
615                Some(F::cb_progress_callback),
616                &mut progress as *mut F as *mut c_void,
617            )
618        };
619        success.then_some(()).ok_or(())
620    }
621
622    /// Pull the list of user permissions from the Remote.
623    pub fn pull_user_permissions(&self) -> Result<(), ()> {
624        self.pull_user_permissions_with_progress(NoProgressCallback)
625    }
626
627    /// Pull the list of user permissions from the Remote.
628    pub fn pull_user_permissions_with_progress<F: ProgressCallback>(
629        &self,
630        mut progress: F,
631    ) -> Result<(), ()> {
632        let success = unsafe {
633            BNRemoteProjectPullUserPermissions(
634                self.handle.as_ptr(),
635                Some(F::cb_progress_callback),
636                &mut progress as *mut F as *mut c_void,
637            )
638        };
639        success.then_some(()).ok_or(())
640    }
641
642    /// Create a new group permission on the remote (and pull it).
643    ///
644    /// # Arguments
645    ///
646    /// * `group_id` - Group id
647    /// * `level` - Permission level
648    pub fn create_group_permission(
649        &self,
650        group_id: i64,
651        level: CollaborationPermissionLevel,
652    ) -> Result<Ref<Permission>, ()> {
653        self.create_group_permission_with_progress(group_id, level, NoProgressCallback)
654    }
655
656    /// Create a new group permission on the remote (and pull it).
657    ///
658    /// # Arguments
659    ///
660    /// * `group_id` - Group id
661    /// * `level` - Permission level
662    /// * `progress` - Function to call for upload progress updates
663    pub fn create_group_permission_with_progress<F: ProgressCallback>(
664        &self,
665        group_id: i64,
666        level: CollaborationPermissionLevel,
667        mut progress: F,
668    ) -> Result<Ref<Permission>, ()> {
669        let value = unsafe {
670            BNRemoteProjectCreateGroupPermission(
671                self.handle.as_ptr(),
672                group_id,
673                level,
674                Some(F::cb_progress_callback),
675                &mut progress as *mut F as *mut c_void,
676            )
677        };
678
679        NonNull::new(value)
680            .map(|v| unsafe { Permission::ref_from_raw(v) })
681            .ok_or(())
682    }
683
684    /// Create a new user permission on the remote (and pull it).
685    ///
686    /// # Arguments
687    ///
688    /// * `user_id` - User id
689    /// * `level` - Permission level
690    pub fn create_user_permission(
691        &self,
692        user_id: &str,
693        level: CollaborationPermissionLevel,
694    ) -> Result<Ref<Permission>, ()> {
695        self.create_user_permission_with_progress(user_id, level, NoProgressCallback)
696    }
697
698    /// Create a new user permission on the remote (and pull it).
699    ///
700    /// # Arguments
701    ///
702    /// * `user_id` - User id
703    /// * `level` - Permission level
704    /// * `progress` - The progress callback to call
705    pub fn create_user_permission_with_progress<F: ProgressCallback>(
706        &self,
707        user_id: &str,
708        level: CollaborationPermissionLevel,
709        mut progress: F,
710    ) -> Result<Ref<Permission>, ()> {
711        let user_id = user_id.to_cstr();
712        let value = unsafe {
713            BNRemoteProjectCreateUserPermission(
714                self.handle.as_ptr(),
715                user_id.as_ptr(),
716                level,
717                Some(F::cb_progress_callback),
718                &mut progress as *mut F as *mut c_void,
719            )
720        };
721
722        NonNull::new(value)
723            .map(|v| unsafe { Permission::ref_from_raw(v) })
724            .ok_or(())
725    }
726
727    /// Push project permissions to the remote.
728    ///
729    /// # Arguments
730    ///
731    /// * `permission` - Permission object which has been updated
732    /// * `extra_fields` - Extra HTTP fields to send with the update
733    pub fn push_permission<I>(&self, permission: &Permission, extra_fields: I) -> Result<(), ()>
734    where
735        I: IntoIterator<Item = (String, String)>,
736    {
737        let (keys, values): (Vec<_>, Vec<_>) = extra_fields
738            .into_iter()
739            .map(|(k, v)| (k.to_cstr(), v.to_cstr()))
740            .unzip();
741        let mut keys_raw = keys.iter().map(|s| s.as_ptr()).collect::<Vec<_>>();
742        let mut values_raw = values.iter().map(|s| s.as_ptr()).collect::<Vec<_>>();
743
744        let success = unsafe {
745            BNRemoteProjectPushPermission(
746                self.handle.as_ptr(),
747                permission.handle.as_ptr(),
748                keys_raw.as_mut_ptr(),
749                values_raw.as_mut_ptr(),
750                keys_raw.len(),
751            )
752        };
753        success.then_some(()).ok_or(())
754    }
755
756    /// Delete a permission from the remote.
757    pub fn delete_permission(&self, permission: &Permission) -> Result<(), ()> {
758        let success = unsafe {
759            BNRemoteProjectDeletePermission(self.handle.as_ptr(), permission.handle.as_ptr())
760        };
761        success.then_some(()).ok_or(())
762    }
763
764    /// Determine if a user is in any of the view/edit/admin groups.
765    ///
766    /// # Arguments
767    ///
768    /// * `username` - Username of user to check
769    pub fn can_user_view(&self, username: &str) -> bool {
770        let username = username.to_cstr();
771        unsafe { BNRemoteProjectCanUserView(self.handle.as_ptr(), username.as_ptr()) }
772    }
773
774    /// Determine if a user is in any of the edit/admin groups.
775    ///
776    /// # Arguments
777    ///
778    /// * `username` - Username of user to check
779    pub fn can_user_edit(&self, username: &str) -> bool {
780        let username = username.to_cstr();
781        unsafe { BNRemoteProjectCanUserEdit(self.handle.as_ptr(), username.as_ptr()) }
782    }
783
784    /// Determine if a user is in the admin group.
785    ///
786    /// # Arguments
787    ///
788    /// * `username` - Username of user to check
789    pub fn can_user_admin(&self, username: &str) -> bool {
790        let username = username.to_cstr();
791        unsafe { BNRemoteProjectCanUserAdmin(self.handle.as_ptr(), username.as_ptr()) }
792    }
793
794    /// Get the default directory path for a remote Project. This is based off
795    /// the Setting for collaboration.directory, the project's id, and the
796    /// project's remote's id.
797    pub fn default_project_path(&self) -> String {
798        let result = unsafe { BNCollaborationDefaultProjectPath(self.handle.as_ptr()) };
799        unsafe { BnString::into_string(result) }
800    }
801
802    /// Upload a file, with database, to the remote under the given project
803    ///
804    /// * `metadata` - Local file with database
805    /// * `parent_folder` - Optional parent folder in which to place this file
806    /// * `name_changeset` - Function to call for naming a pushed changeset, if necessary
807    pub fn upload_database<C>(
808        &self,
809        metadata: &FileMetadata,
810        parent_folder: Option<&RemoteFolder>,
811        name_changeset: C,
812    ) -> Result<Ref<RemoteFile>, ()>
813    where
814        C: NameChangeset,
815    {
816        // TODO: Do we want this?
817        // TODO: If you have not yet pulled files you will have never filled the map you will be placing your
818        // TODO: New file in.
819        if !self.has_pulled_files() {
820            self.pull_files()?;
821        }
822        sync::upload_database(self, parent_folder, metadata, name_changeset)
823    }
824
825    /// Upload a file, with database, to the remote under the given project
826    ///
827    /// * `metadata` - Local file with database
828    /// * `parent_folder` - Optional parent folder in which to place this file
829    /// * `progress` -: Function to call for progress updates
830    /// * `name_changeset` - Function to call for naming a pushed changeset, if necessary
831    pub fn upload_database_with_progress<C>(
832        &self,
833        metadata: &FileMetadata,
834        parent_folder: Option<&RemoteFolder>,
835        name_changeset: C,
836        progress_function: impl ProgressCallback,
837    ) -> Result<Ref<RemoteFile>, ()>
838    where
839        C: NameChangeset,
840    {
841        sync::upload_database_with_progress(
842            self,
843            parent_folder,
844            metadata,
845            name_changeset,
846            progress_function,
847        )
848    }
849
850    // TODO: check remotebrowser.cpp for implementation
851    ///// Upload a file to the project, creating a new File and pulling it
852    /////
853    ///// NOTE: If the project has not been opened, it will be opened upon calling this.
854    /////
855    ///// * `target` - Path to file on disk or BinaryView/FileMetadata object of
856    /////                already-opened file
857    ///// * `parent_folder` - Parent folder to place the uploaded file in
858    ///// * `progress` - Function to call for progress updates
859    //pub fn upload_new_file<S: BnStrCompatible, P: ProgressCallback>(
860    //    &self,
861    //    target: S,
862    //    parent_folder: Option<&RemoteFolder>,
863    //    progress: P,
864    //    open_view_options: u32,
865    //) -> Result<(), ()> {
866    //    if !self.open(NoProgressCallback)? {
867    //        return Err(());
868    //    }
869    //    let target = target.into_bytes_with_nul();
870    //    todo!();
871    //}
872}
873
874impl Debug for RemoteProject {
875    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
876        f.debug_struct("RemoteProject")
877            .field("id", &self.id())
878            .field("name", &self.name())
879            .field("description", &self.description())
880            .finish()
881    }
882}
883
884impl PartialEq for RemoteProject {
885    fn eq(&self, other: &Self) -> bool {
886        self.id() == other.id()
887    }
888}
889
890impl Eq for RemoteProject {}
891
892impl ToOwned for RemoteProject {
893    type Owned = Ref<Self>;
894
895    fn to_owned(&self) -> Self::Owned {
896        unsafe { RefCountable::inc_ref(self) }
897    }
898}
899
900unsafe impl Send for RemoteProject {}
901unsafe impl Sync for RemoteProject {}
902
903unsafe impl RefCountable for RemoteProject {
904    unsafe fn inc_ref(handle: &Self) -> Ref<Self> {
905        Ref::new(Self {
906            handle: NonNull::new(BNNewRemoteProjectReference(handle.handle.as_ptr())).unwrap(),
907        })
908    }
909
910    unsafe fn dec_ref(handle: &Self) {
911        BNFreeRemoteProject(handle.handle.as_ptr());
912    }
913}
914
915impl CoreArrayProvider for RemoteProject {
916    type Raw = *mut BNRemoteProject;
917    type Context = ();
918    type Wrapped<'a> = Guard<'a, Self>;
919}
920
921unsafe impl CoreArrayProviderInner for RemoteProject {
922    unsafe fn free(raw: *mut Self::Raw, count: usize, _context: &Self::Context) {
923        BNFreeRemoteProjectList(raw, count)
924    }
925
926    unsafe fn wrap_raw<'a>(raw: &'a Self::Raw, context: &'a Self::Context) -> Self::Wrapped<'a> {
927        let raw_ptr = NonNull::new(*raw).unwrap();
928        Guard::new(Self::from_raw(raw_ptr), context)
929    }
930}