1use crate::lex::lex;
2use crate::types::*;
3use crate::SyntaxKind;
4use crate::SyntaxKind::*;
5use crate::DEFAULT_VERSION;
6use std::str::FromStr;
7
8#[derive(Debug, Clone, PartialEq, Eq, Hash)]
9pub struct ParseError(Vec<String>);
10
11impl std::fmt::Display for ParseError {
12 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
13 for err in &self.0 {
14 writeln!(f, "{}", err)?;
15 }
16 Ok(())
17 }
18}
19
20impl std::error::Error for ParseError {}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
26enum Lang {}
27impl rowan::Language for Lang {
28 type Kind = SyntaxKind;
29 fn kind_from_raw(raw: rowan::SyntaxKind) -> Self::Kind {
30 unsafe { std::mem::transmute::<u16, SyntaxKind>(raw.0) }
31 }
32 fn kind_to_raw(kind: Self::Kind) -> rowan::SyntaxKind {
33 kind.into()
34 }
35}
36
37use rowan::GreenNode;
40
41use rowan::GreenNodeBuilder;
45
46struct Parse {
49 green_node: GreenNode,
50 #[allow(unused)]
51 errors: Vec<String>,
52 #[allow(unused)]
53 version: i32,
54}
55
56fn parse(text: &str) -> Parse {
57 struct Parser {
58 tokens: Vec<(SyntaxKind, String)>,
61 builder: GreenNodeBuilder<'static>,
63 errors: Vec<String>,
66 }
67
68 impl Parser {
69 fn parse_version(&mut self) -> Option<i32> {
70 let mut version = None;
71 if self.tokens.last() == Some(&(KEY, "version".to_string())) {
72 self.builder.start_node(VERSION.into());
73 self.bump();
74 self.skip_ws();
75 if self.current() != Some(EQUALS) {
76 self.builder.start_node(ERROR.into());
77 self.errors.push("expected `=`".to_string());
78 self.bump();
79 self.builder.finish_node();
80 } else {
81 self.bump();
82 }
83 if self.current() != Some(VALUE) {
84 self.builder.start_node(ERROR.into());
85 self.errors
86 .push(format!("expected value, got {:?}", self.current()));
87 self.bump();
88 self.builder.finish_node();
89 } else {
90 let version_str = self.tokens.last().unwrap().1.clone();
91 match version_str.parse() {
92 Ok(v) => {
93 version = Some(v);
94 self.bump();
95 }
96 Err(_) => {
97 self.builder.start_node(ERROR.into());
98 self.errors
99 .push(format!("invalid version: {}", version_str));
100 self.bump();
101 self.builder.finish_node();
102 }
103 }
104 }
105 if self.current() != Some(NEWLINE) {
106 self.builder.start_node(ERROR.into());
107 self.errors.push("expected newline".to_string());
108 self.bump();
109 self.builder.finish_node();
110 } else {
111 self.bump();
112 }
113 self.builder.finish_node();
114 }
115 version
116 }
117
118 fn parse_watch_entry(&mut self) -> bool {
119 self.skip_ws();
120 if self.current().is_none() {
121 return false;
122 }
123 if self.current() == Some(NEWLINE) {
124 self.bump();
125 return false;
126 }
127 self.builder.start_node(ENTRY.into());
128 self.parse_options_list();
129 for i in 0..4 {
130 if self.current() == Some(NEWLINE) {
131 break;
132 }
133 if self.current() == Some(CONTINUATION) {
134 self.bump();
135 self.skip_ws();
136 continue;
137 }
138 if self.current() != Some(VALUE) && self.current() != Some(KEY) {
139 self.builder.start_node(ERROR.into());
140 self.errors.push(format!(
141 "expected value, got {:?} (i={})",
142 self.current(),
143 i
144 ));
145 if self.current().is_some() {
146 self.bump();
147 }
148 self.builder.finish_node();
149 } else {
150 self.bump();
151 }
152 self.skip_ws();
153 }
154 if self.current() != Some(NEWLINE) && self.current().is_some() {
155 self.builder.start_node(ERROR.into());
156 self.errors
157 .push(format!("expected newline, not {:?}", self.current()));
158 if self.current().is_some() {
159 self.bump();
160 }
161 self.builder.finish_node();
162 } else {
163 self.bump();
164 }
165 self.builder.finish_node();
166 true
167 }
168
169 fn parse_option(&mut self) -> bool {
170 if self.current().is_none() {
171 return false;
172 }
173 while self.current() == Some(CONTINUATION) {
174 self.bump();
175 }
176 if self.current() == Some(WHITESPACE) {
177 return false;
178 }
179 self.builder.start_node(OPTION.into());
180 if self.current() != Some(KEY) {
181 self.builder.start_node(ERROR.into());
182 self.errors.push("expected key".to_string());
183 self.bump();
184 self.builder.finish_node();
185 } else {
186 self.bump();
187 }
188 if self.current() == Some(EQUALS) {
189 self.bump();
190 if self.current() != Some(VALUE) && self.current() != Some(KEY) {
191 self.builder.start_node(ERROR.into());
192 self.errors
193 .push(format!("expected value, got {:?}", self.current()));
194 self.bump();
195 self.builder.finish_node();
196 } else {
197 self.bump();
198 }
199 } else if self.current() == Some(COMMA) {
200 } else {
201 self.builder.start_node(ERROR.into());
202 self.errors.push("expected `=`".to_string());
203 if self.current().is_some() {
204 self.bump();
205 }
206 self.builder.finish_node();
207 }
208 self.builder.finish_node();
209 true
210 }
211
212 fn parse_options_list(&mut self) {
213 self.skip_ws();
214 if self.tokens.last() == Some(&(KEY, "opts".to_string()))
215 || self.tokens.last() == Some(&(KEY, "options".to_string()))
216 {
217 self.builder.start_node(OPTS_LIST.into());
218 self.bump();
219 self.skip_ws();
220 if self.current() != Some(EQUALS) {
221 self.builder.start_node(ERROR.into());
222 self.errors.push("expected `=`".to_string());
223 if self.current().is_some() {
224 self.bump();
225 }
226 self.builder.finish_node();
227 } else {
228 self.bump();
229 }
230 let quoted = if self.current() == Some(QUOTE) {
231 self.bump();
232 true
233 } else {
234 false
235 };
236 loop {
237 if quoted {
238 if self.current() == Some(QUOTE) {
239 self.bump();
240 break;
241 }
242 self.skip_ws();
243 }
244 if !self.parse_option() {
245 break;
246 }
247 if self.current() == Some(COMMA) {
248 self.bump();
249 } else if !quoted {
250 break;
251 }
252 }
253 self.builder.finish_node();
254 self.skip_ws();
255 }
256 }
257
258 fn parse(mut self) -> Parse {
259 let mut version = 1;
260 self.builder.start_node(ROOT.into());
262 if let Some(v) = self.parse_version() {
263 version = v;
264 }
265 loop {
267 if !self.parse_watch_entry() {
268 break;
269 }
270 }
271 self.skip_ws();
273 self.builder.finish_node();
275
276 Parse {
278 green_node: self.builder.finish(),
279 errors: self.errors,
280 version,
281 }
282 }
283 fn bump(&mut self) {
285 let (kind, text) = self.tokens.pop().unwrap();
286 self.builder.token(kind.into(), text.as_str());
287 }
288 fn current(&self) -> Option<SyntaxKind> {
290 self.tokens.last().map(|(kind, _)| *kind)
291 }
292 fn skip_ws(&mut self) {
293 while self.current() == Some(WHITESPACE)
294 || self.current() == Some(CONTINUATION)
295 || self.current() == Some(COMMENT)
296 {
297 self.bump()
298 }
299 }
300 }
301
302 let mut tokens = lex(text);
303 tokens.reverse();
304 Parser {
305 tokens,
306 builder: GreenNodeBuilder::new(),
307 errors: Vec::new(),
308 }
309 .parse()
310}
311
312type SyntaxNode = rowan::SyntaxNode<Lang>;
319#[allow(unused)]
320type SyntaxToken = rowan::SyntaxToken<Lang>;
321#[allow(unused)]
322type SyntaxElement = rowan::NodeOrToken<SyntaxNode, SyntaxToken>;
323
324impl Parse {
325 fn syntax(&self) -> SyntaxNode {
326 SyntaxNode::new_root(self.green_node.clone())
327 }
328
329 fn root(&self) -> WatchFile {
330 WatchFile::cast(self.syntax()).unwrap()
331 }
332}
333
334macro_rules! ast_node {
335 ($ast:ident, $kind:ident) => {
336 #[derive(PartialEq, Eq, Hash)]
337 #[repr(transparent)]
338 pub struct $ast(SyntaxNode);
340 impl $ast {
341 #[allow(unused)]
342 fn cast(node: SyntaxNode) -> Option<Self> {
343 if node.kind() == $kind {
344 Some(Self(node))
345 } else {
346 None
347 }
348 }
349 }
350
351 impl ToString for $ast {
352 fn to_string(&self) -> String {
353 self.0.text().to_string()
354 }
355 }
356 };
357}
358
359ast_node!(WatchFile, ROOT);
360ast_node!(Version, VERSION);
361ast_node!(Entry, ENTRY);
362ast_node!(OptionList, OPTS_LIST);
363ast_node!(_Option, OPTION);
364
365impl WatchFile {
366 pub fn new(version: Option<u32>) -> WatchFile {
368 let mut builder = GreenNodeBuilder::new();
369
370 builder.start_node(ROOT.into());
371 if let Some(version) = version {
372 builder.start_node(VERSION.into());
373 builder.token(KEY.into(), "version");
374 builder.token(EQUALS.into(), "=");
375 builder.token(VALUE.into(), version.to_string().as_str());
376 builder.token(NEWLINE.into(), "\n");
377 builder.finish_node();
378 }
379 builder.finish_node();
380 WatchFile(SyntaxNode::new_root_mut(builder.finish()))
381 }
382
383 pub fn version(&self) -> u32 {
385 self.0
386 .children()
387 .find_map(Version::cast)
388 .map(|it| it.version())
389 .unwrap_or(DEFAULT_VERSION)
390 }
391
392 pub fn entries(&self) -> impl Iterator<Item = Entry> + '_ {
394 self.0.children().filter_map(Entry::cast)
395 }
396
397 pub fn set_version(&mut self, new_version: u32) {
399 let mut builder = GreenNodeBuilder::new();
401 builder.start_node(VERSION.into());
402 builder.token(KEY.into(), "version");
403 builder.token(EQUALS.into(), "=");
404 builder.token(VALUE.into(), new_version.to_string().as_str());
405 builder.token(NEWLINE.into(), "\n");
406 builder.finish_node();
407 let new_version_green = builder.finish();
408
409 let new_version_node = SyntaxNode::new_root_mut(new_version_green);
411
412 let version_pos = self.0.children().position(|child| child.kind() == VERSION);
414
415 if let Some(pos) = version_pos {
416 self.0
418 .splice_children(pos..pos + 1, vec![new_version_node.into()]);
419 } else {
420 self.0.splice_children(0..0, vec![new_version_node.into()]);
422 }
423 }
424}
425
426impl FromStr for WatchFile {
427 type Err = ParseError;
428
429 fn from_str(s: &str) -> Result<Self, Self::Err> {
430 let parsed = parse(s);
431 if parsed.errors.is_empty() {
432 Ok(parsed.root())
433 } else {
434 Err(ParseError(parsed.errors))
435 }
436 }
437}
438
439impl Version {
440 pub fn version(&self) -> u32 {
442 self.0
443 .children_with_tokens()
444 .find_map(|it| match it {
445 SyntaxElement::Token(token) => {
446 if token.kind() == VALUE {
447 Some(token.text().parse().unwrap())
448 } else {
449 None
450 }
451 }
452 _ => None,
453 })
454 .unwrap_or(DEFAULT_VERSION)
455 }
456}
457
458impl Entry {
459 pub fn option_list(&self) -> Option<OptionList> {
461 self.0.children().find_map(OptionList::cast)
462 }
463
464 pub fn get_option(&self, key: &str) -> Option<String> {
466 self.option_list().and_then(|ol| ol.get_option(key))
467 }
468
469 pub fn has_option(&self, key: &str) -> bool {
471 self.option_list().map_or(false, |ol| ol.has_option(key))
472 }
473
474 pub fn component(&self) -> Option<String> {
476 self.get_option("component")
477 }
478
479 pub fn ctype(&self) -> Result<Option<ComponentType>, ()> {
481 self.get_option("ctype").map(|s| s.parse()).transpose()
482 }
483
484 pub fn compression(&self) -> Result<Option<Compression>, ()> {
486 self.get_option("compression")
487 .map(|s| s.parse())
488 .transpose()
489 }
490
491 pub fn repack(&self) -> bool {
493 self.has_option("repack")
494 }
495
496 pub fn repacksuffix(&self) -> Option<String> {
498 self.get_option("repacksuffix")
499 }
500
501 pub fn mode(&self) -> Result<Mode, ()> {
503 Ok(self
504 .get_option("mode")
505 .map(|s| s.parse())
506 .transpose()?
507 .unwrap_or_default())
508 }
509
510 pub fn pretty(&self) -> Result<Pretty, ()> {
512 Ok(self
513 .get_option("pretty")
514 .map(|s| s.parse())
515 .transpose()?
516 .unwrap_or_default())
517 }
518
519 pub fn date(&self) -> String {
522 self.get_option("date")
523 .unwrap_or_else(|| "%Y%m%d".to_string())
524 }
525
526 pub fn gitexport(&self) -> Result<GitExport, ()> {
528 Ok(self
529 .get_option("gitexport")
530 .map(|s| s.parse())
531 .transpose()?
532 .unwrap_or_default())
533 }
534
535 pub fn gitmode(&self) -> Result<GitMode, ()> {
537 Ok(self
538 .get_option("gitmode")
539 .map(|s| s.parse())
540 .transpose()?
541 .unwrap_or_default())
542 }
543
544 pub fn pgpmode(&self) -> Result<PgpMode, ()> {
546 Ok(self
547 .get_option("pgpmode")
548 .map(|s| s.parse())
549 .transpose()?
550 .unwrap_or_default())
551 }
552
553 pub fn searchmode(&self) -> Result<SearchMode, ()> {
555 Ok(self
556 .get_option("searchmode")
557 .map(|s| s.parse())
558 .transpose()?
559 .unwrap_or_default())
560 }
561
562 pub fn decompress(&self) -> bool {
564 self.has_option("decompress")
565 }
566
567 pub fn bare(&self) -> bool {
570 self.has_option("bare")
571 }
572
573 pub fn user_agent(&self) -> Option<String> {
575 self.get_option("user-agent")
576 }
577
578 pub fn passive(&self) -> Option<bool> {
580 if self.has_option("passive") || self.has_option("pasv") {
581 Some(true)
582 } else if self.has_option("active") || self.has_option("nopasv") {
583 Some(false)
584 } else {
585 None
586 }
587 }
588
589 pub fn unzipoptions(&self) -> Option<String> {
592 self.get_option("unzipopt")
593 }
594
595 pub fn dversionmangle(&self) -> Option<String> {
597 self.get_option("dversionmangle")
598 .or_else(|| self.get_option("versionmangle"))
599 }
600
601 pub fn dirversionmangle(&self) -> Option<String> {
605 self.get_option("dirversionmangle")
606 }
607
608 pub fn pagemangle(&self) -> Option<String> {
610 self.get_option("pagemangle")
611 }
612
613 pub fn uversionmangle(&self) -> Option<String> {
617 self.get_option("uversionmangle")
618 .or_else(|| self.get_option("versionmangle"))
619 }
620
621 pub fn versionmangle(&self) -> Option<String> {
623 self.get_option("versionmangle")
624 }
625
626 pub fn hrefdecode(&self) -> bool {
631 self.get_option("hrefdecode").is_some()
632 }
633
634 pub fn downloadurlmangle(&self) -> Option<String> {
637 self.get_option("downloadurlmangle")
638 }
639
640 pub fn filenamemangle(&self) -> Option<String> {
648 self.get_option("filenamemangle")
649 }
650
651 pub fn pgpsigurlmangle(&self) -> Option<String> {
653 self.get_option("pgpsigurlmangle")
654 }
655
656 pub fn oversionmangle(&self) -> Option<String> {
659 self.get_option("oversionmangle")
660 }
661
662 pub fn opts(&self) -> std::collections::HashMap<String, String> {
664 let mut options = std::collections::HashMap::new();
665
666 if let Some(ol) = self.option_list() {
667 for opt in ol.children() {
668 let key = opt.key();
669 let value = opt.value();
670 if let (Some(key), Some(value)) = (key, value) {
671 options.insert(key.to_string(), value.to_string());
672 }
673 }
674 }
675
676 options
677 }
678
679 fn items(&self) -> impl Iterator<Item = String> + '_ {
680 self.0.children_with_tokens().filter_map(|it| match it {
681 SyntaxElement::Token(token) => {
682 if token.kind() == VALUE || token.kind() == KEY {
683 Some(token.text().to_string())
684 } else {
685 None
686 }
687 }
688 _ => None,
689 })
690 }
691
692 pub fn url(&self) -> String {
694 self.items().next().unwrap()
695 }
696
697 pub fn matching_pattern(&self) -> Option<String> {
699 self.items().nth(1)
700 }
701
702 pub fn version(&self) -> Result<Option<crate::VersionPolicy>, String> {
704 self.items().nth(2).map(|it| it.parse()).transpose()
705 }
706
707 pub fn script(&self) -> Option<String> {
709 self.items().nth(3)
710 }
711
712 pub fn format_url(&self, package: impl FnOnce() -> String) -> url::Url {
714 subst(self.url().as_str(), package).parse().unwrap()
715 }
716}
717
718const SUBSTITUTIONS: &[(&str, &str)] = &[
719 ("@ANY_VERSION@", r"[-_]?(\d[\-+\.:\~\da-zA-Z]*)"),
724 (
727 "@ARCHIVE_EXT@",
728 r"(?i)\.(?:tar\.xz|tar\.bz2|tar\.gz|zip|tgz|tbz|txz)",
729 ),
730 (
733 "@SIGNATURE_EXT@",
734 r"(?i)\.(?:tar\.xz|tar\.bz2|tar\.gz|zip|tgz|tbz|txz)\.(?:asc|pgp|gpg|sig|sign)",
735 ),
736 ("@DEB_EXT@", r"[\+~](debian|dfsg|ds|deb)(\.)?(\d+)?$"),
738];
739
740pub fn subst(text: &str, package: impl FnOnce() -> String) -> String {
741 let mut substs = SUBSTITUTIONS.to_vec();
742 let package_name;
743 if text.contains("@PACKAGE@") {
744 package_name = Some(package());
745 substs.push(("@PACKAGE@", package_name.as_deref().unwrap()));
746 }
747
748 let mut text = text.to_string();
749
750 for (k, v) in substs {
751 text = text.replace(k, v);
752 }
753
754 text
755}
756
757#[test]
758fn test_subst() {
759 assert_eq!(
760 subst("@ANY_VERSION@", || unreachable!()),
761 r"[-_]?(\d[\-+\.:\~\da-zA-Z]*)"
762 );
763 assert_eq!(subst("@PACKAGE@", || "dulwich".to_string()), "dulwich");
764}
765
766impl OptionList {
767 fn children(&self) -> impl Iterator<Item = _Option> + '_ {
768 self.0.children().filter_map(_Option::cast)
769 }
770
771 pub fn has_option(&self, key: &str) -> bool {
772 self.children().any(|it| it.key().as_deref() == Some(key))
773 }
774
775 pub fn get_option(&self, key: &str) -> Option<String> {
776 for child in self.children() {
777 if child.key().as_deref() == Some(key) {
778 return child.value();
779 }
780 }
781 None
782 }
783}
784
785impl _Option {
786 pub fn key(&self) -> Option<String> {
788 self.0.children_with_tokens().find_map(|it| match it {
789 SyntaxElement::Token(token) => {
790 if token.kind() == KEY {
791 Some(token.text().to_string())
792 } else {
793 None
794 }
795 }
796 _ => None,
797 })
798 }
799
800 pub fn value(&self) -> Option<String> {
802 self.0
803 .children_with_tokens()
804 .filter_map(|it| match it {
805 SyntaxElement::Token(token) => {
806 if token.kind() == VALUE || token.kind() == KEY {
807 Some(token.text().to_string())
808 } else {
809 None
810 }
811 }
812 _ => None,
813 })
814 .nth(1)
815 }
816}
817
818#[test]
819fn test_parse_v1() {
820 const WATCHV1: &str = r#"version=4
821opts=bare,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1\.tar\.gz/ \
822 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
823"#;
824 let parsed = parse(WATCHV1);
825 let node = parsed.syntax();
827 assert_eq!(
828 format!("{:#?}", node),
829 r#"ROOT@0..161
830 VERSION@0..10
831 KEY@0..7 "version"
832 EQUALS@7..8 "="
833 VALUE@8..9 "4"
834 NEWLINE@9..10 "\n"
835 ENTRY@10..161
836 OPTS_LIST@10..86
837 KEY@10..14 "opts"
838 EQUALS@14..15 "="
839 OPTION@15..19
840 KEY@15..19 "bare"
841 COMMA@19..20 ","
842 OPTION@20..86
843 KEY@20..34 "filenamemangle"
844 EQUALS@34..35 "="
845 VALUE@35..86 "s/.+\\/v?(\\d\\S+)\\.tar\\ ..."
846 WHITESPACE@86..87 " "
847 CONTINUATION@87..89 "\\\n"
848 WHITESPACE@89..91 " "
849 VALUE@91..138 "https://github.com/sy ..."
850 WHITESPACE@138..139 " "
851 VALUE@139..160 ".*/v?(\\d\\S+)\\.tar\\.gz"
852 NEWLINE@160..161 "\n"
853"#
854 );
855
856 let root = parsed.root();
857 assert_eq!(root.version(), 4);
858 let entries = root.entries().collect::<Vec<_>>();
859 assert_eq!(entries.len(), 1);
860 let entry = &entries[0];
861 assert_eq!(
862 entry.url(),
863 "https://github.com/syncthing/syncthing-gtk/tags"
864 );
865 assert_eq!(
866 entry.matching_pattern(),
867 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
868 );
869 assert_eq!(entry.version(), Ok(None));
870 assert_eq!(entry.script(), None);
871
872 assert_eq!(node.text(), WATCHV1);
873}
874
875#[test]
876fn test_parse_v2() {
877 let parsed = parse(
878 r#"version=4
879https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
880# comment
881"#,
882 );
883 assert_eq!(parsed.errors, Vec::<String>::new());
884 let node = parsed.syntax();
885 assert_eq!(
886 format!("{:#?}", node),
887 r###"ROOT@0..90
888 VERSION@0..10
889 KEY@0..7 "version"
890 EQUALS@7..8 "="
891 VALUE@8..9 "4"
892 NEWLINE@9..10 "\n"
893 ENTRY@10..80
894 VALUE@10..57 "https://github.com/sy ..."
895 WHITESPACE@57..58 " "
896 VALUE@58..79 ".*/v?(\\d\\S+)\\.tar\\.gz"
897 NEWLINE@79..80 "\n"
898 COMMENT@80..89 "# comment"
899 NEWLINE@89..90 "\n"
900"###
901 );
902
903 let root = parsed.root();
904 assert_eq!(root.version(), 4);
905 let entries = root.entries().collect::<Vec<_>>();
906 assert_eq!(entries.len(), 1);
907 let entry = &entries[0];
908 assert_eq!(
909 entry.url(),
910 "https://github.com/syncthing/syncthing-gtk/tags"
911 );
912 assert_eq!(
913 entry.format_url(|| "syncthing-gtk".to_string()),
914 "https://github.com/syncthing/syncthing-gtk/tags"
915 .parse()
916 .unwrap()
917 );
918}
919
920#[test]
921fn test_parse_v3() {
922 let parsed = parse(
923 r#"version=4
924https://github.com/syncthing/@PACKAGE@/tags .*/v?(\d\S+)\.tar\.gz
925# comment
926"#,
927 );
928 assert_eq!(parsed.errors, Vec::<String>::new());
929 let root = parsed.root();
930 assert_eq!(root.version(), 4);
931 let entries = root.entries().collect::<Vec<_>>();
932 assert_eq!(entries.len(), 1);
933 let entry = &entries[0];
934 assert_eq!(entry.url(), "https://github.com/syncthing/@PACKAGE@/tags");
935 assert_eq!(
936 entry.format_url(|| "syncthing-gtk".to_string()),
937 "https://github.com/syncthing/syncthing-gtk/tags"
938 .parse()
939 .unwrap()
940 );
941}
942
943#[test]
944fn test_parse_v4() {
945 let cl: super::WatchFile = r#"version=4
946opts=repack,compression=xz,dversionmangle=s/\+ds//,repacksuffix=+ds \
947 https://github.com/example/example-cat/tags \
948 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
949"#
950 .parse()
951 .unwrap();
952 assert_eq!(cl.version(), 4);
953 let entries = cl.entries().collect::<Vec<_>>();
954 assert_eq!(entries.len(), 1);
955 let entry = &entries[0];
956 assert_eq!(entry.url(), "https://github.com/example/example-cat/tags");
957 assert_eq!(
958 entry.matching_pattern(),
959 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
960 );
961 assert!(entry.repack());
962 assert_eq!(entry.compression(), Ok(Some(Compression::Xz)));
963 assert_eq!(entry.dversionmangle(), Some("s/\\+ds//".into()));
964 assert_eq!(entry.repacksuffix(), Some("+ds".into()));
965 assert_eq!(entry.script(), Some("uupdate".into()));
966 assert_eq!(
967 entry.format_url(|| "example-cat".to_string()),
968 "https://github.com/example/example-cat/tags"
969 .parse()
970 .unwrap()
971 );
972 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
973}
974
975#[test]
976fn test_git_mode() {
977 let text = r#"version=3
978opts="mode=git, gitmode=shallow, pgpmode=gittag" \
979https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git \
980refs/tags/(.*) debian
981"#;
982 let parsed = parse(text);
983 assert_eq!(parsed.errors, Vec::<String>::new());
984 let cl = parsed.root();
985 assert_eq!(cl.version(), 3);
986 let entries = cl.entries().collect::<Vec<_>>();
987 assert_eq!(entries.len(), 1);
988 let entry = &entries[0];
989 assert_eq!(
990 entry.url(),
991 "https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git"
992 );
993 assert_eq!(entry.matching_pattern(), Some("refs/tags/(.*)".into()));
994 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
995 assert_eq!(entry.script(), None);
996 assert_eq!(entry.gitmode(), Ok(GitMode::Shallow));
997 assert_eq!(entry.pgpmode(), Ok(PgpMode::GitTag));
998 assert_eq!(entry.mode(), Ok(Mode::Git));
999}
1000
1001#[test]
1002fn test_parse_quoted() {
1003 const WATCHV1: &str = r#"version=4
1004opts="bare, filenamemangle=blah" \
1005 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
1006"#;
1007 let parsed = parse(WATCHV1);
1008 let node = parsed.syntax();
1010
1011 let root = parsed.root();
1012 assert_eq!(root.version(), 4);
1013 let entries = root.entries().collect::<Vec<_>>();
1014 assert_eq!(entries.len(), 1);
1015 let entry = &entries[0];
1016
1017 assert_eq!(
1018 entry.url(),
1019 "https://github.com/syncthing/syncthing-gtk/tags"
1020 );
1021 assert_eq!(
1022 entry.matching_pattern(),
1023 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
1024 );
1025 assert_eq!(entry.version(), Ok(None));
1026 assert_eq!(entry.script(), None);
1027
1028 assert_eq!(node.text(), WATCHV1);
1029}