Skip to main content

rch_common/config/
env.rs

1//! Environment variable parsing with type safety.
2//!
3//! Provides a type-safe parser for RCH environment variables with
4//! validation, error collection, and source tracking.
5
6use super::source::{ConfigSource, Sourced};
7use std::env;
8use std::path::PathBuf;
9use thiserror::Error;
10
11/// Errors that can occur during environment variable parsing.
12#[derive(Debug, Error)]
13pub enum EnvError {
14    /// Invalid value for a variable.
15    #[error("Invalid value for {var}: expected {expected}, got '{value}'")]
16    InvalidValue {
17        var: String,
18        expected: String,
19        value: String,
20    },
21
22    /// Path does not exist.
23    #[error("Path not found for {var}: {path}")]
24    PathNotFound { var: String, path: PathBuf },
25
26    /// Invalid duration format.
27    #[error("Invalid duration for {var}: {value}")]
28    InvalidDuration { var: String, value: String },
29
30    /// Value out of valid range.
31    #[error("Value out of range for {var}: {value} (valid: {min}..={max})")]
32    OutOfRange {
33        var: String,
34        value: String,
35        min: String,
36        max: String,
37    },
38
39    /// Invalid log level.
40    #[error("Invalid log level for {var}: {value}")]
41    InvalidLogLevel { var: String, value: String },
42}
43
44/// Type-safe environment variable parser.
45///
46/// Collects errors during parsing so all issues can be reported at once.
47pub struct EnvParser {
48    prefix: &'static str,
49    errors: Vec<EnvError>,
50}
51
52impl EnvParser {
53    /// Create a new parser with the RCH_ prefix.
54    pub fn new() -> Self {
55        Self {
56            prefix: "RCH_",
57            errors: Vec::new(),
58        }
59    }
60
61    /// Get all accumulated errors.
62    pub fn errors(&self) -> &[EnvError] {
63        &self.errors
64    }
65
66    /// Check if any errors occurred.
67    pub fn has_errors(&self) -> bool {
68        !self.errors.is_empty()
69    }
70
71    /// Take ownership of errors.
72    pub fn take_errors(&mut self) -> Vec<EnvError> {
73        std::mem::take(&mut self.errors)
74    }
75
76    /// Get the full variable name with prefix.
77    fn var_name(&self, name: &str) -> String {
78        format!("{}{}", self.prefix, name)
79    }
80
81    /// Get a string value with default.
82    pub fn get_string(&mut self, name: &str, default: &str) -> Sourced<String> {
83        let var_name = self.var_name(name);
84        match env::var(&var_name) {
85            Ok(value) => Sourced::from_env(value, var_name),
86            Err(_) => Sourced::default_value(default.to_string()),
87        }
88    }
89
90    /// Get a boolean value with default.
91    ///
92    /// Accepts: 1, true, yes, on (for true)
93    ///          0, false, no, off, "" (for false)
94    pub fn get_bool(&mut self, name: &str, default: bool) -> Sourced<bool> {
95        let var_name = self.var_name(name);
96        match env::var(&var_name) {
97            Ok(value) => {
98                let parsed = match value.to_lowercase().as_str() {
99                    "1" | "true" | "yes" | "on" => true,
100                    "0" | "false" | "no" | "off" | "" => false,
101                    _ => {
102                        self.errors.push(EnvError::InvalidValue {
103                            var: var_name.clone(),
104                            expected: "boolean (true/false/1/0/yes/no)".to_string(),
105                            value: value.clone(),
106                        });
107                        default
108                    }
109                };
110                Sourced::from_env(parsed, var_name)
111            }
112            Err(_) => Sourced::default_value(default),
113        }
114    }
115
116    /// Get a u32 value with default and range validation.
117    pub fn get_u32_range(&mut self, name: &str, default: u32, min: u32, max: u32) -> Sourced<u32> {
118        let var_name = self.var_name(name);
119        match env::var(&var_name) {
120            Ok(value) => match value.parse::<u32>() {
121                Ok(n) if n >= min && n <= max => Sourced::from_env(n, var_name),
122                Ok(n) => {
123                    self.errors.push(EnvError::OutOfRange {
124                        var: var_name.clone(),
125                        value: n.to_string(),
126                        min: min.to_string(),
127                        max: max.to_string(),
128                    });
129                    Sourced::from_env(default, var_name)
130                }
131                Err(_) => {
132                    self.errors.push(EnvError::InvalidValue {
133                        var: var_name.clone(),
134                        expected: "unsigned 32-bit integer".to_string(),
135                        value,
136                    });
137                    Sourced::default_value(default)
138                }
139            },
140            Err(_) => Sourced::default_value(default),
141        }
142    }
143
144    /// Get a u64 value with default and range validation.
145    pub fn get_u64_range(&mut self, name: &str, default: u64, min: u64, max: u64) -> Sourced<u64> {
146        let var_name = self.var_name(name);
147        match env::var(&var_name) {
148            Ok(value) => match value.parse::<u64>() {
149                Ok(n) if n >= min && n <= max => Sourced::from_env(n, var_name),
150                Ok(n) => {
151                    self.errors.push(EnvError::OutOfRange {
152                        var: var_name.clone(),
153                        value: n.to_string(),
154                        min: min.to_string(),
155                        max: max.to_string(),
156                    });
157                    Sourced::from_env(default, var_name)
158                }
159                Err(_) => {
160                    self.errors.push(EnvError::InvalidValue {
161                        var: var_name.clone(),
162                        expected: "unsigned 64-bit integer".to_string(),
163                        value,
164                    });
165                    Sourced::default_value(default)
166                }
167            },
168            Err(_) => Sourced::default_value(default),
169        }
170    }
171
172    /// Get an i32 value with default and range validation.
173    pub fn get_i32_range(&mut self, name: &str, default: i32, min: i32, max: i32) -> Sourced<i32> {
174        let var_name = self.var_name(name);
175        match env::var(&var_name) {
176            Ok(value) => match value.parse::<i32>() {
177                Ok(n) if n >= min && n <= max => Sourced::from_env(n, var_name),
178                Ok(n) => {
179                    self.errors.push(EnvError::OutOfRange {
180                        var: var_name.clone(),
181                        value: n.to_string(),
182                        min: min.to_string(),
183                        max: max.to_string(),
184                    });
185                    Sourced::from_env(default, var_name)
186                }
187                Err(_) => {
188                    self.errors.push(EnvError::InvalidValue {
189                        var: var_name.clone(),
190                        expected: "signed 32-bit integer".to_string(),
191                        value,
192                    });
193                    Sourced::default_value(default)
194                }
195            },
196            Err(_) => Sourced::default_value(default),
197        }
198    }
199
200    /// Get a f64 value with default and range validation.
201    pub fn get_f64_range(&mut self, name: &str, default: f64, min: f64, max: f64) -> Sourced<f64> {
202        let var_name = self.var_name(name);
203        match env::var(&var_name) {
204            Ok(value) => match value.parse::<f64>() {
205                Ok(n) if n >= min && n <= max => Sourced::from_env(n, var_name),
206                Ok(n) => {
207                    self.errors.push(EnvError::OutOfRange {
208                        var: var_name.clone(),
209                        value: n.to_string(),
210                        min: min.to_string(),
211                        max: max.to_string(),
212                    });
213                    Sourced::from_env(default, var_name)
214                }
215                Err(_) => {
216                    self.errors.push(EnvError::InvalidValue {
217                        var: var_name.clone(),
218                        expected: "floating-point number".to_string(),
219                        value,
220                    });
221                    Sourced::default_value(default)
222                }
223            },
224            Err(_) => Sourced::default_value(default),
225        }
226    }
227
228    /// Get a path value with ~ expansion.
229    ///
230    /// If `must_exist` is true, records an error if the path doesn't exist.
231    pub fn get_path(&mut self, name: &str, default: &str, must_exist: bool) -> Sourced<PathBuf> {
232        let var_name = self.var_name(name);
233        let (value, source) = match env::var(&var_name) {
234            Ok(v) => (v, ConfigSource::Environment),
235            Err(_) => (default.to_string(), ConfigSource::Default),
236        };
237
238        // Expand ~ to home directory
239        let expanded = if let Some(stripped) = value.strip_prefix("~/") {
240            if let Some(home) = dirs::home_dir() {
241                home.join(stripped)
242            } else {
243                PathBuf::from(&value)
244            }
245        } else {
246            PathBuf::from(&value)
247        };
248
249        if must_exist && !expanded.exists() {
250            self.errors.push(EnvError::PathNotFound {
251                var: var_name.clone(),
252                path: expanded.clone(),
253            });
254        }
255
256        if source == ConfigSource::Environment {
257            Sourced::from_env(expanded, var_name)
258        } else {
259            Sourced::default_value(expanded)
260        }
261    }
262
263    /// Get a log level value with validation.
264    pub fn get_log_level(&mut self, name: &str, default: &str) -> Sourced<String> {
265        let var_name = self.var_name(name);
266        match env::var(&var_name) {
267            Ok(value) => {
268                let lower = value.to_lowercase();
269                match lower.as_str() {
270                    "trace" | "debug" | "info" | "warn" | "error" | "off" => {
271                        Sourced::from_env(lower, var_name)
272                    }
273                    _ => {
274                        self.errors.push(EnvError::InvalidLogLevel {
275                            var: var_name.clone(),
276                            value: value.clone(),
277                        });
278                        Sourced::from_env(default.to_string(), var_name)
279                    }
280                }
281            }
282            Err(_) => Sourced::default_value(default.to_string()),
283        }
284    }
285
286    /// Get a comma-separated list of strings.
287    pub fn get_string_list(&mut self, name: &str, default: Vec<String>) -> Sourced<Vec<String>> {
288        let var_name = self.var_name(name);
289        match env::var(&var_name) {
290            Ok(value) if value.is_empty() => Sourced::from_env(Vec::new(), var_name),
291            Ok(value) => {
292                let items: Vec<String> = value
293                    .split(',')
294                    .map(|s| s.trim().to_string())
295                    .filter(|s| !s.is_empty())
296                    .collect();
297                Sourced::from_env(items, var_name)
298            }
299            Err(_) => Sourced::default_value(default),
300        }
301    }
302
303    /// Get an optional string (None if not set or empty).
304    pub fn get_optional_string(&mut self, name: &str) -> Sourced<Option<String>> {
305        let var_name = self.var_name(name);
306        match env::var(&var_name) {
307            Ok(value) if value.is_empty() => Sourced::from_env(None, var_name),
308            Ok(value) => Sourced::from_env(Some(value), var_name),
309            Err(_) => Sourced::default_value(None),
310        }
311    }
312}
313
314impl Default for EnvParser {
315    fn default() -> Self {
316        Self::new()
317    }
318}
319
320#[cfg(test)]
321#[allow(unsafe_code)]
322mod tests {
323    use super::*;
324    use crate::config::env_test_lock;
325    use std::env;
326
327    fn cleanup_env(vars: &[&str]) {
328        for var in vars {
329            // SAFETY: Tests run single-threaded, no concurrent access to env vars
330            unsafe { env::remove_var(var) };
331        }
332    }
333
334    fn set_env(key: &str, value: &str) {
335        // SAFETY: Tests run single-threaded, no concurrent access to env vars
336        unsafe { env::set_var(key, value) };
337    }
338
339    fn env_guard() -> std::sync::MutexGuard<'static, ()> {
340        env_test_lock()
341    }
342
343    #[test]
344    fn test_get_bool_true_values() {
345        let _guard = env_guard();
346        let vars = ["RCH_TEST_BOOL_TRUE"];
347        cleanup_env(&vars);
348
349        for val in &["1", "true", "yes", "on", "TRUE", "Yes"] {
350            set_env("RCH_TEST_BOOL_TRUE", val);
351            let mut parser = EnvParser::new();
352            let result = parser.get_bool("TEST_BOOL_TRUE", false);
353            assert!(result.value, "Expected true for '{}'", val);
354            assert!(!parser.has_errors());
355        }
356
357        cleanup_env(&vars);
358    }
359
360    #[test]
361    fn test_get_bool_false_values() {
362        let _guard = env_guard();
363        let vars = ["RCH_TEST_BOOL_FALSE"];
364        cleanup_env(&vars);
365
366        for val in &["0", "false", "no", "off", "FALSE", ""] {
367            set_env("RCH_TEST_BOOL_FALSE", val);
368            let mut parser = EnvParser::new();
369            let result = parser.get_bool("TEST_BOOL_FALSE", true);
370            assert!(!result.value, "Expected false for '{}'", val);
371            assert!(!parser.has_errors());
372        }
373
374        cleanup_env(&vars);
375    }
376
377    #[test]
378    fn test_get_bool_invalid_uses_default() {
379        let _guard = env_guard();
380        let vars = ["RCH_BAD_BOOL"];
381        cleanup_env(&vars);
382
383        set_env("RCH_BAD_BOOL", "maybe");
384        let mut parser = EnvParser::new();
385        let result = parser.get_bool("BAD_BOOL", false);
386        assert!(!result.value);
387        assert!(parser.has_errors());
388
389        cleanup_env(&vars);
390    }
391
392    #[test]
393    fn test_get_u64_range_valid() {
394        let _guard = env_guard();
395        let vars = ["RCH_TEST_U64"];
396        cleanup_env(&vars);
397
398        set_env("RCH_TEST_U64", "50");
399        let mut parser = EnvParser::new();
400        let result = parser.get_u64_range("TEST_U64", 10, 0, 100);
401        assert_eq!(result.value, 50);
402        assert!(!parser.has_errors());
403
404        cleanup_env(&vars);
405    }
406
407    #[test]
408    fn test_get_u64_range_out_of_range() {
409        let _guard = env_guard();
410        // Use unique var name to avoid race with test_get_u64_range_valid
411        let vars = ["RCH_TEST_U64_OOR"];
412        cleanup_env(&vars);
413
414        set_env("RCH_TEST_U64_OOR", "200");
415        let mut parser = EnvParser::new();
416        let result = parser.get_u64_range("TEST_U64_OOR", 10, 0, 100);
417        assert_eq!(result.value, 10); // Uses default
418        assert!(parser.has_errors());
419
420        cleanup_env(&vars);
421    }
422
423    #[test]
424    fn test_get_log_level_valid() {
425        let _guard = env_guard();
426        let vars = ["RCH_LOG_LEVEL"];
427        cleanup_env(&vars);
428
429        for level in &["trace", "debug", "info", "warn", "error", "DEBUG", "INFO"] {
430            set_env("RCH_LOG_LEVEL", level);
431            let mut parser = EnvParser::new();
432            let result = parser.get_log_level("LOG_LEVEL", "info");
433            assert!(!parser.has_errors(), "Expected valid for '{}'", level);
434            assert_eq!(result.value, level.to_lowercase());
435        }
436
437        cleanup_env(&vars);
438    }
439
440    #[test]
441    fn test_get_log_level_invalid() {
442        let _guard = env_guard();
443        let vars = ["RCH_LOG_LEVEL"];
444        cleanup_env(&vars);
445
446        set_env("RCH_LOG_LEVEL", "verbose");
447        let mut parser = EnvParser::new();
448        let result = parser.get_log_level("LOG_LEVEL", "info");
449        assert!(parser.has_errors());
450        assert_eq!(result.value, "info"); // Default
451
452        cleanup_env(&vars);
453    }
454
455    #[test]
456    fn test_get_string_list() {
457        let _guard = env_guard();
458        let vars = ["RCH_TEST_LIST"];
459        cleanup_env(&vars);
460
461        set_env("RCH_TEST_LIST", "a, b, c");
462        let mut parser = EnvParser::new();
463        let result = parser.get_string_list("TEST_LIST", vec![]);
464        assert_eq!(result.value, vec!["a", "b", "c"]);
465
466        cleanup_env(&vars);
467    }
468
469    #[test]
470    fn test_get_optional_string() {
471        let _guard = env_guard();
472        let vars = ["RCH_TEST_OPT"];
473        cleanup_env(&vars);
474
475        // Not set
476        let mut parser = EnvParser::new();
477        let result = parser.get_optional_string("TEST_OPT");
478        assert!(result.value.is_none());
479
480        // Set to empty
481        set_env("RCH_TEST_OPT", "");
482        let mut parser = EnvParser::new();
483        let result = parser.get_optional_string("TEST_OPT");
484        assert!(result.value.is_none());
485
486        // Set to value
487        set_env("RCH_TEST_OPT", "value");
488        let mut parser = EnvParser::new();
489        let result = parser.get_optional_string("TEST_OPT");
490        assert_eq!(result.value, Some("value".to_string()));
491
492        cleanup_env(&vars);
493    }
494
495    #[test]
496    fn test_source_tracking() {
497        let _guard = env_guard();
498        let vars = ["RCH_TEST_SRC"];
499        cleanup_env(&vars);
500
501        // Default source
502        let mut parser = EnvParser::new();
503        let result = parser.get_string("TEST_SRC", "default");
504        assert_eq!(result.source, ConfigSource::Default);
505        assert!(result.env_var.is_none());
506
507        // Environment source
508        set_env("RCH_TEST_SRC", "from_env");
509        let mut parser = EnvParser::new();
510        let result = parser.get_string("TEST_SRC", "default");
511        assert_eq!(result.source, ConfigSource::Environment);
512        assert_eq!(result.env_var.as_deref(), Some("RCH_TEST_SRC"));
513
514        cleanup_env(&vars);
515    }
516
517    // ==========================================================================
518    // Proptest: Config parsing with malformed inputs (bd-1dka)
519    // ==========================================================================
520
521    mod proptest_config_parsing {
522        use super::*;
523        use crate::config::env_test_lock;
524        use proptest::prelude::*;
525        use std::env;
526
527        // SAFETY: All proptest tests must acquire the env lock and use unique variable names
528        // to prevent race conditions with concurrent test execution.
529
530        fn cleanup_env(vars: &[&str]) {
531            for var in vars {
532                // SAFETY: Tests are serialized via env_test_lock
533                unsafe { env::remove_var(var) };
534            }
535        }
536
537        fn set_env(key: &str, value: &str) {
538            // SAFETY: Tests are serialized via env_test_lock
539            unsafe { env::set_var(key, value) };
540        }
541
542        // Helper to parse boolean strings (mirrors get_bool logic)
543        fn parse_bool_string(value: &str) -> Option<bool> {
544            match value.to_lowercase().as_str() {
545                "1" | "true" | "yes" | "on" => Some(true),
546                "0" | "false" | "no" | "off" | "" => Some(false),
547                _ => None,
548            }
549        }
550
551        // Helper to parse log level strings (mirrors get_log_level logic)
552        fn parse_log_level_string(value: &str) -> Option<String> {
553            let lower = value.to_lowercase();
554            match lower.as_str() {
555                "trace" | "debug" | "info" | "warn" | "error" | "off" => Some(lower),
556                _ => None,
557            }
558        }
559
560        // Helper to parse string list (mirrors get_string_list logic)
561        fn parse_string_list(value: &str) -> Vec<String> {
562            if value.is_empty() {
563                Vec::new()
564            } else {
565                value
566                    .split(',')
567                    .map(|s| s.trim().to_string())
568                    .filter(|s| !s.is_empty())
569                    .collect()
570            }
571        }
572
573        proptest! {
574            #![proptest_config(ProptestConfig::with_cases(500))]
575
576            // Test 1: Boolean parsing with arbitrary strings never panics
577            #[test]
578            fn test_parse_bool_no_panic(s in ".*") {
579                // Should never panic, just returns None for invalid values
580                let _ = parse_bool_string(&s);
581            }
582
583            // Test 2: Boolean parsing accepts only valid values
584            #[test]
585            fn test_parse_bool_valid_only(s in "[a-zA-Z0-9_-]{0,20}") {
586                let result = parse_bool_string(&s);
587                let valid_true = ["1", "true", "yes", "on"];
588                let valid_false = ["0", "false", "no", "off", ""];
589
590                let is_valid = valid_true.iter().any(|v| s.eq_ignore_ascii_case(v))
591                    || valid_false.iter().any(|v| s.eq_ignore_ascii_case(v));
592
593                if is_valid {
594                    prop_assert!(result.is_some(), "Expected Some for valid input: {}", s);
595                } else {
596                    prop_assert!(result.is_none(), "Expected None for invalid input: {}", s);
597                }
598            }
599
600            // Test 3: Log level parsing with arbitrary strings never panics
601            #[test]
602            fn test_parse_log_level_no_panic(s in ".*") {
603                let _ = parse_log_level_string(&s);
604            }
605
606            // Test 4: Log level parsing accepts only valid levels
607            #[test]
608            fn test_parse_log_level_valid_only(s in "[a-zA-Z]{0,10}") {
609                let result = parse_log_level_string(&s);
610                let valid_levels = ["trace", "debug", "info", "warn", "error", "off"];
611
612                let is_valid = valid_levels.iter().any(|v| s.eq_ignore_ascii_case(v));
613
614                if is_valid {
615                    prop_assert!(result.is_some(), "Expected Some for valid level: {}", s);
616                } else {
617                    prop_assert!(result.is_none(), "Expected None for invalid level: {}", s);
618                }
619            }
620
621            // Test 5: String list parsing never panics
622            #[test]
623            fn test_parse_string_list_no_panic(s in ".*") {
624                let _ = parse_string_list(&s);
625            }
626
627            // Test 6: String list parsing handles various separators
628            #[test]
629            fn test_parse_string_list_separators(
630                items in prop::collection::vec("[a-zA-Z0-9]+", 0..10)
631            ) {
632                let input = items.join(",");
633                let result = parse_string_list(&input);
634                // Should have same number of non-empty items
635                let expected: Vec<String> = items.into_iter().filter(|s| !s.is_empty()).collect();
636                prop_assert_eq!(result, expected);
637            }
638
639            // Test 7: Integer parsing boundary conditions
640            #[test]
641            fn test_integer_parsing_boundaries(
642                s in prop::sample::select(vec![
643                    "0", "-1", "1", "2147483647", "-2147483648",
644                    "18446744073709551615", "18446744073709551616",
645                    "9999999999999999999999999999999999",
646                    "abc", "", " ", "1.5", "1e10", "0x10", "0b10",
647                    "+1", " 1 ", "1 ", " 1",
648                ])
649            ) {
650                // Test that parsing these values doesn't panic
651                let _ = s.parse::<u32>();
652                let _ = s.parse::<u64>();
653                let _ = s.parse::<i32>();
654                let _ = s.parse::<f64>();
655            }
656
657            // Test 8: Float parsing with edge cases
658            #[test]
659            fn test_float_parsing_edge_cases(
660                s in prop::sample::select(vec![
661                    "0", "0.0", "-0.0", "1.0", "-1.0",
662                    "inf", "-inf", "nan", "NaN", "Infinity",
663                    "1e308", "1e-308", "1e309", // overflow/underflow
664                    "1.7976931348623157e308", // f64::MAX
665                    "abc", "", " ", "1,5", "1..0",
666                ])
667            ) {
668                let _ = s.parse::<f64>();
669            }
670        }
671
672        // Integration tests using EnvParser with proptest-generated values
673        proptest! {
674            #![proptest_config(ProptestConfig::with_cases(100))]
675
676            // Test 9: EnvParser.get_bool with random env values
677            #[test]
678            fn test_env_parser_get_bool(value in "[a-zA-Z0-9_-]{0,20}") {
679                let _guard = env_test_lock();
680                let var = "RCH_PROPTEST_BOOL_9";
681                cleanup_env(&[var]);
682
683                set_env(var, &value);
684                let mut parser = EnvParser::new();
685                let result = parser.get_bool("PROPTEST_BOOL_9", false);
686
687                // Should never panic, returns default on invalid
688                prop_assert!(result.value == parse_bool_string(&value).unwrap_or(false));
689
690                cleanup_env(&[var]);
691            }
692
693            // Test 10: EnvParser.get_u32_range with random values
694            #[test]
695            fn test_env_parser_get_u32_range(value in "[-0-9a-zA-Z.]{0,30}") {
696                let _guard = env_test_lock();
697                let var = "RCH_PROPTEST_U32_10";
698                cleanup_env(&[var]);
699
700                set_env(var, &value);
701                let mut parser = EnvParser::new();
702                let result = parser.get_u32_range("PROPTEST_U32_10", 50, 0, 100);
703
704                // Should never panic
705                let parsed = value.parse::<u32>().ok();
706                if let Some(n) = parsed {
707                    if n <= 100 {
708                        prop_assert_eq!(result.value, n);
709                    } else {
710                        prop_assert_eq!(result.value, 50); // Default on out-of-range
711                    }
712                } else {
713                    prop_assert_eq!(result.value, 50); // Default on parse error
714                }
715
716                cleanup_env(&[var]);
717            }
718
719            // Test 11: EnvParser.get_log_level with random values
720            #[test]
721            fn test_env_parser_get_log_level(value in "[a-zA-Z]{0,15}") {
722                let _guard = env_test_lock();
723                let var = "RCH_PROPTEST_LOG_11";
724                cleanup_env(&[var]);
725
726                set_env(var, &value);
727                let mut parser = EnvParser::new();
728                let result = parser.get_log_level("PROPTEST_LOG_11", "info");
729
730                // Should never panic
731                if let Some(valid_level) = parse_log_level_string(&value) {
732                    prop_assert_eq!(result.value, valid_level);
733                    prop_assert!(!parser.has_errors());
734                } else {
735                    prop_assert_eq!(result.value, "info"); // Default
736                    prop_assert!(parser.has_errors());
737                }
738
739                cleanup_env(&[var]);
740            }
741
742            // Test 12: EnvParser.get_string_list with random CSV
743            #[test]
744            fn test_env_parser_get_string_list(value in "[a-zA-Z0-9, ]{0,100}") {
745                let _guard = env_test_lock();
746                let var = "RCH_PROPTEST_LIST_12";
747                cleanup_env(&[var]);
748
749                set_env(var, &value);
750                let mut parser = EnvParser::new();
751                let result = parser.get_string_list("PROPTEST_LIST_12", vec![]);
752
753                // Should never panic
754                prop_assert_eq!(result.value, parse_string_list(&value));
755                prop_assert!(!parser.has_errors());
756
757                cleanup_env(&[var]);
758            }
759        }
760
761        // Edge case tests for malformed inputs
762        #[test]
763        fn test_malformed_inputs_no_panic() {
764            let _guard = env_test_lock();
765
766            // Long string for testing (heap allocation)
767            let long_string = "a".repeat(10000);
768
769            // Test cases that might cause issues
770            let malformed_values = [
771                "",                   // Empty
772                " ",                  // Whitespace only
773                "\t\n\r",             // Control chars
774                "null",               // Common null value
775                "undefined",          // JS undefined
776                "None",               // Python None
777                "nil",                // Ruby nil
778                "\0",                 // Null byte
779                "\x00\x01\x02",       // Binary data
780                "🔥",                 // Emoji
781                "日本語",             // Unicode
782                long_string.as_str(), // Very long string (heap allocation)
783                "-",                  // Just minus sign
784                "+",                  // Just plus sign
785                ".",                  // Just decimal point
786                "e",                  // Just exponent marker
787                "0x",                 // Incomplete hex
788                "0b",                 // Incomplete binary
789            ];
790
791            for value in &malformed_values {
792                // Test boolean parsing
793                let _ = parse_bool_string(value);
794
795                // Test log level parsing
796                let _ = parse_log_level_string(value);
797
798                // Test string list parsing
799                let _ = parse_string_list(value);
800
801                // Test integer parsing
802                let _ = value.parse::<u32>();
803                let _ = value.parse::<u64>();
804                let _ = value.parse::<i32>();
805                let _ = value.parse::<i64>();
806
807                // Test float parsing
808                let _ = value.parse::<f64>();
809            }
810        }
811
812        #[test]
813        fn test_env_parser_with_malformed_values() {
814            let _guard = env_test_lock();
815            let vars = [
816                "RCH_PROPTEST_MAL_BOOL",
817                "RCH_PROPTEST_MAL_U32",
818                "RCH_PROPTEST_MAL_I32",
819                "RCH_PROPTEST_MAL_F64",
820                "RCH_PROPTEST_MAL_LOG",
821            ];
822            cleanup_env(&vars);
823
824            // Set malformed values
825            set_env("RCH_PROPTEST_MAL_BOOL", "maybe");
826            set_env("RCH_PROPTEST_MAL_U32", "not_a_number");
827            set_env("RCH_PROPTEST_MAL_I32", "9999999999999999999");
828            set_env("RCH_PROPTEST_MAL_F64", "1.2.3.4");
829            set_env("RCH_PROPTEST_MAL_LOG", "verbose");
830
831            let mut parser = EnvParser::new();
832
833            // All should return defaults without panicking
834            let bool_result = parser.get_bool("PROPTEST_MAL_BOOL", true);
835            assert!(bool_result.value); // Default
836
837            let u32_result = parser.get_u32_range("PROPTEST_MAL_U32", 42, 0, 100);
838            assert_eq!(u32_result.value, 42); // Default
839
840            let i32_result = parser.get_i32_range("PROPTEST_MAL_I32", -5, -100, 100);
841            assert_eq!(i32_result.value, -5); // Default
842
843            let f64_result = parser.get_f64_range("PROPTEST_MAL_F64", 4.567, 0.0, 10.0);
844            assert!((f64_result.value - 4.567).abs() < 0.001); // Default
845
846            let log_result = parser.get_log_level("PROPTEST_MAL_LOG", "warn");
847            assert_eq!(log_result.value, "warn"); // Default
848
849            // Should have collected multiple errors
850            assert!(parser.errors().len() >= 5);
851
852            cleanup_env(&vars);
853        }
854
855        #[test]
856        fn test_path_expansion_edge_cases() {
857            let _guard = env_test_lock();
858            let vars = ["RCH_PROPTEST_PATH_EDGE"];
859            cleanup_env(&vars);
860
861            let edge_case_paths = [
862                "",                 // Empty
863                "~",                // Just tilde
864                "~/",               // Tilde with slash
865                "~user/file",       // Tilde with username (not expanded)
866                "/absolute/path",   // Absolute path
867                "./relative/path",  // Relative path
868                "../parent/path",   // Parent path
869                "path with spaces", // Spaces
870                "path\twith\ttabs", // Tabs
871                "path/with/日本語", // Unicode
872                "/dev/null",        // Special file
873            ];
874
875            for path in &edge_case_paths {
876                set_env("RCH_PROPTEST_PATH_EDGE", path);
877                let mut parser = EnvParser::new();
878                // must_exist=false to avoid PathNotFound errors
879                let _ = parser.get_path("PROPTEST_PATH_EDGE", "/default", false);
880                // Should never panic
881            }
882
883            cleanup_env(&vars);
884        }
885    }
886}