binaryninja/collaboration/
file.rs

1use std::ffi::c_void;
2use std::fmt::{Debug, Formatter};
3use std::path::Path;
4use std::ptr::NonNull;
5use std::time::SystemTime;
6
7use binaryninjacore_sys::*;
8
9use super::{
10    sync, DatabaseConflictHandler, DatabaseConflictHandlerFail, NameChangeset, NoNameChangeset,
11    Remote, RemoteFolder, RemoteProject, RemoteSnapshot,
12};
13
14use crate::binary_view::{BinaryView, BinaryViewExt};
15use crate::database::Database;
16use crate::file_metadata::FileMetadata;
17use crate::progress::{NoProgressCallback, ProgressCallback, SplitProgressBuilder};
18use crate::project::file::ProjectFile;
19use crate::rc::{Array, CoreArrayProvider, CoreArrayProviderInner, Guard, Ref, RefCountable};
20use crate::string::{BnString, IntoCStr};
21
22pub type RemoteFileType = BNRemoteFileType;
23
24/// A remote project file. It controls the various snapshots and raw file contents associated with the analysis.
25#[repr(transparent)]
26pub struct RemoteFile {
27    pub(crate) handle: NonNull<BNRemoteFile>,
28}
29
30impl RemoteFile {
31    pub(crate) unsafe fn from_raw(handle: NonNull<BNRemoteFile>) -> Self {
32        Self { handle }
33    }
34
35    pub(crate) unsafe fn ref_from_raw(handle: NonNull<BNRemoteFile>) -> Ref<Self> {
36        Ref::new(Self { handle })
37    }
38
39    /// Look up the remote File for a local database, or None if there is no matching
40    /// remote File found.
41    /// See [RemoteFile::get_for_binary_view] to load from a [BinaryView].
42    pub fn get_for_local_database(database: &Database) -> Result<Option<Ref<RemoteFile>>, ()> {
43        // TODO: This sync should be removed?
44        if !sync::pull_files(database)? {
45            return Ok(None);
46        }
47        sync::get_remote_file_for_local_database(database)
48    }
49
50    /// Look up the [`RemoteFile`] for a local [`BinaryView`], or None if there is no matching
51    /// remote File found.
52    pub fn get_for_binary_view(bv: &BinaryView) -> Result<Option<Ref<RemoteFile>>, ()> {
53        let file = bv.file();
54        let Some(database) = file.database() else {
55            return Ok(None);
56        };
57        RemoteFile::get_for_local_database(&database)
58    }
59
60    pub fn core_file(&self) -> Result<ProjectFile, ()> {
61        let result = unsafe { BNRemoteFileGetCoreFile(self.handle.as_ptr()) };
62        NonNull::new(result)
63            .map(|handle| unsafe { ProjectFile::from_raw(handle) })
64            .ok_or(())
65    }
66
67    pub fn project(&self) -> Result<Ref<RemoteProject>, ()> {
68        let result = unsafe { BNRemoteFileGetProject(self.handle.as_ptr()) };
69        NonNull::new(result)
70            .map(|handle| unsafe { RemoteProject::ref_from_raw(handle) })
71            .ok_or(())
72    }
73
74    pub fn remote(&self) -> Result<Ref<Remote>, ()> {
75        let result = unsafe { BNRemoteFileGetRemote(self.handle.as_ptr()) };
76        NonNull::new(result)
77            .map(|handle| unsafe { Remote::ref_from_raw(handle) })
78            .ok_or(())
79    }
80
81    /// Parent folder, if one exists. None if this is in the root of the project.
82    pub fn folder(&self) -> Result<Option<Ref<RemoteFolder>>, ()> {
83        let project = self.project()?;
84        if !project.has_pulled_folders() {
85            project.pull_folders()?;
86        }
87        let result = unsafe { BNRemoteFileGetFolder(self.handle.as_ptr()) };
88        Ok(NonNull::new(result).map(|handle| unsafe { RemoteFolder::ref_from_raw(handle) }))
89    }
90
91    /// Set the parent folder of a file.
92    pub fn set_folder(&self, folder: Option<&RemoteFolder>) -> Result<(), ()> {
93        let folder_raw = folder.map_or(std::ptr::null_mut(), |f| f.handle.as_ptr());
94        let success = unsafe { BNRemoteFileSetFolder(self.handle.as_ptr(), folder_raw) };
95        success.then_some(()).ok_or(())
96    }
97
98    pub fn set_metadata(&self, folder: &str) -> Result<(), ()> {
99        let folder_raw = folder.to_cstr();
100        let success = unsafe { BNRemoteFileSetMetadata(self.handle.as_ptr(), folder_raw.as_ptr()) };
101        success.then_some(()).ok_or(())
102    }
103
104    /// Web API endpoint URL
105    pub fn url(&self) -> String {
106        let result = unsafe { BNRemoteFileGetUrl(self.handle.as_ptr()) };
107        assert!(!result.is_null());
108        unsafe { BnString::into_string(result) }
109    }
110
111    /// Chat log API endpoint URL
112    pub fn chat_log_url(&self) -> String {
113        let result = unsafe { BNRemoteFileGetChatLogUrl(self.handle.as_ptr()) };
114        assert!(!result.is_null());
115        unsafe { BnString::into_string(result) }
116    }
117
118    pub fn user_positions_url(&self) -> String {
119        let result = unsafe { BNRemoteFileGetUserPositionsUrl(self.handle.as_ptr()) };
120        assert!(!result.is_null());
121        unsafe { BnString::into_string(result) }
122    }
123
124    /// Unique ID
125    pub fn id(&self) -> String {
126        let result = unsafe { BNRemoteFileGetId(self.handle.as_ptr()) };
127        assert!(!result.is_null());
128        unsafe { BnString::into_string(result) }
129    }
130
131    /// All files share the same properties, but files with different types may make different
132    /// uses of those properties, or not use some of them at all.
133    pub fn file_type(&self) -> RemoteFileType {
134        unsafe { BNRemoteFileGetType(self.handle.as_ptr()) }
135    }
136
137    /// Created date of the file
138    pub fn created(&self) -> SystemTime {
139        let result = unsafe { BNRemoteFileGetCreated(self.handle.as_ptr()) };
140        crate::ffi::time_from_bn(result.try_into().unwrap())
141    }
142
143    pub fn created_by(&self) -> String {
144        let result = unsafe { BNRemoteFileGetCreatedBy(self.handle.as_ptr()) };
145        assert!(!result.is_null());
146        unsafe { BnString::into_string(result) }
147    }
148
149    /// Last modified of the file
150    pub fn last_modified(&self) -> SystemTime {
151        let result = unsafe { BNRemoteFileGetLastModified(self.handle.as_ptr()) };
152        crate::ffi::time_from_bn(result.try_into().unwrap())
153    }
154
155    /// Date of last snapshot in the file
156    pub fn last_snapshot(&self) -> SystemTime {
157        let result = unsafe { BNRemoteFileGetLastSnapshot(self.handle.as_ptr()) };
158        crate::ffi::time_from_bn(result.try_into().unwrap())
159    }
160
161    /// Username of user who pushed the last snapshot in the file
162    pub fn last_snapshot_by(&self) -> String {
163        let result = unsafe { BNRemoteFileGetLastSnapshotBy(self.handle.as_ptr()) };
164        assert!(!result.is_null());
165        unsafe { BnString::into_string(result) }
166    }
167
168    pub fn last_snapshot_name(&self) -> String {
169        let result = unsafe { BNRemoteFileGetLastSnapshotName(self.handle.as_ptr()) };
170        assert!(!result.is_null());
171        unsafe { BnString::into_string(result) }
172    }
173
174    /// Hash of file contents (no algorithm guaranteed)
175    pub fn hash(&self) -> String {
176        let result = unsafe { BNRemoteFileGetHash(self.handle.as_ptr()) };
177        assert!(!result.is_null());
178        unsafe { BnString::into_string(result) }
179    }
180
181    /// Displayed name of file
182    pub fn name(&self) -> String {
183        let result = unsafe { BNRemoteFileGetName(self.handle.as_ptr()) };
184        assert!(!result.is_null());
185        unsafe { BnString::into_string(result) }
186    }
187
188    /// Set the description of the file. You will need to push the file to update the remote version.
189    pub fn set_name(&self, name: &str) -> Result<(), ()> {
190        let name = name.to_cstr();
191        let success = unsafe { BNRemoteFileSetName(self.handle.as_ptr(), name.as_ptr()) };
192        success.then_some(()).ok_or(())
193    }
194
195    /// Desciprtion of the file
196    pub fn description(&self) -> String {
197        let result = unsafe { BNRemoteFileGetDescription(self.handle.as_ptr()) };
198        assert!(!result.is_null());
199        unsafe { BnString::into_string(result) }
200    }
201
202    /// Set the description of the file. You will need to push the file to update the remote version.
203    pub fn set_description(&self, description: &str) -> Result<(), ()> {
204        let description = description.to_cstr();
205        let success =
206            unsafe { BNRemoteFileSetDescription(self.handle.as_ptr(), description.as_ptr()) };
207        success.then_some(()).ok_or(())
208    }
209
210    pub fn metadata(&self) -> String {
211        let result = unsafe { BNRemoteFileGetMetadata(self.handle.as_ptr()) };
212        assert!(!result.is_null());
213        unsafe { BnString::into_string(result) }
214    }
215
216    /// Size of raw content of file, in bytes
217    pub fn size(&self) -> u64 {
218        unsafe { BNRemoteFileGetSize(self.handle.as_ptr()) }
219    }
220
221    /// Get the default filepath for a remote File. This is based off the Setting for
222    /// collaboration.directory, the file's id, the file's project's id, and the file's
223    /// remote's id.
224    pub fn default_path(&self) -> String {
225        let result = unsafe { BNCollaborationDefaultFilePath(self.handle.as_ptr()) };
226        assert!(!result.is_null());
227        unsafe { BnString::into_string(result) }
228    }
229
230    /// If the file has pulled the snapshots yet
231    pub fn has_pulled_snapshots(&self) -> bool {
232        unsafe { BNRemoteFileHasPulledSnapshots(self.handle.as_ptr()) }
233    }
234
235    /// Get the list of snapshots in this file.
236    ///
237    /// NOTE: If snapshots have not been pulled, they will be pulled upon calling this.
238    pub fn snapshots(&self) -> Result<Array<RemoteSnapshot>, ()> {
239        // TODO: This sync should be removed?
240        if !self.has_pulled_snapshots() {
241            self.pull_snapshots()?;
242        }
243        let mut count = 0;
244        let result = unsafe { BNRemoteFileGetSnapshots(self.handle.as_ptr(), &mut count) };
245        (!result.is_null())
246            .then(|| unsafe { Array::new(result, count, ()) })
247            .ok_or(())
248    }
249
250    /// Get a specific Snapshot in the File by its id
251    ///
252    /// NOTE: If snapshots have not been pulled, they will be pulled upon calling this.
253    pub fn snapshot_by_id(&self, id: &str) -> Result<Option<Ref<RemoteSnapshot>>, ()> {
254        // TODO: This sync should be removed?
255        if !self.has_pulled_snapshots() {
256            self.pull_snapshots()?;
257        }
258        let id = id.to_cstr();
259        let result = unsafe { BNRemoteFileGetSnapshotById(self.handle.as_ptr(), id.as_ptr()) };
260        Ok(NonNull::new(result).map(|handle| unsafe { RemoteSnapshot::ref_from_raw(handle) }))
261    }
262
263    /// Pull the list of Snapshots from the Remote.
264    pub fn pull_snapshots(&self) -> Result<(), ()> {
265        self.pull_snapshots_with_progress(NoProgressCallback)
266    }
267
268    /// Pull the list of Snapshots from the Remote.
269    pub fn pull_snapshots_with_progress<P: ProgressCallback>(
270        &self,
271        mut progress: P,
272    ) -> Result<(), ()> {
273        let success = unsafe {
274            BNRemoteFilePullSnapshots(
275                self.handle.as_ptr(),
276                Some(P::cb_progress_callback),
277                &mut progress as *mut P as *mut c_void,
278            )
279        };
280        success.then_some(()).ok_or(())
281    }
282
283    /// Create a new snapshot on the remote (and pull it)
284    ///
285    /// * `name` - Snapshot name
286    /// * `contents` - Snapshot contents
287    /// * `analysis_cache_contents` - Contents of analysis cache of snapshot
288    /// * `file` - New file contents (if contents changed)
289    /// * `parent_ids` - List of ids of parent snapshots (or empty if this is a root snapshot)
290    pub fn create_snapshot<I>(
291        &self,
292        name: &str,
293        contents: &mut [u8],
294        analysis_cache_contexts: &mut [u8],
295        file: &mut [u8],
296        parent_ids: I,
297    ) -> Result<Ref<RemoteSnapshot>, ()>
298    where
299        I: IntoIterator<Item = String>,
300    {
301        self.create_snapshot_with_progress(
302            name,
303            contents,
304            analysis_cache_contexts,
305            file,
306            parent_ids,
307            NoProgressCallback,
308        )
309    }
310
311    /// Create a new snapshot on the remote (and pull it)
312    ///
313    /// * `name` - Snapshot name
314    /// * `contents` - Snapshot contents
315    /// * `analysis_cache_contents` - Contents of analysis cache of snapshot
316    /// * `file` - New file contents (if contents changed)
317    /// * `parent_ids` - List of ids of parent snapshots (or empty if this is a root snapshot)
318    /// * `progress` - Function to call on progress updates
319    pub fn create_snapshot_with_progress<I, P>(
320        &self,
321        name: &str,
322        contents: &mut [u8],
323        analysis_cache_contexts: &mut [u8],
324        file: &mut [u8],
325        parent_ids: I,
326        mut progress: P,
327    ) -> Result<Ref<RemoteSnapshot>, ()>
328    where
329        I: IntoIterator<Item = String>,
330        P: ProgressCallback,
331    {
332        let name = name.to_cstr();
333        let parent_ids: Vec<_> = parent_ids.into_iter().map(|id| id.to_cstr()).collect();
334        let mut parent_ids_raw: Vec<_> = parent_ids.iter().map(|x| x.as_ptr()).collect();
335        let result = unsafe {
336            BNRemoteFileCreateSnapshot(
337                self.handle.as_ptr(),
338                name.as_ptr(),
339                contents.as_mut_ptr(),
340                contents.len(),
341                analysis_cache_contexts.as_mut_ptr(),
342                analysis_cache_contexts.len(),
343                file.as_mut_ptr(),
344                file.len(),
345                parent_ids_raw.as_mut_ptr(),
346                parent_ids_raw.len(),
347                Some(P::cb_progress_callback),
348                &mut progress as *mut P as *mut c_void,
349            )
350        };
351        let handle = NonNull::new(result).ok_or(())?;
352        Ok(unsafe { RemoteSnapshot::ref_from_raw(handle) })
353    }
354
355    // Delete a snapshot from the remote
356    pub fn delete_snapshot(&self, snapshot: &RemoteSnapshot) -> Result<(), ()> {
357        let success =
358            unsafe { BNRemoteFileDeleteSnapshot(self.handle.as_ptr(), snapshot.handle.as_ptr()) };
359        success.then_some(()).ok_or(())
360    }
361
362    // TODO - This passes and returns a c++ `std::vector<T>`. A BnData can be implement in rust, but the
363    // coreAPI need to include a `FreeData` function, similar to `BNFreeString` does.
364    // The C++ API just assumes that both use the same allocator, and the python API seems to just leak this
365    // memory, never dropping it.
366    //pub fn download_file<S, F>(&self, mut progress_function: F) -> BnData
367    //where
368    //    S: BnStrCompatible,
369    //    F: ProgressCallback,
370    //{
371    //    let mut data = ptr::null_mut();
372    //    let mut data_len = 0;
373    //    let result = unsafe {
374    //        BNRemoteFileDownload(
375    //            self.handle.as_ptr(),
376    //            Some(F::cb_progress_callback),
377    //            &mut progress_function as *mut _ as *mut c_void,
378    //            &mut data,
379    //            &mut data_len,
380    //        )
381    //    };
382    //    todo!()
383    //}
384
385    pub fn request_user_positions(&self) -> String {
386        let result = unsafe { BNRemoteFileRequestUserPositions(self.handle.as_ptr()) };
387        assert!(!result.is_null());
388        unsafe { BnString::into_string(result) }
389    }
390
391    pub fn request_chat_log(&self) -> String {
392        let result = unsafe { BNRemoteFileRequestChatLog(self.handle.as_ptr()) };
393        assert!(!result.is_null());
394        unsafe { BnString::into_string(result) }
395    }
396
397    /// Download a remote file and possibly dependencies to its project
398    /// Dependency download behavior depends on the value of the collaboration.autoDownloadFileDependencies setting
399    ///
400    /// * `progress` - Function to call on progress updates
401    pub fn download(&self) -> Result<(), ()> {
402        self.download_with_progress(NoProgressCallback)
403    }
404
405    /// Download a remote file and possibly dependencies to its project
406    /// Dependency download behavior depends on the value of the collaboration.autoDownloadFileDependencies setting
407    ///
408    /// * `progress` - Function to call on progress updates
409    pub fn download_with_progress<P>(&self, mut progress: P) -> Result<(), ()>
410    where
411        P: ProgressCallback,
412    {
413        let success = unsafe {
414            BNRemoteFileDownload(
415                self.handle.as_ptr(),
416                Some(P::cb_progress_callback),
417                &mut progress as *mut P as *mut c_void,
418            )
419        };
420        success.then_some(()).ok_or(())
421    }
422
423    /// Download a remote file and save it to a BNDB at the given `path`, returning the associated [`FileMetadata`].
424    /// Download a file from its remote, saving all snapshots to a database in the
425    /// specified location. Returns a FileContext for opening the file later.
426    ///
427    /// * `path` - File path for saved database
428    pub fn download_database(&self, path: impl AsRef<Path>) -> Result<Ref<FileMetadata>, ()> {
429        //TODO: deprecated, use RemoteFile.download() and ProjectFile.export()
430        self.download_database_with_progress(path, NoProgressCallback)
431    }
432
433    /// Download a remote file and save it to a BNDB at the given `path`, returning the associated [`FileMetadata`].
434    /// Download a file from its remote, saving all snapshots to a database in the
435    /// specified location. Returns a FileContext for opening the file later.
436    ///
437    /// * `path` - File path for saved database
438    /// * `progress_function` - Function to call for progress updates
439    pub fn download_database_with_progress(
440        &self,
441        path: impl AsRef<Path>,
442        progress: impl ProgressCallback,
443    ) -> Result<Ref<FileMetadata>, ()> {
444        //TODO: deprecated, use RemoteFile.download_with_progress() and ProjectFile.export()
445        let mut progress = progress.split(&[50, 50]);
446        let file = sync::download_file_with_progress(
447            self,
448            path.as_ref(),
449            progress.next_subpart().unwrap(),
450        )?;
451        let database = file.database().ok_or(())?;
452        self.sync_with_progress(
453            &database,
454            DatabaseConflictHandlerFail,
455            NoNameChangeset,
456            progress.next_subpart().unwrap(),
457        )?;
458        Ok(file)
459    }
460
461    /// Completely sync a file, pushing/pulling/merging/applying changes
462    ///
463    /// * `bv_or_db` - Binary view or database to sync with
464    /// * `conflict_handler` - Function to call to resolve snapshot conflicts
465    /// * `name_changeset` - Function to call for naming a pushed changeset, if necessary
466    pub fn sync<C: DatabaseConflictHandler, N: NameChangeset>(
467        &self,
468        database: &Database,
469        conflict_handler: C,
470        name_changeset: N,
471    ) -> Result<(), ()> {
472        sync::sync_database(database, self, conflict_handler, name_changeset)
473    }
474
475    /// Completely sync a file, pushing/pulling/merging/applying changes
476    ///
477    /// * `bv_or_db` - Binary view or database to sync with
478    /// * `conflict_handler` - Function to call to resolve snapshot conflicts
479    /// * `name_changeset` - Function to call for naming a pushed changeset, if necessary
480    /// * `progress` - Function to call for progress updates
481    pub fn sync_with_progress<C: DatabaseConflictHandler, P: ProgressCallback, N: NameChangeset>(
482        &self,
483        database: &Database,
484        conflict_handler: C,
485        name_changeset: N,
486        progress: P,
487    ) -> Result<(), ()> {
488        sync::sync_database_with_progress(
489            database,
490            self,
491            conflict_handler,
492            name_changeset,
493            progress,
494        )
495    }
496
497    /// Pull updated snapshots from the remote. Merge local changes with remote changes and
498    /// potentially create a new snapshot for unsaved changes, named via name_changeset.
499    ///
500    /// * `bv_or_db` - Binary view or database to sync with
501    /// * `conflict_handler` - Function to call to resolve snapshot conflicts
502    /// * `name_changeset` - Function to call for naming a pushed changeset, if necessary
503    pub fn pull<C, N>(
504        &self,
505        database: &Database,
506        conflict_handler: C,
507        name_changeset: N,
508    ) -> Result<usize, ()>
509    where
510        C: DatabaseConflictHandler,
511        N: NameChangeset,
512    {
513        sync::pull_database(database, self, conflict_handler, name_changeset)
514    }
515
516    /// Pull updated snapshots from the remote. Merge local changes with remote changes and
517    /// potentially create a new snapshot for unsaved changes, named via name_changeset.
518    ///
519    /// * `bv_or_db` - Binary view or database to sync with
520    /// * `conflict_handler` - Function to call to resolve snapshot conflicts
521    /// * `name_changeset` - Function to call for naming a pushed changeset, if necessary
522    /// * `progress` - Function to call for progress updates
523    pub fn pull_with_progress<C, P, N>(
524        &self,
525        database: &Database,
526        conflict_handler: C,
527        name_changeset: N,
528        progress: P,
529    ) -> Result<usize, ()>
530    where
531        C: DatabaseConflictHandler,
532        P: ProgressCallback,
533        N: NameChangeset,
534    {
535        sync::pull_database_with_progress(
536            database,
537            self,
538            conflict_handler,
539            name_changeset,
540            progress,
541        )
542    }
543
544    /// Push locally added snapshots to the remote.
545    ///
546    /// * `bv_or_db` - Binary view or database to sync with
547    pub fn push<P>(&self, database: &Database) -> Result<usize, ()>
548    where
549        P: ProgressCallback,
550    {
551        sync::push_database(database, self)
552    }
553
554    /// Push locally added snapshots to the remote.
555    ///
556    /// * `bv_or_db` - Binary view or database to sync with
557    /// * `progress` - Function to call for progress updates
558    pub fn push_with_progress<P>(&self, database: &Database, progress: P) -> Result<usize, ()>
559    where
560        P: ProgressCallback,
561    {
562        sync::push_database_with_progress(database, self, progress)
563    }
564}
565
566impl Debug for RemoteFile {
567    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
568        f.debug_struct("RemoteFile")
569            .field("id", &self.id())
570            .field("name", &self.name())
571            .field("description", &self.description())
572            .field("metadata", &self.metadata())
573            .field("size", &self.size())
574            .field(
575                "snapshot_count",
576                &self.snapshots().map(|s| s.len()).unwrap_or(0),
577            )
578            .finish()
579    }
580}
581
582impl PartialEq for RemoteFile {
583    fn eq(&self, other: &Self) -> bool {
584        self.id() == other.id()
585    }
586}
587impl Eq for RemoteFile {}
588
589impl ToOwned for RemoteFile {
590    type Owned = Ref<Self>;
591
592    fn to_owned(&self) -> Self::Owned {
593        unsafe { RefCountable::inc_ref(self) }
594    }
595}
596
597unsafe impl RefCountable for RemoteFile {
598    unsafe fn inc_ref(handle: &Self) -> Ref<Self> {
599        Ref::new(Self {
600            handle: NonNull::new(BNNewRemoteFileReference(handle.handle.as_ptr())).unwrap(),
601        })
602    }
603
604    unsafe fn dec_ref(handle: &Self) {
605        BNFreeRemoteFile(handle.handle.as_ptr());
606    }
607}
608
609impl CoreArrayProvider for RemoteFile {
610    type Raw = *mut BNRemoteFile;
611    type Context = ();
612    type Wrapped<'a> = Guard<'a, Self>;
613}
614
615unsafe impl CoreArrayProviderInner for RemoteFile {
616    unsafe fn free(raw: *mut Self::Raw, count: usize, _context: &Self::Context) {
617        BNFreeRemoteFileList(raw, count)
618    }
619
620    unsafe fn wrap_raw<'a>(raw: &'a Self::Raw, context: &'a Self::Context) -> Self::Wrapped<'a> {
621        let raw_ptr = NonNull::new(*raw).unwrap();
622        Guard::new(Self::from_raw(raw_ptr), context)
623    }
624}