1use crate::span::{is_single_line, line_column_from_line};
44use crate::{Parse, Source};
45use cfg_if::cfg_if;
46use tanzim_value::{Comment, Error, LocatedValue, Location, Map, Value};
47
48#[derive(Clone, Copy, Default)]
70pub struct Env;
71
72impl Env {
73 pub fn new() -> Self {
75 Self
76 }
77}
78
79fn hash_comment_body(line: &str) -> Option<String> {
80 let trimmed = line.trim();
81 if !trimmed.starts_with('#') {
82 return None;
83 }
84 Some(trimmed[1..].trim_start().to_string())
85}
86
87fn parse_double_quoted_value(input: &str) -> Option<(String, &str)> {
88 let mut out = String::new();
89 let mut index = 1usize;
90 while index < input.len() {
91 let ch = input[index..].chars().next()?;
92 let ch_len = ch.len_utf8();
93 if ch == '"' {
94 return Some((out, &input[index + ch_len..]));
95 }
96 if ch == '\\' {
97 index += ch_len;
98 if index >= input.len() {
99 out.push('\\');
100 break;
101 }
102 let next = input[index..].chars().next()?;
103 let next_len = next.len_utf8();
104 match next {
105 'n' => out.push('\n'),
106 'r' => out.push('\r'),
107 't' => out.push('\t'),
108 '"' => out.push('"'),
109 '\\' => out.push('\\'),
110 other => {
111 out.push('\\');
112 out.push(other);
113 }
114 }
115 index += next_len;
116 } else {
117 out.push(ch);
118 index += ch_len;
119 }
120 }
121 None
122}
123
124fn parse_single_quoted_value(input: &str) -> Option<(String, &str)> {
125 let mut index = 1usize;
126 while index < input.len() {
127 let ch = input[index..].chars().next()?;
128 if ch == '\'' {
129 return Some((input[1..index].to_string(), &input[index + ch.len_utf8()..]));
130 }
131 index += ch.len_utf8();
132 }
133 None
134}
135
136fn parse_env_value_and_comment(value_part: &str) -> (String, Option<String>) {
137 let trimmed = value_part.trim_start();
138 if trimmed.starts_with('"')
139 && let Some((value, rest)) = parse_double_quoted_value(trimmed)
140 {
141 return (value, hash_comment_body(rest));
142 } else if trimmed.starts_with('\'')
143 && let Some((value, rest)) = parse_single_quoted_value(trimmed)
144 {
145 return (value, hash_comment_body(rest));
146 }
147 if let Some(space_index) = trimmed.find(" #") {
148 let value = trimmed[..space_index].trim_end();
149 let comment = trimmed[space_index + 1..].trim();
150 if comment.starts_with('#') {
151 return (value.to_string(), hash_comment_body(comment));
152 }
153 }
154 (trimmed.to_string(), None)
155}
156
157impl Parse for Env {
158 fn name(&self) -> &str {
159 "Environment-Variables"
160 }
161
162 fn supported_format_list(&self) -> Vec<String> {
163 vec!["env".into()]
164 }
165
166 fn parse(
167 &self,
168 source: &Source,
169 bytes: &[u8],
170 other_source_list: &[Source],
171 ) -> Result<LocatedValue, Error> {
172 fn insert_nested(map: &mut Map, parts: &[String], value: LocatedValue) {
173 if parts.is_empty() {
174 return;
175 }
176 if parts.len() == 1 {
177 map.insert(parts[0].clone(), value);
178 return;
179 }
180 let head = parts[0].clone();
181 let rest = &parts[1..];
182 match map.get_mut(&head) {
183 Some(existing) => {
184 if let Value::Map(inner) = existing.value_mut() {
185 insert_nested(inner, rest, value);
186 return;
187 }
188 let loc = value.location().clone();
189 let mut inner = Map::new();
190 insert_nested(&mut inner, rest, value);
191 existing.set_value(Value::Map(inner));
192 existing.set_location(loc);
193 }
194 None => {
195 let loc = value.location().clone();
196 let mut inner = Map::new();
197 insert_nested(&mut inner, rest, value);
198 map.insert(head, LocatedValue::new(Value::Map(inner), loc));
199 }
200 }
201 }
202
203 #[cfg(any(feature = "tracing", feature = "logging"))]
204 let source_name = source.source();
205 #[cfg(any(feature = "tracing", feature = "logging"))]
206 let resource = source.resource();
207 cfg_if! {
208 if #[cfg(feature = "tracing")] {
209 tracing::debug!(msg = "Parsing env-format configuration", source = source_name, resource = resource, bytes = bytes.len());
210 } else if #[cfg(feature = "logging")] {
211 log::debug!("msg=\"Parsing env-format configuration\" source={source_name} resource={resource} bytes={}", bytes.len());
212 }
213 }
214
215 let (separator, lowercase) = if source.source() == "env" {
216 let separator = match source.options().get("separator") {
217 None => None,
218 Some(value) => value.as_string().cloned(),
219 };
220 let lowercase = match source.options().get("lowercase") {
221 None => true,
222 Some(value) => value.as_bool().unwrap_or(true),
223 };
224 (separator, lowercase)
225 } else {
226 let mut env_sources: Vec<&Source> = Vec::new();
227 for other in other_source_list {
228 if other.source() == "env" {
229 env_sources.push(other);
230 }
231 }
232 if env_sources.is_empty() {
233 let separator = match source.options().get("separator") {
234 None => None,
235 Some(value) => value.as_string().cloned(),
236 };
237 let lowercase = match source.options().get("lowercase") {
238 None => true,
239 Some(value) => value.as_bool().unwrap_or(true),
240 };
241 (separator, lowercase)
242 } else {
243 let mut first_separator: Option<Option<String>> = None;
244 for env_source in &env_sources {
245 let sep = match env_source.options().get("separator") {
246 None => None,
247 Some(value) => value.as_string().cloned(),
248 };
249 match &first_separator {
250 None => first_separator = Some(sep),
251 Some(expected) => {
252 if *expected != sep {
253 return Err(Error::Parse {
254 location: Some(Box::new(Location::in_source(
255 source.clone(),
256 None,
257 None,
258 None,
259 ))),
260 message: "cannot determine env separator: multiple env sources with different separator options".to_string(),
261 });
262 }
263 }
264 }
265 }
266 let separator = first_separator.unwrap_or(None);
267 let lowercase = match env_sources[0].options().get("lowercase") {
268 None => true,
269 Some(value) => value.as_bool().unwrap_or(true),
270 };
271 (separator, lowercase)
272 }
273 };
274
275 let text = match std::str::from_utf8(bytes) {
276 Ok(value) => value,
277 Err(_) => {
278 return Err(Error::InvalidUtf8 {
279 location: Box::new(Location::in_source(source.clone(), None, None, None)),
280 });
281 }
282 };
283 let single_line = is_single_line(bytes);
284 let mut map = Map::new();
285 let mut pending_before: Vec<String> = Vec::new();
286 let mut line_number = 0usize;
287 let mut offset = 0usize;
288 while offset < text.len() {
289 let rest = &text[offset..];
290 let line_end = match rest.find('\n') {
291 Some(index) => index,
292 None => rest.len(),
293 };
294 let line = &rest[..line_end];
295 line_number += 1;
296 let trimmed = line.trim();
297 if trimmed.is_empty() {
298 offset += line_end;
299 if offset < text.len() {
300 offset += 1;
301 }
302 continue;
303 }
304 if let Some(body) = hash_comment_body(trimmed) {
305 pending_before.push(body);
306 } else {
307 let mut line_body = trimmed;
308 if line_body.starts_with("export ") {
309 line_body = line_body["export ".len()..].trim_start();
310 }
311 if let Some(equal_index) = line_body.find('=') {
312 let key = line_body[..equal_index].trim();
313 let value_part = line_body[equal_index + 1..].trim();
314 if !key.is_empty() {
315 let key_start = line.find(key).unwrap_or(0);
316 let column = line_column_from_line(line, 1, key_start);
317 let (value, after_comment) = parse_env_value_and_comment(value_part);
318 let location = if single_line {
319 Location::in_source(source.clone(), None, None, None)
320 } else {
321 Location::in_text(
322 source.clone(),
323 text,
324 Some(line_number),
325 Some(column),
326 None,
327 )
328 };
329 let final_key = if lowercase {
330 key.to_lowercase()
331 } else {
332 key.to_string()
333 };
334 let mut located_value = LocatedValue::new(Value::String(value), location);
335 let mut comment = Comment::new();
336 if !pending_before.is_empty() {
337 comment.set_before(pending_before.clone());
338 pending_before.clear();
339 }
340 if let Some(after) = after_comment {
341 comment.set_after(Some(after));
342 }
343 if !comment.before().is_empty() || comment.after().is_some() {
344 located_value = located_value.with_comment(comment);
345 }
346 match &separator {
347 None => {
348 map.insert(final_key, located_value);
349 }
350 Some(sep) => {
351 let mut part_list: Vec<String> = Vec::new();
352 let mut remaining = final_key.as_str();
353 loop {
354 if let Some(index) = remaining.find(sep.as_str()) {
355 part_list.push(remaining[..index].to_string());
356 remaining = &remaining[index + sep.len()..];
357 } else {
358 part_list.push(remaining.to_string());
359 break;
360 }
361 }
362 if part_list.len() == 1 {
363 map.insert(part_list[0].clone(), located_value);
364 } else {
365 insert_nested(&mut map, &part_list, located_value);
366 }
367 }
368 }
369 }
370 }
371 }
372 offset += line_end;
373 if offset < text.len() {
374 offset += 1;
375 }
376 }
377 cfg_if! {
378 if #[cfg(feature = "tracing")] {
379 tracing::trace!(msg = "Parsed env-format configuration", source = source_name, resource = resource, key_count = map.len());
380 } else if #[cfg(feature = "logging")] {
381 log::trace!("msg=\"Parsed env-format configuration\" source={source_name} resource={resource} key_count={}", map.len());
382 }
383 }
384 Ok(LocatedValue::new(
385 Value::Map(map),
386 Location::in_source(source.clone(), None, None, None),
387 ))
388 }
389
390 fn is_format_supported(&self, bytes: &[u8]) -> Option<bool> {
391 let text = std::str::from_utf8(bytes).ok()?;
392 for line in text.split('\n') {
393 let line = line.trim();
394 if !line.is_empty() && !line.starts_with('#') && line.contains('=') {
395 return Some(true);
396 }
397 }
398 Some(false)
399 }
400}
401
402pub fn unparse<V: AsRef<Value>>(
423 source: &Source,
424 value: V,
425) -> Result<String, Box<dyn std::error::Error + Send + Sync + 'static>> {
426 let value = value.as_ref();
427 let map = match value.as_map() {
428 Some(map) => map,
429 None => {
430 return Err(format!("env root must be a map, found {}", value.type_name()).into());
431 }
432 };
433 let separator = source
434 .options()
435 .get("separator")
436 .and_then(|value| value.as_string().cloned());
437 let mut out = String::new();
438 write_env(&mut out, map, "", separator.as_deref())?;
439 Ok(out)
440}
441
442fn write_env(
443 out: &mut String,
444 map: &Map,
445 prefix: &str,
446 separator: Option<&str>,
447) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
448 for (key, item) in map.entries() {
449 if matches!(item.value(), Value::Null) {
450 continue;
451 }
452 let full_key = format!("{prefix}{key}");
453 match item.value() {
454 Value::Map(inner) => {
455 let separator = match separator {
456 Some(separator) => separator,
457 None => {
458 return Err(format!(
459 "cannot serialize nested map at key {full_key:?} to env without a separator option"
460 )
461 .into());
462 }
463 };
464 write_env(
465 out,
466 inner,
467 &format!("{full_key}{separator}"),
468 Some(separator),
469 )?;
470 }
471 Value::List(_) => {
472 return Err(format!(
473 "cannot serialize list at key {full_key:?} to env: env has no list representation"
474 )
475 .into());
476 }
477 scalar => {
478 for before in item.comment().before() {
479 out.push_str("# ");
480 out.push_str(before);
481 if !before.ends_with('\n') {
482 out.push('\n');
483 }
484 }
485 out.push_str(&full_key);
486 out.push('=');
487 match scalar {
488 Value::Bool(value) => out.push_str(if *value { "true" } else { "false" }),
489 Value::Int(value) => out.push_str(&value.to_string()),
490 Value::Float(value) => out.push_str(&format!("{value:?}")),
491 Value::String(value) => {
492 let needs_quote = value.is_empty()
493 || value.contains(|ch: char| {
494 ch.is_whitespace() || matches!(ch, '"' | '\'' | '#' | '=')
495 });
496 if needs_quote {
497 out.push('"');
498 for ch in value.chars() {
499 match ch {
500 '"' => out.push_str("\\\""),
501 '\\' => out.push_str("\\\\"),
502 '\n' => out.push_str("\\n"),
503 '\r' => out.push_str("\\r"),
504 '\t' => out.push_str("\\t"),
505 other => out.push(other),
506 }
507 }
508 out.push('"');
509 } else {
510 out.push_str(value);
511 }
512 }
513 Value::Null => {}
514 Value::List(_) | Value::Map(_) => {}
516 }
517 if let Some(after) = item.comment().after() {
518 out.push_str(" # ");
519 out.push_str(after);
520 }
521 out.push('\n');
522 }
523 }
524 }
525 Ok(())
526}