1use crate::lex::lex;
2use crate::types::*;
3use crate::SyntaxKind;
4use crate::SyntaxKind::*;
5use crate::DEFAULT_VERSION;
6use std::marker::PhantomData;
7use std::str::FromStr;
8
9#[derive(Debug, Clone, PartialEq, Eq, Hash)]
10pub struct ParseError(Vec<String>);
11
12impl std::fmt::Display for ParseError {
13 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
14 for err in &self.0 {
15 writeln!(f, "{}", err)?;
16 }
17 Ok(())
18 }
19}
20
21impl std::error::Error for ParseError {}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
27pub(crate) enum Lang {}
28impl rowan::Language for Lang {
29 type Kind = SyntaxKind;
30 fn kind_from_raw(raw: rowan::SyntaxKind) -> Self::Kind {
31 unsafe { std::mem::transmute::<u16, SyntaxKind>(raw.0) }
32 }
33 fn kind_to_raw(kind: Self::Kind) -> rowan::SyntaxKind {
34 kind.into()
35 }
36}
37
38use rowan::GreenNode;
41
42use rowan::GreenNodeBuilder;
46
47#[derive(Debug, Clone, PartialEq, Eq)]
50pub struct Parse<T> {
51 green: GreenNode,
53 errors: Vec<String>,
55 _ty: PhantomData<T>,
57}
58
59impl<T> Parse<T> {
60 pub(crate) fn new(green: GreenNode, errors: Vec<String>) -> Self {
62 Parse {
63 green,
64 errors,
65 _ty: PhantomData,
66 }
67 }
68
69 pub fn green(&self) -> &GreenNode {
71 &self.green
72 }
73
74 pub fn errors(&self) -> &[String] {
76 &self.errors
77 }
78
79 pub fn is_ok(&self) -> bool {
81 self.errors.is_empty()
82 }
83}
84
85impl Parse<WatchFile> {
86 pub fn tree(&self) -> WatchFile {
88 WatchFile::cast(SyntaxNode::new_root(self.green.clone()))
89 .expect("root node should be a WatchFile")
90 }
91}
92
93struct InternalParse {
95 green_node: GreenNode,
96 errors: Vec<String>,
97}
98
99fn parse(text: &str) -> InternalParse {
100 struct Parser {
101 tokens: Vec<(SyntaxKind, String)>,
104 builder: GreenNodeBuilder<'static>,
106 errors: Vec<String>,
109 }
110
111 impl Parser {
112 fn parse_version(&mut self) -> Option<u32> {
113 let mut version = None;
114 if self.tokens.last() == Some(&(KEY, "version".to_string())) {
115 self.builder.start_node(VERSION.into());
116 self.bump();
117 self.skip_ws();
118 if self.current() != Some(EQUALS) {
119 self.builder.start_node(ERROR.into());
120 self.errors.push("expected `=`".to_string());
121 self.bump();
122 self.builder.finish_node();
123 } else {
124 self.bump();
125 }
126 if self.current() != Some(VALUE) {
127 self.builder.start_node(ERROR.into());
128 self.errors
129 .push(format!("expected value, got {:?}", self.current()));
130 self.bump();
131 self.builder.finish_node();
132 } else if let Some((_, value)) = self.tokens.last() {
133 let version_str = value;
134 match version_str.parse() {
135 Ok(v) => {
136 version = Some(v);
137 self.bump();
138 }
139 Err(_) => {
140 self.builder.start_node(ERROR.into());
141 self.errors
142 .push(format!("invalid version: {}", version_str));
143 self.bump();
144 self.builder.finish_node();
145 }
146 }
147 } else {
148 self.builder.start_node(ERROR.into());
149 self.errors.push("expected version value".to_string());
150 self.builder.finish_node();
151 }
152 if self.current() != Some(NEWLINE) {
153 self.builder.start_node(ERROR.into());
154 self.errors.push("expected newline".to_string());
155 self.bump();
156 self.builder.finish_node();
157 } else {
158 self.bump();
159 }
160 self.builder.finish_node();
161 }
162 version
163 }
164
165 fn parse_watch_entry(&mut self) -> bool {
166 self.skip_ws();
167 if self.current().is_none() {
168 return false;
169 }
170 if self.current() == Some(NEWLINE) {
171 self.bump();
172 return false;
173 }
174 self.builder.start_node(ENTRY.into());
175 self.parse_options_list();
176 for i in 0..4 {
177 if self.current() == Some(NEWLINE) {
178 break;
179 }
180 if self.current() == Some(CONTINUATION) {
181 self.bump();
182 self.skip_ws();
183 continue;
184 }
185 if self.current() != Some(VALUE) && self.current() != Some(KEY) {
186 self.builder.start_node(ERROR.into());
187 self.errors.push(format!(
188 "expected value, got {:?} (i={})",
189 self.current(),
190 i
191 ));
192 if self.current().is_some() {
193 self.bump();
194 }
195 self.builder.finish_node();
196 } else {
197 self.bump();
198 }
199 self.skip_ws();
200 }
201 if self.current() != Some(NEWLINE) && self.current().is_some() {
202 self.builder.start_node(ERROR.into());
203 self.errors
204 .push(format!("expected newline, not {:?}", self.current()));
205 if self.current().is_some() {
206 self.bump();
207 }
208 self.builder.finish_node();
209 } else {
210 self.bump();
211 }
212 self.builder.finish_node();
213 true
214 }
215
216 fn parse_option(&mut self) -> bool {
217 if self.current().is_none() {
218 return false;
219 }
220 while self.current() == Some(CONTINUATION) {
221 self.bump();
222 }
223 if self.current() == Some(WHITESPACE) {
224 return false;
225 }
226 self.builder.start_node(OPTION.into());
227 if self.current() != Some(KEY) {
228 self.builder.start_node(ERROR.into());
229 self.errors.push("expected key".to_string());
230 self.bump();
231 self.builder.finish_node();
232 } else {
233 self.bump();
234 }
235 if self.current() == Some(EQUALS) {
236 self.bump();
237 if self.current() != Some(VALUE) && self.current() != Some(KEY) {
238 self.builder.start_node(ERROR.into());
239 self.errors
240 .push(format!("expected value, got {:?}", self.current()));
241 self.bump();
242 self.builder.finish_node();
243 } else {
244 self.bump();
245 }
246 } else if self.current() == Some(COMMA) {
247 } else {
248 self.builder.start_node(ERROR.into());
249 self.errors.push("expected `=`".to_string());
250 if self.current().is_some() {
251 self.bump();
252 }
253 self.builder.finish_node();
254 }
255 self.builder.finish_node();
256 true
257 }
258
259 fn parse_options_list(&mut self) {
260 self.skip_ws();
261 if self.tokens.last() == Some(&(KEY, "opts".to_string()))
262 || self.tokens.last() == Some(&(KEY, "options".to_string()))
263 {
264 self.builder.start_node(OPTS_LIST.into());
265 self.bump();
266 self.skip_ws();
267 if self.current() != Some(EQUALS) {
268 self.builder.start_node(ERROR.into());
269 self.errors.push("expected `=`".to_string());
270 if self.current().is_some() {
271 self.bump();
272 }
273 self.builder.finish_node();
274 } else {
275 self.bump();
276 }
277 let quoted = if self.current() == Some(QUOTE) {
278 self.bump();
279 true
280 } else {
281 false
282 };
283 loop {
284 if quoted {
285 if self.current() == Some(QUOTE) {
286 self.bump();
287 break;
288 }
289 self.skip_ws();
290 }
291 if !self.parse_option() {
292 break;
293 }
294 if self.current() == Some(COMMA) {
295 self.bump();
296 } else if !quoted {
297 break;
298 }
299 }
300 self.builder.finish_node();
301 self.skip_ws();
302 }
303 }
304
305 fn parse(mut self) -> InternalParse {
306 self.builder.start_node(ROOT.into());
308 while self.current() == Some(WHITESPACE)
310 || self.current() == Some(CONTINUATION)
311 || self.current() == Some(COMMENT)
312 || self.current() == Some(NEWLINE)
313 {
314 self.bump();
315 }
316 if let Some(_v) = self.parse_version() {
317 }
319 loop {
321 if !self.parse_watch_entry() {
322 break;
323 }
324 }
325 self.skip_ws();
327 self.builder.finish_node();
329
330 InternalParse {
332 green_node: self.builder.finish(),
333 errors: self.errors,
334 }
335 }
336 fn bump(&mut self) {
338 if let Some((kind, text)) = self.tokens.pop() {
339 self.builder.token(kind.into(), text.as_str());
340 }
341 }
342 fn current(&self) -> Option<SyntaxKind> {
344 self.tokens.last().map(|(kind, _)| *kind)
345 }
346 fn skip_ws(&mut self) {
347 while self.current() == Some(WHITESPACE)
348 || self.current() == Some(CONTINUATION)
349 || self.current() == Some(COMMENT)
350 {
351 self.bump()
352 }
353 }
354 }
355
356 let mut tokens = lex(text);
357 tokens.reverse();
358 Parser {
359 tokens,
360 builder: GreenNodeBuilder::new(),
361 errors: Vec::new(),
362 }
363 .parse()
364}
365
366type SyntaxNode = rowan::SyntaxNode<Lang>;
372#[allow(unused)]
373type SyntaxToken = rowan::SyntaxToken<Lang>;
374#[allow(unused)]
375type SyntaxElement = rowan::NodeOrToken<SyntaxNode, SyntaxToken>;
376
377impl InternalParse {
378 fn syntax(&self) -> SyntaxNode {
379 SyntaxNode::new_root(self.green_node.clone())
380 }
381
382 fn root(&self) -> WatchFile {
383 WatchFile::cast(self.syntax()).expect("root node should be a WatchFile")
384 }
385}
386
387macro_rules! ast_node {
388 ($ast:ident, $kind:ident) => {
389 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
390 #[repr(transparent)]
391 pub struct $ast(SyntaxNode);
393 impl $ast {
394 #[allow(unused)]
395 fn cast(node: SyntaxNode) -> Option<Self> {
396 if node.kind() == $kind {
397 Some(Self(node))
398 } else {
399 None
400 }
401 }
402 }
403
404 impl std::fmt::Display for $ast {
405 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
406 write!(f, "{}", self.0.text())
407 }
408 }
409 };
410}
411
412ast_node!(WatchFile, ROOT);
413ast_node!(Version, VERSION);
414ast_node!(Entry, ENTRY);
415ast_node!(OptionList, OPTS_LIST);
416ast_node!(_Option, OPTION);
417
418impl WatchFile {
419 pub(crate) fn syntax(&self) -> &SyntaxNode {
421 &self.0
422 }
423
424 pub fn new(version: Option<u32>) -> WatchFile {
426 let mut builder = GreenNodeBuilder::new();
427
428 builder.start_node(ROOT.into());
429 if let Some(version) = version {
430 builder.start_node(VERSION.into());
431 builder.token(KEY.into(), "version");
432 builder.token(EQUALS.into(), "=");
433 builder.token(VALUE.into(), version.to_string().as_str());
434 builder.token(NEWLINE.into(), "\n");
435 builder.finish_node();
436 }
437 builder.finish_node();
438 WatchFile(SyntaxNode::new_root(builder.finish()))
439 }
440
441 pub fn version(&self) -> u32 {
443 self.0
444 .children()
445 .find_map(Version::cast)
446 .map(|it| it.version())
447 .unwrap_or(DEFAULT_VERSION)
448 }
449
450 pub fn entries(&self) -> impl Iterator<Item = Entry> + '_ {
452 self.0.children().filter_map(Entry::cast)
453 }
454}
455
456impl FromStr for WatchFile {
457 type Err = ParseError;
458
459 fn from_str(s: &str) -> Result<Self, Self::Err> {
460 let parsed = parse(s);
461 if parsed.errors.is_empty() {
462 Ok(parsed.root())
463 } else {
464 Err(ParseError(parsed.errors))
465 }
466 }
467}
468
469pub fn parse_watch_file(text: &str) -> Parse<WatchFile> {
472 let parsed = parse(text);
473 Parse::new(parsed.green_node, parsed.errors)
474}
475
476impl Version {
477 pub fn version(&self) -> u32 {
479 self.0
480 .children_with_tokens()
481 .find_map(|it| match it {
482 SyntaxElement::Token(token) => {
483 if token.kind() == VALUE {
484 token.text().parse().ok()
485 } else {
486 None
487 }
488 }
489 _ => None,
490 })
491 .unwrap_or(DEFAULT_VERSION)
492 }
493}
494
495impl Entry {
496 pub(crate) fn syntax(&self) -> &SyntaxNode {
498 &self.0
499 }
500
501 pub fn option_list(&self) -> Option<OptionList> {
503 self.0.children().find_map(OptionList::cast)
504 }
505
506 pub fn get_option(&self, key: &str) -> Option<String> {
508 self.option_list().and_then(|ol| ol.get_option(key))
509 }
510
511 pub fn has_option(&self, key: &str) -> bool {
513 self.option_list().is_some_and(|ol| ol.has_option(key))
514 }
515
516 pub fn component(&self) -> Option<String> {
518 self.get_option("component")
519 }
520
521 pub fn ctype(&self) -> Result<Option<ComponentType>, crate::types::ParseError> {
523 self.get_option("ctype").map(|s| s.parse()).transpose()
524 }
525
526 pub fn compression(&self) -> Result<Option<Compression>, crate::types::ParseError> {
528 self.get_option("compression")
529 .map(|s| s.parse())
530 .transpose()
531 }
532
533 pub fn repack(&self) -> bool {
535 self.has_option("repack")
536 }
537
538 pub fn repacksuffix(&self) -> Option<String> {
540 self.get_option("repacksuffix")
541 }
542
543 pub fn mode(&self) -> Result<Mode, crate::types::ParseError> {
545 Ok(self
546 .get_option("mode")
547 .map(|s| s.parse())
548 .transpose()?
549 .unwrap_or_default())
550 }
551
552 pub fn pretty(&self) -> Result<Pretty, crate::types::ParseError> {
554 Ok(self
555 .get_option("pretty")
556 .map(|s| s.parse())
557 .transpose()?
558 .unwrap_or_default())
559 }
560
561 pub fn date(&self) -> String {
564 self.get_option("date").unwrap_or_else(|| "%Y%m%d".into())
565 }
566
567 pub fn gitexport(&self) -> Result<GitExport, crate::types::ParseError> {
569 Ok(self
570 .get_option("gitexport")
571 .map(|s| s.parse())
572 .transpose()?
573 .unwrap_or_default())
574 }
575
576 pub fn gitmode(&self) -> Result<GitMode, crate::types::ParseError> {
578 Ok(self
579 .get_option("gitmode")
580 .map(|s| s.parse())
581 .transpose()?
582 .unwrap_or_default())
583 }
584
585 pub fn pgpmode(&self) -> Result<PgpMode, crate::types::ParseError> {
587 Ok(self
588 .get_option("pgpmode")
589 .map(|s| s.parse())
590 .transpose()?
591 .unwrap_or_default())
592 }
593
594 pub fn searchmode(&self) -> Result<SearchMode, crate::types::ParseError> {
596 Ok(self
597 .get_option("searchmode")
598 .map(|s| s.parse())
599 .transpose()?
600 .unwrap_or_default())
601 }
602
603 pub fn decompress(&self) -> bool {
605 self.has_option("decompress")
606 }
607
608 pub fn bare(&self) -> bool {
611 self.has_option("bare")
612 }
613
614 pub fn user_agent(&self) -> Option<String> {
616 self.get_option("user-agent")
617 }
618
619 pub fn passive(&self) -> Option<bool> {
621 if self.has_option("passive") || self.has_option("pasv") {
622 Some(true)
623 } else if self.has_option("active") || self.has_option("nopasv") {
624 Some(false)
625 } else {
626 None
627 }
628 }
629
630 pub fn unzipoptions(&self) -> Option<String> {
633 self.get_option("unzipopt")
634 }
635
636 pub fn dversionmangle(&self) -> Option<String> {
638 self.get_option("dversionmangle")
639 .or_else(|| self.get_option("versionmangle"))
640 }
641
642 pub fn dirversionmangle(&self) -> Option<String> {
646 self.get_option("dirversionmangle")
647 }
648
649 pub fn pagemangle(&self) -> Option<String> {
651 self.get_option("pagemangle")
652 }
653
654 pub fn uversionmangle(&self) -> Option<String> {
658 self.get_option("uversionmangle")
659 .or_else(|| self.get_option("versionmangle"))
660 }
661
662 pub fn versionmangle(&self) -> Option<String> {
664 self.get_option("versionmangle")
665 }
666
667 pub fn hrefdecode(&self) -> bool {
672 self.get_option("hrefdecode").is_some()
673 }
674
675 pub fn downloadurlmangle(&self) -> Option<String> {
678 self.get_option("downloadurlmangle")
679 }
680
681 pub fn filenamemangle(&self) -> Option<String> {
689 self.get_option("filenamemangle")
690 }
691
692 pub fn pgpsigurlmangle(&self) -> Option<String> {
694 self.get_option("pgpsigurlmangle")
695 }
696
697 pub fn oversionmangle(&self) -> Option<String> {
700 self.get_option("oversionmangle")
701 }
702
703 pub fn opts(&self) -> std::collections::HashMap<String, String> {
705 let mut options = std::collections::HashMap::new();
706
707 if let Some(ol) = self.option_list() {
708 for opt in ol.children() {
709 let key = opt.key();
710 let value = opt.value();
711 if let (Some(key), Some(value)) = (key, value) {
712 options.insert(key.to_string(), value.to_string());
713 }
714 }
715 }
716
717 options
718 }
719
720 fn items(&self) -> impl Iterator<Item = String> + '_ {
721 self.0.children_with_tokens().filter_map(|it| match it {
722 SyntaxElement::Token(token) => {
723 if token.kind() == VALUE || token.kind() == KEY {
724 Some(token.text().to_string())
725 } else {
726 None
727 }
728 }
729 _ => None,
730 })
731 }
732
733 pub fn url(&self) -> String {
735 self.items().next().unwrap_or_default()
736 }
737
738 pub fn matching_pattern(&self) -> Option<String> {
740 self.items().nth(1)
741 }
742
743 pub fn version(&self) -> Result<Option<crate::VersionPolicy>, crate::types::ParseError> {
745 self.items().nth(2).map(|it| it.parse()).transpose()
746 }
747
748 pub fn script(&self) -> Option<String> {
750 self.items().nth(3)
751 }
752
753 pub fn format_url(
755 &self,
756 package: impl FnOnce() -> String,
757 ) -> Result<url::Url, url::ParseError> {
758 subst(self.url().as_str(), package).parse()
759 }
760}
761
762const SUBSTITUTIONS: &[(&str, &str)] = &[
763 ("@ANY_VERSION@", r"[-_]?(\d[\-+\.:\~\da-zA-Z]*)"),
768 (
771 "@ARCHIVE_EXT@",
772 r"(?i)\.(?:tar\.xz|tar\.bz2|tar\.gz|zip|tgz|tbz|txz)",
773 ),
774 (
777 "@SIGNATURE_EXT@",
778 r"(?i)\.(?:tar\.xz|tar\.bz2|tar\.gz|zip|tgz|tbz|txz)\.(?:asc|pgp|gpg|sig|sign)",
779 ),
780 ("@DEB_EXT@", r"[\+~](debian|dfsg|ds|deb)(\.)?(\d+)?$"),
782];
783
784pub fn subst(text: &str, package: impl FnOnce() -> String) -> String {
785 if !text.contains('@') {
787 return text.to_string();
788 }
789
790 let result = SUBSTITUTIONS
792 .iter()
793 .fold(text.to_string(), |acc, (pattern, replacement)| {
794 acc.replace(pattern, replacement)
795 });
796
797 if result.contains("@PACKAGE@") {
799 let package_name = package();
800 result.replace("@PACKAGE@", &package_name)
801 } else {
802 result
803 }
804}
805
806#[test]
807fn test_subst() {
808 assert_eq!(
809 subst("@ANY_VERSION@", || unreachable!()),
810 r"[-_]?(\d[\-+\.:\~\da-zA-Z]*)"
811 );
812 assert_eq!(subst("@PACKAGE@", || "dulwich".to_string()), "dulwich");
813}
814
815impl OptionList {
816 fn children(&self) -> impl Iterator<Item = _Option> + '_ {
817 self.0.children().filter_map(_Option::cast)
818 }
819
820 pub fn has_option(&self, key: &str) -> bool {
821 self.children().any(|it| it.key().as_deref() == Some(key))
822 }
823
824 pub fn get_option(&self, key: &str) -> Option<String> {
825 self.children().find_map(|child| {
826 if child.key().as_deref() == Some(key) {
827 child.value()
828 } else {
829 None
830 }
831 })
832 }
833
834 pub(crate) fn options(&self) -> impl Iterator<Item = (String, String)> + '_ {
836 self.children().filter_map(|opt| {
837 if let (Some(key), Some(value)) = (opt.key(), opt.value()) {
838 Some((key, value))
839 } else {
840 None
841 }
842 })
843 }
844}
845
846impl _Option {
847 pub fn key(&self) -> Option<String> {
849 self.0.children_with_tokens().find_map(|it| match it {
850 SyntaxElement::Token(token) => {
851 if token.kind() == KEY {
852 Some(token.text().to_string())
853 } else {
854 None
855 }
856 }
857 _ => None,
858 })
859 }
860
861 pub fn value(&self) -> Option<String> {
863 self.0
864 .children_with_tokens()
865 .filter_map(|it| match it {
866 SyntaxElement::Token(token) => {
867 if token.kind() == VALUE || token.kind() == KEY {
868 Some(token.text().to_string())
869 } else {
870 None
871 }
872 }
873 _ => None,
874 })
875 .nth(1)
876 }
877}
878
879#[test]
880fn test_parse_v1() {
881 const WATCHV1: &str = r#"version=4
882opts=bare,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1\.tar\.gz/ \
883 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
884"#;
885 let parsed = parse(WATCHV1);
886 let node = parsed.syntax();
888 assert_eq!(
889 format!("{:#?}", node),
890 r#"ROOT@0..161
891 VERSION@0..10
892 KEY@0..7 "version"
893 EQUALS@7..8 "="
894 VALUE@8..9 "4"
895 NEWLINE@9..10 "\n"
896 ENTRY@10..161
897 OPTS_LIST@10..86
898 KEY@10..14 "opts"
899 EQUALS@14..15 "="
900 OPTION@15..19
901 KEY@15..19 "bare"
902 COMMA@19..20 ","
903 OPTION@20..86
904 KEY@20..34 "filenamemangle"
905 EQUALS@34..35 "="
906 VALUE@35..86 "s/.+\\/v?(\\d\\S+)\\.tar\\ ..."
907 WHITESPACE@86..87 " "
908 CONTINUATION@87..89 "\\\n"
909 WHITESPACE@89..91 " "
910 VALUE@91..138 "https://github.com/sy ..."
911 WHITESPACE@138..139 " "
912 VALUE@139..160 ".*/v?(\\d\\S+)\\.tar\\.gz"
913 NEWLINE@160..161 "\n"
914"#
915 );
916
917 let root = parsed.root();
918 assert_eq!(root.version(), 4);
919 let entries = root.entries().collect::<Vec<_>>();
920 assert_eq!(entries.len(), 1);
921 let entry = &entries[0];
922 assert_eq!(
923 entry.url(),
924 "https://github.com/syncthing/syncthing-gtk/tags"
925 );
926 assert_eq!(
927 entry.matching_pattern(),
928 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
929 );
930 assert_eq!(entry.version(), Ok(None));
931 assert_eq!(entry.script(), None);
932
933 assert_eq!(node.text(), WATCHV1);
934}
935
936#[test]
937fn test_parse_v2() {
938 let parsed = parse(
939 r#"version=4
940https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
941# comment
942"#,
943 );
944 assert_eq!(parsed.errors, Vec::<String>::new());
945 let node = parsed.syntax();
946 assert_eq!(
947 format!("{:#?}", node),
948 r###"ROOT@0..90
949 VERSION@0..10
950 KEY@0..7 "version"
951 EQUALS@7..8 "="
952 VALUE@8..9 "4"
953 NEWLINE@9..10 "\n"
954 ENTRY@10..80
955 VALUE@10..57 "https://github.com/sy ..."
956 WHITESPACE@57..58 " "
957 VALUE@58..79 ".*/v?(\\d\\S+)\\.tar\\.gz"
958 NEWLINE@79..80 "\n"
959 COMMENT@80..89 "# comment"
960 NEWLINE@89..90 "\n"
961"###
962 );
963
964 let root = parsed.root();
965 assert_eq!(root.version(), 4);
966 let entries = root.entries().collect::<Vec<_>>();
967 assert_eq!(entries.len(), 1);
968 let entry = &entries[0];
969 assert_eq!(
970 entry.url(),
971 "https://github.com/syncthing/syncthing-gtk/tags"
972 );
973 assert_eq!(
974 entry.format_url(|| "syncthing-gtk".to_string()).unwrap(),
975 "https://github.com/syncthing/syncthing-gtk/tags"
976 .parse()
977 .unwrap()
978 );
979}
980
981#[test]
982fn test_parse_v3() {
983 let parsed = parse(
984 r#"version=4
985https://github.com/syncthing/@PACKAGE@/tags .*/v?(\d\S+)\.tar\.gz
986# comment
987"#,
988 );
989 assert_eq!(parsed.errors, Vec::<String>::new());
990 let root = parsed.root();
991 assert_eq!(root.version(), 4);
992 let entries = root.entries().collect::<Vec<_>>();
993 assert_eq!(entries.len(), 1);
994 let entry = &entries[0];
995 assert_eq!(entry.url(), "https://github.com/syncthing/@PACKAGE@/tags");
996 assert_eq!(
997 entry.format_url(|| "syncthing-gtk".to_string()).unwrap(),
998 "https://github.com/syncthing/syncthing-gtk/tags"
999 .parse()
1000 .unwrap()
1001 );
1002}
1003
1004#[test]
1005fn test_thread_safe_parsing() {
1006 let text = r#"version=4
1007https://github.com/example/example/tags example-(.*)\.tar\.gz
1008"#;
1009
1010 let parsed = parse_watch_file(text);
1011 assert!(parsed.is_ok());
1012 assert_eq!(parsed.errors().len(), 0);
1013
1014 let watchfile = parsed.tree();
1016 assert_eq!(watchfile.version(), 4);
1017 let entries: Vec<_> = watchfile.entries().collect();
1018 assert_eq!(entries.len(), 1);
1019}
1020
1021#[test]
1022fn test_parse_clone_and_eq() {
1023 let text = r#"version=4
1024https://github.com/example/example/tags example-(.*)\.tar\.gz
1025"#;
1026
1027 let parsed1 = parse_watch_file(text);
1028 let parsed2 = parsed1.clone();
1029
1030 assert_eq!(parsed1, parsed2);
1032
1033 let watchfile1 = parsed1.tree();
1035 let watchfile2 = watchfile1.clone();
1036 assert_eq!(watchfile1, watchfile2);
1037}
1038
1039#[test]
1040fn test_parse_v4() {
1041 let cl: super::WatchFile = r#"version=4
1042opts=repack,compression=xz,dversionmangle=s/\+ds//,repacksuffix=+ds \
1043 https://github.com/example/example-cat/tags \
1044 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
1045"#
1046 .parse()
1047 .unwrap();
1048 assert_eq!(cl.version(), 4);
1049 let entries = cl.entries().collect::<Vec<_>>();
1050 assert_eq!(entries.len(), 1);
1051 let entry = &entries[0];
1052 assert_eq!(entry.url(), "https://github.com/example/example-cat/tags");
1053 assert_eq!(
1054 entry.matching_pattern(),
1055 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
1056 );
1057 assert!(entry.repack());
1058 assert_eq!(entry.compression(), Ok(Some(Compression::Xz)));
1059 assert_eq!(entry.dversionmangle(), Some("s/\\+ds//".into()));
1060 assert_eq!(entry.repacksuffix(), Some("+ds".into()));
1061 assert_eq!(entry.script(), Some("uupdate".into()));
1062 assert_eq!(
1063 entry.format_url(|| "example-cat".to_string()).unwrap(),
1064 "https://github.com/example/example-cat/tags"
1065 .parse()
1066 .unwrap()
1067 );
1068 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
1069}
1070
1071#[test]
1072fn test_git_mode() {
1073 let text = r#"version=3
1074opts="mode=git, gitmode=shallow, pgpmode=gittag" \
1075https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git \
1076refs/tags/(.*) debian
1077"#;
1078 let parsed = parse(text);
1079 assert_eq!(parsed.errors, Vec::<String>::new());
1080 let cl = parsed.root();
1081 assert_eq!(cl.version(), 3);
1082 let entries = cl.entries().collect::<Vec<_>>();
1083 assert_eq!(entries.len(), 1);
1084 let entry = &entries[0];
1085 assert_eq!(
1086 entry.url(),
1087 "https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git"
1088 );
1089 assert_eq!(entry.matching_pattern(), Some("refs/tags/(.*)".into()));
1090 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
1091 assert_eq!(entry.script(), None);
1092 assert_eq!(entry.gitmode(), Ok(GitMode::Shallow));
1093 assert_eq!(entry.pgpmode(), Ok(PgpMode::GitTag));
1094 assert_eq!(entry.mode(), Ok(Mode::Git));
1095}
1096
1097#[test]
1098fn test_parse_quoted() {
1099 const WATCHV1: &str = r#"version=4
1100opts="bare, filenamemangle=blah" \
1101 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
1102"#;
1103 let parsed = parse(WATCHV1);
1104 let node = parsed.syntax();
1106
1107 let root = parsed.root();
1108 assert_eq!(root.version(), 4);
1109 let entries = root.entries().collect::<Vec<_>>();
1110 assert_eq!(entries.len(), 1);
1111 let entry = &entries[0];
1112
1113 assert_eq!(
1114 entry.url(),
1115 "https://github.com/syncthing/syncthing-gtk/tags"
1116 );
1117 assert_eq!(
1118 entry.matching_pattern(),
1119 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
1120 );
1121 assert_eq!(entry.version(), Ok(None));
1122 assert_eq!(entry.script(), None);
1123
1124 assert_eq!(node.text(), WATCHV1);
1125}
1126
1127impl crate::traits::WatchFileFormat for WatchFile {
1130 type Entry = Entry;
1131
1132 fn version(&self) -> u32 {
1133 self.version()
1134 }
1135
1136 fn entries(&self) -> Box<dyn Iterator<Item = Self::Entry> + '_> {
1137 Box::new(WatchFile::entries(self))
1138 }
1139
1140 fn to_string(&self) -> String {
1141 ToString::to_string(self)
1142 }
1143}
1144
1145impl crate::traits::WatchEntry for Entry {
1146 fn url(&self) -> String {
1147 Entry::url(self)
1148 }
1149
1150 fn matching_pattern(&self) -> Option<String> {
1151 Entry::matching_pattern(self)
1152 }
1153
1154 fn version_policy(&self) -> Result<Option<crate::VersionPolicy>, crate::types::ParseError> {
1155 Entry::version(self)
1156 }
1157
1158 fn script(&self) -> Option<String> {
1159 Entry::script(self)
1160 }
1161
1162 fn get_option(&self, key: &str) -> Option<String> {
1163 Entry::get_option(self, key)
1164 }
1165
1166 fn has_option(&self, key: &str) -> bool {
1167 Entry::has_option(self, key)
1168 }
1169}