use serde::Serialize; use serde_yaml_ng::Result; pub fn to_mihomo_config_string(data: &T) -> Result { let yaml = serde_yaml_ng::to_string(data)?; Ok(quote_fake_ip_filter_wildcards(&yaml)) } fn quote_fake_ip_filter_wildcards(yaml: &str) -> String { let mut result = String::with_capacity(yaml.len()); let mut in_fake_ip_filter = false; let mut fake_ip_filter_indent = 0; for line in yaml.lines() { let indent = leading_spaces(line); let trimmed = &line[indent..]; if in_fake_ip_filter && indent <= fake_ip_filter_indent && !trimmed.starts_with("- ") { in_fake_ip_filter = false; } if !in_fake_ip_filter && trimmed == "fake-ip-filter:" { in_fake_ip_filter = true; fake_ip_filter_indent = indent; result.push_str(line); result.push('\n'); continue; } if in_fake_ip_filter && indent >= fake_ip_filter_indent && trimmed.starts_with("- ") { result.push_str(&line[..indent]); result.push_str("- "); result.push_str("e_wildcard_item(&trimmed[2..])); result.push('\n'); continue; } result.push_str(line); result.push('\n'); } result } fn leading_spaces(line: &str) -> usize { line.len() - line.trim_start_matches(' ').len() } fn quote_wildcard_item(item: &str) -> String { if !item.starts_with('\"') && !item.starts_with('\'') && contains_wildcard(item) { quote_string(item) } else { item.to_string() } } fn quote_string(s: &str) -> String { "\'".to_string() + s.replace('\'', "''").as_str() + "\'" } fn contains_wildcard(value: &str) -> bool { value.contains('*') || value.starts_with('+') || value.starts_with('.') } #[cfg(test)] #[allow(clippy::expect_used)] mod tests { use super::to_mihomo_config_string; fn roundtrip(input: &str) { let value: serde_yaml_ng::Value = serde_yaml_ng::from_str(input).expect("input yaml should parse"); let output = to_mihomo_config_string(&value).expect("yaml should serialize"); let reparsed: serde_yaml_ng::Value = serde_yaml_ng::from_str(&output).expect("serialized yaml should parse"); assert_eq!(reparsed, value); } #[test] fn quotes_fake_ip_filter_wildcards_without_changing_values() { let input = r#" dns: fake-ip-filter: - "*.lan" - "+.market.xiaomi.com" - ".example.com" - "time.*.com" - "plain.example.com" "#; roundtrip(input); let value: serde_yaml_ng::Value = serde_yaml_ng::from_str(input).expect("input yaml should parse"); let output = to_mihomo_config_string(&value).expect("yaml should serialize"); assert!(output.contains("- '*.lan'")); assert!(output.contains("- '+.market.xiaomi.com'")); assert!(output.contains("- '.example.com'")); assert!(output.contains("- 'time.*.com'")); assert!(output.contains("- plain.example.com")); } #[test] fn nested_multiline_strings_roundtrip() { roundtrip( r" items: - name: demo desc: | line1 line2 ", ); } }