nuts_tool/config/
plugin.rs1use anyhow::{anyhow, Result};
24use is_executable::IsExecutable;
25use log::{debug, error, trace, warn};
26use nuts_tool_api::tool::Plugin;
27use nuts_tool_api::tool_dir;
28use serde::{Deserialize, Serialize};
29use std::borrow::Cow;
30use std::collections::HashMap;
31use std::path::{Path, PathBuf};
32use std::{env, fs};
33
34use crate::config::load_path;
35
36fn find_in_path(relative: &Path) -> Option<Cow<Path>> {
37 if let Some(path_env) = env::var_os("PATH") {
38 trace!("PATH: {:?}", path_env);
39
40 let abs_path = env::split_paths(&path_env)
41 .map(|path| path.join(relative))
42 .inspect(|path| trace!("testing {}", path.display()))
43 .find(|path| path.exists());
44
45 debug!("absolute path in PATH: {:?}", abs_path);
46
47 abs_path.map(Cow::Owned)
48 } else {
49 warn!("no environment variable PATH found");
50 None
51 }
52}
53
54fn make_from_current_exe(relative: &Path) -> Result<Cow<Path>> {
55 let cur_exe = env::current_exe()
56 .map_err(|err| anyhow!("could not detect path of executable: {}", err))?;
57 let path = cur_exe.with_file_name(relative.as_os_str());
58
59 debug!("absolute path from current exe: '{}'", path.display());
60
61 Ok(Cow::Owned(path))
62}
63
64#[derive(Debug, Deserialize, Serialize)]
65struct Inner {
66 path: PathBuf,
67}
68
69impl Inner {
70 fn absolute_path(&self) -> Result<Cow<Path>> {
71 if self.path.is_absolute() {
72 Ok(Cow::Borrowed(self.path.as_path()))
73 } else {
74 match find_in_path(&self.path) {
75 Some(path) => Ok(path),
76 None => make_from_current_exe(&self.path),
77 }
78 }
79 }
80
81 fn validate(&self) -> bool {
82 match self.absolute_path() {
83 Ok(path) => Self::validate_path(&path),
84 Err(err) => {
85 error!("failed to validate {}: {}", self.path.display(), err);
86 false
87 }
88 }
89 }
90
91 fn validate_path(path: &Path) -> bool {
92 if !path.is_file() {
93 error!("{}: not a file", path.display());
94 return false;
95 }
96
97 if !path.is_executable() {
98 error!("{}: not executable", path.display());
99 return false;
100 }
101
102 let plugin = Plugin::new(path);
103
104 if let Err(err) = plugin.info() {
105 error!("{}: not a plugin ({})", path.display(), err);
106 return false;
107 }
108
109 debug!("{}: is valid", path.display());
110
111 true
112 }
113}
114
115#[derive(Debug, Deserialize, Serialize)]
116pub struct PluginConfig {
117 #[serde(flatten)]
118 plugins: HashMap<String, Inner>,
119}
120
121impl PluginConfig {
122 pub fn load() -> Result<PluginConfig> {
123 match load_path(Self::config_file()?.as_path())? {
124 Some(s) => Ok(toml::from_str(&s)?),
125 None => Ok(PluginConfig {
126 plugins: HashMap::new(),
127 }),
128 }
129 }
130
131 pub fn config_file() -> Result<PathBuf> {
132 Ok(tool_dir()?.join("plugins"))
133 }
134
135 pub fn all_plugins(&self) -> Vec<&str> {
136 let mut keys = self
137 .plugins
138 .iter()
139 .filter(|(_, inner)| inner.validate())
140 .map(|(name, _)| name.as_str())
141 .collect::<Vec<&str>>();
142
143 keys.sort();
144
145 keys
146 }
147
148 pub fn have_plugin(&self, name: &str) -> bool {
149 self.plugins
150 .get(name)
151 .filter(|inner| inner.validate())
152 .is_some()
153 }
154
155 pub fn remove_plugin(&mut self, name: &str) -> bool {
156 self.plugins.remove(name).is_some()
157 }
158
159 pub fn path(&self, name: &str) -> Result<Cow<Path>> {
160 match self.plugins.get(name).filter(|inner| inner.validate()) {
161 Some(inner) => inner.absolute_path(),
162 None => Err(anyhow!("no such plugin: {}", name)),
163 }
164 }
165
166 pub fn set_path<P: AsRef<Path>>(&mut self, name: &str, path: P) -> bool {
167 let inner = Inner {
168 path: path.as_ref().into(),
169 };
170
171 let valid = inner.validate();
172
173 if valid {
174 self.plugins.insert(name.to_string(), inner);
175 }
176
177 valid
178 }
179
180 pub fn save(&self) -> Result<()> {
181 let path = Self::config_file()?;
182 let toml = toml::to_string(self)?;
183
184 debug!("{}: dump {} bytes", path.display(), toml.as_bytes().len());
185
186 fs::write(path, toml)?;
187
188 Ok(())
189 }
190}