1#[derive(Clone, Debug, PartialEq, Eq)]
2pub struct LineEntry {
3 pub number: usize,
4 pub raw_string: String,
5
6 pub is_deleted: bool,
8 pub is_last_line: bool,
10}
11
12impl LineEntry {
13 pub fn new<T>(number: usize, raw_string: T, is_last_line: bool) -> Self
14 where
15 T: Into<String>,
16 {
17 LineEntry {
18 number,
19 raw_string: raw_string.into(),
20 is_deleted: false,
21 is_last_line,
22 }
23 }
24
25 pub fn is_empty_or_comment(&self) -> bool {
26 self.is_empty() || self.is_comment()
27 }
28
29 pub fn is_empty(&self) -> bool {
30 self.trimmed_string().is_empty()
31 }
32
33 pub fn is_comment(&self) -> bool {
34 self.trimmed_string().starts_with('#')
35 }
36
37 pub fn get_key(&self) -> Option<&str> {
38 if self.is_empty_or_comment() {
39 return None;
40 }
41
42 let stripped = self.stripped_export_string();
43 Some(stripped.split('=').next().unwrap_or(stripped))
44 }
45
46 pub fn get_value(&self) -> Option<&str> {
47 if self.is_empty_or_comment() {
48 return None;
49 }
50
51 self.raw_string
52 .find('=')
53 .map(|idx| &self.raw_string[(idx + 1)..])
54 }
55
56 fn trimmed_string(&self) -> &str {
57 self.raw_string.trim()
58 }
59
60 fn stripped_export_string(&self) -> &str {
61 let trimmed = self.trimmed_string();
62 trimmed
63 .strip_prefix("export ")
64 .map(str::trim)
65 .unwrap_or(trimmed)
66 }
67
68 pub fn mark_as_deleted(&mut self) {
69 self.is_deleted = true;
70 }
71
72 pub fn get_comment(&self) -> Option<&str> {
76 if !self.is_comment() {
77 return None;
78 }
79
80 Some(self.raw_string.as_str())
81 }
82
83 pub fn get_substitution_keys(&self) -> Vec<&str> {
84 let mut keys = Vec::new();
85
86 let mut value = match self.get_value().map(str::trim) {
87 Some(value) if !value.starts_with('\'') => value,
88 _ => return keys,
89 };
90
91 if value.starts_with('\"') {
92 if value.len() > 1 && value.ends_with('\"') && !is_escaped(&value[..value.len() - 1]) {
93 value = &value[1..value.len() - 1]
94 } else {
95 return keys;
96 }
97 }
98
99 while let Some(index) = value.find('$') {
100 let prefix = &value[..index];
101 let raw_key = &value[index + 1..];
102
103 if is_escaped(prefix) {
104 value = raw_key;
105 } else {
106 let (key, rest) = raw_key
107 .strip_prefix('{')
108 .and_then(|raw_key| raw_key.find('}').map(|i| raw_key.split_at(i)))
109 .or_else(|| {
110 raw_key
111 .find(|c: char| !c.is_ascii_alphanumeric() && c != '_')
112 .map(|i| raw_key.split_at(i))
113 })
114 .unwrap_or((raw_key, ""));
115 if key.is_empty() {
116 return keys;
117 }
118
119 keys.push(key);
120 value = rest;
121 }
122 }
123 keys
124 }
125}
126
127pub fn is_escaped(prefix: &str) -> bool {
128 prefix.chars().rev().take_while(|ch| *ch == '\\').count() % 2 == 1
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 #[test]
136 fn is_escaped_value_test() {
137 let escaped = "\\";
138 assert!(is_escaped(escaped));
139
140 let escaped = "\\\\\\";
141 assert!(is_escaped(escaped));
142
143 let non_escaped = "\\\\";
144 assert!(!is_escaped(non_escaped));
145
146 let random_string = "text without escaping";
147 assert!(!is_escaped(random_string));
148 }
149
150 pub fn line_entry(number: usize, total_lines: usize, raw_string: &str) -> LineEntry {
151 LineEntry::new(number, raw_string, total_lines == number)
152 }
153
154 mod is_empty_or_comment {
155 use super::*;
156
157 #[test]
158 fn run_with_empty_line_test() {
159 let input = line_entry(1, 1, "");
160
161 assert!(input.is_empty());
162 assert!(!input.is_comment());
163 assert!(input.is_empty_or_comment());
164 }
165
166 #[test]
167 fn run_with_comment_line_test() {
168 let input = line_entry(1, 1, "# Comment");
169
170 assert!(!input.is_empty());
171 assert!(input.is_comment());
172 assert!(input.is_empty_or_comment());
173 }
174
175 #[test]
176 fn run_with_not_comment_or_empty_line_test() {
177 let input = line_entry(1, 1, "NotComment");
178
179 assert!(!input.is_empty());
180 assert!(!input.is_comment());
181 assert!(!input.is_empty_or_comment());
182 }
183 }
184
185 mod get_key {
186 use super::*;
187
188 #[test]
189 fn empty_line_test() {
190 let input = line_entry(1, 1, "");
191 let expected = None;
192
193 assert_eq!(expected, input.get_key());
194 }
195
196 #[test]
197 fn stripped_export_prefix_test() {
198 let input = line_entry(1, 1, "export FOO=BAR");
199 let expected = Some("FOO");
200
201 assert_eq!(expected, input.get_key());
202 }
203
204 #[test]
205 fn correct_line_test() {
206 let input = line_entry(1, 1, "FOO=BAR");
207 let expected = Some("FOO");
208
209 assert_eq!(expected, input.get_key());
210 }
211
212 #[test]
213 fn line_without_value_test() {
214 let input = line_entry(1, 1, "FOO=");
215 let expected = Some("FOO");
216
217 assert_eq!(expected, input.get_key());
218 }
219
220 #[test]
221 fn missing_value_and_equal_sign_test() {
222 let input = line_entry(1, 1, "FOOBAR");
223 let expected = Some("FOOBAR");
224
225 assert_eq!(expected, input.get_key());
226 }
227 }
228
229 mod get_value {
230 use super::*;
231
232 #[test]
233 fn empty_line_test() {
234 let input = line_entry(1, 1, "");
235 let expected = None;
236
237 assert_eq!(expected, input.get_value());
238 }
239
240 #[test]
241 fn correct_line_test() {
242 let input = line_entry(1, 1, "FOO=BAR");
243 let expected = Some("BAR");
244
245 assert_eq!(expected, input.get_value());
246 }
247
248 #[test]
249 fn correct_line_with_single_quote_test() {
250 let input = line_entry(1, 1, "FOO='BAR'");
251 let expected = Some("'BAR'");
252
253 assert_eq!(expected, input.get_value());
254 }
255
256 #[test]
257 fn correct_line_with_double_quote_test() {
258 let input = line_entry(1, 1, "FOO=\"BAR\"");
259 let expected = Some("\"BAR\"");
260
261 assert_eq!(expected, input.get_value());
262 }
263
264 #[test]
265 fn line_without_key_test() {
266 let input = line_entry(1, 1, "=BAR");
267 let expected = Some("BAR");
268
269 assert_eq!(expected, input.get_value());
270 }
271
272 #[test]
273 fn line_without_value_test() {
274 let input = line_entry(1, 1, "FOO=");
275 let expected = Some("");
276
277 assert_eq!(expected, input.get_value());
278 }
279
280 #[test]
281 fn missing_value_and_equal_sign_test() {
282 let input = line_entry(1, 1, "FOOBAR");
283 let expected = None;
284
285 assert_eq!(expected, input.get_value());
286 }
287 }
288
289 mod trimmed_string {
290 use super::*;
291
292 #[test]
293 fn line_without_blank_chars_test() {
294 let entry = line_entry(1, 1, "FOO=BAR");
295
296 assert_eq!("FOO=BAR", entry.trimmed_string());
297 }
298
299 #[test]
300 fn line_with_spaces_test() {
301 let entry = line_entry(1, 1, " FOO=BAR ");
302
303 assert_eq!("FOO=BAR", entry.trimmed_string());
304 }
305
306 #[test]
307 fn line_with_tab_test() {
308 let entry = line_entry(1, 1, "FOO=BAR\t");
309
310 assert_eq!("FOO=BAR", entry.trimmed_string());
311 }
312 }
313
314 mod get_comment {
315 use super::*;
316
317 #[test]
318 fn line_with_comment_test() {
319 let entry = line_entry(1, 1, "# dotenv-linter:off LowercaseKey");
320 let comment = entry.get_comment();
321 assert!(comment.is_some());
322
323 }
328
329 #[test]
330 fn line_with_no_comment_test() {
331 let entry = line_entry(1, 1, "A=B");
332 let comment = entry.get_comment();
333 assert!(comment.is_none());
334 }
335 }
336
337 mod get_substitution_keys {
338 use super::*;
339
340 #[test]
341 fn run_with_empty() {
342 let input = line_entry(1, 1, "");
343 assert!(input.get_substitution_keys().is_empty());
344 }
345
346 #[test]
347 fn run_with_simple() {
348 let input = line_entry(1, 1, "FOO=$BAR");
349 assert_eq!(input.get_substitution_keys(), vec!["BAR"]);
350 }
351
352 #[test]
353 fn run_with_simple_comment() {
354 let input = line_entry(1, 1, "FOO=$BAR # comment");
355 assert_eq!(input.get_substitution_keys(), vec!["BAR"]);
356 }
357
358 #[test]
359 fn run_with_curly_braces() {
360 let input = line_entry(1, 1, "FOO=${BAR}");
361 assert_eq!(input.get_substitution_keys(), vec!["BAR"]);
362
363 let input = line_entry(1, 1, "FOO=$BAR}");
364 assert_eq!(input.get_substitution_keys(), vec!["BAR"]);
365
366 let input = line_entry(1, 1, "FOO=${BAR");
367 assert!(input.get_substitution_keys().is_empty());
368 }
369
370 #[test]
371 fn run_with_double_quotes() {
372 let input = line_entry(1, 1, r#"FOO="$BAR""#);
373 assert_eq!(input.get_substitution_keys(), vec!["BAR"]);
374
375 let input = line_entry(1, 1, r#"FOO=$BAR""#);
376 assert_eq!(input.get_substitution_keys(), vec!["BAR"]);
377
378 let input = line_entry(1, 1, r#"FOO="$BAR"#);
379 assert!(input.get_substitution_keys().is_empty());
380
381 let input = line_entry(1, 1, r#"FOO="$BAR\""#);
382 assert!(input.get_substitution_keys().is_empty());
383
384 let input = line_entry(1, 1, r#"FOO="\""#);
385 assert!(input.get_substitution_keys().is_empty());
386
387 let input = line_entry(1, 1, r#"FOO="${BAR}\\""#);
388 assert_eq!(input.get_substitution_keys(), vec!["BAR"]);
389 }
390
391 #[test]
392 fn run_with_single_quotes() {
393 let input = line_entry(1, 1, "FOO='$BAR'");
394 assert!(input.get_substitution_keys().is_empty());
395
396 let input = line_entry(1, 1, r"FOO=TEST_${BAR}_\'");
397 assert_eq!(input.get_substitution_keys(), vec!["BAR"]);
398 }
399
400 #[test]
401 fn run_with_escaped_dollar() {
402 let input = line_entry(1, 1, r"FOO=\$BAR");
403 assert!(input.get_substitution_keys().is_empty());
404
405 let input = line_entry(1, 1, r"FOO=\\$BAR");
406 assert_eq!(input.get_substitution_keys(), vec!["BAR"]);
407
408 let input = line_entry(1, 1, r"FOO=\\\$BAR");
409 assert!(input.get_substitution_keys().is_empty());
410 }
411
412 #[test]
413 fn run_with_complicated() {
414 let input = line_entry(1, 1, "DATABASE=postgres://${USER}@localhost/database");
415 assert_eq!(input.get_substitution_keys(), vec!["USER"]);
416 }
417
418 #[test]
419 fn run_with_reused() {
420 let input = line_entry(1, 1, "FOO=$BAR$BAR");
421 assert_eq!(input.get_substitution_keys(), vec!["BAR", "BAR"]);
422
423 let input = line_entry(1, 1, "FOO=${BAR}${BAR}");
424 assert_eq!(input.get_substitution_keys(), vec!["BAR", "BAR"]);
425
426 let input = line_entry(1, 1, "FOO=${BAR}${BAZ}");
427 assert_eq!(input.get_substitution_keys(), vec!["BAR", "BAZ"]);
428 }
429
430 #[test]
431 fn run_with_break() {
432 let input = line_entry(1, 1, "FOO=${BAR $BAZ");
433 assert!(input.get_substitution_keys().is_empty());
434 }
435 }
436}