1use anyhow::{Result, bail};
4use std::collections::BTreeSet;
5
6#[derive(Clone, Debug, PartialEq, Eq)]
7pub struct EnvVarSpec {
8 pub source: String,
9 pub target: String,
10}
11
12impl EnvVarSpec {
13 pub fn to_with_arg(&self) -> String {
14 if self.source == self.target {
15 self.source.clone()
16 } else {
17 format!("{}={}", self.source, self.target)
18 }
19 }
20}
21
22pub fn parse_env_specs(specs: &[String]) -> Result<Vec<EnvVarSpec>> {
23 let mut parsed = Vec::with_capacity(specs.len());
24 let mut targets = BTreeSet::new();
25 for spec in specs {
26 let (source, target) = spec.split_once('=').unwrap_or((spec, spec));
27 validate_env_key(source)?;
28 validate_env_key(target)?;
29 if !targets.insert(target.to_string()) {
30 bail!("duplicate target variable: {target}");
31 }
32 parsed.push(EnvVarSpec {
33 source: source.to_string(),
34 target: target.to_string(),
35 });
36 }
37 Ok(parsed)
38}
39
40pub fn validate_env_key(key: &str) -> Result<()> {
41 let mut chars = key.chars();
42 let Some(first) = chars.next() else {
43 bail!("environment variable name must not be empty");
44 };
45 if !(first == '_' || first.is_ascii_alphabetic())
46 || !chars.all(|ch| ch == '_' || ch.is_ascii_alphanumeric())
47 {
48 bail!("invalid environment variable name: {key}");
49 }
50 Ok(())
51}
52
53#[cfg(test)]
54mod tests {
55 use super::*;
56
57 #[test]
58 fn ordered_specs_reject_duplicate_targets() {
59 let error = parse_env_specs(&["A=X".into(), "B=X".into()]).unwrap_err();
60 assert!(error.to_string().contains("duplicate target"));
61 }
62}