binaryninja/collaboration/
project.rs

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