1use crate::Deserialize;
18use crate::span::{is_single_line, line_column_from_line};
19use cfg_if::cfg_if;
20use tanzim_value::{Error, LocatedValue, Location, Map, Value};
21
22#[derive(Clone, Copy, Default)]
23pub struct Env;
24
25impl Env {
26 pub fn new() -> Self {
27 Self
28 }
29}
30
31impl Deserialize for Env {
32 fn name(&self) -> &str {
33 "Environment-Variables"
34 }
35
36 fn supported_format_list(&self) -> Vec<String> {
37 vec!["env".into()]
38 }
39
40 fn parse(&self, source: &str, resource: &str, bytes: &[u8]) -> Result<LocatedValue, Error> {
41 cfg_if! {
42 if #[cfg(feature = "tracing")] {
43 tracing::debug!(msg = "Parsing env-format configuration", source = source, resource = resource, bytes = bytes.len());
44 } else if #[cfg(feature = "logging")] {
45 log::debug!("msg=\"Parsing env-format configuration\" source={source} resource={resource} bytes={}", bytes.len());
46 }
47 }
48 let text = match std::str::from_utf8(bytes) {
49 Ok(value) => value,
50 Err(_) => {
51 return Err(Error::InvalidUtf8 {
52 location: Location::at(source, resource, None, None, None),
53 });
54 }
55 };
56 let single_line = is_single_line(bytes);
57 let mut map = Map::new();
58 let mut line_number = 0usize;
59 let mut offset = 0usize;
60 while offset < text.len() {
61 let rest = &text[offset..];
62 let line_end = match rest.find('\n') {
63 Some(index) => index,
64 None => rest.len(),
65 };
66 let line = &rest[..line_end];
67 line_number += 1;
68 let trimmed = line.trim();
69 if !trimmed.is_empty() && !trimmed.starts_with('#') {
70 let mut line_body = trimmed;
71 if line_body.starts_with("export ") {
72 line_body = line_body["export ".len()..].trim_start();
73 }
74 if let Some(equal_index) = line_body.find('=') {
75 let key = line_body[..equal_index].trim();
76 let value_part = line_body[equal_index + 1..].trim();
77 if !key.is_empty() {
78 let key_start = line.find(key).unwrap_or(0);
79 let column = line_column_from_line(line, 1, key_start);
80 let value = if value_part.starts_with('"')
81 && value_part.ends_with('"')
82 && value_part.len() >= 2
83 {
84 let inner = &value_part[1..value_part.len() - 1];
85 let mut out = String::new();
86 let mut index = 0usize;
87 while index < inner.len() {
88 let ch = inner[index..].chars().next().expect("valid utf-8");
89 let ch_len = ch.len_utf8();
90 if ch == '\\' {
91 index += ch_len;
92 if index < inner.len() {
93 let next =
94 inner[index..].chars().next().expect("valid utf-8");
95 let next_len = next.len_utf8();
96 match next {
97 'n' => out.push('\n'),
98 'r' => out.push('\r'),
99 't' => out.push('\t'),
100 '"' => out.push('"'),
101 '\\' => out.push('\\'),
102 other => {
103 out.push('\\');
104 out.push(other);
105 }
106 }
107 index += next_len;
108 } else {
109 out.push('\\');
110 }
111 } else {
112 out.push(ch);
113 index += ch_len;
114 }
115 }
116 out
117 } else if value_part.starts_with('\'')
118 && value_part.ends_with('\'')
119 && value_part.len() >= 2
120 {
121 value_part[1..value_part.len() - 1].to_string()
122 } else {
123 value_part.to_string()
124 };
125 let location = if single_line {
126 Location::at(source, resource, None, None, None)
127 } else {
128 Location::at(source, resource, Some(line_number), Some(column), None)
129 };
130 map.insert(
131 key.to_string(),
132 LocatedValue {
133 value: Value::String(value),
134 location,
135 },
136 );
137 }
138 }
139 }
140 offset += line_end;
141 if offset < text.len() {
142 offset += 1;
143 }
144 }
145 cfg_if! {
146 if #[cfg(feature = "tracing")] {
147 tracing::trace!(msg = "Parsed env-format configuration", source = source, resource = resource, key_count = map.len());
148 } else if #[cfg(feature = "logging")] {
149 log::trace!("msg=\"Parsed env-format configuration\" source={source} resource={resource} key_count={}", map.len());
150 }
151 }
152 Ok(LocatedValue {
153 value: Value::Map(map),
154 location: Location::at(source, resource, None, None, None),
155 })
156 }
157
158 fn is_format_supported(&self, bytes: &[u8]) -> Option<bool> {
159 let text = std::str::from_utf8(bytes).ok()?;
160 for line in text.split('\n') {
161 let line = line.trim();
162 if !line.is_empty() && !line.starts_with('#') && line.contains('=') {
163 return Some(true);
164 }
165 }
166 Some(false)
167 }
168}
169
170#[cfg(all(test, feature = "env"))]
171mod tests {
172 use super::*;
173
174 #[test]
175 fn parses_dotenv_contents() {
176 let parsed = Env::new()
177 .parse("file", ".env", b"FOO=bar\nBAZ=qux\n")
178 .unwrap();
179 let map = parsed.value.as_map().unwrap();
180 assert_eq!(map.get("FOO").unwrap().value.as_string().unwrap(), "bar");
181 assert_eq!(map.get("BAZ").unwrap().value.as_string().unwrap(), "qux");
182 }
183
184 #[test]
185 fn parses_env_with_line_numbers() {
186 let root = Env::new()
187 .parse("file", ".env", b"FOO=bar\nBAZ=qux\n")
188 .unwrap();
189 let map = root.value.as_map().unwrap();
190 let foo = map.get("FOO").unwrap();
191 assert_eq!(foo.value.as_string().unwrap(), "bar");
192 assert_eq!(foo.location.line, std::num::NonZeroU32::new(1));
193 let baz = map.get("BAZ").unwrap();
194 assert_eq!(baz.location.line, std::num::NonZeroU32::new(2));
195 }
196}