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>,
77 pub ssl: Option<String>,
81 pub pac: Option<String>,
84 pub test_value: Option<String>,
87 pub udp_listen: Vec<String>,
90 pub udp_remote: Vec<String>,
93 pub scheduler: Option<String>,
96 pub alive: Option<String>,
99 pub block_values: Vec<String>,
102 pub log_values: Vec<String>,
105 pub rulefile_values: Vec<String>,
108 pub get_values: Vec<String>,
111 pub unknown_flags: Vec<String>,
113 pub strict_violations: Vec<String>,
117}
118
119impl PproxyArgs {
120 pub fn has_args(raw: &[String]) -> bool {
122 !raw.is_empty()
123 }
124
125 pub fn test_target(&self) -> Option<&str> {
129 self.test_value.as_deref()
130 }
131
132 pub fn recognized_option_names() -> &'static [&'static str] {
137 &[
138 "-l",
139 "-r",
140 "-ul",
141 "-ur",
142 "-b",
143 "-a",
144 "-s",
145 "-d",
146 "-v",
147 "--ssl",
148 "--pac",
149 "--test",
150 "--sys",
151 "--reuse",
152 "--auth",
153 "--get",
154 "--daemon",
155 "--version",
156 "-h",
157 "--help",
158 ]
159 }
160
161 pub fn udp_listen_addrs(&self) -> Vec<&str> {
173 self.udp_listen.iter().map(String::as_str).collect()
174 }
175
176 pub fn udp_remote_addrs(&self) -> Vec<&str> {
178 self.udp_remote.iter().map(String::as_str).collect()
179 }
180
181 pub fn tls_requested(&self) -> bool {
183 self.ssl.is_some()
184 }
185
186 pub fn tls_value(&self) -> Option<&str> {
188 self.ssl.as_deref()
189 }
190
191 pub fn pac_requested(&self) -> bool {
193 self.pac.is_some()
194 }
195
196 pub fn pac_value(&self) -> Option<&str> {
198 self.pac.as_deref()
199 }
200
201 pub fn scheduler_value(&self) -> Option<&str> {
203 self.scheduler.as_deref()
204 }
205
206 pub fn alive_value(&self) -> Option<&str> {
208 self.alive.as_deref()
209 }
210
211 pub fn default_args() -> Self {
216 Self {
217 local: vec!["http+socks4+socks5://:8080".to_string()],
218 remotes: vec![],
219 verbose_level: 0,
220 debug: false,
221 debug_level: 0,
222 daemon: false,
223 reuse_port: false,
224 auth_timeout: None,
225 system_proxy: false,
226 help: false,
227 version: false,
228 known_unsupported: vec![],
229 ssl: None,
230 pac: None,
231 test_value: None,
232 udp_listen: vec![],
233 udp_remote: vec![],
234 scheduler: None,
235 alive: None,
236 block_values: vec![],
237 log_values: vec![],
238 rulefile_values: vec![],
239 get_values: vec![],
240 unknown_flags: vec![],
241 strict_violations: vec![],
242 }
243 }
244
245 pub fn parse(raw: &[String]) -> Result<Self, CompatError> {
247 let mut local = Vec::new();
248 let mut remotes = Vec::new();
249 let mut verbose_level: u8 = 0;
250 let mut debug = false;
251 let mut debug_level: u8 = 0;
252 let mut daemon = false;
253 let mut reuse_port = false;
254 let mut auth_timeout: Option<Duration> = None;
255 let mut system_proxy = false;
256 let mut help = false;
257 let mut version = false;
258 let mut known_unsupported = Vec::new();
259 let mut ssl: Option<String> = None;
260 let mut pac: Option<String> = None;
261 let mut test_value: Option<String> = None;
262 let mut udp_listen: Vec<String> = Vec::new();
263 let mut udp_remote: Vec<String> = Vec::new();
264 let mut scheduler: Option<String> = None;
265 let mut alive: Option<String> = None;
266 let mut block_values: Vec<String> = Vec::new();
267 let mut log_values: Vec<String> = Vec::new();
268 let mut rulefile_values: Vec<String> = Vec::new();
269 let mut get_values: Vec<String> = Vec::new();
270 let mut unknown_flags = Vec::new();
271 let mut strict_violations = Vec::new();
272 let mut i = 0;
273
274 while i < raw.len() {
275 let arg = &raw[i];
276 match arg.as_str() {
277 "-h" | "--help" => {
278 help = true;
279 }
280 "--version" => {
281 version = true;
282 }
283 "-l" | "--listen" => {
284 if arg == "--listen" {
285 strict_violations.push(arg.clone());
286 }
287 local.push(take_required_value(raw, &mut i, arg)?);
288 }
289 "-r" | "--remote" => {
290 if arg == "--remote" {
291 strict_violations.push(arg.clone());
292 }
293 remotes.push(take_required_value(raw, &mut i, arg)?);
294 }
295 "--daemon" => {
296 daemon = true;
297 }
298 "-d" => {
299 debug = true;
300 debug_level = debug_level.saturating_add(1);
301 }
302 "--log" | "-log" => {
303 let value = take_required_value(raw, &mut i, arg)?;
304 known_unsupported.push(format!("log={value}"));
305 log_values.push(value);
306 strict_violations.push(arg.clone());
307 }
308 "-ul" | "--udp-listen" => {
309 if arg == "--udp-listen" {
310 strict_violations.push(arg.clone());
311 }
312 let value = take_required_value(raw, &mut i, arg)?;
313 known_unsupported.push(format!("udp-listen={value}"));
314 udp_listen.push(value);
315 }
316 "-ur" | "--udp-remote" => {
317 if arg == "--udp-remote" {
318 strict_violations.push(arg.clone());
319 }
320 let value = take_required_value(raw, &mut i, arg)?;
321 known_unsupported.push(format!("udp-remote={value}"));
322 udp_remote.push(value);
323 }
324 "--rulefile" | "-rulefile" => {
325 let value = take_required_value(raw, &mut i, arg)?;
326 known_unsupported.push(format!("rulefile={value}"));
327 rulefile_values.push(value);
328 strict_violations.push(arg.clone());
329 }
330 "-v" | "-vv" | "-vvv" => {
331 verbose_level = verbose_level.saturating_add(arg.len() as u8 - 1);
332 }
333 short
334 if short.len() > 2
335 && short.starts_with('-')
336 && short[1..].chars().all(|c| c == 'd' || c == 'v') =>
337 {
338 for flag in short[1..].chars() {
339 if flag == 'd' {
340 debug = true;
341 debug_level = debug_level.saturating_add(1);
342 } else {
343 verbose_level = verbose_level.saturating_add(1);
344 }
345 }
346 }
347 "-s" => {
348 let value = take_required_value(raw, &mut i, arg)?;
349 if !matches!(
350 value.as_str(),
351 "fa" | "rr"
352 | "rc"
353 | "lc"
354 | "first_available"
355 | "round_robin"
356 | "random_choice"
357 | "least_connection"
358 ) {
359 return Err(CompatError::InvalidArgs {
360 message: format!(
361 "invalid choice: '{}' (choose from fa, rr, rc, lc)",
362 value
363 ),
364 });
365 }
366 if !matches!(value.as_str(), "fa" | "rr" | "rc" | "lc") {
367 strict_violations.push(format!("-s {value}"));
370 }
371 known_unsupported.push(format!("scheduler={value}"));
372 scheduler = Some(value.clone());
373 }
374 "-a" => {
375 let value = take_required_value(raw, &mut i, arg)?;
376 known_unsupported.push(format!("alive={value}"));
377 alive = Some(value);
378 }
379 "--ssl" => {
380 let value = take_required_value(raw, &mut i, arg)?;
381 known_unsupported.push(format!("ssl={value}"));
382 ssl = Some(value);
383 }
384 "-b" => {
385 let value = take_required_value(raw, &mut i, arg)?;
386 known_unsupported.push(format!("block={value}"));
387 block_values.push(value);
388 }
389 "--pac" => {
390 let value = take_required_value(raw, &mut i, arg)?;
391 known_unsupported.push(format!("pac={value}"));
392 pac = Some(value);
393 }
394 "--test" => {
395 let value = take_required_value(raw, &mut i, arg)?;
396 known_unsupported.push(format!("test={value}"));
397 test_value = Some(value);
398 }
399 "--sys" => {
400 system_proxy = true;
401 }
402 "--reuse" => {
403 reuse_port = true;
404 }
405 "--get" => {
406 let value = take_required_value(raw, &mut i, arg)?;
407 known_unsupported.push(format!("get={value}"));
408 get_values.push(value);
409 }
410 "--auth" => {
411 let value = take_required_value(raw, &mut i, arg)?;
412 auth_timeout = Some(parse_auth_duration(&value)?);
413 }
414 other if other.starts_with('-') => {
415 unknown_flags.push(other.to_string());
416 }
417 other => {
418 if local.is_empty() {
423 local.push(other.to_string());
424 } else {
425 remotes.push(other.to_string());
426 }
427 strict_violations.push(other.to_string());
428 }
429 }
430 i += 1;
431 }
432
433 Ok(PproxyArgs {
434 local,
435 remotes,
436 verbose_level,
437 debug,
438 debug_level,
439 daemon,
440 reuse_port,
441 auth_timeout,
442 system_proxy,
443 help,
444 version,
445 known_unsupported,
446 ssl,
447 pac,
448 test_value,
449 udp_listen,
450 udp_remote,
451 scheduler,
452 alive,
453 block_values,
454 log_values,
455 rulefile_values,
456 get_values,
457 unknown_flags,
458 strict_violations,
459 })
460 }
461
462 pub fn unknown_flag_diagnostics(&self) -> Vec<CompatWarning> {
464 let mut warnings = Vec::new();
465 for flag in &self.unknown_flags {
466 warnings.push(CompatWarning {
467 category: "unknown-flag",
468 message: format!("unrecognized option '{}'", flag),
469 });
470 }
471 warnings
472 }
473
474 pub fn unknown_flag_translation_output(&self) -> TranslationOutput {
476 let warnings = self.unknown_flag_diagnostics();
477 TranslationOutput::new(String::new()).with_warnings(warnings)
478 }
479
480 pub fn has_unknown_or_unsupported(&self) -> bool {
487 !self.unknown_flags.is_empty() || !self.known_unsupported.is_empty() || self.daemon
488 }
489
490 pub fn strict_parser_violations(&self) -> Vec<String> {
492 self.unknown_flags
493 .iter()
494 .cloned()
495 .chain(self.strict_violations.iter().cloned())
496 .collect()
497 }
498
499 pub fn version_string() -> String {
501 format!("eggress-pproxy-compat {}", env!("CARGO_PKG_VERSION"))
502 }
503
504 pub fn validate_strict_values(&self) -> Result<(), CompatError> {
509 self.parse_local_uris()
510 .map_err(|error| CompatError::InvalidArgs {
511 message: format!("invalid -l URI: {error}"),
512 })?;
513 self.parse_remote_chains()
514 .map_err(|error| CompatError::InvalidArgs {
515 message: format!("invalid -r URI: {error}"),
516 })?;
517 if let Some(scheduler) = self.scheduler.as_deref() {
518 if !matches!(scheduler, "fa" | "rr" | "rc" | "lc") {
519 return Err(CompatError::InvalidArgs {
520 message: format!(
521 "invalid choice: '{}' (choose from fa, rr, rc, lc)",
522 scheduler
523 ),
524 });
525 }
526 }
527 if let Some(interval) = self.alive.as_deref() {
528 interval
529 .parse::<i64>()
530 .map_err(|_| CompatError::InvalidArgs {
531 message: format!("-a value '{}' is not a valid integer", interval),
532 })?;
533 }
534 for (kind, values) in [
535 ("udp-listen", self.udp_listen.iter()),
536 ("udp-remote", self.udp_remote.iter()),
537 ] {
538 for value in values {
539 crate::uri::parse_pproxy_uri(value).map_err(|error| CompatError::InvalidArgs {
540 message: format!("invalid {kind} URI: {error}"),
541 })?;
542 }
543 }
544 Ok(())
545 }
546
547 pub fn effective_auth_timeout(&self) -> Duration {
551 self.auth_timeout
552 .unwrap_or_else(|| Duration::from_secs(86_400 * 30))
553 }
554
555 pub fn default_log_level(&self) -> &'static str {
572 if self.verbose_level >= 3 {
573 "trace"
574 } else if self.verbose_level >= 1 || self.debug {
575 "debug"
576 } else {
577 "info"
578 }
579 }
580
581 pub fn parse_local_uris(&self) -> Result<Vec<PproxyUri>, CompatError> {
583 self.local
584 .iter()
585 .map(|s| crate::uri::parse_pproxy_uri(s))
586 .collect()
587 }
588
589 pub fn parse_remote_uris(&self) -> Result<Vec<PproxyUri>, CompatError> {
591 self.remotes
592 .iter()
593 .map(|s| crate::uri::parse_pproxy_uri(s))
594 .collect()
595 }
596
597 pub fn parse_remote_chains(&self) -> Result<Vec<PproxyChain>, CompatError> {
599 self.remotes
600 .iter()
601 .map(|s| crate::uri::parse_pproxy_chain(s))
602 .collect()
603 }
604}
605
606#[cfg(test)]
607mod tests {
608 use super::*;
609
610 #[test]
611 fn test_parse_basic() {
612 let args = PproxyArgs::parse(&[
613 "-l".into(),
614 "socks5://127.0.0.1:1080".into(),
615 "-r".into(),
616 "http://proxy:8080".into(),
617 ])
618 .unwrap();
619 assert_eq!(args.local.len(), 1);
620 assert_eq!(args.remotes.len(), 1);
621 assert_eq!(args.local[0], "socks5://127.0.0.1:1080");
622 assert_eq!(args.remotes[0], "http://proxy:8080");
623 }
624
625 #[test]
626 fn test_parse_help_and_version_actions() {
627 let help = PproxyArgs::parse(&["--help".into()]).unwrap();
628 assert!(help.help);
629 assert!(!help.version);
630 assert!(help.strict_parser_violations().is_empty());
631
632 let version = PproxyArgs::parse(&["--version".into()]).unwrap();
633 assert!(version.version);
634 assert!(!version.help);
635 assert_eq!(
636 PproxyArgs::version_string(),
637 format!("eggress-pproxy-compat {}", env!("CARGO_PKG_VERSION"))
638 );
639 }
640
641 #[test]
642 fn test_parse_positional() {
643 let args =
644 PproxyArgs::parse(&["socks5://127.0.0.1:1080".into(), "http://proxy:8080".into()])
645 .unwrap();
646 assert_eq!(args.local.len(), 1);
647 assert_eq!(args.remotes.len(), 1);
648 }
649
650 #[test]
651 fn test_parse_multiple_remotes() {
652 let args = PproxyArgs::parse(&[
653 "-l".into(),
654 "socks5://127.0.0.1:1080".into(),
655 "-r".into(),
656 "http://proxy1:8080".into(),
657 "-r".into(),
658 "socks5://proxy2:1080".into(),
659 ])
660 .unwrap();
661 assert_eq!(args.remotes.len(), 2);
662 }
663
664 #[test]
665 fn test_parse_missing_value() {
666 let result = PproxyArgs::parse(&["-l".into()]);
667 assert!(result.is_err());
668 }
669
670 #[test]
671 fn test_parse_daemon_flag() {
672 let args = PproxyArgs::parse(&[
673 "-l".into(),
674 "socks5://127.0.0.1:1080".into(),
675 "--daemon".into(),
676 ])
677 .unwrap();
678 assert!(args.daemon);
679 assert!(!args.debug);
680 }
681
682 #[test]
683 fn test_parse_debug_flag() {
684 let args = PproxyArgs::parse(&["-l".into(), "socks5://127.0.0.1:1080".into(), "-d".into()])
685 .unwrap();
686 assert!(args.debug);
687 assert_eq!(args.debug_level, 1);
688 assert!(!args.daemon);
689 }
690
691 #[test]
692 fn test_count_actions_and_short_clusters() {
693 let args = PproxyArgs::parse(&[
694 "-l".into(),
695 "http://:8080".into(),
696 "-dd".into(),
697 "-v".into(),
698 "-vv".into(),
699 ])
700 .unwrap();
701 assert_eq!(args.debug_level, 2);
702 assert_eq!(args.verbose_level, 3);
703
704 let clustered =
705 PproxyArgs::parse(&["-l".into(), "http://:8080".into(), "-dv".into()]).unwrap();
706 assert_eq!(clustered.debug_level, 1);
707 assert_eq!(clustered.verbose_level, 1);
708 }
709
710 #[test]
711 fn test_strict_parser_rejects_extensions_and_positionals() {
712 let args = PproxyArgs::parse(&[
713 "--log".into(),
714 "access.log".into(),
715 "proxy://listener".into(),
716 ])
717 .unwrap();
718 assert!(args
719 .strict_parser_violations()
720 .iter()
721 .any(|value| value == "--log"));
722 assert!(args
723 .strict_parser_violations()
724 .iter()
725 .any(|value| value == "proxy://listener"));
726 }
727
728 #[test]
729 fn test_target_preserves_value_taking_option() {
730 let args = PproxyArgs::parse(&[
731 "-l".into(),
732 "http://:8080".into(),
733 "--test".into(),
734 "https://example.invalid/health".into(),
735 ])
736 .unwrap();
737 assert_eq!(args.test_target(), Some("https://example.invalid/health"));
738 assert_eq!(
739 args.test_value.as_deref(),
740 Some("https://example.invalid/health")
741 );
742 }
743
744 #[test]
745 fn structured_fields_match_legacy_bucket() {
746 let args = PproxyArgs::parse(&[
747 "-l".into(),
748 "http://:8080".into(),
749 "--ssl".into(),
750 "cert.pem,key.pem".into(),
751 "--pac".into(),
752 "/proxy.pac".into(),
753 "--test".into(),
754 "http://example.com".into(),
755 "-ul".into(),
756 "socks5://:1081".into(),
757 "-ur".into(),
758 "socks5://proxy:1080".into(),
759 "-s".into(),
760 "rr".into(),
761 "-a".into(),
762 "10".into(),
763 "-b".into(),
764 ".*\\.example\\.com".into(),
765 "--log".into(),
766 "access.log".into(),
767 "--rulefile".into(),
768 "rules.txt".into(),
769 "--get".into(),
770 "/index.html,body.txt".into(),
771 ])
772 .unwrap();
773 assert!(args.tls_requested());
776 assert_eq!(args.tls_value(), Some("cert.pem,key.pem"));
777 assert!(args.pac_requested());
778 assert_eq!(args.pac_value(), Some("/proxy.pac"));
779 assert_eq!(args.test_target(), Some("http://example.com"));
780 assert_eq!(args.udp_listen_addrs(), vec!["socks5://:1081"]);
781 assert_eq!(args.udp_remote_addrs(), vec!["socks5://proxy:1080"]);
782 assert_eq!(args.scheduler_value(), Some("rr"));
783 assert_eq!(args.alive_value(), Some("10"));
784 assert_eq!(args.block_values, vec![".*\\.example\\.com".to_string()]);
785 assert_eq!(args.log_values, vec!["access.log".to_string()]);
786 assert_eq!(args.rulefile_values, vec!["rules.txt".to_string()]);
787 assert_eq!(args.get_values, vec!["/index.html,body.txt".to_string()]);
788 for entry in [
790 "ssl=cert.pem,key.pem",
791 "pac=/proxy.pac",
792 "test=http://example.com",
793 "udp-listen=socks5://:1081",
794 "udp-remote=socks5://proxy:1080",
795 "scheduler=rr",
796 "alive=10",
797 ] {
798 assert!(
799 args.known_unsupported.contains(&entry.to_string()),
800 "legacy bucket must still carry '{entry}'"
801 );
802 }
803 }
804
805 #[test]
806 fn test_d_and_daemon_independent() {
807 let args = PproxyArgs::parse(&[
808 "-l".into(),
809 "socks5://127.0.0.1:1080".into(),
810 "-d".into(),
811 "--daemon".into(),
812 ])
813 .unwrap();
814 assert!(args.debug);
815 assert!(args.daemon);
816 }
817
818 #[test]
819 fn test_d_never_sets_daemon() {
820 let args = PproxyArgs::parse(&["-l".into(), "http://:8080".into(), "-d".into()]).unwrap();
821 assert!(args.debug);
822 assert!(!args.daemon);
823 }
824
825 #[test]
826 fn test_daemon_never_sets_debug() {
827 let args =
828 PproxyArgs::parse(&["-l".into(), "http://:8080".into(), "--daemon".into()]).unwrap();
829 assert!(!args.debug);
830 assert!(args.daemon);
831 }
832
833 #[test]
834 fn test_parse_verbose_flag() {
835 let args = PproxyArgs::parse(&["-l".into(), "socks5://127.0.0.1:1080".into(), "-v".into()])
836 .unwrap();
837 assert_eq!(args.verbose_level, 1);
838 }
839
840 #[test]
841 fn test_parse_scheduler_flag() {
842 let args = PproxyArgs::parse(&[
843 "-l".into(),
844 "socks5://127.0.0.1:1080".into(),
845 "-s".into(),
846 "rr".into(),
847 ])
848 .unwrap();
849 assert!(args.known_unsupported.contains(&"scheduler=rr".to_string()));
850 }
851
852 #[test]
853 fn test_scheduler_migration_alias_is_not_strict_executable_surface() {
854 let args = PproxyArgs::parse(&[
855 "-l".into(),
856 "socks5://127.0.0.1:1080".into(),
857 "-s".into(),
858 "round_robin".into(),
859 ])
860 .unwrap();
861 assert!(args
862 .strict_parser_violations()
863 .iter()
864 .any(|value| value == "-s round_robin"));
865 assert!(args.validate_strict_values().is_err());
866 }
867
868 #[test]
869 fn test_parse_alive_flag() {
870 let args = PproxyArgs::parse(&[
871 "-l".into(),
872 "socks5://127.0.0.1:1080".into(),
873 "-a".into(),
874 "10".into(),
875 ])
876 .unwrap();
877 assert!(args.known_unsupported.contains(&"alive=10".to_string()));
878 }
879
880 #[test]
881 fn test_parse_ssl_flag() {
882 let args = PproxyArgs::parse(&[
883 "-l".into(),
884 "socks5://127.0.0.1:1080".into(),
885 "--ssl".into(),
886 "cert.pem,key.pem".into(),
887 ])
888 .unwrap();
889 assert!(args
890 .known_unsupported
891 .contains(&"ssl=cert.pem,key.pem".to_string()));
892 }
893
894 #[test]
895 fn test_parse_block_flag() {
896 let args = PproxyArgs::parse(&[
897 "-l".into(),
898 "socks5://127.0.0.1:1080".into(),
899 "-b".into(),
900 ".*\\.example\\.com".into(),
901 ])
902 .unwrap();
903 assert!(args
904 .known_unsupported
905 .contains(&"block=.*\\.example\\.com".to_string()));
906 }
907
908 #[test]
909 fn test_parse_log_flag() {
910 let args = PproxyArgs::parse(&[
911 "-l".into(),
912 "socks5://127.0.0.1:1080".into(),
913 "--log".into(),
914 "access.log".into(),
915 ])
916 .unwrap();
917 assert!(args
918 .known_unsupported
919 .contains(&"log=access.log".to_string()));
920 }
921
922 #[test]
923 fn test_parse_udp_flags() {
924 let args = PproxyArgs::parse(&[
925 "-l".into(),
926 "socks5://127.0.0.1:1080".into(),
927 "-ul".into(),
928 "socks5://:1081".into(),
929 "-ur".into(),
930 "socks5://proxy:1080".into(),
931 ])
932 .unwrap();
933 assert!(args
934 .known_unsupported
935 .contains(&"udp-listen=socks5://:1081".to_string()));
936 assert!(args
937 .known_unsupported
938 .contains(&"udp-remote=socks5://proxy:1080".to_string()));
939 }
940
941 #[test]
942 fn test_parse_rulefile_flag() {
943 let args = PproxyArgs::parse(&[
944 "-l".into(),
945 "socks5://127.0.0.1:1080".into(),
946 "--rulefile".into(),
947 "rules.txt".into(),
948 ])
949 .unwrap();
950 assert!(args
951 .known_unsupported
952 .contains(&"rulefile=rules.txt".to_string()));
953 }
954
955 #[test]
956 fn test_unknown_flag_diagnostics() {
957 let args = PproxyArgs::parse(&[
958 "-l".into(),
959 "socks5://127.0.0.1:1080".into(),
960 "--unknown-flag".into(),
961 "-x".into(),
962 ])
963 .unwrap();
964 let warnings = args.unknown_flag_diagnostics();
965 assert_eq!(warnings.len(), 2);
966 assert!(warnings
967 .iter()
968 .any(|w| w.message.contains("--unknown-flag")));
969 assert!(warnings.iter().any(|w| w.message.contains("-x")));
970 }
971
972 #[test]
973 fn test_known_flags_no_unknown_warnings() {
974 let args = PproxyArgs::parse(&[
975 "-l".into(),
976 "socks5://127.0.0.1:1080".into(),
977 "-v".into(),
978 "-s".into(),
979 "rr".into(),
980 "-a".into(),
981 "10".into(),
982 "--daemon".into(),
983 "--log".into(),
984 "access.log".into(),
985 "-ul".into(),
986 "socks5://:1081".into(),
987 "-ur".into(),
988 "socks5://proxy:1080".into(),
989 "--rulefile".into(),
990 "rules.txt".into(),
991 "--ssl".into(),
992 "cert.pem,key.pem".into(),
993 "-b".into(),
994 ".*\\.example\\.com".into(),
995 ])
996 .unwrap();
997 let warnings = args.unknown_flag_diagnostics();
998 assert!(warnings.is_empty());
999 }
1000
1001 #[test]
1002 fn test_scheduler_missing_value() {
1003 let result =
1004 PproxyArgs::parse(&["-l".into(), "socks5://127.0.0.1:1080".into(), "-s".into()]);
1005 assert!(result.is_err());
1006 }
1007
1008 #[test]
1009 fn test_log_missing_value() {
1010 let result = PproxyArgs::parse(&[
1011 "-l".into(),
1012 "socks5://127.0.0.1:1080".into(),
1013 "--log".into(),
1014 ]);
1015 assert!(result.is_err());
1016 }
1017
1018 #[test]
1019 fn test_udp_listen_missing_value() {
1020 let result =
1021 PproxyArgs::parse(&["-l".into(), "socks5://127.0.0.1:1080".into(), "-ul".into()]);
1022 assert!(result.is_err());
1023 }
1024
1025 #[test]
1026 fn test_udp_remote_missing_value() {
1027 let result =
1028 PproxyArgs::parse(&["-l".into(), "socks5://127.0.0.1:1080".into(), "-ur".into()]);
1029 assert!(result.is_err());
1030 }
1031
1032 #[test]
1033 fn test_rulefile_missing_value() {
1034 let result = PproxyArgs::parse(&[
1035 "-l".into(),
1036 "socks5://127.0.0.1:1080".into(),
1037 "--rulefile".into(),
1038 ]);
1039 assert!(result.is_err());
1040 }
1041
1042 #[test]
1043 fn test_alive_missing_value() {
1044 let result =
1045 PproxyArgs::parse(&["-l".into(), "socks5://127.0.0.1:1080".into(), "-a".into()]);
1046 assert!(result.is_err());
1047 }
1048
1049 #[test]
1050 fn test_ssl_missing_value() {
1051 let result = PproxyArgs::parse(&[
1052 "-l".into(),
1053 "socks5://127.0.0.1:1080".into(),
1054 "--ssl".into(),
1055 ]);
1056 assert!(result.is_err());
1057 }
1058
1059 #[test]
1060 fn test_block_missing_value() {
1061 let result =
1062 PproxyArgs::parse(&["-l".into(), "socks5://127.0.0.1:1080".into(), "-b".into()]);
1063 assert!(result.is_err());
1064 }
1065
1066 #[test]
1067 fn test_parse_known_flags_pac_test_sys_reuse_get() {
1068 let args = PproxyArgs::parse(&[
1069 "-l".into(),
1070 "socks5://127.0.0.1:1080".into(),
1071 "--pac".into(),
1072 "/proxy.pac".into(),
1073 "--test".into(),
1074 "http://example.com".into(),
1075 "--sys".into(),
1076 "--reuse".into(),
1077 "--get".into(),
1078 "/index.html,body.txt".into(),
1079 ])
1080 .unwrap();
1081 assert!(args.system_proxy);
1082 assert!(args.reuse_port);
1083 let warnings = args.unknown_flag_diagnostics();
1084 assert!(warnings.is_empty(), "unexpected warnings: {:?}", warnings);
1085 }
1086
1087 #[test]
1088 fn test_verbose_level_single() {
1089 let args = PproxyArgs::parse(&["-l".into(), "http://:8080".into(), "-v".into()]).unwrap();
1090 assert_eq!(args.verbose_level, 1);
1091 }
1092
1093 #[test]
1094 fn test_verbose_level_double() {
1095 let args = PproxyArgs::parse(&["-l".into(), "http://:8080".into(), "-vv".into()]).unwrap();
1096 assert_eq!(args.verbose_level, 2);
1097 }
1098
1099 #[test]
1100 fn test_verbose_level_triple() {
1101 let args = PproxyArgs::parse(&["-l".into(), "http://:8080".into(), "-vvv".into()]).unwrap();
1102 assert_eq!(args.verbose_level, 3);
1103 }
1104
1105 #[test]
1106 fn test_verbose_level_default_zero() {
1107 let args = PproxyArgs::parse(&["-l".into(), "http://:8080".into()]).unwrap();
1108 assert_eq!(args.verbose_level, 0);
1109 }
1110
1111 #[test]
1112 fn test_verbose_level_max_of_multiple() {
1113 let args = PproxyArgs::parse(&[
1114 "-l".into(),
1115 "http://:8080".into(),
1116 "-v".into(),
1117 "-vvv".into(),
1118 ])
1119 .unwrap();
1120 assert_eq!(args.verbose_level, 4);
1121 }
1122
1123 #[test]
1124 fn test_has_args_true() {
1125 assert!(PproxyArgs::has_args(&["-l".into(), "http://:8080".into()]));
1126 }
1127
1128 #[test]
1129 fn test_has_args_false_empty() {
1130 assert!(!PproxyArgs::has_args(&[]));
1131 }
1132
1133 #[test]
1134 fn test_default_args() {
1135 let args = PproxyArgs::default_args();
1136 assert_eq!(args.local, vec!["http+socks4+socks5://:8080"]);
1137 assert!(args.remotes.is_empty());
1138 assert_eq!(args.verbose_level, 0);
1139 assert!(!args.debug);
1140 assert!(!args.daemon);
1141 assert!(!args.reuse_port);
1142 assert!(args.auth_timeout.is_none());
1143 assert!(!args.system_proxy);
1144 }
1145
1146 #[test]
1147 fn test_default_args_translates() {
1148 let args = PproxyArgs::default_args();
1149 let output = super::super::translate::translate_pproxy_args(&args).unwrap();
1150 assert!(output.toml.contains("8080"));
1151 assert!(output.toml.contains("socks5") || output.toml.contains("http"));
1152 assert!(!output.has_unsupported());
1153 }
1154
1155 #[test]
1156 fn test_parse_reuse_flag() {
1157 let args = PproxyArgs::parse(&[
1158 "-l".into(),
1159 "socks5://127.0.0.1:1080".into(),
1160 "--reuse".into(),
1161 ])
1162 .unwrap();
1163 assert!(args.reuse_port);
1164 }
1165
1166 #[test]
1167 fn test_parse_auth_valid() {
1168 let args = PproxyArgs::parse(&[
1169 "-l".into(),
1170 "socks5://127.0.0.1:1080".into(),
1171 "--auth".into(),
1172 "3600".into(),
1173 ])
1174 .unwrap();
1175 assert_eq!(args.auth_timeout, Some(Duration::from_secs(3600)));
1176 }
1177
1178 #[test]
1179 fn test_parse_auth_zero() {
1180 let args = PproxyArgs::parse(&[
1181 "-l".into(),
1182 "socks5://127.0.0.1:1080".into(),
1183 "--auth".into(),
1184 "0".into(),
1185 ])
1186 .unwrap();
1187 assert_eq!(args.auth_timeout, Some(Duration::from_secs(0)));
1188 }
1189
1190 #[test]
1191 fn test_parse_auth_invalid_non_numeric() {
1192 let result = PproxyArgs::parse(&[
1193 "-l".into(),
1194 "socks5://127.0.0.1:1080".into(),
1195 "--auth".into(),
1196 "abc".into(),
1197 ]);
1198 assert!(result.is_err());
1199 }
1200
1201 #[test]
1202 fn test_parse_auth_overflow() {
1203 let result = PproxyArgs::parse(&[
1204 "-l".into(),
1205 "socks5://127.0.0.1:1080".into(),
1206 "--auth".into(),
1207 "9223372036854775808".into(),
1208 ]);
1209 assert!(result.is_err());
1210 }
1211
1212 #[test]
1213 fn test_parse_auth_negative_matches_argparse_integer() {
1214 let args = PproxyArgs::parse(&[
1215 "-l".into(),
1216 "http://127.0.0.1:0".into(),
1217 "--auth".into(),
1218 "-1".into(),
1219 ])
1220 .unwrap();
1221 assert_eq!(args.auth_timeout, Some(Duration::ZERO));
1222 }
1223
1224 #[test]
1225 fn test_parse_auth_missing_value() {
1226 let result = PproxyArgs::parse(&[
1227 "-l".into(),
1228 "socks5://127.0.0.1:1080".into(),
1229 "--auth".into(),
1230 ]);
1231 assert!(result.is_err());
1232 }
1233
1234 #[test]
1235 fn test_parse_sys_flag() {
1236 let args = PproxyArgs::parse(&[
1237 "-l".into(),
1238 "socks5://127.0.0.1:1080".into(),
1239 "--sys".into(),
1240 ])
1241 .unwrap();
1242 assert!(args.system_proxy);
1243 }
1244
1245 #[test]
1246 fn test_has_unknown_or_unsupported_with_unknown() {
1247 let args =
1248 PproxyArgs::parse(&["-l".into(), "http://:8080".into(), "--bogus".into()]).unwrap();
1249 assert!(args.has_unknown_or_unsupported());
1250 }
1251
1252 #[test]
1253 fn test_has_unknown_or_unsupported_with_daemon() {
1254 let args =
1255 PproxyArgs::parse(&["-l".into(), "http://:8080".into(), "--daemon".into()]).unwrap();
1256 assert!(args.has_unknown_or_unsupported());
1257 }
1258
1259 #[test]
1260 fn test_no_raw_flags_field() {
1261 let args = PproxyArgs::parse(&["-l".into(), "http://:8080".into()]).unwrap();
1262 assert!(args.known_unsupported.is_empty());
1263 assert!(args.unknown_flags.is_empty());
1264 }
1265
1266 #[test]
1267 fn test_default_log_level_no_flags() {
1268 let args = PproxyArgs::parse(&["-l".into(), "http://:8080".into()]).unwrap();
1269 assert_eq!(args.default_log_level(), "info");
1270 }
1271
1272 #[test]
1273 fn test_default_log_level_d_flag() {
1274 let args = PproxyArgs::parse(&["-l".into(), "http://:8080".into(), "-d".into()]).unwrap();
1275 assert_eq!(args.default_log_level(), "debug");
1276 }
1277
1278 #[test]
1279 fn test_default_log_level_v_flag() {
1280 let args = PproxyArgs::parse(&["-l".into(), "http://:8080".into(), "-v".into()]).unwrap();
1281 assert_eq!(args.default_log_level(), "debug");
1282 }
1283
1284 #[test]
1285 fn test_default_log_level_vv_flag() {
1286 let args = PproxyArgs::parse(&["-l".into(), "http://:8080".into(), "-vv".into()]).unwrap();
1287 assert_eq!(args.default_log_level(), "debug");
1288 }
1289
1290 #[test]
1291 fn test_default_log_level_vvv_flag() {
1292 let args = PproxyArgs::parse(&["-l".into(), "http://:8080".into(), "-vvv".into()]).unwrap();
1293 assert_eq!(args.default_log_level(), "trace");
1294 }
1295
1296 #[test]
1297 fn test_default_log_level_d_and_vv() {
1298 let args = PproxyArgs::parse(&[
1299 "-l".into(),
1300 "http://:8080".into(),
1301 "-d".into(),
1302 "-vv".into(),
1303 ])
1304 .unwrap();
1305 assert_eq!(args.default_log_level(), "debug");
1306 }
1307
1308 #[test]
1309 fn test_default_log_level_d_and_vvv() {
1310 let args = PproxyArgs::parse(&[
1311 "-l".into(),
1312 "http://:8080".into(),
1313 "-d".into(),
1314 "-vvv".into(),
1315 ])
1316 .unwrap();
1317 assert_eq!(args.default_log_level(), "trace");
1318 }
1319
1320 #[test]
1321 fn test_default_log_level_daemon_only_keeps_daemon_separate() {
1322 let args =
1323 PproxyArgs::parse(&["-l".into(), "http://:8080".into(), "--daemon".into()]).unwrap();
1324 assert!(args.daemon);
1325 assert!(!args.debug);
1326 assert_eq!(args.default_log_level(), "info");
1327 }
1328
1329 #[test]
1330 fn test_default_log_level_d_still_works_with_daemon() {
1331 let args = PproxyArgs::parse(&[
1332 "-l".into(),
1333 "http://:8080".into(),
1334 "-d".into(),
1335 "--daemon".into(),
1336 ])
1337 .unwrap();
1338 assert!(args.debug);
1339 assert_eq!(args.default_log_level(), "debug");
1340 }
1341
1342 #[test]
1343 fn test_default_log_level_default_args() {
1344 let args = PproxyArgs::default_args();
1345 assert_eq!(args.default_log_level(), "info");
1346 }
1347
1348 #[test]
1349 fn test_default_log_level_vvvv_saturates_to_trace() {
1350 for flag in ["-vvvv", "-vvvvv", "-vvvvvvvv"] {
1351 let args =
1352 PproxyArgs::parse(&["-l".into(), "http://:8080".into(), flag.into()]).unwrap();
1353 assert_eq!(
1354 args.default_log_level(),
1355 "trace",
1356 "flag {flag} should saturate to trace"
1357 );
1358 }
1359 }
1360
1361 #[test]
1362 fn test_default_log_level_clustered_mixed_forms() {
1363 for flag in ["-dv", "-vd", "-ddv", "-vvd", "-dvv"] {
1365 let args =
1366 PproxyArgs::parse(&["-l".into(), "http://:8080".into(), flag.into()]).unwrap();
1367 assert!(args.debug, "flag {flag} should set debug");
1368 assert!(args.verbose_level >= 1, "flag {flag} should count verbose");
1369 }
1370 let dv = PproxyArgs::parse(&["-l".into(), "http://:8080".into(), "-dv".into()]).unwrap();
1371 assert_eq!(dv.default_log_level(), "debug");
1372 let vd = PproxyArgs::parse(&["-l".into(), "http://:8080".into(), "-vd".into()]).unwrap();
1373 assert_eq!(vd.default_log_level(), "debug");
1374 let dvv = PproxyArgs::parse(&["-l".into(), "http://:8080".into(), "-dvv".into()]).unwrap();
1375 assert_eq!(dvv.verbose_level, 2);
1376 assert_eq!(dvv.default_log_level(), "debug");
1377 }
1378
1379 #[test]
1380 fn test_default_log_level_clustered_trace_forms() {
1381 let dvvv =
1383 PproxyArgs::parse(&["-l".into(), "http://:8080".into(), "-dvvv".into()]).unwrap();
1384 assert_eq!(dvvv.verbose_level, 3);
1385 assert_eq!(dvvv.default_log_level(), "trace");
1386
1387 let mixed = PproxyArgs::parse(&[
1388 "-l".into(),
1389 "http://:8080".into(),
1390 "-dv".into(),
1391 "-vv".into(),
1392 ])
1393 .unwrap();
1394 assert_eq!(mixed.verbose_level, 3);
1395 assert_eq!(mixed.default_log_level(), "trace");
1396 }
1397
1398 #[test]
1399 fn test_default_log_level_repeated_flags_accumulate() {
1400 let args = PproxyArgs::parse(&[
1401 "-l".into(),
1402 "http://:8080".into(),
1403 "-v".into(),
1404 "-v".into(),
1405 "-v".into(),
1406 ])
1407 .unwrap();
1408 assert_eq!(args.verbose_level, 3);
1409 assert_eq!(args.default_log_level(), "trace");
1410
1411 let dd = PproxyArgs::parse(&["-l".into(), "http://:8080".into(), "-d".into(), "-d".into()])
1412 .unwrap();
1413 assert!(dd.debug);
1414 assert_eq!(dd.debug_level, 2);
1415 assert_eq!(dd.default_log_level(), "debug");
1416 }
1417
1418 #[test]
1422 fn test_baseline_value_arity() {
1423 let cases: &[(&[&str], bool)] = &[
1426 (&["-l", "http://:8080"], true),
1428 (&["-r", "http://proxy:8080"], true),
1429 (&["-ul", "socks5://:1081"], true),
1430 (&["-ur", "socks5://proxy:1080"], true),
1431 (&["--ssl", "cert.pem,key.pem"], true),
1432 (&["--pac", "/proxy.pac"], true),
1433 (&["--test", "http://example.com"], true),
1434 (&["--auth", "3600"], true),
1435 (&["--get", "/index.html,body.txt"], true),
1436 (&["-s", "rr"], true),
1437 (&["-a", "10"], true),
1438 (&["-b", ".*\\.example\\.com"], true),
1439 (&["--rulefile", "rules.txt"], true),
1440 (&["--log", "access.log"], true),
1441 (&["-v"], false),
1443 (&["-d"], false),
1444 (&["--daemon"], false),
1445 (&["--sys"], false),
1446 (&["--reuse"], false),
1447 ];
1448
1449 for (args_slice, expects_value) in cases {
1450 let raw: Vec<String> = args_slice.iter().map(|s| s.to_string()).collect();
1451 let result = PproxyArgs::parse(&raw);
1452 if *expects_value && raw.len() == 1 {
1453 assert!(
1455 result.is_err(),
1456 "expected error for {:?} (missing value), got Ok",
1457 args_slice
1458 );
1459 } else {
1460 assert!(
1462 result.is_ok(),
1463 "expected Ok for {:?}, got Err: {:?}",
1464 args_slice,
1465 result.err()
1466 );
1467 }
1468 }
1469 }
1470}