1use crate::cli::{CliRequest, Mode};
2use crate::types::*;
3use agent_first_data::OutputFormat;
8use base64::Engine;
9use serde_json::Value;
10use std::collections::HashMap;
11
12pub fn parse_curl_args(args: &[String]) -> Mode {
13 let mut method: Option<String> = None;
14 let mut url: Option<String> = None;
15 let mut headers: Vec<(String, String)> = Vec::new();
16 let mut data_parts: Vec<String> = Vec::new();
18 let mut data_raw_parts: Vec<String> = Vec::new();
20 let mut data_urlencode: Vec<UrlencodedPart> = Vec::new();
21 let mut form_parts: Vec<MultipartPart> = Vec::new();
22 let mut response_save_file: Option<String> = None;
23 let mut response_save_file_is_basename = false;
24 let mut response_save_resume = false;
25 let mut tls_insecure = false;
26 let mut tls_cacert_file: Option<String> = None;
27 let mut tls_cert_file: Option<String> = None;
28 let mut tls_key_file: Option<String> = None;
29 let mut proxy: Option<String> = None;
30 let mut retry: Option<u32> = None;
31 let mut timeout_connect_s: Option<u64> = None;
32 let mut timeout_idle_s: Option<u64> = None;
33 let mut verbose = false;
34 let mut chunked = false;
35 let mut head_mode = false;
36 let mut upload_file: Option<String> = None;
37 let mut response_redirect: Option<u32> = None;
38
39 let mut i = 0;
40 while i < args.len() {
41 let arg = &args[i];
42
43 if !arg.starts_with('-') || arg == "-" {
45 if url.is_none() && arg != "-" {
46 url = Some(arg.clone());
47 }
48 i += 1;
49 continue;
50 }
51
52 if let Some(rest) = arg.strip_prefix("--") {
54 let (flag, inline_val) = match rest.find('=') {
56 Some(pos) => (&rest[..pos], Some(&rest[pos + 1..])),
57 None => (rest, None),
58 };
59
60 macro_rules! next_val {
62 () => {
63 if let Some(v) = inline_val {
64 Some(v.to_string())
65 } else if i + 1 < args.len() && !args[i + 1].starts_with('-') {
66 i += 1;
67 Some(args[i].clone())
68 } else {
69 None
70 }
71 };
72 }
73
74 match flag {
75 "request" => {
76 if let Some(v) = next_val!() {
77 method = Some(v.to_uppercase());
78 }
79 }
80 "header" => {
81 if let Some(v) = next_val!() {
82 push_header(&v, &mut headers);
83 }
84 }
85 "data" | "data-ascii" => {
86 if let Some(v) = next_val!() {
87 data_parts.push(v);
88 }
89 }
90 "data-raw" => {
91 if let Some(v) = next_val!() {
92 data_raw_parts.push(v);
93 }
94 }
95 "data-urlencode" => {
96 if let Some(v) = next_val!() {
97 push_urlencode_part(&v, &mut data_urlencode);
98 }
99 }
100 "form" => {
101 if let Some(v) = next_val!() {
102 push_form_part(&v, &mut form_parts);
103 }
104 }
105 "output" => {
106 if let Some(v) = next_val!() {
107 response_save_file = Some(v);
108 }
109 }
110 "remote-name" => {
111 response_save_file_is_basename = true;
112 }
113 "location" => {
114 if response_redirect.is_none() {
115 response_redirect = Some(10);
116 }
117 }
118 "max-redirs" => {
119 if let Some(v) = next_val!() {
120 response_redirect = v.parse().ok();
121 }
122 }
123 "head" => {
124 head_mode = true;
125 }
126 "insecure" => {
127 tls_insecure = true;
128 }
129 "cacert" => {
130 if let Some(v) = next_val!() {
131 tls_cacert_file = Some(v);
132 }
133 }
134 "cert" => {
135 if let Some(v) = next_val!() {
136 tls_cert_file = Some(v);
137 }
138 }
139 "key" => {
140 if let Some(v) = next_val!() {
141 tls_key_file = Some(v);
142 }
143 }
144 "proxy" => {
145 if let Some(v) = next_val!() {
146 proxy = Some(v);
147 }
148 }
149 "retry" => {
150 if let Some(v) = next_val!() {
151 retry = v.parse().ok();
152 }
153 }
154 "connect-timeout" => {
155 if let Some(v) = next_val!() {
156 timeout_connect_s = v.parse::<f64>().ok().map(|f| f as u64);
158 }
159 }
160 "max-time" => {
161 if let Some(v) = next_val!() {
162 timeout_idle_s = v.parse::<f64>().ok().map(|f| f as u64);
163 }
164 }
165 "user-agent" => {
166 if let Some(v) = next_val!() {
167 headers.push(("User-Agent".to_string(), v));
168 }
169 }
170 "user" => {
171 if let Some(v) = next_val!() {
172 push_basic_auth(&v, &mut headers);
173 }
174 }
175 "cookie" => {
176 if let Some(v) = next_val!() {
177 headers.push(("Cookie".to_string(), v));
178 }
179 }
180 "referer" => {
181 if let Some(v) = next_val!() {
182 headers.push(("Referer".to_string(), v));
183 }
184 }
185 "upload-file" => {
186 if let Some(v) = next_val!() {
187 upload_file = Some(v);
188 }
189 }
190 "no-buffer" => {
191 chunked = true;
192 }
193 "verbose" => {
194 verbose = true;
195 }
196 "silent" | "compressed" | "fail" | "fail-with-body" | "show-error" | "globoff"
198 | "disable-eprt" | "ipv4" | "ipv6" => {}
199 "continue-at" => {
200 if let Some(v) = inline_val {
201 if v == "-" {
202 response_save_resume = true;
203 }
204 } else if i + 1 < args.len() {
205 i += 1;
206 if args[i] == "-" {
207 response_save_resume = true;
208 }
209 }
210 }
211 _ => {
212 if inline_val.is_none() {
215 }
218 }
219 }
220 i += 1;
221 continue;
222 }
223
224 let chars = &arg[1..]; if chars.is_empty() {
227 i += 1;
228 continue;
229 }
230
231 let mut j = 0;
233 let char_bytes: Vec<char> = chars.chars().collect();
234 while j < char_bytes.len() {
235 let c = char_bytes[j];
236 let rest_of_arg: String = char_bytes[j + 1..].iter().collect();
238
239 match c {
240 'X' => {
241 if !rest_of_arg.is_empty() {
242 method = Some(rest_of_arg.to_uppercase());
243 j = char_bytes.len(); } else {
245 i += 1;
246 if i < args.len() {
247 method = Some(args[i].to_uppercase());
248 }
249 j = char_bytes.len();
250 }
251 }
252 'H' => {
253 if !rest_of_arg.is_empty() {
254 push_header(&rest_of_arg, &mut headers);
255 j = char_bytes.len();
256 } else {
257 i += 1;
258 if i < args.len() {
259 push_header(&args[i], &mut headers);
260 }
261 j = char_bytes.len();
262 }
263 }
264 'd' => {
265 if !rest_of_arg.is_empty() {
266 data_parts.push(rest_of_arg.clone());
267 j = char_bytes.len();
268 } else {
269 i += 1;
270 if i < args.len() {
271 data_parts.push(args[i].clone());
272 }
273 j = char_bytes.len();
274 }
275 }
276 'F' => {
277 if !rest_of_arg.is_empty() {
278 push_form_part(&rest_of_arg, &mut form_parts);
279 j = char_bytes.len();
280 } else {
281 i += 1;
282 if i < args.len() {
283 push_form_part(&args[i], &mut form_parts);
284 }
285 j = char_bytes.len();
286 }
287 }
288 'o' => {
289 if !rest_of_arg.is_empty() {
290 response_save_file = Some(rest_of_arg.clone());
291 j = char_bytes.len();
292 } else {
293 i += 1;
294 if i < args.len() {
295 response_save_file = Some(args[i].clone());
296 }
297 j = char_bytes.len();
298 }
299 }
300 'O' => {
301 response_save_file_is_basename = true;
302 j += 1;
303 }
304 'L' => {
305 if response_redirect.is_none() {
306 response_redirect = Some(10);
307 }
308 j += 1;
309 }
310 'k' => {
311 tls_insecure = true;
312 j += 1;
313 }
314 'x' => {
315 if !rest_of_arg.is_empty() {
316 proxy = Some(rest_of_arg.clone());
317 j = char_bytes.len();
318 } else {
319 i += 1;
320 if i < args.len() {
321 proxy = Some(args[i].clone());
322 }
323 j = char_bytes.len();
324 }
325 }
326 'A' => {
327 if !rest_of_arg.is_empty() {
328 headers.push(("User-Agent".to_string(), rest_of_arg.clone()));
329 j = char_bytes.len();
330 } else {
331 i += 1;
332 if i < args.len() {
333 headers.push(("User-Agent".to_string(), args[i].clone()));
334 }
335 j = char_bytes.len();
336 }
337 }
338 'u' => {
339 if !rest_of_arg.is_empty() {
340 push_basic_auth(&rest_of_arg, &mut headers);
341 j = char_bytes.len();
342 } else {
343 i += 1;
344 if i < args.len() {
345 push_basic_auth(&args[i], &mut headers);
346 }
347 j = char_bytes.len();
348 }
349 }
350 'I' => {
351 head_mode = true;
352 j += 1;
353 }
354 'N' => {
355 chunked = true;
356 j += 1;
357 }
358 'v' => {
359 verbose = true;
360 j += 1;
361 }
362 's' => {
363 j += 1;
365 }
366 'b' => {
367 if !rest_of_arg.is_empty() {
368 headers.push(("Cookie".to_string(), rest_of_arg.clone()));
369 j = char_bytes.len();
370 } else {
371 i += 1;
372 if i < args.len() {
373 headers.push(("Cookie".to_string(), args[i].clone()));
374 }
375 j = char_bytes.len();
376 }
377 }
378 'e' => {
379 if !rest_of_arg.is_empty() {
380 headers.push(("Referer".to_string(), rest_of_arg.clone()));
381 j = char_bytes.len();
382 } else {
383 i += 1;
384 if i < args.len() {
385 headers.push(("Referer".to_string(), args[i].clone()));
386 }
387 j = char_bytes.len();
388 }
389 }
390 'T' => {
391 if !rest_of_arg.is_empty() {
392 upload_file = Some(rest_of_arg.clone());
393 j = char_bytes.len();
394 } else {
395 i += 1;
396 if i < args.len() {
397 upload_file = Some(args[i].clone());
398 }
399 j = char_bytes.len();
400 }
401 }
402 'C' => {
403 if rest_of_arg == "-" {
404 response_save_resume = true;
405 j = char_bytes.len();
406 } else {
407 i += 1;
408 if i < args.len() && args[i] == "-" {
409 response_save_resume = true;
410 }
411 j = char_bytes.len();
412 }
413 }
414 _ => {
415 j += 1;
416 }
417 }
418 }
419
420 i += 1;
421 }
422
423 if response_save_file_is_basename {
425 let basename = url
426 .as_deref()
427 .and_then(remote_name_from_url)
428 .unwrap_or_else(|| "output".to_string());
429 response_save_file = Some(basename);
430 }
431
432 let has_body_data = !data_parts.is_empty()
434 || !data_raw_parts.is_empty()
435 || !data_urlencode.is_empty()
436 || !form_parts.is_empty();
437
438 let method = if head_mode {
439 "HEAD".to_string()
440 } else if let Some(m) = method {
441 m
442 } else if upload_file.is_some() {
443 "PUT".to_string()
445 } else if has_body_data {
446 "POST".to_string()
447 } else {
448 "GET".to_string()
449 };
450
451 let mut headers_map: HashMap<String, Value> = HashMap::new();
453 for (k, v) in headers {
454 headers_map.insert(k, Value::String(v));
455 }
456
457 let (body, body_base64, body_file, body_multipart, body_urlencoded) =
459 if let Some(path) = upload_file {
460 (None, None, Some(path), None, None)
461 } else if !form_parts.is_empty() {
462 (None, None, None, Some(form_parts), None)
463 } else if !data_urlencode.is_empty() {
464 (None, None, None, None, Some(data_urlencode))
465 } else if !data_raw_parts.is_empty() {
466 let combined = data_raw_parts.join("&");
468 (Some(Value::String(combined)), None, None, None, None)
469 } else if !data_parts.is_empty() {
470 let combined = data_parts.join("&");
472 let body_val = match serde_json::from_str::<Value>(combined.trim()) {
473 Ok(v) if v.is_object() || v.is_array() => v,
474 _ => Value::String(combined),
475 };
476 (Some(body_val), None, None, None, None)
477 } else {
478 (None, None, None, None, None)
479 };
480
481 const ALL_CATEGORIES: &[&str] = &["startup", "request", "progress", "retry", "redirect"];
483 let log_categories: Vec<String> = if verbose {
484 ALL_CATEGORIES.iter().map(|s| s.to_string()).collect()
485 } else {
486 vec![]
487 };
488
489 let tls = if tls_insecure
491 || tls_cacert_file.is_some()
492 || tls_cert_file.is_some()
493 || tls_key_file.is_some()
494 {
495 Some(TlsConfigPartial {
496 insecure: if tls_insecure { Some(true) } else { None },
497 cacert_file: tls_cacert_file,
498 cert_file: tls_cert_file,
499 key_file: tls_key_file,
500 ..TlsConfigPartial::default()
501 })
502 } else {
503 None
504 };
505
506 let config_overrides = ConfigPatch {
507 proxy,
508 timeout_connect_s,
509 ..ConfigPatch::default()
510 };
511
512 let chunked_delimiter = Value::String("\n".to_string());
513
514 let options = RequestOptions {
515 timeout_idle_s,
516 retry,
517 response_redirect,
518 response_save_file,
519 response_save_resume: if response_save_resume {
520 Some(true)
521 } else {
522 None
523 },
524 chunked,
525 chunked_delimiter,
526 tls,
527 ..RequestOptions::default()
528 };
529
530 Mode::Cli(Box::new(CliRequest {
531 method,
532 url: url.unwrap_or_default(),
533 headers: headers_map,
534 body,
535 body_base64,
536 body_file,
537 body_multipart,
538 body_urlencoded,
539 options,
540 config_overrides,
541 log_categories,
542 output_format: OutputFormat::Json,
543 dry_run: false,
544 }))
545}
546
547fn remote_name_from_url(raw: &str) -> Option<String> {
552 let parsed = reqwest::Url::parse(raw).ok()?;
553 parsed
554 .path_segments()
555 .and_then(|mut segments| segments.next_back())
556 .filter(|s| !s.is_empty())
557 .map(ToString::to_string)
558}
559
560fn push_header(s: &str, headers: &mut Vec<(String, String)>) {
561 match s.find(':') {
562 Some(pos) => {
563 let name = s[..pos].trim().to_string();
564 let value = s[pos + 1..].trim().to_string();
565 headers.push((name, value));
566 }
567 None => {
568 }
571 }
572}
573
574fn push_basic_auth(s: &str, headers: &mut Vec<(String, String)>) {
575 let encoded = base64::engine::general_purpose::STANDARD.encode(s.as_bytes());
576 headers.push(("Authorization".to_string(), format!("Basic {encoded}")));
577}
578
579fn push_urlencode_part(s: &str, parts: &mut Vec<UrlencodedPart>) {
580 match s.find('=') {
582 Some(pos) => parts.push(UrlencodedPart {
583 name: s[..pos].to_string(),
584 value: s[pos + 1..].to_string(),
585 }),
586 None => {
587 parts.push(UrlencodedPart {
589 name: s.to_string(),
590 value: String::new(),
591 });
592 }
593 }
594}
595
596fn push_form_part(s: &str, parts: &mut Vec<MultipartPart>) {
597 match s.find('=') {
598 Some(pos) => {
599 let name = s[..pos].to_string();
600 let rest = &s[pos + 1..];
601 if let Some(file_rest) = rest.strip_prefix('@') {
602 let segments: Vec<&str> = file_rest.splitn(2, ';').collect();
604 let file = segments[0].to_string();
605 let mut filename = None;
606 let mut content_type = None;
607 if let Some(meta) = segments.get(1) {
608 for part in meta.split(';') {
609 if let Some(f) = part.strip_prefix("filename=") {
610 filename = Some(f.to_string());
611 } else if let Some(t) = part.strip_prefix("type=") {
612 content_type = Some(t.to_string());
613 }
614 }
615 }
616 parts.push(MultipartPart {
617 name,
618 value: None,
619 value_base64: None,
620 file: Some(file),
621 filename,
622 content_type,
623 });
624 } else {
625 parts.push(MultipartPart {
626 name,
627 value: Some(rest.to_string()),
628 value_base64: None,
629 file: None,
630 filename: None,
631 content_type: None,
632 });
633 }
634 }
635 None => {
636 parts.push(MultipartPart {
638 name: s.to_string(),
639 value: Some(String::new()),
640 value_base64: None,
641 file: None,
642 filename: None,
643 content_type: None,
644 });
645 }
646 }
647}
648
649#[cfg(test)]
650#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
651mod tests {
652 use super::*;
653
654 fn to_args(items: &[&str]) -> Vec<String> {
655 items.iter().map(|s| (*s).to_string()).collect()
656 }
657
658 fn parse_cli(items: &[&str]) -> CliRequest {
659 match parse_curl_args(&to_args(items)) {
660 Mode::Cli(req) => *req,
661 _ => panic!("expected Mode::Cli"),
662 }
663 }
664
665 #[test]
666 fn parse_curl_defaults_to_get() {
667 let req = parse_cli(&["https://example.com"]);
668 assert_eq!(req.method, "GET");
669 assert_eq!(req.url, "https://example.com");
670 assert!(req.body.is_none());
671 }
672
673 #[test]
674 fn parse_curl_short_flags_with_body_and_headers() {
675 let req = parse_cli(&[
676 "-XPOST",
677 "-H",
678 "X-Test: 1",
679 "-d",
680 "{\"x\":1}",
681 "https://example.com/api",
682 ]);
683 assert_eq!(req.method, "POST");
684 assert_eq!(req.url, "https://example.com/api");
685 assert_eq!(
686 req.headers.get("X-Test"),
687 Some(&Value::String("1".to_string()))
688 );
689 assert_eq!(req.body, Some(serde_json::json!({"x": 1})));
690 }
691
692 #[test]
693 fn parse_curl_data_raw_is_string_and_verbose_sets_logs() {
694 let req = parse_cli(&["--data-raw", "a=1", "-v", "https://example.com"]);
695 assert_eq!(req.method, "POST");
696 assert_eq!(req.body, Some(Value::String("a=1".to_string())));
697 assert!(req.log_categories.iter().any(|c| c == "startup"));
698 }
699
700 #[test]
701 fn parse_curl_upload_file_defaults_to_put() {
702 let req = parse_cli(&["-T", "/tmp/file.bin", "https://example.com/upload"]);
703 assert_eq!(req.method, "PUT");
704 assert_eq!(req.body_file.as_deref(), Some("/tmp/file.bin"));
705 }
706
707 #[test]
708 fn parse_curl_form_and_urlencoded_modes() {
709 let form = parse_cli(&[
710 "-F",
711 "name=roger",
712 "-F",
713 "file=@/tmp/a.txt;filename=x.txt;type=text/plain",
714 "https://example.com",
715 ]);
716 let parts = form.body_multipart.expect("multipart");
717 assert_eq!(parts.len(), 2);
718 assert_eq!(parts[0].name, "name");
719 assert_eq!(parts[0].value.as_deref(), Some("roger"));
720 assert_eq!(parts[1].file.as_deref(), Some("/tmp/a.txt"));
721 assert_eq!(parts[1].filename.as_deref(), Some("x.txt"));
722 assert_eq!(parts[1].content_type.as_deref(), Some("text/plain"));
723
724 let ue = parse_cli(&[
725 "--data-urlencode",
726 "a=b",
727 "--data-urlencode",
728 "empty",
729 "https://example.com",
730 ]);
731 let parts = ue.body_urlencoded.expect("urlencoded");
732 assert_eq!(parts.len(), 2);
733 assert_eq!(parts[0].name, "a");
734 assert_eq!(parts[0].value, "b");
735 assert_eq!(parts[1].name, "empty");
736 assert_eq!(parts[1].value, "");
737 }
738
739 #[test]
740 fn parse_curl_remote_name_and_resume_and_redirects() {
741 let req = parse_cli(&[
742 "-O",
743 "--continue-at",
744 "-",
745 "-L",
746 "--max-redirs",
747 "3",
748 "https://example.com/path/file.txt",
749 ]);
750 assert_eq!(req.options.response_save_file.as_deref(), Some("file.txt"));
751 assert_eq!(req.options.response_save_resume, Some(true));
752 assert_eq!(req.options.response_redirect, Some(3));
753 }
754
755 #[test]
756 fn parse_curl_maps_tls_proxy_and_timeouts() {
757 let req = parse_cli(&[
758 "--insecure",
759 "--cacert",
760 "/tmp/ca.pem",
761 "--cert",
762 "/tmp/cert.pem",
763 "--key",
764 "/tmp/key.pem",
765 "--proxy",
766 "http://127.0.0.1:8080",
767 "--connect-timeout",
768 "2.9",
769 "--max-time",
770 "7.1",
771 "--retry",
772 "5",
773 "https://example.com",
774 ]);
775 assert_eq!(
776 req.config_overrides.proxy.as_deref(),
777 Some("http://127.0.0.1:8080")
778 );
779 assert_eq!(req.config_overrides.timeout_connect_s, Some(2));
780 assert_eq!(req.options.timeout_idle_s, Some(7));
781 assert_eq!(req.options.retry, Some(5));
782 let tls = req.options.tls.expect("tls");
783 assert_eq!(tls.insecure, Some(true));
784 assert_eq!(tls.cacert_file.as_deref(), Some("/tmp/ca.pem"));
785 assert_eq!(tls.cert_file.as_deref(), Some("/tmp/cert.pem"));
786 assert_eq!(tls.key_file.as_deref(), Some("/tmp/key.pem"));
787 }
788
789 #[test]
790 fn parse_helpers_cover_edge_cases() {
791 assert_eq!(
792 remote_name_from_url("https://example.com/a/b.txt").as_deref(),
793 Some("b.txt")
794 );
795 assert!(remote_name_from_url("not-url").is_none());
796
797 let mut headers = Vec::new();
798 push_header("X-A: 1", &mut headers);
799 push_header("MalformedHeader", &mut headers);
800 assert_eq!(headers.len(), 1);
801 assert_eq!(headers[0].0, "X-A");
802 assert_eq!(headers[0].1, "1");
803
804 let mut auth_headers = Vec::new();
805 push_basic_auth("u:p", &mut auth_headers);
806 assert_eq!(auth_headers[0].0, "Authorization");
807 assert!(auth_headers[0].1.starts_with("Basic "));
808
809 let mut ue = Vec::new();
810 push_urlencode_part("a=1", &mut ue);
811 push_urlencode_part("b", &mut ue);
812 assert_eq!(ue[0].name, "a");
813 assert_eq!(ue[0].value, "1");
814 assert_eq!(ue[1].name, "b");
815 assert_eq!(ue[1].value, "");
816
817 let mut form = Vec::new();
818 push_form_part("field=value", &mut form);
819 push_form_part(
820 "up=@/tmp/x.bin;filename=x.bin;type=application/octet-stream",
821 &mut form,
822 );
823 push_form_part("empty", &mut form);
824 assert_eq!(form.len(), 3);
825 assert_eq!(form[0].value.as_deref(), Some("value"));
826 assert_eq!(form[1].file.as_deref(), Some("/tmp/x.bin"));
827 assert_eq!(form[1].filename.as_deref(), Some("x.bin"));
828 assert_eq!(
829 form[1].content_type.as_deref(),
830 Some("application/octet-stream")
831 );
832 assert_eq!(form[2].value.as_deref(), Some(""));
833 }
834
835 #[test]
836 fn parse_curl_long_equals_and_noop_flags() {
837 let req = parse_cli(&[
838 "--request=patch",
839 "--header=X-A: 1",
840 "--data=hello",
841 "--output=out.txt",
842 "--location",
843 "--max-redirs=2",
844 "--head",
845 "--insecure",
846 "--cacert=/tmp/ca.pem",
847 "--cert=/tmp/cert.pem",
848 "--key=/tmp/key.pem",
849 "--proxy=http://127.0.0.1:8080",
850 "--retry=3",
851 "--connect-timeout=2.5",
852 "--max-time=7.8",
853 "--user-agent=ua",
854 "--user=u:p",
855 "--cookie=a=1",
856 "--referer=https://ref",
857 "--upload-file=/tmp/up.bin",
858 "--no-buffer",
859 "--verbose",
860 "--silent",
861 "--compressed",
862 "--fail",
863 "--fail-with-body",
864 "--show-error",
865 "--globoff",
866 "--disable-eprt",
867 "--ipv4",
868 "--ipv6",
869 "--continue-at=-",
870 "https://example.com/p",
871 ]);
872 assert_eq!(req.method, "HEAD");
873 assert_eq!(req.url, "https://example.com/p");
874 assert_eq!(req.options.response_redirect, Some(2));
875 assert_eq!(req.options.response_save_resume, Some(true));
876 assert_eq!(req.options.timeout_idle_s, Some(7));
877 assert_eq!(req.options.retry, Some(3));
878 assert!(req.options.chunked);
879 assert_eq!(
880 req.config_overrides.proxy.as_deref(),
881 Some("http://127.0.0.1:8080")
882 );
883 assert_eq!(req.config_overrides.timeout_connect_s, Some(2));
884 assert_eq!(
885 req.headers.get("User-Agent"),
886 Some(&Value::String("ua".to_string()))
887 );
888 assert_eq!(
889 req.headers.get("Cookie"),
890 Some(&Value::String("a=1".to_string()))
891 );
892 assert_eq!(
893 req.headers.get("Referer"),
894 Some(&Value::String("https://ref".to_string()))
895 );
896 assert!(req.headers.contains_key("Authorization"));
897 assert_eq!(req.body_file.as_deref(), Some("/tmp/up.bin"));
898
899 let head = parse_cli(&["--head", "https://example.com/head"]);
900 assert_eq!(head.method, "HEAD");
901 }
902
903 #[test]
904 fn parse_curl_short_flags_separate_values_and_unknowns() {
905 let req = parse_cli(&[
906 "-vksLN",
907 "-X",
908 "PUT",
909 "-H",
910 "X-B: 2",
911 "-d",
912 "k=v",
913 "-F",
914 "f=@/tmp/a.txt",
915 "-o",
916 "res.bin",
917 "-x",
918 "http://proxy:8080",
919 "-A",
920 "ua2",
921 "-u",
922 "u2:p2",
923 "-b",
924 "c=1",
925 "-e",
926 "https://r",
927 "-T",
928 "/tmp/body.bin",
929 "-C",
930 "-",
931 "-Z", "-",
933 "https://example.com/file",
934 ]);
935 assert_eq!(req.method, "PUT");
936 assert_eq!(req.options.response_save_file.as_deref(), Some("res.bin"));
937 assert_eq!(req.options.response_save_resume, Some(true));
938 assert!(req.options.chunked);
939 assert_eq!(
940 req.config_overrides.proxy.as_deref(),
941 Some("http://proxy:8080")
942 );
943 assert_eq!(req.body_file.as_deref(), Some("/tmp/body.bin"));
944 assert_eq!(
945 req.headers.get("User-Agent"),
946 Some(&Value::String("ua2".to_string()))
947 );
948 assert_eq!(
949 req.headers.get("Cookie"),
950 Some(&Value::String("c=1".to_string()))
951 );
952 }
953
954 #[test]
955 fn parse_curl_remote_name_fallback_and_missing_values() {
956 let req = parse_cli(&["-O", "https://example.com"]);
957 assert_eq!(req.options.response_save_file.as_deref(), Some("output"));
958
959 let req = parse_cli(&["https://example.com", "--request"]);
960 assert_eq!(req.method, "GET");
961 assert_eq!(req.url, "https://example.com");
962
963 let req = parse_cli(&["--request", "--header", "X-A: 1", "https://example.com"]);
964 assert_eq!(req.method, "GET");
965 assert_eq!(req.url, "https://example.com");
966 assert_eq!(
967 req.headers.get("X-A"),
968 Some(&Value::String("1".to_string()))
969 );
970 }
971}