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
use std::path::{Path, PathBuf};

use crate::{ParseError, ShellError, Spanned};

/// Error when an invalid plugin filename was encountered.
#[derive(Debug, Clone)]
pub struct InvalidPluginFilename(PathBuf);

impl std::fmt::Display for InvalidPluginFilename {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str("invalid plugin filename")
    }
}

impl From<Spanned<InvalidPluginFilename>> for ParseError {
    fn from(error: Spanned<InvalidPluginFilename>) -> ParseError {
        ParseError::LabeledError(
            "Invalid plugin filename".into(),
            "must start with `nu_plugin_`".into(),
            error.span,
        )
    }
}

impl From<Spanned<InvalidPluginFilename>> for ShellError {
    fn from(error: Spanned<InvalidPluginFilename>) -> ShellError {
        ShellError::GenericError {
            error: format!("Invalid plugin filename: {}", error.item.0.display()),
            msg: "not a valid plugin filename".into(),
            span: Some(error.span),
            help: Some("valid Nushell plugin filenames must start with `nu_plugin_`".into()),
            inner: vec![],
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct PluginIdentity {
    /// The filename used to start the plugin
    filename: PathBuf,
    /// The shell used to start the plugin, if required
    shell: Option<PathBuf>,
    /// The friendly name of the plugin (e.g. `inc` for `C:\nu_plugin_inc.exe`)
    name: String,
}

impl PluginIdentity {
    /// Create a new plugin identity from a path to plugin executable and shell option.
    ///
    /// The `filename` must be an absolute path. Canonicalize before trying to construct the
    /// [`PluginIdentity`].
    pub fn new(
        filename: impl Into<PathBuf>,
        shell: Option<PathBuf>,
    ) -> Result<PluginIdentity, InvalidPluginFilename> {
        let filename: PathBuf = filename.into();

        // Must pass absolute path.
        if filename.is_relative() {
            return Err(InvalidPluginFilename(filename));
        }

        let name = filename
            .file_stem()
            .map(|stem| stem.to_string_lossy().into_owned())
            .and_then(|stem| stem.strip_prefix("nu_plugin_").map(|s| s.to_owned()))
            .ok_or_else(|| InvalidPluginFilename(filename.clone()))?;

        Ok(PluginIdentity {
            filename,
            shell,
            name,
        })
    }

    /// The filename of the plugin executable.
    pub fn filename(&self) -> &Path {
        &self.filename
    }

    /// The shell command used by the plugin.
    pub fn shell(&self) -> Option<&Path> {
        self.shell.as_deref()
    }

    /// The name of the plugin, determined by the part of the filename after `nu_plugin_` excluding
    /// the extension.
    ///
    /// - `C:\nu_plugin_inc.exe` becomes `inc`
    /// - `/home/nu/.cargo/bin/nu_plugin_inc` becomes `inc`
    pub fn name(&self) -> &str {
        &self.name
    }

    /// Create a fake identity for testing.
    #[cfg(windows)]
    #[doc(hidden)]
    pub fn new_fake(name: &str) -> PluginIdentity {
        PluginIdentity::new(format!(r"C:\fake\path\nu_plugin_{name}.exe"), None)
            .expect("fake plugin identity path is invalid")
    }

    /// Create a fake identity for testing.
    #[cfg(not(windows))]
    #[doc(hidden)]
    pub fn new_fake(name: &str) -> PluginIdentity {
        PluginIdentity::new(format!(r"/fake/path/nu_plugin_{name}"), None)
            .expect("fake plugin identity path is invalid")
    }

    /// A command that could be used to add the plugin, for suggesting in errors.
    pub fn add_command(&self) -> String {
        if let Some(shell) = self.shell() {
            format!(
                "plugin add --shell '{}' '{}'",
                shell.display(),
                self.filename().display(),
            )
        } else {
            format!("plugin add '{}'", self.filename().display())
        }
    }

    /// A command that could be used to reload the plugin, for suggesting in errors.
    pub fn use_command(&self) -> String {
        format!("plugin use '{}'", self.name())
    }
}

#[test]
fn parses_name_from_path() {
    assert_eq!("test", PluginIdentity::new_fake("test").name());
    assert_eq!("test_2", PluginIdentity::new_fake("test_2").name());
    let absolute_path = if cfg!(windows) {
        r"C:\path\to\nu_plugin_foo.sh"
    } else {
        "/path/to/nu_plugin_foo.sh"
    };
    assert_eq!(
        "foo",
        PluginIdentity::new(absolute_path, Some("sh".into()))
            .expect("should be valid")
            .name()
    );
    // Relative paths should be invalid
    PluginIdentity::new("nu_plugin_foo.sh", Some("sh".into())).expect_err("should be invalid");
    PluginIdentity::new("other", None).expect_err("should be invalid");
    PluginIdentity::new("", None).expect_err("should be invalid");
}