use std::collections::BTreeMap;
use std::ops::Deref;
use serde::{Deserialize, Serialize};
use super::server::ServerCommand;
use super::text_newtype::contains_nul;
#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
#[error("args[{index}] cannot contain NUL")]
pub struct InvalidServerArgs {
pub index: usize,
}
#[derive(Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(try_from = "Vec<String>", into = "Vec<String>")]
pub struct ServerArgs(Vec<String>);
impl std::fmt::Debug for ServerArgs {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ServerArgs")
.field("len", &self.0.len())
.finish()
}
}
impl ServerArgs {
pub fn new<S: Into<String>>(
args: impl IntoIterator<Item = S>,
) -> Result<Self, InvalidServerArgs> {
Self::try_from(args.into_iter().map(Into::into).collect::<Vec<String>>())
}
#[must_use]
pub fn from_static(args: &'static [&'static str]) -> Self {
assert!(
!args.iter().any(|arg| contains_nul(arg)),
"a literal argument must not hold a NUL"
);
Self(args.iter().map(ToString::to_string).collect())
}
pub(crate) fn with_leading(mut self, program: ServerCommand) -> Self {
self.0.insert(0, String::from(program));
self
}
pub(crate) fn with_programs(self, mut programs: BTreeMap<usize, ServerCommand>) -> Self {
Self(
self.0
.into_iter()
.enumerate()
.map(|(index, arg)| programs.remove(&index).map_or(arg, String::from))
.collect(),
)
}
}
impl TryFrom<Vec<String>> for ServerArgs {
type Error = InvalidServerArgs;
fn try_from(args: Vec<String>) -> Result<Self, Self::Error> {
if let Some(index) = args.iter().position(|arg| contains_nul(arg)) {
return Err(InvalidServerArgs { index });
}
Ok(Self(args))
}
}
impl From<ServerArgs> for Vec<String> {
fn from(args: ServerArgs) -> Self {
args.0
}
}
impl Deref for ServerArgs {
type Target = [String];
fn deref(&self) -> &[String] {
&self.0
}
}
impl<S: AsRef<str>> PartialEq<Vec<S>> for ServerArgs {
fn eq(&self, other: &Vec<S>) -> bool {
self.0
.iter()
.map(String::as_str)
.eq(other.iter().map(AsRef::as_ref))
}
}
impl<S: AsRef<str>, const N: usize> PartialEq<[S; N]> for ServerArgs {
fn eq(&self, other: &[S; N]) -> bool {
self.0
.iter()
.map(String::as_str)
.eq(other.iter().map(AsRef::as_ref))
}
}
impl<'a> IntoIterator for &'a ServerArgs {
type Item = &'a String;
type IntoIter = std::slice::Iter<'a, String>;
fn into_iter(self) -> Self::IntoIter {
self.0.iter()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_nul_names_the_first_offending_argument() {
assert_eq!(
ServerArgs::new(["a", "b\0", "c\0"]),
Err(InvalidServerArgs { index: 1 })
);
assert_eq!(ServerArgs::new(["a", ""]).unwrap().len(), 2);
}
#[test]
fn debug_output_never_holds_an_argument() {
let args = ServerArgs::new(["--api-key=SECRETVALUE", "--stdio"]).unwrap();
let other = ServerArgs::new(["--api-key=OTHERVALUE", "--stdio"]).unwrap();
let rendered = format!("{args:?}");
assert!(!rendered.contains("SECRETVALUE"));
assert!(!rendered.contains("stdio"));
assert_eq!(rendered, format!("{other:?}"));
assert_eq!(rendered, "ServerArgs { len: 2 }");
}
#[test]
fn deserializing_checks_the_arguments() {
let ok: ServerArgs = serde_json::from_str(r#"["--x"]"#).unwrap();
assert_eq!(ok[0], "--x");
assert!(serde_json::from_str::<ServerArgs>(r#"["a\u0000"]"#).is_err());
}
}