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

use crate::{ParseError, Spanned};

/// Error when an invalid plugin filename was encountered. This can be converted to [`ParseError`]
/// if a span is added.
#[derive(Debug, Clone)]
pub struct InvalidPluginFilename;

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,
        )
    }
}

#[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.
    pub fn new(
        filename: impl Into<PathBuf>,
        shell: Option<PathBuf>,
    ) -> Result<PluginIdentity, InvalidPluginFilename> {
        let filename = filename.into();

        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(InvalidPluginFilename)?;

        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")
    }
}

#[test]
fn parses_name_from_path() {
    assert_eq!("test", PluginIdentity::new_fake("test").name());
    assert_eq!("test_2", PluginIdentity::new_fake("test_2").name());
    assert_eq!(
        "foo",
        PluginIdentity::new("nu_plugin_foo.sh", Some("sh".into()))
            .expect("should be valid")
            .name()
    );
    PluginIdentity::new("other", None).expect_err("should be invalid");
    PluginIdentity::new("", None).expect_err("should be invalid");
}