use std::collections::HashMap;
use std::fs;
use crate::error::TigerError;
pub fn parse_properties_file(path: &str) -> Result<HashMap<String, String>, TigerError> {
let content = fs::read_to_string(path)
.map_err(|e| TigerError::Config(format!("无法打开配置文件 {}: {}", path, e)))?;
parse_properties(&content)
}
pub fn parse_properties(content: &str) -> Result<HashMap<String, String>, TigerError> {
let mut props = HashMap::new();
let mut current_line = String::new();
let mut continuation = false;
for line in content.lines() {
if continuation {
let trimmed = line.trim_start();
if trimmed.ends_with('\\') {
current_line.push_str(&trimmed[..trimmed.len() - 1]);
continue;
}
current_line.push_str(trimmed);
continuation = false;
} else {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
if trimmed.starts_with('#') || trimmed.starts_with('!') {
continue;
}
if trimmed.ends_with('\\') {
current_line = trimmed[..trimmed.len() - 1].to_string();
continuation = true;
continue;
}
current_line = trimmed.to_string();
}
if let Some((key, value)) = parse_key_value(¤t_line) {
props.insert(key, value);
}
current_line.clear();
}
if continuation && !current_line.is_empty() {
if let Some((key, value)) = parse_key_value(¤t_line) {
props.insert(key, value);
}
}
Ok(props)
}
pub fn serialize_properties(props: &HashMap<String, String>) -> String {
let mut lines: Vec<String> = props
.iter()
.map(|(k, v)| format!("{}={}", k, v))
.collect();
lines.sort();
lines.join("\n")
}
fn parse_key_value(line: &str) -> Option<(String, String)> {
let eq_idx = line.find('=');
let colon_idx = line.find(':');
let sep_idx = match (eq_idx, colon_idx) {
(Some(e), Some(c)) => Some(e.min(c)),
(Some(e), None) => Some(e),
(None, Some(c)) => Some(c),
(None, None) => None,
};
let sep_idx = sep_idx?;
let key = line[..sep_idx].trim().to_string();
let value = line[sep_idx + 1..].trim().to_string();
if key.is_empty() {
return None;
}
Some((key, value))
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
use std::io::Write;
#[test]
fn test_parse_basic_key_value() {
let content = "tiger_id=test123\nprivate_key=abc456\n";
let props = parse_properties(content).unwrap();
assert_eq!(props.get("tiger_id").unwrap(), "test123");
assert_eq!(props.get("private_key").unwrap(), "abc456");
}
#[test]
fn test_parse_colon_separator() {
let content = "tiger_id:test123\nprivate_key:abc456\n";
let props = parse_properties(content).unwrap();
assert_eq!(props.get("tiger_id").unwrap(), "test123");
assert_eq!(props.get("private_key").unwrap(), "abc456");
}
#[test]
fn test_parse_comments() {
let content = "# 这是注释\ntiger_id=test123\n! 这也是注释\nprivate_key=abc456\n";
let props = parse_properties(content).unwrap();
assert_eq!(props.len(), 2);
assert_eq!(props.get("tiger_id").unwrap(), "test123");
assert_eq!(props.get("private_key").unwrap(), "abc456");
}
#[test]
fn test_parse_empty_lines() {
let content = "\ntiger_id=test123\n\n\nprivate_key=abc456\n\n";
let props = parse_properties(content).unwrap();
assert_eq!(props.len(), 2);
}
#[test]
fn test_parse_continuation() {
let content = "private_key=MIIEvgIBADANBg\\\n kqhkiG9w0BAQEF\\\n AASCBKgwggSk\n";
let props = parse_properties(content).unwrap();
assert_eq!(
props.get("private_key").unwrap(),
"MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSk"
);
}
#[test]
fn test_parse_trim_spaces() {
let content = " tiger_id = test123 \n private_key = abc456 \n";
let props = parse_properties(content).unwrap();
assert_eq!(props.get("tiger_id").unwrap(), "test123");
assert_eq!(props.get("private_key").unwrap(), "abc456");
}
#[test]
fn test_parse_value_with_equals() {
let content = "private_key=abc=def=ghi\n";
let props = parse_properties(content).unwrap();
assert_eq!(props.get("private_key").unwrap(), "abc=def=ghi");
}
#[test]
fn test_parse_empty_content() {
let content = "";
let props = parse_properties(content).unwrap();
assert!(props.is_empty());
}
#[test]
fn test_parse_properties_file() {
let dir = std::env::temp_dir();
let path = dir.join("test_rust_config.properties");
let mut file = std::fs::File::create(&path).unwrap();
writeln!(file, "tiger_id=test123").unwrap();
writeln!(file, "private_key=abc456").unwrap();
writeln!(file, "account=DU123456").unwrap();
drop(file);
let props = super::parse_properties_file(path.to_str().unwrap()).unwrap();
assert_eq!(props.get("tiger_id").unwrap(), "test123");
assert_eq!(props.get("private_key").unwrap(), "abc456");
assert_eq!(props.get("account").unwrap(), "DU123456");
std::fs::remove_file(&path).ok();
}
#[test]
fn test_parse_nonexistent_file() {
let result = super::parse_properties_file("/nonexistent/path/config.properties");
assert!(result.is_err());
}
fn valid_key_strategy() -> impl Strategy<Value = String> {
"[a-zA-Z][a-zA-Z0-9_]{0,19}"
}
fn valid_value_strategy() -> impl Strategy<Value = String> {
"[a-zA-Z0-9 _./@+\\-]{1,50}"
.prop_map(|s| s.trim().to_string())
.prop_filter("值不能为空", |s| !s.is_empty())
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn properties_round_trip(
pairs in proptest::collection::hash_map(
valid_key_strategy(),
valid_value_strategy(),
1..10
)
) {
let serialized = serialize_properties(&pairs);
let parsed = parse_properties(&serialized).unwrap();
prop_assert_eq!(parsed, pairs);
}
}
}