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 match i {
152 0 => {
153 self.builder.start_node(URL.into());
155 self.bump();
156 self.builder.finish_node();
157 }
158 1 => {
159 self.builder.start_node(MATCHING_PATTERN.into());
161 self.bump();
162 self.builder.finish_node();
163 }
164 2 => {
165 self.builder.start_node(VERSION_POLICY.into());
167 self.bump();
168 self.builder.finish_node();
169 }
170 3 => {
171 self.builder.start_node(SCRIPT.into());
173 self.bump();
174 self.builder.finish_node();
175 }
176 _ => {
177 self.bump();
178 }
179 }
180 }
181 self.skip_ws();
182 }
183 if self.current() != Some(NEWLINE) && self.current().is_some() {
184 self.builder.start_node(ERROR.into());
185 self.errors
186 .push(format!("expected newline, not {:?}", self.current()));
187 if self.current().is_some() {
188 self.bump();
189 }
190 self.builder.finish_node();
191 } else {
192 self.bump();
193 }
194 self.builder.finish_node();
195 true
196 }
197
198 fn parse_option(&mut self) -> bool {
199 if self.current().is_none() {
200 return false;
201 }
202 while self.current() == Some(CONTINUATION) {
203 self.bump();
204 }
205 if self.current() == Some(WHITESPACE) {
206 return false;
207 }
208 self.builder.start_node(OPTION.into());
209 if self.current() != Some(KEY) {
210 self.builder.start_node(ERROR.into());
211 self.errors.push("expected key".to_string());
212 self.bump();
213 self.builder.finish_node();
214 } else {
215 self.bump();
216 }
217 if self.current() == Some(EQUALS) {
218 self.bump();
219 if self.current() != Some(VALUE) && self.current() != Some(KEY) {
220 self.builder.start_node(ERROR.into());
221 self.errors
222 .push(format!("expected value, got {:?}", self.current()));
223 self.bump();
224 self.builder.finish_node();
225 } else {
226 self.bump();
227 }
228 } else if self.current() == Some(COMMA) {
229 } else {
230 self.builder.start_node(ERROR.into());
231 self.errors.push("expected `=`".to_string());
232 if self.current().is_some() {
233 self.bump();
234 }
235 self.builder.finish_node();
236 }
237 self.builder.finish_node();
238 true
239 }
240
241 fn parse_options_list(&mut self) {
242 self.skip_ws();
243 if self.tokens.last() == Some(&(KEY, "opts".to_string()))
244 || self.tokens.last() == Some(&(KEY, "options".to_string()))
245 {
246 self.builder.start_node(OPTS_LIST.into());
247 self.bump();
248 self.skip_ws();
249 if self.current() != Some(EQUALS) {
250 self.builder.start_node(ERROR.into());
251 self.errors.push("expected `=`".to_string());
252 if self.current().is_some() {
253 self.bump();
254 }
255 self.builder.finish_node();
256 } else {
257 self.bump();
258 }
259 let quoted = if self.current() == Some(QUOTE) {
260 self.bump();
261 true
262 } else {
263 false
264 };
265 loop {
266 if quoted {
267 if self.current() == Some(QUOTE) {
268 self.bump();
269 break;
270 }
271 self.skip_ws();
272 }
273 if !self.parse_option() {
274 break;
275 }
276 if self.current() == Some(COMMA) {
277 self.bump();
278 } else if !quoted {
279 break;
280 }
281 }
282 self.builder.finish_node();
283 self.skip_ws();
284 }
285 }
286
287 fn parse(mut self) -> Parse {
288 let mut version = 1;
289 self.builder.start_node(ROOT.into());
291 if let Some(v) = self.parse_version() {
292 version = v;
293 }
294 loop {
296 if !self.parse_watch_entry() {
297 break;
298 }
299 }
300 self.skip_ws();
302 self.builder.finish_node();
304
305 Parse {
307 green_node: self.builder.finish(),
308 errors: self.errors,
309 version,
310 }
311 }
312 fn bump(&mut self) {
314 let (kind, text) = self.tokens.pop().unwrap();
315 self.builder.token(kind.into(), text.as_str());
316 }
317 fn current(&self) -> Option<SyntaxKind> {
319 self.tokens.last().map(|(kind, _)| *kind)
320 }
321 fn skip_ws(&mut self) {
322 while self.current() == Some(WHITESPACE)
323 || self.current() == Some(CONTINUATION)
324 || self.current() == Some(COMMENT)
325 {
326 self.bump()
327 }
328 }
329 }
330
331 let mut tokens = lex(text);
332 tokens.reverse();
333 Parser {
334 tokens,
335 builder: GreenNodeBuilder::new(),
336 errors: Vec::new(),
337 }
338 .parse()
339}
340
341type SyntaxNode = rowan::SyntaxNode<Lang>;
348#[allow(unused)]
349type SyntaxToken = rowan::SyntaxToken<Lang>;
350#[allow(unused)]
351type SyntaxElement = rowan::NodeOrToken<SyntaxNode, SyntaxToken>;
352
353impl Parse {
354 fn syntax(&self) -> SyntaxNode {
355 SyntaxNode::new_root_mut(self.green_node.clone())
356 }
357
358 fn root(&self) -> WatchFile {
359 WatchFile::cast(self.syntax()).unwrap()
360 }
361}
362
363macro_rules! ast_node {
364 ($ast:ident, $kind:ident) => {
365 #[derive(PartialEq, Eq, Hash)]
366 #[repr(transparent)]
367 pub struct $ast(SyntaxNode);
369 impl $ast {
370 #[allow(unused)]
371 fn cast(node: SyntaxNode) -> Option<Self> {
372 if node.kind() == $kind {
373 Some(Self(node))
374 } else {
375 None
376 }
377 }
378 }
379
380 impl ToString for $ast {
381 fn to_string(&self) -> String {
382 self.0.text().to_string()
383 }
384 }
385 };
386}
387
388ast_node!(WatchFile, ROOT);
389ast_node!(Version, VERSION);
390ast_node!(Entry, ENTRY);
391ast_node!(OptionList, OPTS_LIST);
392ast_node!(_Option, OPTION);
393ast_node!(Url, URL);
394ast_node!(MatchingPattern, MATCHING_PATTERN);
395ast_node!(VersionPolicyNode, VERSION_POLICY);
396ast_node!(ScriptNode, SCRIPT);
397
398impl WatchFile {
399 pub fn new(version: Option<u32>) -> WatchFile {
401 let mut builder = GreenNodeBuilder::new();
402
403 builder.start_node(ROOT.into());
404 if let Some(version) = version {
405 builder.start_node(VERSION.into());
406 builder.token(KEY.into(), "version");
407 builder.token(EQUALS.into(), "=");
408 builder.token(VALUE.into(), version.to_string().as_str());
409 builder.token(NEWLINE.into(), "\n");
410 builder.finish_node();
411 }
412 builder.finish_node();
413 WatchFile(SyntaxNode::new_root_mut(builder.finish()))
414 }
415
416 pub fn version(&self) -> u32 {
418 self.0
419 .children()
420 .find_map(Version::cast)
421 .map(|it| it.version())
422 .unwrap_or(DEFAULT_VERSION)
423 }
424
425 pub fn entries(&self) -> impl Iterator<Item = Entry> + '_ {
427 self.0.children().filter_map(Entry::cast)
428 }
429
430 pub fn set_version(&mut self, new_version: u32) {
432 let mut builder = GreenNodeBuilder::new();
434 builder.start_node(VERSION.into());
435 builder.token(KEY.into(), "version");
436 builder.token(EQUALS.into(), "=");
437 builder.token(VALUE.into(), new_version.to_string().as_str());
438 builder.token(NEWLINE.into(), "\n");
439 builder.finish_node();
440 let new_version_green = builder.finish();
441
442 let new_version_node = SyntaxNode::new_root_mut(new_version_green);
444
445 let version_pos = self.0.children().position(|child| child.kind() == VERSION);
447
448 if let Some(pos) = version_pos {
449 self.0
451 .splice_children(pos..pos + 1, vec![new_version_node.into()]);
452 } else {
453 self.0.splice_children(0..0, vec![new_version_node.into()]);
455 }
456 }
457}
458
459impl FromStr for WatchFile {
460 type Err = ParseError;
461
462 fn from_str(s: &str) -> Result<Self, Self::Err> {
463 let parsed = parse(s);
464 if parsed.errors.is_empty() {
465 Ok(parsed.root())
466 } else {
467 Err(ParseError(parsed.errors))
468 }
469 }
470}
471
472impl Version {
473 pub fn version(&self) -> u32 {
475 self.0
476 .children_with_tokens()
477 .find_map(|it| match it {
478 SyntaxElement::Token(token) => {
479 if token.kind() == VALUE {
480 Some(token.text().parse().unwrap())
481 } else {
482 None
483 }
484 }
485 _ => None,
486 })
487 .unwrap_or(DEFAULT_VERSION)
488 }
489}
490
491impl Entry {
492 pub fn option_list(&self) -> Option<OptionList> {
494 self.0.children().find_map(OptionList::cast)
495 }
496
497 pub fn get_option(&self, key: &str) -> Option<String> {
499 self.option_list().and_then(|ol| ol.get_option(key))
500 }
501
502 pub fn has_option(&self, key: &str) -> bool {
504 self.option_list().map_or(false, |ol| ol.has_option(key))
505 }
506
507 pub fn component(&self) -> Option<String> {
509 self.get_option("component")
510 }
511
512 pub fn ctype(&self) -> Result<Option<ComponentType>, ()> {
514 self.get_option("ctype").map(|s| s.parse()).transpose()
515 }
516
517 pub fn compression(&self) -> Result<Option<Compression>, ()> {
519 self.get_option("compression")
520 .map(|s| s.parse())
521 .transpose()
522 }
523
524 pub fn repack(&self) -> bool {
526 self.has_option("repack")
527 }
528
529 pub fn repacksuffix(&self) -> Option<String> {
531 self.get_option("repacksuffix")
532 }
533
534 pub fn mode(&self) -> Result<Mode, ()> {
536 Ok(self
537 .get_option("mode")
538 .map(|s| s.parse())
539 .transpose()?
540 .unwrap_or_default())
541 }
542
543 pub fn pretty(&self) -> Result<Pretty, ()> {
545 Ok(self
546 .get_option("pretty")
547 .map(|s| s.parse())
548 .transpose()?
549 .unwrap_or_default())
550 }
551
552 pub fn date(&self) -> String {
555 self.get_option("date")
556 .unwrap_or_else(|| "%Y%m%d".to_string())
557 }
558
559 pub fn gitexport(&self) -> Result<GitExport, ()> {
561 Ok(self
562 .get_option("gitexport")
563 .map(|s| s.parse())
564 .transpose()?
565 .unwrap_or_default())
566 }
567
568 pub fn gitmode(&self) -> Result<GitMode, ()> {
570 Ok(self
571 .get_option("gitmode")
572 .map(|s| s.parse())
573 .transpose()?
574 .unwrap_or_default())
575 }
576
577 pub fn pgpmode(&self) -> Result<PgpMode, ()> {
579 Ok(self
580 .get_option("pgpmode")
581 .map(|s| s.parse())
582 .transpose()?
583 .unwrap_or_default())
584 }
585
586 pub fn searchmode(&self) -> Result<SearchMode, ()> {
588 Ok(self
589 .get_option("searchmode")
590 .map(|s| s.parse())
591 .transpose()?
592 .unwrap_or_default())
593 }
594
595 pub fn decompress(&self) -> bool {
597 self.has_option("decompress")
598 }
599
600 pub fn bare(&self) -> bool {
603 self.has_option("bare")
604 }
605
606 pub fn user_agent(&self) -> Option<String> {
608 self.get_option("user-agent")
609 }
610
611 pub fn passive(&self) -> Option<bool> {
613 if self.has_option("passive") || self.has_option("pasv") {
614 Some(true)
615 } else if self.has_option("active") || self.has_option("nopasv") {
616 Some(false)
617 } else {
618 None
619 }
620 }
621
622 pub fn unzipoptions(&self) -> Option<String> {
625 self.get_option("unzipopt")
626 }
627
628 pub fn dversionmangle(&self) -> Option<String> {
630 self.get_option("dversionmangle")
631 .or_else(|| self.get_option("versionmangle"))
632 }
633
634 pub fn dirversionmangle(&self) -> Option<String> {
638 self.get_option("dirversionmangle")
639 }
640
641 pub fn pagemangle(&self) -> Option<String> {
643 self.get_option("pagemangle")
644 }
645
646 pub fn uversionmangle(&self) -> Option<String> {
650 self.get_option("uversionmangle")
651 .or_else(|| self.get_option("versionmangle"))
652 }
653
654 pub fn versionmangle(&self) -> Option<String> {
656 self.get_option("versionmangle")
657 }
658
659 pub fn hrefdecode(&self) -> bool {
664 self.get_option("hrefdecode").is_some()
665 }
666
667 pub fn downloadurlmangle(&self) -> Option<String> {
670 self.get_option("downloadurlmangle")
671 }
672
673 pub fn filenamemangle(&self) -> Option<String> {
681 self.get_option("filenamemangle")
682 }
683
684 pub fn pgpsigurlmangle(&self) -> Option<String> {
686 self.get_option("pgpsigurlmangle")
687 }
688
689 pub fn oversionmangle(&self) -> Option<String> {
692 self.get_option("oversionmangle")
693 }
694
695 pub fn opts(&self) -> std::collections::HashMap<String, String> {
697 let mut options = std::collections::HashMap::new();
698
699 if let Some(ol) = self.option_list() {
700 for opt in ol.children() {
701 let key = opt.key();
702 let value = opt.value();
703 if let (Some(key), Some(value)) = (key, value) {
704 options.insert(key.to_string(), value.to_string());
705 }
706 }
707 }
708
709 options
710 }
711
712 fn items(&self) -> impl Iterator<Item = String> + '_ {
713 self.0.children_with_tokens().filter_map(|it| match it {
714 SyntaxElement::Token(token) => {
715 if token.kind() == VALUE || token.kind() == KEY {
716 Some(token.text().to_string())
717 } else {
718 None
719 }
720 }
721 SyntaxElement::Node(node) => {
722 match node.kind() {
724 URL => Url::cast(node).map(|n| n.url()),
725 MATCHING_PATTERN => MatchingPattern::cast(node).map(|n| n.pattern()),
726 VERSION_POLICY => VersionPolicyNode::cast(node).map(|n| n.policy()),
727 SCRIPT => ScriptNode::cast(node).map(|n| n.script()),
728 _ => None,
729 }
730 }
731 })
732 }
733
734 pub fn url(&self) -> String {
736 self.0
737 .children()
738 .find_map(Url::cast)
739 .map(|it| it.url())
740 .unwrap_or_else(|| {
741 self.items().next().unwrap()
743 })
744 }
745
746 pub fn matching_pattern(&self) -> Option<String> {
748 self.0
749 .children()
750 .find_map(MatchingPattern::cast)
751 .map(|it| it.pattern())
752 .or_else(|| {
753 self.items().nth(1)
755 })
756 }
757
758 pub fn version(&self) -> Result<Option<crate::VersionPolicy>, String> {
760 self.0
761 .children()
762 .find_map(VersionPolicyNode::cast)
763 .map(|it| it.policy().parse())
764 .transpose()
765 .or_else(|_e| {
766 self.items().nth(2).map(|it| it.parse()).transpose()
768 })
769 }
770
771 pub fn script(&self) -> Option<String> {
773 self.0
774 .children()
775 .find_map(ScriptNode::cast)
776 .map(|it| it.script())
777 .or_else(|| {
778 self.items().nth(3)
780 })
781 }
782
783 pub fn format_url(&self, package: impl FnOnce() -> String) -> url::Url {
785 subst(self.url().as_str(), package).parse().unwrap()
786 }
787
788 pub fn set_url(&mut self, new_url: &str) {
790 let mut builder = GreenNodeBuilder::new();
792 builder.start_node(URL.into());
793 builder.token(VALUE.into(), new_url);
794 builder.finish_node();
795 let new_url_green = builder.finish();
796
797 let new_url_node = SyntaxNode::new_root_mut(new_url_green);
799
800 let url_pos = self
802 .0
803 .children_with_tokens()
804 .position(|child| matches!(child, SyntaxElement::Node(node) if node.kind() == URL));
805
806 if let Some(pos) = url_pos {
807 self.0
809 .splice_children(pos..pos + 1, vec![new_url_node.into()]);
810 }
811 }
812
813 pub fn set_matching_pattern(&mut self, new_pattern: &str) {
819 let mut builder = GreenNodeBuilder::new();
821 builder.start_node(MATCHING_PATTERN.into());
822 builder.token(VALUE.into(), new_pattern);
823 builder.finish_node();
824 let new_pattern_green = builder.finish();
825
826 let new_pattern_node = SyntaxNode::new_root_mut(new_pattern_green);
828
829 let pattern_pos = self.0.children_with_tokens().position(
831 |child| matches!(child, SyntaxElement::Node(node) if node.kind() == MATCHING_PATTERN),
832 );
833
834 if let Some(pos) = pattern_pos {
835 self.0
837 .splice_children(pos..pos + 1, vec![new_pattern_node.into()]);
838 }
839 }
841
842 pub fn set_version_policy(&mut self, new_policy: &str) {
848 let mut builder = GreenNodeBuilder::new();
850 builder.start_node(VERSION_POLICY.into());
851 builder.token(VALUE.into(), new_policy);
853 builder.finish_node();
854 let new_policy_green = builder.finish();
855
856 let new_policy_node = SyntaxNode::new_root_mut(new_policy_green);
858
859 let policy_pos = self.0.children_with_tokens().position(
861 |child| matches!(child, SyntaxElement::Node(node) if node.kind() == VERSION_POLICY),
862 );
863
864 if let Some(pos) = policy_pos {
865 self.0
867 .splice_children(pos..pos + 1, vec![new_policy_node.into()]);
868 }
869 }
871
872 pub fn set_script(&mut self, new_script: &str) {
878 let mut builder = GreenNodeBuilder::new();
880 builder.start_node(SCRIPT.into());
881 builder.token(VALUE.into(), new_script);
883 builder.finish_node();
884 let new_script_green = builder.finish();
885
886 let new_script_node = SyntaxNode::new_root_mut(new_script_green);
888
889 let script_pos = self
891 .0
892 .children_with_tokens()
893 .position(|child| matches!(child, SyntaxElement::Node(node) if node.kind() == SCRIPT));
894
895 if let Some(pos) = script_pos {
896 self.0
898 .splice_children(pos..pos + 1, vec![new_script_node.into()]);
899 }
900 }
902}
903
904const SUBSTITUTIONS: &[(&str, &str)] = &[
905 ("@ANY_VERSION@", r"[-_]?(\d[\-+\.:\~\da-zA-Z]*)"),
910 (
913 "@ARCHIVE_EXT@",
914 r"(?i)\.(?:tar\.xz|tar\.bz2|tar\.gz|zip|tgz|tbz|txz)",
915 ),
916 (
919 "@SIGNATURE_EXT@",
920 r"(?i)\.(?:tar\.xz|tar\.bz2|tar\.gz|zip|tgz|tbz|txz)\.(?:asc|pgp|gpg|sig|sign)",
921 ),
922 ("@DEB_EXT@", r"[\+~](debian|dfsg|ds|deb)(\.)?(\d+)?$"),
924];
925
926pub fn subst(text: &str, package: impl FnOnce() -> String) -> String {
927 let mut substs = SUBSTITUTIONS.to_vec();
928 let package_name;
929 if text.contains("@PACKAGE@") {
930 package_name = Some(package());
931 substs.push(("@PACKAGE@", package_name.as_deref().unwrap()));
932 }
933
934 let mut text = text.to_string();
935
936 for (k, v) in substs {
937 text = text.replace(k, v);
938 }
939
940 text
941}
942
943#[test]
944fn test_subst() {
945 assert_eq!(
946 subst("@ANY_VERSION@", || unreachable!()),
947 r"[-_]?(\d[\-+\.:\~\da-zA-Z]*)"
948 );
949 assert_eq!(subst("@PACKAGE@", || "dulwich".to_string()), "dulwich");
950}
951
952impl OptionList {
953 fn children(&self) -> impl Iterator<Item = _Option> + '_ {
954 self.0.children().filter_map(_Option::cast)
955 }
956
957 pub fn has_option(&self, key: &str) -> bool {
958 self.children().any(|it| it.key().as_deref() == Some(key))
959 }
960
961 pub fn get_option(&self, key: &str) -> Option<String> {
962 for child in self.children() {
963 if child.key().as_deref() == Some(key) {
964 return child.value();
965 }
966 }
967 None
968 }
969}
970
971impl _Option {
972 pub fn key(&self) -> Option<String> {
974 self.0.children_with_tokens().find_map(|it| match it {
975 SyntaxElement::Token(token) => {
976 if token.kind() == KEY {
977 Some(token.text().to_string())
978 } else {
979 None
980 }
981 }
982 _ => None,
983 })
984 }
985
986 pub fn value(&self) -> Option<String> {
988 self.0
989 .children_with_tokens()
990 .filter_map(|it| match it {
991 SyntaxElement::Token(token) => {
992 if token.kind() == VALUE || token.kind() == KEY {
993 Some(token.text().to_string())
994 } else {
995 None
996 }
997 }
998 _ => None,
999 })
1000 .nth(1)
1001 }
1002}
1003
1004impl Url {
1005 pub fn url(&self) -> String {
1007 self.0
1008 .children_with_tokens()
1009 .find_map(|it| match it {
1010 SyntaxElement::Token(token) => {
1011 if token.kind() == VALUE {
1012 Some(token.text().to_string())
1013 } else {
1014 None
1015 }
1016 }
1017 _ => None,
1018 })
1019 .unwrap()
1020 }
1021}
1022
1023impl MatchingPattern {
1024 pub fn pattern(&self) -> String {
1026 self.0
1027 .children_with_tokens()
1028 .find_map(|it| match it {
1029 SyntaxElement::Token(token) => {
1030 if token.kind() == VALUE {
1031 Some(token.text().to_string())
1032 } else {
1033 None
1034 }
1035 }
1036 _ => None,
1037 })
1038 .unwrap()
1039 }
1040}
1041
1042impl VersionPolicyNode {
1043 pub fn policy(&self) -> String {
1045 self.0
1046 .children_with_tokens()
1047 .find_map(|it| match it {
1048 SyntaxElement::Token(token) => {
1049 if token.kind() == VALUE || token.kind() == KEY {
1051 Some(token.text().to_string())
1052 } else {
1053 None
1054 }
1055 }
1056 _ => None,
1057 })
1058 .unwrap()
1059 }
1060}
1061
1062impl ScriptNode {
1063 pub fn script(&self) -> String {
1065 self.0
1066 .children_with_tokens()
1067 .find_map(|it| match it {
1068 SyntaxElement::Token(token) => {
1069 if token.kind() == VALUE || token.kind() == KEY {
1071 Some(token.text().to_string())
1072 } else {
1073 None
1074 }
1075 }
1076 _ => None,
1077 })
1078 .unwrap()
1079 }
1080}
1081
1082#[test]
1083fn test_entry_node_structure() {
1084 let wf: super::WatchFile = r#"version=4
1086opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
1087"#
1088 .parse()
1089 .unwrap();
1090
1091 let entry = wf.entries().next().unwrap();
1092
1093 assert_eq!(entry.0.children().find(|n| n.kind() == URL).is_some(), true);
1095 assert_eq!(entry.url(), "https://example.com/releases");
1096
1097 assert_eq!(
1099 entry
1100 .0
1101 .children()
1102 .find(|n| n.kind() == MATCHING_PATTERN)
1103 .is_some(),
1104 true
1105 );
1106 assert_eq!(
1107 entry.matching_pattern(),
1108 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
1109 );
1110
1111 assert_eq!(
1113 entry
1114 .0
1115 .children()
1116 .find(|n| n.kind() == VERSION_POLICY)
1117 .is_some(),
1118 true
1119 );
1120 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
1121
1122 assert_eq!(
1124 entry.0.children().find(|n| n.kind() == SCRIPT).is_some(),
1125 true
1126 );
1127 assert_eq!(entry.script(), Some("uupdate".into()));
1128}
1129
1130#[test]
1131fn test_entry_node_structure_partial() {
1132 let wf: super::WatchFile = r#"version=4
1134https://github.com/example/tags .*/v?(\d\S+)\.tar\.gz
1135"#
1136 .parse()
1137 .unwrap();
1138
1139 let entry = wf.entries().next().unwrap();
1140
1141 assert_eq!(entry.0.children().find(|n| n.kind() == URL).is_some(), true);
1143 assert_eq!(
1144 entry
1145 .0
1146 .children()
1147 .find(|n| n.kind() == MATCHING_PATTERN)
1148 .is_some(),
1149 true
1150 );
1151
1152 assert_eq!(
1154 entry
1155 .0
1156 .children()
1157 .find(|n| n.kind() == VERSION_POLICY)
1158 .is_some(),
1159 false
1160 );
1161 assert_eq!(
1162 entry.0.children().find(|n| n.kind() == SCRIPT).is_some(),
1163 false
1164 );
1165
1166 assert_eq!(entry.url(), "https://github.com/example/tags");
1168 assert_eq!(
1169 entry.matching_pattern(),
1170 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
1171 );
1172 assert_eq!(entry.version(), Ok(None));
1173 assert_eq!(entry.script(), None);
1174}
1175
1176#[test]
1177fn test_parse_v1() {
1178 const WATCHV1: &str = r#"version=4
1179opts=bare,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1\.tar\.gz/ \
1180 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
1181"#;
1182 let parsed = parse(WATCHV1);
1183 let node = parsed.syntax();
1185 assert_eq!(
1186 format!("{:#?}", node),
1187 r#"ROOT@0..161
1188 VERSION@0..10
1189 KEY@0..7 "version"
1190 EQUALS@7..8 "="
1191 VALUE@8..9 "4"
1192 NEWLINE@9..10 "\n"
1193 ENTRY@10..161
1194 OPTS_LIST@10..86
1195 KEY@10..14 "opts"
1196 EQUALS@14..15 "="
1197 OPTION@15..19
1198 KEY@15..19 "bare"
1199 COMMA@19..20 ","
1200 OPTION@20..86
1201 KEY@20..34 "filenamemangle"
1202 EQUALS@34..35 "="
1203 VALUE@35..86 "s/.+\\/v?(\\d\\S+)\\.tar\\ ..."
1204 WHITESPACE@86..87 " "
1205 CONTINUATION@87..89 "\\\n"
1206 WHITESPACE@89..91 " "
1207 URL@91..138
1208 VALUE@91..138 "https://github.com/sy ..."
1209 WHITESPACE@138..139 " "
1210 MATCHING_PATTERN@139..160
1211 VALUE@139..160 ".*/v?(\\d\\S+)\\.tar\\.gz"
1212 NEWLINE@160..161 "\n"
1213"#
1214 );
1215
1216 let root = parsed.root();
1217 assert_eq!(root.version(), 4);
1218 let entries = root.entries().collect::<Vec<_>>();
1219 assert_eq!(entries.len(), 1);
1220 let entry = &entries[0];
1221 assert_eq!(
1222 entry.url(),
1223 "https://github.com/syncthing/syncthing-gtk/tags"
1224 );
1225 assert_eq!(
1226 entry.matching_pattern(),
1227 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
1228 );
1229 assert_eq!(entry.version(), Ok(None));
1230 assert_eq!(entry.script(), None);
1231
1232 assert_eq!(node.text(), WATCHV1);
1233}
1234
1235#[test]
1236fn test_parse_v2() {
1237 let parsed = parse(
1238 r#"version=4
1239https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
1240# comment
1241"#,
1242 );
1243 assert_eq!(parsed.errors, Vec::<String>::new());
1244 let node = parsed.syntax();
1245 assert_eq!(
1246 format!("{:#?}", node),
1247 r###"ROOT@0..90
1248 VERSION@0..10
1249 KEY@0..7 "version"
1250 EQUALS@7..8 "="
1251 VALUE@8..9 "4"
1252 NEWLINE@9..10 "\n"
1253 ENTRY@10..80
1254 URL@10..57
1255 VALUE@10..57 "https://github.com/sy ..."
1256 WHITESPACE@57..58 " "
1257 MATCHING_PATTERN@58..79
1258 VALUE@58..79 ".*/v?(\\d\\S+)\\.tar\\.gz"
1259 NEWLINE@79..80 "\n"
1260 COMMENT@80..89 "# comment"
1261 NEWLINE@89..90 "\n"
1262"###
1263 );
1264
1265 let root = parsed.root();
1266 assert_eq!(root.version(), 4);
1267 let entries = root.entries().collect::<Vec<_>>();
1268 assert_eq!(entries.len(), 1);
1269 let entry = &entries[0];
1270 assert_eq!(
1271 entry.url(),
1272 "https://github.com/syncthing/syncthing-gtk/tags"
1273 );
1274 assert_eq!(
1275 entry.format_url(|| "syncthing-gtk".to_string()),
1276 "https://github.com/syncthing/syncthing-gtk/tags"
1277 .parse()
1278 .unwrap()
1279 );
1280}
1281
1282#[test]
1283fn test_parse_v3() {
1284 let parsed = parse(
1285 r#"version=4
1286https://github.com/syncthing/@PACKAGE@/tags .*/v?(\d\S+)\.tar\.gz
1287# comment
1288"#,
1289 );
1290 assert_eq!(parsed.errors, Vec::<String>::new());
1291 let root = parsed.root();
1292 assert_eq!(root.version(), 4);
1293 let entries = root.entries().collect::<Vec<_>>();
1294 assert_eq!(entries.len(), 1);
1295 let entry = &entries[0];
1296 assert_eq!(entry.url(), "https://github.com/syncthing/@PACKAGE@/tags");
1297 assert_eq!(
1298 entry.format_url(|| "syncthing-gtk".to_string()),
1299 "https://github.com/syncthing/syncthing-gtk/tags"
1300 .parse()
1301 .unwrap()
1302 );
1303}
1304
1305#[test]
1306fn test_parse_v4() {
1307 let cl: super::WatchFile = r#"version=4
1308opts=repack,compression=xz,dversionmangle=s/\+ds//,repacksuffix=+ds \
1309 https://github.com/example/example-cat/tags \
1310 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
1311"#
1312 .parse()
1313 .unwrap();
1314 assert_eq!(cl.version(), 4);
1315 let entries = cl.entries().collect::<Vec<_>>();
1316 assert_eq!(entries.len(), 1);
1317 let entry = &entries[0];
1318 assert_eq!(entry.url(), "https://github.com/example/example-cat/tags");
1319 assert_eq!(
1320 entry.matching_pattern(),
1321 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
1322 );
1323 assert!(entry.repack());
1324 assert_eq!(entry.compression(), Ok(Some(Compression::Xz)));
1325 assert_eq!(entry.dversionmangle(), Some("s/\\+ds//".into()));
1326 assert_eq!(entry.repacksuffix(), Some("+ds".into()));
1327 assert_eq!(entry.script(), Some("uupdate".into()));
1328 assert_eq!(
1329 entry.format_url(|| "example-cat".to_string()),
1330 "https://github.com/example/example-cat/tags"
1331 .parse()
1332 .unwrap()
1333 );
1334 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
1335}
1336
1337#[test]
1338fn test_git_mode() {
1339 let text = r#"version=3
1340opts="mode=git, gitmode=shallow, pgpmode=gittag" \
1341https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git \
1342refs/tags/(.*) debian
1343"#;
1344 let parsed = parse(text);
1345 assert_eq!(parsed.errors, Vec::<String>::new());
1346 let cl = parsed.root();
1347 assert_eq!(cl.version(), 3);
1348 let entries = cl.entries().collect::<Vec<_>>();
1349 assert_eq!(entries.len(), 1);
1350 let entry = &entries[0];
1351 assert_eq!(
1352 entry.url(),
1353 "https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git"
1354 );
1355 assert_eq!(entry.matching_pattern(), Some("refs/tags/(.*)".into()));
1356 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
1357 assert_eq!(entry.script(), None);
1358 assert_eq!(entry.gitmode(), Ok(GitMode::Shallow));
1359 assert_eq!(entry.pgpmode(), Ok(PgpMode::GitTag));
1360 assert_eq!(entry.mode(), Ok(Mode::Git));
1361}
1362
1363#[test]
1364fn test_parse_quoted() {
1365 const WATCHV1: &str = r#"version=4
1366opts="bare, filenamemangle=blah" \
1367 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
1368"#;
1369 let parsed = parse(WATCHV1);
1370 let node = parsed.syntax();
1372
1373 let root = parsed.root();
1374 assert_eq!(root.version(), 4);
1375 let entries = root.entries().collect::<Vec<_>>();
1376 assert_eq!(entries.len(), 1);
1377 let entry = &entries[0];
1378
1379 assert_eq!(
1380 entry.url(),
1381 "https://github.com/syncthing/syncthing-gtk/tags"
1382 );
1383 assert_eq!(
1384 entry.matching_pattern(),
1385 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
1386 );
1387 assert_eq!(entry.version(), Ok(None));
1388 assert_eq!(entry.script(), None);
1389
1390 assert_eq!(node.text(), WATCHV1);
1391}
1392
1393#[test]
1394fn test_set_url() {
1395 let wf: super::WatchFile = r#"version=4
1397https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
1398"#
1399 .parse()
1400 .unwrap();
1401
1402 let mut entry = wf.entries().next().unwrap();
1403 assert_eq!(
1404 entry.url(),
1405 "https://github.com/syncthing/syncthing-gtk/tags"
1406 );
1407
1408 entry.set_url("https://newurl.example.org/path");
1409 assert_eq!(entry.url(), "https://newurl.example.org/path");
1410 assert_eq!(
1411 entry.matching_pattern(),
1412 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
1413 );
1414
1415 assert_eq!(
1417 entry.to_string(),
1418 "https://newurl.example.org/path .*/v?(\\d\\S+)\\.tar\\.gz\n"
1419 );
1420}
1421
1422#[test]
1423fn test_set_url_with_options() {
1424 let wf: super::WatchFile = r#"version=4
1426opts=foo=blah https://foo.com/bar .*/v?(\d\S+)\.tar\.gz
1427"#
1428 .parse()
1429 .unwrap();
1430
1431 let mut entry = wf.entries().next().unwrap();
1432 assert_eq!(entry.url(), "https://foo.com/bar");
1433 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
1434
1435 entry.set_url("https://example.com/baz");
1436 assert_eq!(entry.url(), "https://example.com/baz");
1437
1438 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
1440 assert_eq!(
1441 entry.matching_pattern(),
1442 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
1443 );
1444
1445 assert_eq!(
1447 entry.to_string(),
1448 "opts=foo=blah https://example.com/baz .*/v?(\\d\\S+)\\.tar\\.gz\n"
1449 );
1450}
1451
1452#[test]
1453fn test_set_url_complex() {
1454 let wf: super::WatchFile = r#"version=4
1456opts=bare,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1\.tar\.gz/ \
1457 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
1458"#
1459 .parse()
1460 .unwrap();
1461
1462 let mut entry = wf.entries().next().unwrap();
1463 assert_eq!(
1464 entry.url(),
1465 "https://github.com/syncthing/syncthing-gtk/tags"
1466 );
1467
1468 entry.set_url("https://gitlab.com/newproject/tags");
1469 assert_eq!(entry.url(), "https://gitlab.com/newproject/tags");
1470
1471 assert!(entry.bare());
1473 assert_eq!(
1474 entry.filenamemangle(),
1475 Some("s/.+\\/v?(\\d\\S+)\\.tar\\.gz/syncthing-gtk-$1\\.tar\\.gz/".into())
1476 );
1477 assert_eq!(
1478 entry.matching_pattern(),
1479 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
1480 );
1481
1482 assert_eq!(
1484 entry.to_string(),
1485 r#"opts=bare,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1\.tar\.gz/ \
1486 https://gitlab.com/newproject/tags .*/v?(\d\S+)\.tar\.gz
1487"#
1488 );
1489}
1490
1491#[test]
1492fn test_set_url_with_all_fields() {
1493 let wf: super::WatchFile = r#"version=4
1495opts=repack,compression=xz,dversionmangle=s/\+ds//,repacksuffix=+ds \
1496 https://github.com/example/example-cat/tags \
1497 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
1498"#
1499 .parse()
1500 .unwrap();
1501
1502 let mut entry = wf.entries().next().unwrap();
1503 assert_eq!(entry.url(), "https://github.com/example/example-cat/tags");
1504 assert_eq!(
1505 entry.matching_pattern(),
1506 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
1507 );
1508 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
1509 assert_eq!(entry.script(), Some("uupdate".into()));
1510
1511 entry.set_url("https://gitlab.example.org/project/releases");
1512 assert_eq!(entry.url(), "https://gitlab.example.org/project/releases");
1513
1514 assert!(entry.repack());
1516 assert_eq!(entry.compression(), Ok(Some(super::Compression::Xz)));
1517 assert_eq!(entry.dversionmangle(), Some("s/\\+ds//".into()));
1518 assert_eq!(entry.repacksuffix(), Some("+ds".into()));
1519 assert_eq!(
1520 entry.matching_pattern(),
1521 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
1522 );
1523 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
1524 assert_eq!(entry.script(), Some("uupdate".into()));
1525
1526 assert_eq!(
1528 entry.to_string(),
1529 r#"opts=repack,compression=xz,dversionmangle=s/\+ds//,repacksuffix=+ds \
1530 https://gitlab.example.org/project/releases \
1531 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
1532"#
1533 );
1534}
1535
1536#[test]
1537fn test_set_url_quoted_options() {
1538 let wf: super::WatchFile = r#"version=4
1540opts="bare, filenamemangle=blah" \
1541 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
1542"#
1543 .parse()
1544 .unwrap();
1545
1546 let mut entry = wf.entries().next().unwrap();
1547 assert_eq!(
1548 entry.url(),
1549 "https://github.com/syncthing/syncthing-gtk/tags"
1550 );
1551
1552 entry.set_url("https://example.org/new/path");
1553 assert_eq!(entry.url(), "https://example.org/new/path");
1554
1555 assert_eq!(
1557 entry.to_string(),
1558 r#"opts="bare, filenamemangle=blah" \
1559 https://example.org/new/path .*/v?(\d\S+)\.tar\.gz
1560"#
1561 );
1562}
1563
1564#[test]
1565fn test_set_matching_pattern() {
1566 let wf: super::WatchFile = r#"version=4
1568https://github.com/example/tags .*/v?(\d\S+)\.tar\.gz
1569"#
1570 .parse()
1571 .unwrap();
1572
1573 let mut entry = wf.entries().next().unwrap();
1574 assert_eq!(
1575 entry.matching_pattern(),
1576 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
1577 );
1578
1579 entry.set_matching_pattern("(?:.*?/)?v?([\\d.]+)\\.tar\\.gz");
1580 assert_eq!(
1581 entry.matching_pattern(),
1582 Some("(?:.*?/)?v?([\\d.]+)\\.tar\\.gz".into())
1583 );
1584
1585 assert_eq!(entry.url(), "https://github.com/example/tags");
1587
1588 assert_eq!(
1590 entry.to_string(),
1591 "https://github.com/example/tags (?:.*?/)?v?([\\d.]+)\\.tar\\.gz\n"
1592 );
1593}
1594
1595#[test]
1596fn test_set_matching_pattern_with_all_fields() {
1597 let wf: super::WatchFile = r#"version=4
1599opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
1600"#
1601 .parse()
1602 .unwrap();
1603
1604 let mut entry = wf.entries().next().unwrap();
1605 assert_eq!(
1606 entry.matching_pattern(),
1607 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
1608 );
1609
1610 entry.set_matching_pattern(".*/version-([\\d.]+)\\.tar\\.xz");
1611 assert_eq!(
1612 entry.matching_pattern(),
1613 Some(".*/version-([\\d.]+)\\.tar\\.xz".into())
1614 );
1615
1616 assert_eq!(entry.url(), "https://example.com/releases");
1618 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
1619 assert_eq!(entry.script(), Some("uupdate".into()));
1620 assert_eq!(entry.compression(), Ok(Some(super::Compression::Xz)));
1621
1622 assert_eq!(
1624 entry.to_string(),
1625 "opts=compression=xz https://example.com/releases .*/version-([\\d.]+)\\.tar\\.xz debian uupdate\n"
1626 );
1627}
1628
1629#[test]
1630fn test_set_version_policy() {
1631 let wf: super::WatchFile = r#"version=4
1633https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
1634"#
1635 .parse()
1636 .unwrap();
1637
1638 let mut entry = wf.entries().next().unwrap();
1639 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
1640
1641 entry.set_version_policy("previous");
1642 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Previous)));
1643
1644 assert_eq!(entry.url(), "https://example.com/releases");
1646 assert_eq!(
1647 entry.matching_pattern(),
1648 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
1649 );
1650 assert_eq!(entry.script(), Some("uupdate".into()));
1651
1652 assert_eq!(
1654 entry.to_string(),
1655 "https://example.com/releases (?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz previous uupdate\n"
1656 );
1657}
1658
1659#[test]
1660fn test_set_version_policy_with_options() {
1661 let wf: super::WatchFile = r#"version=4
1663opts=repack,compression=xz \
1664 https://github.com/example/example-cat/tags \
1665 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
1666"#
1667 .parse()
1668 .unwrap();
1669
1670 let mut entry = wf.entries().next().unwrap();
1671 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
1672
1673 entry.set_version_policy("ignore");
1674 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Ignore)));
1675
1676 assert_eq!(entry.url(), "https://github.com/example/example-cat/tags");
1678 assert_eq!(
1679 entry.matching_pattern(),
1680 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
1681 );
1682 assert_eq!(entry.script(), Some("uupdate".into()));
1683 assert!(entry.repack());
1684
1685 assert_eq!(
1687 entry.to_string(),
1688 r#"opts=repack,compression=xz \
1689 https://github.com/example/example-cat/tags \
1690 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz ignore uupdate
1691"#
1692 );
1693}
1694
1695#[test]
1696fn test_set_script() {
1697 let wf: super::WatchFile = r#"version=4
1699https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
1700"#
1701 .parse()
1702 .unwrap();
1703
1704 let mut entry = wf.entries().next().unwrap();
1705 assert_eq!(entry.script(), Some("uupdate".into()));
1706
1707 entry.set_script("uscan");
1708 assert_eq!(entry.script(), Some("uscan".into()));
1709
1710 assert_eq!(entry.url(), "https://example.com/releases");
1712 assert_eq!(
1713 entry.matching_pattern(),
1714 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
1715 );
1716 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
1717
1718 assert_eq!(
1720 entry.to_string(),
1721 "https://example.com/releases (?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz debian uscan\n"
1722 );
1723}
1724
1725#[test]
1726fn test_set_script_with_options() {
1727 let wf: super::WatchFile = r#"version=4
1729opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
1730"#
1731 .parse()
1732 .unwrap();
1733
1734 let mut entry = wf.entries().next().unwrap();
1735 assert_eq!(entry.script(), Some("uupdate".into()));
1736
1737 entry.set_script("custom-script.sh");
1738 assert_eq!(entry.script(), Some("custom-script.sh".into()));
1739
1740 assert_eq!(entry.url(), "https://example.com/releases");
1742 assert_eq!(
1743 entry.matching_pattern(),
1744 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
1745 );
1746 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
1747 assert_eq!(entry.compression(), Ok(Some(super::Compression::Xz)));
1748
1749 assert_eq!(
1751 entry.to_string(),
1752 "opts=compression=xz https://example.com/releases (?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz debian custom-script.sh\n"
1753 );
1754}