diff options
| author | Mason Reed <mason@vector35.com> | 2025-08-26 23:59:38 -0400 |
|---|---|---|
| committer | Mason Reed <mason@vector35.com> | 2025-10-01 21:38:39 -0400 |
| commit | ede39aee7e00c40a43b67ca18dd8ab80ee863d85 (patch) | |
| tree | 67c5eda347ece2282e3c888f38066b496e06dec8 /plugins/warp/src/container | |
| parent | a1c46813e7f279aa4cfdb9dbb91c45b559ebeacd (diff) | |
[WARP] Enhanced network support
Diffstat (limited to 'plugins/warp/src/container')
| -rw-r--r-- | plugins/warp/src/container/disk.rs | 57 | ||||
| -rw-r--r-- | plugins/warp/src/container/memory.rs | 15 | ||||
| -rw-r--r-- | plugins/warp/src/container/network.rs | 133 | ||||
| -rw-r--r-- | plugins/warp/src/container/network/client.rs | 405 |
4 files changed, 544 insertions, 66 deletions
diff --git a/plugins/warp/src/container/disk.rs b/plugins/warp/src/container/disk.rs index ca685a5f..02da0f30 100644 --- a/plugins/warp/src/container/disk.rs +++ b/plugins/warp/src/container/disk.rs @@ -1,5 +1,7 @@ -use crate::container::{Container, ContainerError, ContainerResult, SourceId, SourcePath}; -use std::collections::HashMap; +use crate::container::{ + Container, ContainerError, ContainerResult, SourceId, SourcePath, SourceTag, +}; +use std::collections::{HashMap, HashSet}; use std::fmt::{Debug, Display, Formatter}; use std::hash::{Hash, Hasher}; use std::path::PathBuf; @@ -20,11 +22,12 @@ pub const NAMESPACE_DISK_SOURCE: Uuid = uuid!("ea89e8ab-a27a-432b-8fbd-77b026cd5 pub struct DiskContainer { pub name: String, pub sources: HashMap<SourceId, DiskContainerSource>, + pub writable: bool, } impl DiskContainer { pub fn new(name: String, sources: HashMap<SourceId, DiskContainerSource>) -> Self { - Self { name, sources } + Self { name, sources, writable: true } } pub fn new_from_dir(dir_path: PathBuf) -> Self { @@ -56,6 +59,22 @@ impl DiskContainer { Self::new(name, sources) } + + pub fn insert_source(&mut self, id: SourceId, path: SourcePath) -> ContainerResult<()> { + if !self.writable || self.sources.contains_key(&id) { + return Err(ContainerError::SourceAlreadyExists(path)); + } + // NOTE: We let anyone add a file from anywhere on the file system because of this. + let disk_source = match path.0.exists() { + true => DiskContainerSource::new_from_path(path.clone())?, + false => { + let file = WarpFile::new(WarpFileHeader::new(), vec![]); + DiskContainerSource::new(path, file) + } + }; + self.sources.insert(id, disk_source); + Ok(()) + } } impl Container for DiskContainer { @@ -66,23 +85,8 @@ impl Container for DiskContainer { fn add_source(&mut self, path: SourcePath) -> ContainerResult<SourceId> { // Disk sources have there source id computed from the path. let source_id = path.to_source_id(); - if self.sources.contains_key(&source_id) { - return Err(ContainerError::SourceAlreadyExists(path)); - } - // NOTE: We let anyone add a file from anywhere on the file system because of this. - match path.0.exists() { - true => { - let disk_source = DiskContainerSource::new_from_path(path.clone())?; - self.sources.insert(source_id, disk_source); - Ok(source_id) - } - false => { - let file = WarpFile::new(WarpFileHeader::new(), vec![]); - let disk_source = DiskContainerSource::new(path, file); - self.sources.insert(source_id, disk_source); - Ok(source_id) - } - } + self.insert_source(source_id, path)?; + Ok(source_id) } fn commit_source(&mut self, source: &SourceId) -> ContainerResult<bool> { @@ -99,8 +103,7 @@ impl Container for DiskContainer { .sources .get(source) .ok_or(ContainerError::SourceNotFound(*source))?; - // TODO: I think this should be up to the container. (cant write to bundled files) - Ok(true) + Ok(self.writable) } fn is_source_uncommitted(&self, source: &SourceId) -> ContainerResult<bool> { @@ -111,6 +114,14 @@ impl Container for DiskContainer { Ok(disk_source.uncommitted) } + fn source_tags(&self, source: &SourceId) -> ContainerResult<HashSet<SourceTag>> { + let disk_source = self + .sources + .get(source) + .ok_or(ContainerError::SourceNotFound(*source))?; + Ok(disk_source.tags.clone()) + } + fn source_path(&self, source: &SourceId) -> ContainerResult<SourcePath> { let disk_source = self .sources @@ -277,6 +288,7 @@ impl Debug for DiskContainer { pub struct DiskContainerSource { pub path: SourcePath, + pub tags: HashSet<SourceTag>, file: WarpFile<'static>, uncommitted: bool, } @@ -285,6 +297,7 @@ impl DiskContainerSource { pub fn new(path: SourcePath, file: WarpFile<'static>) -> Self { Self { path, + tags: HashSet::new(), file, uncommitted: false, } diff --git a/plugins/warp/src/container/memory.rs b/plugins/warp/src/container/memory.rs index cf53390e..51628d6c 100644 --- a/plugins/warp/src/container/memory.rs +++ b/plugins/warp/src/container/memory.rs @@ -1,5 +1,7 @@ -use crate::container::{Container, ContainerError, ContainerResult, SourceId, SourcePath}; -use std::collections::HashMap; +use crate::container::{ + Container, ContainerError, ContainerResult, SourceId, SourcePath, SourceTag, +}; +use std::collections::{HashMap, HashSet}; use std::fmt::Display; use warp::r#type::guid::TypeGUID; use warp::r#type::{ComputedType, Type}; @@ -73,6 +75,15 @@ impl Container for MemoryContainer { Ok(false) } + fn source_tags(&self, source: &SourceId) -> ContainerResult<HashSet<SourceTag>> { + let _memory_source = self + .sources + .get(source) + .ok_or(ContainerError::SourceNotFound(*source))?; + // NOTE: Memory containers do not have a notion of tags. + Ok(HashSet::default()) + } + fn source_path(&self, source: &SourceId) -> ContainerResult<SourcePath> { Err(ContainerError::SourcePathUnavailable(*source)) } diff --git a/plugins/warp/src/container/network.rs b/plugins/warp/src/container/network.rs index 2a2d7c65..11f4db6b 100644 --- a/plugins/warp/src/container/network.rs +++ b/plugins/warp/src/container/network.rs @@ -1,7 +1,12 @@ use crate::container::disk::DiskContainer; -use crate::container::{Container, ContainerError, ContainerResult, SourceId, SourcePath}; -use std::collections::HashMap; +use crate::container::{ + Container, ContainerError, ContainerResult, ContainerSearchQuery, ContainerSearchResponse, + SourceId, SourcePath, SourceTag, +}; +use directories::ProjectDirs; +use std::collections::{HashMap, HashSet}; use std::fmt::{Debug, Display, Formatter}; +use std::path::PathBuf; use warp::chunk::{Chunk, ChunkKind, CompressionType}; use warp::r#type::guid::TypeGUID; use warp::r#type::{ComputedType, Type}; @@ -12,6 +17,7 @@ use warp::{WarpFile, WarpFileHeader}; pub mod client; +use crate::container::ContainerError::CannotCreateSource; pub use client::NetworkClient; /// This is the id on the server for the [`Target`], we can get it via [`NetworkClient::query_target_id`]. @@ -22,23 +28,42 @@ pub struct NetworkContainer { /// This is the store that the interface will write to; then we have special functions for pulling /// and pushing to the network source. cache: DiskContainer, + /// Where to place newly created sources. + /// + /// This is typically a directory inside [`NetworkContainer::root_cache_location`]. + cache_path: PathBuf, /// Populated when targets are queried. known_targets: HashMap<Target, Option<NetworkTargetId>>, - /// Populated with function sources are queried. + /// Populated when function sources are queried. known_function_sources: HashMap<FunctionGUID, Vec<SourceId>>, /// Populated when user adds function, this is used for writing back to the server. added_chunks: HashMap<SourceId, Vec<Chunk<'static>>>, + /// Populated when connecting to the server, this is used to determine which sources are writable. + /// + /// NOTE: This is only populated when logged in, as guest users do not have write permissions. + writable_sources: HashSet<SourceId>, } impl NetworkContainer { - pub fn new(client: NetworkClient) -> Self { - Self { - cache: DiskContainer::new("Network Container".to_string(), HashMap::new()), + pub fn new(client: NetworkClient, cache_path: PathBuf, writable_sources: &[SourceId]) -> Self { + let mut container = Self { + cache: DiskContainer::new_from_dir(cache_path.clone()), + cache_path, client, known_targets: HashMap::new(), known_function_sources: HashMap::new(), added_chunks: HashMap::new(), + writable_sources: writable_sources.into_iter().copied().collect(), + }; + + // TODO: Because of this little hack, methinks we should move writable sources to after the + // TODO: container is actually created, but before it is moved into the global container cache. + // Probe all writable sources, so the container knows about them properly. + for source in writable_sources { + container.probe_source(*source); } + + container } /// Gets the network id for the `target`, this will be used in later function queries. @@ -72,6 +97,7 @@ impl NetworkContainer { pub fn get_unseen_functions_source( &mut self, target: Option<&Target>, + tags: &[SourceTag], guids: &[FunctionGUID], ) -> HashMap<SourceId, Vec<FunctionGUID>> { let Some(target_id) = target.and_then(|t| self.get_target_id(t)) else { @@ -88,9 +114,9 @@ impl NetworkContainer { let mut result: HashMap<SourceId, Vec<FunctionGUID>> = HashMap::new(); // Only query server for unknown guids if we have any. if !unknown.is_empty() { - if let Some(queried_results) = self - .client - .query_functions_source(Some(target_id), &unknown) + if let Some(queried_results) = + self.client + .query_functions_source(Some(target_id), tags, &unknown) { // Cache the new results, this means we will not try and contact the server for that guids source. // NOTE: Here we do not just simply list the queried results because we also @@ -139,6 +165,8 @@ impl NetworkContainer { match &chunk.kind { ChunkKind::Signature(sc) => { let functions: Vec<_> = sc.functions().collect(); + // Probe the source before attempting to access it, as it might not exist locally. + self.probe_source(*source); match self.cache.add_functions(target, source, &functions) { Ok(_) => log::debug!( "Added {} functions into cached source '{}'", @@ -164,7 +192,46 @@ impl NetworkContainer { /// /// **This is blocking** pub fn push_file(&mut self, source_id: SourceId, file: &WarpFile) { - self.client.push_file(source_id, file); + // TODO: We need a better name for the commit. I would like to derive it automatically from + // TODO: something instead of having the user give it TBH. + self.client.push_file(source_id, file, "commit"); + } + + /// Probe the source to make sure it exists in the cache. Retrieving the name from the server. + /// + /// **This is blocking** + pub fn probe_source(&mut self, source_id: SourceId) { + if !self.cache.source_path(&source_id).is_ok() { + // Add the source to the cache. Using the source id and source name as the source path. + match self.client.source_name(source_id) { + Ok(source_name) => { + // To prevent two sources with the same name colliding, we add the source id to the source name. + let source_path = self + .cache_path + .join(source_id.to_string()) + .join(source_name); + let _ = self.cache.insert_source(source_id, SourcePath(source_path)); + } + Err(e) => { + log::error!("Failed to probe source '{}': {}", source_id, e); + } + } + } + } + + pub fn root_cache_location() -> PathBuf { + // - Windows: %LOCALAPPDATA%\<org>\<app>\cache + // - macOS: ~/Library/Caches/<org>.<app> + // - Linux: $XDG_CACHE_HOME/<app> or ~/.cache/<app> + if let Some(proj_dirs) = ProjectDirs::from("", "Vector35", "Binary Ninja") { + proj_dirs.cache_dir().to_path_buf() + } else { + // Fallback if OS dirs cannot be determined + std::env::current_dir() + .unwrap_or_else(|_| PathBuf::from(".")) + .join(".cache") + .join("binaryninja") + } } } @@ -174,9 +241,16 @@ impl Container for NetworkContainer { } fn add_source(&mut self, path: SourcePath) -> ContainerResult<SourceId> { - // TODO: How do we want to let users create new sources? - log::error!("NetworkContainer::add_source not allowed"); - Err(ContainerError::CannotCreateSource(path)) + // Send a **blocking** request to the server to create the source. + // NOTE: The user must be logged in for this to work. + // TODO: Some better error handling to alert the user that they are not logged in / creating existing sources. + let source = self + .client + .create_source(&path.to_string()) + .map_err(|_| CannotCreateSource(path))?; + // Must probe the source before attempting to access it, as it does not exist locally. + self.probe_source(source); + Ok(source) } fn commit_source(&mut self, source: &SourceId) -> ContainerResult<bool> { @@ -184,21 +258,26 @@ impl Container for NetworkContainer { .added_chunks .remove(source) .ok_or(ContainerError::SourceNotFound(source.clone()))?; - let file = WarpFile::new(WarpFileHeader::new(), chunks); + // Because each add operation is its own chunk, we should merge them into larger chunks before sending. + let merged_chunks = Chunk::merge(&chunks, CompressionType::Zstd); + let file = WarpFile::new(WarpFileHeader::new(), merged_chunks); self.push_file(*source, &file); Ok(true) } fn is_source_writable(&self, source: &SourceId) -> ContainerResult<bool> { - // TODO: This is retrievable from /users/me/sources we will grab it when connecting. - log::error!("NetworkContainer::is_source_writable not allowed"); - Err(ContainerError::SourceNotWritable(source.clone())) + // Assume that all writable_sources are also in the cache (through `probe_source`). + Ok(self.writable_sources.contains(source)) } fn is_source_uncommitted(&self, source: &SourceId) -> ContainerResult<bool> { Ok(self.added_chunks.contains_key(source)) } + fn source_tags(&self, source: &SourceId) -> ContainerResult<HashSet<SourceTag>> { + self.cache.source_tags(source) + } + fn source_path(&self, source: &SourceId) -> ContainerResult<SourcePath> { self.cache.source_path(source) } @@ -246,10 +325,12 @@ impl Container for NetworkContainer { fn fetch_functions( &mut self, target: &Target, + tags: &[SourceTag], functions: &[FunctionGUID], ) -> ContainerResult<()> { // NOTE: Blocking request to get the mapped function sources. - let mapped_unseen_functions = self.get_unseen_functions_source(Some(&target), functions); + let mapped_unseen_functions = + self.get_unseen_functions_source(Some(&target), tags, functions); // Actually get the function data for the unseen guids, we really only want to do this once per // session, anymore, and this is annoying! @@ -308,16 +389,28 @@ impl Container for NetworkContainer { ) -> ContainerResult<Vec<Function>> { self.cache.functions_with_guid(target, source, guid) } + + fn search(&self, query: &ContainerSearchQuery) -> ContainerResult<ContainerSearchResponse> { + // TODO: Give this an actual network error. + self.client + .search(query) + .ok_or(ContainerError::CorruptedData( + "search query failed to validate", + )) + } } impl Debug for NetworkContainer { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.debug_struct("NetworkContainer").finish() + f.debug_struct("NetworkContainer") + .field("client", &self.client) + .field("cache_path", &self.cache_path) + .finish() } } impl Display for NetworkContainer { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.debug_struct("NetworkContainer").finish() + Display::fmt(&self.client.server_url, f) } } diff --git a/plugins/warp/src/container/network/client.rs b/plugins/warp/src/container/network/client.rs index f77f1118..39e7640a 100644 --- a/plugins/warp/src/container/network/client.rs +++ b/plugins/warp/src/container/network/client.rs @@ -1,12 +1,20 @@ use crate::container::network::NetworkTargetId; -use crate::container::SourceId; +use crate::container::{ + ContainerSearchItem, ContainerSearchItemKind, ContainerSearchQuery, ContainerSearchResponse, + SourceId, SourcePath, SourceTag, +}; use reqwest::blocking::Client; use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION}; use reqwest::StatusCode; +use serde::Deserialize; use serde_json::json; use std::collections::HashMap; use std::str::FromStr; -use warp::signature::function::FunctionGUID; +use uuid::Uuid; +use warp::chunk::ChunkKind; +use warp::r#type::guid::TypeGUID; +use warp::r#type::{ComputedType, Type}; +use warp::signature::function::{Function, FunctionGUID}; use warp::target::Target; use warp::WarpFile; @@ -16,7 +24,7 @@ use warp::WarpFile; #[derive(Clone, Debug)] pub struct NetworkClient { client: Client, - server_url: String, + pub server_url: String, } impl NetworkClient { @@ -65,6 +73,116 @@ impl NetworkClient { Ok(resp.status()) } + /// Query the logged in user. + /// + /// NOTE: **THIS IS BLOCKING** + /// + /// Route: `api/v1/users/me` (TODO: Comment about the query) + pub fn current_user(&self) -> reqwest::Result<(i32, String)> { + let current_user_url = format!("{}/api/v1/users/me", self.server_url); + + #[derive(Deserialize)] + struct CurrentUser { + username: String, + id: i32, + } + + let resp = self + .client + .get(¤t_user_url) + .send()? + .error_for_status()?; + let user: CurrentUser = resp.json()?; + Ok((user.id, user.username)) + } + + /// Query the logged in user. + /// + /// NOTE: **THIS IS BLOCKING** + /// + /// Route: `api/v1/users/me` (TODO: Comment about the query) + pub fn source_name(&self, id: SourceId) -> reqwest::Result<String> { + let source_url = format!("{}/api/v1/sources/{}", self.server_url, id); + + #[derive(Deserialize)] + struct Source { + name: String, + } + + let resp = self.client.get(&source_url).send()?.error_for_status()?; + let src: Source = resp.json()?; + Ok(src.name) + } + + /// Create a new source with the given name. + /// + /// The current user will be added to the source. + /// + /// NOTE: You must be logged in to create a source. + /// + /// NOTE: **THIS IS BLOCKING** + /// + /// Route: `api/v1/sources/` + pub fn create_source(&self, name: &str) -> reqwest::Result<SourceId> { + let source_url = format!("{}/api/v1/sources", self.server_url); + + let body = json!({ + "name": name, + // Passing nothing here will add the current user to the source. + "user_ids": [] + }); + + #[derive(Deserialize)] + struct CreateSourceResponse { + id: Uuid, + } + + let resp = self + .client + .post(&source_url) + .json(&body) + .send()? + .error_for_status()?; + + let parsed: CreateSourceResponse = resp.json()?; + Ok(SourceId(parsed.id)) + } + + /// Query the [`SourceId`]s for the given user. + /// + /// NOTE: **THIS IS BLOCKING** + /// + /// Route: `api/v1/sources/query` (TODO: Comment about the query) + pub fn query_sources(&self, user_id: Option<i32>) -> reqwest::Result<Vec<SourceId>> { + let sources_url = format!("{}/api/v1/sources/query", self.server_url); + + #[derive(Deserialize)] + struct SourceItem { + id: Uuid, + } + + #[derive(Deserialize)] + struct SourcesQueryResponse { + items: Vec<SourceItem>, + } + + let mut query = HashMap::new(); + if let Some(user_id) = user_id { + query.insert("user_id", user_id); + } + let query_str = json!(query).to_string(); + let resp = self + .client + .post(&sources_url) + .body(query_str) + .header("Content-Type", "application/json") + .send()? + .error_for_status()?; + + let parsed: SourcesQueryResponse = resp.json()?; + Ok(parsed.items.into_iter().map(|it| SourceId(it.id)).collect()) + } + /// Query the [`NetworkTargetId`] for the given [`Target`]. /// /// NOTE: **THIS IS BLOCKING** @@ -78,25 +196,36 @@ impl NetworkClient { query.insert("platform", platform); } if let Some(architecture) = &target.architecture { - query.insert("architecture", architecture); + query.insert("arch", architecture); + } + let query_str = json!(query).to_string(); + + #[derive(Deserialize)] + struct TargetQueryResponse { + id: NetworkTargetId, } // NOTE: This is blocking. - let target_id: NetworkTargetId = self + let response = self .client - .get(query_target_url) - .query(&query) + .post(query_target_url) + .body(query_str) + .header("Content-Type", "application/json") .send() - .ok()? - .json::<NetworkTargetId>() .ok()?; - Some(target_id) + // Assuming the first response is the one we want. + // TODO: Handle multiple responses, or error out. + let json_response: Vec<TargetQueryResponse> = response.json().ok()?; + let first_response = json_response.first()?; + + Some(first_response.id) } fn query_functions_body( target: Option<NetworkTargetId>, source: Option<SourceId>, + source_tags: &[SourceTag], guids: &[FunctionGUID], ) -> serde_json::Value { let guids_str: Vec<String> = guids.iter().map(|g| g.to_string()).collect(); @@ -112,6 +241,9 @@ impl NetworkClient { if let Some(source_id) = source { body["source_id"] = json!(source_id.to_string()); } + if !source_tags.is_empty() { + body["source_tags"] = json!(source_tags); + } body } @@ -127,7 +259,9 @@ impl NetworkClient { guids: &[FunctionGUID], ) -> Option<WarpFile<'static>> { let query_functions_url = format!("{}/api/v1/functions/query", self.server_url); - let payload = Self::query_functions_body(target, source, guids); + // TODO: Allow for source tags? We really only need this in query_functions_source as that + // TODO: is what prevents a undesired source from being "known" to the container. + let payload = Self::query_functions_body(target, source, &[], guids); // Make the POST request let response = self @@ -154,11 +288,12 @@ impl NetworkClient { pub fn query_functions_source( &self, target: Option<NetworkTargetId>, + tags: &[SourceTag], guids: &[FunctionGUID], ) -> Option<HashMap<SourceId, Vec<FunctionGUID>>> { let query_functions_source_url = format!("{}/api/v1/functions/query/source", self.server_url); - let payload = Self::query_functions_body(target, None, guids); + let payload = Self::query_functions_body(target, None, tags, guids); // Make the POST request let response = self @@ -194,20 +329,24 @@ impl NetworkClient { /// NOTE: **THIS IS BLOCKING** /// /// Route: `api/v1/files/{source}` - pub fn push_file(&self, source_id: SourceId, file: &WarpFile) -> bool { - let push_file_url = format!("{}/api/v1/files/{}", self.server_url, source_id.to_string()); + pub fn push_file(&self, source_id: SourceId, file: &WarpFile, name: &str) -> bool { + let push_file_url = format!("{}/api/v1/files", self.server_url); // Convert WarpFile to bytes let file_bytes = file.to_bytes(); - // Create the form part with the file - let form = reqwest::blocking::multipart::Form::new().part( - "file", - reqwest::blocking::multipart::Part::bytes(file_bytes) - .file_name("data.warp") - .mime_str("application/octet-stream") - .unwrap(), - ); + let Ok(file_part) = reqwest::blocking::multipart::Part::bytes(file_bytes) + .file_name("data.warp") + .mime_str("application/octet-stream") + else { + log::error!("Failed to create file part"); + return false; + }; + + let form = reqwest::blocking::multipart::Form::new() + .part("file", file_part) + .text("name", name.to_string()) + .text("source", source_id.to_string()); // Send the request match self.client.post(&push_file_url).multipart(form).send() { @@ -225,4 +364,226 @@ impl NetworkClient { } } } + + pub fn function_data(&self, id: i32) -> Option<Function> { + let function_data_url = format!("{}/api/v1/functions/{}/data", self.server_url, id); + let response = self.client.get(&function_data_url).send().ok()?; + if !response.status().is_success() { + log::error!( + "Failed to fetch function data for {}: {}", + id, + response.status() + ); + return None; + } + let bytes = response.bytes().ok()?; + Function::from_bytes(bytes.as_ref()) + } + + pub fn function_datas(&self, ids: &[i32]) -> Option<Vec<Function>> { + if ids.is_empty() { + return Some(Vec::new()); + } + let function_data_url = format!("{}/api/v1/functions/data", self.server_url); + let body = json!({ + "ids": ids, + }); + let response = self + .client + .post(&function_data_url) + .json(&body) + .send() + .ok()?; + if !response.status().is_success() { + log::error!("Failed to fetch function data: {}", response.status()); + return None; + } + let bytes = response.bytes().ok()?; + let file = WarpFile::from_bytes(bytes.as_ref())?; + let mut functions = Vec::with_capacity(ids.len()); + for chunk in file.chunks { + let ChunkKind::Signature(sc) = chunk.kind else { + continue; + }; + functions.extend(sc.functions()); + } + Some(functions) + } + + pub fn type_data(&self, guid: TypeGUID) -> Option<Type> { + let type_data_url = format!("{}/api/v1/types/{}/data", self.server_url, guid.to_string()); + let response = self.client.get(&type_data_url).send().ok()?; + if !response.status().is_success() { + log::error!( + "Failed to fetch type data for {}: {}", + guid.to_string(), + response.status() + ); + return None; + } + let bytes = response.bytes().ok()?; + Type::from_bytes(bytes.as_ref()) + } + + pub fn type_datas(&self, guids: &[TypeGUID]) -> Option<Vec<ComputedType>> { + if guids.is_empty() { + return Some(Vec::new()); + } + let type_data_url = format!("{}/api/v1/types/data", self.server_url); + let body = json!({ + "ids": guids.iter().map(|g| g.to_string()).collect::<Vec<_>>(), + }); + let response = self.client.post(&type_data_url).json(&body).send().ok()?; + if !response.status().is_success() { + log::error!("Failed to fetch type data: {}", response.status()); + return None; + } + let bytes = response.bytes().ok()?; + let file = WarpFile::from_bytes(bytes.as_ref())?; + let mut types = Vec::with_capacity(guids.len()); + for chunk in file.chunks { + let ChunkKind::Type(tc) = chunk.kind else { + continue; + }; + types.extend(tc.types()); + } + Some(types) + } + + pub fn search(&self, query: &ContainerSearchQuery) -> Option<ContainerSearchResponse> { + let search_url = format!("{}/api/v1/search", self.server_url); + + #[derive(serde::Serialize)] + struct SearchRequest<'a> { + #[serde(rename = "q")] + q: &'a str, + #[serde(skip_serializing_if = "Option::is_none")] + limit: Option<usize>, + #[serde(skip_serializing_if = "Option::is_none")] + offset: Option<usize>, + #[serde(rename = "source_id", skip_serializing_if = "Option::is_none")] + source_id: Option<String>, + #[serde(skip_serializing_if = "Option::is_none")] + source_tags: Option<Vec<SourceTag>>, + #[serde(skip_serializing_if = "Option::is_none")] + retrieve_data: Option<bool>, + } + + #[derive(serde::Deserialize)] + struct SearchResponse { + items: Vec<SearchItem>, + offset: usize, + total: usize, + } + + #[derive(serde::Deserialize)] + struct SearchItem { + id: String, + kind: String, + #[serde(default)] + name: Option<String>, + #[serde(default)] + source_id: Option<Uuid>, + #[serde(default)] + data: Option<Vec<u8>>, + } + + let source_id_str = query.source.map(|s| s.to_string()); + let request = SearchRequest { + q: &query.query, + limit: query.limit, + offset: query.offset, + source_id: source_id_str, + source_tags: match query.tags.is_empty() { + true => None, + false => Some(query.tags.clone()), + }, + // This must be passed to retrieve the function and type data. + retrieve_data: Some(true), + }; + + let resp = match self.client.get(search_url).query(&request).send() { + Ok(r) => r, + Err(err) => { + log::error!("Failed to send search request: {}", err); + return None; + } + }; + + let Ok(parsed) = resp.json::<SearchResponse>() else { + log::error!("Failed to parse search response"); + return None; + }; + + // TODO: This is quite scuffed, but it works for now. (Mostly just that it looks bad and queries a lot) + // TODO: Here I think would be a good place to sort it so sources always come first. + // TODO: Users searching will want to get to the source first, likely to whitelist or blacklist. + let mut items = Vec::with_capacity(parsed.items.len()); + for item in parsed.items { + let Some(source_uuid) = item.source_id else { + // Currently not interested in items without a source id. + // Things like symbols do not have a source id. + continue; + }; + + let kind = match item.kind.as_str() { + "function" => { + let Some(data) = &item.data else { + log::warn!( + "Function item {} has no data from network, skipping...", + item.id + ); + continue; + }; + let Some(func) = Function::from_bytes(&data) else { + log::warn!( + "Function item {} has invalid data from network, skipping...", + item.id + ); + continue; + }; + ContainerSearchItemKind::Function(func) + } + "source" => ContainerSearchItemKind::Source { + path: match item.name { + None => { + log::warn!("Source item {} has no name", item.id); + continue; + } + Some(name) => SourcePath(format!("{}/{}", self.server_url, name).into()), + }, + id: SourceId(source_uuid), + }, + "type" => { + let Some(data) = &item.data else { + log::warn!( + "Type item {} has no data from network, skipping...", + item.id + ); + continue; + }; + let Some(ty) = Type::from_bytes(&data) else { + log::warn!( + "Type item {} has invalid data from network, skipping...", + item.id + ); + continue; + }; + ContainerSearchItemKind::Type(ty) + } + _ => continue, + }; + + items.push(ContainerSearchItem { + source: SourceId(source_uuid), + kind, + }); + } + + Some(ContainerSearchResponse { + items, + total: parsed.total, + offset: parsed.offset, + }) + } } |
