mcpls_core/config/
server_args.rs1use 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#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
13#[error("args[{index}] cannot contain NUL")]
14pub struct InvalidServerArgs {
15 pub index: usize,
17}
18
19#[derive(Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
37#[serde(try_from = "Vec<String>", into = "Vec<String>")]
38pub struct ServerArgs(Vec<String>);
39
40impl 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 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 #[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 pub(crate) fn with_leading(mut self, program: ServerCommand) -> Self {
77 self.0.insert(0, String::from(program));
78 self
79 }
80
81 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}