diff options
Diffstat (limited to 'rust/src/collaboration/file.rs')
| -rw-r--r-- | rust/src/collaboration/file.rs | 647 |
1 files changed, 647 insertions, 0 deletions
diff --git a/rust/src/collaboration/file.rs b/rust/src/collaboration/file.rs new file mode 100644 index 00000000..2651d3c7 --- /dev/null +++ b/rust/src/collaboration/file.rs @@ -0,0 +1,647 @@ +use std::ffi::{c_char, c_void}; +use std::fmt::{Debug, Formatter}; +use std::ptr::NonNull; +use std::time::SystemTime; + +use binaryninjacore_sys::*; + +use super::{ + sync, DatabaseConflictHandler, DatabaseConflictHandlerFail, NameChangeset, NoNameChangeset, + Remote, RemoteFolder, RemoteProject, RemoteSnapshot, +}; + +use crate::binary_view::{BinaryView, BinaryViewExt}; +use crate::database::Database; +use crate::file_metadata::FileMetadata; +use crate::progress::{NoProgressCallback, ProgressCallback, SplitProgressBuilder}; +use crate::project::file::ProjectFile; +use crate::rc::{Array, CoreArrayProvider, CoreArrayProviderInner, Guard, Ref, RefCountable}; +use crate::string::{BnStrCompatible, BnString}; + +pub type RemoteFileType = BNRemoteFileType; + +/// A remote project file. It controls the various snapshots and raw file contents associated with the analysis. +#[repr(transparent)] +pub struct RemoteFile { + pub(crate) handle: NonNull<BNRemoteFile>, +} + +impl RemoteFile { + pub(crate) unsafe fn from_raw(handle: NonNull<BNRemoteFile>) -> Self { + Self { handle } + } + + pub(crate) unsafe fn ref_from_raw(handle: NonNull<BNRemoteFile>) -> Ref<Self> { + Ref::new(Self { handle }) + } + + /// Look up the remote File for a local database, or None if there is no matching + /// remote File found. + /// See [RemoteFile::get_for_binary_view] to load from a [BinaryView]. + pub fn get_for_local_database(database: &Database) -> Result<Option<Ref<RemoteFile>>, ()> { + // TODO: This sync should be removed? + if !sync::pull_files(database)? { + return Ok(None); + } + sync::get_remote_file_for_local_database(database) + } + + /// Look up the [`RemoteFile`] for a local [`BinaryView`], or None if there is no matching + /// remote File found. + pub fn get_for_binary_view(bv: &BinaryView) -> Result<Option<Ref<RemoteFile>>, ()> { + let file = bv.file(); + let Some(database) = file.database() else { + return Ok(None); + }; + RemoteFile::get_for_local_database(&database) + } + + pub fn core_file(&self) -> Result<ProjectFile, ()> { + let result = unsafe { BNRemoteFileGetCoreFile(self.handle.as_ptr()) }; + NonNull::new(result) + .map(|handle| unsafe { ProjectFile::from_raw(handle) }) + .ok_or(()) + } + + pub fn project(&self) -> Result<Ref<RemoteProject>, ()> { + let result = unsafe { BNRemoteFileGetProject(self.handle.as_ptr()) }; + NonNull::new(result) + .map(|handle| unsafe { RemoteProject::ref_from_raw(handle) }) + .ok_or(()) + } + + pub fn remote(&self) -> Result<Ref<Remote>, ()> { + let result = unsafe { BNRemoteFileGetRemote(self.handle.as_ptr()) }; + NonNull::new(result) + .map(|handle| unsafe { Remote::ref_from_raw(handle) }) + .ok_or(()) + } + + /// Parent folder, if one exists. None if this is in the root of the project. + pub fn folder(&self) -> Result<Option<Ref<RemoteFolder>>, ()> { + let project = self.project()?; + if !project.has_pulled_folders() { + project.pull_folders()?; + } + let result = unsafe { BNRemoteFileGetFolder(self.handle.as_ptr()) }; + Ok(NonNull::new(result).map(|handle| unsafe { RemoteFolder::ref_from_raw(handle) })) + } + + /// Set the parent folder of a file. + pub fn set_folder(&self, folder: Option<&RemoteFolder>) -> Result<(), ()> { + let folder_raw = folder.map_or(std::ptr::null_mut(), |f| f.handle.as_ptr()); + let success = unsafe { BNRemoteFileSetFolder(self.handle.as_ptr(), folder_raw) }; + success.then_some(()).ok_or(()) + } + + pub fn set_metadata<S: BnStrCompatible>(&self, folder: S) -> Result<(), ()> { + let folder_raw = folder.into_bytes_with_nul(); + let success = unsafe { + BNRemoteFileSetMetadata( + self.handle.as_ptr(), + folder_raw.as_ref().as_ptr() as *const c_char, + ) + }; + success.then_some(()).ok_or(()) + } + + /// Web API endpoint URL + pub fn url(&self) -> BnString { + let result = unsafe { BNRemoteFileGetUrl(self.handle.as_ptr()) }; + assert!(!result.is_null()); + unsafe { BnString::from_raw(result) } + } + + /// Chat log API endpoint URL + pub fn chat_log_url(&self) -> BnString { + let result = unsafe { BNRemoteFileGetChatLogUrl(self.handle.as_ptr()) }; + assert!(!result.is_null()); + unsafe { BnString::from_raw(result) } + } + + pub fn user_positions_url(&self) -> BnString { + let result = unsafe { BNRemoteFileGetUserPositionsUrl(self.handle.as_ptr()) }; + assert!(!result.is_null()); + unsafe { BnString::from_raw(result) } + } + + /// Unique ID + pub fn id(&self) -> BnString { + let result = unsafe { BNRemoteFileGetId(self.handle.as_ptr()) }; + assert!(!result.is_null()); + unsafe { BnString::from_raw(result) } + } + + /// All files share the same properties, but files with different types may make different + /// uses of those properties, or not use some of them at all. + pub fn file_type(&self) -> RemoteFileType { + unsafe { BNRemoteFileGetType(self.handle.as_ptr()) } + } + + /// Created date of the file + pub fn created(&self) -> SystemTime { + let result = unsafe { BNRemoteFileGetCreated(self.handle.as_ptr()) }; + crate::ffi::time_from_bn(result.try_into().unwrap()) + } + + pub fn created_by(&self) -> BnString { + let result = unsafe { BNRemoteFileGetCreatedBy(self.handle.as_ptr()) }; + assert!(!result.is_null()); + unsafe { BnString::from_raw(result) } + } + + /// Last modified of the file + pub fn last_modified(&self) -> SystemTime { + let result = unsafe { BNRemoteFileGetLastModified(self.handle.as_ptr()) }; + crate::ffi::time_from_bn(result.try_into().unwrap()) + } + + /// Date of last snapshot in the file + pub fn last_snapshot(&self) -> SystemTime { + let result = unsafe { BNRemoteFileGetLastSnapshot(self.handle.as_ptr()) }; + crate::ffi::time_from_bn(result.try_into().unwrap()) + } + + /// Username of user who pushed the last snapshot in the file + pub fn last_snapshot_by(&self) -> BnString { + let result = unsafe { BNRemoteFileGetLastSnapshotBy(self.handle.as_ptr()) }; + assert!(!result.is_null()); + unsafe { BnString::from_raw(result) } + } + + pub fn last_snapshot_name(&self) -> BnString { + let result = unsafe { BNRemoteFileGetLastSnapshotName(self.handle.as_ptr()) }; + assert!(!result.is_null()); + unsafe { BnString::from_raw(result) } + } + + /// Hash of file contents (no algorithm guaranteed) + pub fn hash(&self) -> BnString { + let result = unsafe { BNRemoteFileGetHash(self.handle.as_ptr()) }; + assert!(!result.is_null()); + unsafe { BnString::from_raw(result) } + } + + /// Displayed name of file + pub fn name(&self) -> BnString { + let result = unsafe { BNRemoteFileGetName(self.handle.as_ptr()) }; + assert!(!result.is_null()); + unsafe { BnString::from_raw(result) } + } + + /// Set the description of the file. You will need to push the file to update the remote version. + pub fn set_name<S: BnStrCompatible>(&self, name: S) -> Result<(), ()> { + let name = name.into_bytes_with_nul(); + let success = unsafe { + BNRemoteFileSetName( + self.handle.as_ptr(), + name.as_ref().as_ptr() as *const c_char, + ) + }; + success.then_some(()).ok_or(()) + } + + /// Desciprtion of the file + pub fn description(&self) -> BnString { + let result = unsafe { BNRemoteFileGetDescription(self.handle.as_ptr()) }; + assert!(!result.is_null()); + unsafe { BnString::from_raw(result) } + } + + /// Set the description of the file. You will need to push the file to update the remote version. + pub fn set_description<S: BnStrCompatible>(&self, description: S) -> Result<(), ()> { + let description = description.into_bytes_with_nul(); + let success = unsafe { + BNRemoteFileSetDescription( + self.handle.as_ptr(), + description.as_ref().as_ptr() as *const c_char, + ) + }; + success.then_some(()).ok_or(()) + } + + pub fn metadata(&self) -> BnString { + let result = unsafe { BNRemoteFileGetMetadata(self.handle.as_ptr()) }; + assert!(!result.is_null()); + unsafe { BnString::from_raw(result) } + } + + /// Size of raw content of file, in bytes + pub fn size(&self) -> u64 { + unsafe { BNRemoteFileGetSize(self.handle.as_ptr()) } + } + + /// Get the default filepath for a remote File. This is based off the Setting for + /// collaboration.directory, the file's id, the file's project's id, and the file's + /// remote's id. + pub fn default_path(&self) -> BnString { + let result = unsafe { BNCollaborationDefaultFilePath(self.handle.as_ptr()) }; + assert!(!result.is_null()); + unsafe { BnString::from_raw(result) } + } + + /// If the file has pulled the snapshots yet + pub fn has_pulled_snapshots(&self) -> bool { + unsafe { BNRemoteFileHasPulledSnapshots(self.handle.as_ptr()) } + } + + /// Get the list of snapshots in this file. + /// + /// NOTE: If snapshots have not been pulled, they will be pulled upon calling this. + pub fn snapshots(&self) -> Result<Array<RemoteSnapshot>, ()> { + // TODO: This sync should be removed? + if !self.has_pulled_snapshots() { + self.pull_snapshots()?; + } + let mut count = 0; + let result = unsafe { BNRemoteFileGetSnapshots(self.handle.as_ptr(), &mut count) }; + (!result.is_null()) + .then(|| unsafe { Array::new(result, count, ()) }) + .ok_or(()) + } + + /// Get a specific Snapshot in the File by its id + /// + /// NOTE: If snapshots have not been pulled, they will be pulled upon calling this. + pub fn snapshot_by_id<S: BnStrCompatible>( + &self, + id: S, + ) -> Result<Option<Ref<RemoteSnapshot>>, ()> { + // TODO: This sync should be removed? + if !self.has_pulled_snapshots() { + self.pull_snapshots()?; + } + let id = id.into_bytes_with_nul(); + let result = unsafe { + BNRemoteFileGetSnapshotById(self.handle.as_ptr(), id.as_ref().as_ptr() as *const c_char) + }; + Ok(NonNull::new(result).map(|handle| unsafe { RemoteSnapshot::ref_from_raw(handle) })) + } + + /// Pull the list of Snapshots from the Remote. + pub fn pull_snapshots(&self) -> Result<(), ()> { + self.pull_snapshots_with_progress(NoProgressCallback) + } + + /// Pull the list of Snapshots from the Remote. + pub fn pull_snapshots_with_progress<P: ProgressCallback>( + &self, + mut progress: P, + ) -> Result<(), ()> { + let success = unsafe { + BNRemoteFilePullSnapshots( + self.handle.as_ptr(), + Some(P::cb_progress_callback), + &mut progress as *mut P as *mut c_void, + ) + }; + success.then_some(()).ok_or(()) + } + + /// Create a new snapshot on the remote (and pull it) + /// + /// * `name` - Snapshot name + /// * `contents` - Snapshot contents + /// * `analysis_cache_contents` - Contents of analysis cache of snapshot + /// * `file` - New file contents (if contents changed) + /// * `parent_ids` - List of ids of parent snapshots (or empty if this is a root snapshot) + pub fn create_snapshot<S, I>( + &self, + name: S, + contents: &mut [u8], + analysis_cache_contexts: &mut [u8], + file: &mut [u8], + parent_ids: I, + ) -> Result<Ref<RemoteSnapshot>, ()> + where + S: BnStrCompatible, + I: IntoIterator, + I::Item: BnStrCompatible, + { + self.create_snapshot_with_progress( + name, + contents, + analysis_cache_contexts, + file, + parent_ids, + NoProgressCallback, + ) + } + + /// Create a new snapshot on the remote (and pull it) + /// + /// * `name` - Snapshot name + /// * `contents` - Snapshot contents + /// * `analysis_cache_contents` - Contents of analysis cache of snapshot + /// * `file` - New file contents (if contents changed) + /// * `parent_ids` - List of ids of parent snapshots (or empty if this is a root snapshot) + /// * `progress` - Function to call on progress updates + pub fn create_snapshot_with_progress<S, I, P>( + &self, + name: S, + contents: &mut [u8], + analysis_cache_contexts: &mut [u8], + file: &mut [u8], + parent_ids: I, + mut progress: P, + ) -> Result<Ref<RemoteSnapshot>, ()> + where + S: BnStrCompatible, + P: ProgressCallback, + I: IntoIterator, + I::Item: BnStrCompatible, + { + let name = name.into_bytes_with_nul(); + let parent_ids: Vec<_> = parent_ids + .into_iter() + .map(|id| id.into_bytes_with_nul()) + .collect(); + let mut parent_ids_raw: Vec<_> = parent_ids + .iter() + .map(|x| x.as_ref().as_ptr() as *const c_char) + .collect(); + let result = unsafe { + BNRemoteFileCreateSnapshot( + self.handle.as_ptr(), + name.as_ref().as_ptr() as *const c_char, + contents.as_mut_ptr(), + contents.len(), + analysis_cache_contexts.as_mut_ptr(), + analysis_cache_contexts.len(), + file.as_mut_ptr(), + file.len(), + parent_ids_raw.as_mut_ptr(), + parent_ids_raw.len(), + Some(P::cb_progress_callback), + &mut progress as *mut P as *mut c_void, + ) + }; + let handle = NonNull::new(result).ok_or(())?; + Ok(unsafe { RemoteSnapshot::ref_from_raw(handle) }) + } + + // Delete a snapshot from the remote + pub fn delete_snapshot(&self, snapshot: &RemoteSnapshot) -> Result<(), ()> { + let success = + unsafe { BNRemoteFileDeleteSnapshot(self.handle.as_ptr(), snapshot.handle.as_ptr()) }; + success.then_some(()).ok_or(()) + } + + // TODO - This passes and returns a c++ `std::vector<T>`. A BnData can be implement in rust, but the + // coreAPI need to include a `FreeData` function, similar to `BNFreeString` does. + // The C++ API just assumes that both use the same allocator, and the python API seems to just leak this + // memory, never dropping it. + //pub fn download_file<S, F>(&self, mut progress_function: F) -> BnData + //where + // S: BnStrCompatible, + // F: ProgressCallback, + //{ + // let mut data = ptr::null_mut(); + // let mut data_len = 0; + // let result = unsafe { + // BNRemoteFileDownload( + // self.handle.as_ptr(), + // Some(F::cb_progress_callback), + // &mut progress_function as *mut _ as *mut c_void, + // &mut data, + // &mut data_len, + // ) + // }; + // todo!() + //} + + pub fn request_user_positions(&self) -> BnString { + let result = unsafe { BNRemoteFileRequestUserPositions(self.handle.as_ptr()) }; + assert!(!result.is_null()); + unsafe { BnString::from_raw(result) } + } + + pub fn request_chat_log(&self) -> BnString { + let result = unsafe { BNRemoteFileRequestChatLog(self.handle.as_ptr()) }; + assert!(!result.is_null()); + unsafe { BnString::from_raw(result) } + } + + // TODO: AsRef<Path> + /// Download a file from its remote, saving all snapshots to a database in the + /// specified location. Returns a FileContext for opening the file later. + /// + /// * `db_path` - File path for saved database + /// * `progress_function` - Function to call for progress updates + pub fn download<S>(&self, db_path: S) -> Result<Ref<FileMetadata>, ()> + where + S: BnStrCompatible, + { + sync::download_file(self, db_path) + } + + // TODO: AsRef<Path> + /// Download a file from its remote, saving all snapshots to a database in the + /// specified location. Returns a FileContext for opening the file later. + /// + /// * `db_path` - File path for saved database + /// * `progress_function` - Function to call for progress updates + pub fn download_with_progress<S, F>( + &self, + db_path: S, + progress_function: F, + ) -> Result<Ref<FileMetadata>, ()> + where + S: BnStrCompatible, + F: ProgressCallback, + { + sync::download_file_with_progress(self, db_path, progress_function) + } + + /// Download a remote file and save it to a BNDB at the given `path`, returning the associated [`FileMetadata`]. + pub fn download_database<S: BnStrCompatible>(&self, path: S) -> Result<Ref<FileMetadata>, ()> { + let file = self.download(path)?; + let database = file.database().ok_or(())?; + self.sync(&database, DatabaseConflictHandlerFail, NoNameChangeset)?; + Ok(file) + } + + // TODO: This might be a bad helper... maybe remove... + // TODO: AsRef<Path> + /// Download a remote file and save it to a BNDB at the given `path`. + pub fn download_database_with_progress<S: BnStrCompatible>( + &self, + path: S, + progress: impl ProgressCallback, + ) -> Result<Ref<FileMetadata>, ()> { + let mut progress = progress.split(&[50, 50]); + let file = self.download_with_progress(path, progress.next_subpart().unwrap())?; + let database = file.database().ok_or(())?; + self.sync_with_progress( + &database, + DatabaseConflictHandlerFail, + NoNameChangeset, + progress.next_subpart().unwrap(), + )?; + Ok(file) + } + + /// Completely sync a file, pushing/pulling/merging/applying changes + /// + /// * `bv_or_db` - Binary view or database to sync with + /// * `conflict_handler` - Function to call to resolve snapshot conflicts + /// * `name_changeset` - Function to call for naming a pushed changeset, if necessary + pub fn sync<C: DatabaseConflictHandler, N: NameChangeset>( + &self, + database: &Database, + conflict_handler: C, + name_changeset: N, + ) -> Result<(), ()> { + sync::sync_database(database, self, conflict_handler, name_changeset) + } + + /// Completely sync a file, pushing/pulling/merging/applying changes + /// + /// * `bv_or_db` - Binary view or database to sync with + /// * `conflict_handler` - Function to call to resolve snapshot conflicts + /// * `name_changeset` - Function to call for naming a pushed changeset, if necessary + /// * `progress` - Function to call for progress updates + pub fn sync_with_progress<C: DatabaseConflictHandler, P: ProgressCallback, N: NameChangeset>( + &self, + database: &Database, + conflict_handler: C, + name_changeset: N, + progress: P, + ) -> Result<(), ()> { + sync::sync_database_with_progress( + database, + self, + conflict_handler, + name_changeset, + progress, + ) + } + + /// Pull updated snapshots from the remote. Merge local changes with remote changes and + /// potentially create a new snapshot for unsaved changes, named via name_changeset. + /// + /// * `bv_or_db` - Binary view or database to sync with + /// * `conflict_handler` - Function to call to resolve snapshot conflicts + /// * `name_changeset` - Function to call for naming a pushed changeset, if necessary + pub fn pull<C, N>( + &self, + database: &Database, + conflict_handler: C, + name_changeset: N, + ) -> Result<usize, ()> + where + C: DatabaseConflictHandler, + N: NameChangeset, + { + sync::pull_database(database, self, conflict_handler, name_changeset) + } + + /// Pull updated snapshots from the remote. Merge local changes with remote changes and + /// potentially create a new snapshot for unsaved changes, named via name_changeset. + /// + /// * `bv_or_db` - Binary view or database to sync with + /// * `conflict_handler` - Function to call to resolve snapshot conflicts + /// * `name_changeset` - Function to call for naming a pushed changeset, if necessary + /// * `progress` - Function to call for progress updates + pub fn pull_with_progress<C, P, N>( + &self, + database: &Database, + conflict_handler: C, + name_changeset: N, + progress: P, + ) -> Result<usize, ()> + where + C: DatabaseConflictHandler, + P: ProgressCallback, + N: NameChangeset, + { + sync::pull_database_with_progress( + database, + self, + conflict_handler, + name_changeset, + progress, + ) + } + + /// Push locally added snapshots to the remote. + /// + /// * `bv_or_db` - Binary view or database to sync with + pub fn push<P>(&self, database: &Database) -> Result<usize, ()> + where + P: ProgressCallback, + { + sync::push_database(database, self) + } + + /// Push locally added snapshots to the remote. + /// + /// * `bv_or_db` - Binary view or database to sync with + /// * `progress` - Function to call for progress updates + pub fn push_with_progress<P>(&self, database: &Database, progress: P) -> Result<usize, ()> + where + P: ProgressCallback, + { + sync::push_database_with_progress(database, self, progress) + } +} + +impl Debug for RemoteFile { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("RemoteFile") + .field("id", &self.id()) + .field("name", &self.name()) + .field("description", &self.description()) + .field("metadata", &self.metadata()) + .field("size", &self.size()) + .field( + "snapshot_count", + &self.snapshots().map(|s| s.len()).unwrap_or(0), + ) + .finish() + } +} + +impl PartialEq for RemoteFile { + fn eq(&self, other: &Self) -> bool { + self.id() == other.id() + } +} +impl Eq for RemoteFile {} + +impl ToOwned for RemoteFile { + type Owned = Ref<Self>; + + fn to_owned(&self) -> Self::Owned { + unsafe { RefCountable::inc_ref(self) } + } +} + +unsafe impl RefCountable for RemoteFile { + unsafe fn inc_ref(handle: &Self) -> Ref<Self> { + Ref::new(Self { + handle: NonNull::new(BNNewRemoteFileReference(handle.handle.as_ptr())).unwrap(), + }) + } + + unsafe fn dec_ref(handle: &Self) { + BNFreeRemoteFile(handle.handle.as_ptr()); + } +} + +impl CoreArrayProvider for RemoteFile { + type Raw = *mut BNRemoteFile; + type Context = (); + type Wrapped<'a> = Guard<'a, Self>; +} + +unsafe impl CoreArrayProviderInner for RemoteFile { + unsafe fn free(raw: *mut Self::Raw, count: usize, _context: &Self::Context) { + BNFreeRemoteFileList(raw, count) + } + + unsafe fn wrap_raw<'a>(raw: &'a Self::Raw, context: &'a Self::Context) -> Self::Wrapped<'a> { + let raw_ptr = NonNull::new(*raw).unwrap(); + Guard::new(Self::from_raw(raw_ptr), context) + } +} |
