1#![cfg(any(feature = "linebased", feature = "deb822"))]
2#[derive(Debug)]
8pub enum ParseError {
9 #[cfg(feature = "linebased")]
11 LineBased(crate::linebased::ParseError),
12 #[cfg(feature = "deb822")]
14 Deb822(crate::deb822::ParseError),
15 UnknownVersion,
17 FeatureNotEnabled(String),
19}
20
21impl std::fmt::Display for ParseError {
22 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
23 match self {
24 #[cfg(feature = "linebased")]
25 ParseError::LineBased(e) => write!(f, "{}", e),
26 #[cfg(feature = "deb822")]
27 ParseError::Deb822(e) => write!(f, "{}", e),
28 ParseError::UnknownVersion => write!(f, "Could not detect watch file version"),
29 ParseError::FeatureNotEnabled(msg) => write!(f, "{}", msg),
30 }
31 }
32}
33
34impl std::error::Error for ParseError {}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum WatchFileVersion {
39 LineBased(u32),
41 Deb822,
43}
44
45pub fn detect_version(content: &str) -> Option<WatchFileVersion> {
66 let trimmed = content.trim_start();
67
68 if trimmed.starts_with("Version:") || trimmed.starts_with("version:") {
70 if let Some(first_line) = trimmed.lines().next() {
72 if let Some(colon_pos) = first_line.find(':') {
73 let version_str = first_line[colon_pos + 1..].trim();
74 if version_str == "5" {
75 return Some(WatchFileVersion::Deb822);
76 }
77 }
78 }
79 }
80
81 for line in trimmed.lines() {
84 let line = line.trim();
85
86 if line.starts_with('#') || line.is_empty() {
88 continue;
89 }
90
91 if line.starts_with("version=") || line.starts_with("version =") {
93 let version_part = if line.starts_with("version=") {
94 &line[8..]
95 } else {
96 &line[9..]
97 };
98
99 if let Ok(version) = version_part.trim().parse::<u32>() {
100 return Some(WatchFileVersion::LineBased(version));
101 }
102 }
103
104 break;
106 }
107
108 Some(WatchFileVersion::LineBased(crate::DEFAULT_VERSION))
110}
111
112#[derive(Debug)]
114pub enum ParsedWatchFile {
115 #[cfg(feature = "linebased")]
117 LineBased(crate::linebased::WatchFile),
118 #[cfg(feature = "deb822")]
120 Deb822(crate::deb822::WatchFile),
121}
122
123#[derive(Debug)]
125pub enum ParsedEntry {
126 #[cfg(feature = "linebased")]
128 LineBased(crate::linebased::Entry),
129 #[cfg(feature = "deb822")]
131 Deb822(crate::deb822::Entry),
132}
133
134impl ParsedWatchFile {
135 pub fn new(version: u32) -> Result<Self, ParseError> {
152 match version {
153 #[cfg(feature = "deb822")]
154 5 => Ok(ParsedWatchFile::Deb822(crate::deb822::WatchFile::new())),
155 #[cfg(not(feature = "deb822"))]
156 5 => Err(ParseError::FeatureNotEnabled(
157 "deb822 feature required for v5 format".to_string(),
158 )),
159 #[cfg(feature = "linebased")]
160 v @ 1..=4 => Ok(ParsedWatchFile::LineBased(
161 crate::linebased::WatchFile::new(Some(v)),
162 )),
163 #[cfg(not(feature = "linebased"))]
164 v @ 1..=4 => Err(ParseError::FeatureNotEnabled(format!(
165 "linebased feature required for v{} format",
166 v
167 ))),
168 v => Err(ParseError::FeatureNotEnabled(format!(
169 "unsupported watch file version: {}",
170 v
171 ))),
172 }
173 }
174
175 pub fn version(&self) -> u32 {
177 match self {
178 #[cfg(feature = "linebased")]
179 ParsedWatchFile::LineBased(wf) => wf.version(),
180 #[cfg(feature = "deb822")]
181 ParsedWatchFile::Deb822(wf) => wf.version(),
182 }
183 }
184
185 pub fn entries(&self) -> impl Iterator<Item = ParsedEntry> + '_ {
187 let entries: Vec<_> = match self {
189 #[cfg(feature = "linebased")]
190 ParsedWatchFile::LineBased(wf) => wf.entries().map(ParsedEntry::LineBased).collect(),
191 #[cfg(feature = "deb822")]
192 ParsedWatchFile::Deb822(wf) => wf.entries().map(ParsedEntry::Deb822).collect(),
193 };
194 entries.into_iter()
195 }
196
197 pub fn add_entry(&mut self, source: &str, matching_pattern: &str) -> ParsedEntry {
218 match self {
219 #[cfg(feature = "linebased")]
220 ParsedWatchFile::LineBased(wf) => {
221 let entry = crate::linebased::EntryBuilder::new(source)
222 .matching_pattern(matching_pattern)
223 .build();
224 let added_entry = wf.add_entry(entry);
225 ParsedEntry::LineBased(added_entry)
226 }
227 #[cfg(feature = "deb822")]
228 ParsedWatchFile::Deb822(wf) => {
229 let added_entry = wf.add_entry(source, matching_pattern);
230 ParsedEntry::Deb822(added_entry)
231 }
232 }
233 }
234}
235
236impl ParsedEntry {
237 pub fn url(&self) -> String {
239 match self {
240 #[cfg(feature = "linebased")]
241 ParsedEntry::LineBased(e) => e.url(),
242 #[cfg(feature = "deb822")]
243 ParsedEntry::Deb822(e) => e.source().unwrap_or(None).unwrap_or_default(),
244 }
245 }
246
247 pub fn matching_pattern(&self) -> Option<String> {
249 match self {
250 #[cfg(feature = "linebased")]
251 ParsedEntry::LineBased(e) => e.matching_pattern(),
252 #[cfg(feature = "deb822")]
253 ParsedEntry::Deb822(e) => e.matching_pattern().unwrap_or(None),
254 }
255 }
256
257 pub fn get_option(&self, key: &str) -> Option<String> {
263 match self {
264 #[cfg(feature = "linebased")]
265 ParsedEntry::LineBased(e) => e.get_option(key),
266 #[cfg(feature = "deb822")]
267 ParsedEntry::Deb822(e) => {
268 e.get_field(key).or_else(|| {
270 let mut chars = key.chars();
271 if let Some(first) = chars.next() {
272 let capitalized = first.to_uppercase().chain(chars).collect::<String>();
273 e.get_field(&capitalized)
274 } else {
275 None
276 }
277 })
278 }
279 }
280 }
281
282 pub fn has_option(&self, key: &str) -> bool {
284 self.get_option(key).is_some()
285 }
286
287 pub fn script(&self) -> Option<String> {
289 self.get_option("script")
290 }
291
292 pub fn component(&self) -> Option<String> {
294 self.get_option("component")
295 }
296
297 pub fn format_url(
299 &self,
300 package: impl FnOnce() -> String,
301 component: impl FnOnce() -> String,
302 ) -> Result<url::Url, url::ParseError> {
303 crate::subst::subst(&self.url(), package, component).parse()
304 }
305
306 pub fn user_agent(&self) -> Option<String> {
308 self.get_option("user-agent")
309 }
310
311 pub fn pagemangle(&self) -> Option<String> {
313 self.get_option("pagemangle")
314 }
315
316 pub fn uversionmangle(&self) -> Option<String> {
318 self.get_option("uversionmangle")
319 }
320
321 pub fn downloadurlmangle(&self) -> Option<String> {
323 self.get_option("downloadurlmangle")
324 }
325
326 pub fn pgpsigurlmangle(&self) -> Option<String> {
328 self.get_option("pgpsigurlmangle")
329 }
330
331 pub fn filenamemangle(&self) -> Option<String> {
333 self.get_option("filenamemangle")
334 }
335
336 pub fn oversionmangle(&self) -> Option<String> {
338 self.get_option("oversionmangle")
339 }
340
341 pub fn searchmode(&self) -> crate::types::SearchMode {
343 self.get_option("searchmode")
344 .and_then(|s| s.parse().ok())
345 .unwrap_or_default()
346 }
347
348 pub fn set_option(&mut self, option: crate::types::WatchOption) {
370 match self {
371 #[cfg(feature = "linebased")]
372 ParsedEntry::LineBased(e) => {
373 e.set_option(option);
374 }
375 #[cfg(feature = "deb822")]
376 ParsedEntry::Deb822(e) => {
377 e.set_option(option);
378 }
379 }
380 }
381
382 pub fn set_url(&mut self, url: &str) {
398 match self {
399 #[cfg(feature = "linebased")]
400 ParsedEntry::LineBased(e) => e.set_url(url),
401 #[cfg(feature = "deb822")]
402 ParsedEntry::Deb822(e) => e.set_source(url),
403 }
404 }
405
406 pub fn set_matching_pattern(&mut self, pattern: &str) {
422 match self {
423 #[cfg(feature = "linebased")]
424 ParsedEntry::LineBased(e) => e.set_matching_pattern(pattern),
425 #[cfg(feature = "deb822")]
426 ParsedEntry::Deb822(e) => e.set_matching_pattern(pattern),
427 }
428 }
429
430 pub fn line(&self) -> usize {
450 match self {
451 #[cfg(feature = "linebased")]
452 ParsedEntry::LineBased(e) => e.line(),
453 #[cfg(feature = "deb822")]
454 ParsedEntry::Deb822(e) => e.line(),
455 }
456 }
457
458 pub fn remove_option(&mut self, option: crate::types::WatchOption) {
481 match self {
482 #[cfg(feature = "linebased")]
483 ParsedEntry::LineBased(e) => e.del_opt(option),
484 #[cfg(feature = "deb822")]
485 ParsedEntry::Deb822(e) => e.delete_option(option),
486 }
487 }
488
489 pub fn mode(&self) -> Result<crate::types::Mode, crate::types::ParseError> {
514 match self {
515 #[cfg(feature = "linebased")]
516 ParsedEntry::LineBased(e) => e.try_mode(),
517 #[cfg(feature = "deb822")]
518 ParsedEntry::Deb822(e) => e.mode(),
519 }
520 }
521}
522
523impl std::fmt::Display for ParsedWatchFile {
524 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
525 match self {
526 #[cfg(feature = "linebased")]
527 ParsedWatchFile::LineBased(wf) => write!(f, "{}", wf),
528 #[cfg(feature = "deb822")]
529 ParsedWatchFile::Deb822(wf) => write!(f, "{}", wf),
530 }
531 }
532}
533
534pub fn parse(content: &str) -> Result<ParsedWatchFile, ParseError> {
553 let version = detect_version(content).ok_or(ParseError::UnknownVersion)?;
554
555 match version {
556 #[cfg(feature = "linebased")]
557 WatchFileVersion::LineBased(_v) => {
558 let wf: crate::linebased::WatchFile = content.parse().map_err(ParseError::LineBased)?;
559 Ok(ParsedWatchFile::LineBased(wf))
560 }
561 #[cfg(not(feature = "linebased"))]
562 WatchFileVersion::LineBased(_v) => Err(ParseError::FeatureNotEnabled(
563 "linebased feature required for v1-4 formats".to_string(),
564 )),
565 #[cfg(feature = "deb822")]
566 WatchFileVersion::Deb822 => {
567 let wf: crate::deb822::WatchFile = content.parse().map_err(ParseError::Deb822)?;
568 Ok(ParsedWatchFile::Deb822(wf))
569 }
570 #[cfg(not(feature = "deb822"))]
571 WatchFileVersion::Deb822 => Err(ParseError::FeatureNotEnabled(
572 "deb822 feature required for v5 format".to_string(),
573 )),
574 }
575}
576
577#[cfg(test)]
578mod tests {
579 use super::*;
580
581 #[test]
582 fn test_detect_version_v1_default() {
583 let content = "https://example.com/ .*.tar.gz";
584 assert_eq!(
585 detect_version(content),
586 Some(WatchFileVersion::LineBased(1))
587 );
588 }
589
590 #[test]
591 fn test_detect_version_v4() {
592 let content = "version=4\nhttps://example.com/ .*.tar.gz";
593 assert_eq!(
594 detect_version(content),
595 Some(WatchFileVersion::LineBased(4))
596 );
597 }
598
599 #[test]
600 fn test_detect_version_v4_with_spaces() {
601 let content = "version = 4\nhttps://example.com/ .*.tar.gz";
602 assert_eq!(
603 detect_version(content),
604 Some(WatchFileVersion::LineBased(4))
605 );
606 }
607
608 #[test]
609 fn test_detect_version_v5() {
610 let content = "Version: 5\n\nSource: https://example.com/";
611 assert_eq!(detect_version(content), Some(WatchFileVersion::Deb822));
612 }
613
614 #[test]
615 fn test_detect_version_v5_lowercase() {
616 let content = "version: 5\n\nSource: https://example.com/";
617 assert_eq!(detect_version(content), Some(WatchFileVersion::Deb822));
618 }
619
620 #[test]
621 fn test_detect_version_with_leading_comments() {
622 let content = "# This is a comment\nversion=4\nhttps://example.com/ .*.tar.gz";
623 assert_eq!(
624 detect_version(content),
625 Some(WatchFileVersion::LineBased(4))
626 );
627 }
628
629 #[test]
630 fn test_detect_version_with_leading_whitespace() {
631 let content = " \n version=3\nhttps://example.com/ .*.tar.gz";
632 assert_eq!(
633 detect_version(content),
634 Some(WatchFileVersion::LineBased(3))
635 );
636 }
637
638 #[test]
639 fn test_detect_version_v2() {
640 let content = "version=2\nhttps://example.com/ .*.tar.gz";
641 assert_eq!(
642 detect_version(content),
643 Some(WatchFileVersion::LineBased(2))
644 );
645 }
646
647 #[cfg(feature = "linebased")]
648 #[test]
649 fn test_parse_linebased() {
650 let content = "version=4\nhttps://example.com/ .*.tar.gz";
651 let parsed = parse(content).unwrap();
652 assert_eq!(parsed.version(), 4);
653 }
654
655 #[cfg(feature = "deb822")]
656 #[test]
657 fn test_parse_deb822() {
658 let content = "Version: 5\n\nSource: https://example.com/\nMatching-Pattern: .*.tar.gz";
659 let parsed = parse(content).unwrap();
660 assert_eq!(parsed.version(), 5);
661 }
662
663 #[cfg(all(feature = "linebased", feature = "deb822"))]
664 #[test]
665 fn test_parse_both_formats() {
666 let v4_content = "version=4\nhttps://example.com/ .*.tar.gz";
668 let v4_parsed = parse(v4_content).unwrap();
669 assert_eq!(v4_parsed.version(), 4);
670
671 let v5_content = "Version: 5\n\nSource: https://example.com/\nMatching-Pattern: .*.tar.gz";
673 let v5_parsed = parse(v5_content).unwrap();
674 assert_eq!(v5_parsed.version(), 5);
675 }
676
677 #[cfg(feature = "linebased")]
678 #[test]
679 fn test_parse_roundtrip() {
680 let content = "version=4\n# Comment\nhttps://example.com/ .*.tar.gz";
681 let parsed = parse(content).unwrap();
682 let output = parsed.to_string();
683
684 let reparsed = parse(&output).unwrap();
686 assert_eq!(reparsed.version(), 4);
687 }
688
689 #[cfg(feature = "deb822")]
690 #[test]
691 fn test_parsed_watch_file_new_v5() {
692 let wf = ParsedWatchFile::new(5).unwrap();
693 assert_eq!(wf.version(), 5);
694 assert_eq!(wf.entries().count(), 0);
695 }
696
697 #[cfg(feature = "linebased")]
698 #[test]
699 fn test_parsed_watch_file_new_v4() {
700 let wf = ParsedWatchFile::new(4).unwrap();
701 assert_eq!(wf.version(), 4);
702 assert_eq!(wf.entries().count(), 0);
703 }
704
705 #[cfg(feature = "deb822")]
706 #[test]
707 fn test_parsed_watch_file_add_entry_v5() {
708 let mut wf = ParsedWatchFile::new(5).unwrap();
709 let mut entry = wf.add_entry("https://github.com/foo/bar/tags", r".*/v?([\d.]+)\.tar\.gz");
710
711 assert_eq!(wf.entries().count(), 1);
712 assert_eq!(entry.url(), "https://github.com/foo/bar/tags");
713 assert_eq!(
714 entry.matching_pattern(),
715 Some(r".*/v?([\d.]+)\.tar\.gz".to_string())
716 );
717
718 entry.set_option(crate::types::WatchOption::Component("upstream".to_string()));
720 entry.set_option(crate::types::WatchOption::Compression(
721 crate::types::Compression::Xz,
722 ));
723
724 assert_eq!(entry.get_option("Component"), Some("upstream".to_string()));
725 assert_eq!(entry.get_option("Compression"), Some("xz".to_string()));
726 }
727
728 #[cfg(feature = "linebased")]
729 #[test]
730 fn test_parsed_watch_file_add_entry_v4() {
731 let mut wf = ParsedWatchFile::new(4).unwrap();
732 let entry = wf.add_entry("https://github.com/foo/bar/tags", r".*/v?([\d.]+)\.tar\.gz");
733
734 assert_eq!(wf.entries().count(), 1);
735 assert_eq!(entry.url(), "https://github.com/foo/bar/tags");
736 assert_eq!(
737 entry.matching_pattern(),
738 Some(r".*/v?([\d.]+)\.tar\.gz".to_string())
739 );
740 }
741
742 #[cfg(feature = "deb822")]
743 #[test]
744 fn test_parsed_watch_file_roundtrip_with_add_entry() {
745 let mut wf = ParsedWatchFile::new(5).unwrap();
746 let mut entry = wf.add_entry(
747 "https://github.com/owner/repo/tags",
748 r".*/v?([\d.]+)\.tar\.gz",
749 );
750 entry.set_option(crate::types::WatchOption::Compression(
751 crate::types::Compression::Xz,
752 ));
753
754 let output = wf.to_string();
755
756 let reparsed = parse(&output).unwrap();
758 assert_eq!(reparsed.version(), 5);
759
760 let entries: Vec<_> = reparsed.entries().collect();
761 assert_eq!(entries.len(), 1);
762 assert_eq!(entries[0].url(), "https://github.com/owner/repo/tags");
763 assert_eq!(entries[0].get_option("Compression"), Some("xz".to_string()));
764 }
765
766 #[cfg(feature = "linebased")]
767 #[test]
768 fn test_parsed_entry_set_url_v4() {
769 let mut wf = ParsedWatchFile::new(4).unwrap();
770 let mut entry = wf.add_entry("https://github.com/foo/bar/tags", r".*/v?([\d.]+)\.tar\.gz");
771
772 assert_eq!(entry.url(), "https://github.com/foo/bar/tags");
773
774 entry.set_url("https://github.com/foo/bar/releases");
775 assert_eq!(entry.url(), "https://github.com/foo/bar/releases");
776 }
777
778 #[cfg(feature = "deb822")]
779 #[test]
780 fn test_parsed_entry_set_url_v5() {
781 let mut wf = ParsedWatchFile::new(5).unwrap();
782 let mut entry = wf.add_entry("https://github.com/foo/bar/tags", r".*/v?([\d.]+)\.tar\.gz");
783
784 assert_eq!(entry.url(), "https://github.com/foo/bar/tags");
785
786 entry.set_url("https://github.com/foo/bar/releases");
787 assert_eq!(entry.url(), "https://github.com/foo/bar/releases");
788 }
789
790 #[cfg(feature = "linebased")]
791 #[test]
792 fn test_parsed_entry_set_matching_pattern_v4() {
793 let mut wf = ParsedWatchFile::new(4).unwrap();
794 let mut entry = wf.add_entry("https://github.com/foo/bar/tags", r".*/v?([\d.]+)\.tar\.gz");
795
796 assert_eq!(
797 entry.matching_pattern(),
798 Some(r".*/v?([\d.]+)\.tar\.gz".to_string())
799 );
800
801 entry.set_matching_pattern(r".*/release-([\d.]+)\.tar\.gz");
802 assert_eq!(
803 entry.matching_pattern(),
804 Some(r".*/release-([\d.]+)\.tar\.gz".to_string())
805 );
806 }
807
808 #[cfg(feature = "deb822")]
809 #[test]
810 fn test_parsed_entry_set_matching_pattern_v5() {
811 let mut wf = ParsedWatchFile::new(5).unwrap();
812 let mut entry = wf.add_entry("https://github.com/foo/bar/tags", r".*/v?([\d.]+)\.tar\.gz");
813
814 assert_eq!(
815 entry.matching_pattern(),
816 Some(r".*/v?([\d.]+)\.tar\.gz".to_string())
817 );
818
819 entry.set_matching_pattern(r".*/release-([\d.]+)\.tar\.gz");
820 assert_eq!(
821 entry.matching_pattern(),
822 Some(r".*/release-([\d.]+)\.tar\.gz".to_string())
823 );
824 }
825
826 #[cfg(feature = "linebased")]
827 #[test]
828 fn test_parsed_entry_line_v4() {
829 let content = "version=4\nhttps://example.com/ .*.tar.gz\nhttps://example2.com/ .*.tar.gz";
830 let wf = parse(content).unwrap();
831 let entries: Vec<_> = wf.entries().collect();
832
833 assert_eq!(entries[0].line(), 1); assert_eq!(entries[1].line(), 2); }
836
837 #[cfg(feature = "deb822")]
838 #[test]
839 fn test_parsed_entry_line_v5() {
840 let content = r#"Version: 5
841
842Source: https://example.com/repo1
843Matching-Pattern: .*\.tar\.gz
844
845Source: https://example.com/repo2
846Matching-Pattern: .*\.tar\.xz
847"#;
848 let wf = parse(content).unwrap();
849 let entries: Vec<_> = wf.entries().collect();
850
851 assert_eq!(entries[0].line(), 2); assert_eq!(entries[1].line(), 5); }
854}
855
856#[derive(Clone, PartialEq, Eq)]
862pub struct Parse {
863 inner: ParseInner,
864}
865
866#[derive(Clone, PartialEq, Eq)]
867enum ParseInner {
868 #[cfg(feature = "linebased")]
869 LineBased(crate::linebased::Parse<crate::linebased::WatchFile>),
870 #[cfg(feature = "deb822")]
871 Deb822(String), }
873
874impl Parse {
875 pub fn parse(text: &str) -> Self {
877 let version = detect_version(text);
878
879 let inner = match version {
880 #[cfg(feature = "linebased")]
881 Some(WatchFileVersion::LineBased(_)) => {
882 ParseInner::LineBased(crate::linebased::parse_watch_file(text))
883 }
884 #[cfg(feature = "deb822")]
885 Some(WatchFileVersion::Deb822) => {
886 ParseInner::Deb822(text.to_string())
887 }
888 #[cfg(not(feature = "linebased"))]
889 Some(WatchFileVersion::LineBased(_)) => {
890 #[cfg(feature = "deb822")]
892 { ParseInner::Deb822(text.to_string()) }
893 #[cfg(not(feature = "deb822"))]
894 { panic!("No watch file parsing features enabled") }
895 }
896 #[cfg(not(feature = "deb822"))]
897 Some(WatchFileVersion::Deb822) => {
898 #[cfg(feature = "linebased")]
900 { ParseInner::LineBased(crate::linebased::parse_watch_file(text)) }
901 #[cfg(not(feature = "linebased"))]
902 { panic!("No watch file parsing features enabled") }
903 }
904 None => {
905 #[cfg(feature = "linebased")]
907 { ParseInner::LineBased(crate::linebased::parse_watch_file(text)) }
908 #[cfg(not(feature = "linebased"))]
909 #[cfg(feature = "deb822")]
910 { ParseInner::Deb822(text.to_string()) }
911 #[cfg(not(any(feature = "linebased", feature = "deb822")))]
912 { panic!("No watch file parsing features enabled") }
913 }
914 };
915
916 Parse { inner }
917 }
918
919 pub fn to_watch_file(&self) -> ParsedWatchFile {
921 match &self.inner {
922 #[cfg(feature = "linebased")]
923 ParseInner::LineBased(parse) => {
924 ParsedWatchFile::LineBased(parse.tree())
925 }
926 #[cfg(feature = "deb822")]
927 ParseInner::Deb822(text) => {
928 let wf: crate::deb822::WatchFile = text.parse().unwrap();
929 ParsedWatchFile::Deb822(wf)
930 }
931 }
932 }
933
934 pub fn version(&self) -> u32 {
936 match &self.inner {
937 #[cfg(feature = "linebased")]
938 ParseInner::LineBased(parse) => {
939 parse.tree().version()
940 }
941 #[cfg(feature = "deb822")]
942 ParseInner::Deb822(_) => 5,
943 }
944 }
945}
946
947unsafe impl Send for Parse {}
951unsafe impl Sync for Parse {}