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.builder.start_node(OPTION_SEPARATOR.into());
278 self.bump();
279 self.builder.finish_node();
280 } else if !quoted {
281 break;
282 }
283 }
284 self.builder.finish_node();
285 self.skip_ws();
286 }
287 }
288
289 fn parse(mut self) -> Parse {
290 let mut version = 1;
291 self.builder.start_node(ROOT.into());
293 if let Some(v) = self.parse_version() {
294 version = v;
295 }
296 loop {
298 if !self.parse_watch_entry() {
299 break;
300 }
301 }
302 self.skip_ws();
304 self.builder.finish_node();
306
307 Parse {
309 green_node: self.builder.finish(),
310 errors: self.errors,
311 version,
312 }
313 }
314 fn bump(&mut self) {
316 let (kind, text) = self.tokens.pop().unwrap();
317 self.builder.token(kind.into(), text.as_str());
318 }
319 fn current(&self) -> Option<SyntaxKind> {
321 self.tokens.last().map(|(kind, _)| *kind)
322 }
323 fn skip_ws(&mut self) {
324 while self.current() == Some(WHITESPACE)
325 || self.current() == Some(CONTINUATION)
326 || self.current() == Some(COMMENT)
327 {
328 self.bump()
329 }
330 }
331 }
332
333 let mut tokens = lex(text);
334 tokens.reverse();
335 Parser {
336 tokens,
337 builder: GreenNodeBuilder::new(),
338 errors: Vec::new(),
339 }
340 .parse()
341}
342
343type SyntaxNode = rowan::SyntaxNode<Lang>;
350#[allow(unused)]
351type SyntaxToken = rowan::SyntaxToken<Lang>;
352#[allow(unused)]
353type SyntaxElement = rowan::NodeOrToken<SyntaxNode, SyntaxToken>;
354
355impl Parse {
356 fn syntax(&self) -> SyntaxNode {
357 SyntaxNode::new_root_mut(self.green_node.clone())
358 }
359
360 fn root(&self) -> WatchFile {
361 WatchFile::cast(self.syntax()).unwrap()
362 }
363}
364
365macro_rules! ast_node {
366 ($ast:ident, $kind:ident) => {
367 #[derive(PartialEq, Eq, Hash)]
368 #[repr(transparent)]
369 pub struct $ast(SyntaxNode);
371 impl $ast {
372 #[allow(unused)]
373 fn cast(node: SyntaxNode) -> Option<Self> {
374 if node.kind() == $kind {
375 Some(Self(node))
376 } else {
377 None
378 }
379 }
380 }
381
382 impl ToString for $ast {
383 fn to_string(&self) -> String {
384 self.0.text().to_string()
385 }
386 }
387 };
388}
389
390ast_node!(WatchFile, ROOT);
391ast_node!(Version, VERSION);
392ast_node!(Entry, ENTRY);
393ast_node!(OptionList, OPTS_LIST);
394ast_node!(_Option, OPTION);
395ast_node!(Url, URL);
396ast_node!(MatchingPattern, MATCHING_PATTERN);
397ast_node!(VersionPolicyNode, VERSION_POLICY);
398ast_node!(ScriptNode, SCRIPT);
399
400impl WatchFile {
401 pub fn new(version: Option<u32>) -> WatchFile {
403 let mut builder = GreenNodeBuilder::new();
404
405 builder.start_node(ROOT.into());
406 if let Some(version) = version {
407 builder.start_node(VERSION.into());
408 builder.token(KEY.into(), "version");
409 builder.token(EQUALS.into(), "=");
410 builder.token(VALUE.into(), version.to_string().as_str());
411 builder.token(NEWLINE.into(), "\n");
412 builder.finish_node();
413 }
414 builder.finish_node();
415 WatchFile(SyntaxNode::new_root_mut(builder.finish()))
416 }
417
418 pub fn version(&self) -> u32 {
420 self.0
421 .children()
422 .find_map(Version::cast)
423 .map(|it| it.version())
424 .unwrap_or(DEFAULT_VERSION)
425 }
426
427 pub fn entries(&self) -> impl Iterator<Item = Entry> + '_ {
429 self.0.children().filter_map(Entry::cast)
430 }
431
432 pub fn set_version(&mut self, new_version: u32) {
434 let mut builder = GreenNodeBuilder::new();
436 builder.start_node(VERSION.into());
437 builder.token(KEY.into(), "version");
438 builder.token(EQUALS.into(), "=");
439 builder.token(VALUE.into(), new_version.to_string().as_str());
440 builder.token(NEWLINE.into(), "\n");
441 builder.finish_node();
442 let new_version_green = builder.finish();
443
444 let new_version_node = SyntaxNode::new_root_mut(new_version_green);
446
447 let version_pos = self.0.children().position(|child| child.kind() == VERSION);
449
450 if let Some(pos) = version_pos {
451 self.0
453 .splice_children(pos..pos + 1, vec![new_version_node.into()]);
454 } else {
455 self.0.splice_children(0..0, vec![new_version_node.into()]);
457 }
458 }
459}
460
461impl FromStr for WatchFile {
462 type Err = ParseError;
463
464 fn from_str(s: &str) -> Result<Self, Self::Err> {
465 let parsed = parse(s);
466 if parsed.errors.is_empty() {
467 Ok(parsed.root())
468 } else {
469 Err(ParseError(parsed.errors))
470 }
471 }
472}
473
474impl Version {
475 pub fn version(&self) -> u32 {
477 self.0
478 .children_with_tokens()
479 .find_map(|it| match it {
480 SyntaxElement::Token(token) => {
481 if token.kind() == VALUE {
482 Some(token.text().parse().unwrap())
483 } else {
484 None
485 }
486 }
487 _ => None,
488 })
489 .unwrap_or(DEFAULT_VERSION)
490 }
491}
492
493impl Entry {
494 pub fn option_list(&self) -> Option<OptionList> {
496 self.0.children().find_map(OptionList::cast)
497 }
498
499 pub fn get_option(&self, key: &str) -> Option<String> {
501 self.option_list().and_then(|ol| ol.get_option(key))
502 }
503
504 pub fn has_option(&self, key: &str) -> bool {
506 self.option_list().map_or(false, |ol| ol.has_option(key))
507 }
508
509 pub fn component(&self) -> Option<String> {
511 self.get_option("component")
512 }
513
514 pub fn ctype(&self) -> Result<Option<ComponentType>, ()> {
516 self.get_option("ctype").map(|s| s.parse()).transpose()
517 }
518
519 pub fn compression(&self) -> Result<Option<Compression>, ()> {
521 self.get_option("compression")
522 .map(|s| s.parse())
523 .transpose()
524 }
525
526 pub fn repack(&self) -> bool {
528 self.has_option("repack")
529 }
530
531 pub fn repacksuffix(&self) -> Option<String> {
533 self.get_option("repacksuffix")
534 }
535
536 pub fn mode(&self) -> Result<Mode, ()> {
538 Ok(self
539 .get_option("mode")
540 .map(|s| s.parse())
541 .transpose()?
542 .unwrap_or_default())
543 }
544
545 pub fn pretty(&self) -> Result<Pretty, ()> {
547 Ok(self
548 .get_option("pretty")
549 .map(|s| s.parse())
550 .transpose()?
551 .unwrap_or_default())
552 }
553
554 pub fn date(&self) -> String {
557 self.get_option("date")
558 .unwrap_or_else(|| "%Y%m%d".to_string())
559 }
560
561 pub fn gitexport(&self) -> Result<GitExport, ()> {
563 Ok(self
564 .get_option("gitexport")
565 .map(|s| s.parse())
566 .transpose()?
567 .unwrap_or_default())
568 }
569
570 pub fn gitmode(&self) -> Result<GitMode, ()> {
572 Ok(self
573 .get_option("gitmode")
574 .map(|s| s.parse())
575 .transpose()?
576 .unwrap_or_default())
577 }
578
579 pub fn pgpmode(&self) -> Result<PgpMode, ()> {
581 Ok(self
582 .get_option("pgpmode")
583 .map(|s| s.parse())
584 .transpose()?
585 .unwrap_or_default())
586 }
587
588 pub fn searchmode(&self) -> Result<SearchMode, ()> {
590 Ok(self
591 .get_option("searchmode")
592 .map(|s| s.parse())
593 .transpose()?
594 .unwrap_or_default())
595 }
596
597 pub fn decompress(&self) -> bool {
599 self.has_option("decompress")
600 }
601
602 pub fn bare(&self) -> bool {
605 self.has_option("bare")
606 }
607
608 pub fn user_agent(&self) -> Option<String> {
610 self.get_option("user-agent")
611 }
612
613 pub fn passive(&self) -> Option<bool> {
615 if self.has_option("passive") || self.has_option("pasv") {
616 Some(true)
617 } else if self.has_option("active") || self.has_option("nopasv") {
618 Some(false)
619 } else {
620 None
621 }
622 }
623
624 pub fn unzipoptions(&self) -> Option<String> {
627 self.get_option("unzipopt")
628 }
629
630 pub fn dversionmangle(&self) -> Option<String> {
632 self.get_option("dversionmangle")
633 .or_else(|| self.get_option("versionmangle"))
634 }
635
636 pub fn dirversionmangle(&self) -> Option<String> {
640 self.get_option("dirversionmangle")
641 }
642
643 pub fn pagemangle(&self) -> Option<String> {
645 self.get_option("pagemangle")
646 }
647
648 pub fn uversionmangle(&self) -> Option<String> {
652 self.get_option("uversionmangle")
653 .or_else(|| self.get_option("versionmangle"))
654 }
655
656 pub fn versionmangle(&self) -> Option<String> {
658 self.get_option("versionmangle")
659 }
660
661 pub fn hrefdecode(&self) -> bool {
666 self.get_option("hrefdecode").is_some()
667 }
668
669 pub fn downloadurlmangle(&self) -> Option<String> {
672 self.get_option("downloadurlmangle")
673 }
674
675 pub fn filenamemangle(&self) -> Option<String> {
683 self.get_option("filenamemangle")
684 }
685
686 pub fn pgpsigurlmangle(&self) -> Option<String> {
688 self.get_option("pgpsigurlmangle")
689 }
690
691 pub fn oversionmangle(&self) -> Option<String> {
694 self.get_option("oversionmangle")
695 }
696
697 pub fn opts(&self) -> std::collections::HashMap<String, String> {
699 let mut options = std::collections::HashMap::new();
700
701 if let Some(ol) = self.option_list() {
702 for opt in ol.children() {
703 let key = opt.key();
704 let value = opt.value();
705 if let (Some(key), Some(value)) = (key, value) {
706 options.insert(key.to_string(), value.to_string());
707 }
708 }
709 }
710
711 options
712 }
713
714 fn items(&self) -> impl Iterator<Item = String> + '_ {
715 self.0.children_with_tokens().filter_map(|it| match it {
716 SyntaxElement::Token(token) => {
717 if token.kind() == VALUE || token.kind() == KEY {
718 Some(token.text().to_string())
719 } else {
720 None
721 }
722 }
723 SyntaxElement::Node(node) => {
724 match node.kind() {
726 URL => Url::cast(node).map(|n| n.url()),
727 MATCHING_PATTERN => MatchingPattern::cast(node).map(|n| n.pattern()),
728 VERSION_POLICY => VersionPolicyNode::cast(node).map(|n| n.policy()),
729 SCRIPT => ScriptNode::cast(node).map(|n| n.script()),
730 _ => None,
731 }
732 }
733 })
734 }
735
736 pub fn url(&self) -> String {
738 self.0
739 .children()
740 .find_map(Url::cast)
741 .map(|it| it.url())
742 .unwrap_or_else(|| {
743 self.items().next().unwrap()
745 })
746 }
747
748 pub fn matching_pattern(&self) -> Option<String> {
750 self.0
751 .children()
752 .find_map(MatchingPattern::cast)
753 .map(|it| it.pattern())
754 .or_else(|| {
755 self.items().nth(1)
757 })
758 }
759
760 pub fn version(&self) -> Result<Option<crate::VersionPolicy>, String> {
762 self.0
763 .children()
764 .find_map(VersionPolicyNode::cast)
765 .map(|it| it.policy().parse())
766 .transpose()
767 .or_else(|_e| {
768 self.items().nth(2).map(|it| it.parse()).transpose()
770 })
771 }
772
773 pub fn script(&self) -> Option<String> {
775 self.0
776 .children()
777 .find_map(ScriptNode::cast)
778 .map(|it| it.script())
779 .or_else(|| {
780 self.items().nth(3)
782 })
783 }
784
785 pub fn format_url(&self, package: impl FnOnce() -> String) -> url::Url {
787 subst(self.url().as_str(), package).parse().unwrap()
788 }
789
790 pub fn set_url(&mut self, new_url: &str) {
792 let mut builder = GreenNodeBuilder::new();
794 builder.start_node(URL.into());
795 builder.token(VALUE.into(), new_url);
796 builder.finish_node();
797 let new_url_green = builder.finish();
798
799 let new_url_node = SyntaxNode::new_root_mut(new_url_green);
801
802 let url_pos = self
804 .0
805 .children_with_tokens()
806 .position(|child| matches!(child, SyntaxElement::Node(node) if node.kind() == URL));
807
808 if let Some(pos) = url_pos {
809 self.0
811 .splice_children(pos..pos + 1, vec![new_url_node.into()]);
812 }
813 }
814
815 pub fn set_matching_pattern(&mut self, new_pattern: &str) {
821 let mut builder = GreenNodeBuilder::new();
823 builder.start_node(MATCHING_PATTERN.into());
824 builder.token(VALUE.into(), new_pattern);
825 builder.finish_node();
826 let new_pattern_green = builder.finish();
827
828 let new_pattern_node = SyntaxNode::new_root_mut(new_pattern_green);
830
831 let pattern_pos = self.0.children_with_tokens().position(
833 |child| matches!(child, SyntaxElement::Node(node) if node.kind() == MATCHING_PATTERN),
834 );
835
836 if let Some(pos) = pattern_pos {
837 self.0
839 .splice_children(pos..pos + 1, vec![new_pattern_node.into()]);
840 }
841 }
843
844 pub fn set_version_policy(&mut self, new_policy: &str) {
850 let mut builder = GreenNodeBuilder::new();
852 builder.start_node(VERSION_POLICY.into());
853 builder.token(VALUE.into(), new_policy);
855 builder.finish_node();
856 let new_policy_green = builder.finish();
857
858 let new_policy_node = SyntaxNode::new_root_mut(new_policy_green);
860
861 let policy_pos = self.0.children_with_tokens().position(
863 |child| matches!(child, SyntaxElement::Node(node) if node.kind() == VERSION_POLICY),
864 );
865
866 if let Some(pos) = policy_pos {
867 self.0
869 .splice_children(pos..pos + 1, vec![new_policy_node.into()]);
870 }
871 }
873
874 pub fn set_script(&mut self, new_script: &str) {
880 let mut builder = GreenNodeBuilder::new();
882 builder.start_node(SCRIPT.into());
883 builder.token(VALUE.into(), new_script);
885 builder.finish_node();
886 let new_script_green = builder.finish();
887
888 let new_script_node = SyntaxNode::new_root_mut(new_script_green);
890
891 let script_pos = self
893 .0
894 .children_with_tokens()
895 .position(|child| matches!(child, SyntaxElement::Node(node) if node.kind() == SCRIPT));
896
897 if let Some(pos) = script_pos {
898 self.0
900 .splice_children(pos..pos + 1, vec![new_script_node.into()]);
901 }
902 }
904
905 pub fn set_opt(&mut self, key: &str, value: &str) {
911 let opts_pos = self.0.children_with_tokens().position(
913 |child| matches!(child, SyntaxElement::Node(node) if node.kind() == OPTS_LIST),
914 );
915
916 if let Some(_opts_idx) = opts_pos {
917 if let Some(mut ol) = self.option_list() {
918 if let Some(mut opt) = ol.find_option(key) {
920 opt.set_value(value);
922 } else {
924 ol.add_option(key, value);
926 }
928 }
929 } else {
930 let mut builder = GreenNodeBuilder::new();
932 builder.start_node(OPTS_LIST.into());
933 builder.token(KEY.into(), "opts");
934 builder.token(EQUALS.into(), "=");
935 builder.start_node(OPTION.into());
936 builder.token(KEY.into(), key);
937 builder.token(EQUALS.into(), "=");
938 builder.token(VALUE.into(), value);
939 builder.finish_node();
940 builder.finish_node();
941 let new_opts_green = builder.finish();
942 let new_opts_node = SyntaxNode::new_root_mut(new_opts_green);
943
944 let url_pos = self
946 .0
947 .children_with_tokens()
948 .position(|child| matches!(child, SyntaxElement::Node(node) if node.kind() == URL));
949
950 if let Some(url_idx) = url_pos {
951 let mut combined_builder = GreenNodeBuilder::new();
954 combined_builder.start_node(ROOT.into()); combined_builder.token(WHITESPACE.into(), " ");
956 combined_builder.finish_node();
957 let temp_green = combined_builder.finish();
958 let temp_root = SyntaxNode::new_root_mut(temp_green);
959 let space_element = temp_root.children_with_tokens().next().unwrap();
960
961 self.0
962 .splice_children(url_idx..url_idx, vec![new_opts_node.into(), space_element]);
963 } else {
964 self.0.splice_children(0..0, vec![new_opts_node.into()]);
965 }
966 }
967 }
968
969 pub fn del_opt(&mut self, key: &str) {
976 if let Some(mut ol) = self.option_list() {
977 let option_count = ol.0.children().filter(|n| n.kind() == OPTION).count();
978
979 if option_count == 1 && ol.has_option(key) {
980 let opts_pos = self.0.children().position(|node| node.kind() == OPTS_LIST);
982
983 if let Some(opts_idx) = opts_pos {
984 self.0.splice_children(opts_idx..opts_idx + 1, vec![]);
986
987 while self.0.children_with_tokens().next().map_or(false, |e| {
989 matches!(
990 e,
991 SyntaxElement::Token(t) if t.kind() == WHITESPACE || t.kind() == CONTINUATION
992 )
993 }) {
994 self.0.splice_children(0..1, vec![]);
995 }
996 }
997 } else {
998 ol.remove_option(key);
1000 }
1001 }
1002 }
1003}
1004
1005const SUBSTITUTIONS: &[(&str, &str)] = &[
1006 ("@ANY_VERSION@", r"[-_]?(\d[\-+\.:\~\da-zA-Z]*)"),
1011 (
1014 "@ARCHIVE_EXT@",
1015 r"(?i)\.(?:tar\.xz|tar\.bz2|tar\.gz|zip|tgz|tbz|txz)",
1016 ),
1017 (
1020 "@SIGNATURE_EXT@",
1021 r"(?i)\.(?:tar\.xz|tar\.bz2|tar\.gz|zip|tgz|tbz|txz)\.(?:asc|pgp|gpg|sig|sign)",
1022 ),
1023 ("@DEB_EXT@", r"[\+~](debian|dfsg|ds|deb)(\.)?(\d+)?$"),
1025];
1026
1027pub fn subst(text: &str, package: impl FnOnce() -> String) -> String {
1028 let mut substs = SUBSTITUTIONS.to_vec();
1029 let package_name;
1030 if text.contains("@PACKAGE@") {
1031 package_name = Some(package());
1032 substs.push(("@PACKAGE@", package_name.as_deref().unwrap()));
1033 }
1034
1035 let mut text = text.to_string();
1036
1037 for (k, v) in substs {
1038 text = text.replace(k, v);
1039 }
1040
1041 text
1042}
1043
1044#[test]
1045fn test_subst() {
1046 assert_eq!(
1047 subst("@ANY_VERSION@", || unreachable!()),
1048 r"[-_]?(\d[\-+\.:\~\da-zA-Z]*)"
1049 );
1050 assert_eq!(subst("@PACKAGE@", || "dulwich".to_string()), "dulwich");
1051}
1052
1053impl std::fmt::Debug for OptionList {
1054 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1055 f.debug_struct("OptionList")
1056 .field("text", &self.0.text().to_string())
1057 .finish()
1058 }
1059}
1060
1061impl OptionList {
1062 fn children(&self) -> impl Iterator<Item = _Option> + '_ {
1063 self.0.children().filter_map(_Option::cast)
1064 }
1065
1066 pub fn has_option(&self, key: &str) -> bool {
1067 self.children().any(|it| it.key().as_deref() == Some(key))
1068 }
1069
1070 pub fn get_option(&self, key: &str) -> Option<String> {
1071 for child in self.children() {
1072 if child.key().as_deref() == Some(key) {
1073 return child.value();
1074 }
1075 }
1076 None
1077 }
1078
1079 fn find_option(&self, key: &str) -> Option<_Option> {
1081 self.children()
1082 .find(|opt| opt.key().as_deref() == Some(key))
1083 }
1084
1085 fn add_option(&mut self, key: &str, value: &str) {
1087 let option_count = self.0.children().filter(|n| n.kind() == OPTION).count();
1088
1089 let mut builder = GreenNodeBuilder::new();
1091 builder.start_node(ROOT.into()); if option_count > 0 {
1094 builder.start_node(OPTION_SEPARATOR.into());
1095 builder.token(COMMA.into(), ",");
1096 builder.finish_node();
1097 }
1098
1099 builder.start_node(OPTION.into());
1100 builder.token(KEY.into(), key);
1101 builder.token(EQUALS.into(), "=");
1102 builder.token(VALUE.into(), value);
1103 builder.finish_node();
1104
1105 builder.finish_node(); let combined_green = builder.finish();
1107
1108 let temp_root = SyntaxNode::new_root_mut(combined_green);
1110 let new_children: Vec<_> = temp_root.children_with_tokens().collect();
1111
1112 let insert_pos = self.0.children_with_tokens().count();
1113 self.0.splice_children(insert_pos..insert_pos, new_children);
1114 }
1115
1116 fn remove_option(&mut self, key: &str) -> bool {
1118 if let Some(mut opt) = self.find_option(key) {
1119 opt.remove();
1120 true
1121 } else {
1122 false
1123 }
1124 }
1125}
1126
1127impl _Option {
1128 pub fn key(&self) -> Option<String> {
1130 self.0.children_with_tokens().find_map(|it| match it {
1131 SyntaxElement::Token(token) => {
1132 if token.kind() == KEY {
1133 Some(token.text().to_string())
1134 } else {
1135 None
1136 }
1137 }
1138 _ => None,
1139 })
1140 }
1141
1142 pub fn value(&self) -> Option<String> {
1144 self.0
1145 .children_with_tokens()
1146 .filter_map(|it| match it {
1147 SyntaxElement::Token(token) => {
1148 if token.kind() == VALUE || token.kind() == KEY {
1149 Some(token.text().to_string())
1150 } else {
1151 None
1152 }
1153 }
1154 _ => None,
1155 })
1156 .nth(1)
1157 }
1158
1159 pub fn set_value(&mut self, new_value: &str) {
1161 let key = self.key().expect("Option must have a key");
1162
1163 let mut builder = GreenNodeBuilder::new();
1165 builder.start_node(OPTION.into());
1166 builder.token(KEY.into(), &key);
1167 builder.token(EQUALS.into(), "=");
1168 builder.token(VALUE.into(), new_value);
1169 builder.finish_node();
1170 let new_option_green = builder.finish();
1171 let new_option_node = SyntaxNode::new_root_mut(new_option_green);
1172
1173 if let Some(parent) = self.0.parent() {
1175 let idx = self.0.index();
1176 parent.splice_children(idx..idx + 1, vec![new_option_node.into()]);
1177 }
1178 }
1179
1180 pub fn remove(&mut self) {
1182 let next_sep = self
1184 .0
1185 .next_sibling()
1186 .filter(|n| n.kind() == OPTION_SEPARATOR);
1187 let prev_sep = self
1188 .0
1189 .prev_sibling()
1190 .filter(|n| n.kind() == OPTION_SEPARATOR);
1191
1192 if let Some(sep) = next_sep {
1194 sep.detach();
1195 } else if let Some(sep) = prev_sep {
1196 sep.detach();
1197 }
1198
1199 self.0.detach();
1201 }
1202}
1203
1204impl Url {
1205 pub fn url(&self) -> String {
1207 self.0
1208 .children_with_tokens()
1209 .find_map(|it| match it {
1210 SyntaxElement::Token(token) => {
1211 if token.kind() == VALUE {
1212 Some(token.text().to_string())
1213 } else {
1214 None
1215 }
1216 }
1217 _ => None,
1218 })
1219 .unwrap()
1220 }
1221}
1222
1223impl MatchingPattern {
1224 pub fn pattern(&self) -> String {
1226 self.0
1227 .children_with_tokens()
1228 .find_map(|it| match it {
1229 SyntaxElement::Token(token) => {
1230 if token.kind() == VALUE {
1231 Some(token.text().to_string())
1232 } else {
1233 None
1234 }
1235 }
1236 _ => None,
1237 })
1238 .unwrap()
1239 }
1240}
1241
1242impl VersionPolicyNode {
1243 pub fn policy(&self) -> String {
1245 self.0
1246 .children_with_tokens()
1247 .find_map(|it| match it {
1248 SyntaxElement::Token(token) => {
1249 if token.kind() == VALUE || token.kind() == KEY {
1251 Some(token.text().to_string())
1252 } else {
1253 None
1254 }
1255 }
1256 _ => None,
1257 })
1258 .unwrap()
1259 }
1260}
1261
1262impl ScriptNode {
1263 pub fn script(&self) -> String {
1265 self.0
1266 .children_with_tokens()
1267 .find_map(|it| match it {
1268 SyntaxElement::Token(token) => {
1269 if token.kind() == VALUE || token.kind() == KEY {
1271 Some(token.text().to_string())
1272 } else {
1273 None
1274 }
1275 }
1276 _ => None,
1277 })
1278 .unwrap()
1279 }
1280}
1281
1282#[test]
1283fn test_entry_node_structure() {
1284 let wf: super::WatchFile = r#"version=4
1286opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
1287"#
1288 .parse()
1289 .unwrap();
1290
1291 let entry = wf.entries().next().unwrap();
1292
1293 assert_eq!(entry.0.children().find(|n| n.kind() == URL).is_some(), true);
1295 assert_eq!(entry.url(), "https://example.com/releases");
1296
1297 assert_eq!(
1299 entry
1300 .0
1301 .children()
1302 .find(|n| n.kind() == MATCHING_PATTERN)
1303 .is_some(),
1304 true
1305 );
1306 assert_eq!(
1307 entry.matching_pattern(),
1308 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
1309 );
1310
1311 assert_eq!(
1313 entry
1314 .0
1315 .children()
1316 .find(|n| n.kind() == VERSION_POLICY)
1317 .is_some(),
1318 true
1319 );
1320 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
1321
1322 assert_eq!(
1324 entry.0.children().find(|n| n.kind() == SCRIPT).is_some(),
1325 true
1326 );
1327 assert_eq!(entry.script(), Some("uupdate".into()));
1328}
1329
1330#[test]
1331fn test_entry_node_structure_partial() {
1332 let wf: super::WatchFile = r#"version=4
1334https://github.com/example/tags .*/v?(\d\S+)\.tar\.gz
1335"#
1336 .parse()
1337 .unwrap();
1338
1339 let entry = wf.entries().next().unwrap();
1340
1341 assert_eq!(entry.0.children().find(|n| n.kind() == URL).is_some(), true);
1343 assert_eq!(
1344 entry
1345 .0
1346 .children()
1347 .find(|n| n.kind() == MATCHING_PATTERN)
1348 .is_some(),
1349 true
1350 );
1351
1352 assert_eq!(
1354 entry
1355 .0
1356 .children()
1357 .find(|n| n.kind() == VERSION_POLICY)
1358 .is_some(),
1359 false
1360 );
1361 assert_eq!(
1362 entry.0.children().find(|n| n.kind() == SCRIPT).is_some(),
1363 false
1364 );
1365
1366 assert_eq!(entry.url(), "https://github.com/example/tags");
1368 assert_eq!(
1369 entry.matching_pattern(),
1370 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
1371 );
1372 assert_eq!(entry.version(), Ok(None));
1373 assert_eq!(entry.script(), None);
1374}
1375
1376#[test]
1377fn test_parse_v1() {
1378 const WATCHV1: &str = r#"version=4
1379opts=bare,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1\.tar\.gz/ \
1380 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
1381"#;
1382 let parsed = parse(WATCHV1);
1383 let node = parsed.syntax();
1385 assert_eq!(
1386 format!("{:#?}", node),
1387 r#"ROOT@0..161
1388 VERSION@0..10
1389 KEY@0..7 "version"
1390 EQUALS@7..8 "="
1391 VALUE@8..9 "4"
1392 NEWLINE@9..10 "\n"
1393 ENTRY@10..161
1394 OPTS_LIST@10..86
1395 KEY@10..14 "opts"
1396 EQUALS@14..15 "="
1397 OPTION@15..19
1398 KEY@15..19 "bare"
1399 OPTION_SEPARATOR@19..20
1400 COMMA@19..20 ","
1401 OPTION@20..86
1402 KEY@20..34 "filenamemangle"
1403 EQUALS@34..35 "="
1404 VALUE@35..86 "s/.+\\/v?(\\d\\S+)\\.tar\\ ..."
1405 WHITESPACE@86..87 " "
1406 CONTINUATION@87..89 "\\\n"
1407 WHITESPACE@89..91 " "
1408 URL@91..138
1409 VALUE@91..138 "https://github.com/sy ..."
1410 WHITESPACE@138..139 " "
1411 MATCHING_PATTERN@139..160
1412 VALUE@139..160 ".*/v?(\\d\\S+)\\.tar\\.gz"
1413 NEWLINE@160..161 "\n"
1414"#
1415 );
1416
1417 let root = parsed.root();
1418 assert_eq!(root.version(), 4);
1419 let entries = root.entries().collect::<Vec<_>>();
1420 assert_eq!(entries.len(), 1);
1421 let entry = &entries[0];
1422 assert_eq!(
1423 entry.url(),
1424 "https://github.com/syncthing/syncthing-gtk/tags"
1425 );
1426 assert_eq!(
1427 entry.matching_pattern(),
1428 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
1429 );
1430 assert_eq!(entry.version(), Ok(None));
1431 assert_eq!(entry.script(), None);
1432
1433 assert_eq!(node.text(), WATCHV1);
1434}
1435
1436#[test]
1437fn test_parse_v2() {
1438 let parsed = parse(
1439 r#"version=4
1440https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
1441# comment
1442"#,
1443 );
1444 assert_eq!(parsed.errors, Vec::<String>::new());
1445 let node = parsed.syntax();
1446 assert_eq!(
1447 format!("{:#?}", node),
1448 r###"ROOT@0..90
1449 VERSION@0..10
1450 KEY@0..7 "version"
1451 EQUALS@7..8 "="
1452 VALUE@8..9 "4"
1453 NEWLINE@9..10 "\n"
1454 ENTRY@10..80
1455 URL@10..57
1456 VALUE@10..57 "https://github.com/sy ..."
1457 WHITESPACE@57..58 " "
1458 MATCHING_PATTERN@58..79
1459 VALUE@58..79 ".*/v?(\\d\\S+)\\.tar\\.gz"
1460 NEWLINE@79..80 "\n"
1461 COMMENT@80..89 "# comment"
1462 NEWLINE@89..90 "\n"
1463"###
1464 );
1465
1466 let root = parsed.root();
1467 assert_eq!(root.version(), 4);
1468 let entries = root.entries().collect::<Vec<_>>();
1469 assert_eq!(entries.len(), 1);
1470 let entry = &entries[0];
1471 assert_eq!(
1472 entry.url(),
1473 "https://github.com/syncthing/syncthing-gtk/tags"
1474 );
1475 assert_eq!(
1476 entry.format_url(|| "syncthing-gtk".to_string()),
1477 "https://github.com/syncthing/syncthing-gtk/tags"
1478 .parse()
1479 .unwrap()
1480 );
1481}
1482
1483#[test]
1484fn test_parse_v3() {
1485 let parsed = parse(
1486 r#"version=4
1487https://github.com/syncthing/@PACKAGE@/tags .*/v?(\d\S+)\.tar\.gz
1488# comment
1489"#,
1490 );
1491 assert_eq!(parsed.errors, Vec::<String>::new());
1492 let root = parsed.root();
1493 assert_eq!(root.version(), 4);
1494 let entries = root.entries().collect::<Vec<_>>();
1495 assert_eq!(entries.len(), 1);
1496 let entry = &entries[0];
1497 assert_eq!(entry.url(), "https://github.com/syncthing/@PACKAGE@/tags");
1498 assert_eq!(
1499 entry.format_url(|| "syncthing-gtk".to_string()),
1500 "https://github.com/syncthing/syncthing-gtk/tags"
1501 .parse()
1502 .unwrap()
1503 );
1504}
1505
1506#[test]
1507fn test_parse_v4() {
1508 let cl: super::WatchFile = r#"version=4
1509opts=repack,compression=xz,dversionmangle=s/\+ds//,repacksuffix=+ds \
1510 https://github.com/example/example-cat/tags \
1511 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
1512"#
1513 .parse()
1514 .unwrap();
1515 assert_eq!(cl.version(), 4);
1516 let entries = cl.entries().collect::<Vec<_>>();
1517 assert_eq!(entries.len(), 1);
1518 let entry = &entries[0];
1519 assert_eq!(entry.url(), "https://github.com/example/example-cat/tags");
1520 assert_eq!(
1521 entry.matching_pattern(),
1522 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
1523 );
1524 assert!(entry.repack());
1525 assert_eq!(entry.compression(), Ok(Some(Compression::Xz)));
1526 assert_eq!(entry.dversionmangle(), Some("s/\\+ds//".into()));
1527 assert_eq!(entry.repacksuffix(), Some("+ds".into()));
1528 assert_eq!(entry.script(), Some("uupdate".into()));
1529 assert_eq!(
1530 entry.format_url(|| "example-cat".to_string()),
1531 "https://github.com/example/example-cat/tags"
1532 .parse()
1533 .unwrap()
1534 );
1535 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
1536}
1537
1538#[test]
1539fn test_git_mode() {
1540 let text = r#"version=3
1541opts="mode=git, gitmode=shallow, pgpmode=gittag" \
1542https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git \
1543refs/tags/(.*) debian
1544"#;
1545 let parsed = parse(text);
1546 assert_eq!(parsed.errors, Vec::<String>::new());
1547 let cl = parsed.root();
1548 assert_eq!(cl.version(), 3);
1549 let entries = cl.entries().collect::<Vec<_>>();
1550 assert_eq!(entries.len(), 1);
1551 let entry = &entries[0];
1552 assert_eq!(
1553 entry.url(),
1554 "https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git"
1555 );
1556 assert_eq!(entry.matching_pattern(), Some("refs/tags/(.*)".into()));
1557 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
1558 assert_eq!(entry.script(), None);
1559 assert_eq!(entry.gitmode(), Ok(GitMode::Shallow));
1560 assert_eq!(entry.pgpmode(), Ok(PgpMode::GitTag));
1561 assert_eq!(entry.mode(), Ok(Mode::Git));
1562}
1563
1564#[test]
1565fn test_parse_quoted() {
1566 const WATCHV1: &str = r#"version=4
1567opts="bare, filenamemangle=blah" \
1568 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
1569"#;
1570 let parsed = parse(WATCHV1);
1571 let node = parsed.syntax();
1573
1574 let root = parsed.root();
1575 assert_eq!(root.version(), 4);
1576 let entries = root.entries().collect::<Vec<_>>();
1577 assert_eq!(entries.len(), 1);
1578 let entry = &entries[0];
1579
1580 assert_eq!(
1581 entry.url(),
1582 "https://github.com/syncthing/syncthing-gtk/tags"
1583 );
1584 assert_eq!(
1585 entry.matching_pattern(),
1586 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
1587 );
1588 assert_eq!(entry.version(), Ok(None));
1589 assert_eq!(entry.script(), None);
1590
1591 assert_eq!(node.text(), WATCHV1);
1592}
1593
1594#[test]
1595fn test_set_url() {
1596 let wf: super::WatchFile = r#"version=4
1598https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
1599"#
1600 .parse()
1601 .unwrap();
1602
1603 let mut entry = wf.entries().next().unwrap();
1604 assert_eq!(
1605 entry.url(),
1606 "https://github.com/syncthing/syncthing-gtk/tags"
1607 );
1608
1609 entry.set_url("https://newurl.example.org/path");
1610 assert_eq!(entry.url(), "https://newurl.example.org/path");
1611 assert_eq!(
1612 entry.matching_pattern(),
1613 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
1614 );
1615
1616 assert_eq!(
1618 entry.to_string(),
1619 "https://newurl.example.org/path .*/v?(\\d\\S+)\\.tar\\.gz\n"
1620 );
1621}
1622
1623#[test]
1624fn test_set_url_with_options() {
1625 let wf: super::WatchFile = r#"version=4
1627opts=foo=blah https://foo.com/bar .*/v?(\d\S+)\.tar\.gz
1628"#
1629 .parse()
1630 .unwrap();
1631
1632 let mut entry = wf.entries().next().unwrap();
1633 assert_eq!(entry.url(), "https://foo.com/bar");
1634 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
1635
1636 entry.set_url("https://example.com/baz");
1637 assert_eq!(entry.url(), "https://example.com/baz");
1638
1639 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
1641 assert_eq!(
1642 entry.matching_pattern(),
1643 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
1644 );
1645
1646 assert_eq!(
1648 entry.to_string(),
1649 "opts=foo=blah https://example.com/baz .*/v?(\\d\\S+)\\.tar\\.gz\n"
1650 );
1651}
1652
1653#[test]
1654fn test_set_url_complex() {
1655 let wf: super::WatchFile = r#"version=4
1657opts=bare,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1\.tar\.gz/ \
1658 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
1659"#
1660 .parse()
1661 .unwrap();
1662
1663 let mut entry = wf.entries().next().unwrap();
1664 assert_eq!(
1665 entry.url(),
1666 "https://github.com/syncthing/syncthing-gtk/tags"
1667 );
1668
1669 entry.set_url("https://gitlab.com/newproject/tags");
1670 assert_eq!(entry.url(), "https://gitlab.com/newproject/tags");
1671
1672 assert!(entry.bare());
1674 assert_eq!(
1675 entry.filenamemangle(),
1676 Some("s/.+\\/v?(\\d\\S+)\\.tar\\.gz/syncthing-gtk-$1\\.tar\\.gz/".into())
1677 );
1678 assert_eq!(
1679 entry.matching_pattern(),
1680 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
1681 );
1682
1683 assert_eq!(
1685 entry.to_string(),
1686 r#"opts=bare,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1\.tar\.gz/ \
1687 https://gitlab.com/newproject/tags .*/v?(\d\S+)\.tar\.gz
1688"#
1689 );
1690}
1691
1692#[test]
1693fn test_set_url_with_all_fields() {
1694 let wf: super::WatchFile = r#"version=4
1696opts=repack,compression=xz,dversionmangle=s/\+ds//,repacksuffix=+ds \
1697 https://github.com/example/example-cat/tags \
1698 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
1699"#
1700 .parse()
1701 .unwrap();
1702
1703 let mut entry = wf.entries().next().unwrap();
1704 assert_eq!(entry.url(), "https://github.com/example/example-cat/tags");
1705 assert_eq!(
1706 entry.matching_pattern(),
1707 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
1708 );
1709 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
1710 assert_eq!(entry.script(), Some("uupdate".into()));
1711
1712 entry.set_url("https://gitlab.example.org/project/releases");
1713 assert_eq!(entry.url(), "https://gitlab.example.org/project/releases");
1714
1715 assert!(entry.repack());
1717 assert_eq!(entry.compression(), Ok(Some(super::Compression::Xz)));
1718 assert_eq!(entry.dversionmangle(), Some("s/\\+ds//".into()));
1719 assert_eq!(entry.repacksuffix(), Some("+ds".into()));
1720 assert_eq!(
1721 entry.matching_pattern(),
1722 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
1723 );
1724 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
1725 assert_eq!(entry.script(), Some("uupdate".into()));
1726
1727 assert_eq!(
1729 entry.to_string(),
1730 r#"opts=repack,compression=xz,dversionmangle=s/\+ds//,repacksuffix=+ds \
1731 https://gitlab.example.org/project/releases \
1732 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
1733"#
1734 );
1735}
1736
1737#[test]
1738fn test_set_url_quoted_options() {
1739 let wf: super::WatchFile = r#"version=4
1741opts="bare, filenamemangle=blah" \
1742 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
1743"#
1744 .parse()
1745 .unwrap();
1746
1747 let mut entry = wf.entries().next().unwrap();
1748 assert_eq!(
1749 entry.url(),
1750 "https://github.com/syncthing/syncthing-gtk/tags"
1751 );
1752
1753 entry.set_url("https://example.org/new/path");
1754 assert_eq!(entry.url(), "https://example.org/new/path");
1755
1756 assert_eq!(
1758 entry.to_string(),
1759 r#"opts="bare, filenamemangle=blah" \
1760 https://example.org/new/path .*/v?(\d\S+)\.tar\.gz
1761"#
1762 );
1763}
1764
1765#[test]
1766fn test_set_opt_update_existing() {
1767 let wf: super::WatchFile = r#"version=4
1769opts=foo=blah,bar=baz https://example.com/releases .*/v?(\d\S+)\.tar\.gz
1770"#
1771 .parse()
1772 .unwrap();
1773
1774 let mut entry = wf.entries().next().unwrap();
1775 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
1776 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
1777
1778 entry.set_opt("foo", "updated");
1779 assert_eq!(entry.get_option("foo"), Some("updated".to_string()));
1780 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
1781
1782 assert_eq!(
1784 entry.to_string(),
1785 "opts=foo=updated,bar=baz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
1786 );
1787}
1788
1789#[test]
1790fn test_set_opt_add_new() {
1791 let wf: super::WatchFile = r#"version=4
1793opts=foo=blah https://example.com/releases .*/v?(\d\S+)\.tar\.gz
1794"#
1795 .parse()
1796 .unwrap();
1797
1798 let mut entry = wf.entries().next().unwrap();
1799 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
1800 assert_eq!(entry.get_option("bar"), None);
1801
1802 entry.set_opt("bar", "baz");
1803 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
1804 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
1805
1806 assert_eq!(
1808 entry.to_string(),
1809 "opts=foo=blah,bar=baz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
1810 );
1811}
1812
1813#[test]
1814fn test_set_opt_create_options_list() {
1815 let wf: super::WatchFile = r#"version=4
1817https://example.com/releases .*/v?(\d\S+)\.tar\.gz
1818"#
1819 .parse()
1820 .unwrap();
1821
1822 let mut entry = wf.entries().next().unwrap();
1823 assert_eq!(entry.option_list(), None);
1824
1825 entry.set_opt("compression", "xz");
1826 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
1827
1828 assert_eq!(
1830 entry.to_string(),
1831 "opts=compression=xz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
1832 );
1833}
1834
1835#[test]
1836fn test_del_opt_remove_single() {
1837 let wf: super::WatchFile = r#"version=4
1839opts=foo=blah,bar=baz,qux=quux https://example.com/releases .*/v?(\d\S+)\.tar\.gz
1840"#
1841 .parse()
1842 .unwrap();
1843
1844 let mut entry = wf.entries().next().unwrap();
1845 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
1846 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
1847 assert_eq!(entry.get_option("qux"), Some("quux".to_string()));
1848
1849 entry.del_opt("bar");
1850 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
1851 assert_eq!(entry.get_option("bar"), None);
1852 assert_eq!(entry.get_option("qux"), Some("quux".to_string()));
1853
1854 assert_eq!(
1856 entry.to_string(),
1857 "opts=foo=blah,qux=quux https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
1858 );
1859}
1860
1861#[test]
1862fn test_del_opt_remove_first() {
1863 let wf: super::WatchFile = r#"version=4
1865opts=foo=blah,bar=baz https://example.com/releases .*/v?(\d\S+)\.tar\.gz
1866"#
1867 .parse()
1868 .unwrap();
1869
1870 let mut entry = wf.entries().next().unwrap();
1871 entry.del_opt("foo");
1872 assert_eq!(entry.get_option("foo"), None);
1873 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
1874
1875 assert_eq!(
1877 entry.to_string(),
1878 "opts=bar=baz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
1879 );
1880}
1881
1882#[test]
1883fn test_del_opt_remove_last() {
1884 let wf: super::WatchFile = r#"version=4
1886opts=foo=blah,bar=baz https://example.com/releases .*/v?(\d\S+)\.tar\.gz
1887"#
1888 .parse()
1889 .unwrap();
1890
1891 let mut entry = wf.entries().next().unwrap();
1892 entry.del_opt("bar");
1893 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
1894 assert_eq!(entry.get_option("bar"), None);
1895
1896 assert_eq!(
1898 entry.to_string(),
1899 "opts=foo=blah https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
1900 );
1901}
1902
1903#[test]
1904fn test_del_opt_remove_only_option() {
1905 let wf: super::WatchFile = r#"version=4
1907opts=foo=blah https://example.com/releases .*/v?(\d\S+)\.tar\.gz
1908"#
1909 .parse()
1910 .unwrap();
1911
1912 let mut entry = wf.entries().next().unwrap();
1913 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
1914
1915 entry.del_opt("foo");
1916 assert_eq!(entry.get_option("foo"), None);
1917 assert_eq!(entry.option_list(), None);
1918
1919 assert_eq!(
1921 entry.to_string(),
1922 "https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
1923 );
1924}
1925
1926#[test]
1927fn test_del_opt_nonexistent() {
1928 let wf: super::WatchFile = r#"version=4
1930opts=foo=blah https://example.com/releases .*/v?(\d\S+)\.tar\.gz
1931"#
1932 .parse()
1933 .unwrap();
1934
1935 let mut entry = wf.entries().next().unwrap();
1936 let original = entry.to_string();
1937
1938 entry.del_opt("nonexistent");
1939 assert_eq!(entry.to_string(), original);
1940}
1941
1942#[test]
1943fn test_set_opt_multiple_operations() {
1944 let wf: super::WatchFile = r#"version=4
1946https://example.com/releases .*/v?(\d\S+)\.tar\.gz
1947"#
1948 .parse()
1949 .unwrap();
1950
1951 let mut entry = wf.entries().next().unwrap();
1952
1953 entry.set_opt("compression", "xz");
1954 entry.set_opt("repack", "");
1955 entry.set_opt("dversionmangle", "s/\\+ds//");
1956
1957 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
1958 assert_eq!(
1959 entry.get_option("dversionmangle"),
1960 Some("s/\\+ds//".to_string())
1961 );
1962}
1963
1964#[test]
1965fn test_set_matching_pattern() {
1966 let wf: super::WatchFile = r#"version=4
1968https://github.com/example/tags .*/v?(\d\S+)\.tar\.gz
1969"#
1970 .parse()
1971 .unwrap();
1972
1973 let mut entry = wf.entries().next().unwrap();
1974 assert_eq!(
1975 entry.matching_pattern(),
1976 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
1977 );
1978
1979 entry.set_matching_pattern("(?:.*?/)?v?([\\d.]+)\\.tar\\.gz");
1980 assert_eq!(
1981 entry.matching_pattern(),
1982 Some("(?:.*?/)?v?([\\d.]+)\\.tar\\.gz".into())
1983 );
1984
1985 assert_eq!(entry.url(), "https://github.com/example/tags");
1987
1988 assert_eq!(
1990 entry.to_string(),
1991 "https://github.com/example/tags (?:.*?/)?v?([\\d.]+)\\.tar\\.gz\n"
1992 );
1993}
1994
1995#[test]
1996fn test_set_matching_pattern_with_all_fields() {
1997 let wf: super::WatchFile = r#"version=4
1999opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2000"#
2001 .parse()
2002 .unwrap();
2003
2004 let mut entry = wf.entries().next().unwrap();
2005 assert_eq!(
2006 entry.matching_pattern(),
2007 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2008 );
2009
2010 entry.set_matching_pattern(".*/version-([\\d.]+)\\.tar\\.xz");
2011 assert_eq!(
2012 entry.matching_pattern(),
2013 Some(".*/version-([\\d.]+)\\.tar\\.xz".into())
2014 );
2015
2016 assert_eq!(entry.url(), "https://example.com/releases");
2018 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2019 assert_eq!(entry.script(), Some("uupdate".into()));
2020 assert_eq!(entry.compression(), Ok(Some(super::Compression::Xz)));
2021
2022 assert_eq!(
2024 entry.to_string(),
2025 "opts=compression=xz https://example.com/releases .*/version-([\\d.]+)\\.tar\\.xz debian uupdate\n"
2026 );
2027}
2028
2029#[test]
2030fn test_set_version_policy() {
2031 let wf: super::WatchFile = r#"version=4
2033https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2034"#
2035 .parse()
2036 .unwrap();
2037
2038 let mut entry = wf.entries().next().unwrap();
2039 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2040
2041 entry.set_version_policy("previous");
2042 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Previous)));
2043
2044 assert_eq!(entry.url(), "https://example.com/releases");
2046 assert_eq!(
2047 entry.matching_pattern(),
2048 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2049 );
2050 assert_eq!(entry.script(), Some("uupdate".into()));
2051
2052 assert_eq!(
2054 entry.to_string(),
2055 "https://example.com/releases (?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz previous uupdate\n"
2056 );
2057}
2058
2059#[test]
2060fn test_set_version_policy_with_options() {
2061 let wf: super::WatchFile = r#"version=4
2063opts=repack,compression=xz \
2064 https://github.com/example/example-cat/tags \
2065 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2066"#
2067 .parse()
2068 .unwrap();
2069
2070 let mut entry = wf.entries().next().unwrap();
2071 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2072
2073 entry.set_version_policy("ignore");
2074 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Ignore)));
2075
2076 assert_eq!(entry.url(), "https://github.com/example/example-cat/tags");
2078 assert_eq!(
2079 entry.matching_pattern(),
2080 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2081 );
2082 assert_eq!(entry.script(), Some("uupdate".into()));
2083 assert!(entry.repack());
2084
2085 assert_eq!(
2087 entry.to_string(),
2088 r#"opts=repack,compression=xz \
2089 https://github.com/example/example-cat/tags \
2090 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz ignore uupdate
2091"#
2092 );
2093}
2094
2095#[test]
2096fn test_set_script() {
2097 let wf: super::WatchFile = r#"version=4
2099https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2100"#
2101 .parse()
2102 .unwrap();
2103
2104 let mut entry = wf.entries().next().unwrap();
2105 assert_eq!(entry.script(), Some("uupdate".into()));
2106
2107 entry.set_script("uscan");
2108 assert_eq!(entry.script(), Some("uscan".into()));
2109
2110 assert_eq!(entry.url(), "https://example.com/releases");
2112 assert_eq!(
2113 entry.matching_pattern(),
2114 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2115 );
2116 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2117
2118 assert_eq!(
2120 entry.to_string(),
2121 "https://example.com/releases (?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz debian uscan\n"
2122 );
2123}
2124
2125#[test]
2126fn test_set_script_with_options() {
2127 let wf: super::WatchFile = r#"version=4
2129opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2130"#
2131 .parse()
2132 .unwrap();
2133
2134 let mut entry = wf.entries().next().unwrap();
2135 assert_eq!(entry.script(), Some("uupdate".into()));
2136
2137 entry.set_script("custom-script.sh");
2138 assert_eq!(entry.script(), Some("custom-script.sh".into()));
2139
2140 assert_eq!(entry.url(), "https://example.com/releases");
2142 assert_eq!(
2143 entry.matching_pattern(),
2144 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2145 );
2146 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2147 assert_eq!(entry.compression(), Ok(Some(super::Compression::Xz)));
2148
2149 assert_eq!(
2151 entry.to_string(),
2152 "opts=compression=xz https://example.com/releases (?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz debian custom-script.sh\n"
2153 );
2154}