1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
|
use binaryninja::collaboration::RemoteFile;
use binaryninja::project::Project;
use binaryninja::project::file::ProjectFile;
use binaryninja::project::folder::ProjectFolder;
use binaryninja::rc::Ref;
use bntl_utils::url::{BnParsedUrl, BnResource};
use std::fmt::Display;
use std::path::PathBuf;
use std::str::FromStr;
use thiserror::Error;
#[derive(Debug)]
pub enum ResolvedInput {
Path(PathBuf),
Project(Ref<Project>),
ProjectFolder(Ref<ProjectFolder>),
ProjectFile(Ref<ProjectFile>),
}
#[derive(Error, Debug)]
pub enum InputResolveError {
#[error("Resource resolution failed: {0}")]
ResourceError(#[from] bntl_utils::url::BnResourceError),
#[error("Collaboration API error: {0}")]
CollaborationError(String),
#[error("Download failed for {url}: status {status}")]
DownloadFailed { url: String, status: u16 },
#[error("Download provider error: {0}")]
DownloadProviderError(String),
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("Environment error: {0}")]
EnvError(String),
}
/// An input to the CLI to locate a "resource", such as a file or directory.
#[derive(Debug, Clone)]
pub enum Input {
/// A URL which references a Binary Ninja resource, such as a remote project or file.
ParsedUrl(BnParsedUrl),
/// A local filesystem path pointing to a file or directory.
LocalPath(PathBuf),
}
impl Input {
/// Attempt to acquire a path from this input, this can download files over the network and
/// is meant to be called when the file contents are desired.
pub fn resolve(&self) -> Result<ResolvedInput, InputResolveError> {
let try_download_file = |file: &RemoteFile| -> Result<(), InputResolveError> {
if !file.core_file().unwrap().exists_on_disk() {
let _span =
tracing::info_span!("Downloading project file", file = %file.name()).entered();
file.download().map_err(|_| {
InputResolveError::CollaborationError("Failed to download project file".into())
})?;
}
Ok(())
};
match self {
Input::ParsedUrl(url) => match url.to_resource()? {
BnResource::RemoteProject(project) => {
let files = project.files().map_err(|_| {
InputResolveError::CollaborationError("Failed to get files".into())
})?;
for file in &files {
try_download_file(&file)?;
}
let core = project.core_project().map_err(|_| {
InputResolveError::CollaborationError("Missing core project".into())
})?;
Ok(ResolvedInput::Project(core))
}
BnResource::RemoteProjectFile(file) => {
try_download_file(&file)?;
let core = file.core_file().expect("Missing core file");
Ok(ResolvedInput::ProjectFile(core))
}
BnResource::RemoteProjectFolder(folder) => {
let project = folder.project().map_err(|_| {
InputResolveError::CollaborationError("Failed to get project".into())
})?;
let files = project.files().map_err(|_| {
InputResolveError::CollaborationError("Failed to get files".into())
})?;
for file in &files {
if let Some(file_folder) = file.folder().ok().flatten() {
if file_folder == folder {
try_download_file(&file)?;
}
}
}
let core = folder.core_folder().map_err(|_| {
InputResolveError::CollaborationError("Missing core folder".into())
})?;
Ok(ResolvedInput::ProjectFolder(core))
}
BnResource::RemoteFile(url) => {
let safe_name = url.to_string().replace(['/', ':', '?'], "_");
let cached_file_path = std::env::temp_dir().join(safe_name);
if cached_file_path.exists() {
return Ok(ResolvedInput::Path(cached_file_path));
}
let download_provider = binaryninja::download::DownloadProvider::try_default()
.expect("Failed to get default download provider");
let mut instance = download_provider
.create_instance()
.expect("Failed to create download provider instance");
let _span =
tracing::info_span!("Downloading remote file", url = %url).entered();
let response = instance
.get(&url.to_string(), Vec::new())
.map_err(|e| InputResolveError::DownloadProviderError(e.to_string()))?;
if response.is_success() {
std::fs::write(&cached_file_path, response.data)?;
Ok(ResolvedInput::Path(cached_file_path))
} else {
Err(InputResolveError::DownloadFailed {
url: url.to_string(),
status: response.status_code,
})
}
}
BnResource::LocalFile(path) => Ok(ResolvedInput::Path(path.clone())),
},
Input::LocalPath(path) => Ok(ResolvedInput::Path(path.clone())),
}
}
}
impl FromStr for Input {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
// Try to parse as a Binary Ninja URL
if s.starts_with("binaryninja:") {
let url = BnParsedUrl::parse(s).map_err(|e| format!("URL Parse Error: {}", e))?;
return Ok(Input::ParsedUrl(url));
}
let path = PathBuf::from(s);
Ok(Input::LocalPath(path))
}
}
impl Display for Input {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Input::ParsedUrl(url) => write!(f, "{}", url),
Input::LocalPath(path) => write!(f, "{}", path.display()),
}
}
}
|