1use crate::{ArgLimits, CliError, CliErrorKind, OptionSpec, ValueMode, ValueType};
14use std::ffi::{OsStr, OsString};
15use std::net::SocketAddr;
16use std::path::PathBuf;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
20#[non_exhaustive]
21pub enum ValueKind {
22 #[default]
24 Text,
25 Path,
27 SocketAddr,
29}
30
31#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct RawOsArgs {
34 words: Vec<OsString>,
35 limits: ArgLimits,
36}
37impl RawOsArgs {
38 pub fn from_env() -> Result<Self, CliError> {
40 Self::parse(std::env::args_os().skip(1))
41 }
42 pub fn parse(args: impl IntoIterator<Item = OsString>) -> Result<Self, CliError> {
44 Self::parse_with_limits(args, ArgLimits::default())
45 }
46 pub fn parse_with_limits(
48 args: impl IntoIterator<Item = OsString>,
49 limits: ArgLimits,
50 ) -> Result<Self, CliError> {
51 let mut words = Vec::new();
52 let mut total = 0;
53 for word in args {
54 validate_word(&word, words.len(), total, limits)?;
55 total += word.as_encoded_bytes().len();
56 words.push(word);
57 }
58 Ok(Self { words, limits })
59 }
60 pub fn words(&self) -> &[OsString] {
62 &self.words
63 }
64 pub(crate) fn limits(&self) -> ArgLimits {
65 self.limits
66 }
67}
68
69#[derive(Clone, PartialEq, Eq)]
71pub struct ParsedOsOption {
72 pub(crate) name: String,
73 pub(crate) value: Option<OsString>,
74}
75impl std::fmt::Debug for ParsedOsOption {
76 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77 f.debug_struct("ParsedOsOption")
78 .field("name", &self.name)
79 .field("value", &"[REDACTED]")
80 .finish()
81 }
82}
83impl ParsedOsOption {
84 pub fn name(&self) -> &str {
86 &self.name
87 }
88 pub fn value(&self) -> Option<&OsStr> {
90 self.value.as_deref()
91 }
92}
93#[derive(Debug, Clone, PartialEq, Eq)]
95pub struct ParsedOsCli {
96 pub(crate) command_path: Vec<String>,
97 pub(crate) options: Vec<ParsedOsOption>,
98 pub(crate) positionals: Vec<OsString>,
99 pub(crate) passthrough: Vec<OsString>,
100}
101impl ParsedOsCli {
102 pub fn command_path(&self) -> &[String] {
104 &self.command_path
105 }
106 pub fn options(&self) -> &[ParsedOsOption] {
108 &self.options
109 }
110 pub fn positionals(&self) -> &[OsString] {
112 &self.positionals
113 }
114 pub fn passthrough(&self) -> &[OsString] {
116 &self.passthrough
117 }
118 pub fn has_flag(&self, name: &str) -> bool {
120 self.options.iter().any(|o| o.name == name)
121 }
122 pub fn option_value(&self, name: &str) -> Option<&OsStr> {
124 self.options
125 .iter()
126 .rev()
127 .find(|o| o.name == name)
128 .and_then(ParsedOsOption::value)
129 }
130 pub fn option_values<'a>(&'a self, name: &'a str) -> impl Iterator<Item = &'a OsStr> + 'a {
132 self.options
133 .iter()
134 .filter(move |o| o.name == name)
135 .filter_map(ParsedOsOption::value)
136 }
137 pub fn option_occurrences(&self, name: &str) -> usize {
139 self.options.iter().filter(|o| o.name == name).count()
140 }
141 pub fn option_path(&self, name: &str) -> Option<PathBuf> {
143 self.option_value(name).map(PathBuf::from)
144 }
145 pub fn option_socket_addr(&self, name: &str) -> Result<Option<SocketAddr>, CliError> {
147 self.option_value(name)
148 .map(|v| {
149 v.to_str().and_then(|s| s.parse().ok()).ok_or_else(|| {
150 CliError::new(
151 CliErrorKind::InvalidValue,
152 format!("invalid socket address for --{name}"),
153 )
154 })
155 })
156 .transpose()
157 }
158 pub(crate) fn into_text(self) -> Result<crate::ParsedCli, CliError> {
159 let convert = |v: OsString| {
160 v.into_string().map_err(|_| {
161 CliError::new(CliErrorKind::InvalidInput, "native value requires parse_os")
162 })
163 };
164 Ok(crate::ParsedCli {
165 command_path: self.command_path,
166 options: self
167 .options
168 .into_iter()
169 .map(|o| {
170 Ok(crate::ParsedOption {
171 name: o.name,
172 value: o.value.map(&convert).transpose()?,
173 })
174 })
175 .collect::<Result<_, CliError>>()?,
176 positionals: self
177 .positionals
178 .into_iter()
179 .map(&convert)
180 .collect::<Result<_, _>>()?,
181 passthrough: self
182 .passthrough
183 .into_iter()
184 .map(convert)
185 .collect::<Result<_, _>>()?,
186 })
187 }
188}
189
190pub(crate) fn validate_word(
191 value: &OsStr,
192 count: usize,
193 total: usize,
194 limits: ArgLimits,
195) -> Result<(), CliError> {
196 let bytes = value.as_encoded_bytes();
197 if count >= limits.max_words
198 || bytes.len() > limits.max_word_bytes
199 || total.saturating_add(bytes.len()) > limits.max_total_bytes
200 || bytes.contains(&0)
201 {
202 return Err(CliError::new(
203 CliErrorKind::InvalidInput,
204 "native argument exceeds count/byte budget or contains NUL",
205 ));
206 }
207 Ok(())
208}
209
210pub(crate) fn validate_native(
211 value: &OsStr,
212 kind: ValueKind,
213 ty: ValueType,
214 possible: &[String],
215 owner: &str,
216) -> Result<(), CliError> {
217 if kind == ValueKind::Path {
218 if value.is_empty() {
219 return Err(CliError::new(
220 CliErrorKind::InvalidValue,
221 format!("empty path for {owner}"),
222 ));
223 }
224 if possible.is_empty() {
225 return Ok(());
226 }
227 }
228 let text = value.to_str().ok_or_else(|| {
229 CliError::new(
230 CliErrorKind::InvalidValue,
231 format!("{owner} requires UTF-8 text"),
232 )
233 })?;
234 if kind == ValueKind::SocketAddr && text.parse::<SocketAddr>().is_err() {
235 return Err(CliError::new(
236 CliErrorKind::InvalidValue,
237 format!("{owner} requires a numeric socket address"),
238 ));
239 }
240 crate::value_parser::validate_value(text, ty, possible, owner)
242}
243
244pub(crate) fn split_attached(word: &OsStr, separator: u8) -> (OsString, Option<OsString>) {
245 let bytes = word.as_encoded_bytes();
246 match bytes.iter().position(|b| *b == separator) {
247 Some(i) => (
248 os_slice(word, 0, i),
249 Some(os_slice(word, i + 1, bytes.len())),
250 ),
251 None => (word.to_os_string(), None),
252 }
253}
254
255pub(crate) fn os_slice(word: &OsStr, start: usize, end: usize) -> OsString {
256 #[cfg(unix)]
259 {
260 use std::os::unix::ffi::{OsStrExt, OsStringExt};
261 OsString::from_vec(word.as_bytes()[start..end].to_vec())
262 }
263 #[cfg(windows)]
264 {
265 use std::os::windows::ffi::{OsStrExt, OsStringExt};
266 let units = word.encode_wide().collect::<Vec<_>>();
267 let bytes = word.as_encoded_bytes();
268 if !bytes[..start].is_ascii() || (end != bytes.len() && !bytes[..end].is_ascii()) {
269 return word.to_os_string();
270 }
271 let length = if end == bytes.len() { units.len() } else { end };
272 OsString::from_wide(&units[start..length])
273 }
274 #[cfg(not(any(unix, windows)))]
275 {
276 OsString::from(&word.to_str().unwrap_or("")[start..end])
277 }
278}
279
280pub(crate) fn option_value(
281 words: &[OsString],
282 index: usize,
283 inline: Option<OsString>,
284 option: &OptionSpec,
285 short: bool,
286) -> Result<(Option<OsString>, usize), CliError> {
287 match option.value_mode() {
288 ValueMode::Forbidden if inline.is_some() && !short => Err(CliError::new(
289 CliErrorKind::InvalidValue,
290 "flag does not accept a value",
291 )),
292 ValueMode::Forbidden => Ok((None, index + 1)),
293 ValueMode::Required => match inline {
294 Some(value) => Ok((Some(value), index + 1)),
295 None => words
296 .get(index + 1)
297 .cloned()
298 .map(|v| (Some(v), index + 2))
299 .ok_or_else(|| {
300 CliError::new(
301 CliErrorKind::MissingValue,
302 format!("--{} requires a value", option.long()),
303 )
304 }),
305 },
306 ValueMode::Optional => {
307 if inline.is_some() || !option.accepts_detached_optional_value() {
308 return Ok((inline, index + 1));
309 }
310 match words.get(index + 1).filter(|v| {
311 validate_native(
312 v,
313 option.kind,
314 option.value_type_kind(),
315 option.possible_values(),
316 option.long(),
317 )
318 .is_ok()
319 }) {
320 Some(v) => Ok((Some(v.clone()), index + 2)),
321 None => Ok((None, index + 1)),
322 }
323 }
324 }
325}