/ src / parser.rs
parser.rs
  1  use crate::error::Error;
  2  use crate::types::{Entry, KeyValuePair, ParseOptions, QuoteType, Span};
  3  use std::borrow::Cow;
  4  
  5  struct ParsedValue<'a> {
  6      value: Cow<'a, str>,
  7      value_start: usize,
  8      raw_len: usize,
  9      quote: QuoteType,
 10  }
 11  
 12  struct CommentScanState {
 13      line_start: usize,
 14      line_end: usize,
 15      scan_pos: usize,
 16      returned_comment_entry: bool,
 17  }
 18  
 19  pub struct Parser<'a> {
 20      input: &'a str,
 21      bytes: &'a [u8],
 22      cursor: usize,
 23      options: ParseOptions,
 24      bom_checked: bool,
 25      comment_state: Option<CommentScanState>,
 26  }
 27  
 28  impl<'a> Parser<'a> {
 29      #[inline(always)]
 30      pub fn new(input: &'a str) -> Self {
 31          Self::with_options(input, ParseOptions::default())
 32      }
 33  
 34      #[inline(always)]
 35      pub fn with_options(input: &'a str, options: ParseOptions) -> Self {
 36          Self {
 37              input,
 38              bytes: input.as_bytes(),
 39              cursor: 0,
 40              options,
 41              bom_checked: false,
 42              comment_state: None,
 43          }
 44      }
 45  
 46      pub fn parse(&mut self) -> Vec<Entry<'a>> {
 47          let mut entries = Vec::with_capacity(32);
 48          while let Some(entry) = self.next_entry() {
 49              entries.push(entry);
 50          }
 51          entries
 52      }
 53  
 54      pub fn iter(self) -> EnvIterator<'a> {
 55          EnvIterator { parser: self }
 56      }
 57  
 58      #[inline]
 59      pub fn next_entry(&mut self) -> Option<Entry<'a>> {
 60          if let Some(state) = self.comment_state.as_mut() {
 61              if !state.returned_comment_entry {
 62                  state.returned_comment_entry = true;
 63                  return Some(Entry::Comment(Span::from_offsets(
 64                      state.line_start - 1,
 65                      state.line_end,
 66                  )));
 67              }
 68  
 69              let line_start = state.line_start;
 70              let line_end = state.line_end;
 71              let mut scan_pos = state.scan_pos;
 72  
 73              // Scan for next pair
 74              let line_bytes = &self.bytes[line_start..line_end];
 75  
 76              while scan_pos < line_bytes.len() {
 77                  if line_bytes[scan_pos] == b'=' {
 78                      let eq_pos = line_start + scan_pos;
 79                      scan_pos += 1;
 80  
 81                      // Fast backwards key scan with unsafe for speed
 82                      if let Some((key_start, key_end)) =
 83                          unsafe { self.find_key_backwards_fast(eq_pos) }
 84                      {
 85                          if let Some(parsed) = self.parse_value_in_comment_fast(eq_pos + 1, line_end)
 86                          {
 87                              let key_str = &self.input[key_start..key_end];
 88  
 89                              // Check for export prefix in comment
 90                              let is_exported =
 91                                  unsafe { self.has_export_prefix(key_start, line_start) };
 92  
 93                              // Update state for next call
 94                              if let Some(s) = self.comment_state.as_mut() {
 95                                  s.scan_pos = parsed.value_start + parsed.raw_len - line_start;
 96                              }
 97  
 98                              let pair = if self.options.track_positions {
 99                                  KeyValuePair::new(
100                                      key_str,
101                                      key_start,
102                                      parsed.value,
103                                      parsed.value_start,
104                                      parsed.raw_len,
105                                      parsed.quote,
106                                      is_exported,
107                                      None,
108                                      true,
109                                  )
110                              } else {
111                                  KeyValuePair::new_fast(
112                                      key_str,
113                                      parsed.value,
114                                      parsed.quote,
115                                      is_exported,
116                                      true,
117                                  )
118                              };
119                              return Some(Entry::Pair(Box::new(pair)));
120                          }
121                      }
122  
123                      // Failed to parse, update position and continue
124                      if let Some(s) = self.comment_state.as_mut() {
125                          s.scan_pos = scan_pos;
126                      }
127                  } else {
128                      scan_pos += 1;
129                  }
130              }
131  
132              // Done scanning - no more pairs found
133              self.cursor = line_end;
134              if self.cursor < self.bytes.len() && self.bytes[self.cursor] == b'\n' {
135                  self.cursor += 1;
136              }
137              self.comment_state = None;
138  
139              return self.next_entry();
140          }
141  
142          if !self.bom_checked {
143              if let Some(err) = self.check_bom() {
144                  return Some(err);
145              }
146          }
147  
148          loop {
149              if self.cursor >= self.bytes.len() {
150                  return None;
151              }
152  
153              // Fast whitespace skip
154              while self.cursor < self.bytes.len() {
155                  let b = unsafe { *self.bytes.get_unchecked(self.cursor) };
156                  if b != b' ' && b != b'\t' {
157                      break;
158                  }
159                  self.cursor += 1;
160              }
161  
162              if self.cursor >= self.bytes.len() {
163                  return None;
164              }
165  
166              match unsafe { *self.bytes.get_unchecked(self.cursor) } {
167                  b'\n' => {
168                      self.cursor += 1;
169                      continue;
170                  }
171                  b'#' => return self.handle_comment_fast(),
172                  _ => {
173                      if let Some(entry) = self.parse_pair() {
174                          return Some(entry);
175                      }
176                  }
177              }
178          }
179      }
180  }
181  
182  impl<'a> Parser<'a> {
183      #[inline]
184      fn check_bom(&mut self) -> Option<Entry<'a>> {
185          self.bom_checked = true;
186          if self.bytes.starts_with(b"\xEF\xBB\xBF") {
187              self.cursor += 3;
188          }
189          if let Some(idx) = self.input[self.cursor..].find('\u{FEFF}') {
190              return Some(Entry::Error(Error::InvalidBom {
191                  offset: self.cursor + idx,
192              }));
193          }
194          None
195      }
196  
197      #[inline]
198      fn handle_comment_fast(&mut self) -> Option<Entry<'a>> {
199          self.cursor += 1;
200  
201          let line_start = self.cursor;
202  
203          // Fast newline search
204          let mut line_end = line_start;
205          while line_end < self.bytes.len() {
206              let b = unsafe { *self.bytes.get_unchecked(line_end) };
207              if b == b'\n' || b == b'\r' {
208                  break;
209              }
210              line_end += 1;
211          }
212  
213          if self.options.include_comments {
214              // Set up state for scanning - will return Comment entry first, then pairs
215              self.comment_state = Some(CommentScanState {
216                  line_start,
217                  line_end,
218                  scan_pos: 0,
219                  returned_comment_entry: false,
220              });
221  
222              return self.next_entry();
223          } else {
224              // Just return the Comment entry and skip the line
225              self.cursor = line_end;
226              if self.cursor < self.bytes.len() && self.bytes[self.cursor] == b'\n' {
227                  self.cursor += 1;
228              }
229              return Some(Entry::Comment(Span::from_offsets(
230                  line_start - 1, // Include '#'
231                  line_end,
232              )));
233          }
234      }
235  
236      #[inline(always)]
237      unsafe fn find_key_backwards_fast(&self, eq_pos: usize) -> Option<(usize, usize)> {
238          if eq_pos == 0 {
239              return None;
240          }
241  
242          let mut key_end = eq_pos;
243  
244          // Skip whitespace (invalid but check)
245          while key_end > 0 {
246              let b = *self.bytes.get_unchecked(key_end - 1);
247              if b != b' ' && b != b'\t' {
248                  break;
249              }
250              key_end -= 1;
251          }
252  
253          if key_end == 0 {
254              return None;
255          }
256  
257          let mut key_start = key_end;
258  
259          // Fast backwards scan
260          while key_start > 0 {
261              let b = *self.bytes.get_unchecked(key_start - 1);
262              if b.is_ascii_alphanumeric() || b == b'_' {
263                  key_start -= 1;
264              } else {
265                  break;
266              }
267          }
268  
269          if key_start == key_end {
270              return None;
271          }
272  
273          // Validate
274          let first_byte = *self.bytes.get_unchecked(key_start);
275          if first_byte.is_ascii_digit() {
276              return None;
277          }
278  
279          if key_start > 0 {
280              let prev = *self.bytes.get_unchecked(key_start - 1);
281              if prev != b' ' && prev != b'\t' && prev != b'#' {
282                  return None;
283              }
284          }
285  
286          Some((key_start, key_end))
287      }
288  
289      #[inline(always)]
290      unsafe fn has_export_prefix(&self, key_start: usize, line_start: usize) -> bool {
291          // Need at least 7 bytes for "export " before key_start
292          if key_start < line_start + 7 {
293              return false;
294          }
295  
296          let mut check_pos = key_start;
297  
298          // Skip back over any whitespace immediately before the key
299          while check_pos > line_start {
300              let b = *self.bytes.get_unchecked(check_pos - 1);
301              if b != b' ' && b != b'\t' {
302                  break;
303              }
304              check_pos -= 1;
305          }
306  
307          // Need at least 6 bytes for "export"
308          if check_pos < line_start + 6 {
309              return false;
310          }
311  
312          // Check if preceded by "export"
313          let export_end = check_pos;
314          let export_start = export_end - 6;
315  
316          if export_start < line_start {
317              return false;
318          }
319  
320          // Fast memcmp for "export"
321          if &self.bytes[export_start..export_end] != b"export" {
322              return false;
323          }
324  
325          // Verify there's whitespace or start of line before "export"
326          if export_start > line_start {
327              let before = *self.bytes.get_unchecked(export_start - 1);
328              if before != b' ' && before != b'\t' {
329                  return false;
330              }
331          }
332  
333          true
334      }
335  
336      #[inline]
337      fn parse_value_in_comment_fast(
338          &self,
339          value_start: usize,
340          line_end: usize,
341      ) -> Option<ParsedValue<'a>> {
342          if value_start >= line_end {
343              return Some(ParsedValue {
344                  value: Cow::Borrowed(""),
345                  value_start,
346                  raw_len: 0,
347                  quote: QuoteType::None,
348              });
349          }
350  
351          let first_byte = unsafe { *self.bytes.get_unchecked(value_start) };
352  
353          // Single-quoted - fast path
354          if first_byte == b'\'' {
355              let content_start = value_start + 1;
356              let mut pos = content_start;
357              while pos < line_end {
358                  if unsafe { *self.bytes.get_unchecked(pos) } == b'\'' {
359                      return Some(ParsedValue {
360                          value: Cow::Borrowed(&self.input[content_start..pos]),
361                          value_start,
362                          raw_len: pos + 1 - value_start,
363                          quote: QuoteType::Single,
364                      });
365                  }
366                  pos += 1;
367              }
368              return None;
369          }
370  
371          // Double-quoted
372          if first_byte == b'"' {
373              let content_start = value_start + 1;
374              let mut pos = content_start;
375  
376              // Fast scan for quote or escape
377              while pos < line_end {
378                  let b = unsafe { *self.bytes.get_unchecked(pos) };
379                  if b == b'"' {
380                      return Some(ParsedValue {
381                          value: Cow::Borrowed(&self.input[content_start..pos]),
382                          value_start,
383                          raw_len: pos + 1 - value_start,
384                          quote: QuoteType::Double,
385                      });
386                  }
387                  if b == b'\\' {
388                      // Has escapes - fallback to slow path
389                      return self.parse_double_quoted_with_escapes(value_start, line_end);
390                  }
391                  pos += 1;
392              }
393              return None;
394          }
395  
396          // Unquoted - stop at whitespace
397          let mut pos = value_start;
398          while pos < line_end {
399              let b = unsafe { *self.bytes.get_unchecked(pos) };
400              if b == b' ' || b == b'\t' {
401                  break;
402              }
403              pos += 1;
404          }
405  
406          Some(ParsedValue {
407              value: Cow::Borrowed(&self.input[value_start..pos]),
408              value_start,
409              raw_len: pos - value_start,
410              quote: QuoteType::None,
411          })
412      }
413  
414      #[cold]
415      fn parse_double_quoted_with_escapes(
416          &self,
417          start: usize,
418          line_end: usize,
419      ) -> Option<ParsedValue<'a>> {
420          let mut pos = start + 1;
421          let mut value = String::new();
422  
423          while pos < line_end {
424              let b = unsafe { *self.bytes.get_unchecked(pos) };
425              if b == b'\\' && pos + 1 < line_end {
426                  pos += 1;
427                  let c = unsafe { *self.bytes.get_unchecked(pos) };
428                  match c {
429                      b'n' => value.push('\n'),
430                      b'r' => value.push('\r'),
431                      b't' => value.push('\t'),
432                      b'\\' => value.push('\\'),
433                      b'"' => value.push('"'),
434                      b'$' => value.push('$'),
435                      _ => {
436                          value.push('\\');
437                          value.push(c as char);
438                      }
439                  }
440                  pos += 1;
441              } else if b == b'"' {
442                  return Some(ParsedValue {
443                      value: Cow::Owned(value),
444                      value_start: start,
445                      raw_len: pos + 1 - start,
446                      quote: QuoteType::Double,
447                  });
448              } else {
449                  value.push(b as char);
450                  pos += 1;
451              }
452          }
453          None
454      }
455  
456      fn parse_pair(&mut self) -> Option<Entry<'a>> {
457          let export_span = self.consume_export_keyword();
458          let is_exported = export_span.is_some();
459  
460          let key_start = self.cursor;
461  
462          // Fast key scan
463          while self.cursor < self.bytes.len() {
464              let b = unsafe { *self.bytes.get_unchecked(self.cursor) };
465              if !b.is_ascii_alphanumeric() && b != b'_' {
466                  break;
467              }
468              self.cursor += 1;
469          }
470          let key_end = self.cursor;
471  
472          if key_start == key_end {
473              // Fast whitespace skip
474              while self.cursor < self.bytes.len() {
475                  let b = unsafe { *self.bytes.get_unchecked(self.cursor) };
476                  if b != b' ' && b != b'\t' {
477                      break;
478                  }
479                  self.cursor += 1;
480              }
481  
482              if self.cursor < self.bytes.len() && self.bytes[self.cursor] == b'=' {
483                  return Some(self.error_and_recover(Error::Generic {
484                      offset: key_start,
485                      message: "Empty key".into(),
486                  }));
487              }
488  
489              if is_exported {
490                  self.skip_to_newline();
491                  if self.cursor < self.bytes.len() && self.bytes[self.cursor] == b'\n' {
492                      self.cursor += 1;
493                  }
494                  return None;
495              }
496  
497              self.skip_to_newline();
498              if self.cursor < self.bytes.len() && self.bytes[self.cursor] == b'\n' {
499                  self.cursor += 1;
500              }
501              return None;
502          }
503  
504          let key_str = &self.input[key_start..key_end];
505  
506          if unsafe { *self.bytes.get_unchecked(key_start) }.is_ascii_digit() {
507              return Some(self.error_and_recover(Error::InvalidKey {
508                  offset: key_start,
509                  reason: "Key starts with digit".into(),
510              }));
511          }
512  
513          // Check whitespace before '='
514          if self.cursor < self.bytes.len() && matches!(self.bytes[self.cursor], b' ' | b'\t') {
515              let ws_start = self.cursor;
516              while self.cursor < self.bytes.len() {
517                  let b = unsafe { *self.bytes.get_unchecked(self.cursor) };
518                  if b != b' ' && b != b'\t' {
519                      break;
520                  }
521                  self.cursor += 1;
522              }
523  
524              if self.cursor < self.bytes.len() && self.bytes[self.cursor] == b'=' {
525                  return Some(self.error_and_recover(Error::ForbiddenWhitespace {
526                      offset: ws_start,
527                      location: "between key and equals",
528                  }));
529              }
530          }
531  
532          if self.cursor >= self.bytes.len() || self.bytes[self.cursor] != b'=' {
533              return Some(self.error_and_recover(Error::Expected {
534                  offset: self.cursor,
535                  expected: "'='",
536              }));
537          }
538          self.cursor += 1;
539  
540          if self.cursor < self.bytes.len() && self.bytes[self.cursor] == b'=' {
541              return Some(self.error_and_recover(Error::DoubleEquals {
542                  offset: self.cursor,
543              }));
544          }
545  
546          if self.cursor < self.bytes.len() && matches!(self.bytes[self.cursor], b' ' | b'\t') {
547              return Some(self.error_and_recover(Error::ForbiddenWhitespace {
548                  offset: self.cursor,
549                  location: "after equals",
550              }));
551          }
552  
553          let value_start = self.cursor;
554          // Fast path for the common case: unquoted value ending at '\n' or EOF
555          // with no continuation backslash.
556          if self.cursor < self.bytes.len() {
557              let first = unsafe { *self.bytes.get_unchecked(self.cursor) };
558              if first != b'\'' && first != b'"' {
559                  let mut end = self.cursor;
560                  while end < self.bytes.len() {
561                      let b = unsafe { *self.bytes.get_unchecked(end) };
562                      if matches!(b, b' ' | b'\t' | b'\n' | b'\r') {
563                          break;
564                      }
565                      end += 1;
566                  }
567  
568                  let ends_line_cleanly =
569                      end >= self.bytes.len() || unsafe { *self.bytes.get_unchecked(end) } == b'\n';
570                  let has_continuation_backslash =
571                      end > value_start && unsafe { *self.bytes.get_unchecked(end - 1) } == b'\\';
572  
573                  if ends_line_cleanly && !has_continuation_backslash {
574                      self.cursor = end;
575                      let value = Cow::Borrowed(&self.input[value_start..end]);
576                      let pair = if self.options.track_positions {
577                          KeyValuePair::new(
578                              key_str,
579                              key_start,
580                              value,
581                              value_start,
582                              end - value_start,
583                              QuoteType::None,
584                              is_exported,
585                              export_span,
586                              false,
587                          )
588                      } else {
589                          KeyValuePair::new_fast(key_str, value, QuoteType::None, is_exported, false)
590                      };
591                      if self.cursor < self.bytes.len()
592                          && unsafe { *self.bytes.get_unchecked(self.cursor) } == b'\n'
593                      {
594                          self.cursor += 1;
595                      }
596                      return Some(Entry::Pair(Box::new(pair)));
597                  }
598              }
599          }
600  
601          let parsed_value = if self.cursor < self.bytes.len() && self.bytes[self.cursor] == b'\'' {
602              self.parse_single_quoted_value(value_start)
603          } else if self.cursor < self.bytes.len() && self.bytes[self.cursor] == b'"' {
604              self.parse_double_quoted_value(value_start)
605          } else {
606              self.parse_unquoted_value(value_start)
607          };
608  
609          let entry = match parsed_value {
610              Ok(pv) => {
611                  let pair = if self.options.track_positions {
612                      KeyValuePair::new(
613                          key_str,
614                          key_start,
615                          pv.value,
616                          pv.value_start,
617                          pv.raw_len,
618                          pv.quote,
619                          is_exported,
620                          export_span,
621                          false,
622                      )
623                  } else {
624                      KeyValuePair::new_fast(key_str, pv.value, pv.quote, is_exported, false)
625                  };
626                  Entry::Pair(Box::new(pair))
627              }
628              Err(e) => Entry::Error(e),
629          };
630  
631          // Check inline comment.
632          let mut scan_pos = self.cursor;
633          while scan_pos < self.bytes.len() {
634              let b = unsafe { *self.bytes.get_unchecked(scan_pos) };
635              if b != b' ' && b != b'\t' {
636                  break;
637              }
638              scan_pos += 1;
639          }
640          self.cursor = scan_pos;
641          let has_comment = self.cursor < self.bytes.len() && self.bytes[self.cursor] == b'#';
642  
643          if !has_comment {
644              self.skip_to_newline();
645          }
646  
647          if self.cursor < self.bytes.len() && self.bytes[self.cursor] == b'\n' {
648              self.cursor += 1;
649          }
650          Some(entry)
651      }
652  }
653  
654  impl<'a> Parser<'a> {
655      #[inline]
656      fn parse_single_quoted_value(&mut self, start: usize) -> Result<ParsedValue<'a>, Error> {
657          self.cursor += 1;
658          let content_start = self.cursor;
659  
660          while self.cursor < self.bytes.len() {
661              if unsafe { *self.bytes.get_unchecked(self.cursor) } == b'\'' {
662                  let content_end = self.cursor;
663                  self.cursor += 1;
664                  return Ok(ParsedValue {
665                      value: Cow::Borrowed(&self.input[content_start..content_end]),
666                      value_start: start,
667                      raw_len: self.cursor - start,
668                      quote: QuoteType::Single,
669                  });
670              }
671              self.cursor += 1;
672          }
673  
674          self.cursor = self.bytes.len();
675          Err(Error::UnclosedQuote {
676              offset: start,
677              quote_type: "single",
678          })
679      }
680  
681      #[inline]
682      fn parse_double_quoted_value(&mut self, start: usize) -> Result<ParsedValue<'a>, Error> {
683          self.cursor += 1;
684          let content_start = self.cursor;
685  
686          // Fast path: scan for quote or escape
687          while self.cursor < self.bytes.len() {
688              let b = unsafe { *self.bytes.get_unchecked(self.cursor) };
689              if b == b'"' {
690                  let content_end = self.cursor;
691                  self.cursor += 1;
692                  return Ok(ParsedValue {
693                      value: Cow::Borrowed(&self.input[content_start..content_end]),
694                      value_start: start,
695                      raw_len: self.cursor - start,
696                      quote: QuoteType::Double,
697                  });
698              }
699              if b == b'\\' {
700                  break;
701              }
702              self.cursor += 1;
703          }
704  
705          // Slow path: has escapes
706          self.cursor = content_start;
707          let mut value = String::with_capacity(64);
708  
709          loop {
710              if self.cursor >= self.bytes.len() {
711                  return Err(Error::UnclosedQuote {
712                      offset: start,
713                      quote_type: "double",
714                  });
715              }
716              let b = unsafe { *self.bytes.get_unchecked(self.cursor) };
717              if b == b'\\' && self.cursor + 1 < self.bytes.len() {
718                  self.cursor += 1;
719                  let c = unsafe { *self.bytes.get_unchecked(self.cursor) };
720                  match c {
721                      b'n' => value.push('\n'),
722                      b'r' => value.push('\r'),
723                      b't' => value.push('\t'),
724                      b'\\' => value.push('\\'),
725                      b'"' => value.push('"'),
726                      b'$' => value.push('$'),
727                      _ => {
728                          value.push('\\');
729                          value.push(c as char);
730                      }
731                  }
732                  self.cursor += 1;
733              } else if b == b'"' {
734                  self.cursor += 1;
735                  return Ok(ParsedValue {
736                      value: Cow::Owned(value),
737                      value_start: start,
738                      raw_len: self.cursor - start,
739                      quote: QuoteType::Double,
740                  });
741              } else {
742                  value.push(b as char);
743                  self.cursor += 1;
744              }
745          }
746      }
747  
748      #[inline]
749      fn parse_unquoted_value(&mut self, start: usize) -> Result<ParsedValue<'a>, Error> {
750          let start_pos = self.cursor;
751          let mut limit = self.cursor;
752          while limit < self.bytes.len() {
753              let b = unsafe { *self.bytes.get_unchecked(limit) };
754              if matches!(b, b' ' | b'\t' | b'\n' | b'\r') {
755                  break;
756              }
757              limit += 1;
758          }
759          self.cursor = limit;
760  
761          let stopped_at_eol = limit >= self.bytes.len()
762              || matches!(unsafe { *self.bytes.get_unchecked(limit) }, b'\n' | b'\r');
763          let has_trailing_backslash =
764              limit > start_pos && unsafe { *self.bytes.get_unchecked(limit - 1) } == b'\\';
765  
766          if !stopped_at_eol || !has_trailing_backslash {
767              return Ok(ParsedValue {
768                  value: Cow::Borrowed(&self.input[start_pos..self.cursor]),
769                  value_start: start,
770                  raw_len: self.cursor - start,
771                  quote: QuoteType::None,
772              });
773          }
774  
775          self.parse_unquoted_value_with_continuation(start, start_pos, limit)
776      }
777  
778      #[cold]
779      fn parse_unquoted_value_with_continuation(
780          &mut self,
781          start: usize,
782          start_pos: usize,
783          first_limit: usize,
784      ) -> Result<ParsedValue<'a>, Error> {
785          let first_chunk = &self.input[start_pos..first_limit];
786          let mut value = String::with_capacity(first_chunk.len());
787          value.push_str(&first_chunk[..first_chunk.len() - 1]);
788  
789          self.cursor = first_limit;
790          if self.cursor < self.bytes.len() {
791              if unsafe { *self.bytes.get_unchecked(self.cursor) } == b'\r' {
792                  self.cursor += 1;
793              }
794              if self.cursor < self.bytes.len()
795                  && unsafe { *self.bytes.get_unchecked(self.cursor) } == b'\n'
796              {
797                  self.cursor += 1;
798              }
799          } else {
800              value.push('\\');
801              return Ok(ParsedValue {
802                  value: Cow::Owned(value),
803                  value_start: start,
804                  raw_len: self.cursor - start,
805                  quote: QuoteType::None,
806              });
807          }
808  
809          loop {
810              if self.cursor >= self.bytes.len() {
811                  break;
812              }
813  
814              let line_start = self.cursor;
815              while self.cursor < self.bytes.len() {
816                  let b = unsafe { *self.bytes.get_unchecked(self.cursor) };
817                  if matches!(b, b' ' | b'\t' | b'\n' | b'\r') {
818                      break;
819                  }
820                  self.cursor += 1;
821              }
822  
823              let limit = self.cursor;
824              let stopped_at_eol = limit >= self.bytes.len()
825                  || matches!(unsafe { *self.bytes.get_unchecked(limit) }, b'\n' | b'\r');
826              let is_continuation = stopped_at_eol
827                  && limit > line_start
828                  && unsafe { *self.bytes.get_unchecked(limit - 1) } == b'\\';
829  
830              if is_continuation {
831                  let chunk = &self.input[line_start..limit];
832                  value.push_str(&chunk[..chunk.len() - 1]);
833  
834                  if self.cursor < self.bytes.len() {
835                      if unsafe { *self.bytes.get_unchecked(self.cursor) } == b'\r' {
836                          self.cursor += 1;
837                      }
838                      if self.cursor < self.bytes.len()
839                          && unsafe { *self.bytes.get_unchecked(self.cursor) } == b'\n'
840                      {
841                          self.cursor += 1;
842                      }
843                  } else {
844                      value.push('\\');
845                      break;
846                  }
847              } else {
848                  value.push_str(&self.input[line_start..limit]);
849                  break;
850              }
851          }
852  
853          Ok(ParsedValue {
854              value: Cow::Owned(value),
855              value_start: start,
856              raw_len: self.cursor - start,
857              quote: QuoteType::None,
858          })
859      }
860  }
861  
862  impl<'a> Parser<'a> {
863      #[inline]
864      fn consume_export_keyword(&mut self) -> Option<Span> {
865          if self.cursor + 6 < self.bytes.len()
866              && unsafe { *self.bytes.get_unchecked(self.cursor..self.cursor + 6) == *b"export" }
867          {
868              let next = unsafe { *self.bytes.get_unchecked(self.cursor + 6) };
869              if matches!(next, b' ' | b'\t') {
870                  let start_pos = self.cursor;
871                  self.cursor += 6;
872                  let end_pos = self.cursor;
873                  while self.cursor < self.bytes.len() {
874                      let b = unsafe { *self.bytes.get_unchecked(self.cursor) };
875                      if b != b' ' && b != b'\t' {
876                          break;
877                      }
878                      self.cursor += 1;
879                  }
880                  return Some(Span::from_offsets(start_pos, end_pos));
881              }
882          }
883          None
884      }
885  
886      #[inline]
887      fn skip_to_newline(&mut self) {
888          while self.cursor < self.bytes.len() {
889              if unsafe { *self.bytes.get_unchecked(self.cursor) } == b'\n' {
890                  break;
891              }
892              self.cursor += 1;
893          }
894      }
895  
896      fn error_and_recover(&mut self, err: Error) -> Entry<'a> {
897          self.skip_to_newline();
898          if self.cursor < self.bytes.len() {
899              self.cursor += 1;
900          }
901          Entry::Error(err)
902      }
903  }
904  
905  pub struct EnvIterator<'a> {
906      parser: Parser<'a>,
907  }
908  
909  impl<'a> Iterator for EnvIterator<'a> {
910      type Item = Entry<'a>;
911      fn next(&mut self) -> Option<Self::Item> {
912          self.parser.next_entry()
913      }
914  }