use std::{
fmt,
path::{Path, PathBuf},
time::Duration,
};
use serde::{Deserialize, Serialize};
use crate::{Error, Result, TaskMonitorOptions, TaskMonitorReport};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProgramPath {
folder: String,
name: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LoaderArgument {
pub name: String,
pub value: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProgramLoadOptions {
pub binary: PathBuf,
pub path: ProgramPath,
pub loader: Option<String>,
pub language: Option<String>,
pub compiler: Option<String>,
pub loader_args: Vec<LoaderArgument>,
pub monitor: TaskMonitorOptions,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProgramOpenOptions {
pub path: ProgramPath,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ProgramLoadInfo {
pub opened_existing: bool,
pub project_path: String,
pub folder: String,
pub name: String,
#[serde(default)]
pub loader: Option<String>,
pub language: String,
pub compiler: String,
pub monitor: TaskMonitorReport,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AnalysisMode {
Skip,
IfNeeded,
Force,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AnalysisOptions {
pub mode: AnalysisMode,
pub monitor: TaskMonitorOptions,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AnalysisReport {
pub mode: AnalysisMode,
pub analyzed: bool,
pub already_analyzed: bool,
pub message_log: String,
pub monitor: TaskMonitorReport,
}
impl ProgramPath {
pub fn root(name: impl Into<String>) -> Result<Self> {
Self::new("/", name)
}
pub fn new(folder: impl Into<String>, name: impl Into<String>) -> Result<Self> {
let folder = normalize_folder(folder.into())?;
let name = name.into();
validate_name(&name)?;
Ok(Self { folder, name })
}
pub fn from_project_path(path: impl AsRef<str>) -> Result<Self> {
let path = path.as_ref();
if !path.starts_with('/') {
return Err(Error::invalid_input("program path must start with /"));
}
if path.starts_with("//") {
return Err(Error::invalid_input("program path must not start with //"));
}
let Some((folder, name)) = path.rsplit_once('/') else {
return Err(Error::invalid_input(
"program path must include a program name",
));
};
let folder = if folder.is_empty() { "/" } else { folder };
Self::new(folder, name)
}
pub fn folder(&self) -> &str {
&self.folder
}
pub fn name(&self) -> &str {
&self.name
}
pub fn as_project_path(&self) -> String {
if self.folder == "/" {
format!("/{}", self.name)
} else {
format!("{}/{}", self.folder, self.name)
}
}
}
impl fmt::Display for ProgramPath {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.as_project_path())
}
}
impl ProgramLoadOptions {
pub fn new(binary: impl Into<PathBuf>) -> Result<Self> {
let binary = binary.into();
let name = file_name(&binary)?;
Ok(Self {
binary,
path: ProgramPath::root(name)?,
loader: None,
language: None,
compiler: None,
loader_args: Vec::new(),
monitor: TaskMonitorOptions::none(),
})
}
pub fn with_path(mut self, path: ProgramPath) -> Self {
self.path = path;
self
}
pub fn with_loader(mut self, loader: impl Into<String>) -> Self {
self.loader = Some(loader.into());
self
}
pub fn with_language(mut self, language: impl Into<String>) -> Self {
self.language = Some(language.into());
self
}
pub fn with_compiler(mut self, compiler: impl Into<String>) -> Self {
self.compiler = Some(compiler.into());
self
}
pub fn with_loader_arg(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.loader_args.push(LoaderArgument {
name: name.into(),
value: value.into(),
});
self
}
pub fn with_monitor(mut self, monitor: TaskMonitorOptions) -> Self {
self.monitor = monitor;
self
}
pub fn with_timeout(mut self, timeout: Duration) -> Result<Self> {
self.monitor = TaskMonitorOptions::timeout(timeout)?;
Ok(self)
}
}
impl ProgramOpenOptions {
pub fn new(path: ProgramPath) -> Self {
Self { path }
}
}
impl AnalysisOptions {
pub fn skip() -> Self {
Self {
mode: AnalysisMode::Skip,
monitor: TaskMonitorOptions::none(),
}
}
pub fn if_needed() -> Self {
Self {
mode: AnalysisMode::IfNeeded,
monitor: TaskMonitorOptions::none(),
}
}
pub fn force() -> Self {
Self {
mode: AnalysisMode::Force,
monitor: TaskMonitorOptions::none(),
}
}
pub fn with_monitor(mut self, monitor: TaskMonitorOptions) -> Self {
self.monitor = monitor;
self
}
pub fn with_timeout(mut self, timeout: Duration) -> Result<Self> {
self.monitor = TaskMonitorOptions::timeout(timeout)?;
Ok(self)
}
}
impl Default for AnalysisOptions {
fn default() -> Self {
Self::if_needed()
}
}
impl AnalysisMode {
pub(crate) fn as_bridge_str(self) -> &'static str {
match self {
Self::Skip => "skip",
Self::IfNeeded => "if_needed",
Self::Force => "force",
}
}
}
fn file_name(path: &Path) -> Result<String> {
path.file_name()
.and_then(|name| name.to_str())
.filter(|name| !name.is_empty())
.map(str::to_owned)
.ok_or_else(|| Error::invalid_input("binary path must have a valid file name"))
}
fn normalize_folder(folder: String) -> Result<String> {
if folder.is_empty() {
return Err(Error::invalid_input("program folder must not be empty"));
}
if !folder.starts_with('/') {
return Err(Error::invalid_input("program folder must start with /"));
}
if folder.len() > 1 && folder.ends_with('/') {
return Err(Error::invalid_input("program folder must not end with /"));
}
if folder == "/" {
return Ok(folder);
}
if folder
.split('/')
.skip(1)
.any(|part| part.is_empty() || part == "." || part == "..")
{
return Err(Error::invalid_input(
"program folder must not contain empty, . or .. segments",
));
}
Ok(folder)
}
fn validate_name(name: &str) -> Result<()> {
if name.is_empty() {
return Err(Error::invalid_input("program name must not be empty"));
}
if name.contains('/') {
return Err(Error::invalid_input("program name must not contain /"));
}
if name == "." || name == ".." {
return Err(Error::invalid_input("program name must not be . or .."));
}
Ok(())
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::{AnalysisMode, AnalysisOptions, ProgramLoadOptions, ProgramPath};
#[test]
fn program_path_builds_root_paths() {
let path = ProgramPath::root("sample").expect("valid path");
assert_eq!(path.folder(), "/");
assert_eq!(path.name(), "sample");
assert_eq!(path.as_project_path(), "/sample");
}
#[test]
fn program_path_builds_nested_paths() {
let path = ProgramPath::new("/firmware/tests", "sample").expect("valid path");
assert_eq!(path.folder(), "/firmware/tests");
assert_eq!(path.name(), "sample");
assert_eq!(path.to_string(), "/firmware/tests/sample");
}
#[test]
fn program_path_parses_project_paths() {
assert_eq!(
ProgramPath::from_project_path("/firmware/sample")
.expect("valid path")
.as_project_path(),
"/firmware/sample"
);
}
#[test]
fn program_path_rejects_invalid_paths() {
assert!(ProgramPath::root("").is_err());
assert!(ProgramPath::new("relative", "sample").is_err());
assert!(ProgramPath::new("/folder/", "sample").is_err());
assert!(ProgramPath::new("/folder//nested", "sample").is_err());
assert!(ProgramPath::new("/folder", "bad/name").is_err());
assert!(ProgramPath::from_project_path("sample").is_err());
assert!(ProgramPath::from_project_path("//sample").is_err());
}
#[test]
fn load_options_default_to_root_program_name() {
let options = ProgramLoadOptions::new("/tmp/sample.bin").expect("valid options");
assert_eq!(options.path.as_project_path(), "/sample.bin");
assert!(options.loader.is_none());
assert!(options.language.is_none());
assert!(options.compiler.is_none());
assert!(options.loader_args.is_empty());
assert!(options.monitor.timeout_duration().is_none());
}
#[test]
fn analysis_options_default_to_if_needed() {
assert_eq!(AnalysisOptions::default().mode, AnalysisMode::IfNeeded);
assert_eq!(AnalysisOptions::skip().mode, AnalysisMode::Skip);
assert_eq!(AnalysisOptions::force().mode, AnalysisMode::Force);
assert!(
AnalysisOptions::default()
.monitor
.timeout_duration()
.is_none()
);
}
#[test]
fn load_and_analysis_options_accept_monitor_timeouts() {
let load = ProgramLoadOptions::new("/tmp/sample.bin")
.expect("valid options")
.with_timeout(Duration::from_secs(5))
.expect("valid timeout");
assert_eq!(
load.monitor.timeout_seconds().expect("valid timeout"),
Some(5)
);
let analysis = AnalysisOptions::force()
.with_timeout(Duration::from_millis(1))
.expect("valid timeout");
assert_eq!(
analysis.monitor.timeout_seconds().expect("valid timeout"),
Some(1)
);
}
}