1use std::time::Duration;
2
3use crate::error::CompatError;
4use crate::uri::{PproxyChain, PproxyUri};
5use crate::warnings::{CompatWarning, TranslationOutput};
6
7fn take_required_value(
8 raw: &[String],
9 index: &mut usize,
10 flag: &str,
11) -> Result<String, CompatError> {
12 *index += 1;
13 let value = raw
14 .get(*index)
15 .cloned()
16 .ok_or_else(|| CompatError::MissingArgument(format!("{flag} requires a value")))?;
17 if value.starts_with('-') && !(matches!(flag, "-a" | "--auth") && value.parse::<i64>().is_ok())
18 {
19 return Err(CompatError::MissingArgument(format!(
20 "{flag} requires a value"
21 )));
22 }
23 Ok(value)
24}
25
26fn parse_auth_duration(value: &str) -> Result<Duration, CompatError> {
27 let trimmed = value.trim();
28 if trimmed.is_empty() {
29 return Err(CompatError::InvalidArgs {
30 message: "--auth requires a non-empty numeric value".to_string(),
31 });
32 }
33 let seconds: i64 = trimmed.parse().map_err(|_| CompatError::InvalidArgs {
34 message: format!("--auth value '{}' is not a valid integer", trimmed),
35 })?;
36 Ok(Duration::from_secs(seconds.max(0) as u64))
40}
41
42#[derive(Debug, Clone)]
44pub struct PproxyArgs {
45 pub local: Vec<String>,
47 pub remotes: Vec<String>,
49 pub verbose_level: u8,
51 pub debug: bool,
53 pub debug_level: u8,
55 pub daemon: bool,
57 pub reuse_port: bool,
59 pub auth_timeout: Option<Duration>,
61 pub system_proxy: bool,
63 pub help: bool,
65 pub version: bool,
67 pub known_unsupported: Vec<String>,
69 pub unknown_flags: Vec<String>,
71 pub strict_violations: Vec<String>,
75}
76
77impl PproxyArgs {
78 pub fn has_args(raw: &[String]) -> bool {
80 !raw.is_empty()
81 }
82
83 pub fn test_target(&self) -> Option<&str> {
86 self.known_unsupported
87 .iter()
88 .find_map(|flag| flag.strip_prefix("test="))
89 }
90
91 pub fn default_args() -> Self {
96 Self {
97 local: vec!["http+socks4+socks5://:8080".to_string()],
98 remotes: vec![],
99 verbose_level: 0,
100 debug: false,
101 debug_level: 0,
102 daemon: false,
103 reuse_port: false,
104 auth_timeout: None,
105 system_proxy: false,
106 help: false,
107 version: false,
108 known_unsupported: vec![],
109 unknown_flags: vec![],
110 strict_violations: vec![],
111 }
112 }
113
114 pub fn parse(raw: &[String]) -> Result<Self, CompatError> {
116 let mut local = Vec::new();
117 let mut remotes = Vec::new();
118 let mut verbose_level: u8 = 0;
119 let mut debug = false;
120 let mut debug_level: u8 = 0;
121 let mut daemon = false;
122 let mut reuse_port = false;
123 let mut auth_timeout: Option<Duration> = None;
124 let mut system_proxy = false;
125 let mut help = false;
126 let mut version = false;
127 let mut known_unsupported = Vec::new();
128 let mut unknown_flags = Vec::new();
129 let mut strict_violations = Vec::new();
130 let mut i = 0;
131
132 while i < raw.len() {
133 let arg = &raw[i];
134 match arg.as_str() {
135 "-h" | "--help" => {
136 help = true;
137 }
138 "--version" => {
139 version = true;
140 }
141 "-l" | "--listen" => {
142 if arg == "--listen" {
143 strict_violations.push(arg.clone());
144 }
145 local.push(take_required_value(raw, &mut i, arg)?);
146 }
147 "-r" | "--remote" => {
148 if arg == "--remote" {
149 strict_violations.push(arg.clone());
150 }
151 remotes.push(take_required_value(raw, &mut i, arg)?);
152 }
153 "--daemon" => {
154 daemon = true;
155 }
156 "-d" => {
157 debug = true;
158 debug_level = debug_level.saturating_add(1);
159 }
160 "--log" | "-log" => {
161 let value = take_required_value(raw, &mut i, arg)?;
162 known_unsupported.push(format!("log={value}"));
163 strict_violations.push(arg.clone());
164 }
165 "-ul" | "--udp-listen" => {
166 if arg == "--udp-listen" {
167 strict_violations.push(arg.clone());
168 }
169 let value = take_required_value(raw, &mut i, arg)?;
170 known_unsupported.push(format!("udp-listen={value}"));
171 }
172 "-ur" | "--udp-remote" => {
173 if arg == "--udp-remote" {
174 strict_violations.push(arg.clone());
175 }
176 let value = take_required_value(raw, &mut i, arg)?;
177 known_unsupported.push(format!("udp-remote={value}"));
178 }
179 "--rulefile" | "-rulefile" => {
180 let value = take_required_value(raw, &mut i, arg)?;
181 known_unsupported.push(format!("rulefile={value}"));
182 strict_violations.push(arg.clone());
183 }
184 "-v" | "-vv" | "-vvv" => {
185 verbose_level = verbose_level.saturating_add(arg.len() as u8 - 1);
186 }
187 short
188 if short.len() > 2
189 && short.starts_with('-')
190 && short[1..].chars().all(|c| c == 'd' || c == 'v') =>
191 {
192 for flag in short[1..].chars() {
193 if flag == 'd' {
194 debug = true;
195 debug_level = debug_level.saturating_add(1);
196 } else {
197 verbose_level = verbose_level.saturating_add(1);
198 }
199 }
200 }
201 "-s" => {
202 let value = take_required_value(raw, &mut i, arg)?;
203 if !matches!(
204 value.as_str(),
205 "fa" | "rr"
206 | "rc"
207 | "lc"
208 | "first_available"
209 | "round_robin"
210 | "random_choice"
211 | "least_connection"
212 ) {
213 return Err(CompatError::InvalidArgs {
214 message: format!(
215 "invalid choice: '{}' (choose from fa, rr, rc, lc)",
216 value
217 ),
218 });
219 }
220 if !matches!(value.as_str(), "fa" | "rr" | "rc" | "lc") {
221 strict_violations.push(format!("-s {value}"));
224 }
225 known_unsupported.push(format!("scheduler={value}"));
226 }
227 "-a" => {
228 let value = take_required_value(raw, &mut i, arg)?;
229 known_unsupported.push(format!("alive={value}"));
230 }
231 "--ssl" => {
232 let value = take_required_value(raw, &mut i, arg)?;
233 known_unsupported.push(format!("ssl={value}"));
234 }
235 "-b" => {
236 let value = take_required_value(raw, &mut i, arg)?;
237 known_unsupported.push(format!("block={value}"));
238 }
239 "--pac" => {
240 let value = take_required_value(raw, &mut i, arg)?;
241 known_unsupported.push(format!("pac={value}"));
242 }
243 "--test" => {
244 let value = take_required_value(raw, &mut i, arg)?;
245 known_unsupported.push(format!("test={value}"));
246 }
247 "--sys" => {
248 system_proxy = true;
249 }
250 "--reuse" => {
251 reuse_port = true;
252 }
253 "--get" => {
254 let value = take_required_value(raw, &mut i, arg)?;
255 known_unsupported.push(format!("get={value}"));
256 }
257 "--auth" => {
258 let value = take_required_value(raw, &mut i, arg)?;
259 auth_timeout = Some(parse_auth_duration(&value)?);
260 }
261 other if other.starts_with('-') => {
262 unknown_flags.push(other.to_string());
263 }
264 other => {
265 if local.is_empty() {
270 local.push(other.to_string());
271 } else {
272 remotes.push(other.to_string());
273 }
274 strict_violations.push(other.to_string());
275 }
276 }
277 i += 1;
278 }
279
280 Ok(PproxyArgs {
281 local,
282 remotes,
283 verbose_level,
284 debug,
285 debug_level,
286 daemon,
287 reuse_port,
288 auth_timeout,
289 system_proxy,
290 help,
291 version,
292 known_unsupported,
293 unknown_flags,
294 strict_violations,
295 })
296 }
297
298 pub fn unknown_flag_diagnostics(&self) -> Vec<CompatWarning> {
300 let mut warnings = Vec::new();
301 for flag in &self.unknown_flags {
302 warnings.push(CompatWarning {
303 category: "unknown-flag",
304 message: format!("unrecognized option '{}'", flag),
305 });
306 }
307 warnings
308 }
309
310 pub fn unknown_flag_translation_output(&self) -> TranslationOutput {
312 let warnings = self.unknown_flag_diagnostics();
313 TranslationOutput::new(String::new()).with_warnings(warnings)
314 }
315
316 pub fn has_unknown_or_unsupported(&self) -> bool {
318 !self.unknown_flags.is_empty() || !self.known_unsupported.is_empty() || self.daemon
319 }
320
321 pub fn strict_parser_violations(&self) -> Vec<String> {
323 self.unknown_flags
324 .iter()
325 .cloned()
326 .chain(self.strict_violations.iter().cloned())
327 .collect()
328 }
329
330 pub fn version_string() -> String {
332 format!("eggress-pproxy-compat {}", env!("CARGO_PKG_VERSION"))
333 }
334
335 pub fn validate_strict_values(&self) -> Result<(), CompatError> {
339 self.parse_local_uris()
340 .map_err(|error| CompatError::InvalidArgs {
341 message: format!("invalid -l URI: {error}"),
342 })?;
343 self.parse_remote_chains()
344 .map_err(|error| CompatError::InvalidArgs {
345 message: format!("invalid -r URI: {error}"),
346 })?;
347 if let Some(scheduler) = self
348 .known_unsupported
349 .iter()
350 .find_map(|flag| flag.strip_prefix("scheduler="))
351 {
352 if !matches!(scheduler, "fa" | "rr" | "rc" | "lc") {
353 return Err(CompatError::InvalidArgs {
354 message: format!(
355 "invalid choice: '{}' (choose from fa, rr, rc, lc)",
356 scheduler
357 ),
358 });
359 }
360 }
361 if let Some(interval) = self
362 .known_unsupported
363 .iter()
364 .find_map(|flag| flag.strip_prefix("alive="))
365 {
366 interval
367 .parse::<i64>()
368 .map_err(|_| CompatError::InvalidArgs {
369 message: format!("-a value '{}' is not a valid integer", interval),
370 })?;
371 }
372 for kind in ["udp-listen=", "udp-remote="] {
373 for value in self
374 .known_unsupported
375 .iter()
376 .filter_map(|flag| flag.strip_prefix(kind))
377 {
378 crate::uri::parse_pproxy_uri(value).map_err(|error| CompatError::InvalidArgs {
379 message: format!("invalid {} URI: {error}", kind.trim_end_matches('=')),
380 })?;
381 }
382 }
383 Ok(())
384 }
385
386 pub fn effective_auth_timeout(&self) -> Duration {
390 self.auth_timeout
391 .unwrap_or_else(|| Duration::from_secs(86_400 * 30))
392 }
393
394 pub fn default_log_level(&self) -> &'static str {
411 if self.verbose_level >= 3 {
412 "trace"
413 } else if self.verbose_level >= 1 || self.debug {
414 "debug"
415 } else {
416 "info"
417 }
418 }
419
420 pub fn parse_local_uris(&self) -> Result<Vec<PproxyUri>, CompatError> {
422 self.local
423 .iter()
424 .map(|s| crate::uri::parse_pproxy_uri(s))
425 .collect()
426 }
427
428 pub fn parse_remote_uris(&self) -> Result<Vec<PproxyUri>, CompatError> {
430 self.remotes
431 .iter()
432 .map(|s| crate::uri::parse_pproxy_uri(s))
433 .collect()
434 }
435
436 pub fn parse_remote_chains(&self) -> Result<Vec<PproxyChain>, CompatError> {
438 self.remotes
439 .iter()
440 .map(|s| crate::uri::parse_pproxy_chain(s))
441 .collect()
442 }
443}
444
445#[cfg(test)]
446mod tests {
447 use super::*;
448
449 #[test]
450 fn test_parse_basic() {
451 let args = PproxyArgs::parse(&[
452 "-l".into(),
453 "socks5://127.0.0.1:1080".into(),
454 "-r".into(),
455 "http://proxy:8080".into(),
456 ])
457 .unwrap();
458 assert_eq!(args.local.len(), 1);
459 assert_eq!(args.remotes.len(), 1);
460 assert_eq!(args.local[0], "socks5://127.0.0.1:1080");
461 assert_eq!(args.remotes[0], "http://proxy:8080");
462 }
463
464 #[test]
465 fn test_parse_help_and_version_actions() {
466 let help = PproxyArgs::parse(&["--help".into()]).unwrap();
467 assert!(help.help);
468 assert!(!help.version);
469 assert!(help.strict_parser_violations().is_empty());
470
471 let version = PproxyArgs::parse(&["--version".into()]).unwrap();
472 assert!(version.version);
473 assert!(!version.help);
474 assert_eq!(
475 PproxyArgs::version_string(),
476 format!("eggress-pproxy-compat {}", env!("CARGO_PKG_VERSION"))
477 );
478 }
479
480 #[test]
481 fn test_parse_positional() {
482 let args =
483 PproxyArgs::parse(&["socks5://127.0.0.1:1080".into(), "http://proxy:8080".into()])
484 .unwrap();
485 assert_eq!(args.local.len(), 1);
486 assert_eq!(args.remotes.len(), 1);
487 }
488
489 #[test]
490 fn test_parse_multiple_remotes() {
491 let args = PproxyArgs::parse(&[
492 "-l".into(),
493 "socks5://127.0.0.1:1080".into(),
494 "-r".into(),
495 "http://proxy1:8080".into(),
496 "-r".into(),
497 "socks5://proxy2:1080".into(),
498 ])
499 .unwrap();
500 assert_eq!(args.remotes.len(), 2);
501 }
502
503 #[test]
504 fn test_parse_missing_value() {
505 let result = PproxyArgs::parse(&["-l".into()]);
506 assert!(result.is_err());
507 }
508
509 #[test]
510 fn test_parse_daemon_flag() {
511 let args = PproxyArgs::parse(&[
512 "-l".into(),
513 "socks5://127.0.0.1:1080".into(),
514 "--daemon".into(),
515 ])
516 .unwrap();
517 assert!(args.daemon);
518 assert!(!args.debug);
519 }
520
521 #[test]
522 fn test_parse_debug_flag() {
523 let args = PproxyArgs::parse(&["-l".into(), "socks5://127.0.0.1:1080".into(), "-d".into()])
524 .unwrap();
525 assert!(args.debug);
526 assert_eq!(args.debug_level, 1);
527 assert!(!args.daemon);
528 }
529
530 #[test]
531 fn test_count_actions_and_short_clusters() {
532 let args = PproxyArgs::parse(&[
533 "-l".into(),
534 "http://:8080".into(),
535 "-dd".into(),
536 "-v".into(),
537 "-vv".into(),
538 ])
539 .unwrap();
540 assert_eq!(args.debug_level, 2);
541 assert_eq!(args.verbose_level, 3);
542
543 let clustered =
544 PproxyArgs::parse(&["-l".into(), "http://:8080".into(), "-dv".into()]).unwrap();
545 assert_eq!(clustered.debug_level, 1);
546 assert_eq!(clustered.verbose_level, 1);
547 }
548
549 #[test]
550 fn test_strict_parser_rejects_extensions_and_positionals() {
551 let args = PproxyArgs::parse(&[
552 "--log".into(),
553 "access.log".into(),
554 "proxy://listener".into(),
555 ])
556 .unwrap();
557 assert!(args
558 .strict_parser_violations()
559 .iter()
560 .any(|value| value == "--log"));
561 assert!(args
562 .strict_parser_violations()
563 .iter()
564 .any(|value| value == "proxy://listener"));
565 }
566
567 #[test]
568 fn test_target_preserves_value_taking_option() {
569 let args = PproxyArgs::parse(&[
570 "-l".into(),
571 "http://:8080".into(),
572 "--test".into(),
573 "https://example.invalid/health".into(),
574 ])
575 .unwrap();
576 assert_eq!(args.test_target(), Some("https://example.invalid/health"));
577 }
578
579 #[test]
580 fn test_d_and_daemon_independent() {
581 let args = PproxyArgs::parse(&[
582 "-l".into(),
583 "socks5://127.0.0.1:1080".into(),
584 "-d".into(),
585 "--daemon".into(),
586 ])
587 .unwrap();
588 assert!(args.debug);
589 assert!(args.daemon);
590 }
591
592 #[test]
593 fn test_d_never_sets_daemon() {
594 let args = PproxyArgs::parse(&["-l".into(), "http://:8080".into(), "-d".into()]).unwrap();
595 assert!(args.debug);
596 assert!(!args.daemon);
597 }
598
599 #[test]
600 fn test_daemon_never_sets_debug() {
601 let args =
602 PproxyArgs::parse(&["-l".into(), "http://:8080".into(), "--daemon".into()]).unwrap();
603 assert!(!args.debug);
604 assert!(args.daemon);
605 }
606
607 #[test]
608 fn test_parse_verbose_flag() {
609 let args = PproxyArgs::parse(&["-l".into(), "socks5://127.0.0.1:1080".into(), "-v".into()])
610 .unwrap();
611 assert_eq!(args.verbose_level, 1);
612 }
613
614 #[test]
615 fn test_parse_scheduler_flag() {
616 let args = PproxyArgs::parse(&[
617 "-l".into(),
618 "socks5://127.0.0.1:1080".into(),
619 "-s".into(),
620 "rr".into(),
621 ])
622 .unwrap();
623 assert!(args.known_unsupported.contains(&"scheduler=rr".to_string()));
624 }
625
626 #[test]
627 fn test_scheduler_migration_alias_is_not_strict_executable_surface() {
628 let args = PproxyArgs::parse(&[
629 "-l".into(),
630 "socks5://127.0.0.1:1080".into(),
631 "-s".into(),
632 "round_robin".into(),
633 ])
634 .unwrap();
635 assert!(args
636 .strict_parser_violations()
637 .iter()
638 .any(|value| value == "-s round_robin"));
639 assert!(args.validate_strict_values().is_err());
640 }
641
642 #[test]
643 fn test_parse_alive_flag() {
644 let args = PproxyArgs::parse(&[
645 "-l".into(),
646 "socks5://127.0.0.1:1080".into(),
647 "-a".into(),
648 "10".into(),
649 ])
650 .unwrap();
651 assert!(args.known_unsupported.contains(&"alive=10".to_string()));
652 }
653
654 #[test]
655 fn test_parse_ssl_flag() {
656 let args = PproxyArgs::parse(&[
657 "-l".into(),
658 "socks5://127.0.0.1:1080".into(),
659 "--ssl".into(),
660 "cert.pem,key.pem".into(),
661 ])
662 .unwrap();
663 assert!(args
664 .known_unsupported
665 .contains(&"ssl=cert.pem,key.pem".to_string()));
666 }
667
668 #[test]
669 fn test_parse_block_flag() {
670 let args = PproxyArgs::parse(&[
671 "-l".into(),
672 "socks5://127.0.0.1:1080".into(),
673 "-b".into(),
674 ".*\\.example\\.com".into(),
675 ])
676 .unwrap();
677 assert!(args
678 .known_unsupported
679 .contains(&"block=.*\\.example\\.com".to_string()));
680 }
681
682 #[test]
683 fn test_parse_log_flag() {
684 let args = PproxyArgs::parse(&[
685 "-l".into(),
686 "socks5://127.0.0.1:1080".into(),
687 "--log".into(),
688 "access.log".into(),
689 ])
690 .unwrap();
691 assert!(args
692 .known_unsupported
693 .contains(&"log=access.log".to_string()));
694 }
695
696 #[test]
697 fn test_parse_udp_flags() {
698 let args = PproxyArgs::parse(&[
699 "-l".into(),
700 "socks5://127.0.0.1:1080".into(),
701 "-ul".into(),
702 "socks5://:1081".into(),
703 "-ur".into(),
704 "socks5://proxy:1080".into(),
705 ])
706 .unwrap();
707 assert!(args
708 .known_unsupported
709 .contains(&"udp-listen=socks5://:1081".to_string()));
710 assert!(args
711 .known_unsupported
712 .contains(&"udp-remote=socks5://proxy:1080".to_string()));
713 }
714
715 #[test]
716 fn test_parse_rulefile_flag() {
717 let args = PproxyArgs::parse(&[
718 "-l".into(),
719 "socks5://127.0.0.1:1080".into(),
720 "--rulefile".into(),
721 "rules.txt".into(),
722 ])
723 .unwrap();
724 assert!(args
725 .known_unsupported
726 .contains(&"rulefile=rules.txt".to_string()));
727 }
728
729 #[test]
730 fn test_unknown_flag_diagnostics() {
731 let args = PproxyArgs::parse(&[
732 "-l".into(),
733 "socks5://127.0.0.1:1080".into(),
734 "--unknown-flag".into(),
735 "-x".into(),
736 ])
737 .unwrap();
738 let warnings = args.unknown_flag_diagnostics();
739 assert_eq!(warnings.len(), 2);
740 assert!(warnings
741 .iter()
742 .any(|w| w.message.contains("--unknown-flag")));
743 assert!(warnings.iter().any(|w| w.message.contains("-x")));
744 }
745
746 #[test]
747 fn test_known_flags_no_unknown_warnings() {
748 let args = PproxyArgs::parse(&[
749 "-l".into(),
750 "socks5://127.0.0.1:1080".into(),
751 "-v".into(),
752 "-s".into(),
753 "rr".into(),
754 "-a".into(),
755 "10".into(),
756 "--daemon".into(),
757 "--log".into(),
758 "access.log".into(),
759 "-ul".into(),
760 "socks5://:1081".into(),
761 "-ur".into(),
762 "socks5://proxy:1080".into(),
763 "--rulefile".into(),
764 "rules.txt".into(),
765 "--ssl".into(),
766 "cert.pem,key.pem".into(),
767 "-b".into(),
768 ".*\\.example\\.com".into(),
769 ])
770 .unwrap();
771 let warnings = args.unknown_flag_diagnostics();
772 assert!(warnings.is_empty());
773 }
774
775 #[test]
776 fn test_scheduler_missing_value() {
777 let result =
778 PproxyArgs::parse(&["-l".into(), "socks5://127.0.0.1:1080".into(), "-s".into()]);
779 assert!(result.is_err());
780 }
781
782 #[test]
783 fn test_log_missing_value() {
784 let result = PproxyArgs::parse(&[
785 "-l".into(),
786 "socks5://127.0.0.1:1080".into(),
787 "--log".into(),
788 ]);
789 assert!(result.is_err());
790 }
791
792 #[test]
793 fn test_udp_listen_missing_value() {
794 let result =
795 PproxyArgs::parse(&["-l".into(), "socks5://127.0.0.1:1080".into(), "-ul".into()]);
796 assert!(result.is_err());
797 }
798
799 #[test]
800 fn test_udp_remote_missing_value() {
801 let result =
802 PproxyArgs::parse(&["-l".into(), "socks5://127.0.0.1:1080".into(), "-ur".into()]);
803 assert!(result.is_err());
804 }
805
806 #[test]
807 fn test_rulefile_missing_value() {
808 let result = PproxyArgs::parse(&[
809 "-l".into(),
810 "socks5://127.0.0.1:1080".into(),
811 "--rulefile".into(),
812 ]);
813 assert!(result.is_err());
814 }
815
816 #[test]
817 fn test_alive_missing_value() {
818 let result =
819 PproxyArgs::parse(&["-l".into(), "socks5://127.0.0.1:1080".into(), "-a".into()]);
820 assert!(result.is_err());
821 }
822
823 #[test]
824 fn test_ssl_missing_value() {
825 let result = PproxyArgs::parse(&[
826 "-l".into(),
827 "socks5://127.0.0.1:1080".into(),
828 "--ssl".into(),
829 ]);
830 assert!(result.is_err());
831 }
832
833 #[test]
834 fn test_block_missing_value() {
835 let result =
836 PproxyArgs::parse(&["-l".into(), "socks5://127.0.0.1:1080".into(), "-b".into()]);
837 assert!(result.is_err());
838 }
839
840 #[test]
841 fn test_parse_known_flags_pac_test_sys_reuse_get() {
842 let args = PproxyArgs::parse(&[
843 "-l".into(),
844 "socks5://127.0.0.1:1080".into(),
845 "--pac".into(),
846 "/proxy.pac".into(),
847 "--test".into(),
848 "http://example.com".into(),
849 "--sys".into(),
850 "--reuse".into(),
851 "--get".into(),
852 "/index.html,body.txt".into(),
853 ])
854 .unwrap();
855 assert!(args.system_proxy);
856 assert!(args.reuse_port);
857 let warnings = args.unknown_flag_diagnostics();
858 assert!(warnings.is_empty(), "unexpected warnings: {:?}", warnings);
859 }
860
861 #[test]
862 fn test_verbose_level_single() {
863 let args = PproxyArgs::parse(&["-l".into(), "http://:8080".into(), "-v".into()]).unwrap();
864 assert_eq!(args.verbose_level, 1);
865 }
866
867 #[test]
868 fn test_verbose_level_double() {
869 let args = PproxyArgs::parse(&["-l".into(), "http://:8080".into(), "-vv".into()]).unwrap();
870 assert_eq!(args.verbose_level, 2);
871 }
872
873 #[test]
874 fn test_verbose_level_triple() {
875 let args = PproxyArgs::parse(&["-l".into(), "http://:8080".into(), "-vvv".into()]).unwrap();
876 assert_eq!(args.verbose_level, 3);
877 }
878
879 #[test]
880 fn test_verbose_level_default_zero() {
881 let args = PproxyArgs::parse(&["-l".into(), "http://:8080".into()]).unwrap();
882 assert_eq!(args.verbose_level, 0);
883 }
884
885 #[test]
886 fn test_verbose_level_max_of_multiple() {
887 let args = PproxyArgs::parse(&[
888 "-l".into(),
889 "http://:8080".into(),
890 "-v".into(),
891 "-vvv".into(),
892 ])
893 .unwrap();
894 assert_eq!(args.verbose_level, 4);
895 }
896
897 #[test]
898 fn test_has_args_true() {
899 assert!(PproxyArgs::has_args(&["-l".into(), "http://:8080".into()]));
900 }
901
902 #[test]
903 fn test_has_args_false_empty() {
904 assert!(!PproxyArgs::has_args(&[]));
905 }
906
907 #[test]
908 fn test_default_args() {
909 let args = PproxyArgs::default_args();
910 assert_eq!(args.local, vec!["http+socks4+socks5://:8080"]);
911 assert!(args.remotes.is_empty());
912 assert_eq!(args.verbose_level, 0);
913 assert!(!args.debug);
914 assert!(!args.daemon);
915 assert!(!args.reuse_port);
916 assert!(args.auth_timeout.is_none());
917 assert!(!args.system_proxy);
918 }
919
920 #[test]
921 fn test_default_args_translates() {
922 let args = PproxyArgs::default_args();
923 let output = super::super::translate::translate_pproxy_args(&args).unwrap();
924 assert!(output.toml.contains("8080"));
925 assert!(output.toml.contains("socks5") || output.toml.contains("http"));
926 assert!(!output.has_unsupported());
927 }
928
929 #[test]
930 fn test_parse_reuse_flag() {
931 let args = PproxyArgs::parse(&[
932 "-l".into(),
933 "socks5://127.0.0.1:1080".into(),
934 "--reuse".into(),
935 ])
936 .unwrap();
937 assert!(args.reuse_port);
938 }
939
940 #[test]
941 fn test_parse_auth_valid() {
942 let args = PproxyArgs::parse(&[
943 "-l".into(),
944 "socks5://127.0.0.1:1080".into(),
945 "--auth".into(),
946 "3600".into(),
947 ])
948 .unwrap();
949 assert_eq!(args.auth_timeout, Some(Duration::from_secs(3600)));
950 }
951
952 #[test]
953 fn test_parse_auth_zero() {
954 let args = PproxyArgs::parse(&[
955 "-l".into(),
956 "socks5://127.0.0.1:1080".into(),
957 "--auth".into(),
958 "0".into(),
959 ])
960 .unwrap();
961 assert_eq!(args.auth_timeout, Some(Duration::from_secs(0)));
962 }
963
964 #[test]
965 fn test_parse_auth_invalid_non_numeric() {
966 let result = PproxyArgs::parse(&[
967 "-l".into(),
968 "socks5://127.0.0.1:1080".into(),
969 "--auth".into(),
970 "abc".into(),
971 ]);
972 assert!(result.is_err());
973 }
974
975 #[test]
976 fn test_parse_auth_overflow() {
977 let result = PproxyArgs::parse(&[
978 "-l".into(),
979 "socks5://127.0.0.1:1080".into(),
980 "--auth".into(),
981 "9223372036854775808".into(),
982 ]);
983 assert!(result.is_err());
984 }
985
986 #[test]
987 fn test_parse_auth_negative_matches_argparse_integer() {
988 let args = PproxyArgs::parse(&[
989 "-l".into(),
990 "http://127.0.0.1:0".into(),
991 "--auth".into(),
992 "-1".into(),
993 ])
994 .unwrap();
995 assert_eq!(args.auth_timeout, Some(Duration::ZERO));
996 }
997
998 #[test]
999 fn test_parse_auth_missing_value() {
1000 let result = PproxyArgs::parse(&[
1001 "-l".into(),
1002 "socks5://127.0.0.1:1080".into(),
1003 "--auth".into(),
1004 ]);
1005 assert!(result.is_err());
1006 }
1007
1008 #[test]
1009 fn test_parse_sys_flag() {
1010 let args = PproxyArgs::parse(&[
1011 "-l".into(),
1012 "socks5://127.0.0.1:1080".into(),
1013 "--sys".into(),
1014 ])
1015 .unwrap();
1016 assert!(args.system_proxy);
1017 }
1018
1019 #[test]
1020 fn test_has_unknown_or_unsupported_with_unknown() {
1021 let args =
1022 PproxyArgs::parse(&["-l".into(), "http://:8080".into(), "--bogus".into()]).unwrap();
1023 assert!(args.has_unknown_or_unsupported());
1024 }
1025
1026 #[test]
1027 fn test_has_unknown_or_unsupported_with_daemon() {
1028 let args =
1029 PproxyArgs::parse(&["-l".into(), "http://:8080".into(), "--daemon".into()]).unwrap();
1030 assert!(args.has_unknown_or_unsupported());
1031 }
1032
1033 #[test]
1034 fn test_no_raw_flags_field() {
1035 let args = PproxyArgs::parse(&["-l".into(), "http://:8080".into()]).unwrap();
1036 assert!(args.known_unsupported.is_empty());
1037 assert!(args.unknown_flags.is_empty());
1038 }
1039
1040 #[test]
1041 fn test_default_log_level_no_flags() {
1042 let args = PproxyArgs::parse(&["-l".into(), "http://:8080".into()]).unwrap();
1043 assert_eq!(args.default_log_level(), "info");
1044 }
1045
1046 #[test]
1047 fn test_default_log_level_d_flag() {
1048 let args = PproxyArgs::parse(&["-l".into(), "http://:8080".into(), "-d".into()]).unwrap();
1049 assert_eq!(args.default_log_level(), "debug");
1050 }
1051
1052 #[test]
1053 fn test_default_log_level_v_flag() {
1054 let args = PproxyArgs::parse(&["-l".into(), "http://:8080".into(), "-v".into()]).unwrap();
1055 assert_eq!(args.default_log_level(), "debug");
1056 }
1057
1058 #[test]
1059 fn test_default_log_level_vv_flag() {
1060 let args = PproxyArgs::parse(&["-l".into(), "http://:8080".into(), "-vv".into()]).unwrap();
1061 assert_eq!(args.default_log_level(), "debug");
1062 }
1063
1064 #[test]
1065 fn test_default_log_level_vvv_flag() {
1066 let args = PproxyArgs::parse(&["-l".into(), "http://:8080".into(), "-vvv".into()]).unwrap();
1067 assert_eq!(args.default_log_level(), "trace");
1068 }
1069
1070 #[test]
1071 fn test_default_log_level_d_and_vv() {
1072 let args = PproxyArgs::parse(&[
1073 "-l".into(),
1074 "http://:8080".into(),
1075 "-d".into(),
1076 "-vv".into(),
1077 ])
1078 .unwrap();
1079 assert_eq!(args.default_log_level(), "debug");
1080 }
1081
1082 #[test]
1083 fn test_default_log_level_d_and_vvv() {
1084 let args = PproxyArgs::parse(&[
1085 "-l".into(),
1086 "http://:8080".into(),
1087 "-d".into(),
1088 "-vvv".into(),
1089 ])
1090 .unwrap();
1091 assert_eq!(args.default_log_level(), "trace");
1092 }
1093
1094 #[test]
1095 fn test_default_log_level_daemon_only_keeps_daemon_separate() {
1096 let args =
1097 PproxyArgs::parse(&["-l".into(), "http://:8080".into(), "--daemon".into()]).unwrap();
1098 assert!(args.daemon);
1099 assert!(!args.debug);
1100 assert_eq!(args.default_log_level(), "info");
1101 }
1102
1103 #[test]
1104 fn test_default_log_level_d_still_works_with_daemon() {
1105 let args = PproxyArgs::parse(&[
1106 "-l".into(),
1107 "http://:8080".into(),
1108 "-d".into(),
1109 "--daemon".into(),
1110 ])
1111 .unwrap();
1112 assert!(args.debug);
1113 assert_eq!(args.default_log_level(), "debug");
1114 }
1115
1116 #[test]
1117 fn test_default_log_level_default_args() {
1118 let args = PproxyArgs::default_args();
1119 assert_eq!(args.default_log_level(), "info");
1120 }
1121
1122 #[test]
1126 fn test_baseline_value_arity() {
1127 let cases: &[(&[&str], bool)] = &[
1130 (&["-l", "http://:8080"], true),
1132 (&["-r", "http://proxy:8080"], true),
1133 (&["-ul", "socks5://:1081"], true),
1134 (&["-ur", "socks5://proxy:1080"], true),
1135 (&["--ssl", "cert.pem,key.pem"], true),
1136 (&["--pac", "/proxy.pac"], true),
1137 (&["--test", "http://example.com"], true),
1138 (&["--auth", "3600"], true),
1139 (&["--get", "/index.html,body.txt"], true),
1140 (&["-s", "rr"], true),
1141 (&["-a", "10"], true),
1142 (&["-b", ".*\\.example\\.com"], true),
1143 (&["--rulefile", "rules.txt"], true),
1144 (&["--log", "access.log"], true),
1145 (&["-v"], false),
1147 (&["-d"], false),
1148 (&["--daemon"], false),
1149 (&["--sys"], false),
1150 (&["--reuse"], false),
1151 ];
1152
1153 for (args_slice, expects_value) in cases {
1154 let raw: Vec<String> = args_slice.iter().map(|s| s.to_string()).collect();
1155 let result = PproxyArgs::parse(&raw);
1156 if *expects_value && raw.len() == 1 {
1157 assert!(
1159 result.is_err(),
1160 "expected error for {:?} (missing value), got Ok",
1161 args_slice
1162 );
1163 } else {
1164 assert!(
1166 result.is_ok(),
1167 "expected Ok for {:?}, got Err: {:?}",
1168 args_slice,
1169 result.err()
1170 );
1171 }
1172 }
1173 }
1174}