Skip to main content

mcpls_core/config/
server_args.rs

1//! The command-line arguments of a configured server.
2
3use std::collections::BTreeMap;
4use std::ops::Deref;
5
6use serde::{Deserialize, Serialize};
7
8use super::server::ServerCommand;
9use super::text_newtype::contains_nul;
10
11/// An argument holds a NUL, which the operating system cannot pass.
12#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
13#[error("args[{index}] cannot contain NUL")]
14pub struct InvalidServerArgs {
15    /// The position of the first argument that holds a NUL.
16    pub index: usize,
17}
18
19/// The arguments passed to a server's command: any text, none of it holding a
20/// NUL, so a spawn cannot fail on one.
21///
22/// Reads as the `[String]` it wraps. Built through [`Self::new`] or
23/// `TryFrom<Vec<String>>`, which refuse a NUL, and by deserializing.
24///
25/// # Examples
26///
27/// ```
28/// use mcpls_core::config::ServerArgs;
29///
30/// let args = ServerArgs::new(["--stdio", "--log"])?;
31/// assert_eq!(args.len(), 2);
32/// assert_eq!(args[0], "--stdio");
33/// assert_eq!(ServerArgs::new(["ok", "a\0b"]).unwrap_err().index, 1);
34/// # Ok::<(), mcpls_core::config::InvalidServerArgs>(())
35/// ```
36#[derive(Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
37#[serde(try_from = "Vec<String>", into = "Vec<String>")]
38pub struct ServerArgs(Vec<String>);
39
40/// Prints the argument count only: arguments may carry credentials such as `--api-key=...`.
41impl std::fmt::Debug for ServerArgs {
42    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43        f.debug_struct("ServerArgs")
44            .field("len", &self.0.len())
45            .finish()
46    }
47}
48
49impl ServerArgs {
50    /// Validates `args`.
51    ///
52    /// # Errors
53    ///
54    /// [`InvalidServerArgs`] naming the first argument that holds a NUL.
55    pub fn new<S: Into<String>>(
56        args: impl IntoIterator<Item = S>,
57    ) -> Result<Self, InvalidServerArgs> {
58        Self::try_from(args.into_iter().map(Into::into).collect::<Vec<String>>())
59    }
60
61    /// Arguments written as literals in the source.
62    ///
63    /// # Panics
64    ///
65    /// Panics if a literal holds a NUL, which a source literal never should.
66    #[must_use]
67    pub fn from_static(args: &'static [&'static str]) -> Self {
68        assert!(
69            !args.iter().any(|arg| contains_nul(arg)),
70            "a literal argument must not hold a NUL"
71        );
72        Self(args.iter().map(ToString::to_string).collect())
73    }
74
75    /// These arguments preceded by `program`, which holds no NUL by its type.
76    pub(crate) fn with_leading(mut self, program: ServerCommand) -> Self {
77        self.0.insert(0, String::from(program));
78        self
79    }
80
81    /// These arguments with the one at each key of `programs` replaced by the
82    /// program, which holds no NUL by its type. Programs are found by index,
83    /// so no ordering of the analysis can make a replacement be skipped.
84    pub(crate) fn with_programs(self, mut programs: BTreeMap<usize, ServerCommand>) -> Self {
85        Self(
86            self.0
87                .into_iter()
88                .enumerate()
89                .map(|(index, arg)| programs.remove(&index).map_or(arg, String::from))
90                .collect(),
91        )
92    }
93}
94
95impl TryFrom<Vec<String>> for ServerArgs {
96    type Error = InvalidServerArgs;
97
98    fn try_from(args: Vec<String>) -> Result<Self, Self::Error> {
99        if let Some(index) = args.iter().position(|arg| contains_nul(arg)) {
100            return Err(InvalidServerArgs { index });
101        }
102        Ok(Self(args))
103    }
104}
105
106impl From<ServerArgs> for Vec<String> {
107    fn from(args: ServerArgs) -> Self {
108        args.0
109    }
110}
111
112impl Deref for ServerArgs {
113    type Target = [String];
114
115    fn deref(&self) -> &[String] {
116        &self.0
117    }
118}
119
120impl<S: AsRef<str>> PartialEq<Vec<S>> for ServerArgs {
121    fn eq(&self, other: &Vec<S>) -> bool {
122        self.0
123            .iter()
124            .map(String::as_str)
125            .eq(other.iter().map(AsRef::as_ref))
126    }
127}
128
129impl<S: AsRef<str>, const N: usize> PartialEq<[S; N]> for ServerArgs {
130    fn eq(&self, other: &[S; N]) -> bool {
131        self.0
132            .iter()
133            .map(String::as_str)
134            .eq(other.iter().map(AsRef::as_ref))
135    }
136}
137
138impl<'a> IntoIterator for &'a ServerArgs {
139    type Item = &'a String;
140    type IntoIter = std::slice::Iter<'a, String>;
141
142    fn into_iter(self) -> Self::IntoIter {
143        self.0.iter()
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    #[test]
152    fn a_nul_names_the_first_offending_argument() {
153        assert_eq!(
154            ServerArgs::new(["a", "b\0", "c\0"]),
155            Err(InvalidServerArgs { index: 1 })
156        );
157        assert_eq!(ServerArgs::new(["a", ""]).unwrap().len(), 2);
158    }
159
160    #[test]
161    fn debug_output_never_holds_an_argument() {
162        let args = ServerArgs::new(["--api-key=SECRETVALUE", "--stdio"]).unwrap();
163        let other = ServerArgs::new(["--api-key=OTHERVALUE", "--stdio"]).unwrap();
164        let rendered = format!("{args:?}");
165        assert!(!rendered.contains("SECRETVALUE"));
166        assert!(!rendered.contains("stdio"));
167        assert_eq!(rendered, format!("{other:?}"));
168        assert_eq!(rendered, "ServerArgs { len: 2 }");
169    }
170
171    #[test]
172    fn deserializing_checks_the_arguments() {
173        let ok: ServerArgs = serde_json::from_str(r#"["--x"]"#).unwrap();
174        assert_eq!(ok[0], "--x");
175        assert!(serde_json::from_str::<ServerArgs>(r#"["a\u0000"]"#).is_err());
176    }
177}