1use crate::{CliError, CliErrorKind};
14
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub struct ArgLimits {
17 pub max_words: usize,
18 pub max_word_bytes: usize,
19 pub max_total_bytes: usize,
20}
21
22#[derive(Clone, Debug, PartialEq, Eq)]
23pub struct RawArgs {
24 words: Vec<String>,
25 limits: ArgLimits,
26}
27
28impl Default for ArgLimits {
29 fn default() -> Self {
30 Self {
31 max_words: 1024,
32 max_word_bytes: 64 * 1024,
33 max_total_bytes: 1024 * 1024,
34 }
35 }
36}
37
38impl RawArgs {
39 pub fn from_env() -> Result<Self, CliError> {
42 Self::from_os_words(std::env::args_os().skip(1), ArgLimits::default())
43 }
44
45 fn from_os_words(
46 args: impl IntoIterator<Item = std::ffi::OsString>,
47 limits: ArgLimits,
48 ) -> Result<Self, CliError> {
49 let mut words = Vec::new();
50 let mut total_bytes = 0usize;
51 for (index, word) in args.into_iter().enumerate() {
52 let word = word.into_string().map_err(|_| {
53 CliError::new(
54 CliErrorKind::InvalidInput,
55 format!("argument {} is not valid UTF-8", index + 1),
56 )
57 })?;
58 validate_raw_word(&word, words.len(), total_bytes, limits)?;
59 total_bytes = total_bytes.saturating_add(word.len());
60 words.push(word);
61 }
62 Ok(Self { words, limits })
63 }
64
65 pub fn parse<I>(args: I) -> Result<Self, CliError>
66 where
67 I: IntoIterator,
68 I::Item: Into<String>,
69 {
70 Self::parse_with_limits(args, ArgLimits::default())
71 }
72
73 pub fn parse_with_limits<I>(args: I, limits: ArgLimits) -> Result<Self, CliError>
74 where
75 I: IntoIterator,
76 I::Item: Into<String>,
77 {
78 let mut words = Vec::new();
79 let mut total_bytes = 0usize;
80 for arg in args {
81 let value = arg.into();
82 validate_raw_word(&value, words.len(), total_bytes, limits)?;
83 total_bytes = total_bytes.saturating_add(value.len());
84 words.push(value);
85 }
86 Ok(Self { words, limits })
87 }
88
89 pub(crate) fn limits(&self) -> ArgLimits {
90 self.limits
91 }
92
93 pub fn words(&self) -> &[String] {
94 &self.words
95 }
96}
97
98fn validate_raw_word(
99 value: &str,
100 count: usize,
101 total: usize,
102 limits: ArgLimits,
103) -> Result<(), CliError> {
104 if count >= limits.max_words {
105 return Err(CliError::new(
106 CliErrorKind::InvalidInput,
107 format!("argument count exceeds limit of {}", limits.max_words),
108 ));
109 }
110 if value.len() > limits.max_word_bytes {
111 return Err(CliError::new(
112 CliErrorKind::InvalidInput,
113 format!(
114 "argument {} exceeds byte limit of {}",
115 count + 1,
116 limits.max_word_bytes
117 ),
118 ));
119 }
120 if total.saturating_add(value.len()) > limits.max_total_bytes {
121 return Err(CliError::new(
122 CliErrorKind::InvalidInput,
123 format!(
124 "total argument bytes exceed limit of {}",
125 limits.max_total_bytes
126 ),
127 ));
128 }
129 if value.contains('\0') {
130 return Err(CliError::new(
131 CliErrorKind::InvalidInput,
132 format!("argument {} contains a NUL byte", count + 1),
133 ));
134 }
135 Ok(())
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141 use std::ffi::OsString;
142
143 #[test]
144 fn environment_ingestion_stops_consuming_at_first_limit_failure() {
145 let mut consumed = 0;
146 let args = ["first", "second", "never-read"].into_iter().map(|word| {
147 consumed += 1;
148 OsString::from(word)
149 });
150 let limits = ArgLimits {
151 max_words: 1,
152 ..ArgLimits::default()
153 };
154 assert!(RawArgs::from_os_words(args, limits).is_err());
155 assert_eq!(consumed, 2);
156 }
157
158 #[test]
159 fn environment_ingestion_enforces_exact_aggregate_utf8_bytes() {
160 let limits = ArgLimits {
161 max_words: 2,
162 max_word_bytes: 2,
163 max_total_bytes: 3,
164 };
165 let accepted =
166 RawArgs::from_os_words([OsString::from("é"), OsString::from("a")], limits).unwrap();
167 assert_eq!(accepted.words(), &["é", "a"]);
168 assert!(
169 RawArgs::from_os_words([OsString::from("é"), OsString::from("ab")], limits).is_err()
170 );
171 }
172
173 #[cfg(unix)]
174 #[test]
175 fn invalid_environment_encoding_keeps_the_argument_index() {
176 use std::os::unix::ffi::OsStringExt;
177 let error = RawArgs::from_os_words(
178 [OsString::from("valid"), OsString::from_vec(vec![0xff])],
179 ArgLimits::default(),
180 )
181 .unwrap_err();
182 assert_eq!(error.to_string(), "argument 2 is not valid UTF-8");
183 }
184}