Skip to main content

nu_protocol/plugin/registry_file/
mod.rs

1use std::{
2    io::{Read, Write},
3    path::PathBuf,
4};
5
6use serde::{Deserialize, Serialize};
7
8use crate::{
9    PluginIdentity, PluginMetadata, PluginSignature, ShellError, Span,
10    shell_error::generic::GenericError,
11};
12
13// This has a big impact on performance
14const BUFFER_SIZE: usize = 65536;
15
16// Chose settings at the low end, because we're just trying to get the maximum speed
17const COMPRESSION_QUALITY: u32 = 3; // 1 can be very bad
18const WIN_SIZE: u32 = 20; // recommended 20-22
19
20#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
21pub struct PluginRegistryFile {
22    /// The Nushell version that last updated the file.
23    pub nushell_version: String,
24
25    /// The installed plugins.
26    pub plugins: Vec<PluginRegistryItem>,
27}
28
29impl Default for PluginRegistryFile {
30    fn default() -> Self {
31        Self::new()
32    }
33}
34
35impl PluginRegistryFile {
36    /// Create a new, empty plugin registry file.
37    pub fn new() -> PluginRegistryFile {
38        PluginRegistryFile {
39            nushell_version: env!("CARGO_PKG_VERSION").to_owned(),
40            plugins: vec![],
41        }
42    }
43
44    /// Read the plugin registry file from a reader, e.g. [`File`](std::fs::File).
45    pub fn read_from(
46        reader: impl Read,
47        error_span: Option<Span>,
48    ) -> Result<PluginRegistryFile, ShellError> {
49        let load_error = |err: String| {
50            let error = format!("Failed to load plugin file: {err}");
51            let msg = "plugin file load attempted here";
52            let help = "it may be corrupt. Try deleting it and registering your plugins again";
53            match error_span {
54                Some(span) => {
55                    ShellError::Generic(GenericError::new(error, msg, span).with_help(help))
56                }
57                None => ShellError::Generic(GenericError::new_internal(error, msg).with_help(help)),
58            }
59        };
60
61        // Format is brotli compressed messagepack
62        let mut bytes = Vec::new();
63        brotli::Decompressor::new(reader, BUFFER_SIZE)
64            .read_to_end(&mut bytes)
65            .map_err(|err| load_error(err.to_string()))?;
66
67        // `PluginRegistryItem` flattens an untagged enum so that an entry whose signatures no
68        // longer deserialize is loaded as `Invalid` instead of failing the whole file. That
69        // tolerance makes serde buffer every entry into a generic value tree and decode it a
70        // second time, which is the bulk of the time spent loading the registry at startup.
71        // The entries are all valid in the common case, so decode the plain layout in one pass
72        // first and only fall back to the tolerant decoder when that fails.
73        match rmp_serde::from_slice::<StrictPluginRegistryFile>(&bytes) {
74            Ok(file) => Ok(file.into()),
75            Err(_) => rmp_serde::from_slice(&bytes).map_err(|err| load_error(err.to_string())),
76        }
77    }
78
79    /// Write the plugin registry file to a writer, e.g. [`File`](std::fs::File).
80    ///
81    /// The `nushell_version` will be updated to the current version before writing.
82    pub fn write_to(
83        &mut self,
84        writer: impl Write,
85        error_span: Option<Span>,
86    ) -> Result<(), ShellError> {
87        // Update the Nushell version before writing
88        env!("CARGO_PKG_VERSION").clone_into(&mut self.nushell_version);
89
90        // Format is brotli compressed messagepack
91        let mut brotli_writer =
92            brotli::CompressorWriter::new(writer, BUFFER_SIZE, COMPRESSION_QUALITY, WIN_SIZE);
93
94        rmp_serde::encode::write_named(&mut brotli_writer, self)
95            .map_err(|err| err.to_string())
96            .and_then(|_| brotli_writer.flush().map_err(|err| err.to_string()))
97            .map_err(|err| {
98                let error = "Failed to save plugin file";
99                let msg = "plugin file save attempted here";
100                match error_span {
101                    Some(span) => {
102                        ShellError::Generic(GenericError::new(error, msg, span).with_help(err))
103                    }
104                    None => {
105                        ShellError::Generic(GenericError::new_internal(error, msg).with_help(err))
106                    }
107                }
108            })
109    }
110
111    /// Insert or update a plugin in the plugin registry file.
112    pub fn upsert_plugin(&mut self, item: PluginRegistryItem) {
113        if let Some(existing_item) = self.plugins.iter_mut().find(|p| p.name == item.name) {
114            *existing_item = item;
115        } else {
116            self.plugins.push(item);
117
118            // Sort the plugins for consistency
119            self.plugins
120                .sort_by(|item1, item2| item1.name.cmp(&item2.name));
121        }
122    }
123}
124
125/// The on-disk layout of [`PluginRegistryFile`] without the error tolerance of
126/// [`PluginRegistryItemData`]: the same maps, decoded in a single pass. Only used for reading;
127/// see [`PluginRegistryFile::read_from`].
128#[derive(Deserialize)]
129struct StrictPluginRegistryFile {
130    nushell_version: String,
131    plugins: Vec<StrictPluginRegistryItem>,
132}
133
134/// A [`PluginRegistryItem`] whose data is known to be valid. See [`StrictPluginRegistryFile`].
135#[derive(Deserialize)]
136struct StrictPluginRegistryItem {
137    name: String,
138    filename: PathBuf,
139    shell: Option<PathBuf>,
140    #[serde(default)]
141    metadata: PluginMetadata,
142    commands: Vec<PluginSignature>,
143}
144
145impl From<StrictPluginRegistryFile> for PluginRegistryFile {
146    fn from(file: StrictPluginRegistryFile) -> Self {
147        PluginRegistryFile {
148            nushell_version: file.nushell_version,
149            plugins: file
150                .plugins
151                .into_iter()
152                .map(|item| PluginRegistryItem {
153                    name: item.name,
154                    filename: item.filename,
155                    shell: item.shell,
156                    data: PluginRegistryItemData::Valid {
157                        metadata: item.metadata,
158                        commands: item.commands,
159                    },
160                })
161                .collect(),
162        }
163    }
164}
165
166/// A single plugin definition from a [`PluginRegistryFile`].
167///
168/// Contains the information necessary for the [`PluginIdentity`], as well as possibly valid data
169/// about the plugin including the registered command signatures.
170#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
171pub struct PluginRegistryItem {
172    /// The name of the plugin, as would show in `plugin list`. This does not include the file
173    /// extension or the `nu_plugin_` prefix.
174    pub name: String,
175
176    /// The path to the file.
177    pub filename: PathBuf,
178
179    /// The shell program used to run the plugin, if applicable.
180    pub shell: Option<PathBuf>,
181
182    /// Additional data that might be invalid so that we don't fail to load the whole plugin file
183    /// if there's a deserialization error.
184    #[serde(flatten)]
185    pub data: PluginRegistryItemData,
186}
187
188impl PluginRegistryItem {
189    /// Create a [`PluginRegistryItem`] from an identity, metadata, and signatures.
190    pub fn new(
191        identity: &PluginIdentity,
192        metadata: PluginMetadata,
193        mut commands: Vec<PluginSignature>,
194    ) -> PluginRegistryItem {
195        // Sort the commands for consistency
196        commands.sort_by(|cmd1, cmd2| cmd1.sig.name.cmp(&cmd2.sig.name));
197
198        PluginRegistryItem {
199            name: identity.name().to_owned(),
200            filename: identity.filename().to_owned(),
201            shell: identity.shell().map(|p| p.to_owned()),
202            data: PluginRegistryItemData::Valid { metadata, commands },
203        }
204    }
205}
206
207/// Possibly valid data about a plugin in a [`PluginRegistryFile`]. If deserialization fails, it will
208/// be `Invalid`.
209#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
210#[serde(untagged)]
211pub enum PluginRegistryItemData {
212    Valid {
213        /// Metadata for the plugin, including its version.
214        #[serde(default)]
215        metadata: PluginMetadata,
216        /// Signatures and examples for each command provided by the plugin.
217        commands: Vec<PluginSignature>,
218    },
219    #[serde(
220        serialize_with = "serialize_invalid",
221        deserialize_with = "deserialize_invalid"
222    )]
223    Invalid,
224}
225
226fn serialize_invalid<S>(serializer: S) -> Result<S::Ok, S::Error>
227where
228    S: serde::Serializer,
229{
230    ().serialize(serializer)
231}
232
233fn deserialize_invalid<'de, D>(deserializer: D) -> Result<(), D::Error>
234where
235    D: serde::Deserializer<'de>,
236{
237    serde::de::IgnoredAny::deserialize(deserializer)?;
238    Ok(())
239}
240
241#[cfg(test)]
242mod tests;