Skip to main content

sz_rust_core/
env.rs

1//! Env 模块 — 对齐 PHP `think\facade\Env`
2//!
3//! 本模块实现环境变量管理,对齐 PHP `think\facade\Env` 的核心 API。
4//!
5//! ## PHP 对齐
6//!
7//! ### 核心 API 映射
8//!
9//! | PHP 方法 | Rust 方法 | 说明 |
10//! |---------|-----------|------|
11//! | `Env::get($name, $default = null)` | [`Env::get`] / [`Env::get_with_default`] | 获取环境变量 |
12//! | `Env::set($name, $value)` | [`Env::set`] | 设置环境变量(仅写入内部存储) |
13//! | `Env::has($name)` | [`Env::has`] | 检查环境变量是否存在 |
14//! | `Env::load($file)` | [`Env::load_from_file`] | 从 `.env` 文件加载 |
15//!
16//! ### PHP 行为对齐
17//!
18//! - **优先级**:PHP `Env::get()` 优先返回真实环境变量(`$_SERVER` / `getenv()`),
19//!   其次返回 `.env` 文件加载的值。Rust 同样优先 `std::env::var()`,其次查内部存储。
20//! - **点分隔访问**:PHP `.env` 文件支持 `[section]` 段,通过 `section.key` 访问。
21//!   Rust 通过 [`Env::load_from_file`] 解析 INI 风格 section,存储为 `section.key` 形式。
22//! - **不污染进程环境**:PHP `Env::set()` 仅修改内部数组,不调用 `putenv()`。
23//!   Rust `set()` 同样仅写入内部 `HashMap`,避免 `std::env::set_var` 的线程安全问题。
24//!
25//! ## .env 文件格式
26//!
27//! 支持 INI 风格的 section 嵌套:
28//!
29//! ```ini
30//! APP_DEBUG = true
31//! APP_KEY = base64:xxxxxx
32//!
33//! [database]
34//! hostname = localhost
35//! port = 3306
36//! ```
37//!
38//! 访问方式:
39//! - `APP_DEBUG` → 顶层键
40//! - `database.hostname` → section 内键
41//!
42//! ## 架构说明
43//!
44//! - **无外部依赖**:不依赖 `dotenv` / `dotenvy` crate,自行实现 INI 解析
45//! - **线程安全**:通过 `Arc<RwLock<HashMap>>` 提供并发读、互斥写
46//! - **不修改进程环境变量**:所有 `set()` 仅写入内部存储
47
48use parking_lot::RwLock;
49use std::collections::HashMap;
50use std::fs;
51use std::path::Path;
52use std::sync::Arc;
53use thiserror::Error;
54
55// ============================================================================
56// 错误类型
57// ============================================================================
58
59/// Env 错误
60#[derive(Debug, Error)]
61pub enum EnvError {
62    /// `.env` 文件读取失败
63    #[error(".env 文件读取失败: {path} — {source}")]
64    FileRead {
65        /// 文件路径
66        path: String,
67        /// 底层 IO 错误
68        #[source]
69        source: std::io::Error,
70    },
71    /// `.env` 文件解析失败
72    #[error(".env 文件解析失败: {path} — 行 {line}: {message}")]
73    Parse {
74        /// 文件路径
75        path: String,
76        /// 出错的行号(从 1 开始)
77        line: usize,
78        /// 错误描述
79        message: String,
80    },
81}
82
83// ============================================================================
84// Env 主体
85// ============================================================================
86
87/// 环境变量管理器 — 对齐 PHP `think\facade\Env`
88///
89/// 通过 `.env` 文件加载配置,同时支持读取真实进程环境变量。
90///
91/// # 优先级
92///
93/// `get()` 查找顺序:
94/// 1. 真实进程环境变量 `std::env::var(name)`
95/// 2. 内部存储(`.env` 文件加载或 `set()` 写入的值)
96///
97/// # 线程安全
98///
99/// 内部存储通过 `Arc<RwLock<HashMap>>` 保护,支持并发读、互斥写。
100/// 不调用 `std::env::set_var`,避免 Rust 2024 edition 的线程安全警告。
101///
102/// # PHP 对齐
103///
104/// ```php
105/// // PHP think\facade\Env
106/// Env::load('.env');          // 加载 .env 文件
107/// Env::set('APP_KEY', 'xxx'); // 设置内部变量
108/// Env::get('APP_KEY');        // 获取(优先真实环境变量)
109/// Env::has('APP_KEY');        // 检查存在
110/// ```
111#[derive(Debug, Clone, Default)]
112pub struct Env {
113    /// 内部存储(.env 文件加载 + set() 写入)
114    data: Arc<RwLock<HashMap<String, String>>>,
115}
116
117impl Env {
118    /// 创建空的 Env 实例
119    pub fn new() -> Self {
120        Self::default()
121    }
122
123    /// 从 `.env` 文件加载配置
124    ///
125    /// 支持 INI 风格的 `[section]` 段,section 内的键会以 `section.key` 形式存储。
126    ///
127    /// # 参数
128    ///
129    /// - `path`: `.env` 文件路径
130    ///
131    /// # 返回
132    ///
133    /// 成功返回 `Ok(())`,失败返回 [`EnvError`]。
134    ///
135    /// # PHP 对齐
136    ///
137    /// ```php
138    /// Env::load('.env');
139    /// ```
140    ///
141    /// # 错误
142    ///
143    /// - [`EnvError::FileRead`][]: 文件读取失败
144    /// - [`EnvError::Parse`][]: 文件解析失败(格式错误)
145    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    /// 解析 INI 风格内容并写入内部存储
156    ///
157    /// # 格式规则
158    ///
159    /// - `key = value` → 顶层键值对
160    /// - `[section]` → 后续键值对存储为 `section.key`
161    /// - `#` 或 `;` 开头的行 → 注释,忽略
162    /// - 空行 → 忽略
163    /// - 引号包裹的值会去除引号(`"value"` → `value`)
164    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            // 空行跳过
173            if line.is_empty() {
174                continue;
175            }
176
177            // 注释行跳过(# 或 ; 开头)
178            if line.starts_with('#') || line.starts_with(';') {
179                continue;
180            }
181
182            // section 头:[section]
183            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            // 键值对:key = value
197            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                // 去除引号包裹
210                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                // 拼接完整键名(section.key 或顶层 key)
219                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    /// 获取环境变量值
239    ///
240    /// # 优先级
241    ///
242    /// 1. 真实进程环境变量 `std::env::var(name)`
243    /// 2. 内部存储(`.env` 文件加载或 `set()` 写入的值)
244    ///
245    /// # 参数
246    ///
247    /// - `name`: 环境变量名(支持点分隔,如 `database.hostname`)
248    ///
249    /// # 返回
250    ///
251    /// 存在返回 `Some(value)`,不存在返回 `None`。
252    ///
253    /// # PHP 对齐
254    ///
255    /// ```php
256    /// Env::get('APP_KEY');  // 无默认值,不存在返回 null
257    /// ```
258    pub fn get(&self, name: &str) -> Option<String> {
259        // 优先真实进程环境变量
260        if let Ok(value) = std::env::var(name) {
261            if !value.is_empty() {
262                return Some(value);
263            }
264        }
265
266        // 其次内部存储
267        let data = self.data.read();
268        data.get(name).cloned()
269    }
270
271    /// 获取环境变量值,不存在时返回默认值
272    ///
273    /// # 参数
274    ///
275    /// - `name`: 环境变量名
276    /// - `default`: 默认值
277    ///
278    /// # 返回
279    ///
280    /// 存在返回实际值,不存在返回 `default`。
281    ///
282    /// # PHP 对齐
283    ///
284    /// ```php
285    /// Env::get('APP_DEBUG', false);  // 不存在时返回 false
286    /// ```
287    pub fn get_with_default(&self, name: &str, default: &str) -> String {
288        self.get(name).unwrap_or_else(|| default.to_string())
289    }
290
291    /// 检查环境变量是否存在
292    ///
293    /// # 优先级
294    ///
295    /// 同 [`Env::get`]:真实进程环境变量优先于内部存储。
296    ///
297    /// # 参数
298    ///
299    /// - `name`: 环境变量名
300    ///
301    /// # 返回
302    ///
303    /// 存在返回 `true`,否则返回 `false`。
304    ///
305    /// # PHP 对齐
306    ///
307    /// ```php
308    /// Env::has('APP_KEY');
309    /// ```
310    pub fn has(&self, name: &str) -> bool {
311        // 优先真实进程环境变量
312        if let Ok(value) = std::env::var(name) {
313            if !value.is_empty() {
314                return true;
315            }
316        }
317
318        // 其次内部存储
319        let data = self.data.read();
320        data.contains_key(name)
321    }
322
323    /// 设置环境变量(仅写入内部存储)
324    ///
325    /// # 注意
326    ///
327    /// 本方法**不调用** `std::env::set_var`,仅修改内部 `HashMap`。
328    /// 这样做的原因:
329    /// 1. 避免 Rust 2024 edition 中 `set_var` 的线程安全警告
330    /// 2. 对齐 PHP `think\facade\Env::set()` 的行为(仅修改内部数组,不调用 `putenv()`)
331    ///
332    /// # 参数
333    ///
334    /// - `name`: 环境变量名
335    /// - `value`: 环境变量值
336    ///
337    /// # PHP 对齐
338    ///
339    /// ```php
340    /// Env::set('APP_KEY', 'base64:xxxxxx');
341    /// ```
342    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    /// 删除内部存储中的环境变量
348    ///
349    /// # 注意
350    ///
351    /// 本方法仅删除内部存储中的值,**不影响**真实进程环境变量。
352    ///
353    /// # 参数
354    ///
355    /// - `name`: 环境变量名
356    ///
357    /// # 返回
358    ///
359    /// 如果内部存储中存在该键并已删除,返回 `true`;否则返回 `false`。
360    pub fn remove(&self, name: &str) -> bool {
361        let mut data = self.data.write();
362        data.remove(name).is_some()
363    }
364
365    /// 获取内部存储的所有键值对(快照)
366    ///
367    /// # 注意
368    ///
369    /// 返回的是内部存储的副本,**不包含**真实进程环境变量。
370    /// 主要用于调试和测试。
371    ///
372    /// # 返回
373    ///
374    /// 所有内部存储键值对的 `HashMap`。
375    pub fn all(&self) -> HashMap<String, String> {
376        let data = self.data.read();
377        data.clone()
378    }
379
380    /// 清空内部存储
381    ///
382    /// # 注意
383    ///
384    /// 仅清空内部存储,**不影响**真实进程环境变量。
385    pub fn clear(&self) {
386        let mut data = self.data.write();
387        data.clear();
388    }
389}
390
391// ============================================================================
392// 单元测试
393// ============================================================================
394
395#[cfg(test)]
396mod tests {
397    use super::*;
398    use std::io::Write;
399
400    /// 测试空 Env 实例
401    #[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    /// 测试 set/get/remove 基本流程
410    #[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    /// 测试 get_with_default
424    #[test]
425    fn test_get_with_default() {
426        let env = Env::new();
427
428        // 不存在时返回默认值
429        assert_eq!(env.get_with_default("MISSING", "fallback"), "fallback");
430
431        // 存在时返回实际值
432        env.set("EXISTING", "actual");
433        assert_eq!(env.get_with_default("EXISTING", "fallback"), "actual");
434    }
435
436    /// 测试从 INI 格式字符串加载(含 section)
437    #[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        // 验证顶层键
455        assert_eq!(env.get("APP_DEBUG"), Some("true".to_string()));
456        assert_eq!(env.get("APP_KEY"), Some("base64:secret".to_string()));
457
458        // 验证 section 内键
459        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    /// 测试引号去除(双引号和单引号)
465    #[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    /// 测试注释行跳过(# 和 ;)
483    #[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    /// 测试从真实文件加载
499    #[test]
500    fn test_load_from_file() {
501        // 创建临时 .env 文件
502        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    /// 测试文件不存在时返回错误
523    #[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    /// 测试解析错误:section 头未闭合
535    #[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    /// 测试解析错误:缺少等号
550    #[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    /// 测试解析错误:键为空
565    #[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    /// 测试真实进程环境变量优先于内部存储
580    ///
581    /// 验证:当 `std::env::var(name)` 返回非空值时,`get()` 返回进程环境变量值,
582    /// 而非内部存储的值。
583    #[test]
584    fn test_process_env_takes_priority() {
585        let env = Env::new();
586
587        // 内部存储设置一个值
588        env.set("SZ_RUST_TEST_ENV_PRIORITY", "internal_value");
589
590        // 同时设置进程环境变量
591        std::env::set_var("SZ_RUST_TEST_ENV_PRIORITY", "process_value");
592
593        // get() 应返回进程环境变量值
594        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    /// 测试进程环境变量为空字符串时回退到内部存储
603    ///
604    /// 验证:当 `std::env::var(name)` 返回空字符串时,`get()` 回退到内部存储。
605    #[test]
606    fn test_empty_process_env_falls_back_to_internal() {
607        let env = Env::new();
608
609        // 内部存储设置一个值
610        env.set("SZ_RUST_TEST_EMPTY_FALLBACK", "internal_value");
611
612        // 设置进程环境变量为空字符串
613        std::env::set_var("SZ_RUST_TEST_EMPTY_FALLBACK", "");
614
615        // get() 应回退到内部存储
616        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    /// 测试 clear 清空内部存储
625    #[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    /// 测试 all() 返回内部存储快照
637    #[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        // 修改内部存储不影响快照
649        env.set("KEY3", "value3");
650        assert_eq!(snapshot.len(), 2);
651    }
652
653    /// 测试 remove 不存在的键返回 false
654    #[test]
655    fn test_remove_nonexistent_returns_false() {
656        let env = Env::new();
657        assert!(!env.remove("NON_EXISTENT"));
658    }
659
660    /// 测试跨 section 重复加载(覆盖语义)
661    ///
662    /// 验证:同一键名在不同 section 下是独立的(`section1.key` vs `section2.key`),
663    /// 但同 section 内的同名键会被覆盖。
664    #[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    /// 测试多次 load_from_file 累加而非覆盖
681    ///
682    /// 验证:连续调用 `load_from_file` 会累加键值对,而非清空后重新加载。
683    /// 这对齐 PHP `think\facade\Env::load()` 的行为。
684    #[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}