1use parking_lot::RwLock;
49use std::collections::HashMap;
50use std::fs;
51use std::path::Path;
52use std::sync::Arc;
53use thiserror::Error;
54
55#[derive(Debug, Error)]
61pub enum EnvError {
62 #[error(".env 文件读取失败: {path} — {source}")]
64 FileRead {
65 path: String,
67 #[source]
69 source: std::io::Error,
70 },
71 #[error(".env 文件解析失败: {path} — 行 {line}: {message}")]
73 Parse {
74 path: String,
76 line: usize,
78 message: String,
80 },
81}
82
83#[derive(Debug, Clone, Default)]
112pub struct Env {
113 data: Arc<RwLock<HashMap<String, String>>>,
115}
116
117impl Env {
118 pub fn new() -> Self {
120 Self::default()
121 }
122
123 pub fn load_from_file(&self, path: impl AsRef<Path>) -> Result<(), EnvError> {
146 let path_ref = path.as_ref();
147 let content = fs::read_to_string(path_ref).map_err(|e| EnvError::FileRead {
148 path: path_ref.display().to_string(),
149 source: e,
150 })?;
151
152 self.parse_ini_content(&content, &path_ref.display().to_string())
153 }
154
155 fn parse_ini_content(&self, content: &str, path: &str) -> Result<(), EnvError> {
165 let mut data = self.data.write();
166 let mut current_section: String = String::new();
167
168 for (line_idx, raw_line) in content.lines().enumerate() {
169 let line_no = line_idx + 1;
170 let line = raw_line.trim();
171
172 if line.is_empty() {
174 continue;
175 }
176
177 if line.starts_with('#') || line.starts_with(';') {
179 continue;
180 }
181
182 if line.starts_with('[') {
184 if let Some(end) = line.find(']') {
185 current_section = line[1..end].trim().to_string();
186 } else {
187 return Err(EnvError::Parse {
188 path: path.to_string(),
189 line: line_no,
190 message: "section 头缺少闭合的 ']'".to_string(),
191 });
192 }
193 continue;
194 }
195
196 if let Some(eq_pos) = line.find('=') {
198 let key = line[..eq_pos].trim().to_string();
199 let mut value = line[eq_pos + 1..].trim().to_string();
200
201 if key.is_empty() {
202 return Err(EnvError::Parse {
203 path: path.to_string(),
204 line: line_no,
205 message: "键为空".to_string(),
206 });
207 }
208
209 if value.len() >= 2 {
211 let first = value.chars().next().expect("已检查 value.len() >= 2");
212 let last = value.chars().last().expect("已检查 value.len() >= 2");
213 if (first == '"' && last == '"') || (first == '\'' && last == '\'') {
214 value = value[1..value.len() - 1].to_string();
215 }
216 }
217
218 let full_key = if current_section.is_empty() {
220 key
221 } else {
222 format!("{}.{}", current_section, key)
223 };
224
225 data.insert(full_key, value);
226 } else {
227 return Err(EnvError::Parse {
228 path: path.to_string(),
229 line: line_no,
230 message: "缺少 '=' 分隔符".to_string(),
231 });
232 }
233 }
234
235 Ok(())
236 }
237
238 pub fn get(&self, name: &str) -> Option<String> {
259 if let Ok(value) = std::env::var(name) {
261 if !value.is_empty() {
262 return Some(value);
263 }
264 }
265
266 let data = self.data.read();
268 data.get(name).cloned()
269 }
270
271 pub fn get_with_default(&self, name: &str, default: &str) -> String {
288 self.get(name).unwrap_or_else(|| default.to_string())
289 }
290
291 pub fn has(&self, name: &str) -> bool {
311 if let Ok(value) = std::env::var(name) {
313 if !value.is_empty() {
314 return true;
315 }
316 }
317
318 let data = self.data.read();
320 data.contains_key(name)
321 }
322
323 pub fn set(&self, name: &str, value: &str) {
343 let mut data = self.data.write();
344 data.insert(name.to_string(), value.to_string());
345 }
346
347 pub fn remove(&self, name: &str) -> bool {
361 let mut data = self.data.write();
362 data.remove(name).is_some()
363 }
364
365 pub fn all(&self) -> HashMap<String, String> {
376 let data = self.data.read();
377 data.clone()
378 }
379
380 pub fn clear(&self) {
386 let mut data = self.data.write();
387 data.clear();
388 }
389}
390
391#[cfg(test)]
396mod tests {
397 use super::*;
398 use std::io::Write;
399
400 #[test]
402 fn test_new_env_is_empty() {
403 let env = Env::new();
404 assert!(env.all().is_empty());
405 assert!(!env.has("NON_EXISTENT_KEY"));
406 assert_eq!(env.get("NON_EXISTENT_KEY"), None);
407 }
408
409 #[test]
411 fn test_set_get_remove() {
412 let env = Env::new();
413
414 env.set("APP_KEY", "base64:xxxxxx");
415 assert!(env.has("APP_KEY"));
416 assert_eq!(env.get("APP_KEY"), Some("base64:xxxxxx".to_string()));
417
418 assert!(env.remove("APP_KEY"));
419 assert!(!env.has("APP_KEY"));
420 assert_eq!(env.get("APP_KEY"), None);
421 }
422
423 #[test]
425 fn test_get_with_default() {
426 let env = Env::new();
427
428 assert_eq!(env.get_with_default("MISSING", "fallback"), "fallback");
430
431 env.set("EXISTING", "actual");
433 assert_eq!(env.get_with_default("EXISTING", "fallback"), "actual");
434 }
435
436 #[test]
438 fn test_load_from_ini_content_with_section() {
439 let env = Env::new();
440 let content = r#"
441# 顶层配置
442APP_DEBUG = true
443APP_KEY = "base64:secret"
444
445[database]
446hostname = localhost
447port = 3306
448
449[redis]
450host = "127.0.0.1"
451"#;
452 env.parse_ini_content(content, "<test>").unwrap();
453
454 assert_eq!(env.get("APP_DEBUG"), Some("true".to_string()));
456 assert_eq!(env.get("APP_KEY"), Some("base64:secret".to_string()));
457
458 assert_eq!(env.get("database.hostname"), Some("localhost".to_string()));
460 assert_eq!(env.get("database.port"), Some("3306".to_string()));
461 assert_eq!(env.get("redis.host"), Some("127.0.0.1".to_string()));
462 }
463
464 #[test]
466 fn test_quote_stripping() {
467 let env = Env::new();
468 let content = r#"
469DOUBLE = "value with spaces"
470SINGLE = 'another value'
471NO_QUOTE = plain
472EMPTY = ""
473"#;
474 env.parse_ini_content(content, "<test>").unwrap();
475
476 assert_eq!(env.get("DOUBLE"), Some("value with spaces".to_string()));
477 assert_eq!(env.get("SINGLE"), Some("another value".to_string()));
478 assert_eq!(env.get("NO_QUOTE"), Some("plain".to_string()));
479 assert_eq!(env.get("EMPTY"), Some("".to_string()));
480 }
481
482 #[test]
484 fn test_comment_lines_skipped() {
485 let env = Env::new();
486 let content = r#"
487# 这是注释
488APP_KEY = value1
489; 这也是注释
490APP_DEBUG = value2
491"#;
492 env.parse_ini_content(content, "<test>").unwrap();
493
494 assert_eq!(env.get("APP_KEY"), Some("value1".to_string()));
495 assert_eq!(env.get("APP_DEBUG"), Some("value2".to_string()));
496 }
497
498 #[test]
500 fn test_load_from_file() {
501 let temp_dir = std::env::temp_dir().join("sz_rust_env_test");
503 let _ = std::fs::create_dir_all(&temp_dir);
504 let env_file = temp_dir.join(".env");
505
506 let mut file = std::fs::File::create(&env_file).unwrap();
507 writeln!(file, "TEST_KEY = test_value").unwrap();
508 writeln!(file).unwrap();
509 writeln!(file, "[section]").unwrap();
510 writeln!(file, "inner = inner_value").unwrap();
511 drop(file);
512
513 let env = Env::new();
514 env.load_from_file(&env_file).unwrap();
515
516 assert_eq!(env.get("TEST_KEY"), Some("test_value".to_string()));
517 assert_eq!(env.get("section.inner"), Some("inner_value".to_string()));
518
519 let _ = std::fs::remove_dir_all(&temp_dir);
520 }
521
522 #[test]
524 fn test_load_nonexistent_file_errors() {
525 let env = Env::new();
526 let result = env.load_from_file("/nonexistent/path/.env");
527 assert!(result.is_err());
528 match result {
529 Err(EnvError::FileRead { .. }) => {}
530 _ => panic!("期望 FileRead 错误"),
531 }
532 }
533
534 #[test]
536 fn test_parse_unclosed_section_errors() {
537 let env = Env::new();
538 let content = "[unclosed_section\nkey = value";
539 let result = env.parse_ini_content(content, "<test>");
540 assert!(result.is_err());
541 match result {
542 Err(EnvError::Parse { line, .. }) => {
543 assert_eq!(line, 1);
544 }
545 _ => panic!("期望 Parse 错误"),
546 }
547 }
548
549 #[test]
551 fn test_parse_missing_equals_errors() {
552 let env = Env::new();
553 let content = "this_is_not_a_key_value_pair";
554 let result = env.parse_ini_content(content, "<test>");
555 assert!(result.is_err());
556 match result {
557 Err(EnvError::Parse { line, .. }) => {
558 assert_eq!(line, 1);
559 }
560 _ => panic!("期望 Parse 错误"),
561 }
562 }
563
564 #[test]
566 fn test_parse_empty_key_errors() {
567 let env = Env::new();
568 let content = " = value";
569 let result = env.parse_ini_content(content, "<test>");
570 assert!(result.is_err());
571 match result {
572 Err(EnvError::Parse { line, .. }) => {
573 assert_eq!(line, 1);
574 }
575 _ => panic!("期望 Parse 错误"),
576 }
577 }
578
579 #[test]
584 fn test_process_env_takes_priority() {
585 let env = Env::new();
586
587 env.set("SZ_RUST_TEST_ENV_PRIORITY", "internal_value");
589
590 std::env::set_var("SZ_RUST_TEST_ENV_PRIORITY", "process_value");
592
593 assert_eq!(
595 env.get("SZ_RUST_TEST_ENV_PRIORITY"),
596 Some("process_value".to_string())
597 );
598
599 std::env::remove_var("SZ_RUST_TEST_ENV_PRIORITY");
600 }
601
602 #[test]
606 fn test_empty_process_env_falls_back_to_internal() {
607 let env = Env::new();
608
609 env.set("SZ_RUST_TEST_EMPTY_FALLBACK", "internal_value");
611
612 std::env::set_var("SZ_RUST_TEST_EMPTY_FALLBACK", "");
614
615 assert_eq!(
617 env.get("SZ_RUST_TEST_EMPTY_FALLBACK"),
618 Some("internal_value".to_string())
619 );
620
621 std::env::remove_var("SZ_RUST_TEST_EMPTY_FALLBACK");
622 }
623
624 #[test]
626 fn test_clear() {
627 let env = Env::new();
628 env.set("KEY1", "value1");
629 env.set("KEY2", "value2");
630 assert_eq!(env.all().len(), 2);
631
632 env.clear();
633 assert!(env.all().is_empty());
634 }
635
636 #[test]
638 fn test_all_returns_snapshot() {
639 let env = Env::new();
640 env.set("KEY1", "value1");
641 env.set("KEY2", "value2");
642
643 let snapshot = env.all();
644 assert_eq!(snapshot.len(), 2);
645 assert_eq!(snapshot.get("KEY1"), Some(&"value1".to_string()));
646 assert_eq!(snapshot.get("KEY2"), Some(&"value2".to_string()));
647
648 env.set("KEY3", "value3");
650 assert_eq!(snapshot.len(), 2);
651 }
652
653 #[test]
655 fn test_remove_nonexistent_returns_false() {
656 let env = Env::new();
657 assert!(!env.remove("NON_EXISTENT"));
658 }
659
660 #[test]
665 fn test_section_isolation() {
666 let env = Env::new();
667 let content = r#"
668[section1]
669key = value1
670
671[section2]
672key = value2
673"#;
674 env.parse_ini_content(content, "<test>").unwrap();
675
676 assert_eq!(env.get("section1.key"), Some("value1".to_string()));
677 assert_eq!(env.get("section2.key"), Some("value2".to_string()));
678 }
679
680 #[test]
685 fn test_multiple_load_accumulates() {
686 let env = Env::new();
687 let content1 = "KEY1 = value1";
688 let content2 = "KEY2 = value2";
689
690 env.parse_ini_content(content1, "<test1>").unwrap();
691 env.parse_ini_content(content2, "<test2>").unwrap();
692
693 assert_eq!(env.get("KEY1"), Some("value1".to_string()));
694 assert_eq!(env.get("KEY2"), Some("value2".to_string()));
695 }
696}