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 #[cfg(feature = "discover")]
480 pub async fn uscan(
481 &self,
482 package: impl Fn() -> String,
483 ) -> Result<Vec<Vec<crate::Release>>, Box<dyn std::error::Error>> {
484 let mut all_releases = Vec::new();
485
486 for entry in self.entries() {
487 let releases = entry.discover(|| package()).await?;
488 all_releases.push(releases);
489 }
490
491 Ok(all_releases)
492 }
493
494 #[cfg(all(feature = "discover", feature = "blocking"))]
512 pub fn uscan_blocking(
513 &self,
514 package: impl Fn() -> String,
515 ) -> Result<Vec<Vec<crate::Release>>, Box<dyn std::error::Error>> {
516 let mut all_releases = Vec::new();
517
518 for entry in self.entries() {
519 let releases = entry.discover_blocking(|| package())?;
520 all_releases.push(releases);
521 }
522
523 Ok(all_releases)
524 }
525
526 pub fn add_entry(&mut self, entry: Entry) {
553 let insert_pos = self.0.children_with_tokens().count();
555
556 let entry_green = entry.0.green().into_owned();
558 let new_entry_node = SyntaxNode::new_root_mut(entry_green);
559
560 self.0
562 .splice_children(insert_pos..insert_pos, vec![new_entry_node.into()]);
563 }
564}
565
566impl FromStr for WatchFile {
567 type Err = ParseError;
568
569 fn from_str(s: &str) -> Result<Self, Self::Err> {
570 let parsed = parse(s);
571 if parsed.errors.is_empty() {
572 Ok(parsed.root())
573 } else {
574 Err(ParseError(parsed.errors))
575 }
576 }
577}
578
579impl Version {
580 pub fn version(&self) -> u32 {
582 self.0
583 .children_with_tokens()
584 .find_map(|it| match it {
585 SyntaxElement::Token(token) => {
586 if token.kind() == VALUE {
587 Some(token.text().parse().unwrap())
588 } else {
589 None
590 }
591 }
592 _ => None,
593 })
594 .unwrap_or(DEFAULT_VERSION)
595 }
596}
597
598#[derive(Debug, Clone, Default)]
622pub struct EntryBuilder {
623 url: Option<String>,
624 matching_pattern: Option<String>,
625 version_policy: Option<String>,
626 script: Option<String>,
627 opts: std::collections::HashMap<String, String>,
628}
629
630impl EntryBuilder {
631 pub fn new(url: impl Into<String>) -> Self {
633 EntryBuilder {
634 url: Some(url.into()),
635 matching_pattern: None,
636 version_policy: None,
637 script: None,
638 opts: std::collections::HashMap::new(),
639 }
640 }
641
642 pub fn matching_pattern(mut self, pattern: impl Into<String>) -> Self {
644 self.matching_pattern = Some(pattern.into());
645 self
646 }
647
648 pub fn version_policy(mut self, policy: impl Into<String>) -> Self {
650 self.version_policy = Some(policy.into());
651 self
652 }
653
654 pub fn script(mut self, script: impl Into<String>) -> Self {
656 self.script = Some(script.into());
657 self
658 }
659
660 pub fn opt(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
662 self.opts.insert(key.into(), value.into());
663 self
664 }
665
666 pub fn flag(mut self, key: impl Into<String>) -> Self {
670 self.opts.insert(key.into(), String::new());
671 self
672 }
673
674 pub fn build(self) -> Entry {
680 let url = self.url.expect("URL is required for entry");
681
682 let mut builder = GreenNodeBuilder::new();
683
684 builder.start_node(ENTRY.into());
685
686 if !self.opts.is_empty() {
688 builder.start_node(OPTS_LIST.into());
689 builder.token(KEY.into(), "opts");
690 builder.token(EQUALS.into(), "=");
691
692 let mut first = true;
693 for (key, value) in self.opts.iter() {
694 if !first {
695 builder.token(COMMA.into(), ",");
696 }
697 first = false;
698
699 builder.start_node(OPTION.into());
700 builder.token(KEY.into(), key);
701 if !value.is_empty() {
702 builder.token(EQUALS.into(), "=");
703 builder.token(VALUE.into(), value);
704 }
705 builder.finish_node();
706 }
707
708 builder.finish_node();
709 builder.token(WHITESPACE.into(), " ");
710 }
711
712 builder.start_node(URL.into());
714 builder.token(VALUE.into(), &url);
715 builder.finish_node();
716
717 if let Some(pattern) = self.matching_pattern {
719 builder.token(WHITESPACE.into(), " ");
720 builder.start_node(MATCHING_PATTERN.into());
721 builder.token(VALUE.into(), &pattern);
722 builder.finish_node();
723 }
724
725 if let Some(policy) = self.version_policy {
727 builder.token(WHITESPACE.into(), " ");
728 builder.start_node(VERSION_POLICY.into());
729 builder.token(VALUE.into(), &policy);
730 builder.finish_node();
731 }
732
733 if let Some(script_val) = self.script {
735 builder.token(WHITESPACE.into(), " ");
736 builder.start_node(SCRIPT.into());
737 builder.token(VALUE.into(), &script_val);
738 builder.finish_node();
739 }
740
741 builder.token(NEWLINE.into(), "\n");
742 builder.finish_node();
743
744 Entry(SyntaxNode::new_root_mut(builder.finish()))
745 }
746}
747
748impl Entry {
749 pub fn builder(url: impl Into<String>) -> EntryBuilder {
763 EntryBuilder::new(url)
764 }
765
766 pub fn option_list(&self) -> Option<OptionList> {
768 self.0.children().find_map(OptionList::cast)
769 }
770
771 pub fn get_option(&self, key: &str) -> Option<String> {
773 self.option_list().and_then(|ol| ol.get_option(key))
774 }
775
776 pub fn has_option(&self, key: &str) -> bool {
778 self.option_list().map_or(false, |ol| ol.has_option(key))
779 }
780
781 pub fn component(&self) -> Option<String> {
783 self.get_option("component")
784 }
785
786 pub fn ctype(&self) -> Result<Option<ComponentType>, ()> {
788 self.get_option("ctype").map(|s| s.parse()).transpose()
789 }
790
791 pub fn compression(&self) -> Result<Option<Compression>, ()> {
793 self.get_option("compression")
794 .map(|s| s.parse())
795 .transpose()
796 }
797
798 pub fn repack(&self) -> bool {
800 self.has_option("repack")
801 }
802
803 pub fn repacksuffix(&self) -> Option<String> {
805 self.get_option("repacksuffix")
806 }
807
808 pub fn mode(&self) -> Result<Mode, ()> {
810 Ok(self
811 .get_option("mode")
812 .map(|s| s.parse())
813 .transpose()?
814 .unwrap_or_default())
815 }
816
817 pub fn pretty(&self) -> Result<Pretty, ()> {
819 Ok(self
820 .get_option("pretty")
821 .map(|s| s.parse())
822 .transpose()?
823 .unwrap_or_default())
824 }
825
826 pub fn date(&self) -> String {
829 self.get_option("date")
830 .unwrap_or_else(|| "%Y%m%d".to_string())
831 }
832
833 pub fn gitexport(&self) -> Result<GitExport, ()> {
835 Ok(self
836 .get_option("gitexport")
837 .map(|s| s.parse())
838 .transpose()?
839 .unwrap_or_default())
840 }
841
842 pub fn gitmode(&self) -> Result<GitMode, ()> {
844 Ok(self
845 .get_option("gitmode")
846 .map(|s| s.parse())
847 .transpose()?
848 .unwrap_or_default())
849 }
850
851 pub fn pgpmode(&self) -> Result<PgpMode, ()> {
853 Ok(self
854 .get_option("pgpmode")
855 .map(|s| s.parse())
856 .transpose()?
857 .unwrap_or_default())
858 }
859
860 pub fn searchmode(&self) -> Result<SearchMode, ()> {
862 Ok(self
863 .get_option("searchmode")
864 .map(|s| s.parse())
865 .transpose()?
866 .unwrap_or_default())
867 }
868
869 pub fn decompress(&self) -> bool {
871 self.has_option("decompress")
872 }
873
874 pub fn bare(&self) -> bool {
877 self.has_option("bare")
878 }
879
880 pub fn user_agent(&self) -> Option<String> {
882 self.get_option("user-agent")
883 }
884
885 pub fn passive(&self) -> Option<bool> {
887 if self.has_option("passive") || self.has_option("pasv") {
888 Some(true)
889 } else if self.has_option("active") || self.has_option("nopasv") {
890 Some(false)
891 } else {
892 None
893 }
894 }
895
896 pub fn unzipoptions(&self) -> Option<String> {
899 self.get_option("unzipopt")
900 }
901
902 pub fn dversionmangle(&self) -> Option<String> {
904 self.get_option("dversionmangle")
905 .or_else(|| self.get_option("versionmangle"))
906 }
907
908 pub fn dirversionmangle(&self) -> Option<String> {
912 self.get_option("dirversionmangle")
913 }
914
915 pub fn pagemangle(&self) -> Option<String> {
917 self.get_option("pagemangle")
918 }
919
920 pub fn uversionmangle(&self) -> Option<String> {
924 self.get_option("uversionmangle")
925 .or_else(|| self.get_option("versionmangle"))
926 }
927
928 pub fn versionmangle(&self) -> Option<String> {
930 self.get_option("versionmangle")
931 }
932
933 pub fn hrefdecode(&self) -> bool {
938 self.get_option("hrefdecode").is_some()
939 }
940
941 pub fn downloadurlmangle(&self) -> Option<String> {
944 self.get_option("downloadurlmangle")
945 }
946
947 pub fn filenamemangle(&self) -> Option<String> {
955 self.get_option("filenamemangle")
956 }
957
958 pub fn pgpsigurlmangle(&self) -> Option<String> {
960 self.get_option("pgpsigurlmangle")
961 }
962
963 pub fn oversionmangle(&self) -> Option<String> {
966 self.get_option("oversionmangle")
967 }
968
969 pub fn apply_uversionmangle(
982 &self,
983 version: &str,
984 ) -> Result<String, crate::mangle::MangleError> {
985 if let Some(vm) = self.uversionmangle() {
986 crate::mangle::apply_mangle(&vm, version)
987 } else {
988 Ok(version.to_string())
989 }
990 }
991
992 pub fn apply_dversionmangle(
1005 &self,
1006 version: &str,
1007 ) -> Result<String, crate::mangle::MangleError> {
1008 if let Some(vm) = self.dversionmangle() {
1009 crate::mangle::apply_mangle(&vm, version)
1010 } else {
1011 Ok(version.to_string())
1012 }
1013 }
1014
1015 pub fn apply_oversionmangle(
1028 &self,
1029 version: &str,
1030 ) -> Result<String, crate::mangle::MangleError> {
1031 if let Some(vm) = self.oversionmangle() {
1032 crate::mangle::apply_mangle(&vm, version)
1033 } else {
1034 Ok(version.to_string())
1035 }
1036 }
1037
1038 pub fn apply_dirversionmangle(
1051 &self,
1052 version: &str,
1053 ) -> Result<String, crate::mangle::MangleError> {
1054 if let Some(vm) = self.dirversionmangle() {
1055 crate::mangle::apply_mangle(&vm, version)
1056 } else {
1057 Ok(version.to_string())
1058 }
1059 }
1060
1061 pub fn apply_filenamemangle(&self, url: &str) -> Result<String, crate::mangle::MangleError> {
1077 if let Some(vm) = self.filenamemangle() {
1078 crate::mangle::apply_mangle(&vm, url)
1079 } else {
1080 Ok(url.to_string())
1081 }
1082 }
1083
1084 pub fn apply_pagemangle(&self, page: &[u8]) -> Result<Vec<u8>, crate::mangle::MangleError> {
1100 if let Some(vm) = self.pagemangle() {
1101 let page_str = String::from_utf8_lossy(page);
1102 let mangled = crate::mangle::apply_mangle(&vm, &page_str)?;
1103 Ok(mangled.into_bytes())
1104 } else {
1105 Ok(page.to_vec())
1106 }
1107 }
1108
1109 pub fn apply_downloadurlmangle(&self, url: &str) -> Result<String, crate::mangle::MangleError> {
1125 if let Some(vm) = self.downloadurlmangle() {
1126 crate::mangle::apply_mangle(&vm, url)
1127 } else {
1128 Ok(url.to_string())
1129 }
1130 }
1131
1132 #[cfg(feature = "discover")]
1153 pub async fn discover(
1154 &self,
1155 package: impl FnOnce() -> String,
1156 ) -> Result<Vec<crate::Release>, Box<dyn std::error::Error>> {
1157 let url = self.format_url(package);
1158 let user_agent = self
1159 .user_agent()
1160 .unwrap_or_else(|| crate::DEFAULT_USER_AGENT.to_string());
1161 let searchmode = self.searchmode().unwrap_or(crate::SearchMode::Html);
1162
1163 let client = reqwest::Client::builder().user_agent(user_agent).build()?;
1164
1165 let response = client.get(url.as_str()).send().await?;
1166 let body = response.bytes().await?;
1167
1168 let mangled_body = self.apply_pagemangle(&body)?;
1170
1171 let matching_pattern = self
1172 .matching_pattern()
1173 .ok_or("matching_pattern is required")?;
1174
1175 let package_name = String::new(); let results = crate::search::search(
1177 match searchmode {
1178 crate::SearchMode::Html => "html",
1179 crate::SearchMode::Plain => "plain",
1180 },
1181 std::io::Cursor::new(mangled_body.as_ref() as &[u8]),
1182 &subst(&matching_pattern, || package_name.clone()),
1183 &package_name,
1184 url.as_str(),
1185 )?;
1186
1187 let mut releases = Vec::new();
1188 for (version, full_url) in results {
1189 let mangled_version = self.apply_uversionmangle(&version)?;
1191
1192 let mangled_url = self.apply_downloadurlmangle(&full_url)?;
1194
1195 let pgpsigurl = if let Some(mangle) = self.pgpsigurlmangle() {
1197 Some(crate::mangle::apply_mangle(&mangle, &mangled_url)?)
1198 } else {
1199 None
1200 };
1201
1202 let target_filename = if self.filenamemangle().is_some() {
1204 Some(self.apply_filenamemangle(&mangled_url)?)
1205 } else {
1206 None
1207 };
1208
1209 let package_version = if self.oversionmangle().is_some() {
1211 Some(self.apply_oversionmangle(&mangled_version)?)
1212 } else {
1213 None
1214 };
1215
1216 releases.push(crate::Release::new_full(
1217 mangled_version,
1218 mangled_url,
1219 pgpsigurl,
1220 target_filename,
1221 package_version,
1222 ));
1223 }
1224
1225 Ok(releases)
1226 }
1227
1228 #[cfg(all(feature = "discover", feature = "blocking"))]
1247 pub fn discover_blocking(
1248 &self,
1249 package: impl FnOnce() -> String,
1250 ) -> Result<Vec<crate::Release>, Box<dyn std::error::Error>> {
1251 let url = self.format_url(package);
1252 let user_agent = self
1253 .user_agent()
1254 .unwrap_or_else(|| crate::DEFAULT_USER_AGENT.to_string());
1255 let searchmode = self.searchmode().unwrap_or(crate::SearchMode::Html);
1256
1257 let client = reqwest::blocking::Client::builder()
1258 .user_agent(user_agent)
1259 .build()?;
1260
1261 let response = client.get(url.as_str()).send()?;
1262 let body = response.bytes()?;
1263
1264 let mangled_body = self.apply_pagemangle(&body)?;
1266
1267 let matching_pattern = self
1268 .matching_pattern()
1269 .ok_or("matching_pattern is required")?;
1270
1271 let package_name = String::new(); let results = crate::search::search(
1273 match searchmode {
1274 crate::SearchMode::Html => "html",
1275 crate::SearchMode::Plain => "plain",
1276 },
1277 std::io::Cursor::new(mangled_body.as_ref() as &[u8]),
1278 &subst(&matching_pattern, || package_name.clone()),
1279 &package_name,
1280 url.as_str(),
1281 )?;
1282
1283 let mut releases = Vec::new();
1284 for (version, full_url) in results {
1285 let mangled_version = self.apply_uversionmangle(&version)?;
1287
1288 let mangled_url = self.apply_downloadurlmangle(&full_url)?;
1290
1291 let pgpsigurl = if let Some(mangle) = self.pgpsigurlmangle() {
1293 Some(crate::mangle::apply_mangle(&mangle, &mangled_url)?)
1294 } else {
1295 None
1296 };
1297
1298 let target_filename = if self.filenamemangle().is_some() {
1300 Some(self.apply_filenamemangle(&mangled_url)?)
1301 } else {
1302 None
1303 };
1304
1305 let package_version = if self.oversionmangle().is_some() {
1307 Some(self.apply_oversionmangle(&mangled_version)?)
1308 } else {
1309 None
1310 };
1311
1312 releases.push(crate::Release::new_full(
1313 mangled_version,
1314 mangled_url,
1315 pgpsigurl,
1316 target_filename,
1317 package_version,
1318 ));
1319 }
1320
1321 Ok(releases)
1322 }
1323
1324 pub fn opts(&self) -> std::collections::HashMap<String, String> {
1326 let mut options = std::collections::HashMap::new();
1327
1328 if let Some(ol) = self.option_list() {
1329 for opt in ol.children() {
1330 let key = opt.key();
1331 let value = opt.value();
1332 if let (Some(key), Some(value)) = (key, value) {
1333 options.insert(key.to_string(), value.to_string());
1334 }
1335 }
1336 }
1337
1338 options
1339 }
1340
1341 fn items(&self) -> impl Iterator<Item = String> + '_ {
1342 self.0.children_with_tokens().filter_map(|it| match it {
1343 SyntaxElement::Token(token) => {
1344 if token.kind() == VALUE || token.kind() == KEY {
1345 Some(token.text().to_string())
1346 } else {
1347 None
1348 }
1349 }
1350 SyntaxElement::Node(node) => {
1351 match node.kind() {
1353 URL => Url::cast(node).map(|n| n.url()),
1354 MATCHING_PATTERN => MatchingPattern::cast(node).map(|n| n.pattern()),
1355 VERSION_POLICY => VersionPolicyNode::cast(node).map(|n| n.policy()),
1356 SCRIPT => ScriptNode::cast(node).map(|n| n.script()),
1357 _ => None,
1358 }
1359 }
1360 })
1361 }
1362
1363 pub fn url(&self) -> String {
1365 self.0
1366 .children()
1367 .find_map(Url::cast)
1368 .map(|it| it.url())
1369 .unwrap_or_else(|| {
1370 self.items().next().unwrap()
1372 })
1373 }
1374
1375 pub fn matching_pattern(&self) -> Option<String> {
1377 self.0
1378 .children()
1379 .find_map(MatchingPattern::cast)
1380 .map(|it| it.pattern())
1381 .or_else(|| {
1382 self.items().nth(1)
1384 })
1385 }
1386
1387 pub fn version(&self) -> Result<Option<crate::VersionPolicy>, String> {
1389 self.0
1390 .children()
1391 .find_map(VersionPolicyNode::cast)
1392 .map(|it| it.policy().parse())
1393 .transpose()
1394 .or_else(|_e| {
1395 self.items().nth(2).map(|it| it.parse()).transpose()
1397 })
1398 }
1399
1400 pub fn script(&self) -> Option<String> {
1402 self.0
1403 .children()
1404 .find_map(ScriptNode::cast)
1405 .map(|it| it.script())
1406 .or_else(|| {
1407 self.items().nth(3)
1409 })
1410 }
1411
1412 pub fn format_url(&self, package: impl FnOnce() -> String) -> url::Url {
1414 subst(self.url().as_str(), package).parse().unwrap()
1415 }
1416
1417 pub fn set_url(&mut self, new_url: &str) {
1419 let mut builder = GreenNodeBuilder::new();
1421 builder.start_node(URL.into());
1422 builder.token(VALUE.into(), new_url);
1423 builder.finish_node();
1424 let new_url_green = builder.finish();
1425
1426 let new_url_node = SyntaxNode::new_root_mut(new_url_green);
1428
1429 let url_pos = self
1431 .0
1432 .children_with_tokens()
1433 .position(|child| matches!(child, SyntaxElement::Node(node) if node.kind() == URL));
1434
1435 if let Some(pos) = url_pos {
1436 self.0
1438 .splice_children(pos..pos + 1, vec![new_url_node.into()]);
1439 }
1440 }
1441
1442 pub fn set_matching_pattern(&mut self, new_pattern: &str) {
1448 let mut builder = GreenNodeBuilder::new();
1450 builder.start_node(MATCHING_PATTERN.into());
1451 builder.token(VALUE.into(), new_pattern);
1452 builder.finish_node();
1453 let new_pattern_green = builder.finish();
1454
1455 let new_pattern_node = SyntaxNode::new_root_mut(new_pattern_green);
1457
1458 let pattern_pos = self.0.children_with_tokens().position(
1460 |child| matches!(child, SyntaxElement::Node(node) if node.kind() == MATCHING_PATTERN),
1461 );
1462
1463 if let Some(pos) = pattern_pos {
1464 self.0
1466 .splice_children(pos..pos + 1, vec![new_pattern_node.into()]);
1467 }
1468 }
1470
1471 pub fn set_version_policy(&mut self, new_policy: &str) {
1477 let mut builder = GreenNodeBuilder::new();
1479 builder.start_node(VERSION_POLICY.into());
1480 builder.token(VALUE.into(), new_policy);
1482 builder.finish_node();
1483 let new_policy_green = builder.finish();
1484
1485 let new_policy_node = SyntaxNode::new_root_mut(new_policy_green);
1487
1488 let policy_pos = self.0.children_with_tokens().position(
1490 |child| matches!(child, SyntaxElement::Node(node) if node.kind() == VERSION_POLICY),
1491 );
1492
1493 if let Some(pos) = policy_pos {
1494 self.0
1496 .splice_children(pos..pos + 1, vec![new_policy_node.into()]);
1497 }
1498 }
1500
1501 pub fn set_script(&mut self, new_script: &str) {
1507 let mut builder = GreenNodeBuilder::new();
1509 builder.start_node(SCRIPT.into());
1510 builder.token(VALUE.into(), new_script);
1512 builder.finish_node();
1513 let new_script_green = builder.finish();
1514
1515 let new_script_node = SyntaxNode::new_root_mut(new_script_green);
1517
1518 let script_pos = self
1520 .0
1521 .children_with_tokens()
1522 .position(|child| matches!(child, SyntaxElement::Node(node) if node.kind() == SCRIPT));
1523
1524 if let Some(pos) = script_pos {
1525 self.0
1527 .splice_children(pos..pos + 1, vec![new_script_node.into()]);
1528 }
1529 }
1531
1532 pub fn set_opt(&mut self, key: &str, value: &str) {
1538 let opts_pos = self.0.children_with_tokens().position(
1540 |child| matches!(child, SyntaxElement::Node(node) if node.kind() == OPTS_LIST),
1541 );
1542
1543 if let Some(_opts_idx) = opts_pos {
1544 if let Some(mut ol) = self.option_list() {
1545 if let Some(mut opt) = ol.find_option(key) {
1547 opt.set_value(value);
1549 } else {
1551 ol.add_option(key, value);
1553 }
1555 }
1556 } else {
1557 let mut builder = GreenNodeBuilder::new();
1559 builder.start_node(OPTS_LIST.into());
1560 builder.token(KEY.into(), "opts");
1561 builder.token(EQUALS.into(), "=");
1562 builder.start_node(OPTION.into());
1563 builder.token(KEY.into(), key);
1564 builder.token(EQUALS.into(), "=");
1565 builder.token(VALUE.into(), value);
1566 builder.finish_node();
1567 builder.finish_node();
1568 let new_opts_green = builder.finish();
1569 let new_opts_node = SyntaxNode::new_root_mut(new_opts_green);
1570
1571 let url_pos = self
1573 .0
1574 .children_with_tokens()
1575 .position(|child| matches!(child, SyntaxElement::Node(node) if node.kind() == URL));
1576
1577 if let Some(url_idx) = url_pos {
1578 let mut combined_builder = GreenNodeBuilder::new();
1581 combined_builder.start_node(ROOT.into()); combined_builder.token(WHITESPACE.into(), " ");
1583 combined_builder.finish_node();
1584 let temp_green = combined_builder.finish();
1585 let temp_root = SyntaxNode::new_root_mut(temp_green);
1586 let space_element = temp_root.children_with_tokens().next().unwrap();
1587
1588 self.0
1589 .splice_children(url_idx..url_idx, vec![new_opts_node.into(), space_element]);
1590 } else {
1591 self.0.splice_children(0..0, vec![new_opts_node.into()]);
1592 }
1593 }
1594 }
1595
1596 pub fn del_opt(&mut self, key: &str) {
1603 if let Some(mut ol) = self.option_list() {
1604 let option_count = ol.0.children().filter(|n| n.kind() == OPTION).count();
1605
1606 if option_count == 1 && ol.has_option(key) {
1607 let opts_pos = self.0.children().position(|node| node.kind() == OPTS_LIST);
1609
1610 if let Some(opts_idx) = opts_pos {
1611 self.0.splice_children(opts_idx..opts_idx + 1, vec![]);
1613
1614 while self.0.children_with_tokens().next().map_or(false, |e| {
1616 matches!(
1617 e,
1618 SyntaxElement::Token(t) if t.kind() == WHITESPACE || t.kind() == CONTINUATION
1619 )
1620 }) {
1621 self.0.splice_children(0..1, vec![]);
1622 }
1623 }
1624 } else {
1625 ol.remove_option(key);
1627 }
1628 }
1629 }
1630}
1631
1632const SUBSTITUTIONS: &[(&str, &str)] = &[
1633 ("@ANY_VERSION@", r"[-_]?(\d[\-+\.:\~\da-zA-Z]*)"),
1638 (
1641 "@ARCHIVE_EXT@",
1642 r"(?i)\.(?:tar\.xz|tar\.bz2|tar\.gz|zip|tgz|tbz|txz)",
1643 ),
1644 (
1647 "@SIGNATURE_EXT@",
1648 r"(?i)\.(?:tar\.xz|tar\.bz2|tar\.gz|zip|tgz|tbz|txz)\.(?:asc|pgp|gpg|sig|sign)",
1649 ),
1650 ("@DEB_EXT@", r"[\+~](debian|dfsg|ds|deb)(\.)?(\d+)?$"),
1652];
1653
1654pub fn subst(text: &str, package: impl FnOnce() -> String) -> String {
1655 let mut substs = SUBSTITUTIONS.to_vec();
1656 let package_name;
1657 if text.contains("@PACKAGE@") {
1658 package_name = Some(package());
1659 substs.push(("@PACKAGE@", package_name.as_deref().unwrap()));
1660 }
1661
1662 let mut text = text.to_string();
1663
1664 for (k, v) in substs {
1665 text = text.replace(k, v);
1666 }
1667
1668 text
1669}
1670
1671#[test]
1672fn test_subst() {
1673 assert_eq!(
1674 subst("@ANY_VERSION@", || unreachable!()),
1675 r"[-_]?(\d[\-+\.:\~\da-zA-Z]*)"
1676 );
1677 assert_eq!(subst("@PACKAGE@", || "dulwich".to_string()), "dulwich");
1678}
1679
1680impl std::fmt::Debug for OptionList {
1681 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1682 f.debug_struct("OptionList")
1683 .field("text", &self.0.text().to_string())
1684 .finish()
1685 }
1686}
1687
1688impl OptionList {
1689 fn children(&self) -> impl Iterator<Item = _Option> + '_ {
1690 self.0.children().filter_map(_Option::cast)
1691 }
1692
1693 pub fn has_option(&self, key: &str) -> bool {
1694 self.children().any(|it| it.key().as_deref() == Some(key))
1695 }
1696
1697 pub fn get_option(&self, key: &str) -> Option<String> {
1698 for child in self.children() {
1699 if child.key().as_deref() == Some(key) {
1700 return child.value();
1701 }
1702 }
1703 None
1704 }
1705
1706 fn find_option(&self, key: &str) -> Option<_Option> {
1708 self.children()
1709 .find(|opt| opt.key().as_deref() == Some(key))
1710 }
1711
1712 fn add_option(&mut self, key: &str, value: &str) {
1714 let option_count = self.0.children().filter(|n| n.kind() == OPTION).count();
1715
1716 let mut builder = GreenNodeBuilder::new();
1718 builder.start_node(ROOT.into()); if option_count > 0 {
1721 builder.start_node(OPTION_SEPARATOR.into());
1722 builder.token(COMMA.into(), ",");
1723 builder.finish_node();
1724 }
1725
1726 builder.start_node(OPTION.into());
1727 builder.token(KEY.into(), key);
1728 builder.token(EQUALS.into(), "=");
1729 builder.token(VALUE.into(), value);
1730 builder.finish_node();
1731
1732 builder.finish_node(); let combined_green = builder.finish();
1734
1735 let temp_root = SyntaxNode::new_root_mut(combined_green);
1737 let new_children: Vec<_> = temp_root.children_with_tokens().collect();
1738
1739 let insert_pos = self.0.children_with_tokens().count();
1740 self.0.splice_children(insert_pos..insert_pos, new_children);
1741 }
1742
1743 fn remove_option(&mut self, key: &str) -> bool {
1745 if let Some(mut opt) = self.find_option(key) {
1746 opt.remove();
1747 true
1748 } else {
1749 false
1750 }
1751 }
1752}
1753
1754impl _Option {
1755 pub fn key(&self) -> Option<String> {
1757 self.0.children_with_tokens().find_map(|it| match it {
1758 SyntaxElement::Token(token) => {
1759 if token.kind() == KEY {
1760 Some(token.text().to_string())
1761 } else {
1762 None
1763 }
1764 }
1765 _ => None,
1766 })
1767 }
1768
1769 pub fn value(&self) -> Option<String> {
1771 self.0
1772 .children_with_tokens()
1773 .filter_map(|it| match it {
1774 SyntaxElement::Token(token) => {
1775 if token.kind() == VALUE || token.kind() == KEY {
1776 Some(token.text().to_string())
1777 } else {
1778 None
1779 }
1780 }
1781 _ => None,
1782 })
1783 .nth(1)
1784 }
1785
1786 pub fn set_value(&mut self, new_value: &str) {
1788 let key = self.key().expect("Option must have a key");
1789
1790 let mut builder = GreenNodeBuilder::new();
1792 builder.start_node(OPTION.into());
1793 builder.token(KEY.into(), &key);
1794 builder.token(EQUALS.into(), "=");
1795 builder.token(VALUE.into(), new_value);
1796 builder.finish_node();
1797 let new_option_green = builder.finish();
1798 let new_option_node = SyntaxNode::new_root_mut(new_option_green);
1799
1800 if let Some(parent) = self.0.parent() {
1802 let idx = self.0.index();
1803 parent.splice_children(idx..idx + 1, vec![new_option_node.into()]);
1804 }
1805 }
1806
1807 pub fn remove(&mut self) {
1809 let next_sep = self
1811 .0
1812 .next_sibling()
1813 .filter(|n| n.kind() == OPTION_SEPARATOR);
1814 let prev_sep = self
1815 .0
1816 .prev_sibling()
1817 .filter(|n| n.kind() == OPTION_SEPARATOR);
1818
1819 if let Some(sep) = next_sep {
1821 sep.detach();
1822 } else if let Some(sep) = prev_sep {
1823 sep.detach();
1824 }
1825
1826 self.0.detach();
1828 }
1829}
1830
1831impl Url {
1832 pub fn url(&self) -> String {
1834 self.0
1835 .children_with_tokens()
1836 .find_map(|it| match it {
1837 SyntaxElement::Token(token) => {
1838 if token.kind() == VALUE {
1839 Some(token.text().to_string())
1840 } else {
1841 None
1842 }
1843 }
1844 _ => None,
1845 })
1846 .unwrap()
1847 }
1848}
1849
1850impl MatchingPattern {
1851 pub fn pattern(&self) -> String {
1853 self.0
1854 .children_with_tokens()
1855 .find_map(|it| match it {
1856 SyntaxElement::Token(token) => {
1857 if token.kind() == VALUE {
1858 Some(token.text().to_string())
1859 } else {
1860 None
1861 }
1862 }
1863 _ => None,
1864 })
1865 .unwrap()
1866 }
1867}
1868
1869impl VersionPolicyNode {
1870 pub fn policy(&self) -> String {
1872 self.0
1873 .children_with_tokens()
1874 .find_map(|it| match it {
1875 SyntaxElement::Token(token) => {
1876 if token.kind() == VALUE || token.kind() == KEY {
1878 Some(token.text().to_string())
1879 } else {
1880 None
1881 }
1882 }
1883 _ => None,
1884 })
1885 .unwrap()
1886 }
1887}
1888
1889impl ScriptNode {
1890 pub fn script(&self) -> String {
1892 self.0
1893 .children_with_tokens()
1894 .find_map(|it| match it {
1895 SyntaxElement::Token(token) => {
1896 if token.kind() == VALUE || token.kind() == KEY {
1898 Some(token.text().to_string())
1899 } else {
1900 None
1901 }
1902 }
1903 _ => None,
1904 })
1905 .unwrap()
1906 }
1907}
1908
1909#[test]
1910fn test_entry_node_structure() {
1911 let wf: super::WatchFile = r#"version=4
1913opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
1914"#
1915 .parse()
1916 .unwrap();
1917
1918 let entry = wf.entries().next().unwrap();
1919
1920 assert_eq!(entry.0.children().find(|n| n.kind() == URL).is_some(), true);
1922 assert_eq!(entry.url(), "https://example.com/releases");
1923
1924 assert_eq!(
1926 entry
1927 .0
1928 .children()
1929 .find(|n| n.kind() == MATCHING_PATTERN)
1930 .is_some(),
1931 true
1932 );
1933 assert_eq!(
1934 entry.matching_pattern(),
1935 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
1936 );
1937
1938 assert_eq!(
1940 entry
1941 .0
1942 .children()
1943 .find(|n| n.kind() == VERSION_POLICY)
1944 .is_some(),
1945 true
1946 );
1947 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
1948
1949 assert_eq!(
1951 entry.0.children().find(|n| n.kind() == SCRIPT).is_some(),
1952 true
1953 );
1954 assert_eq!(entry.script(), Some("uupdate".into()));
1955}
1956
1957#[test]
1958fn test_entry_node_structure_partial() {
1959 let wf: super::WatchFile = r#"version=4
1961https://github.com/example/tags .*/v?(\d\S+)\.tar\.gz
1962"#
1963 .parse()
1964 .unwrap();
1965
1966 let entry = wf.entries().next().unwrap();
1967
1968 assert_eq!(entry.0.children().find(|n| n.kind() == URL).is_some(), true);
1970 assert_eq!(
1971 entry
1972 .0
1973 .children()
1974 .find(|n| n.kind() == MATCHING_PATTERN)
1975 .is_some(),
1976 true
1977 );
1978
1979 assert_eq!(
1981 entry
1982 .0
1983 .children()
1984 .find(|n| n.kind() == VERSION_POLICY)
1985 .is_some(),
1986 false
1987 );
1988 assert_eq!(
1989 entry.0.children().find(|n| n.kind() == SCRIPT).is_some(),
1990 false
1991 );
1992
1993 assert_eq!(entry.url(), "https://github.com/example/tags");
1995 assert_eq!(
1996 entry.matching_pattern(),
1997 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
1998 );
1999 assert_eq!(entry.version(), Ok(None));
2000 assert_eq!(entry.script(), None);
2001}
2002
2003#[test]
2004fn test_parse_v1() {
2005 const WATCHV1: &str = r#"version=4
2006opts=bare,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1\.tar\.gz/ \
2007 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2008"#;
2009 let parsed = parse(WATCHV1);
2010 let node = parsed.syntax();
2012 assert_eq!(
2013 format!("{:#?}", node),
2014 r#"ROOT@0..161
2015 VERSION@0..10
2016 KEY@0..7 "version"
2017 EQUALS@7..8 "="
2018 VALUE@8..9 "4"
2019 NEWLINE@9..10 "\n"
2020 ENTRY@10..161
2021 OPTS_LIST@10..86
2022 KEY@10..14 "opts"
2023 EQUALS@14..15 "="
2024 OPTION@15..19
2025 KEY@15..19 "bare"
2026 OPTION_SEPARATOR@19..20
2027 COMMA@19..20 ","
2028 OPTION@20..86
2029 KEY@20..34 "filenamemangle"
2030 EQUALS@34..35 "="
2031 VALUE@35..86 "s/.+\\/v?(\\d\\S+)\\.tar\\ ..."
2032 WHITESPACE@86..87 " "
2033 CONTINUATION@87..89 "\\\n"
2034 WHITESPACE@89..91 " "
2035 URL@91..138
2036 VALUE@91..138 "https://github.com/sy ..."
2037 WHITESPACE@138..139 " "
2038 MATCHING_PATTERN@139..160
2039 VALUE@139..160 ".*/v?(\\d\\S+)\\.tar\\.gz"
2040 NEWLINE@160..161 "\n"
2041"#
2042 );
2043
2044 let root = parsed.root();
2045 assert_eq!(root.version(), 4);
2046 let entries = root.entries().collect::<Vec<_>>();
2047 assert_eq!(entries.len(), 1);
2048 let entry = &entries[0];
2049 assert_eq!(
2050 entry.url(),
2051 "https://github.com/syncthing/syncthing-gtk/tags"
2052 );
2053 assert_eq!(
2054 entry.matching_pattern(),
2055 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2056 );
2057 assert_eq!(entry.version(), Ok(None));
2058 assert_eq!(entry.script(), None);
2059
2060 assert_eq!(node.text(), WATCHV1);
2061}
2062
2063#[test]
2064fn test_parse_v2() {
2065 let parsed = parse(
2066 r#"version=4
2067https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2068# comment
2069"#,
2070 );
2071 assert_eq!(parsed.errors, Vec::<String>::new());
2072 let node = parsed.syntax();
2073 assert_eq!(
2074 format!("{:#?}", node),
2075 r###"ROOT@0..90
2076 VERSION@0..10
2077 KEY@0..7 "version"
2078 EQUALS@7..8 "="
2079 VALUE@8..9 "4"
2080 NEWLINE@9..10 "\n"
2081 ENTRY@10..80
2082 URL@10..57
2083 VALUE@10..57 "https://github.com/sy ..."
2084 WHITESPACE@57..58 " "
2085 MATCHING_PATTERN@58..79
2086 VALUE@58..79 ".*/v?(\\d\\S+)\\.tar\\.gz"
2087 NEWLINE@79..80 "\n"
2088 COMMENT@80..89 "# comment"
2089 NEWLINE@89..90 "\n"
2090"###
2091 );
2092
2093 let root = parsed.root();
2094 assert_eq!(root.version(), 4);
2095 let entries = root.entries().collect::<Vec<_>>();
2096 assert_eq!(entries.len(), 1);
2097 let entry = &entries[0];
2098 assert_eq!(
2099 entry.url(),
2100 "https://github.com/syncthing/syncthing-gtk/tags"
2101 );
2102 assert_eq!(
2103 entry.format_url(|| "syncthing-gtk".to_string()),
2104 "https://github.com/syncthing/syncthing-gtk/tags"
2105 .parse()
2106 .unwrap()
2107 );
2108}
2109
2110#[test]
2111fn test_parse_v3() {
2112 let parsed = parse(
2113 r#"version=4
2114https://github.com/syncthing/@PACKAGE@/tags .*/v?(\d\S+)\.tar\.gz
2115# comment
2116"#,
2117 );
2118 assert_eq!(parsed.errors, Vec::<String>::new());
2119 let root = parsed.root();
2120 assert_eq!(root.version(), 4);
2121 let entries = root.entries().collect::<Vec<_>>();
2122 assert_eq!(entries.len(), 1);
2123 let entry = &entries[0];
2124 assert_eq!(entry.url(), "https://github.com/syncthing/@PACKAGE@/tags");
2125 assert_eq!(
2126 entry.format_url(|| "syncthing-gtk".to_string()),
2127 "https://github.com/syncthing/syncthing-gtk/tags"
2128 .parse()
2129 .unwrap()
2130 );
2131}
2132
2133#[test]
2134fn test_parse_v4() {
2135 let cl: super::WatchFile = r#"version=4
2136opts=repack,compression=xz,dversionmangle=s/\+ds//,repacksuffix=+ds \
2137 https://github.com/example/example-cat/tags \
2138 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2139"#
2140 .parse()
2141 .unwrap();
2142 assert_eq!(cl.version(), 4);
2143 let entries = cl.entries().collect::<Vec<_>>();
2144 assert_eq!(entries.len(), 1);
2145 let entry = &entries[0];
2146 assert_eq!(entry.url(), "https://github.com/example/example-cat/tags");
2147 assert_eq!(
2148 entry.matching_pattern(),
2149 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2150 );
2151 assert!(entry.repack());
2152 assert_eq!(entry.compression(), Ok(Some(Compression::Xz)));
2153 assert_eq!(entry.dversionmangle(), Some("s/\\+ds//".into()));
2154 assert_eq!(entry.repacksuffix(), Some("+ds".into()));
2155 assert_eq!(entry.script(), Some("uupdate".into()));
2156 assert_eq!(
2157 entry.format_url(|| "example-cat".to_string()),
2158 "https://github.com/example/example-cat/tags"
2159 .parse()
2160 .unwrap()
2161 );
2162 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
2163}
2164
2165#[test]
2166fn test_git_mode() {
2167 let text = r#"version=3
2168opts="mode=git, gitmode=shallow, pgpmode=gittag" \
2169https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git \
2170refs/tags/(.*) debian
2171"#;
2172 let parsed = parse(text);
2173 assert_eq!(parsed.errors, Vec::<String>::new());
2174 let cl = parsed.root();
2175 assert_eq!(cl.version(), 3);
2176 let entries = cl.entries().collect::<Vec<_>>();
2177 assert_eq!(entries.len(), 1);
2178 let entry = &entries[0];
2179 assert_eq!(
2180 entry.url(),
2181 "https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git"
2182 );
2183 assert_eq!(entry.matching_pattern(), Some("refs/tags/(.*)".into()));
2184 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
2185 assert_eq!(entry.script(), None);
2186 assert_eq!(entry.gitmode(), Ok(GitMode::Shallow));
2187 assert_eq!(entry.pgpmode(), Ok(PgpMode::GitTag));
2188 assert_eq!(entry.mode(), Ok(Mode::Git));
2189}
2190
2191#[test]
2192fn test_parse_quoted() {
2193 const WATCHV1: &str = r#"version=4
2194opts="bare, filenamemangle=blah" \
2195 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2196"#;
2197 let parsed = parse(WATCHV1);
2198 let node = parsed.syntax();
2200
2201 let root = parsed.root();
2202 assert_eq!(root.version(), 4);
2203 let entries = root.entries().collect::<Vec<_>>();
2204 assert_eq!(entries.len(), 1);
2205 let entry = &entries[0];
2206
2207 assert_eq!(
2208 entry.url(),
2209 "https://github.com/syncthing/syncthing-gtk/tags"
2210 );
2211 assert_eq!(
2212 entry.matching_pattern(),
2213 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2214 );
2215 assert_eq!(entry.version(), Ok(None));
2216 assert_eq!(entry.script(), None);
2217
2218 assert_eq!(node.text(), WATCHV1);
2219}
2220
2221#[test]
2222fn test_set_url() {
2223 let wf: super::WatchFile = r#"version=4
2225https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2226"#
2227 .parse()
2228 .unwrap();
2229
2230 let mut entry = wf.entries().next().unwrap();
2231 assert_eq!(
2232 entry.url(),
2233 "https://github.com/syncthing/syncthing-gtk/tags"
2234 );
2235
2236 entry.set_url("https://newurl.example.org/path");
2237 assert_eq!(entry.url(), "https://newurl.example.org/path");
2238 assert_eq!(
2239 entry.matching_pattern(),
2240 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2241 );
2242
2243 assert_eq!(
2245 entry.to_string(),
2246 "https://newurl.example.org/path .*/v?(\\d\\S+)\\.tar\\.gz\n"
2247 );
2248}
2249
2250#[test]
2251fn test_set_url_with_options() {
2252 let wf: super::WatchFile = r#"version=4
2254opts=foo=blah https://foo.com/bar .*/v?(\d\S+)\.tar\.gz
2255"#
2256 .parse()
2257 .unwrap();
2258
2259 let mut entry = wf.entries().next().unwrap();
2260 assert_eq!(entry.url(), "https://foo.com/bar");
2261 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2262
2263 entry.set_url("https://example.com/baz");
2264 assert_eq!(entry.url(), "https://example.com/baz");
2265
2266 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2268 assert_eq!(
2269 entry.matching_pattern(),
2270 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2271 );
2272
2273 assert_eq!(
2275 entry.to_string(),
2276 "opts=foo=blah https://example.com/baz .*/v?(\\d\\S+)\\.tar\\.gz\n"
2277 );
2278}
2279
2280#[test]
2281fn test_set_url_complex() {
2282 let wf: super::WatchFile = r#"version=4
2284opts=bare,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1\.tar\.gz/ \
2285 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2286"#
2287 .parse()
2288 .unwrap();
2289
2290 let mut entry = wf.entries().next().unwrap();
2291 assert_eq!(
2292 entry.url(),
2293 "https://github.com/syncthing/syncthing-gtk/tags"
2294 );
2295
2296 entry.set_url("https://gitlab.com/newproject/tags");
2297 assert_eq!(entry.url(), "https://gitlab.com/newproject/tags");
2298
2299 assert!(entry.bare());
2301 assert_eq!(
2302 entry.filenamemangle(),
2303 Some("s/.+\\/v?(\\d\\S+)\\.tar\\.gz/syncthing-gtk-$1\\.tar\\.gz/".into())
2304 );
2305 assert_eq!(
2306 entry.matching_pattern(),
2307 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2308 );
2309
2310 assert_eq!(
2312 entry.to_string(),
2313 r#"opts=bare,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1\.tar\.gz/ \
2314 https://gitlab.com/newproject/tags .*/v?(\d\S+)\.tar\.gz
2315"#
2316 );
2317}
2318
2319#[test]
2320fn test_set_url_with_all_fields() {
2321 let wf: super::WatchFile = r#"version=4
2323opts=repack,compression=xz,dversionmangle=s/\+ds//,repacksuffix=+ds \
2324 https://github.com/example/example-cat/tags \
2325 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2326"#
2327 .parse()
2328 .unwrap();
2329
2330 let mut entry = wf.entries().next().unwrap();
2331 assert_eq!(entry.url(), "https://github.com/example/example-cat/tags");
2332 assert_eq!(
2333 entry.matching_pattern(),
2334 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2335 );
2336 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2337 assert_eq!(entry.script(), Some("uupdate".into()));
2338
2339 entry.set_url("https://gitlab.example.org/project/releases");
2340 assert_eq!(entry.url(), "https://gitlab.example.org/project/releases");
2341
2342 assert!(entry.repack());
2344 assert_eq!(entry.compression(), Ok(Some(super::Compression::Xz)));
2345 assert_eq!(entry.dversionmangle(), Some("s/\\+ds//".into()));
2346 assert_eq!(entry.repacksuffix(), Some("+ds".into()));
2347 assert_eq!(
2348 entry.matching_pattern(),
2349 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2350 );
2351 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2352 assert_eq!(entry.script(), Some("uupdate".into()));
2353
2354 assert_eq!(
2356 entry.to_string(),
2357 r#"opts=repack,compression=xz,dversionmangle=s/\+ds//,repacksuffix=+ds \
2358 https://gitlab.example.org/project/releases \
2359 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2360"#
2361 );
2362}
2363
2364#[test]
2365fn test_set_url_quoted_options() {
2366 let wf: super::WatchFile = r#"version=4
2368opts="bare, filenamemangle=blah" \
2369 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2370"#
2371 .parse()
2372 .unwrap();
2373
2374 let mut entry = wf.entries().next().unwrap();
2375 assert_eq!(
2376 entry.url(),
2377 "https://github.com/syncthing/syncthing-gtk/tags"
2378 );
2379
2380 entry.set_url("https://example.org/new/path");
2381 assert_eq!(entry.url(), "https://example.org/new/path");
2382
2383 assert_eq!(
2385 entry.to_string(),
2386 r#"opts="bare, filenamemangle=blah" \
2387 https://example.org/new/path .*/v?(\d\S+)\.tar\.gz
2388"#
2389 );
2390}
2391
2392#[test]
2393fn test_set_opt_update_existing() {
2394 let wf: super::WatchFile = r#"version=4
2396opts=foo=blah,bar=baz https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2397"#
2398 .parse()
2399 .unwrap();
2400
2401 let mut entry = wf.entries().next().unwrap();
2402 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2403 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2404
2405 entry.set_opt("foo", "updated");
2406 assert_eq!(entry.get_option("foo"), Some("updated".to_string()));
2407 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2408
2409 assert_eq!(
2411 entry.to_string(),
2412 "opts=foo=updated,bar=baz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2413 );
2414}
2415
2416#[test]
2417fn test_set_opt_add_new() {
2418 let wf: super::WatchFile = r#"version=4
2420opts=foo=blah https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2421"#
2422 .parse()
2423 .unwrap();
2424
2425 let mut entry = wf.entries().next().unwrap();
2426 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2427 assert_eq!(entry.get_option("bar"), None);
2428
2429 entry.set_opt("bar", "baz");
2430 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2431 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2432
2433 assert_eq!(
2435 entry.to_string(),
2436 "opts=foo=blah,bar=baz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2437 );
2438}
2439
2440#[test]
2441fn test_set_opt_create_options_list() {
2442 let wf: super::WatchFile = r#"version=4
2444https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2445"#
2446 .parse()
2447 .unwrap();
2448
2449 let mut entry = wf.entries().next().unwrap();
2450 assert_eq!(entry.option_list(), None);
2451
2452 entry.set_opt("compression", "xz");
2453 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
2454
2455 assert_eq!(
2457 entry.to_string(),
2458 "opts=compression=xz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2459 );
2460}
2461
2462#[test]
2463fn test_del_opt_remove_single() {
2464 let wf: super::WatchFile = r#"version=4
2466opts=foo=blah,bar=baz,qux=quux https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2467"#
2468 .parse()
2469 .unwrap();
2470
2471 let mut entry = wf.entries().next().unwrap();
2472 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2473 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2474 assert_eq!(entry.get_option("qux"), Some("quux".to_string()));
2475
2476 entry.del_opt("bar");
2477 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2478 assert_eq!(entry.get_option("bar"), None);
2479 assert_eq!(entry.get_option("qux"), Some("quux".to_string()));
2480
2481 assert_eq!(
2483 entry.to_string(),
2484 "opts=foo=blah,qux=quux https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2485 );
2486}
2487
2488#[test]
2489fn test_del_opt_remove_first() {
2490 let wf: super::WatchFile = r#"version=4
2492opts=foo=blah,bar=baz https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2493"#
2494 .parse()
2495 .unwrap();
2496
2497 let mut entry = wf.entries().next().unwrap();
2498 entry.del_opt("foo");
2499 assert_eq!(entry.get_option("foo"), None);
2500 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2501
2502 assert_eq!(
2504 entry.to_string(),
2505 "opts=bar=baz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2506 );
2507}
2508
2509#[test]
2510fn test_del_opt_remove_last() {
2511 let wf: super::WatchFile = r#"version=4
2513opts=foo=blah,bar=baz https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2514"#
2515 .parse()
2516 .unwrap();
2517
2518 let mut entry = wf.entries().next().unwrap();
2519 entry.del_opt("bar");
2520 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2521 assert_eq!(entry.get_option("bar"), None);
2522
2523 assert_eq!(
2525 entry.to_string(),
2526 "opts=foo=blah https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2527 );
2528}
2529
2530#[test]
2531fn test_del_opt_remove_only_option() {
2532 let wf: super::WatchFile = r#"version=4
2534opts=foo=blah https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2535"#
2536 .parse()
2537 .unwrap();
2538
2539 let mut entry = wf.entries().next().unwrap();
2540 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2541
2542 entry.del_opt("foo");
2543 assert_eq!(entry.get_option("foo"), None);
2544 assert_eq!(entry.option_list(), None);
2545
2546 assert_eq!(
2548 entry.to_string(),
2549 "https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2550 );
2551}
2552
2553#[test]
2554fn test_del_opt_nonexistent() {
2555 let wf: super::WatchFile = r#"version=4
2557opts=foo=blah https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2558"#
2559 .parse()
2560 .unwrap();
2561
2562 let mut entry = wf.entries().next().unwrap();
2563 let original = entry.to_string();
2564
2565 entry.del_opt("nonexistent");
2566 assert_eq!(entry.to_string(), original);
2567}
2568
2569#[test]
2570fn test_set_opt_multiple_operations() {
2571 let wf: super::WatchFile = r#"version=4
2573https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2574"#
2575 .parse()
2576 .unwrap();
2577
2578 let mut entry = wf.entries().next().unwrap();
2579
2580 entry.set_opt("compression", "xz");
2581 entry.set_opt("repack", "");
2582 entry.set_opt("dversionmangle", "s/\\+ds//");
2583
2584 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
2585 assert_eq!(
2586 entry.get_option("dversionmangle"),
2587 Some("s/\\+ds//".to_string())
2588 );
2589}
2590
2591#[test]
2592fn test_set_matching_pattern() {
2593 let wf: super::WatchFile = r#"version=4
2595https://github.com/example/tags .*/v?(\d\S+)\.tar\.gz
2596"#
2597 .parse()
2598 .unwrap();
2599
2600 let mut entry = wf.entries().next().unwrap();
2601 assert_eq!(
2602 entry.matching_pattern(),
2603 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2604 );
2605
2606 entry.set_matching_pattern("(?:.*?/)?v?([\\d.]+)\\.tar\\.gz");
2607 assert_eq!(
2608 entry.matching_pattern(),
2609 Some("(?:.*?/)?v?([\\d.]+)\\.tar\\.gz".into())
2610 );
2611
2612 assert_eq!(entry.url(), "https://github.com/example/tags");
2614
2615 assert_eq!(
2617 entry.to_string(),
2618 "https://github.com/example/tags (?:.*?/)?v?([\\d.]+)\\.tar\\.gz\n"
2619 );
2620}
2621
2622#[test]
2623fn test_set_matching_pattern_with_all_fields() {
2624 let wf: super::WatchFile = r#"version=4
2626opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2627"#
2628 .parse()
2629 .unwrap();
2630
2631 let mut entry = wf.entries().next().unwrap();
2632 assert_eq!(
2633 entry.matching_pattern(),
2634 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2635 );
2636
2637 entry.set_matching_pattern(".*/version-([\\d.]+)\\.tar\\.xz");
2638 assert_eq!(
2639 entry.matching_pattern(),
2640 Some(".*/version-([\\d.]+)\\.tar\\.xz".into())
2641 );
2642
2643 assert_eq!(entry.url(), "https://example.com/releases");
2645 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2646 assert_eq!(entry.script(), Some("uupdate".into()));
2647 assert_eq!(entry.compression(), Ok(Some(super::Compression::Xz)));
2648
2649 assert_eq!(
2651 entry.to_string(),
2652 "opts=compression=xz https://example.com/releases .*/version-([\\d.]+)\\.tar\\.xz debian uupdate\n"
2653 );
2654}
2655
2656#[test]
2657fn test_set_version_policy() {
2658 let wf: super::WatchFile = r#"version=4
2660https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2661"#
2662 .parse()
2663 .unwrap();
2664
2665 let mut entry = wf.entries().next().unwrap();
2666 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2667
2668 entry.set_version_policy("previous");
2669 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Previous)));
2670
2671 assert_eq!(entry.url(), "https://example.com/releases");
2673 assert_eq!(
2674 entry.matching_pattern(),
2675 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2676 );
2677 assert_eq!(entry.script(), Some("uupdate".into()));
2678
2679 assert_eq!(
2681 entry.to_string(),
2682 "https://example.com/releases (?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz previous uupdate\n"
2683 );
2684}
2685
2686#[test]
2687fn test_set_version_policy_with_options() {
2688 let wf: super::WatchFile = r#"version=4
2690opts=repack,compression=xz \
2691 https://github.com/example/example-cat/tags \
2692 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2693"#
2694 .parse()
2695 .unwrap();
2696
2697 let mut entry = wf.entries().next().unwrap();
2698 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2699
2700 entry.set_version_policy("ignore");
2701 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Ignore)));
2702
2703 assert_eq!(entry.url(), "https://github.com/example/example-cat/tags");
2705 assert_eq!(
2706 entry.matching_pattern(),
2707 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2708 );
2709 assert_eq!(entry.script(), Some("uupdate".into()));
2710 assert!(entry.repack());
2711
2712 assert_eq!(
2714 entry.to_string(),
2715 r#"opts=repack,compression=xz \
2716 https://github.com/example/example-cat/tags \
2717 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz ignore uupdate
2718"#
2719 );
2720}
2721
2722#[test]
2723fn test_set_script() {
2724 let wf: super::WatchFile = r#"version=4
2726https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2727"#
2728 .parse()
2729 .unwrap();
2730
2731 let mut entry = wf.entries().next().unwrap();
2732 assert_eq!(entry.script(), Some("uupdate".into()));
2733
2734 entry.set_script("uscan");
2735 assert_eq!(entry.script(), Some("uscan".into()));
2736
2737 assert_eq!(entry.url(), "https://example.com/releases");
2739 assert_eq!(
2740 entry.matching_pattern(),
2741 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2742 );
2743 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2744
2745 assert_eq!(
2747 entry.to_string(),
2748 "https://example.com/releases (?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz debian uscan\n"
2749 );
2750}
2751
2752#[test]
2753fn test_set_script_with_options() {
2754 let wf: super::WatchFile = r#"version=4
2756opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2757"#
2758 .parse()
2759 .unwrap();
2760
2761 let mut entry = wf.entries().next().unwrap();
2762 assert_eq!(entry.script(), Some("uupdate".into()));
2763
2764 entry.set_script("custom-script.sh");
2765 assert_eq!(entry.script(), Some("custom-script.sh".into()));
2766
2767 assert_eq!(entry.url(), "https://example.com/releases");
2769 assert_eq!(
2770 entry.matching_pattern(),
2771 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2772 );
2773 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2774 assert_eq!(entry.compression(), Ok(Some(super::Compression::Xz)));
2775
2776 assert_eq!(
2778 entry.to_string(),
2779 "opts=compression=xz https://example.com/releases (?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz debian custom-script.sh\n"
2780 );
2781}
2782
2783#[test]
2784fn test_apply_dversionmangle() {
2785 let wf: super::WatchFile = r#"version=4
2787opts=dversionmangle=s/\+dfsg$// https://example.com/ .*
2788"#
2789 .parse()
2790 .unwrap();
2791 let entry = wf.entries().next().unwrap();
2792 assert_eq!(entry.apply_dversionmangle("1.0+dfsg").unwrap(), "1.0");
2793 assert_eq!(entry.apply_dversionmangle("1.0").unwrap(), "1.0");
2794
2795 let wf: super::WatchFile = r#"version=4
2797opts=versionmangle=s/^v// https://example.com/ .*
2798"#
2799 .parse()
2800 .unwrap();
2801 let entry = wf.entries().next().unwrap();
2802 assert_eq!(entry.apply_dversionmangle("v1.0").unwrap(), "1.0");
2803
2804 let wf: super::WatchFile = r#"version=4
2806opts=dversionmangle=s/\+ds//,versionmangle=s/^v// https://example.com/ .*
2807"#
2808 .parse()
2809 .unwrap();
2810 let entry = wf.entries().next().unwrap();
2811 assert_eq!(entry.apply_dversionmangle("1.0+ds").unwrap(), "1.0");
2812
2813 let wf: super::WatchFile = r#"version=4
2815https://example.com/ .*
2816"#
2817 .parse()
2818 .unwrap();
2819 let entry = wf.entries().next().unwrap();
2820 assert_eq!(entry.apply_dversionmangle("1.0+dfsg").unwrap(), "1.0+dfsg");
2821}
2822
2823#[test]
2824fn test_apply_oversionmangle() {
2825 let wf: super::WatchFile = r#"version=4
2827opts=oversionmangle=s/$/-1/ https://example.com/ .*
2828"#
2829 .parse()
2830 .unwrap();
2831 let entry = wf.entries().next().unwrap();
2832 assert_eq!(entry.apply_oversionmangle("1.0").unwrap(), "1.0-1");
2833 assert_eq!(entry.apply_oversionmangle("2.5.3").unwrap(), "2.5.3-1");
2834
2835 let wf: super::WatchFile = r#"version=4
2837opts=oversionmangle=s/$/.dfsg/ https://example.com/ .*
2838"#
2839 .parse()
2840 .unwrap();
2841 let entry = wf.entries().next().unwrap();
2842 assert_eq!(entry.apply_oversionmangle("1.0").unwrap(), "1.0.dfsg");
2843
2844 let wf: super::WatchFile = r#"version=4
2846https://example.com/ .*
2847"#
2848 .parse()
2849 .unwrap();
2850 let entry = wf.entries().next().unwrap();
2851 assert_eq!(entry.apply_oversionmangle("1.0").unwrap(), "1.0");
2852}
2853
2854#[test]
2855fn test_apply_dirversionmangle() {
2856 let wf: super::WatchFile = r#"version=4
2858opts=dirversionmangle=s/^v// https://example.com/ .*
2859"#
2860 .parse()
2861 .unwrap();
2862 let entry = wf.entries().next().unwrap();
2863 assert_eq!(entry.apply_dirversionmangle("v1.0").unwrap(), "1.0");
2864 assert_eq!(entry.apply_dirversionmangle("v2.5.3").unwrap(), "2.5.3");
2865
2866 let wf: super::WatchFile = r#"version=4
2868opts=dirversionmangle=s/v(\d)/$1/ https://example.com/ .*
2869"#
2870 .parse()
2871 .unwrap();
2872 let entry = wf.entries().next().unwrap();
2873 assert_eq!(entry.apply_dirversionmangle("v1.0").unwrap(), "1.0");
2874
2875 let wf: super::WatchFile = r#"version=4
2877https://example.com/ .*
2878"#
2879 .parse()
2880 .unwrap();
2881 let entry = wf.entries().next().unwrap();
2882 assert_eq!(entry.apply_dirversionmangle("v1.0").unwrap(), "v1.0");
2883}
2884
2885#[test]
2886fn test_apply_filenamemangle() {
2887 let wf: super::WatchFile = r#"version=4
2889opts=filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/mypackage-$1.tar.gz/ https://example.com/ .*
2890"#
2891 .parse()
2892 .unwrap();
2893 let entry = wf.entries().next().unwrap();
2894 assert_eq!(
2895 entry
2896 .apply_filenamemangle("https://example.com/v1.0.tar.gz")
2897 .unwrap(),
2898 "mypackage-1.0.tar.gz"
2899 );
2900 assert_eq!(
2901 entry
2902 .apply_filenamemangle("https://example.com/2.5.3.tar.gz")
2903 .unwrap(),
2904 "mypackage-2.5.3.tar.gz"
2905 );
2906
2907 let wf: super::WatchFile = r#"version=4
2909opts=filenamemangle=s/.*\/(.*)/$1/ https://example.com/ .*
2910"#
2911 .parse()
2912 .unwrap();
2913 let entry = wf.entries().next().unwrap();
2914 assert_eq!(
2915 entry
2916 .apply_filenamemangle("https://example.com/path/to/file.tar.gz")
2917 .unwrap(),
2918 "file.tar.gz"
2919 );
2920
2921 let wf: super::WatchFile = r#"version=4
2923https://example.com/ .*
2924"#
2925 .parse()
2926 .unwrap();
2927 let entry = wf.entries().next().unwrap();
2928 assert_eq!(
2929 entry
2930 .apply_filenamemangle("https://example.com/file.tar.gz")
2931 .unwrap(),
2932 "https://example.com/file.tar.gz"
2933 );
2934}
2935
2936#[test]
2937fn test_apply_pagemangle() {
2938 let wf: super::WatchFile = r#"version=4
2940opts=pagemangle=s/&/&/g https://example.com/ .*
2941"#
2942 .parse()
2943 .unwrap();
2944 let entry = wf.entries().next().unwrap();
2945 assert_eq!(
2946 entry.apply_pagemangle(b"foo & bar").unwrap(),
2947 b"foo & bar"
2948 );
2949 assert_eq!(
2950 entry
2951 .apply_pagemangle(b"& foo & bar &")
2952 .unwrap(),
2953 b"& foo & bar &"
2954 );
2955
2956 let wf: super::WatchFile = r#"version=4
2958opts=pagemangle=s/<[^>]+>//g https://example.com/ .*
2959"#
2960 .parse()
2961 .unwrap();
2962 let entry = wf.entries().next().unwrap();
2963 assert_eq!(entry.apply_pagemangle(b"<div>text</div>").unwrap(), b"text");
2964
2965 let wf: super::WatchFile = r#"version=4
2967https://example.com/ .*
2968"#
2969 .parse()
2970 .unwrap();
2971 let entry = wf.entries().next().unwrap();
2972 assert_eq!(
2973 entry.apply_pagemangle(b"foo & bar").unwrap(),
2974 b"foo & bar"
2975 );
2976}
2977
2978#[test]
2979fn test_apply_downloadurlmangle() {
2980 let wf: super::WatchFile = r#"version=4
2982opts=downloadurlmangle=s|/archive/|/download/| https://example.com/ .*
2983"#
2984 .parse()
2985 .unwrap();
2986 let entry = wf.entries().next().unwrap();
2987 assert_eq!(
2988 entry
2989 .apply_downloadurlmangle("https://example.com/archive/file.tar.gz")
2990 .unwrap(),
2991 "https://example.com/download/file.tar.gz"
2992 );
2993
2994 let wf: super::WatchFile = r#"version=4
2996opts=downloadurlmangle=s/github\.com/raw.githubusercontent.com/ https://example.com/ .*
2997"#
2998 .parse()
2999 .unwrap();
3000 let entry = wf.entries().next().unwrap();
3001 assert_eq!(
3002 entry
3003 .apply_downloadurlmangle("https://github.com/user/repo/file.tar.gz")
3004 .unwrap(),
3005 "https://raw.githubusercontent.com/user/repo/file.tar.gz"
3006 );
3007
3008 let wf: super::WatchFile = r#"version=4
3010https://example.com/ .*
3011"#
3012 .parse()
3013 .unwrap();
3014 let entry = wf.entries().next().unwrap();
3015 assert_eq!(
3016 entry
3017 .apply_downloadurlmangle("https://example.com/archive/file.tar.gz")
3018 .unwrap(),
3019 "https://example.com/archive/file.tar.gz"
3020 );
3021}
3022
3023#[test]
3024fn test_entry_builder_minimal() {
3025 let entry = super::EntryBuilder::new("https://github.com/example/tags")
3027 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3028 .build();
3029
3030 assert_eq!(entry.url(), "https://github.com/example/tags");
3031 assert_eq!(
3032 entry.matching_pattern().as_deref(),
3033 Some(".*/v?(\\d\\S+)\\.tar\\.gz")
3034 );
3035 assert_eq!(entry.version(), Ok(None));
3036 assert_eq!(entry.script(), None);
3037 assert!(entry.opts().is_empty());
3038}
3039
3040#[test]
3041fn test_entry_builder_url_only() {
3042 let entry = super::EntryBuilder::new("https://example.com/releases").build();
3044
3045 assert_eq!(entry.url(), "https://example.com/releases");
3046 assert_eq!(entry.matching_pattern(), None);
3047 assert_eq!(entry.version(), Ok(None));
3048 assert_eq!(entry.script(), None);
3049 assert!(entry.opts().is_empty());
3050}
3051
3052#[test]
3053fn test_entry_builder_with_all_fields() {
3054 let entry = super::EntryBuilder::new("https://github.com/example/tags")
3056 .matching_pattern(".*/v?(\\d[\\d.]*)\\.tar\\.gz")
3057 .version_policy("debian")
3058 .script("uupdate")
3059 .opt("compression", "xz")
3060 .flag("repack")
3061 .build();
3062
3063 assert_eq!(entry.url(), "https://github.com/example/tags");
3064 assert_eq!(
3065 entry.matching_pattern().as_deref(),
3066 Some(".*/v?(\\d[\\d.]*)\\.tar\\.gz")
3067 );
3068 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
3069 assert_eq!(entry.script(), Some("uupdate".into()));
3070 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
3071 assert!(entry.has_option("repack"));
3072 assert!(entry.repack());
3073}
3074
3075#[test]
3076fn test_entry_builder_multiple_options() {
3077 let entry = super::EntryBuilder::new("https://example.com/tags")
3079 .matching_pattern(".*/v?(\\d+\\.\\d+)\\.tar\\.gz")
3080 .opt("compression", "xz")
3081 .opt("dversionmangle", "s/\\+ds//")
3082 .opt("repacksuffix", "+ds")
3083 .build();
3084
3085 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
3086 assert_eq!(
3087 entry.get_option("dversionmangle"),
3088 Some("s/\\+ds//".to_string())
3089 );
3090 assert_eq!(entry.get_option("repacksuffix"), Some("+ds".to_string()));
3091}
3092
3093#[test]
3094fn test_entry_builder_via_entry() {
3095 let entry = super::Entry::builder("https://github.com/example/tags")
3097 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3098 .version_policy("debian")
3099 .build();
3100
3101 assert_eq!(entry.url(), "https://github.com/example/tags");
3102 assert_eq!(
3103 entry.matching_pattern().as_deref(),
3104 Some(".*/v?(\\d\\S+)\\.tar\\.gz")
3105 );
3106 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
3107}
3108
3109#[test]
3110fn test_watchfile_add_entry_to_empty() {
3111 let mut wf = super::WatchFile::new(Some(4));
3113
3114 let entry = super::EntryBuilder::new("https://github.com/example/tags")
3115 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3116 .build();
3117
3118 wf.add_entry(entry);
3119
3120 assert_eq!(wf.version(), 4);
3121 assert_eq!(wf.entries().count(), 1);
3122
3123 let added_entry = wf.entries().next().unwrap();
3124 assert_eq!(added_entry.url(), "https://github.com/example/tags");
3125 assert_eq!(
3126 added_entry.matching_pattern().as_deref(),
3127 Some(".*/v?(\\d\\S+)\\.tar\\.gz")
3128 );
3129}
3130
3131#[test]
3132fn test_watchfile_add_multiple_entries() {
3133 let mut wf = super::WatchFile::new(Some(4));
3135
3136 wf.add_entry(
3137 super::EntryBuilder::new("https://github.com/example1/tags")
3138 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3139 .build(),
3140 );
3141
3142 wf.add_entry(
3143 super::EntryBuilder::new("https://github.com/example2/releases")
3144 .matching_pattern(".*/(\\d+\\.\\d+)\\.tar\\.gz")
3145 .opt("compression", "xz")
3146 .build(),
3147 );
3148
3149 assert_eq!(wf.entries().count(), 2);
3150
3151 let entries: Vec<_> = wf.entries().collect();
3152 assert_eq!(entries[0].url(), "https://github.com/example1/tags");
3153 assert_eq!(entries[1].url(), "https://github.com/example2/releases");
3154 assert_eq!(entries[1].get_option("compression"), Some("xz".to_string()));
3155}
3156
3157#[test]
3158fn test_watchfile_add_entry_to_existing() {
3159 let mut wf: super::WatchFile = r#"version=4
3161https://example.com/old .*/v?(\\d\\S+)\\.tar\\.gz
3162"#
3163 .parse()
3164 .unwrap();
3165
3166 assert_eq!(wf.entries().count(), 1);
3167
3168 wf.add_entry(
3169 super::EntryBuilder::new("https://github.com/example/new")
3170 .matching_pattern(".*/v?(\\d+\\.\\d+)\\.tar\\.gz")
3171 .opt("compression", "xz")
3172 .version_policy("debian")
3173 .build(),
3174 );
3175
3176 assert_eq!(wf.entries().count(), 2);
3177
3178 let entries: Vec<_> = wf.entries().collect();
3179 assert_eq!(entries[0].url(), "https://example.com/old");
3180 assert_eq!(entries[1].url(), "https://github.com/example/new");
3181 assert_eq!(entries[1].version(), Ok(Some(VersionPolicy::Debian)));
3182}
3183
3184#[test]
3185fn test_entry_builder_formatting() {
3186 let entry = super::EntryBuilder::new("https://github.com/example/tags")
3188 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3189 .opt("compression", "xz")
3190 .flag("repack")
3191 .version_policy("debian")
3192 .script("uupdate")
3193 .build();
3194
3195 let entry_str = entry.to_string();
3196
3197 assert!(entry_str.starts_with("opts="));
3199 assert!(entry_str.contains("https://github.com/example/tags"));
3201 assert!(entry_str.contains(".*/v?(\\d\\S+)\\.tar\\.gz"));
3203 assert!(entry_str.contains("debian"));
3205 assert!(entry_str.contains("uupdate"));
3207 assert!(entry_str.ends_with('\n'));
3209}
3210
3211#[test]
3212fn test_watchfile_add_entry_preserves_format() {
3213 let mut wf = super::WatchFile::new(Some(4));
3215
3216 wf.add_entry(
3217 super::EntryBuilder::new("https://github.com/example/tags")
3218 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3219 .build(),
3220 );
3221
3222 let wf_str = wf.to_string();
3223
3224 assert!(wf_str.starts_with("version=4\n"));
3226 assert!(wf_str.contains("https://github.com/example/tags"));
3228
3229 let reparsed: super::WatchFile = wf_str.parse().unwrap();
3231 assert_eq!(reparsed.version(), 4);
3232 assert_eq!(reparsed.entries().count(), 1);
3233}