1use anyhow::Context as _;
13
14pub fn split_env_entry(entry: &str) -> Result<(&str, &str), String> {
20 let (k, v) = entry
21 .split_once('=')
22 .ok_or_else(|| format!("env entry must be KEY=VALUE, got: {entry:?}"))?;
23 let key = k.trim();
24 if key.is_empty() {
25 return Err(format!("env entry has empty key: {entry:?}"));
26 }
27 Ok((key, v))
28}
29
30pub fn parse_env_entries(entries: &[String]) -> anyhow::Result<Vec<(String, String)>> {
35 entries
36 .iter()
37 .map(|e| {
38 split_env_entry(e)
39 .map(|(k, v)| (k.to_string(), v.to_string()))
40 .map_err(anyhow::Error::msg)
41 })
42 .collect()
43}
44
45pub fn render_env_entries<F>(entries: &[String], render: F) -> anyhow::Result<Vec<(String, String)>>
61where
62 F: Fn(&str) -> anyhow::Result<String>,
63{
64 let parsed = parse_env_entries(entries)?;
65 parsed
66 .into_iter()
67 .map(|(k, v)| {
68 let rendered = render(&v).with_context(|| format!("render env value for '{k}'"))?;
69 Ok((k, rendered))
70 })
71 .collect()
72}
73
74#[cfg(test)]
75mod tests {
76 use super::*;
77
78 #[test]
79 fn test_split_env_entry_basic() {
80 assert_eq!(split_env_entry("KEY=value").unwrap(), ("KEY", "value"));
81 }
82
83 #[test]
84 fn test_split_env_entry_split_on_first_equals() {
85 assert_eq!(
86 split_env_entry("FLAGS=--key=val --other=stuff").unwrap(),
87 ("FLAGS", "--key=val --other=stuff")
88 );
89 }
90
91 #[test]
92 fn test_split_env_entry_no_equals_errors() {
93 let err = split_env_entry("COSIGN_PASSWORD").unwrap_err();
94 assert!(err.contains("must be KEY=VALUE"), "{err}");
95 }
96
97 #[test]
98 fn test_split_env_entry_empty_key_errors() {
99 let err = split_env_entry("=value").unwrap_err();
100 assert!(err.contains("empty key"), "{err}");
101 }
102
103 #[test]
104 fn test_parse_env_entries_preserves_order() {
105 let input = vec![
106 "FIRST=1".to_string(),
107 "SECOND=2".to_string(),
108 "THIRD=3".to_string(),
109 ];
110 let parsed = parse_env_entries(&input).unwrap();
111 assert_eq!(
112 parsed,
113 vec![
114 ("FIRST".to_string(), "1".to_string()),
115 ("SECOND".to_string(), "2".to_string()),
116 ("THIRD".to_string(), "3".to_string()),
117 ]
118 );
119 }
120
121 #[test]
122 fn test_render_env_entries_propagates_render_errors() {
123 let input = vec!["GOOD=ok".to_string(), "BAD=fail".to_string()];
124 let err = render_env_entries(&input, |v| {
125 if v == "fail" {
126 Err(anyhow::anyhow!("render boom"))
127 } else {
128 Ok(v.to_string())
129 }
130 })
131 .unwrap_err();
132 let msg = format!("{err:#}");
133 assert!(msg.contains("BAD"), "error should label key BAD: {msg}");
134 assert!(
135 msg.contains("render boom"),
136 "error chain should include underlying cause: {msg}"
137 );
138 }
139
140 #[test]
141 fn test_render_env_entries_passes_through_when_render_is_identity() {
142 let input = vec!["A=1".to_string(), "B=2".to_string()];
143 let rendered = render_env_entries(&input, |v| Ok(v.to_string())).unwrap();
144 assert_eq!(
145 rendered,
146 vec![("A".into(), "1".into()), ("B".into(), "2".into())]
147 );
148 }
149}