1use std::io::{Read, Write};
2
3use indexmap::IndexMap;
4
5use crate::format::{FormatReader, FormatWriter};
6use crate::value::Value;
7
8pub struct EnvReader;
16
17impl EnvReader {
18 fn parse_line(line: &str) -> Option<(String, Value)> {
21 let trimmed = line.trim();
22
23 if trimmed.is_empty() || trimmed.starts_with('#') {
25 return None;
26 }
27
28 let trimmed = trimmed
30 .strip_prefix("export ")
31 .or_else(|| trimmed.strip_prefix("export\t"))
32 .unwrap_or(trimmed);
33
34 let eq_pos = trimmed.find('=')?;
36 let key = trimmed[..eq_pos].trim().to_string();
37 if key.is_empty() {
38 return None;
39 }
40
41 let raw_value = trimmed[eq_pos + 1..].trim();
42 let value = Self::parse_value(raw_value);
43
44 Some((key, Value::String(value)))
45 }
46
47 fn parse_value(raw: &str) -> String {
50 if raw.len() >= 2
51 && ((raw.starts_with('"') && raw.ends_with('"'))
52 || (raw.starts_with('\'') && raw.ends_with('\'')))
53 {
54 let inner = &raw[1..raw.len() - 1];
55 if raw.starts_with('"') {
57 return inner
58 .replace("\\n", "\n")
59 .replace("\\r", "\r")
60 .replace("\\t", "\t")
61 .replace("\\\"", "\"")
62 .replace("\\\\", "\\");
63 }
64 return inner.to_string();
66 }
67
68 if let Some(comment_pos) = raw.find(" #") {
70 raw[..comment_pos].trim_end().to_string()
71 } else {
72 raw.to_string()
73 }
74 }
75}
76
77impl FormatReader for EnvReader {
78 fn read(&self, input: &str) -> anyhow::Result<Value> {
79 let mut map = IndexMap::new();
80
81 for (line_num, line) in input.lines().enumerate() {
82 match Self::parse_line(line) {
83 Some((key, value)) => {
84 map.insert(key, value);
85 }
86 None => {
87 let trimmed = line.trim();
90 if !trimmed.is_empty()
91 && !trimmed.starts_with('#')
92 && !trimmed.starts_with("export ")
93 && !trimmed.contains('=')
94 {
95 return Err(crate::error::DkitError::ParseErrorAt {
96 format: "ENV".to_string(),
97 source: "invalid line: expected KEY=VALUE format".to_string().into(),
98 line: line_num + 1,
99 column: 1,
100 line_text: line.to_string(),
101 }
102 .into());
103 }
104 }
105 }
106 }
107
108 Ok(Value::Object(map))
109 }
110
111 fn read_from_reader(&self, mut reader: impl Read) -> anyhow::Result<Value> {
112 let mut input = String::new();
113 reader
114 .read_to_string(&mut input)
115 .map_err(|e| crate::error::DkitError::ParseError {
116 format: "ENV".to_string(),
117 source: Box::new(e),
118 })?;
119 self.read(&input)
120 }
121}
122
123pub struct EnvWriter;
128
129impl EnvWriter {
130 fn format_value(value: &Value) -> String {
133 match value {
134 Value::String(s) => Self::quote_if_needed(s),
135 Value::Null => String::new(),
136 Value::Bool(b) => b.to_string(),
137 Value::Integer(n) => n.to_string(),
138 Value::Float(f) => f.to_string(),
139 Value::Array(_) | Value::Object(_) => {
140 let json = serde_json::to_string(value).unwrap_or_default();
142 format!("'{json}'")
143 }
144 }
145 }
146
147 fn quote_if_needed(s: &str) -> String {
149 if s.is_empty() {
150 return "\"\"".to_string();
151 }
152
153 let needs_quoting = s.contains(' ')
154 || s.contains('#')
155 || s.contains('"')
156 || s.contains('\'')
157 || s.contains('\n')
158 || s.contains('\r')
159 || s.contains('\t')
160 || s.contains('\\');
161
162 if needs_quoting {
163 let escaped = s
164 .replace('\\', "\\\\")
165 .replace('"', "\\\"")
166 .replace('\n', "\\n")
167 .replace('\r', "\\r")
168 .replace('\t', "\\t");
169 format!("\"{escaped}\"")
170 } else {
171 s.to_string()
172 }
173 }
174}
175
176impl FormatWriter for EnvWriter {
177 fn write(&self, value: &Value) -> anyhow::Result<String> {
178 match value {
179 Value::Object(map) => {
180 let mut lines: Vec<String> = Vec::with_capacity(map.len());
181 for (key, val) in map {
182 let formatted = Self::format_value(val);
183 lines.push(format!("{key}={formatted}"));
184 }
185 let mut output = lines.join("\n");
186 if !output.is_empty() {
187 output.push('\n');
188 }
189 Ok(output)
190 }
191 Value::Array(arr) => {
192 if arr.is_empty() {
194 return Ok(String::new());
195 }
196 let mut map = IndexMap::new();
198 for item in arr {
199 if let Value::Object(obj) = item {
200 for (k, v) in obj {
201 map.insert(k.clone(), v.clone());
202 }
203 } else {
204 anyhow::bail!(
205 "ENV format only supports flat Object data. \
206 Got array with non-object elements."
207 );
208 }
209 }
210 self.write(&Value::Object(map))
211 }
212 _ => {
213 anyhow::bail!(
214 "ENV format only supports Object (key-value pairs). \
215 Got: {}",
216 match value {
217 Value::Null => "null",
218 Value::Bool(_) => "boolean",
219 Value::Integer(_) => "integer",
220 Value::Float(_) => "float",
221 Value::String(_) => "string",
222 _ => "unknown",
223 }
224 );
225 }
226 }
227 }
228
229 fn write_to_writer(&self, value: &Value, mut writer: impl Write) -> anyhow::Result<()> {
230 let output = self.write(value)?;
231 writer
232 .write_all(output.as_bytes())
233 .map_err(|e| crate::error::DkitError::WriteError {
234 format: "ENV".to_string(),
235 source: Box::new(e),
236 })?;
237 Ok(())
238 }
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244
245 #[test]
248 fn test_reader_simple() {
249 let reader = EnvReader;
250 let input = "DB_HOST=localhost\nDB_PORT=5432\n";
251 let v = reader.read(input).unwrap();
252 let obj = v.as_object().unwrap();
253 assert_eq!(
254 obj.get("DB_HOST"),
255 Some(&Value::String("localhost".to_string()))
256 );
257 assert_eq!(obj.get("DB_PORT"), Some(&Value::String("5432".to_string())));
258 }
259
260 #[test]
261 fn test_reader_comments() {
262 let reader = EnvReader;
263 let input = "# This is a comment\nKEY=value\n# Another comment\n";
264 let v = reader.read(input).unwrap();
265 let obj = v.as_object().unwrap();
266 assert_eq!(obj.len(), 1);
267 assert_eq!(obj.get("KEY"), Some(&Value::String("value".to_string())));
268 }
269
270 #[test]
271 fn test_reader_empty_lines() {
272 let reader = EnvReader;
273 let input = "A=1\n\n\nB=2\n";
274 let v = reader.read(input).unwrap();
275 let obj = v.as_object().unwrap();
276 assert_eq!(obj.len(), 2);
277 }
278
279 #[test]
280 fn test_reader_double_quoted() {
281 let reader = EnvReader;
282 let input = "MSG=\"hello world\"\n";
283 let v = reader.read(input).unwrap();
284 assert_eq!(
285 v.as_object().unwrap().get("MSG"),
286 Some(&Value::String("hello world".to_string()))
287 );
288 }
289
290 #[test]
291 fn test_reader_single_quoted() {
292 let reader = EnvReader;
293 let input = "MSG='hello world'\n";
294 let v = reader.read(input).unwrap();
295 assert_eq!(
296 v.as_object().unwrap().get("MSG"),
297 Some(&Value::String("hello world".to_string()))
298 );
299 }
300
301 #[test]
302 fn test_reader_export_prefix() {
303 let reader = EnvReader;
304 let input = "export DB_HOST=localhost\nexport DB_PORT=5432\n";
305 let v = reader.read(input).unwrap();
306 let obj = v.as_object().unwrap();
307 assert_eq!(
308 obj.get("DB_HOST"),
309 Some(&Value::String("localhost".to_string()))
310 );
311 assert_eq!(obj.get("DB_PORT"), Some(&Value::String("5432".to_string())));
312 }
313
314 #[test]
315 fn test_reader_escape_sequences() {
316 let reader = EnvReader;
317 let input = "MSG=\"line1\\nline2\"\n";
318 let v = reader.read(input).unwrap();
319 assert_eq!(
320 v.as_object().unwrap().get("MSG"),
321 Some(&Value::String("line1\nline2".to_string()))
322 );
323 }
324
325 #[test]
326 fn test_reader_single_quote_no_escape() {
327 let reader = EnvReader;
328 let input = "MSG='line1\\nline2'\n";
329 let v = reader.read(input).unwrap();
330 assert_eq!(
331 v.as_object().unwrap().get("MSG"),
332 Some(&Value::String("line1\\nline2".to_string()))
333 );
334 }
335
336 #[test]
337 fn test_reader_empty_value() {
338 let reader = EnvReader;
339 let input = "EMPTY=\n";
340 let v = reader.read(input).unwrap();
341 assert_eq!(
342 v.as_object().unwrap().get("EMPTY"),
343 Some(&Value::String(String::new()))
344 );
345 }
346
347 #[test]
348 fn test_reader_value_with_equals() {
349 let reader = EnvReader;
350 let input = "URL=https://example.com?key=value\n";
351 let v = reader.read(input).unwrap();
352 assert_eq!(
353 v.as_object().unwrap().get("URL"),
354 Some(&Value::String("https://example.com?key=value".to_string()))
355 );
356 }
357
358 #[test]
359 fn test_reader_inline_comment() {
360 let reader = EnvReader;
361 let input = "KEY=value # this is a comment\n";
362 let v = reader.read(input).unwrap();
363 assert_eq!(
364 v.as_object().unwrap().get("KEY"),
365 Some(&Value::String("value".to_string()))
366 );
367 }
368
369 #[test]
370 fn test_reader_quoted_value_preserves_hash() {
371 let reader = EnvReader;
372 let input = "KEY=\"value # not a comment\"\n";
373 let v = reader.read(input).unwrap();
374 assert_eq!(
375 v.as_object().unwrap().get("KEY"),
376 Some(&Value::String("value # not a comment".to_string()))
377 );
378 }
379
380 #[test]
381 fn test_reader_empty_input() {
382 let reader = EnvReader;
383 let v = reader.read("").unwrap();
384 assert!(v.as_object().unwrap().is_empty());
385 }
386
387 #[test]
388 fn test_reader_whitespace_around_key() {
389 let reader = EnvReader;
390 let input = " KEY = value\n";
391 let v = reader.read(input).unwrap();
392 assert_eq!(
393 v.as_object().unwrap().get("KEY"),
394 Some(&Value::String("value".to_string()))
395 );
396 }
397
398 #[test]
399 fn test_reader_from_reader() {
400 let reader = EnvReader;
401 let input = b"KEY=value" as &[u8];
402 let v = reader.read_from_reader(input).unwrap();
403 assert_eq!(
404 v.as_object().unwrap().get("KEY"),
405 Some(&Value::String("value".to_string()))
406 );
407 }
408
409 #[test]
410 fn test_reader_duplicate_keys_last_wins() {
411 let reader = EnvReader;
412 let input = "KEY=first\nKEY=second\n";
413 let v = reader.read(input).unwrap();
414 assert_eq!(
415 v.as_object().unwrap().get("KEY"),
416 Some(&Value::String("second".to_string()))
417 );
418 }
419
420 #[test]
423 fn test_writer_simple() {
424 let writer = EnvWriter;
425 let v = Value::Object({
426 let mut m = IndexMap::new();
427 m.insert("KEY".to_string(), Value::String("value".to_string()));
428 m
429 });
430 let output = writer.write(&v).unwrap();
431 assert_eq!(output, "KEY=value\n");
432 }
433
434 #[test]
435 fn test_writer_multiple_keys() {
436 let writer = EnvWriter;
437 let v = Value::Object({
438 let mut m = IndexMap::new();
439 m.insert("A".to_string(), Value::String("1".to_string()));
440 m.insert("B".to_string(), Value::String("2".to_string()));
441 m
442 });
443 let output = writer.write(&v).unwrap();
444 assert!(output.contains("A=1\n"));
445 assert!(output.contains("B=2\n"));
446 }
447
448 #[test]
449 fn test_writer_quotes_spaces() {
450 let writer = EnvWriter;
451 let v = Value::Object({
452 let mut m = IndexMap::new();
453 m.insert("MSG".to_string(), Value::String("hello world".to_string()));
454 m
455 });
456 let output = writer.write(&v).unwrap();
457 assert_eq!(output, "MSG=\"hello world\"\n");
458 }
459
460 #[test]
461 fn test_writer_empty_value() {
462 let writer = EnvWriter;
463 let v = Value::Object({
464 let mut m = IndexMap::new();
465 m.insert("EMPTY".to_string(), Value::String(String::new()));
466 m
467 });
468 let output = writer.write(&v).unwrap();
469 assert_eq!(output, "EMPTY=\"\"\n");
470 }
471
472 #[test]
473 fn test_writer_null_value() {
474 let writer = EnvWriter;
475 let v = Value::Object({
476 let mut m = IndexMap::new();
477 m.insert("VAL".to_string(), Value::Null);
478 m
479 });
480 let output = writer.write(&v).unwrap();
481 assert_eq!(output, "VAL=\n");
482 }
483
484 #[test]
485 fn test_writer_boolean() {
486 let writer = EnvWriter;
487 let v = Value::Object({
488 let mut m = IndexMap::new();
489 m.insert("DEBUG".to_string(), Value::Bool(true));
490 m
491 });
492 let output = writer.write(&v).unwrap();
493 assert_eq!(output, "DEBUG=true\n");
494 }
495
496 #[test]
497 fn test_writer_integer() {
498 let writer = EnvWriter;
499 let v = Value::Object({
500 let mut m = IndexMap::new();
501 m.insert("PORT".to_string(), Value::Integer(5432));
502 m
503 });
504 let output = writer.write(&v).unwrap();
505 assert_eq!(output, "PORT=5432\n");
506 }
507
508 #[test]
509 fn test_writer_escapes_newline() {
510 let writer = EnvWriter;
511 let v = Value::Object({
512 let mut m = IndexMap::new();
513 m.insert("MSG".to_string(), Value::String("line1\nline2".to_string()));
514 m
515 });
516 let output = writer.write(&v).unwrap();
517 assert_eq!(output, "MSG=\"line1\\nline2\"\n");
518 }
519
520 #[test]
521 fn test_writer_non_object_error() {
522 let writer = EnvWriter;
523 let result = writer.write(&Value::String("hello".to_string()));
524 assert!(result.is_err());
525 }
526
527 #[test]
528 fn test_writer_to_writer() {
529 let writer = EnvWriter;
530 let v = Value::Object({
531 let mut m = IndexMap::new();
532 m.insert("KEY".to_string(), Value::String("value".to_string()));
533 m
534 });
535 let mut buf = Vec::new();
536 writer.write_to_writer(&v, &mut buf).unwrap();
537 let output = String::from_utf8(buf).unwrap();
538 assert_eq!(output, "KEY=value\n");
539 }
540
541 #[test]
544 fn test_roundtrip() {
545 let input = "DB_HOST=localhost\nDB_PORT=5432\nDEBUG=true\n";
546 let reader = EnvReader;
547 let writer = EnvWriter;
548
549 let value = reader.read(input).unwrap();
550 let output = writer.write(&value).unwrap();
551 let value2 = reader.read(&output).unwrap();
552
553 assert_eq!(value, value2);
554 }
555
556 #[test]
557 fn test_roundtrip_quoted() {
558 let input = "MSG=\"hello world\"\nPATH=\"/usr/bin:/usr/local/bin\"\n";
559 let reader = EnvReader;
560 let writer = EnvWriter;
561
562 let value = reader.read(input).unwrap();
563 let output = writer.write(&value).unwrap();
564 let value2 = reader.read(&output).unwrap();
565
566 assert_eq!(value, value2);
567 }
568
569 #[test]
570 fn test_reader_escaped_double_quote() {
571 let reader = EnvReader;
572 let input = r#"MSG="say \"hello\"""#;
573 let v = reader.read(input).unwrap();
574 assert_eq!(
575 v.as_object().unwrap().get("MSG"),
576 Some(&Value::String("say \"hello\"".to_string()))
577 );
578 }
579
580 #[test]
581 fn test_writer_escapes_double_quote() {
582 let writer = EnvWriter;
583 let v = Value::Object({
584 let mut m = IndexMap::new();
585 m.insert(
586 "MSG".to_string(),
587 Value::String("say \"hello\"".to_string()),
588 );
589 m
590 });
591 let output = writer.write(&v).unwrap();
592 assert_eq!(output, "MSG=\"say \\\"hello\\\"\"\n");
593 }
594}