nuts_tool/config/
plugin.rs

1// MIT License
2//
3// Copyright (c) 2024 Robin Doer
4//
5// Permission is hereby granted, free of charge, to any person obtaining a copy
6// of this software and associated documentation files (the "Software"), to
7// deal in the Software without restriction, including without limitation the
8// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
9// sell copies of the Software, and to permit persons to whom the Software is
10// furnished to do so, subject to the following conditions:
11//
12// The above copyright notice and this permission notice shall be included in
13// all copies or substantial portions of the Software.
14//
15// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
21// IN THE SOFTWARE.
22
23use 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}