diff options
Diffstat (limited to 'plugins/warp/src/processor.rs')
| -rw-r--r-- | plugins/warp/src/processor.rs | 829 |
1 files changed, 829 insertions, 0 deletions
diff --git a/plugins/warp/src/processor.rs b/plugins/warp/src/processor.rs new file mode 100644 index 00000000..4b973753 --- /dev/null +++ b/plugins/warp/src/processor.rs @@ -0,0 +1,829 @@ +use std::collections::{HashMap, HashSet}; +use std::fs::File; +use std::path::{Path, PathBuf}; +use std::sync::atomic::Ordering::Relaxed; +use std::sync::atomic::{AtomicBool, AtomicUsize}; +use std::sync::Arc; +use std::time::{Duration, Instant}; + +use ar::Archive; +use dashmap::DashMap; +use rayon::iter::IntoParallelIterator; +use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; +use regex::Regex; +use serde_json::{json, Value}; +use tempdir::TempDir; +use thiserror::Error; +use walkdir::WalkDir; + +use binaryninja::background_task::BackgroundTask; +use binaryninja::binary_view::{BinaryView, BinaryViewExt}; +use binaryninja::function::Function as BNFunction; +use binaryninja::interaction::{Form, FormInputField}; +use binaryninja::project::file::ProjectFile; +use binaryninja::project::Project; +use binaryninja::rc::{Guard, Ref}; + +use warp::chunk::{Chunk, ChunkKind, CompressionType}; +use warp::r#type::chunk::TypeChunk; +use warp::signature::chunk::SignatureChunk; +use warp::signature::function::Function; +use warp::target::Target; +use warp::{WarpFile, WarpFileHeader}; + +use crate::cache::cached_type_references; +use crate::convert::platform_to_target; +use crate::{build_function, INCLUDE_TAG_ICON, INCLUDE_TAG_NAME}; + +#[derive(Error, Debug)] +pub enum ProcessingError { + #[error("Failed to open archive: {0}")] + ArchiveOpen(std::io::Error), + + #[error("Failed to read archive entry: {0}")] + ArchiveRead(std::io::Error), + + #[error("Binary view load error: {0}")] + BinaryViewLoad(PathBuf), + + #[error("Existing data load error: {0}")] + ExistingDataLoad(PathBuf), + + #[error("Temporary directory creation failed: {0}")] + TempDirCreation(std::io::Error), + + #[error("Failed to read file: {0}")] + FileRead(std::io::Error), + + #[error("Failed to create chunk, possibly too large")] + ChunkCreationFailed, + + #[error("Failed to retrieve path to project file: {0:?}")] + NoPathToProjectFile(Ref<ProjectFile>), + + #[error("Processing state has been poisoned")] + StatePoisoned, + + #[error("Processing has been cancelled")] + Cancelled, +} + +#[derive(Debug, Clone, Default)] +pub struct FileFilterField; + +impl FileFilterField { + pub fn to_field() -> FormInputField { + FormInputField::TextLine { + prompt: "File Filter".to_string(), + default: None, + value: None, + } + } + + pub fn from_form(form: &Form) -> Option<Regex> { + let field = form.get_field_with_name("File Filter")?; + let field_value = field.try_value_string()?; + + // TODO: This is pretty absurd but whatever. + let pattern = if field_value.contains(['*', '.', '[', '(']) { + // Assume it's a regex if it contains meta-characters. + field_value + } else { + // Treat it as a substring + format!(".*{}.*", regex::escape(&field_value)) + }; + + Regex::new(&pattern).ok() + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Default)] +pub enum FileDataKindField { + Symbols, + Signatures, + Types, + #[default] + All, +} + +impl FileDataKindField { + pub fn to_field(&self) -> FormInputField { + FormInputField::Choice { + prompt: "File Data".to_string(), + choices: vec![ + "Symbols".to_string(), + "Signatures".to_string(), + "Types".to_string(), + "All".to_string(), + ], + default: Some(match self { + Self::Symbols => 0, + Self::Signatures => 1, + Self::Types => 2, + Self::All => 3, + }), + value: 0, + } + } + + pub fn from_form(form: &Form) -> Option<Self> { + let field = form.get_field_with_name("File Data")?; + let field_value = field.try_value_index()?; + match field_value { + 3 => Some(Self::All), + 2 => Some(Self::Types), + 1 => Some(Self::Signatures), + 0 => Some(Self::Symbols), + _ => None, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Default)] +pub enum IncludedFunctionsField { + Selected, + #[default] + Annotated, + All, +} + +impl IncludedFunctionsField { + pub fn to_field(&self) -> FormInputField { + // If the user has selected any functions, change the default value of the included functions field. + FormInputField::Choice { + prompt: "Included Functions".to_string(), + choices: vec![ + format!("Selected {}", INCLUDE_TAG_ICON), + "Annotated".to_string(), + "All".to_string(), + ], + default: Some(match self { + Self::Selected => 0, + Self::Annotated => 1, + Self::All => 2, + }), + value: 0, + } + } + + pub fn from_form(form: &Form) -> Option<Self> { + let field = form.get_field_with_name("Included Functions")?; + let field_value = field.try_value_index()?; + match field_value { + 2 => Some(Self::All), + 1 => Some(Self::Annotated), + 0 => Some(Self::Selected), + _ => None, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Default)] +pub enum SaveReportToDiskField { + No, + #[default] + Yes, +} + +impl SaveReportToDiskField { + pub fn to_field(&self) -> FormInputField { + FormInputField::Checkbox { + prompt: "Save Report to Disk".to_string(), + default: Some(true), + value: false, + } + } + + pub fn from_form(form: &Form) -> Option<Self> { + let field = form.get_field_with_name("Save Report to Disk")?; + let field_value = field.try_value_int()?; + match field_value { + 1 => Some(Self::Yes), + _ => Some(Self::No), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Default)] +pub enum CompressionTypeField { + None, + #[default] + Zstd, +} + +impl CompressionTypeField { + pub fn to_field(&self) -> FormInputField { + FormInputField::Choice { + prompt: "Compression Type".to_string(), + choices: vec!["None".to_string(), "Zstd".to_string()], + default: Some(match self { + Self::None => 0, + Self::Zstd => 1, + }), + value: 0, + } + } + + pub fn from_form(form: &Form) -> Option<Self> { + let field = form.get_field_with_name("Compression Type")?; + let field_value = field.try_value_index()?; + match field_value { + 1 => Some(Self::Zstd), + _ => Some(Self::None), + } + } +} + +impl From<CompressionTypeField> for CompressionType { + fn from(field: CompressionTypeField) -> Self { + match field { + CompressionTypeField::None => CompressionType::None, + CompressionTypeField::Zstd => CompressionType::Zstd, + } + } +} + +pub fn new_processing_state_background_thread( + task: Ref<BackgroundTask>, + state: Arc<ProcessingState>, +) { + std::thread::spawn(move || { + let start = Instant::now(); + while !task.is_finished() { + std::thread::sleep(Duration::from_millis(100)); + // Check if the user wants to cancel the processing. + if task.is_cancelled() { + state.cancel(); + } + + let total = state.total_files(); + let processed = state.files_with_state(ProcessingFileState::Processed); + let unprocessed = state.files_with_state(ProcessingFileState::Unprocessed); + let analyzing = state.files_with_state(ProcessingFileState::Analyzing); + let processing = state.files_with_state(ProcessingFileState::Processing); + let completion = (processed as f64 / total as f64) * 100.0; + let elapsed = start.elapsed().as_secs_f32(); + let text = format!( + "Processing {} files... {{{}|{}|{}|{}}} ({:.2}%) [{:.2}s]", + total, unprocessed, analyzing, processing, processed, completion, elapsed + ); + task.set_progress_text(&text); + } + }); +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum ProcessingFileState { + /// File is yet to be processed. + Unprocessed, + /// File is being analyzed by Binary Ninja. + Analyzing, + /// File is currently generating WARP data. + /// TODO: (AtomicUsize) for the total and done functions, we can then write to it with Relaxed when processing. + Processing, + /// File is done being processed. + Processed, +} + +#[derive(Debug, Default)] +pub struct ProcessingState { + pub cancelled: AtomicBool, + pub files: DashMap<PathBuf, ProcessingFileState>, + pub total_functions: AtomicUsize, +} + +impl ProcessingState { + pub fn is_cancelled(&self) -> bool { + self.cancelled.load(Relaxed) + } + + pub fn cancel(&self) { + self.cancelled.store(true, Relaxed) + } + + pub fn increment_functions(&self) { + self.total_functions.fetch_add(1, Relaxed); + } + + pub fn total_files(&self) -> usize { + self.files.len() + } + + pub fn files_with_state(&self, state: ProcessingFileState) -> usize { + self.files.iter().filter(|f| *f.value() == state).count() + } + + pub fn set_file_state(&self, path: PathBuf, state: ProcessingFileState) { + self.files.insert(path, state); + } +} + +/// Create a new [`WarpFile`] from files, projects, and directories. +#[derive(Debug, Clone)] +pub struct WarpFileProcessor { + /// The Binary Ninja settings to use when analyzing the binaries. + analysis_settings: Value, + /// For any function without an LLIL, request analysis to be run, waiting for analysis to + /// complete to include in the analysis. + request_analysis: bool, + // TODO: Project cache path, so we save to a project instead of some temp path. + // TODO: Databases will require regenerating LLIL in some cases, so we must support generating the LLIL. + /// The path to a folder to intake and output analysis artifacts. + cache_path: Option<PathBuf>, + file_data: FileDataKindField, + included_functions: IncludedFunctionsField, + compression_type: CompressionTypeField, + /// Regex pattern used to filter out files. + file_filter: Option<Regex>, + /// Processor state, this is shareable between threads, so the processor and the consumer can + /// read / write to the state, use this if you want to show a progress indicator. + state: Arc<ProcessingState>, +} + +impl WarpFileProcessor { + pub fn new() -> Self { + Self { + analysis_settings: json!({ + "analysis.linearSweep.autorun": false, + "analysis.signatureMatcher.autorun": false, + "analysis.mode": "full", + // Disable warp when opening views. + "analysis.warp.guid": false, + "analysis.warp.matcher": false, + "analysis.warp.apply": false, + }), + request_analysis: true, + cache_path: None, + file_data: Default::default(), + included_functions: Default::default(), + compression_type: Default::default(), + file_filter: None, + state: Arc::new(ProcessingState::default()), + } + } + + /// Retrieve a thread-safe shared reference to the [`ProcessingState`]. + pub fn state(&self) -> Arc<ProcessingState> { + self.state.clone() + } + + pub fn with_analysis_settings(mut self, analysis_settings: Value) -> Self { + self.analysis_settings = analysis_settings; + self + } + + pub fn with_request_analysis(mut self, request_analysis: bool) -> Self { + self.request_analysis = request_analysis; + self + } + + pub fn with_cache_path(mut self, cache_path: PathBuf) -> Self { + self.cache_path = Some(cache_path); + self + } + + pub fn with_file_data(mut self, file_data: FileDataKindField) -> Self { + self.file_data = file_data; + self + } + + pub fn with_included_functions(mut self, included_functions: IncludedFunctionsField) -> Self { + self.included_functions = included_functions; + self + } + + pub fn with_compression_type(mut self, compression_type: CompressionTypeField) -> Self { + self.compression_type = compression_type; + self + } + + pub fn with_file_filter(mut self, file_filter: Regex) -> Self { + self.file_filter = Some(file_filter); + self + } + + pub fn file_filter(&self, path: &Path) -> bool { + match (&self.file_filter, path.to_str()) { + (Some(filter), Some(path)) => filter.is_match(path), + _ => true, + } + } + + /// Place a call to this in places to interrupt when canceled. + fn check_cancelled(&self) -> Result<(), ProcessingError> { + match self.state.is_cancelled() { + true => Err(ProcessingError::Cancelled), + false => Ok(()), + } + } + + pub fn process(&self, path: PathBuf) -> Result<WarpFile<'static>, ProcessingError> { + match path.extension() { + Some(ext) if ext == "a" || ext == "lib" || ext == "rlib" => self.process_archive(path), + Some(ext) if ext == "warp" => self.process_warp_file(path), + _ if path.is_dir() => self.process_directory(&path), + // TODO: process_database? + _ => self.process_file(path), + } + } + + pub fn process_project(&self, project: &Project) -> Result<WarpFile<'static>, ProcessingError> { + let filter_project_file = |file: &Guard<ProjectFile>| { + let path = project_file_path(file); + self.file_filter(&path) + }; + + let files: Vec<_> = project + .files() + .iter() + .filter(filter_project_file) + .map(|f| f.to_owned()) + .collect(); + + // Inform the state of the new unprocessed project files. + for project_file in &files { + // NOTE: We use the on disk path here because the downstream file state uses that. + if let Some(path) = project_file.path_on_disk() { + self.state + .set_file_state(path, ProcessingFileState::Unprocessed); + } + } + + let unmerged_files: Result<Vec<_>, _> = files + .par_iter() + .map(|file| { + self.check_cancelled()?; + self.process_project_file(file) + }) + .filter_map(|res| match res { + Ok(result) => Some(Ok(result)), + Err(ProcessingError::Cancelled) => Some(Err(ProcessingError::Cancelled)), + Err(e) => { + log::error!("Project file processing error: {:?}", e); + None + } + }) + .collect(); + + let unmerged_chunks: Vec<_> = unmerged_files? + .iter() + .flat_map(|f| f.chunks.clone()) + .collect(); + let merged_chunks = Chunk::merge(&unmerged_chunks, self.compression_type.into()); + Ok(WarpFile::new(WarpFileHeader::new(), merged_chunks)) + } + + pub fn process_project_file( + &self, + project_file: &ProjectFile, + ) -> Result<WarpFile<'static>, ProcessingError> { + let file_name = project_file.name(); + let extension = file_name.split('.').last(); + let path = project_file + .path_on_disk() + .ok_or_else(|| ProcessingError::NoPathToProjectFile(project_file.to_owned()))?; + match extension { + Some(ext) if ext == "a" || ext == "lib" || ext == "rlib" => self.process_archive(path), + Some("warp") => self.process_warp_file(path), + _ => self.process_file(path), + } + } + + pub fn process_warp_file(&self, path: PathBuf) -> Result<WarpFile<'static>, ProcessingError> { + let contents = std::fs::read(&path).map_err(ProcessingError::FileRead)?; + let file = WarpFile::from_owned_bytes(contents) + .ok_or(ProcessingError::ExistingDataLoad(path.clone())); + + // Inform the state of the new processed warp file. + self.state + .set_file_state(path, ProcessingFileState::Processed); + + file + } + + pub fn process_file(&self, path: PathBuf) -> Result<WarpFile<'static>, ProcessingError> { + // Inform the state of the new analyzing file. + self.state + .set_file_state(path.clone(), ProcessingFileState::Analyzing); + + // Load the view, either from the cache or from the given path. + // Using the cache can speed up the processing, especially for larger binaries. + let settings_str = self.analysis_settings.to_string(); + let view = match &self.cache_path { + Some(cache_path) => { + // Processor is caching analysis, try and find our file in the cache. + let file_cache_path = cache_path + .join(path.file_name().unwrap()) + .with_extension("bndb"); + if file_cache_path.exists() { + // TODO: Update analysis and wait option + log::debug!("Analysis database found in cache: {:?}", file_cache_path); + binaryninja::load_with_options(&file_cache_path, true, Some(settings_str)) + } else { + log::debug!("No database found in cache: {:?}", file_cache_path); + binaryninja::load_with_options(&path, true, Some(settings_str)) + } + } + None => { + // Processor is not caching analysis + binaryninja::load_with_options(&path, true, Some(settings_str)) + } + } + .ok_or(ProcessingError::BinaryViewLoad(path.clone()))?; + + // Analysis is complete, if needed, save the database to cache. + if let Some(cache_path) = &self.cache_path { + // Before we process the view we should cache the analysis database. + // Only cache the analysis database if there has been a change. + // TODO: What if there is multiple paths with the same name? + // TODO: We need more context than just the path, likely we need a processing path stack. + let file_cache_path = cache_path + .join(path.file_name().unwrap()) + .with_extension("bndb"); + // TODO: We should also update the cache if analysis has changed! + if !view.file().is_database_backed() { + // Update the cache. + log::debug!("Saving analysis database to {:?}", file_cache_path); + if !view.file().create_database(&file_cache_path) { + // TODO: We might want to error here... + log::warn!("Failed to save analysis database to {:?}", file_cache_path); + } + } else { + log::debug!( + "Analysis database unchanged, skipping save to {:?}", + file_cache_path + ); + } + } + + // Process the view + let warp_file = self.process_view(path, &view); + // Close the view manually, see comment in [`BinaryView`]. + view.file().close(); + warp_file + } + + pub fn process_directory(&self, path: &Path) -> Result<WarpFile<'static>, ProcessingError> { + // Collect all files in the directory + let files = WalkDir::new(path) + .into_iter() + .filter_map(|e| { + let path = e.ok()?.into_path(); + if path.is_file() && self.file_filter(&path) { + Some(path) + } else { + None + } + }) + .collect::<Vec<_>>(); + + // Inform the state of the new unprocessed files. + for entry_file in &files { + self.state + .set_file_state(entry_file.clone(), ProcessingFileState::Unprocessed); + } + + // Process all the files. + let unmerged_files: Result<Vec<_>, _> = files + .into_par_iter() + .inspect(|path| log::debug!("Processing file: {:?}", path)) + .map(|path| { + self.check_cancelled()?; + self.process(path) + }) + .filter_map(|res| match res { + Ok(result) => Some(Ok(result)), + Err(ProcessingError::Cancelled) => Some(Err(ProcessingError::Cancelled)), + Err(e) => { + log::error!("Directory file processing error: {:?}", e); + None + } + }) + .collect(); + + let unmerged_chunks: Vec<_> = unmerged_files? + .iter() + .flat_map(|f| f.chunks.clone()) + .collect(); + let merged_chunks = Chunk::merge(&unmerged_chunks, self.compression_type.into()); + Ok(WarpFile::new(WarpFileHeader::new(), merged_chunks)) + } + + pub fn process_archive(&self, path: PathBuf) -> Result<WarpFile<'static>, ProcessingError> { + // Open the archive. + let archive_file = File::open(&path).map_err(ProcessingError::ArchiveOpen)?; + let mut archive = Archive::new(archive_file); + + // Create a temp directory to store the archive entries. + let temp_dir = TempDir::new("tmp_archive").map_err(ProcessingError::TempDirCreation)?; + + // TODO: Use the file_filter? We would need to normalize the path then. + // Iterate through the entries in the ar file and make a temp dir with them + let mut entry_files: HashSet<PathBuf> = HashSet::new(); + while let Some(entry) = archive.next_entry() { + let mut entry = entry.map_err(ProcessingError::ArchiveRead)?; + // NOTE: The entry name here may resemble a full path, on unix this is fine, but + // on Windows this will prevent a file from being created, so we "normalize" the file name. + let name = String::from_utf8_lossy(entry.header().identifier()).to_string(); + // Normalize file name for Windows compatibility + let normalized_name = name + .replace(':', "_") + .replace('/', "_") + .replace('\\', "_") + .split_whitespace() + .collect::<Vec<_>>() + .join("_"); + let output_path = temp_dir.path().join(&normalized_name); + if !entry_files.contains(&output_path) { + let mut output_file = + File::create(&output_path).map_err(ProcessingError::TempDirCreation)?; + std::io::copy(&mut entry, &mut output_file).map_err(ProcessingError::FileRead)?; + entry_files.insert(output_path); + } else { + log::debug!("Skipping already inserted entry: {}", normalized_name); + } + } + + // Inform the state of the new unprocessed files. + for entry_file in &entry_files { + self.state + .set_file_state(entry_file.clone(), ProcessingFileState::Unprocessed); + } + + // TODO: Par iter? + // Process all the entries. + let unmerged_files: Result<Vec<_>, _> = entry_files + .into_par_iter() + .inspect(|path| log::debug!("Processing entry: {:?}", path)) + .map(|path| { + self.check_cancelled()?; + self.process_file(path) + }) + .filter_map(|res| match res { + Ok(result) => Some(Ok(result)), + Err(ProcessingError::Cancelled) => Some(Err(ProcessingError::Cancelled)), + Err(e) => { + log::error!("Archive file processing error: {:?}", e); + None + } + }) + .collect(); + + let unmerged_chunks: Vec<_> = unmerged_files? + .iter() + .flat_map(|f| f.chunks.clone()) + .collect(); + let merged_chunks = Chunk::merge(&unmerged_chunks, self.compression_type.into()); + Ok(WarpFile::new(WarpFileHeader::new(), merged_chunks)) + } + + pub fn process_view( + &self, + path: PathBuf, + view: &BinaryView, + ) -> Result<WarpFile<'static>, ProcessingError> { + self.state + .set_file_state(path.clone(), ProcessingFileState::Processing); + + let mut chunks = Vec::new(); + if self.file_data != FileDataKindField::Types { + let mut signature_chunks = self.create_signature_chunks(view)?; + for (target, signature_chunk) in signature_chunks.drain() { + let chunk = Chunk::new_with_target( + ChunkKind::Signature(signature_chunk), + self.compression_type.into(), + target, + ); + chunks.push(chunk) + } + } + + if self.file_data != FileDataKindField::Signatures { + chunks.push(Chunk::new( + ChunkKind::Type(self.create_type_chunk(view)?), + self.compression_type.into(), + )); + } + + self.state + .set_file_state(path, ProcessingFileState::Processed); + + Ok(WarpFile::new(WarpFileHeader::new(), chunks)) + } + + /// Create signature chunks for each unique [`Target`]. + /// + /// A [`Target`] in Binary Ninja is a [`Platform`], so we just fill in that information. + pub fn create_signature_chunks( + &self, + view: &BinaryView, + ) -> Result<HashMap<Target, SignatureChunk<'static>>, ProcessingError> { + let is_function_named = |f: &Guard<BNFunction>| { + self.included_functions == IncludedFunctionsField::All + || view.symbol_by_address(f.start()).is_some() + || f.has_user_annotations() + }; + let is_function_tagged = |f: &Guard<BNFunction>| { + self.included_functions != IncludedFunctionsField::Selected + || !f.function_tags(None, Some(INCLUDE_TAG_NAME)).is_empty() + }; + // TODO: is_function_blacklisted (use tag) + + // TODO: Move this background task to use the ProcessingState. + let view_functions = view.functions(); + let total_functions = view_functions.len(); + let done_functions = AtomicUsize::default(); + let background_task = BackgroundTask::new( + &format!("Generating signatures... ({}/{})", 0, total_functions), + true, + ); + + // Create all of the "built" functions, for the chunk. + // NOTE: This does a bit of filtering to remove undesired functions, look at this if + // a desired function is not in the created chunk. + // TODO: Make this interruptable. with background_task.is_cancelled. + let start = Instant::now(); + let built_functions: DashMap<Target, Vec<Function>> = view_functions + .par_iter() + .inspect(|_| { + done_functions.fetch_add(1, Relaxed); + background_task.set_progress_text(&format!( + "Generating signatures... ({}/{}) [{}s]", + done_functions.load(Relaxed), + total_functions, + start.elapsed().as_secs_f32() + )) + }) + .filter(is_function_tagged) + .filter(is_function_named) + .filter(|f| !f.analysis_skipped()) + .filter_map(|func| { + let lifted_il = func.lifted_il().ok()?; + let target = platform_to_target(&func.platform()); + let mut built_function = build_function(&func, &lifted_il); + // User asked to only save symbols, so we will remove the function type. + if self.file_data == FileDataKindField::Symbols { + built_function.ty = None; + } + Some((target, built_function)) + }) + .fold( + DashMap::new, + |acc: DashMap<Target, Vec<Function>>, (target, function)| { + acc.entry(target).or_default().push(function); + acc + }, + ) + .reduce(DashMap::new, |acc, other| { + other.into_iter().for_each(|(key, value)| { + acc.entry(key).or_default().extend(value); + }); + acc + }); + + let chunks: Result<HashMap<Target, SignatureChunk<'static>>, ProcessingError> = + built_functions + .into_iter() + .map(|(target, functions)| { + Ok(( + target, + SignatureChunk::new(&functions) + .ok_or(ProcessingError::ChunkCreationFailed)?, + )) + }) + .collect(); + + background_task.finish(); + chunks + } + + // TODO: Add a background task here. + pub fn create_type_chunk( + &self, + view: &BinaryView, + ) -> Result<TypeChunk<'static>, ProcessingError> { + let mut referenced_types = Vec::new(); + if let Some(ref_ty_cache) = cached_type_references(view) { + referenced_types = ref_ty_cache + .cache + .iter() + .filter_map(|t| t.to_owned()) + .collect::<Vec<_>>(); + } + TypeChunk::new_with_computed(&referenced_types).ok_or(ProcessingError::ChunkCreationFailed) + } +} + +fn project_file_path(file: &ProjectFile) -> PathBuf { + // Recurse up the folders to build a string like /foldera/folderb/myfile + let mut path = PathBuf::new(); + // Add file name + path.push(file.name()); + // Recursively add parent folder names + let mut current = file.folder(); + while let Some(folder) = current { + path = PathBuf::from(folder.name()).join(path); + current = folder.parent(); + } + path +} |
