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 snapshot(&self) -> Self {
142 match self {
143 #[cfg(feature = "linebased")]
144 ParsedWatchFile::LineBased(wf) => ParsedWatchFile::LineBased(wf.snapshot()),
145 #[cfg(feature = "deb822")]
146 ParsedWatchFile::Deb822(wf) => ParsedWatchFile::Deb822(wf.snapshot()),
147 }
148 }
149
150 pub fn tree_eq(&self, other: &Self) -> bool {
155 match (self, other) {
156 #[cfg(feature = "linebased")]
157 (ParsedWatchFile::LineBased(a), ParsedWatchFile::LineBased(b)) => a.tree_eq(b),
158 #[cfg(feature = "deb822")]
159 (ParsedWatchFile::Deb822(a), ParsedWatchFile::Deb822(b)) => a.tree_eq(b),
160 #[allow(unreachable_patterns)]
161 _ => false,
162 }
163 }
164
165 pub fn new(version: u32) -> Result<Self, ParseError> {
182 match version {
183 #[cfg(feature = "deb822")]
184 5 => Ok(ParsedWatchFile::Deb822(crate::deb822::WatchFile::new())),
185 #[cfg(not(feature = "deb822"))]
186 5 => Err(ParseError::FeatureNotEnabled(
187 "deb822 feature required for v5 format".to_string(),
188 )),
189 #[cfg(feature = "linebased")]
190 v @ 1..=4 => Ok(ParsedWatchFile::LineBased(
191 crate::linebased::WatchFile::new(Some(v)),
192 )),
193 #[cfg(not(feature = "linebased"))]
194 v @ 1..=4 => Err(ParseError::FeatureNotEnabled(format!(
195 "linebased feature required for v{} format",
196 v
197 ))),
198 v => Err(ParseError::FeatureNotEnabled(format!(
199 "unsupported watch file version: {}",
200 v
201 ))),
202 }
203 }
204
205 pub fn version(&self) -> u32 {
207 match self {
208 #[cfg(feature = "linebased")]
209 ParsedWatchFile::LineBased(wf) => wf.version(),
210 #[cfg(feature = "deb822")]
211 ParsedWatchFile::Deb822(wf) => wf.version(),
212 }
213 }
214
215 pub fn entries(&self) -> impl Iterator<Item = ParsedEntry> + '_ {
217 let entries: Vec<_> = match self {
219 #[cfg(feature = "linebased")]
220 ParsedWatchFile::LineBased(wf) => wf.entries().map(ParsedEntry::LineBased).collect(),
221 #[cfg(feature = "deb822")]
222 ParsedWatchFile::Deb822(wf) => wf.entries().map(ParsedEntry::Deb822).collect(),
223 };
224 entries.into_iter()
225 }
226
227 pub fn add_entry(&mut self, source: &str, matching_pattern: &str) -> ParsedEntry {
248 match self {
249 #[cfg(feature = "linebased")]
250 ParsedWatchFile::LineBased(wf) => {
251 let entry = crate::linebased::EntryBuilder::new(source)
252 .matching_pattern(matching_pattern)
253 .build();
254 let added_entry = wf.add_entry(entry);
255 ParsedEntry::LineBased(added_entry)
256 }
257 #[cfg(feature = "deb822")]
258 ParsedWatchFile::Deb822(wf) => {
259 let added_entry = wf.add_entry(source, matching_pattern);
260 ParsedEntry::Deb822(added_entry)
261 }
262 }
263 }
264
265 pub fn version_range(&self) -> Option<rowan::TextRange> {
272 match self {
273 #[cfg(feature = "linebased")]
274 ParsedWatchFile::LineBased(wf) => wf.version_node().map(|v| v.text_range()),
275 #[cfg(feature = "deb822")]
276 ParsedWatchFile::Deb822(wf) => {
277 let first = wf.as_deb822().paragraphs().next()?;
280 first.get_entry("Version").map(|e| e.text_range())
281 }
282 }
283 }
284}
285
286impl ParsedEntry {
287 pub fn url(&self) -> String {
289 match self {
290 #[cfg(feature = "linebased")]
291 ParsedEntry::LineBased(e) => e.url(),
292 #[cfg(feature = "deb822")]
293 ParsedEntry::Deb822(e) => e.source().unwrap_or(None).unwrap_or_default(),
294 }
295 }
296
297 pub fn matching_pattern(&self) -> Option<String> {
299 match self {
300 #[cfg(feature = "linebased")]
301 ParsedEntry::LineBased(e) => e.matching_pattern(),
302 #[cfg(feature = "deb822")]
303 ParsedEntry::Deb822(e) => e.matching_pattern().unwrap_or(None),
304 }
305 }
306
307 pub fn get_option(&self, key: &str) -> Option<String> {
313 match self {
314 #[cfg(feature = "linebased")]
315 ParsedEntry::LineBased(e) => e.get_option(key),
316 #[cfg(feature = "deb822")]
317 ParsedEntry::Deb822(e) => {
318 e.get_field(key).or_else(|| {
320 let mut chars = key.chars();
321 if let Some(first) = chars.next() {
322 let capitalized = first.to_uppercase().chain(chars).collect::<String>();
323 e.get_field(&capitalized)
324 } else {
325 None
326 }
327 })
328 }
329 }
330 }
331
332 pub fn has_option(&self, key: &str) -> bool {
334 self.get_option(key).is_some()
335 }
336
337 pub fn url_range(&self) -> Option<rowan::TextRange> {
344 match self {
345 #[cfg(feature = "linebased")]
346 ParsedEntry::LineBased(e) => e.url_node().map(|n| n.text_range()),
347 #[cfg(feature = "deb822")]
348 ParsedEntry::Deb822(e) => deb822_field_range(e.as_deb822(), &["Source", "URL"]),
349 }
350 }
351
352 pub fn matching_pattern_range(&self) -> Option<rowan::TextRange> {
357 match self {
358 #[cfg(feature = "linebased")]
359 ParsedEntry::LineBased(e) => e.matching_pattern_node().map(|n| n.text_range()),
360 #[cfg(feature = "deb822")]
361 ParsedEntry::Deb822(e) => deb822_field_range(e.as_deb822(), &["Matching-Pattern"]),
362 }
363 }
364
365 pub fn option_range(&self, key: &str) -> Option<rowan::TextRange> {
371 match self {
372 #[cfg(feature = "linebased")]
373 ParsedEntry::LineBased(e) => {
374 let list = e.option_list()?;
375 let opt = list.find_option(key)?;
376 Some(opt.text_range())
377 }
378 #[cfg(feature = "deb822")]
379 ParsedEntry::Deb822(e) => {
380 if let Some(r) = deb822_field_range(e.as_deb822(), &[key]) {
385 return Some(r);
386 }
387 let mut chars = key.chars();
388 if let Some(first) = chars.next() {
389 let capitalized = first.to_uppercase().chain(chars).collect::<String>();
390 deb822_field_range(e.as_deb822(), &[capitalized.as_str()])
391 } else {
392 None
393 }
394 }
395 }
396 }
397
398 pub fn version_policy_range(&self) -> Option<rowan::TextRange> {
404 match self {
405 #[cfg(feature = "linebased")]
406 ParsedEntry::LineBased(e) => e.version_node().map(|n| n.text_range()),
407 #[cfg(feature = "deb822")]
408 ParsedEntry::Deb822(_) => None,
409 }
410 }
411
412 pub fn template_range(&self) -> Option<rowan::TextRange> {
416 match self {
417 #[cfg(feature = "linebased")]
418 ParsedEntry::LineBased(_) => None,
419 #[cfg(feature = "deb822")]
420 ParsedEntry::Deb822(e) => deb822_field_range(e.as_deb822(), &["Template"]),
421 }
422 }
423
424 pub fn template_kind(&self) -> Option<String> {
428 match self {
429 #[cfg(feature = "linebased")]
430 ParsedEntry::LineBased(_) => None,
431 #[cfg(feature = "deb822")]
432 ParsedEntry::Deb822(e) => e.as_deb822().get("Template"),
433 }
434 }
435
436 pub fn script(&self) -> Option<String> {
438 match self {
439 #[cfg(feature = "linebased")]
440 ParsedEntry::LineBased(e) => e.script(),
441 #[cfg(feature = "deb822")]
442 ParsedEntry::Deb822(_) => self.get_option("script"),
443 }
444 }
445
446 pub fn component(&self) -> Option<String> {
448 self.get_option("component")
449 }
450
451 pub fn format_url(
453 &self,
454 package: impl FnOnce() -> String,
455 component: impl FnOnce() -> String,
456 ) -> Result<url::Url, url::ParseError> {
457 crate::subst::subst(&self.url(), package, component).parse()
458 }
459
460 pub fn user_agent(&self) -> Option<String> {
462 self.get_option("user-agent")
463 }
464
465 pub fn pagemangle(&self) -> Option<String> {
467 self.get_option("pagemangle")
468 }
469
470 pub fn uversionmangle(&self) -> Option<String> {
472 self.get_option("uversionmangle")
473 }
474
475 pub fn downloadurlmangle(&self) -> Option<String> {
477 self.get_option("downloadurlmangle")
478 }
479
480 pub fn pgpsigurlmangle(&self) -> Option<String> {
482 self.get_option("pgpsigurlmangle")
483 }
484
485 pub fn filenamemangle(&self) -> Option<String> {
487 self.get_option("filenamemangle")
488 }
489
490 pub fn oversionmangle(&self) -> Option<String> {
492 self.get_option("oversionmangle")
493 }
494
495 pub fn searchmode(&self) -> crate::types::SearchMode {
497 self.get_option("searchmode")
498 .and_then(|s| s.parse().ok())
499 .unwrap_or_default()
500 }
501
502 pub fn set_option(&mut self, option: crate::types::WatchOption) {
524 match self {
525 #[cfg(feature = "linebased")]
526 ParsedEntry::LineBased(e) => {
527 e.set_option(option);
528 }
529 #[cfg(feature = "deb822")]
530 ParsedEntry::Deb822(e) => {
531 e.set_option(option);
532 }
533 }
534 }
535
536 pub fn set_url(&mut self, url: &str) {
552 match self {
553 #[cfg(feature = "linebased")]
554 ParsedEntry::LineBased(e) => e.set_url(url),
555 #[cfg(feature = "deb822")]
556 ParsedEntry::Deb822(e) => e.set_source(url),
557 }
558 }
559
560 pub fn set_matching_pattern(&mut self, pattern: &str) {
576 match self {
577 #[cfg(feature = "linebased")]
578 ParsedEntry::LineBased(e) => e.set_matching_pattern(pattern),
579 #[cfg(feature = "deb822")]
580 ParsedEntry::Deb822(e) => e.set_matching_pattern(pattern),
581 }
582 }
583
584 pub fn line(&self) -> usize {
604 match self {
605 #[cfg(feature = "linebased")]
606 ParsedEntry::LineBased(e) => e.line(),
607 #[cfg(feature = "deb822")]
608 ParsedEntry::Deb822(e) => e.line(),
609 }
610 }
611
612 pub fn remove_option(&mut self, option: crate::types::WatchOption) {
635 match self {
636 #[cfg(feature = "linebased")]
637 ParsedEntry::LineBased(e) => e.del_opt(option),
638 #[cfg(feature = "deb822")]
639 ParsedEntry::Deb822(e) => e.delete_option(option),
640 }
641 }
642
643 pub fn mode(&self) -> Result<crate::types::Mode, crate::types::ParseError> {
668 match self {
669 #[cfg(feature = "linebased")]
670 ParsedEntry::LineBased(e) => e.try_mode(),
671 #[cfg(feature = "deb822")]
672 ParsedEntry::Deb822(e) => e.mode(),
673 }
674 }
675}
676
677#[cfg(feature = "deb822")]
682fn deb822_field_range(
683 paragraph: &deb822_lossless::Paragraph,
684 names: &[&str],
685) -> Option<rowan::TextRange> {
686 for name in names {
687 if let Some(entry) = paragraph.get_entry(name) {
688 return Some(entry.text_range());
689 }
690 }
691 None
692}
693
694impl std::fmt::Display for ParsedWatchFile {
695 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
696 match self {
697 #[cfg(feature = "linebased")]
698 ParsedWatchFile::LineBased(wf) => write!(f, "{}", wf),
699 #[cfg(feature = "deb822")]
700 ParsedWatchFile::Deb822(wf) => write!(f, "{}", wf),
701 }
702 }
703}
704
705pub fn parse(content: &str) -> Result<ParsedWatchFile, ParseError> {
724 let version = detect_version(content).ok_or(ParseError::UnknownVersion)?;
725
726 match version {
727 #[cfg(feature = "linebased")]
728 WatchFileVersion::LineBased(_v) => {
729 let wf: crate::linebased::WatchFile = content.parse().map_err(ParseError::LineBased)?;
730 Ok(ParsedWatchFile::LineBased(wf))
731 }
732 #[cfg(not(feature = "linebased"))]
733 WatchFileVersion::LineBased(_v) => Err(ParseError::FeatureNotEnabled(
734 "linebased feature required for v1-4 formats".to_string(),
735 )),
736 #[cfg(feature = "deb822")]
737 WatchFileVersion::Deb822 => {
738 let wf: crate::deb822::WatchFile = content.parse().map_err(ParseError::Deb822)?;
739 Ok(ParsedWatchFile::Deb822(wf))
740 }
741 #[cfg(not(feature = "deb822"))]
742 WatchFileVersion::Deb822 => Err(ParseError::FeatureNotEnabled(
743 "deb822 feature required for v5 format".to_string(),
744 )),
745 }
746}
747
748#[cfg(test)]
749mod tests {
750 use super::*;
751
752 #[test]
753 fn test_detect_version_v1_default() {
754 let content = "https://example.com/ .*.tar.gz";
755 assert_eq!(
756 detect_version(content),
757 Some(WatchFileVersion::LineBased(1))
758 );
759 }
760
761 #[test]
762 fn test_detect_version_v4() {
763 let content = "version=4\nhttps://example.com/ .*.tar.gz";
764 assert_eq!(
765 detect_version(content),
766 Some(WatchFileVersion::LineBased(4))
767 );
768 }
769
770 #[test]
771 fn test_detect_version_v4_with_spaces() {
772 let content = "version = 4\nhttps://example.com/ .*.tar.gz";
773 assert_eq!(
774 detect_version(content),
775 Some(WatchFileVersion::LineBased(4))
776 );
777 }
778
779 #[test]
780 fn test_detect_version_v5() {
781 let content = "Version: 5\n\nSource: https://example.com/";
782 assert_eq!(detect_version(content), Some(WatchFileVersion::Deb822));
783 }
784
785 #[test]
786 fn test_detect_version_v5_lowercase() {
787 let content = "version: 5\n\nSource: https://example.com/";
788 assert_eq!(detect_version(content), Some(WatchFileVersion::Deb822));
789 }
790
791 #[test]
792 fn test_detect_version_with_leading_comments() {
793 let content = "# This is a comment\nversion=4\nhttps://example.com/ .*.tar.gz";
794 assert_eq!(
795 detect_version(content),
796 Some(WatchFileVersion::LineBased(4))
797 );
798 }
799
800 #[test]
801 fn test_detect_version_with_leading_whitespace() {
802 let content = " \n version=3\nhttps://example.com/ .*.tar.gz";
803 assert_eq!(
804 detect_version(content),
805 Some(WatchFileVersion::LineBased(3))
806 );
807 }
808
809 #[test]
810 fn test_detect_version_v2() {
811 let content = "version=2\nhttps://example.com/ .*.tar.gz";
812 assert_eq!(
813 detect_version(content),
814 Some(WatchFileVersion::LineBased(2))
815 );
816 }
817
818 #[cfg(feature = "linebased")]
819 #[test]
820 fn test_parse_linebased() {
821 let content = "version=4\nhttps://example.com/ .*.tar.gz";
822 let parsed = parse(content).unwrap();
823 assert_eq!(parsed.version(), 4);
824 }
825
826 #[cfg(feature = "deb822")]
827 #[test]
828 fn test_parse_deb822() {
829 let content = "Version: 5\n\nSource: https://example.com/\nMatching-Pattern: .*.tar.gz";
830 let parsed = parse(content).unwrap();
831 assert_eq!(parsed.version(), 5);
832 }
833
834 #[cfg(all(feature = "linebased", feature = "deb822"))]
835 #[test]
836 fn test_parse_both_formats() {
837 let v4_content = "version=4\nhttps://example.com/ .*.tar.gz";
839 let v4_parsed = parse(v4_content).unwrap();
840 assert_eq!(v4_parsed.version(), 4);
841
842 let v5_content = "Version: 5\n\nSource: https://example.com/\nMatching-Pattern: .*.tar.gz";
844 let v5_parsed = parse(v5_content).unwrap();
845 assert_eq!(v5_parsed.version(), 5);
846 }
847
848 #[cfg(feature = "linebased")]
849 #[test]
850 fn test_parse_roundtrip() {
851 let content = "version=4\n# Comment\nhttps://example.com/ .*.tar.gz";
852 let parsed = parse(content).unwrap();
853 let output = parsed.to_string();
854
855 let reparsed = parse(&output).unwrap();
857 assert_eq!(reparsed.version(), 4);
858 }
859
860 #[cfg(feature = "deb822")]
861 #[test]
862 fn test_parsed_watch_file_new_v5() {
863 let wf = ParsedWatchFile::new(5).unwrap();
864 assert_eq!(wf.version(), 5);
865 assert_eq!(wf.entries().count(), 0);
866 }
867
868 #[cfg(feature = "linebased")]
869 #[test]
870 fn test_parsed_watch_file_new_v4() {
871 let wf = ParsedWatchFile::new(4).unwrap();
872 assert_eq!(wf.version(), 4);
873 assert_eq!(wf.entries().count(), 0);
874 }
875
876 #[cfg(feature = "deb822")]
877 #[test]
878 fn test_parsed_watch_file_add_entry_v5() {
879 let mut wf = ParsedWatchFile::new(5).unwrap();
880 let mut entry = wf.add_entry("https://github.com/foo/bar/tags", r".*/v?([\d.]+)\.tar\.gz");
881
882 assert_eq!(wf.entries().count(), 1);
883 assert_eq!(entry.url(), "https://github.com/foo/bar/tags");
884 assert_eq!(
885 entry.matching_pattern(),
886 Some(r".*/v?([\d.]+)\.tar\.gz".to_string())
887 );
888
889 entry.set_option(crate::types::WatchOption::Component("upstream".to_string()));
891 entry.set_option(crate::types::WatchOption::Compression(
892 crate::types::Compression::Xz,
893 ));
894
895 assert_eq!(entry.get_option("Component"), Some("upstream".to_string()));
896 assert_eq!(entry.get_option("Compression"), Some("xz".to_string()));
897 }
898
899 #[cfg(feature = "linebased")]
900 #[test]
901 fn test_parsed_watch_file_add_entry_v4() {
902 let mut wf = ParsedWatchFile::new(4).unwrap();
903 let entry = wf.add_entry("https://github.com/foo/bar/tags", r".*/v?([\d.]+)\.tar\.gz");
904
905 assert_eq!(wf.entries().count(), 1);
906 assert_eq!(entry.url(), "https://github.com/foo/bar/tags");
907 assert_eq!(
908 entry.matching_pattern(),
909 Some(r".*/v?([\d.]+)\.tar\.gz".to_string())
910 );
911 }
912
913 #[cfg(feature = "deb822")]
914 #[test]
915 fn test_parsed_watch_file_roundtrip_with_add_entry() {
916 let mut wf = ParsedWatchFile::new(5).unwrap();
917 let mut entry = wf.add_entry(
918 "https://github.com/owner/repo/tags",
919 r".*/v?([\d.]+)\.tar\.gz",
920 );
921 entry.set_option(crate::types::WatchOption::Compression(
922 crate::types::Compression::Xz,
923 ));
924
925 let output = wf.to_string();
926
927 let reparsed = parse(&output).unwrap();
929 assert_eq!(reparsed.version(), 5);
930
931 let entries: Vec<_> = reparsed.entries().collect();
932 assert_eq!(entries.len(), 1);
933 assert_eq!(entries[0].url(), "https://github.com/owner/repo/tags");
934 assert_eq!(entries[0].get_option("Compression"), Some("xz".to_string()));
935 }
936
937 #[cfg(feature = "linebased")]
938 #[test]
939 fn test_parsed_entry_set_url_v4() {
940 let mut wf = ParsedWatchFile::new(4).unwrap();
941 let mut entry = wf.add_entry("https://github.com/foo/bar/tags", r".*/v?([\d.]+)\.tar\.gz");
942
943 assert_eq!(entry.url(), "https://github.com/foo/bar/tags");
944
945 entry.set_url("https://github.com/foo/bar/releases");
946 assert_eq!(entry.url(), "https://github.com/foo/bar/releases");
947 }
948
949 #[cfg(feature = "deb822")]
950 #[test]
951 fn test_parsed_entry_set_url_v5() {
952 let mut wf = ParsedWatchFile::new(5).unwrap();
953 let mut entry = wf.add_entry("https://github.com/foo/bar/tags", r".*/v?([\d.]+)\.tar\.gz");
954
955 assert_eq!(entry.url(), "https://github.com/foo/bar/tags");
956
957 entry.set_url("https://github.com/foo/bar/releases");
958 assert_eq!(entry.url(), "https://github.com/foo/bar/releases");
959 }
960
961 #[cfg(feature = "linebased")]
962 #[test]
963 fn test_parsed_entry_set_matching_pattern_v4() {
964 let mut wf = ParsedWatchFile::new(4).unwrap();
965 let mut entry = wf.add_entry("https://github.com/foo/bar/tags", r".*/v?([\d.]+)\.tar\.gz");
966
967 assert_eq!(
968 entry.matching_pattern(),
969 Some(r".*/v?([\d.]+)\.tar\.gz".to_string())
970 );
971
972 entry.set_matching_pattern(r".*/release-([\d.]+)\.tar\.gz");
973 assert_eq!(
974 entry.matching_pattern(),
975 Some(r".*/release-([\d.]+)\.tar\.gz".to_string())
976 );
977 }
978
979 #[cfg(feature = "deb822")]
980 #[test]
981 fn test_parsed_entry_set_matching_pattern_v5() {
982 let mut wf = ParsedWatchFile::new(5).unwrap();
983 let mut entry = wf.add_entry("https://github.com/foo/bar/tags", r".*/v?([\d.]+)\.tar\.gz");
984
985 assert_eq!(
986 entry.matching_pattern(),
987 Some(r".*/v?([\d.]+)\.tar\.gz".to_string())
988 );
989
990 entry.set_matching_pattern(r".*/release-([\d.]+)\.tar\.gz");
991 assert_eq!(
992 entry.matching_pattern(),
993 Some(r".*/release-([\d.]+)\.tar\.gz".to_string())
994 );
995 }
996
997 #[cfg(feature = "linebased")]
998 #[test]
999 fn test_parsed_entry_line_v4() {
1000 let content = "version=4\nhttps://example.com/ .*.tar.gz\nhttps://example2.com/ .*.tar.gz";
1001 let wf = parse(content).unwrap();
1002 let entries: Vec<_> = wf.entries().collect();
1003
1004 assert_eq!(entries[0].line(), 1); assert_eq!(entries[1].line(), 2); }
1007
1008 #[cfg(feature = "deb822")]
1009 #[test]
1010 fn test_parsed_entry_line_v5() {
1011 let content = r#"Version: 5
1012
1013Source: https://example.com/repo1
1014Matching-Pattern: .*\.tar\.gz
1015
1016Source: https://example.com/repo2
1017Matching-Pattern: .*\.tar\.xz
1018"#;
1019 let wf = parse(content).unwrap();
1020 let entries: Vec<_> = wf.entries().collect();
1021
1022 assert_eq!(entries[0].line(), 2); assert_eq!(entries[1].line(), 5); }
1025
1026 #[cfg(feature = "linebased")]
1027 #[test]
1028 fn test_url_range_linebased() {
1029 let content = "version=4\nhttps://example.com/ .*-([\\d.]+)\\.tar\\.gz\n";
1030 let wf = parse(content).unwrap();
1031 let entry = wf.entries().next().unwrap();
1032 let range = entry.url_range().expect("entry has url");
1033 let start: usize = range.start().into();
1034 let end: usize = range.end().into();
1035 assert_eq!(&content[start..end], "https://example.com/");
1036 }
1037
1038 #[cfg(feature = "linebased")]
1039 #[test]
1040 fn test_matching_pattern_range_linebased() {
1041 let content = "version=4\nhttps://example.com/ .*-([\\d.]+)\\.tar\\.gz\n";
1042 let wf = parse(content).unwrap();
1043 let entry = wf.entries().next().unwrap();
1044 let range = entry.matching_pattern_range().expect("has pattern");
1045 let start: usize = range.start().into();
1046 let end: usize = range.end().into();
1047 assert_eq!(&content[start..end], ".*-([\\d.]+)\\.tar\\.gz");
1048 }
1049
1050 #[cfg(feature = "linebased")]
1051 #[test]
1052 fn test_option_range_linebased() {
1053 let content = "version=4\nopts=mode=git,pretty=raw https://example.com/ .*\n";
1054 let wf = parse(content).unwrap();
1055 let entry = wf.entries().next().unwrap();
1056 let mode = entry.option_range("mode").expect("mode option");
1057 let start: usize = mode.start().into();
1058 let end: usize = mode.end().into();
1059 assert_eq!(&content[start..end], "mode=git");
1060
1061 let pretty = entry.option_range("pretty").expect("pretty option");
1062 let start: usize = pretty.start().into();
1063 let end: usize = pretty.end().into();
1064 assert_eq!(&content[start..end], "pretty=raw");
1065
1066 assert!(entry.option_range("not-a-real-option").is_none());
1067 }
1068
1069 #[cfg(feature = "linebased")]
1070 #[test]
1071 fn test_version_range_linebased() {
1072 let content = "version=4\nhttps://example.com/ .*\n";
1073 let wf = parse(content).unwrap();
1074 let range = wf.version_range().expect("has version");
1075 let start: usize = range.start().into();
1076 let end: usize = range.end().into();
1077 assert_eq!(&content[start..end], "version=4\n");
1078 }
1079
1080 #[cfg(feature = "deb822")]
1081 #[test]
1082 fn test_url_range_deb822() {
1083 let content =
1084 "Version: 5\n\nSource: https://example.com/foo\nMatching-Pattern: .*\\.tar\\.gz\n";
1085 let wf = parse(content).unwrap();
1086 let entry = wf.entries().next().unwrap();
1087 let range = entry.url_range().expect("has source");
1088 let start: usize = range.start().into();
1089 let end: usize = range.end().into();
1090 assert_eq!(&content[start..end], "Source: https://example.com/foo\n");
1093 }
1094
1095 #[cfg(feature = "deb822")]
1096 #[test]
1097 fn test_matching_pattern_range_deb822() {
1098 let content =
1099 "Version: 5\n\nSource: https://example.com/foo\nMatching-Pattern: v(.+)\\.tar\\.gz\n";
1100 let wf = parse(content).unwrap();
1101 let entry = wf.entries().next().unwrap();
1102 let range = entry.matching_pattern_range().expect("has pattern");
1103 let start: usize = range.start().into();
1104 let end: usize = range.end().into();
1105 assert_eq!(&content[start..end], "Matching-Pattern: v(.+)\\.tar\\.gz\n");
1106 }
1107
1108 #[cfg(feature = "deb822")]
1109 #[test]
1110 fn test_option_range_deb822_lookup_capitalises_key() {
1111 let content =
1116 "Version: 5\n\nSource: https://example.com/foo\nMatching-Pattern: x\nMode: git\n";
1117 let wf = parse(content).unwrap();
1118 let entry = wf.entries().next().unwrap();
1119 let range = entry.option_range("mode").expect("mode field");
1120 let start: usize = range.start().into();
1121 let end: usize = range.end().into();
1122 assert_eq!(&content[start..end], "Mode: git\n");
1123 }
1124
1125 #[cfg(feature = "deb822")]
1126 #[test]
1127 fn test_version_range_deb822() {
1128 let content = "Version: 5\n\nSource: https://example.com/foo\nMatching-Pattern: x\n";
1129 let wf = parse(content).unwrap();
1130 let range = wf.version_range().expect("has version");
1131 let start: usize = range.start().into();
1132 let end: usize = range.end().into();
1133 assert_eq!(&content[start..end], "Version: 5\n");
1134 }
1135
1136 #[cfg(feature = "deb822")]
1137 #[test]
1138 fn test_template_range_deb822() {
1139 let content = "Version: 5\n\nSource: https://github.com/foo/bar\nTemplate: GitHub\n";
1140 let wf = parse(content).unwrap();
1141 let entry = wf.entries().next().unwrap();
1142 let range = entry.template_range().expect("has template");
1143 let start: usize = range.start().into();
1144 let end: usize = range.end().into();
1145 assert_eq!(&content[start..end], "Template: GitHub\n");
1146 assert_eq!(entry.template_kind(), Some("GitHub".to_string()));
1147 }
1148}
1149
1150#[derive(Clone, PartialEq, Eq)]
1156pub struct Parse {
1157 inner: ParseInner,
1158}
1159
1160#[derive(Clone, PartialEq, Eq)]
1161enum ParseInner {
1162 #[cfg(feature = "linebased")]
1163 LineBased(crate::linebased::Parse<crate::linebased::WatchFile>),
1164 #[cfg(feature = "deb822")]
1165 Deb822(deb822_lossless::Parse<deb822_lossless::Deb822>),
1166}
1167
1168impl Parse {
1169 pub fn parse(text: &str) -> Self {
1171 let version = detect_version(text);
1172
1173 let inner = match version {
1174 #[cfg(feature = "linebased")]
1175 Some(WatchFileVersion::LineBased(_)) => {
1176 ParseInner::LineBased(crate::linebased::parse_watch_file(text))
1177 }
1178 #[cfg(feature = "deb822")]
1179 Some(WatchFileVersion::Deb822) => {
1180 ParseInner::Deb822(deb822_lossless::Deb822::parse(text))
1181 }
1182 #[cfg(not(feature = "linebased"))]
1183 Some(WatchFileVersion::LineBased(_)) => {
1184 #[cfg(feature = "deb822")]
1186 {
1187 ParseInner::Deb822(deb822_lossless::Deb822::parse(text))
1188 }
1189 #[cfg(not(feature = "deb822"))]
1190 {
1191 panic!("No watch file parsing features enabled")
1192 }
1193 }
1194 #[cfg(not(feature = "deb822"))]
1195 Some(WatchFileVersion::Deb822) => {
1196 #[cfg(feature = "linebased")]
1198 {
1199 ParseInner::LineBased(crate::linebased::parse_watch_file(text))
1200 }
1201 #[cfg(not(feature = "linebased"))]
1202 {
1203 panic!("No watch file parsing features enabled")
1204 }
1205 }
1206 None => {
1207 #[cfg(feature = "linebased")]
1209 {
1210 ParseInner::LineBased(crate::linebased::parse_watch_file(text))
1211 }
1212 #[cfg(not(feature = "linebased"))]
1213 #[cfg(feature = "deb822")]
1214 {
1215 ParseInner::Deb822(deb822_lossless::Deb822::parse(text))
1216 }
1217 #[cfg(not(any(feature = "linebased", feature = "deb822")))]
1218 {
1219 panic!("No watch file parsing features enabled")
1220 }
1221 }
1222 };
1223
1224 Parse { inner }
1225 }
1226
1227 pub fn to_watch_file(&self) -> ParsedWatchFile {
1229 match &self.inner {
1230 #[cfg(feature = "linebased")]
1231 ParseInner::LineBased(parse) => ParsedWatchFile::LineBased(parse.tree()),
1232 #[cfg(feature = "deb822")]
1233 ParseInner::Deb822(parse) => {
1234 let deb822 = parse.tree();
1235 ParsedWatchFile::Deb822(crate::deb822::WatchFile::from_deb822(deb822))
1236 }
1237 }
1238 }
1239
1240 pub fn version(&self) -> u32 {
1242 match &self.inner {
1243 #[cfg(feature = "linebased")]
1244 ParseInner::LineBased(parse) => parse.tree().version(),
1245 #[cfg(feature = "deb822")]
1246 ParseInner::Deb822(_) => 5,
1247 }
1248 }
1249}
1250
1251unsafe impl Send for Parse {}
1254unsafe impl Sync for Parse {}