cfgd_core/util/
strings.rs1use crate::config;
2
3pub fn parse_env_var(input: &str) -> std::result::Result<config::EnvVar, String> {
5 let (key, value) = input
6 .split_once('=')
7 .ok_or_else(|| format!("invalid env var '{}' — expected KEY=VALUE", input))?;
8 validate_env_var_user_name(key)?;
9 Ok(config::EnvVar {
10 name: key.to_string(),
11 value: value.to_string(),
12 })
13}
14
15pub fn validate_env_var_user_name(name: &str) -> std::result::Result<(), String> {
18 validate_env_var_name(name)?;
19 if name.starts_with("CFGD_") {
20 return Err(format!(
21 "env var name '{}' is reserved — the CFGD_* prefix is for \
22 cfgd runtime metadata. Rename to e.g. APP_{} or MY_{}.",
23 name,
24 name.trim_start_matches("CFGD_"),
25 name.trim_start_matches("CFGD_"),
26 ));
27 }
28 if name == "BASH_ENV" || name == "ZDOTDIR" {
29 return Err(format!(
30 "env var name '{name}' is reserved — cfgd uses it for \
31 alias delivery to lifecycle scripts"
32 ));
33 }
34 Ok(())
35}
36
37pub fn validate_env_var_name(name: &str) -> std::result::Result<(), String> {
40 if name.is_empty() {
41 return Err("environment variable name must not be empty".to_string());
42 }
43 let first = name.as_bytes()[0];
44 if !first.is_ascii_alphabetic() && first != b'_' {
45 return Err(format!(
46 "invalid env var name '{}' — must start with a letter or underscore",
47 name
48 ));
49 }
50 if !name.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_') {
51 return Err(format!(
52 "invalid env var name '{}' — must contain only letters, digits, and underscores",
53 name
54 ));
55 }
56 Ok(())
57}
58
59pub fn validate_alias_name(name: &str) -> std::result::Result<(), String> {
62 if name.is_empty() {
63 return Err("alias name must not be empty".to_string());
64 }
65 if !name
66 .bytes()
67 .all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-' || b == b'.')
68 {
69 return Err(format!(
70 "invalid alias name '{}' — must contain only letters, digits, underscores, hyphens, and dots",
71 name
72 ));
73 }
74 Ok(())
75}
76
77pub fn parse_alias(input: &str) -> std::result::Result<config::ShellAlias, String> {
79 let (name, command) = input
80 .split_once('=')
81 .ok_or_else(|| format!("invalid alias '{}' — expected name=command", input))?;
82 validate_alias_name(name)?;
83 Ok(config::ShellAlias {
84 name: name.to_string(),
85 command: command.to_string(),
86 })
87}
88
89pub fn sanitize_k8s_name(name: &str) -> String {
93 name.to_ascii_lowercase()
94 .replace('_', "-")
95 .chars()
96 .filter(|c| c.is_ascii_alphanumeric() || *c == '-')
97 .collect::<String>()
98 .trim_matches('-')
99 .to_string()
100}
101
102pub fn shell_escape_value(value: &str) -> String {
109 if !value
110 .bytes()
111 .any(|b| matches!(b, b'$' | b'`' | b'\\' | b'"' | b'\''))
112 {
113 return format!("\"{}\"", value);
114 }
115 if !value.contains('\'') {
117 return format!("'{}'", value);
118 }
119 let mut out = String::with_capacity(value.len() + 8);
121 out.push('\'');
122 for c in value.chars() {
123 if c == '\'' {
124 out.push_str("'\\''");
125 } else {
126 out.push(c);
127 }
128 }
129 out.push('\'');
130 out
131}
132
133pub fn escape_double_quoted(s: &str) -> String {
137 let mut out = String::with_capacity(s.len() + s.len() / 8);
138 for c in s.chars() {
139 match c {
140 '\\' | '"' | '`' | '!' => {
141 out.push('\\');
142 out.push(c);
143 }
144 _ => out.push(c),
145 }
146 }
147 out
148}
149
150pub fn xml_escape(s: &str) -> String {
152 let mut out = String::with_capacity(s.len() + s.len() / 8);
153 for c in s.chars() {
154 match c {
155 '&' => out.push_str("&"),
156 '<' => out.push_str("<"),
157 '>' => out.push_str(">"),
158 '"' => out.push_str("""),
159 '\'' => out.push_str("'"),
160 _ => out.push(c),
161 }
162 }
163 out
164}