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 }