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
527impl FromStr for WatchFile {
528 type Err = ParseError;
529
530 fn from_str(s: &str) -> Result<Self, Self::Err> {
531 let parsed = parse(s);
532 if parsed.errors.is_empty() {
533 Ok(parsed.root())
534 } else {
535 Err(ParseError(parsed.errors))
536 }
537 }
538}
539
540impl Version {
541 pub fn version(&self) -> u32 {
543 self.0
544 .children_with_tokens()
545 .find_map(|it| match it {
546 SyntaxElement::Token(token) => {
547 if token.kind() == VALUE {
548 Some(token.text().parse().unwrap())
549 } else {
550 None
551 }
552 }
553 _ => None,
554 })
555 .unwrap_or(DEFAULT_VERSION)
556 }
557}
558
559impl Entry {
560 pub fn option_list(&self) -> Option<OptionList> {
562 self.0.children().find_map(OptionList::cast)
563 }
564
565 pub fn get_option(&self, key: &str) -> Option<String> {
567 self.option_list().and_then(|ol| ol.get_option(key))
568 }
569
570 pub fn has_option(&self, key: &str) -> bool {
572 self.option_list().map_or(false, |ol| ol.has_option(key))
573 }
574
575 pub fn component(&self) -> Option<String> {
577 self.get_option("component")
578 }
579
580 pub fn ctype(&self) -> Result<Option<ComponentType>, ()> {
582 self.get_option("ctype").map(|s| s.parse()).transpose()
583 }
584
585 pub fn compression(&self) -> Result<Option<Compression>, ()> {
587 self.get_option("compression")
588 .map(|s| s.parse())
589 .transpose()
590 }
591
592 pub fn repack(&self) -> bool {
594 self.has_option("repack")
595 }
596
597 pub fn repacksuffix(&self) -> Option<String> {
599 self.get_option("repacksuffix")
600 }
601
602 pub fn mode(&self) -> Result<Mode, ()> {
604 Ok(self
605 .get_option("mode")
606 .map(|s| s.parse())
607 .transpose()?
608 .unwrap_or_default())
609 }
610
611 pub fn pretty(&self) -> Result<Pretty, ()> {
613 Ok(self
614 .get_option("pretty")
615 .map(|s| s.parse())
616 .transpose()?
617 .unwrap_or_default())
618 }
619
620 pub fn date(&self) -> String {
623 self.get_option("date")
624 .unwrap_or_else(|| "%Y%m%d".to_string())
625 }
626
627 pub fn gitexport(&self) -> Result<GitExport, ()> {
629 Ok(self
630 .get_option("gitexport")
631 .map(|s| s.parse())
632 .transpose()?
633 .unwrap_or_default())
634 }
635
636 pub fn gitmode(&self) -> Result<GitMode, ()> {
638 Ok(self
639 .get_option("gitmode")
640 .map(|s| s.parse())
641 .transpose()?
642 .unwrap_or_default())
643 }
644
645 pub fn pgpmode(&self) -> Result<PgpMode, ()> {
647 Ok(self
648 .get_option("pgpmode")
649 .map(|s| s.parse())
650 .transpose()?
651 .unwrap_or_default())
652 }
653
654 pub fn searchmode(&self) -> Result<SearchMode, ()> {
656 Ok(self
657 .get_option("searchmode")
658 .map(|s| s.parse())
659 .transpose()?
660 .unwrap_or_default())
661 }
662
663 pub fn decompress(&self) -> bool {
665 self.has_option("decompress")
666 }
667
668 pub fn bare(&self) -> bool {
671 self.has_option("bare")
672 }
673
674 pub fn user_agent(&self) -> Option<String> {
676 self.get_option("user-agent")
677 }
678
679 pub fn passive(&self) -> Option<bool> {
681 if self.has_option("passive") || self.has_option("pasv") {
682 Some(true)
683 } else if self.has_option("active") || self.has_option("nopasv") {
684 Some(false)
685 } else {
686 None
687 }
688 }
689
690 pub fn unzipoptions(&self) -> Option<String> {
693 self.get_option("unzipopt")
694 }
695
696 pub fn dversionmangle(&self) -> Option<String> {
698 self.get_option("dversionmangle")
699 .or_else(|| self.get_option("versionmangle"))
700 }
701
702 pub fn dirversionmangle(&self) -> Option<String> {
706 self.get_option("dirversionmangle")
707 }
708
709 pub fn pagemangle(&self) -> Option<String> {
711 self.get_option("pagemangle")
712 }
713
714 pub fn uversionmangle(&self) -> Option<String> {
718 self.get_option("uversionmangle")
719 .or_else(|| self.get_option("versionmangle"))
720 }
721
722 pub fn versionmangle(&self) -> Option<String> {
724 self.get_option("versionmangle")
725 }
726
727 pub fn hrefdecode(&self) -> bool {
732 self.get_option("hrefdecode").is_some()
733 }
734
735 pub fn downloadurlmangle(&self) -> Option<String> {
738 self.get_option("downloadurlmangle")
739 }
740
741 pub fn filenamemangle(&self) -> Option<String> {
749 self.get_option("filenamemangle")
750 }
751
752 pub fn pgpsigurlmangle(&self) -> Option<String> {
754 self.get_option("pgpsigurlmangle")
755 }
756
757 pub fn oversionmangle(&self) -> Option<String> {
760 self.get_option("oversionmangle")
761 }
762
763 pub fn apply_uversionmangle(
776 &self,
777 version: &str,
778 ) -> Result<String, crate::mangle::MangleError> {
779 if let Some(vm) = self.uversionmangle() {
780 crate::mangle::apply_mangle(&vm, version)
781 } else {
782 Ok(version.to_string())
783 }
784 }
785
786 pub fn apply_dversionmangle(
799 &self,
800 version: &str,
801 ) -> Result<String, crate::mangle::MangleError> {
802 if let Some(vm) = self.dversionmangle() {
803 crate::mangle::apply_mangle(&vm, version)
804 } else {
805 Ok(version.to_string())
806 }
807 }
808
809 pub fn apply_oversionmangle(
822 &self,
823 version: &str,
824 ) -> Result<String, crate::mangle::MangleError> {
825 if let Some(vm) = self.oversionmangle() {
826 crate::mangle::apply_mangle(&vm, version)
827 } else {
828 Ok(version.to_string())
829 }
830 }
831
832 pub fn apply_dirversionmangle(
845 &self,
846 version: &str,
847 ) -> Result<String, crate::mangle::MangleError> {
848 if let Some(vm) = self.dirversionmangle() {
849 crate::mangle::apply_mangle(&vm, version)
850 } else {
851 Ok(version.to_string())
852 }
853 }
854
855 pub fn apply_filenamemangle(&self, url: &str) -> Result<String, crate::mangle::MangleError> {
871 if let Some(vm) = self.filenamemangle() {
872 crate::mangle::apply_mangle(&vm, url)
873 } else {
874 Ok(url.to_string())
875 }
876 }
877
878 pub fn apply_pagemangle(&self, page: &[u8]) -> Result<Vec<u8>, crate::mangle::MangleError> {
894 if let Some(vm) = self.pagemangle() {
895 let page_str = String::from_utf8_lossy(page);
896 let mangled = crate::mangle::apply_mangle(&vm, &page_str)?;
897 Ok(mangled.into_bytes())
898 } else {
899 Ok(page.to_vec())
900 }
901 }
902
903 pub fn apply_downloadurlmangle(&self, url: &str) -> Result<String, crate::mangle::MangleError> {
919 if let Some(vm) = self.downloadurlmangle() {
920 crate::mangle::apply_mangle(&vm, url)
921 } else {
922 Ok(url.to_string())
923 }
924 }
925
926 #[cfg(feature = "discover")]
947 pub async fn discover(
948 &self,
949 package: impl FnOnce() -> String,
950 ) -> Result<Vec<crate::Release>, Box<dyn std::error::Error>> {
951 let url = self.format_url(package);
952 let user_agent = self
953 .user_agent()
954 .unwrap_or_else(|| crate::DEFAULT_USER_AGENT.to_string());
955 let searchmode = self.searchmode().unwrap_or(crate::SearchMode::Html);
956
957 let client = reqwest::Client::builder().user_agent(user_agent).build()?;
958
959 let response = client.get(url.as_str()).send().await?;
960 let body = response.bytes().await?;
961
962 let mangled_body = self.apply_pagemangle(&body)?;
964
965 let matching_pattern = self
966 .matching_pattern()
967 .ok_or("matching_pattern is required")?;
968
969 let package_name = String::new(); let results = crate::search::search(
971 match searchmode {
972 crate::SearchMode::Html => "html",
973 crate::SearchMode::Plain => "plain",
974 },
975 std::io::Cursor::new(mangled_body.as_ref() as &[u8]),
976 &subst(&matching_pattern, || package_name.clone()),
977 &package_name,
978 url.as_str(),
979 )?;
980
981 let mut releases = Vec::new();
982 for (version, full_url) in results {
983 let mangled_version = self.apply_uversionmangle(&version)?;
985
986 let mangled_url = self.apply_downloadurlmangle(&full_url)?;
988
989 let pgpsigurl = if let Some(mangle) = self.pgpsigurlmangle() {
991 Some(crate::mangle::apply_mangle(&mangle, &mangled_url)?)
992 } else {
993 None
994 };
995
996 let target_filename = if self.filenamemangle().is_some() {
998 Some(self.apply_filenamemangle(&mangled_url)?)
999 } else {
1000 None
1001 };
1002
1003 let package_version = if self.oversionmangle().is_some() {
1005 Some(self.apply_oversionmangle(&mangled_version)?)
1006 } else {
1007 None
1008 };
1009
1010 releases.push(crate::Release::new_full(
1011 mangled_version,
1012 mangled_url,
1013 pgpsigurl,
1014 target_filename,
1015 package_version,
1016 ));
1017 }
1018
1019 Ok(releases)
1020 }
1021
1022 #[cfg(all(feature = "discover", feature = "blocking"))]
1041 pub fn discover_blocking(
1042 &self,
1043 package: impl FnOnce() -> String,
1044 ) -> Result<Vec<crate::Release>, Box<dyn std::error::Error>> {
1045 let url = self.format_url(package);
1046 let user_agent = self
1047 .user_agent()
1048 .unwrap_or_else(|| crate::DEFAULT_USER_AGENT.to_string());
1049 let searchmode = self.searchmode().unwrap_or(crate::SearchMode::Html);
1050
1051 let client = reqwest::blocking::Client::builder()
1052 .user_agent(user_agent)
1053 .build()?;
1054
1055 let response = client.get(url.as_str()).send()?;
1056 let body = response.bytes()?;
1057
1058 let mangled_body = self.apply_pagemangle(&body)?;
1060
1061 let matching_pattern = self
1062 .matching_pattern()
1063 .ok_or("matching_pattern is required")?;
1064
1065 let package_name = String::new(); let results = crate::search::search(
1067 match searchmode {
1068 crate::SearchMode::Html => "html",
1069 crate::SearchMode::Plain => "plain",
1070 },
1071 std::io::Cursor::new(mangled_body.as_ref() as &[u8]),
1072 &subst(&matching_pattern, || package_name.clone()),
1073 &package_name,
1074 url.as_str(),
1075 )?;
1076
1077 let mut releases = Vec::new();
1078 for (version, full_url) in results {
1079 let mangled_version = self.apply_uversionmangle(&version)?;
1081
1082 let mangled_url = self.apply_downloadurlmangle(&full_url)?;
1084
1085 let pgpsigurl = if let Some(mangle) = self.pgpsigurlmangle() {
1087 Some(crate::mangle::apply_mangle(&mangle, &mangled_url)?)
1088 } else {
1089 None
1090 };
1091
1092 let target_filename = if self.filenamemangle().is_some() {
1094 Some(self.apply_filenamemangle(&mangled_url)?)
1095 } else {
1096 None
1097 };
1098
1099 let package_version = if self.oversionmangle().is_some() {
1101 Some(self.apply_oversionmangle(&mangled_version)?)
1102 } else {
1103 None
1104 };
1105
1106 releases.push(crate::Release::new_full(
1107 mangled_version,
1108 mangled_url,
1109 pgpsigurl,
1110 target_filename,
1111 package_version,
1112 ));
1113 }
1114
1115 Ok(releases)
1116 }
1117
1118 pub fn opts(&self) -> std::collections::HashMap<String, String> {
1120 let mut options = std::collections::HashMap::new();
1121
1122 if let Some(ol) = self.option_list() {
1123 for opt in ol.children() {
1124 let key = opt.key();
1125 let value = opt.value();
1126 if let (Some(key), Some(value)) = (key, value) {
1127 options.insert(key.to_string(), value.to_string());
1128 }
1129 }
1130 }
1131
1132 options
1133 }
1134
1135 fn items(&self) -> impl Iterator<Item = String> + '_ {
1136 self.0.children_with_tokens().filter_map(|it| match it {
1137 SyntaxElement::Token(token) => {
1138 if token.kind() == VALUE || token.kind() == KEY {
1139 Some(token.text().to_string())
1140 } else {
1141 None
1142 }
1143 }
1144 SyntaxElement::Node(node) => {
1145 match node.kind() {
1147 URL => Url::cast(node).map(|n| n.url()),
1148 MATCHING_PATTERN => MatchingPattern::cast(node).map(|n| n.pattern()),
1149 VERSION_POLICY => VersionPolicyNode::cast(node).map(|n| n.policy()),
1150 SCRIPT => ScriptNode::cast(node).map(|n| n.script()),
1151 _ => None,
1152 }
1153 }
1154 })
1155 }
1156
1157 pub fn url(&self) -> String {
1159 self.0
1160 .children()
1161 .find_map(Url::cast)
1162 .map(|it| it.url())
1163 .unwrap_or_else(|| {
1164 self.items().next().unwrap()
1166 })
1167 }
1168
1169 pub fn matching_pattern(&self) -> Option<String> {
1171 self.0
1172 .children()
1173 .find_map(MatchingPattern::cast)
1174 .map(|it| it.pattern())
1175 .or_else(|| {
1176 self.items().nth(1)
1178 })
1179 }
1180
1181 pub fn version(&self) -> Result<Option<crate::VersionPolicy>, String> {
1183 self.0
1184 .children()
1185 .find_map(VersionPolicyNode::cast)
1186 .map(|it| it.policy().parse())
1187 .transpose()
1188 .or_else(|_e| {
1189 self.items().nth(2).map(|it| it.parse()).transpose()
1191 })
1192 }
1193
1194 pub fn script(&self) -> Option<String> {
1196 self.0
1197 .children()
1198 .find_map(ScriptNode::cast)
1199 .map(|it| it.script())
1200 .or_else(|| {
1201 self.items().nth(3)
1203 })
1204 }
1205
1206 pub fn format_url(&self, package: impl FnOnce() -> String) -> url::Url {
1208 subst(self.url().as_str(), package).parse().unwrap()
1209 }
1210
1211 pub fn set_url(&mut self, new_url: &str) {
1213 let mut builder = GreenNodeBuilder::new();
1215 builder.start_node(URL.into());
1216 builder.token(VALUE.into(), new_url);
1217 builder.finish_node();
1218 let new_url_green = builder.finish();
1219
1220 let new_url_node = SyntaxNode::new_root_mut(new_url_green);
1222
1223 let url_pos = self
1225 .0
1226 .children_with_tokens()
1227 .position(|child| matches!(child, SyntaxElement::Node(node) if node.kind() == URL));
1228
1229 if let Some(pos) = url_pos {
1230 self.0
1232 .splice_children(pos..pos + 1, vec![new_url_node.into()]);
1233 }
1234 }
1235
1236 pub fn set_matching_pattern(&mut self, new_pattern: &str) {
1242 let mut builder = GreenNodeBuilder::new();
1244 builder.start_node(MATCHING_PATTERN.into());
1245 builder.token(VALUE.into(), new_pattern);
1246 builder.finish_node();
1247 let new_pattern_green = builder.finish();
1248
1249 let new_pattern_node = SyntaxNode::new_root_mut(new_pattern_green);
1251
1252 let pattern_pos = self.0.children_with_tokens().position(
1254 |child| matches!(child, SyntaxElement::Node(node) if node.kind() == MATCHING_PATTERN),
1255 );
1256
1257 if let Some(pos) = pattern_pos {
1258 self.0
1260 .splice_children(pos..pos + 1, vec![new_pattern_node.into()]);
1261 }
1262 }
1264
1265 pub fn set_version_policy(&mut self, new_policy: &str) {
1271 let mut builder = GreenNodeBuilder::new();
1273 builder.start_node(VERSION_POLICY.into());
1274 builder.token(VALUE.into(), new_policy);
1276 builder.finish_node();
1277 let new_policy_green = builder.finish();
1278
1279 let new_policy_node = SyntaxNode::new_root_mut(new_policy_green);
1281
1282 let policy_pos = self.0.children_with_tokens().position(
1284 |child| matches!(child, SyntaxElement::Node(node) if node.kind() == VERSION_POLICY),
1285 );
1286
1287 if let Some(pos) = policy_pos {
1288 self.0
1290 .splice_children(pos..pos + 1, vec![new_policy_node.into()]);
1291 }
1292 }
1294
1295 pub fn set_script(&mut self, new_script: &str) {
1301 let mut builder = GreenNodeBuilder::new();
1303 builder.start_node(SCRIPT.into());
1304 builder.token(VALUE.into(), new_script);
1306 builder.finish_node();
1307 let new_script_green = builder.finish();
1308
1309 let new_script_node = SyntaxNode::new_root_mut(new_script_green);
1311
1312 let script_pos = self
1314 .0
1315 .children_with_tokens()
1316 .position(|child| matches!(child, SyntaxElement::Node(node) if node.kind() == SCRIPT));
1317
1318 if let Some(pos) = script_pos {
1319 self.0
1321 .splice_children(pos..pos + 1, vec![new_script_node.into()]);
1322 }
1323 }
1325
1326 pub fn set_opt(&mut self, key: &str, value: &str) {
1332 let opts_pos = self.0.children_with_tokens().position(
1334 |child| matches!(child, SyntaxElement::Node(node) if node.kind() == OPTS_LIST),
1335 );
1336
1337 if let Some(_opts_idx) = opts_pos {
1338 if let Some(mut ol) = self.option_list() {
1339 if let Some(mut opt) = ol.find_option(key) {
1341 opt.set_value(value);
1343 } else {
1345 ol.add_option(key, value);
1347 }
1349 }
1350 } else {
1351 let mut builder = GreenNodeBuilder::new();
1353 builder.start_node(OPTS_LIST.into());
1354 builder.token(KEY.into(), "opts");
1355 builder.token(EQUALS.into(), "=");
1356 builder.start_node(OPTION.into());
1357 builder.token(KEY.into(), key);
1358 builder.token(EQUALS.into(), "=");
1359 builder.token(VALUE.into(), value);
1360 builder.finish_node();
1361 builder.finish_node();
1362 let new_opts_green = builder.finish();
1363 let new_opts_node = SyntaxNode::new_root_mut(new_opts_green);
1364
1365 let url_pos = self
1367 .0
1368 .children_with_tokens()
1369 .position(|child| matches!(child, SyntaxElement::Node(node) if node.kind() == URL));
1370
1371 if let Some(url_idx) = url_pos {
1372 let mut combined_builder = GreenNodeBuilder::new();
1375 combined_builder.start_node(ROOT.into()); combined_builder.token(WHITESPACE.into(), " ");
1377 combined_builder.finish_node();
1378 let temp_green = combined_builder.finish();
1379 let temp_root = SyntaxNode::new_root_mut(temp_green);
1380 let space_element = temp_root.children_with_tokens().next().unwrap();
1381
1382 self.0
1383 .splice_children(url_idx..url_idx, vec![new_opts_node.into(), space_element]);
1384 } else {
1385 self.0.splice_children(0..0, vec![new_opts_node.into()]);
1386 }
1387 }
1388 }
1389
1390 pub fn del_opt(&mut self, key: &str) {
1397 if let Some(mut ol) = self.option_list() {
1398 let option_count = ol.0.children().filter(|n| n.kind() == OPTION).count();
1399
1400 if option_count == 1 && ol.has_option(key) {
1401 let opts_pos = self.0.children().position(|node| node.kind() == OPTS_LIST);
1403
1404 if let Some(opts_idx) = opts_pos {
1405 self.0.splice_children(opts_idx..opts_idx + 1, vec![]);
1407
1408 while self.0.children_with_tokens().next().map_or(false, |e| {
1410 matches!(
1411 e,
1412 SyntaxElement::Token(t) if t.kind() == WHITESPACE || t.kind() == CONTINUATION
1413 )
1414 }) {
1415 self.0.splice_children(0..1, vec![]);
1416 }
1417 }
1418 } else {
1419 ol.remove_option(key);
1421 }
1422 }
1423 }
1424}
1425
1426const SUBSTITUTIONS: &[(&str, &str)] = &[
1427 ("@ANY_VERSION@", r"[-_]?(\d[\-+\.:\~\da-zA-Z]*)"),
1432 (
1435 "@ARCHIVE_EXT@",
1436 r"(?i)\.(?:tar\.xz|tar\.bz2|tar\.gz|zip|tgz|tbz|txz)",
1437 ),
1438 (
1441 "@SIGNATURE_EXT@",
1442 r"(?i)\.(?:tar\.xz|tar\.bz2|tar\.gz|zip|tgz|tbz|txz)\.(?:asc|pgp|gpg|sig|sign)",
1443 ),
1444 ("@DEB_EXT@", r"[\+~](debian|dfsg|ds|deb)(\.)?(\d+)?$"),
1446];
1447
1448pub fn subst(text: &str, package: impl FnOnce() -> String) -> String {
1449 let mut substs = SUBSTITUTIONS.to_vec();
1450 let package_name;
1451 if text.contains("@PACKAGE@") {
1452 package_name = Some(package());
1453 substs.push(("@PACKAGE@", package_name.as_deref().unwrap()));
1454 }
1455
1456 let mut text = text.to_string();
1457
1458 for (k, v) in substs {
1459 text = text.replace(k, v);
1460 }
1461
1462 text
1463}
1464
1465#[test]
1466fn test_subst() {
1467 assert_eq!(
1468 subst("@ANY_VERSION@", || unreachable!()),
1469 r"[-_]?(\d[\-+\.:\~\da-zA-Z]*)"
1470 );
1471 assert_eq!(subst("@PACKAGE@", || "dulwich".to_string()), "dulwich");
1472}
1473
1474impl std::fmt::Debug for OptionList {
1475 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1476 f.debug_struct("OptionList")
1477 .field("text", &self.0.text().to_string())
1478 .finish()
1479 }
1480}
1481
1482impl OptionList {
1483 fn children(&self) -> impl Iterator<Item = _Option> + '_ {
1484 self.0.children().filter_map(_Option::cast)
1485 }
1486
1487 pub fn has_option(&self, key: &str) -> bool {
1488 self.children().any(|it| it.key().as_deref() == Some(key))
1489 }
1490
1491 pub fn get_option(&self, key: &str) -> Option<String> {
1492 for child in self.children() {
1493 if child.key().as_deref() == Some(key) {
1494 return child.value();
1495 }
1496 }
1497 None
1498 }
1499
1500 fn find_option(&self, key: &str) -> Option<_Option> {
1502 self.children()
1503 .find(|opt| opt.key().as_deref() == Some(key))
1504 }
1505
1506 fn add_option(&mut self, key: &str, value: &str) {
1508 let option_count = self.0.children().filter(|n| n.kind() == OPTION).count();
1509
1510 let mut builder = GreenNodeBuilder::new();
1512 builder.start_node(ROOT.into()); if option_count > 0 {
1515 builder.start_node(OPTION_SEPARATOR.into());
1516 builder.token(COMMA.into(), ",");
1517 builder.finish_node();
1518 }
1519
1520 builder.start_node(OPTION.into());
1521 builder.token(KEY.into(), key);
1522 builder.token(EQUALS.into(), "=");
1523 builder.token(VALUE.into(), value);
1524 builder.finish_node();
1525
1526 builder.finish_node(); let combined_green = builder.finish();
1528
1529 let temp_root = SyntaxNode::new_root_mut(combined_green);
1531 let new_children: Vec<_> = temp_root.children_with_tokens().collect();
1532
1533 let insert_pos = self.0.children_with_tokens().count();
1534 self.0.splice_children(insert_pos..insert_pos, new_children);
1535 }
1536
1537 fn remove_option(&mut self, key: &str) -> bool {
1539 if let Some(mut opt) = self.find_option(key) {
1540 opt.remove();
1541 true
1542 } else {
1543 false
1544 }
1545 }
1546}
1547
1548impl _Option {
1549 pub fn key(&self) -> Option<String> {
1551 self.0.children_with_tokens().find_map(|it| match it {
1552 SyntaxElement::Token(token) => {
1553 if token.kind() == KEY {
1554 Some(token.text().to_string())
1555 } else {
1556 None
1557 }
1558 }
1559 _ => None,
1560 })
1561 }
1562
1563 pub fn value(&self) -> Option<String> {
1565 self.0
1566 .children_with_tokens()
1567 .filter_map(|it| match it {
1568 SyntaxElement::Token(token) => {
1569 if token.kind() == VALUE || token.kind() == KEY {
1570 Some(token.text().to_string())
1571 } else {
1572 None
1573 }
1574 }
1575 _ => None,
1576 })
1577 .nth(1)
1578 }
1579
1580 pub fn set_value(&mut self, new_value: &str) {
1582 let key = self.key().expect("Option must have a key");
1583
1584 let mut builder = GreenNodeBuilder::new();
1586 builder.start_node(OPTION.into());
1587 builder.token(KEY.into(), &key);
1588 builder.token(EQUALS.into(), "=");
1589 builder.token(VALUE.into(), new_value);
1590 builder.finish_node();
1591 let new_option_green = builder.finish();
1592 let new_option_node = SyntaxNode::new_root_mut(new_option_green);
1593
1594 if let Some(parent) = self.0.parent() {
1596 let idx = self.0.index();
1597 parent.splice_children(idx..idx + 1, vec![new_option_node.into()]);
1598 }
1599 }
1600
1601 pub fn remove(&mut self) {
1603 let next_sep = self
1605 .0
1606 .next_sibling()
1607 .filter(|n| n.kind() == OPTION_SEPARATOR);
1608 let prev_sep = self
1609 .0
1610 .prev_sibling()
1611 .filter(|n| n.kind() == OPTION_SEPARATOR);
1612
1613 if let Some(sep) = next_sep {
1615 sep.detach();
1616 } else if let Some(sep) = prev_sep {
1617 sep.detach();
1618 }
1619
1620 self.0.detach();
1622 }
1623}
1624
1625impl Url {
1626 pub fn url(&self) -> String {
1628 self.0
1629 .children_with_tokens()
1630 .find_map(|it| match it {
1631 SyntaxElement::Token(token) => {
1632 if token.kind() == VALUE {
1633 Some(token.text().to_string())
1634 } else {
1635 None
1636 }
1637 }
1638 _ => None,
1639 })
1640 .unwrap()
1641 }
1642}
1643
1644impl MatchingPattern {
1645 pub fn pattern(&self) -> String {
1647 self.0
1648 .children_with_tokens()
1649 .find_map(|it| match it {
1650 SyntaxElement::Token(token) => {
1651 if token.kind() == VALUE {
1652 Some(token.text().to_string())
1653 } else {
1654 None
1655 }
1656 }
1657 _ => None,
1658 })
1659 .unwrap()
1660 }
1661}
1662
1663impl VersionPolicyNode {
1664 pub fn policy(&self) -> String {
1666 self.0
1667 .children_with_tokens()
1668 .find_map(|it| match it {
1669 SyntaxElement::Token(token) => {
1670 if token.kind() == VALUE || token.kind() == KEY {
1672 Some(token.text().to_string())
1673 } else {
1674 None
1675 }
1676 }
1677 _ => None,
1678 })
1679 .unwrap()
1680 }
1681}
1682
1683impl ScriptNode {
1684 pub fn script(&self) -> String {
1686 self.0
1687 .children_with_tokens()
1688 .find_map(|it| match it {
1689 SyntaxElement::Token(token) => {
1690 if token.kind() == VALUE || token.kind() == KEY {
1692 Some(token.text().to_string())
1693 } else {
1694 None
1695 }
1696 }
1697 _ => None,
1698 })
1699 .unwrap()
1700 }
1701}
1702
1703#[test]
1704fn test_entry_node_structure() {
1705 let wf: super::WatchFile = r#"version=4
1707opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
1708"#
1709 .parse()
1710 .unwrap();
1711
1712 let entry = wf.entries().next().unwrap();
1713
1714 assert_eq!(entry.0.children().find(|n| n.kind() == URL).is_some(), true);
1716 assert_eq!(entry.url(), "https://example.com/releases");
1717
1718 assert_eq!(
1720 entry
1721 .0
1722 .children()
1723 .find(|n| n.kind() == MATCHING_PATTERN)
1724 .is_some(),
1725 true
1726 );
1727 assert_eq!(
1728 entry.matching_pattern(),
1729 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
1730 );
1731
1732 assert_eq!(
1734 entry
1735 .0
1736 .children()
1737 .find(|n| n.kind() == VERSION_POLICY)
1738 .is_some(),
1739 true
1740 );
1741 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
1742
1743 assert_eq!(
1745 entry.0.children().find(|n| n.kind() == SCRIPT).is_some(),
1746 true
1747 );
1748 assert_eq!(entry.script(), Some("uupdate".into()));
1749}
1750
1751#[test]
1752fn test_entry_node_structure_partial() {
1753 let wf: super::WatchFile = r#"version=4
1755https://github.com/example/tags .*/v?(\d\S+)\.tar\.gz
1756"#
1757 .parse()
1758 .unwrap();
1759
1760 let entry = wf.entries().next().unwrap();
1761
1762 assert_eq!(entry.0.children().find(|n| n.kind() == URL).is_some(), true);
1764 assert_eq!(
1765 entry
1766 .0
1767 .children()
1768 .find(|n| n.kind() == MATCHING_PATTERN)
1769 .is_some(),
1770 true
1771 );
1772
1773 assert_eq!(
1775 entry
1776 .0
1777 .children()
1778 .find(|n| n.kind() == VERSION_POLICY)
1779 .is_some(),
1780 false
1781 );
1782 assert_eq!(
1783 entry.0.children().find(|n| n.kind() == SCRIPT).is_some(),
1784 false
1785 );
1786
1787 assert_eq!(entry.url(), "https://github.com/example/tags");
1789 assert_eq!(
1790 entry.matching_pattern(),
1791 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
1792 );
1793 assert_eq!(entry.version(), Ok(None));
1794 assert_eq!(entry.script(), None);
1795}
1796
1797#[test]
1798fn test_parse_v1() {
1799 const WATCHV1: &str = r#"version=4
1800opts=bare,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1\.tar\.gz/ \
1801 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
1802"#;
1803 let parsed = parse(WATCHV1);
1804 let node = parsed.syntax();
1806 assert_eq!(
1807 format!("{:#?}", node),
1808 r#"ROOT@0..161
1809 VERSION@0..10
1810 KEY@0..7 "version"
1811 EQUALS@7..8 "="
1812 VALUE@8..9 "4"
1813 NEWLINE@9..10 "\n"
1814 ENTRY@10..161
1815 OPTS_LIST@10..86
1816 KEY@10..14 "opts"
1817 EQUALS@14..15 "="
1818 OPTION@15..19
1819 KEY@15..19 "bare"
1820 OPTION_SEPARATOR@19..20
1821 COMMA@19..20 ","
1822 OPTION@20..86
1823 KEY@20..34 "filenamemangle"
1824 EQUALS@34..35 "="
1825 VALUE@35..86 "s/.+\\/v?(\\d\\S+)\\.tar\\ ..."
1826 WHITESPACE@86..87 " "
1827 CONTINUATION@87..89 "\\\n"
1828 WHITESPACE@89..91 " "
1829 URL@91..138
1830 VALUE@91..138 "https://github.com/sy ..."
1831 WHITESPACE@138..139 " "
1832 MATCHING_PATTERN@139..160
1833 VALUE@139..160 ".*/v?(\\d\\S+)\\.tar\\.gz"
1834 NEWLINE@160..161 "\n"
1835"#
1836 );
1837
1838 let root = parsed.root();
1839 assert_eq!(root.version(), 4);
1840 let entries = root.entries().collect::<Vec<_>>();
1841 assert_eq!(entries.len(), 1);
1842 let entry = &entries[0];
1843 assert_eq!(
1844 entry.url(),
1845 "https://github.com/syncthing/syncthing-gtk/tags"
1846 );
1847 assert_eq!(
1848 entry.matching_pattern(),
1849 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
1850 );
1851 assert_eq!(entry.version(), Ok(None));
1852 assert_eq!(entry.script(), None);
1853
1854 assert_eq!(node.text(), WATCHV1);
1855}
1856
1857#[test]
1858fn test_parse_v2() {
1859 let parsed = parse(
1860 r#"version=4
1861https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
1862# comment
1863"#,
1864 );
1865 assert_eq!(parsed.errors, Vec::<String>::new());
1866 let node = parsed.syntax();
1867 assert_eq!(
1868 format!("{:#?}", node),
1869 r###"ROOT@0..90
1870 VERSION@0..10
1871 KEY@0..7 "version"
1872 EQUALS@7..8 "="
1873 VALUE@8..9 "4"
1874 NEWLINE@9..10 "\n"
1875 ENTRY@10..80
1876 URL@10..57
1877 VALUE@10..57 "https://github.com/sy ..."
1878 WHITESPACE@57..58 " "
1879 MATCHING_PATTERN@58..79
1880 VALUE@58..79 ".*/v?(\\d\\S+)\\.tar\\.gz"
1881 NEWLINE@79..80 "\n"
1882 COMMENT@80..89 "# comment"
1883 NEWLINE@89..90 "\n"
1884"###
1885 );
1886
1887 let root = parsed.root();
1888 assert_eq!(root.version(), 4);
1889 let entries = root.entries().collect::<Vec<_>>();
1890 assert_eq!(entries.len(), 1);
1891 let entry = &entries[0];
1892 assert_eq!(
1893 entry.url(),
1894 "https://github.com/syncthing/syncthing-gtk/tags"
1895 );
1896 assert_eq!(
1897 entry.format_url(|| "syncthing-gtk".to_string()),
1898 "https://github.com/syncthing/syncthing-gtk/tags"
1899 .parse()
1900 .unwrap()
1901 );
1902}
1903
1904#[test]
1905fn test_parse_v3() {
1906 let parsed = parse(
1907 r#"version=4
1908https://github.com/syncthing/@PACKAGE@/tags .*/v?(\d\S+)\.tar\.gz
1909# comment
1910"#,
1911 );
1912 assert_eq!(parsed.errors, Vec::<String>::new());
1913 let root = parsed.root();
1914 assert_eq!(root.version(), 4);
1915 let entries = root.entries().collect::<Vec<_>>();
1916 assert_eq!(entries.len(), 1);
1917 let entry = &entries[0];
1918 assert_eq!(entry.url(), "https://github.com/syncthing/@PACKAGE@/tags");
1919 assert_eq!(
1920 entry.format_url(|| "syncthing-gtk".to_string()),
1921 "https://github.com/syncthing/syncthing-gtk/tags"
1922 .parse()
1923 .unwrap()
1924 );
1925}
1926
1927#[test]
1928fn test_parse_v4() {
1929 let cl: super::WatchFile = r#"version=4
1930opts=repack,compression=xz,dversionmangle=s/\+ds//,repacksuffix=+ds \
1931 https://github.com/example/example-cat/tags \
1932 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
1933"#
1934 .parse()
1935 .unwrap();
1936 assert_eq!(cl.version(), 4);
1937 let entries = cl.entries().collect::<Vec<_>>();
1938 assert_eq!(entries.len(), 1);
1939 let entry = &entries[0];
1940 assert_eq!(entry.url(), "https://github.com/example/example-cat/tags");
1941 assert_eq!(
1942 entry.matching_pattern(),
1943 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
1944 );
1945 assert!(entry.repack());
1946 assert_eq!(entry.compression(), Ok(Some(Compression::Xz)));
1947 assert_eq!(entry.dversionmangle(), Some("s/\\+ds//".into()));
1948 assert_eq!(entry.repacksuffix(), Some("+ds".into()));
1949 assert_eq!(entry.script(), Some("uupdate".into()));
1950 assert_eq!(
1951 entry.format_url(|| "example-cat".to_string()),
1952 "https://github.com/example/example-cat/tags"
1953 .parse()
1954 .unwrap()
1955 );
1956 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
1957}
1958
1959#[test]
1960fn test_git_mode() {
1961 let text = r#"version=3
1962opts="mode=git, gitmode=shallow, pgpmode=gittag" \
1963https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git \
1964refs/tags/(.*) debian
1965"#;
1966 let parsed = parse(text);
1967 assert_eq!(parsed.errors, Vec::<String>::new());
1968 let cl = parsed.root();
1969 assert_eq!(cl.version(), 3);
1970 let entries = cl.entries().collect::<Vec<_>>();
1971 assert_eq!(entries.len(), 1);
1972 let entry = &entries[0];
1973 assert_eq!(
1974 entry.url(),
1975 "https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git"
1976 );
1977 assert_eq!(entry.matching_pattern(), Some("refs/tags/(.*)".into()));
1978 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
1979 assert_eq!(entry.script(), None);
1980 assert_eq!(entry.gitmode(), Ok(GitMode::Shallow));
1981 assert_eq!(entry.pgpmode(), Ok(PgpMode::GitTag));
1982 assert_eq!(entry.mode(), Ok(Mode::Git));
1983}
1984
1985#[test]
1986fn test_parse_quoted() {
1987 const WATCHV1: &str = r#"version=4
1988opts="bare, filenamemangle=blah" \
1989 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
1990"#;
1991 let parsed = parse(WATCHV1);
1992 let node = parsed.syntax();
1994
1995 let root = parsed.root();
1996 assert_eq!(root.version(), 4);
1997 let entries = root.entries().collect::<Vec<_>>();
1998 assert_eq!(entries.len(), 1);
1999 let entry = &entries[0];
2000
2001 assert_eq!(
2002 entry.url(),
2003 "https://github.com/syncthing/syncthing-gtk/tags"
2004 );
2005 assert_eq!(
2006 entry.matching_pattern(),
2007 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2008 );
2009 assert_eq!(entry.version(), Ok(None));
2010 assert_eq!(entry.script(), None);
2011
2012 assert_eq!(node.text(), WATCHV1);
2013}
2014
2015#[test]
2016fn test_set_url() {
2017 let wf: super::WatchFile = r#"version=4
2019https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2020"#
2021 .parse()
2022 .unwrap();
2023
2024 let mut entry = wf.entries().next().unwrap();
2025 assert_eq!(
2026 entry.url(),
2027 "https://github.com/syncthing/syncthing-gtk/tags"
2028 );
2029
2030 entry.set_url("https://newurl.example.org/path");
2031 assert_eq!(entry.url(), "https://newurl.example.org/path");
2032 assert_eq!(
2033 entry.matching_pattern(),
2034 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2035 );
2036
2037 assert_eq!(
2039 entry.to_string(),
2040 "https://newurl.example.org/path .*/v?(\\d\\S+)\\.tar\\.gz\n"
2041 );
2042}
2043
2044#[test]
2045fn test_set_url_with_options() {
2046 let wf: super::WatchFile = r#"version=4
2048opts=foo=blah https://foo.com/bar .*/v?(\d\S+)\.tar\.gz
2049"#
2050 .parse()
2051 .unwrap();
2052
2053 let mut entry = wf.entries().next().unwrap();
2054 assert_eq!(entry.url(), "https://foo.com/bar");
2055 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2056
2057 entry.set_url("https://example.com/baz");
2058 assert_eq!(entry.url(), "https://example.com/baz");
2059
2060 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2062 assert_eq!(
2063 entry.matching_pattern(),
2064 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2065 );
2066
2067 assert_eq!(
2069 entry.to_string(),
2070 "opts=foo=blah https://example.com/baz .*/v?(\\d\\S+)\\.tar\\.gz\n"
2071 );
2072}
2073
2074#[test]
2075fn test_set_url_complex() {
2076 let wf: super::WatchFile = r#"version=4
2078opts=bare,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1\.tar\.gz/ \
2079 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2080"#
2081 .parse()
2082 .unwrap();
2083
2084 let mut entry = wf.entries().next().unwrap();
2085 assert_eq!(
2086 entry.url(),
2087 "https://github.com/syncthing/syncthing-gtk/tags"
2088 );
2089
2090 entry.set_url("https://gitlab.com/newproject/tags");
2091 assert_eq!(entry.url(), "https://gitlab.com/newproject/tags");
2092
2093 assert!(entry.bare());
2095 assert_eq!(
2096 entry.filenamemangle(),
2097 Some("s/.+\\/v?(\\d\\S+)\\.tar\\.gz/syncthing-gtk-$1\\.tar\\.gz/".into())
2098 );
2099 assert_eq!(
2100 entry.matching_pattern(),
2101 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2102 );
2103
2104 assert_eq!(
2106 entry.to_string(),
2107 r#"opts=bare,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1\.tar\.gz/ \
2108 https://gitlab.com/newproject/tags .*/v?(\d\S+)\.tar\.gz
2109"#
2110 );
2111}
2112
2113#[test]
2114fn test_set_url_with_all_fields() {
2115 let wf: super::WatchFile = r#"version=4
2117opts=repack,compression=xz,dversionmangle=s/\+ds//,repacksuffix=+ds \
2118 https://github.com/example/example-cat/tags \
2119 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2120"#
2121 .parse()
2122 .unwrap();
2123
2124 let mut entry = wf.entries().next().unwrap();
2125 assert_eq!(entry.url(), "https://github.com/example/example-cat/tags");
2126 assert_eq!(
2127 entry.matching_pattern(),
2128 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2129 );
2130 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2131 assert_eq!(entry.script(), Some("uupdate".into()));
2132
2133 entry.set_url("https://gitlab.example.org/project/releases");
2134 assert_eq!(entry.url(), "https://gitlab.example.org/project/releases");
2135
2136 assert!(entry.repack());
2138 assert_eq!(entry.compression(), Ok(Some(super::Compression::Xz)));
2139 assert_eq!(entry.dversionmangle(), Some("s/\\+ds//".into()));
2140 assert_eq!(entry.repacksuffix(), Some("+ds".into()));
2141 assert_eq!(
2142 entry.matching_pattern(),
2143 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2144 );
2145 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2146 assert_eq!(entry.script(), Some("uupdate".into()));
2147
2148 assert_eq!(
2150 entry.to_string(),
2151 r#"opts=repack,compression=xz,dversionmangle=s/\+ds//,repacksuffix=+ds \
2152 https://gitlab.example.org/project/releases \
2153 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2154"#
2155 );
2156}
2157
2158#[test]
2159fn test_set_url_quoted_options() {
2160 let wf: super::WatchFile = r#"version=4
2162opts="bare, filenamemangle=blah" \
2163 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2164"#
2165 .parse()
2166 .unwrap();
2167
2168 let mut entry = wf.entries().next().unwrap();
2169 assert_eq!(
2170 entry.url(),
2171 "https://github.com/syncthing/syncthing-gtk/tags"
2172 );
2173
2174 entry.set_url("https://example.org/new/path");
2175 assert_eq!(entry.url(), "https://example.org/new/path");
2176
2177 assert_eq!(
2179 entry.to_string(),
2180 r#"opts="bare, filenamemangle=blah" \
2181 https://example.org/new/path .*/v?(\d\S+)\.tar\.gz
2182"#
2183 );
2184}
2185
2186#[test]
2187fn test_set_opt_update_existing() {
2188 let wf: super::WatchFile = r#"version=4
2190opts=foo=blah,bar=baz https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2191"#
2192 .parse()
2193 .unwrap();
2194
2195 let mut entry = wf.entries().next().unwrap();
2196 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2197 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2198
2199 entry.set_opt("foo", "updated");
2200 assert_eq!(entry.get_option("foo"), Some("updated".to_string()));
2201 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2202
2203 assert_eq!(
2205 entry.to_string(),
2206 "opts=foo=updated,bar=baz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2207 );
2208}
2209
2210#[test]
2211fn test_set_opt_add_new() {
2212 let wf: super::WatchFile = r#"version=4
2214opts=foo=blah https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2215"#
2216 .parse()
2217 .unwrap();
2218
2219 let mut entry = wf.entries().next().unwrap();
2220 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2221 assert_eq!(entry.get_option("bar"), None);
2222
2223 entry.set_opt("bar", "baz");
2224 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2225 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2226
2227 assert_eq!(
2229 entry.to_string(),
2230 "opts=foo=blah,bar=baz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2231 );
2232}
2233
2234#[test]
2235fn test_set_opt_create_options_list() {
2236 let wf: super::WatchFile = r#"version=4
2238https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2239"#
2240 .parse()
2241 .unwrap();
2242
2243 let mut entry = wf.entries().next().unwrap();
2244 assert_eq!(entry.option_list(), None);
2245
2246 entry.set_opt("compression", "xz");
2247 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
2248
2249 assert_eq!(
2251 entry.to_string(),
2252 "opts=compression=xz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2253 );
2254}
2255
2256#[test]
2257fn test_del_opt_remove_single() {
2258 let wf: super::WatchFile = r#"version=4
2260opts=foo=blah,bar=baz,qux=quux https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2261"#
2262 .parse()
2263 .unwrap();
2264
2265 let mut entry = wf.entries().next().unwrap();
2266 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2267 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2268 assert_eq!(entry.get_option("qux"), Some("quux".to_string()));
2269
2270 entry.del_opt("bar");
2271 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2272 assert_eq!(entry.get_option("bar"), None);
2273 assert_eq!(entry.get_option("qux"), Some("quux".to_string()));
2274
2275 assert_eq!(
2277 entry.to_string(),
2278 "opts=foo=blah,qux=quux https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2279 );
2280}
2281
2282#[test]
2283fn test_del_opt_remove_first() {
2284 let wf: super::WatchFile = r#"version=4
2286opts=foo=blah,bar=baz https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2287"#
2288 .parse()
2289 .unwrap();
2290
2291 let mut entry = wf.entries().next().unwrap();
2292 entry.del_opt("foo");
2293 assert_eq!(entry.get_option("foo"), None);
2294 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2295
2296 assert_eq!(
2298 entry.to_string(),
2299 "opts=bar=baz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2300 );
2301}
2302
2303#[test]
2304fn test_del_opt_remove_last() {
2305 let wf: super::WatchFile = r#"version=4
2307opts=foo=blah,bar=baz https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2308"#
2309 .parse()
2310 .unwrap();
2311
2312 let mut entry = wf.entries().next().unwrap();
2313 entry.del_opt("bar");
2314 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2315 assert_eq!(entry.get_option("bar"), None);
2316
2317 assert_eq!(
2319 entry.to_string(),
2320 "opts=foo=blah https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2321 );
2322}
2323
2324#[test]
2325fn test_del_opt_remove_only_option() {
2326 let wf: super::WatchFile = r#"version=4
2328opts=foo=blah https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2329"#
2330 .parse()
2331 .unwrap();
2332
2333 let mut entry = wf.entries().next().unwrap();
2334 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2335
2336 entry.del_opt("foo");
2337 assert_eq!(entry.get_option("foo"), None);
2338 assert_eq!(entry.option_list(), None);
2339
2340 assert_eq!(
2342 entry.to_string(),
2343 "https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2344 );
2345}
2346
2347#[test]
2348fn test_del_opt_nonexistent() {
2349 let wf: super::WatchFile = r#"version=4
2351opts=foo=blah https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2352"#
2353 .parse()
2354 .unwrap();
2355
2356 let mut entry = wf.entries().next().unwrap();
2357 let original = entry.to_string();
2358
2359 entry.del_opt("nonexistent");
2360 assert_eq!(entry.to_string(), original);
2361}
2362
2363#[test]
2364fn test_set_opt_multiple_operations() {
2365 let wf: super::WatchFile = r#"version=4
2367https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2368"#
2369 .parse()
2370 .unwrap();
2371
2372 let mut entry = wf.entries().next().unwrap();
2373
2374 entry.set_opt("compression", "xz");
2375 entry.set_opt("repack", "");
2376 entry.set_opt("dversionmangle", "s/\\+ds//");
2377
2378 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
2379 assert_eq!(
2380 entry.get_option("dversionmangle"),
2381 Some("s/\\+ds//".to_string())
2382 );
2383}
2384
2385#[test]
2386fn test_set_matching_pattern() {
2387 let wf: super::WatchFile = r#"version=4
2389https://github.com/example/tags .*/v?(\d\S+)\.tar\.gz
2390"#
2391 .parse()
2392 .unwrap();
2393
2394 let mut entry = wf.entries().next().unwrap();
2395 assert_eq!(
2396 entry.matching_pattern(),
2397 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2398 );
2399
2400 entry.set_matching_pattern("(?:.*?/)?v?([\\d.]+)\\.tar\\.gz");
2401 assert_eq!(
2402 entry.matching_pattern(),
2403 Some("(?:.*?/)?v?([\\d.]+)\\.tar\\.gz".into())
2404 );
2405
2406 assert_eq!(entry.url(), "https://github.com/example/tags");
2408
2409 assert_eq!(
2411 entry.to_string(),
2412 "https://github.com/example/tags (?:.*?/)?v?([\\d.]+)\\.tar\\.gz\n"
2413 );
2414}
2415
2416#[test]
2417fn test_set_matching_pattern_with_all_fields() {
2418 let wf: super::WatchFile = r#"version=4
2420opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2421"#
2422 .parse()
2423 .unwrap();
2424
2425 let mut entry = wf.entries().next().unwrap();
2426 assert_eq!(
2427 entry.matching_pattern(),
2428 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2429 );
2430
2431 entry.set_matching_pattern(".*/version-([\\d.]+)\\.tar\\.xz");
2432 assert_eq!(
2433 entry.matching_pattern(),
2434 Some(".*/version-([\\d.]+)\\.tar\\.xz".into())
2435 );
2436
2437 assert_eq!(entry.url(), "https://example.com/releases");
2439 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2440 assert_eq!(entry.script(), Some("uupdate".into()));
2441 assert_eq!(entry.compression(), Ok(Some(super::Compression::Xz)));
2442
2443 assert_eq!(
2445 entry.to_string(),
2446 "opts=compression=xz https://example.com/releases .*/version-([\\d.]+)\\.tar\\.xz debian uupdate\n"
2447 );
2448}
2449
2450#[test]
2451fn test_set_version_policy() {
2452 let wf: super::WatchFile = r#"version=4
2454https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2455"#
2456 .parse()
2457 .unwrap();
2458
2459 let mut entry = wf.entries().next().unwrap();
2460 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2461
2462 entry.set_version_policy("previous");
2463 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Previous)));
2464
2465 assert_eq!(entry.url(), "https://example.com/releases");
2467 assert_eq!(
2468 entry.matching_pattern(),
2469 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2470 );
2471 assert_eq!(entry.script(), Some("uupdate".into()));
2472
2473 assert_eq!(
2475 entry.to_string(),
2476 "https://example.com/releases (?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz previous uupdate\n"
2477 );
2478}
2479
2480#[test]
2481fn test_set_version_policy_with_options() {
2482 let wf: super::WatchFile = r#"version=4
2484opts=repack,compression=xz \
2485 https://github.com/example/example-cat/tags \
2486 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2487"#
2488 .parse()
2489 .unwrap();
2490
2491 let mut entry = wf.entries().next().unwrap();
2492 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2493
2494 entry.set_version_policy("ignore");
2495 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Ignore)));
2496
2497 assert_eq!(entry.url(), "https://github.com/example/example-cat/tags");
2499 assert_eq!(
2500 entry.matching_pattern(),
2501 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2502 );
2503 assert_eq!(entry.script(), Some("uupdate".into()));
2504 assert!(entry.repack());
2505
2506 assert_eq!(
2508 entry.to_string(),
2509 r#"opts=repack,compression=xz \
2510 https://github.com/example/example-cat/tags \
2511 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz ignore uupdate
2512"#
2513 );
2514}
2515
2516#[test]
2517fn test_set_script() {
2518 let wf: super::WatchFile = r#"version=4
2520https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2521"#
2522 .parse()
2523 .unwrap();
2524
2525 let mut entry = wf.entries().next().unwrap();
2526 assert_eq!(entry.script(), Some("uupdate".into()));
2527
2528 entry.set_script("uscan");
2529 assert_eq!(entry.script(), Some("uscan".into()));
2530
2531 assert_eq!(entry.url(), "https://example.com/releases");
2533 assert_eq!(
2534 entry.matching_pattern(),
2535 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2536 );
2537 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2538
2539 assert_eq!(
2541 entry.to_string(),
2542 "https://example.com/releases (?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz debian uscan\n"
2543 );
2544}
2545
2546#[test]
2547fn test_set_script_with_options() {
2548 let wf: super::WatchFile = r#"version=4
2550opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2551"#
2552 .parse()
2553 .unwrap();
2554
2555 let mut entry = wf.entries().next().unwrap();
2556 assert_eq!(entry.script(), Some("uupdate".into()));
2557
2558 entry.set_script("custom-script.sh");
2559 assert_eq!(entry.script(), Some("custom-script.sh".into()));
2560
2561 assert_eq!(entry.url(), "https://example.com/releases");
2563 assert_eq!(
2564 entry.matching_pattern(),
2565 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2566 );
2567 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2568 assert_eq!(entry.compression(), Ok(Some(super::Compression::Xz)));
2569
2570 assert_eq!(
2572 entry.to_string(),
2573 "opts=compression=xz https://example.com/releases (?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz debian custom-script.sh\n"
2574 );
2575}
2576
2577#[test]
2578fn test_apply_dversionmangle() {
2579 let wf: super::WatchFile = r#"version=4
2581opts=dversionmangle=s/\+dfsg$// https://example.com/ .*
2582"#
2583 .parse()
2584 .unwrap();
2585 let entry = wf.entries().next().unwrap();
2586 assert_eq!(entry.apply_dversionmangle("1.0+dfsg").unwrap(), "1.0");
2587 assert_eq!(entry.apply_dversionmangle("1.0").unwrap(), "1.0");
2588
2589 let wf: super::WatchFile = r#"version=4
2591opts=versionmangle=s/^v// https://example.com/ .*
2592"#
2593 .parse()
2594 .unwrap();
2595 let entry = wf.entries().next().unwrap();
2596 assert_eq!(entry.apply_dversionmangle("v1.0").unwrap(), "1.0");
2597
2598 let wf: super::WatchFile = r#"version=4
2600opts=dversionmangle=s/\+ds//,versionmangle=s/^v// https://example.com/ .*
2601"#
2602 .parse()
2603 .unwrap();
2604 let entry = wf.entries().next().unwrap();
2605 assert_eq!(entry.apply_dversionmangle("1.0+ds").unwrap(), "1.0");
2606
2607 let wf: super::WatchFile = r#"version=4
2609https://example.com/ .*
2610"#
2611 .parse()
2612 .unwrap();
2613 let entry = wf.entries().next().unwrap();
2614 assert_eq!(entry.apply_dversionmangle("1.0+dfsg").unwrap(), "1.0+dfsg");
2615}
2616
2617#[test]
2618fn test_apply_oversionmangle() {
2619 let wf: super::WatchFile = r#"version=4
2621opts=oversionmangle=s/$/-1/ https://example.com/ .*
2622"#
2623 .parse()
2624 .unwrap();
2625 let entry = wf.entries().next().unwrap();
2626 assert_eq!(entry.apply_oversionmangle("1.0").unwrap(), "1.0-1");
2627 assert_eq!(entry.apply_oversionmangle("2.5.3").unwrap(), "2.5.3-1");
2628
2629 let wf: super::WatchFile = r#"version=4
2631opts=oversionmangle=s/$/.dfsg/ https://example.com/ .*
2632"#
2633 .parse()
2634 .unwrap();
2635 let entry = wf.entries().next().unwrap();
2636 assert_eq!(entry.apply_oversionmangle("1.0").unwrap(), "1.0.dfsg");
2637
2638 let wf: super::WatchFile = r#"version=4
2640https://example.com/ .*
2641"#
2642 .parse()
2643 .unwrap();
2644 let entry = wf.entries().next().unwrap();
2645 assert_eq!(entry.apply_oversionmangle("1.0").unwrap(), "1.0");
2646}
2647
2648#[test]
2649fn test_apply_dirversionmangle() {
2650 let wf: super::WatchFile = r#"version=4
2652opts=dirversionmangle=s/^v// https://example.com/ .*
2653"#
2654 .parse()
2655 .unwrap();
2656 let entry = wf.entries().next().unwrap();
2657 assert_eq!(entry.apply_dirversionmangle("v1.0").unwrap(), "1.0");
2658 assert_eq!(entry.apply_dirversionmangle("v2.5.3").unwrap(), "2.5.3");
2659
2660 let wf: super::WatchFile = r#"version=4
2662opts=dirversionmangle=s/v(\d)/$1/ https://example.com/ .*
2663"#
2664 .parse()
2665 .unwrap();
2666 let entry = wf.entries().next().unwrap();
2667 assert_eq!(entry.apply_dirversionmangle("v1.0").unwrap(), "1.0");
2668
2669 let wf: super::WatchFile = r#"version=4
2671https://example.com/ .*
2672"#
2673 .parse()
2674 .unwrap();
2675 let entry = wf.entries().next().unwrap();
2676 assert_eq!(entry.apply_dirversionmangle("v1.0").unwrap(), "v1.0");
2677}
2678
2679#[test]
2680fn test_apply_filenamemangle() {
2681 let wf: super::WatchFile = r#"version=4
2683opts=filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/mypackage-$1.tar.gz/ https://example.com/ .*
2684"#
2685 .parse()
2686 .unwrap();
2687 let entry = wf.entries().next().unwrap();
2688 assert_eq!(
2689 entry
2690 .apply_filenamemangle("https://example.com/v1.0.tar.gz")
2691 .unwrap(),
2692 "mypackage-1.0.tar.gz"
2693 );
2694 assert_eq!(
2695 entry
2696 .apply_filenamemangle("https://example.com/2.5.3.tar.gz")
2697 .unwrap(),
2698 "mypackage-2.5.3.tar.gz"
2699 );
2700
2701 let wf: super::WatchFile = r#"version=4
2703opts=filenamemangle=s/.*\/(.*)/$1/ https://example.com/ .*
2704"#
2705 .parse()
2706 .unwrap();
2707 let entry = wf.entries().next().unwrap();
2708 assert_eq!(
2709 entry
2710 .apply_filenamemangle("https://example.com/path/to/file.tar.gz")
2711 .unwrap(),
2712 "file.tar.gz"
2713 );
2714
2715 let wf: super::WatchFile = r#"version=4
2717https://example.com/ .*
2718"#
2719 .parse()
2720 .unwrap();
2721 let entry = wf.entries().next().unwrap();
2722 assert_eq!(
2723 entry
2724 .apply_filenamemangle("https://example.com/file.tar.gz")
2725 .unwrap(),
2726 "https://example.com/file.tar.gz"
2727 );
2728}
2729
2730#[test]
2731fn test_apply_pagemangle() {
2732 let wf: super::WatchFile = r#"version=4
2734opts=pagemangle=s/&/&/g https://example.com/ .*
2735"#
2736 .parse()
2737 .unwrap();
2738 let entry = wf.entries().next().unwrap();
2739 assert_eq!(
2740 entry.apply_pagemangle(b"foo & bar").unwrap(),
2741 b"foo & bar"
2742 );
2743 assert_eq!(
2744 entry
2745 .apply_pagemangle(b"& foo & bar &")
2746 .unwrap(),
2747 b"& foo & bar &"
2748 );
2749
2750 let wf: super::WatchFile = r#"version=4
2752opts=pagemangle=s/<[^>]+>//g https://example.com/ .*
2753"#
2754 .parse()
2755 .unwrap();
2756 let entry = wf.entries().next().unwrap();
2757 assert_eq!(entry.apply_pagemangle(b"<div>text</div>").unwrap(), b"text");
2758
2759 let wf: super::WatchFile = r#"version=4
2761https://example.com/ .*
2762"#
2763 .parse()
2764 .unwrap();
2765 let entry = wf.entries().next().unwrap();
2766 assert_eq!(
2767 entry.apply_pagemangle(b"foo & bar").unwrap(),
2768 b"foo & bar"
2769 );
2770}
2771
2772#[test]
2773fn test_apply_downloadurlmangle() {
2774 let wf: super::WatchFile = r#"version=4
2776opts=downloadurlmangle=s|/archive/|/download/| https://example.com/ .*
2777"#
2778 .parse()
2779 .unwrap();
2780 let entry = wf.entries().next().unwrap();
2781 assert_eq!(
2782 entry
2783 .apply_downloadurlmangle("https://example.com/archive/file.tar.gz")
2784 .unwrap(),
2785 "https://example.com/download/file.tar.gz"
2786 );
2787
2788 let wf: super::WatchFile = r#"version=4
2790opts=downloadurlmangle=s/github\.com/raw.githubusercontent.com/ https://example.com/ .*
2791"#
2792 .parse()
2793 .unwrap();
2794 let entry = wf.entries().next().unwrap();
2795 assert_eq!(
2796 entry
2797 .apply_downloadurlmangle("https://github.com/user/repo/file.tar.gz")
2798 .unwrap(),
2799 "https://raw.githubusercontent.com/user/repo/file.tar.gz"
2800 );
2801
2802 let wf: super::WatchFile = r#"version=4
2804https://example.com/ .*
2805"#
2806 .parse()
2807 .unwrap();
2808 let entry = wf.entries().next().unwrap();
2809 assert_eq!(
2810 entry
2811 .apply_downloadurlmangle("https://example.com/archive/file.tar.gz")
2812 .unwrap(),
2813 "https://example.com/archive/file.tar.gz"
2814 );
2815}