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, Clone)]
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, Clone)]
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 pub fn version_range(&self) -> Option<rowan::TextRange> {
242 match self {
243 #[cfg(feature = "linebased")]
244 ParsedWatchFile::LineBased(wf) => wf.version_node().map(|v| v.text_range()),
245 #[cfg(feature = "deb822")]
246 ParsedWatchFile::Deb822(wf) => {
247 let first = wf.as_deb822().paragraphs().next()?;
250 first.get_entry("Version").map(|e| e.text_range())
251 }
252 }
253 }
254}
255
256impl ParsedEntry {
257 pub fn url(&self) -> String {
259 match self {
260 #[cfg(feature = "linebased")]
261 ParsedEntry::LineBased(e) => e.url(),
262 #[cfg(feature = "deb822")]
263 ParsedEntry::Deb822(e) => e.source().unwrap_or(None).unwrap_or_default(),
264 }
265 }
266
267 pub fn matching_pattern(&self) -> Option<String> {
269 match self {
270 #[cfg(feature = "linebased")]
271 ParsedEntry::LineBased(e) => e.matching_pattern(),
272 #[cfg(feature = "deb822")]
273 ParsedEntry::Deb822(e) => e.matching_pattern().unwrap_or(None),
274 }
275 }
276
277 pub fn get_option(&self, key: &str) -> Option<String> {
283 match self {
284 #[cfg(feature = "linebased")]
285 ParsedEntry::LineBased(e) => e.get_option(key),
286 #[cfg(feature = "deb822")]
287 ParsedEntry::Deb822(e) => {
288 e.get_field(key).or_else(|| {
290 let mut chars = key.chars();
291 if let Some(first) = chars.next() {
292 let capitalized = first.to_uppercase().chain(chars).collect::<String>();
293 e.get_field(&capitalized)
294 } else {
295 None
296 }
297 })
298 }
299 }
300 }
301
302 pub fn has_option(&self, key: &str) -> bool {
304 self.get_option(key).is_some()
305 }
306
307 pub fn url_range(&self) -> Option<rowan::TextRange> {
314 match self {
315 #[cfg(feature = "linebased")]
316 ParsedEntry::LineBased(e) => e.url_node().map(|n| n.text_range()),
317 #[cfg(feature = "deb822")]
318 ParsedEntry::Deb822(e) => deb822_field_range(e.as_deb822(), &["Source", "URL"]),
319 }
320 }
321
322 pub fn matching_pattern_range(&self) -> Option<rowan::TextRange> {
327 match self {
328 #[cfg(feature = "linebased")]
329 ParsedEntry::LineBased(e) => e.matching_pattern_node().map(|n| n.text_range()),
330 #[cfg(feature = "deb822")]
331 ParsedEntry::Deb822(e) => deb822_field_range(e.as_deb822(), &["Matching-Pattern"]),
332 }
333 }
334
335 pub fn option_range(&self, key: &str) -> Option<rowan::TextRange> {
341 match self {
342 #[cfg(feature = "linebased")]
343 ParsedEntry::LineBased(e) => {
344 let list = e.option_list()?;
345 let opt = list.find_option(key)?;
346 Some(opt.text_range())
347 }
348 #[cfg(feature = "deb822")]
349 ParsedEntry::Deb822(e) => {
350 if let Some(r) = deb822_field_range(e.as_deb822(), &[key]) {
355 return Some(r);
356 }
357 let mut chars = key.chars();
358 if let Some(first) = chars.next() {
359 let capitalized = first.to_uppercase().chain(chars).collect::<String>();
360 deb822_field_range(e.as_deb822(), &[capitalized.as_str()])
361 } else {
362 None
363 }
364 }
365 }
366 }
367
368 pub fn version_policy_range(&self) -> Option<rowan::TextRange> {
374 match self {
375 #[cfg(feature = "linebased")]
376 ParsedEntry::LineBased(e) => e.version_node().map(|n| n.text_range()),
377 #[cfg(feature = "deb822")]
378 ParsedEntry::Deb822(_) => None,
379 }
380 }
381
382 pub fn template_range(&self) -> Option<rowan::TextRange> {
386 match self {
387 #[cfg(feature = "linebased")]
388 ParsedEntry::LineBased(_) => None,
389 #[cfg(feature = "deb822")]
390 ParsedEntry::Deb822(e) => deb822_field_range(e.as_deb822(), &["Template"]),
391 }
392 }
393
394 pub fn template_kind(&self) -> Option<String> {
398 match self {
399 #[cfg(feature = "linebased")]
400 ParsedEntry::LineBased(_) => None,
401 #[cfg(feature = "deb822")]
402 ParsedEntry::Deb822(e) => e.as_deb822().get("Template"),
403 }
404 }
405
406 pub fn script(&self) -> Option<String> {
408 self.get_option("script")
409 }
410
411 pub fn component(&self) -> Option<String> {
413 self.get_option("component")
414 }
415
416 pub fn format_url(
418 &self,
419 package: impl FnOnce() -> String,
420 component: impl FnOnce() -> String,
421 ) -> Result<url::Url, url::ParseError> {
422 crate::subst::subst(&self.url(), package, component).parse()
423 }
424
425 pub fn user_agent(&self) -> Option<String> {
427 self.get_option("user-agent")
428 }
429
430 pub fn pagemangle(&self) -> Option<String> {
432 self.get_option("pagemangle")
433 }
434
435 pub fn uversionmangle(&self) -> Option<String> {
437 self.get_option("uversionmangle")
438 }
439
440 pub fn downloadurlmangle(&self) -> Option<String> {
442 self.get_option("downloadurlmangle")
443 }
444
445 pub fn pgpsigurlmangle(&self) -> Option<String> {
447 self.get_option("pgpsigurlmangle")
448 }
449
450 pub fn filenamemangle(&self) -> Option<String> {
452 self.get_option("filenamemangle")
453 }
454
455 pub fn oversionmangle(&self) -> Option<String> {
457 self.get_option("oversionmangle")
458 }
459
460 pub fn searchmode(&self) -> crate::types::SearchMode {
462 self.get_option("searchmode")
463 .and_then(|s| s.parse().ok())
464 .unwrap_or_default()
465 }
466
467 pub fn set_option(&mut self, option: crate::types::WatchOption) {
489 match self {
490 #[cfg(feature = "linebased")]
491 ParsedEntry::LineBased(e) => {
492 e.set_option(option);
493 }
494 #[cfg(feature = "deb822")]
495 ParsedEntry::Deb822(e) => {
496 e.set_option(option);
497 }
498 }
499 }
500
501 pub fn set_url(&mut self, url: &str) {
517 match self {
518 #[cfg(feature = "linebased")]
519 ParsedEntry::LineBased(e) => e.set_url(url),
520 #[cfg(feature = "deb822")]
521 ParsedEntry::Deb822(e) => e.set_source(url),
522 }
523 }
524
525 pub fn set_matching_pattern(&mut self, pattern: &str) {
541 match self {
542 #[cfg(feature = "linebased")]
543 ParsedEntry::LineBased(e) => e.set_matching_pattern(pattern),
544 #[cfg(feature = "deb822")]
545 ParsedEntry::Deb822(e) => e.set_matching_pattern(pattern),
546 }
547 }
548
549 pub fn line(&self) -> usize {
569 match self {
570 #[cfg(feature = "linebased")]
571 ParsedEntry::LineBased(e) => e.line(),
572 #[cfg(feature = "deb822")]
573 ParsedEntry::Deb822(e) => e.line(),
574 }
575 }
576
577 pub fn remove_option(&mut self, option: crate::types::WatchOption) {
600 match self {
601 #[cfg(feature = "linebased")]
602 ParsedEntry::LineBased(e) => e.del_opt(option),
603 #[cfg(feature = "deb822")]
604 ParsedEntry::Deb822(e) => e.delete_option(option),
605 }
606 }
607
608 pub fn mode(&self) -> Result<crate::types::Mode, crate::types::ParseError> {
633 match self {
634 #[cfg(feature = "linebased")]
635 ParsedEntry::LineBased(e) => e.try_mode(),
636 #[cfg(feature = "deb822")]
637 ParsedEntry::Deb822(e) => e.mode(),
638 }
639 }
640}
641
642#[cfg(feature = "deb822")]
647fn deb822_field_range(
648 paragraph: &deb822_lossless::Paragraph,
649 names: &[&str],
650) -> Option<rowan::TextRange> {
651 for name in names {
652 if let Some(entry) = paragraph.get_entry(name) {
653 return Some(entry.text_range());
654 }
655 }
656 None
657}
658
659impl std::fmt::Display for ParsedWatchFile {
660 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
661 match self {
662 #[cfg(feature = "linebased")]
663 ParsedWatchFile::LineBased(wf) => write!(f, "{}", wf),
664 #[cfg(feature = "deb822")]
665 ParsedWatchFile::Deb822(wf) => write!(f, "{}", wf),
666 }
667 }
668}
669
670pub fn parse(content: &str) -> Result<ParsedWatchFile, ParseError> {
689 let version = detect_version(content).ok_or(ParseError::UnknownVersion)?;
690
691 match version {
692 #[cfg(feature = "linebased")]
693 WatchFileVersion::LineBased(_v) => {
694 let wf: crate::linebased::WatchFile = content.parse().map_err(ParseError::LineBased)?;
695 Ok(ParsedWatchFile::LineBased(wf))
696 }
697 #[cfg(not(feature = "linebased"))]
698 WatchFileVersion::LineBased(_v) => Err(ParseError::FeatureNotEnabled(
699 "linebased feature required for v1-4 formats".to_string(),
700 )),
701 #[cfg(feature = "deb822")]
702 WatchFileVersion::Deb822 => {
703 let wf: crate::deb822::WatchFile = content.parse().map_err(ParseError::Deb822)?;
704 Ok(ParsedWatchFile::Deb822(wf))
705 }
706 #[cfg(not(feature = "deb822"))]
707 WatchFileVersion::Deb822 => Err(ParseError::FeatureNotEnabled(
708 "deb822 feature required for v5 format".to_string(),
709 )),
710 }
711}
712
713#[cfg(test)]
714mod tests {
715 use super::*;
716
717 #[test]
718 fn test_detect_version_v1_default() {
719 let content = "https://example.com/ .*.tar.gz";
720 assert_eq!(
721 detect_version(content),
722 Some(WatchFileVersion::LineBased(1))
723 );
724 }
725
726 #[test]
727 fn test_detect_version_v4() {
728 let content = "version=4\nhttps://example.com/ .*.tar.gz";
729 assert_eq!(
730 detect_version(content),
731 Some(WatchFileVersion::LineBased(4))
732 );
733 }
734
735 #[test]
736 fn test_detect_version_v4_with_spaces() {
737 let content = "version = 4\nhttps://example.com/ .*.tar.gz";
738 assert_eq!(
739 detect_version(content),
740 Some(WatchFileVersion::LineBased(4))
741 );
742 }
743
744 #[test]
745 fn test_detect_version_v5() {
746 let content = "Version: 5\n\nSource: https://example.com/";
747 assert_eq!(detect_version(content), Some(WatchFileVersion::Deb822));
748 }
749
750 #[test]
751 fn test_detect_version_v5_lowercase() {
752 let content = "version: 5\n\nSource: https://example.com/";
753 assert_eq!(detect_version(content), Some(WatchFileVersion::Deb822));
754 }
755
756 #[test]
757 fn test_detect_version_with_leading_comments() {
758 let content = "# This is a comment\nversion=4\nhttps://example.com/ .*.tar.gz";
759 assert_eq!(
760 detect_version(content),
761 Some(WatchFileVersion::LineBased(4))
762 );
763 }
764
765 #[test]
766 fn test_detect_version_with_leading_whitespace() {
767 let content = " \n version=3\nhttps://example.com/ .*.tar.gz";
768 assert_eq!(
769 detect_version(content),
770 Some(WatchFileVersion::LineBased(3))
771 );
772 }
773
774 #[test]
775 fn test_detect_version_v2() {
776 let content = "version=2\nhttps://example.com/ .*.tar.gz";
777 assert_eq!(
778 detect_version(content),
779 Some(WatchFileVersion::LineBased(2))
780 );
781 }
782
783 #[cfg(feature = "linebased")]
784 #[test]
785 fn test_parse_linebased() {
786 let content = "version=4\nhttps://example.com/ .*.tar.gz";
787 let parsed = parse(content).unwrap();
788 assert_eq!(parsed.version(), 4);
789 }
790
791 #[cfg(feature = "deb822")]
792 #[test]
793 fn test_parse_deb822() {
794 let content = "Version: 5\n\nSource: https://example.com/\nMatching-Pattern: .*.tar.gz";
795 let parsed = parse(content).unwrap();
796 assert_eq!(parsed.version(), 5);
797 }
798
799 #[cfg(all(feature = "linebased", feature = "deb822"))]
800 #[test]
801 fn test_parse_both_formats() {
802 let v4_content = "version=4\nhttps://example.com/ .*.tar.gz";
804 let v4_parsed = parse(v4_content).unwrap();
805 assert_eq!(v4_parsed.version(), 4);
806
807 let v5_content = "Version: 5\n\nSource: https://example.com/\nMatching-Pattern: .*.tar.gz";
809 let v5_parsed = parse(v5_content).unwrap();
810 assert_eq!(v5_parsed.version(), 5);
811 }
812
813 #[cfg(feature = "linebased")]
814 #[test]
815 fn test_parse_roundtrip() {
816 let content = "version=4\n# Comment\nhttps://example.com/ .*.tar.gz";
817 let parsed = parse(content).unwrap();
818 let output = parsed.to_string();
819
820 let reparsed = parse(&output).unwrap();
822 assert_eq!(reparsed.version(), 4);
823 }
824
825 #[cfg(feature = "deb822")]
826 #[test]
827 fn test_parsed_watch_file_new_v5() {
828 let wf = ParsedWatchFile::new(5).unwrap();
829 assert_eq!(wf.version(), 5);
830 assert_eq!(wf.entries().count(), 0);
831 }
832
833 #[cfg(feature = "linebased")]
834 #[test]
835 fn test_parsed_watch_file_new_v4() {
836 let wf = ParsedWatchFile::new(4).unwrap();
837 assert_eq!(wf.version(), 4);
838 assert_eq!(wf.entries().count(), 0);
839 }
840
841 #[cfg(feature = "deb822")]
842 #[test]
843 fn test_parsed_watch_file_add_entry_v5() {
844 let mut wf = ParsedWatchFile::new(5).unwrap();
845 let mut entry = wf.add_entry("https://github.com/foo/bar/tags", r".*/v?([\d.]+)\.tar\.gz");
846
847 assert_eq!(wf.entries().count(), 1);
848 assert_eq!(entry.url(), "https://github.com/foo/bar/tags");
849 assert_eq!(
850 entry.matching_pattern(),
851 Some(r".*/v?([\d.]+)\.tar\.gz".to_string())
852 );
853
854 entry.set_option(crate::types::WatchOption::Component("upstream".to_string()));
856 entry.set_option(crate::types::WatchOption::Compression(
857 crate::types::Compression::Xz,
858 ));
859
860 assert_eq!(entry.get_option("Component"), Some("upstream".to_string()));
861 assert_eq!(entry.get_option("Compression"), Some("xz".to_string()));
862 }
863
864 #[cfg(feature = "linebased")]
865 #[test]
866 fn test_parsed_watch_file_add_entry_v4() {
867 let mut wf = ParsedWatchFile::new(4).unwrap();
868 let entry = wf.add_entry("https://github.com/foo/bar/tags", r".*/v?([\d.]+)\.tar\.gz");
869
870 assert_eq!(wf.entries().count(), 1);
871 assert_eq!(entry.url(), "https://github.com/foo/bar/tags");
872 assert_eq!(
873 entry.matching_pattern(),
874 Some(r".*/v?([\d.]+)\.tar\.gz".to_string())
875 );
876 }
877
878 #[cfg(feature = "deb822")]
879 #[test]
880 fn test_parsed_watch_file_roundtrip_with_add_entry() {
881 let mut wf = ParsedWatchFile::new(5).unwrap();
882 let mut entry = wf.add_entry(
883 "https://github.com/owner/repo/tags",
884 r".*/v?([\d.]+)\.tar\.gz",
885 );
886 entry.set_option(crate::types::WatchOption::Compression(
887 crate::types::Compression::Xz,
888 ));
889
890 let output = wf.to_string();
891
892 let reparsed = parse(&output).unwrap();
894 assert_eq!(reparsed.version(), 5);
895
896 let entries: Vec<_> = reparsed.entries().collect();
897 assert_eq!(entries.len(), 1);
898 assert_eq!(entries[0].url(), "https://github.com/owner/repo/tags");
899 assert_eq!(entries[0].get_option("Compression"), Some("xz".to_string()));
900 }
901
902 #[cfg(feature = "linebased")]
903 #[test]
904 fn test_parsed_entry_set_url_v4() {
905 let mut wf = ParsedWatchFile::new(4).unwrap();
906 let mut entry = wf.add_entry("https://github.com/foo/bar/tags", r".*/v?([\d.]+)\.tar\.gz");
907
908 assert_eq!(entry.url(), "https://github.com/foo/bar/tags");
909
910 entry.set_url("https://github.com/foo/bar/releases");
911 assert_eq!(entry.url(), "https://github.com/foo/bar/releases");
912 }
913
914 #[cfg(feature = "deb822")]
915 #[test]
916 fn test_parsed_entry_set_url_v5() {
917 let mut wf = ParsedWatchFile::new(5).unwrap();
918 let mut entry = wf.add_entry("https://github.com/foo/bar/tags", r".*/v?([\d.]+)\.tar\.gz");
919
920 assert_eq!(entry.url(), "https://github.com/foo/bar/tags");
921
922 entry.set_url("https://github.com/foo/bar/releases");
923 assert_eq!(entry.url(), "https://github.com/foo/bar/releases");
924 }
925
926 #[cfg(feature = "linebased")]
927 #[test]
928 fn test_parsed_entry_set_matching_pattern_v4() {
929 let mut wf = ParsedWatchFile::new(4).unwrap();
930 let mut entry = wf.add_entry("https://github.com/foo/bar/tags", r".*/v?([\d.]+)\.tar\.gz");
931
932 assert_eq!(
933 entry.matching_pattern(),
934 Some(r".*/v?([\d.]+)\.tar\.gz".to_string())
935 );
936
937 entry.set_matching_pattern(r".*/release-([\d.]+)\.tar\.gz");
938 assert_eq!(
939 entry.matching_pattern(),
940 Some(r".*/release-([\d.]+)\.tar\.gz".to_string())
941 );
942 }
943
944 #[cfg(feature = "deb822")]
945 #[test]
946 fn test_parsed_entry_set_matching_pattern_v5() {
947 let mut wf = ParsedWatchFile::new(5).unwrap();
948 let mut entry = wf.add_entry("https://github.com/foo/bar/tags", r".*/v?([\d.]+)\.tar\.gz");
949
950 assert_eq!(
951 entry.matching_pattern(),
952 Some(r".*/v?([\d.]+)\.tar\.gz".to_string())
953 );
954
955 entry.set_matching_pattern(r".*/release-([\d.]+)\.tar\.gz");
956 assert_eq!(
957 entry.matching_pattern(),
958 Some(r".*/release-([\d.]+)\.tar\.gz".to_string())
959 );
960 }
961
962 #[cfg(feature = "linebased")]
963 #[test]
964 fn test_parsed_entry_line_v4() {
965 let content = "version=4\nhttps://example.com/ .*.tar.gz\nhttps://example2.com/ .*.tar.gz";
966 let wf = parse(content).unwrap();
967 let entries: Vec<_> = wf.entries().collect();
968
969 assert_eq!(entries[0].line(), 1); assert_eq!(entries[1].line(), 2); }
972
973 #[cfg(feature = "deb822")]
974 #[test]
975 fn test_parsed_entry_line_v5() {
976 let content = r#"Version: 5
977
978Source: https://example.com/repo1
979Matching-Pattern: .*\.tar\.gz
980
981Source: https://example.com/repo2
982Matching-Pattern: .*\.tar\.xz
983"#;
984 let wf = parse(content).unwrap();
985 let entries: Vec<_> = wf.entries().collect();
986
987 assert_eq!(entries[0].line(), 2); assert_eq!(entries[1].line(), 5); }
990
991 #[cfg(feature = "linebased")]
992 #[test]
993 fn test_url_range_linebased() {
994 let content = "version=4\nhttps://example.com/ .*-([\\d.]+)\\.tar\\.gz\n";
995 let wf = parse(content).unwrap();
996 let entry = wf.entries().next().unwrap();
997 let range = entry.url_range().expect("entry has url");
998 let start: usize = range.start().into();
999 let end: usize = range.end().into();
1000 assert_eq!(&content[start..end], "https://example.com/");
1001 }
1002
1003 #[cfg(feature = "linebased")]
1004 #[test]
1005 fn test_matching_pattern_range_linebased() {
1006 let content = "version=4\nhttps://example.com/ .*-([\\d.]+)\\.tar\\.gz\n";
1007 let wf = parse(content).unwrap();
1008 let entry = wf.entries().next().unwrap();
1009 let range = entry.matching_pattern_range().expect("has pattern");
1010 let start: usize = range.start().into();
1011 let end: usize = range.end().into();
1012 assert_eq!(&content[start..end], ".*-([\\d.]+)\\.tar\\.gz");
1013 }
1014
1015 #[cfg(feature = "linebased")]
1016 #[test]
1017 fn test_option_range_linebased() {
1018 let content = "version=4\nopts=mode=git,pretty=raw https://example.com/ .*\n";
1019 let wf = parse(content).unwrap();
1020 let entry = wf.entries().next().unwrap();
1021 let mode = entry.option_range("mode").expect("mode option");
1022 let start: usize = mode.start().into();
1023 let end: usize = mode.end().into();
1024 assert_eq!(&content[start..end], "mode=git");
1025
1026 let pretty = entry.option_range("pretty").expect("pretty option");
1027 let start: usize = pretty.start().into();
1028 let end: usize = pretty.end().into();
1029 assert_eq!(&content[start..end], "pretty=raw");
1030
1031 assert!(entry.option_range("not-a-real-option").is_none());
1032 }
1033
1034 #[cfg(feature = "linebased")]
1035 #[test]
1036 fn test_version_range_linebased() {
1037 let content = "version=4\nhttps://example.com/ .*\n";
1038 let wf = parse(content).unwrap();
1039 let range = wf.version_range().expect("has version");
1040 let start: usize = range.start().into();
1041 let end: usize = range.end().into();
1042 assert_eq!(&content[start..end], "version=4\n");
1043 }
1044
1045 #[cfg(feature = "deb822")]
1046 #[test]
1047 fn test_url_range_deb822() {
1048 let content =
1049 "Version: 5\n\nSource: https://example.com/foo\nMatching-Pattern: .*\\.tar\\.gz\n";
1050 let wf = parse(content).unwrap();
1051 let entry = wf.entries().next().unwrap();
1052 let range = entry.url_range().expect("has source");
1053 let start: usize = range.start().into();
1054 let end: usize = range.end().into();
1055 assert_eq!(&content[start..end], "Source: https://example.com/foo\n");
1058 }
1059
1060 #[cfg(feature = "deb822")]
1061 #[test]
1062 fn test_matching_pattern_range_deb822() {
1063 let content =
1064 "Version: 5\n\nSource: https://example.com/foo\nMatching-Pattern: v(.+)\\.tar\\.gz\n";
1065 let wf = parse(content).unwrap();
1066 let entry = wf.entries().next().unwrap();
1067 let range = entry.matching_pattern_range().expect("has pattern");
1068 let start: usize = range.start().into();
1069 let end: usize = range.end().into();
1070 assert_eq!(&content[start..end], "Matching-Pattern: v(.+)\\.tar\\.gz\n");
1071 }
1072
1073 #[cfg(feature = "deb822")]
1074 #[test]
1075 fn test_option_range_deb822_lookup_capitalises_key() {
1076 let content =
1081 "Version: 5\n\nSource: https://example.com/foo\nMatching-Pattern: x\nMode: git\n";
1082 let wf = parse(content).unwrap();
1083 let entry = wf.entries().next().unwrap();
1084 let range = entry.option_range("mode").expect("mode field");
1085 let start: usize = range.start().into();
1086 let end: usize = range.end().into();
1087 assert_eq!(&content[start..end], "Mode: git\n");
1088 }
1089
1090 #[cfg(feature = "deb822")]
1091 #[test]
1092 fn test_version_range_deb822() {
1093 let content = "Version: 5\n\nSource: https://example.com/foo\nMatching-Pattern: x\n";
1094 let wf = parse(content).unwrap();
1095 let range = wf.version_range().expect("has version");
1096 let start: usize = range.start().into();
1097 let end: usize = range.end().into();
1098 assert_eq!(&content[start..end], "Version: 5\n");
1099 }
1100
1101 #[cfg(feature = "deb822")]
1102 #[test]
1103 fn test_template_range_deb822() {
1104 let content = "Version: 5\n\nSource: https://github.com/foo/bar\nTemplate: GitHub\n";
1105 let wf = parse(content).unwrap();
1106 let entry = wf.entries().next().unwrap();
1107 let range = entry.template_range().expect("has template");
1108 let start: usize = range.start().into();
1109 let end: usize = range.end().into();
1110 assert_eq!(&content[start..end], "Template: GitHub\n");
1111 assert_eq!(entry.template_kind(), Some("GitHub".to_string()));
1112 }
1113}
1114
1115#[derive(Clone, PartialEq, Eq)]
1121pub struct Parse {
1122 inner: ParseInner,
1123}
1124
1125#[derive(Clone, PartialEq, Eq)]
1126enum ParseInner {
1127 #[cfg(feature = "linebased")]
1128 LineBased(crate::linebased::Parse<crate::linebased::WatchFile>),
1129 #[cfg(feature = "deb822")]
1130 Deb822(deb822_lossless::Parse<deb822_lossless::Deb822>),
1131}
1132
1133impl Parse {
1134 pub fn parse(text: &str) -> Self {
1136 let version = detect_version(text);
1137
1138 let inner = match version {
1139 #[cfg(feature = "linebased")]
1140 Some(WatchFileVersion::LineBased(_)) => {
1141 ParseInner::LineBased(crate::linebased::parse_watch_file(text))
1142 }
1143 #[cfg(feature = "deb822")]
1144 Some(WatchFileVersion::Deb822) => {
1145 ParseInner::Deb822(deb822_lossless::Deb822::parse(text))
1146 }
1147 #[cfg(not(feature = "linebased"))]
1148 Some(WatchFileVersion::LineBased(_)) => {
1149 #[cfg(feature = "deb822")]
1151 {
1152 ParseInner::Deb822(deb822_lossless::Deb822::parse(text))
1153 }
1154 #[cfg(not(feature = "deb822"))]
1155 {
1156 panic!("No watch file parsing features enabled")
1157 }
1158 }
1159 #[cfg(not(feature = "deb822"))]
1160 Some(WatchFileVersion::Deb822) => {
1161 #[cfg(feature = "linebased")]
1163 {
1164 ParseInner::LineBased(crate::linebased::parse_watch_file(text))
1165 }
1166 #[cfg(not(feature = "linebased"))]
1167 {
1168 panic!("No watch file parsing features enabled")
1169 }
1170 }
1171 None => {
1172 #[cfg(feature = "linebased")]
1174 {
1175 ParseInner::LineBased(crate::linebased::parse_watch_file(text))
1176 }
1177 #[cfg(not(feature = "linebased"))]
1178 #[cfg(feature = "deb822")]
1179 {
1180 ParseInner::Deb822(deb822_lossless::Deb822::parse(text))
1181 }
1182 #[cfg(not(any(feature = "linebased", feature = "deb822")))]
1183 {
1184 panic!("No watch file parsing features enabled")
1185 }
1186 }
1187 };
1188
1189 Parse { inner }
1190 }
1191
1192 pub fn to_watch_file(&self) -> ParsedWatchFile {
1194 match &self.inner {
1195 #[cfg(feature = "linebased")]
1196 ParseInner::LineBased(parse) => ParsedWatchFile::LineBased(parse.tree()),
1197 #[cfg(feature = "deb822")]
1198 ParseInner::Deb822(parse) => {
1199 let deb822 = parse.tree();
1200 ParsedWatchFile::Deb822(crate::deb822::WatchFile::from_deb822(deb822))
1201 }
1202 }
1203 }
1204
1205 pub fn version(&self) -> u32 {
1207 match &self.inner {
1208 #[cfg(feature = "linebased")]
1209 ParseInner::LineBased(parse) => parse.tree().version(),
1210 #[cfg(feature = "deb822")]
1211 ParseInner::Deb822(_) => 5,
1212 }
1213 }
1214}
1215
1216unsafe impl Send for Parse {}
1219unsafe impl Sync for Parse {}