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
365fn line_col_at_offset(node: &SyntaxNode, offset: rowan::TextSize) -> (usize, usize) {
368 let root = node.ancestors().last().unwrap_or_else(|| node.clone());
369 let mut line = 0;
370 let mut last_newline_offset = rowan::TextSize::from(0);
371
372 for element in root.preorder_with_tokens() {
373 if let rowan::WalkEvent::Enter(rowan::NodeOrToken::Token(token)) = element {
374 if token.text_range().start() >= offset {
375 break;
376 }
377
378 for (idx, _) in token.text().match_indices('\n') {
380 line += 1;
381 last_newline_offset =
382 token.text_range().start() + rowan::TextSize::from((idx + 1) as u32);
383 }
384 }
385 }
386
387 let column: usize = (offset - last_newline_offset).into();
388 (line, column)
389}
390
391macro_rules! ast_node {
392 ($ast:ident, $kind:ident) => {
393 #[derive(PartialEq, Eq, Hash)]
394 #[repr(transparent)]
395 pub struct $ast(SyntaxNode);
397 impl $ast {
398 #[allow(unused)]
399 fn cast(node: SyntaxNode) -> Option<Self> {
400 if node.kind() == $kind {
401 Some(Self(node))
402 } else {
403 None
404 }
405 }
406
407 pub fn line(&self) -> usize {
409 line_col_at_offset(&self.0, self.0.text_range().start()).0
410 }
411
412 pub fn column(&self) -> usize {
414 line_col_at_offset(&self.0, self.0.text_range().start()).1
415 }
416
417 pub fn line_col(&self) -> (usize, usize) {
420 line_col_at_offset(&self.0, self.0.text_range().start())
421 }
422 }
423
424 impl ToString for $ast {
425 fn to_string(&self) -> String {
426 self.0.text().to_string()
427 }
428 }
429 };
430}
431
432ast_node!(WatchFile, ROOT);
433ast_node!(Version, VERSION);
434ast_node!(Entry, ENTRY);
435ast_node!(OptionList, OPTS_LIST);
436ast_node!(_Option, OPTION);
437ast_node!(Url, URL);
438ast_node!(MatchingPattern, MATCHING_PATTERN);
439ast_node!(VersionPolicyNode, VERSION_POLICY);
440ast_node!(ScriptNode, SCRIPT);
441
442impl WatchFile {
443 pub fn new(version: Option<u32>) -> WatchFile {
445 let mut builder = GreenNodeBuilder::new();
446
447 builder.start_node(ROOT.into());
448 if let Some(version) = version {
449 builder.start_node(VERSION.into());
450 builder.token(KEY.into(), "version");
451 builder.token(EQUALS.into(), "=");
452 builder.token(VALUE.into(), version.to_string().as_str());
453 builder.token(NEWLINE.into(), "\n");
454 builder.finish_node();
455 }
456 builder.finish_node();
457 WatchFile(SyntaxNode::new_root_mut(builder.finish()))
458 }
459
460 pub fn version_node(&self) -> Option<Version> {
462 self.0.children().find_map(Version::cast)
463 }
464
465 pub fn version(&self) -> u32 {
467 self.version_node()
468 .map(|it| it.version())
469 .unwrap_or(DEFAULT_VERSION)
470 }
471
472 pub fn entries(&self) -> impl Iterator<Item = Entry> + '_ {
474 self.0.children().filter_map(Entry::cast)
475 }
476
477 pub fn set_version(&mut self, new_version: u32) {
479 let mut builder = GreenNodeBuilder::new();
481 builder.start_node(VERSION.into());
482 builder.token(KEY.into(), "version");
483 builder.token(EQUALS.into(), "=");
484 builder.token(VALUE.into(), new_version.to_string().as_str());
485 builder.token(NEWLINE.into(), "\n");
486 builder.finish_node();
487 let new_version_green = builder.finish();
488
489 let new_version_node = SyntaxNode::new_root_mut(new_version_green);
491
492 let version_pos = self.0.children().position(|child| child.kind() == VERSION);
494
495 if let Some(pos) = version_pos {
496 self.0
498 .splice_children(pos..pos + 1, vec![new_version_node.into()]);
499 } else {
500 self.0.splice_children(0..0, vec![new_version_node.into()]);
502 }
503 }
504
505 #[cfg(feature = "discover")]
525 pub async fn uscan(
526 &self,
527 package: impl Fn() -> String,
528 ) -> Result<Vec<Vec<crate::Release>>, Box<dyn std::error::Error>> {
529 let mut all_releases = Vec::new();
530
531 for entry in self.entries() {
532 let releases = entry.discover(|| package()).await?;
533 all_releases.push(releases);
534 }
535
536 Ok(all_releases)
537 }
538
539 #[cfg(all(feature = "discover", feature = "blocking"))]
557 pub fn uscan_blocking(
558 &self,
559 package: impl Fn() -> String,
560 ) -> Result<Vec<Vec<crate::Release>>, Box<dyn std::error::Error>> {
561 let mut all_releases = Vec::new();
562
563 for entry in self.entries() {
564 let releases = entry.discover_blocking(|| package())?;
565 all_releases.push(releases);
566 }
567
568 Ok(all_releases)
569 }
570
571 pub fn add_entry(&mut self, entry: Entry) {
598 let insert_pos = self.0.children_with_tokens().count();
600
601 let entry_green = entry.0.green().into_owned();
603 let new_entry_node = SyntaxNode::new_root_mut(entry_green);
604
605 self.0
607 .splice_children(insert_pos..insert_pos, vec![new_entry_node.into()]);
608 }
609}
610
611impl FromStr for WatchFile {
612 type Err = ParseError;
613
614 fn from_str(s: &str) -> Result<Self, Self::Err> {
615 let parsed = parse(s);
616 if parsed.errors.is_empty() {
617 Ok(parsed.root())
618 } else {
619 Err(ParseError(parsed.errors))
620 }
621 }
622}
623
624impl Version {
625 pub fn version(&self) -> u32 {
627 self.0
628 .children_with_tokens()
629 .find_map(|it| match it {
630 SyntaxElement::Token(token) => {
631 if token.kind() == VALUE {
632 Some(token.text().parse().unwrap())
633 } else {
634 None
635 }
636 }
637 _ => None,
638 })
639 .unwrap_or(DEFAULT_VERSION)
640 }
641}
642
643#[derive(Debug, Clone, Default)]
667pub struct EntryBuilder {
668 url: Option<String>,
669 matching_pattern: Option<String>,
670 version_policy: Option<String>,
671 script: Option<String>,
672 opts: std::collections::HashMap<String, String>,
673}
674
675impl EntryBuilder {
676 pub fn new(url: impl Into<String>) -> Self {
678 EntryBuilder {
679 url: Some(url.into()),
680 matching_pattern: None,
681 version_policy: None,
682 script: None,
683 opts: std::collections::HashMap::new(),
684 }
685 }
686
687 pub fn matching_pattern(mut self, pattern: impl Into<String>) -> Self {
689 self.matching_pattern = Some(pattern.into());
690 self
691 }
692
693 pub fn version_policy(mut self, policy: impl Into<String>) -> Self {
695 self.version_policy = Some(policy.into());
696 self
697 }
698
699 pub fn script(mut self, script: impl Into<String>) -> Self {
701 self.script = Some(script.into());
702 self
703 }
704
705 pub fn opt(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
707 self.opts.insert(key.into(), value.into());
708 self
709 }
710
711 pub fn flag(mut self, key: impl Into<String>) -> Self {
715 self.opts.insert(key.into(), String::new());
716 self
717 }
718
719 pub fn build(self) -> Entry {
725 let url = self.url.expect("URL is required for entry");
726
727 let mut builder = GreenNodeBuilder::new();
728
729 builder.start_node(ENTRY.into());
730
731 if !self.opts.is_empty() {
733 builder.start_node(OPTS_LIST.into());
734 builder.token(KEY.into(), "opts");
735 builder.token(EQUALS.into(), "=");
736
737 let mut first = true;
738 for (key, value) in self.opts.iter() {
739 if !first {
740 builder.token(COMMA.into(), ",");
741 }
742 first = false;
743
744 builder.start_node(OPTION.into());
745 builder.token(KEY.into(), key);
746 if !value.is_empty() {
747 builder.token(EQUALS.into(), "=");
748 builder.token(VALUE.into(), value);
749 }
750 builder.finish_node();
751 }
752
753 builder.finish_node();
754 builder.token(WHITESPACE.into(), " ");
755 }
756
757 builder.start_node(URL.into());
759 builder.token(VALUE.into(), &url);
760 builder.finish_node();
761
762 if let Some(pattern) = self.matching_pattern {
764 builder.token(WHITESPACE.into(), " ");
765 builder.start_node(MATCHING_PATTERN.into());
766 builder.token(VALUE.into(), &pattern);
767 builder.finish_node();
768 }
769
770 if let Some(policy) = self.version_policy {
772 builder.token(WHITESPACE.into(), " ");
773 builder.start_node(VERSION_POLICY.into());
774 builder.token(VALUE.into(), &policy);
775 builder.finish_node();
776 }
777
778 if let Some(script_val) = self.script {
780 builder.token(WHITESPACE.into(), " ");
781 builder.start_node(SCRIPT.into());
782 builder.token(VALUE.into(), &script_val);
783 builder.finish_node();
784 }
785
786 builder.token(NEWLINE.into(), "\n");
787 builder.finish_node();
788
789 Entry(SyntaxNode::new_root_mut(builder.finish()))
790 }
791}
792
793impl Entry {
794 pub fn builder(url: impl Into<String>) -> EntryBuilder {
808 EntryBuilder::new(url)
809 }
810
811 pub fn option_list(&self) -> Option<OptionList> {
813 self.0.children().find_map(OptionList::cast)
814 }
815
816 pub fn get_option(&self, key: &str) -> Option<String> {
818 self.option_list().and_then(|ol| ol.get_option(key))
819 }
820
821 pub fn has_option(&self, key: &str) -> bool {
823 self.option_list().map_or(false, |ol| ol.has_option(key))
824 }
825
826 pub fn component(&self) -> Option<String> {
828 self.get_option("component")
829 }
830
831 pub fn ctype(&self) -> Result<Option<ComponentType>, ()> {
833 self.get_option("ctype").map(|s| s.parse()).transpose()
834 }
835
836 pub fn compression(&self) -> Result<Option<Compression>, ()> {
838 self.get_option("compression")
839 .map(|s| s.parse())
840 .transpose()
841 }
842
843 pub fn repack(&self) -> bool {
845 self.has_option("repack")
846 }
847
848 pub fn repacksuffix(&self) -> Option<String> {
850 self.get_option("repacksuffix")
851 }
852
853 pub fn mode(&self) -> Result<Mode, ()> {
855 Ok(self
856 .get_option("mode")
857 .map(|s| s.parse())
858 .transpose()?
859 .unwrap_or_default())
860 }
861
862 pub fn pretty(&self) -> Result<Pretty, ()> {
864 Ok(self
865 .get_option("pretty")
866 .map(|s| s.parse())
867 .transpose()?
868 .unwrap_or_default())
869 }
870
871 pub fn date(&self) -> String {
874 self.get_option("date")
875 .unwrap_or_else(|| "%Y%m%d".to_string())
876 }
877
878 pub fn gitexport(&self) -> Result<GitExport, ()> {
880 Ok(self
881 .get_option("gitexport")
882 .map(|s| s.parse())
883 .transpose()?
884 .unwrap_or_default())
885 }
886
887 pub fn gitmode(&self) -> Result<GitMode, ()> {
889 Ok(self
890 .get_option("gitmode")
891 .map(|s| s.parse())
892 .transpose()?
893 .unwrap_or_default())
894 }
895
896 pub fn pgpmode(&self) -> Result<PgpMode, ()> {
898 Ok(self
899 .get_option("pgpmode")
900 .map(|s| s.parse())
901 .transpose()?
902 .unwrap_or_default())
903 }
904
905 pub fn searchmode(&self) -> Result<SearchMode, ()> {
907 Ok(self
908 .get_option("searchmode")
909 .map(|s| s.parse())
910 .transpose()?
911 .unwrap_or_default())
912 }
913
914 pub fn decompress(&self) -> bool {
916 self.has_option("decompress")
917 }
918
919 pub fn bare(&self) -> bool {
922 self.has_option("bare")
923 }
924
925 pub fn user_agent(&self) -> Option<String> {
927 self.get_option("user-agent")
928 }
929
930 pub fn passive(&self) -> Option<bool> {
932 if self.has_option("passive") || self.has_option("pasv") {
933 Some(true)
934 } else if self.has_option("active") || self.has_option("nopasv") {
935 Some(false)
936 } else {
937 None
938 }
939 }
940
941 pub fn unzipoptions(&self) -> Option<String> {
944 self.get_option("unzipopt")
945 }
946
947 pub fn dversionmangle(&self) -> Option<String> {
949 self.get_option("dversionmangle")
950 .or_else(|| self.get_option("versionmangle"))
951 }
952
953 pub fn dirversionmangle(&self) -> Option<String> {
957 self.get_option("dirversionmangle")
958 }
959
960 pub fn pagemangle(&self) -> Option<String> {
962 self.get_option("pagemangle")
963 }
964
965 pub fn uversionmangle(&self) -> Option<String> {
969 self.get_option("uversionmangle")
970 .or_else(|| self.get_option("versionmangle"))
971 }
972
973 pub fn versionmangle(&self) -> Option<String> {
975 self.get_option("versionmangle")
976 }
977
978 pub fn hrefdecode(&self) -> bool {
983 self.get_option("hrefdecode").is_some()
984 }
985
986 pub fn downloadurlmangle(&self) -> Option<String> {
989 self.get_option("downloadurlmangle")
990 }
991
992 pub fn filenamemangle(&self) -> Option<String> {
1000 self.get_option("filenamemangle")
1001 }
1002
1003 pub fn pgpsigurlmangle(&self) -> Option<String> {
1005 self.get_option("pgpsigurlmangle")
1006 }
1007
1008 pub fn oversionmangle(&self) -> Option<String> {
1011 self.get_option("oversionmangle")
1012 }
1013
1014 pub fn apply_uversionmangle(
1027 &self,
1028 version: &str,
1029 ) -> Result<String, crate::mangle::MangleError> {
1030 if let Some(vm) = self.uversionmangle() {
1031 crate::mangle::apply_mangle(&vm, version)
1032 } else {
1033 Ok(version.to_string())
1034 }
1035 }
1036
1037 pub fn apply_dversionmangle(
1050 &self,
1051 version: &str,
1052 ) -> Result<String, crate::mangle::MangleError> {
1053 if let Some(vm) = self.dversionmangle() {
1054 crate::mangle::apply_mangle(&vm, version)
1055 } else {
1056 Ok(version.to_string())
1057 }
1058 }
1059
1060 pub fn apply_oversionmangle(
1073 &self,
1074 version: &str,
1075 ) -> Result<String, crate::mangle::MangleError> {
1076 if let Some(vm) = self.oversionmangle() {
1077 crate::mangle::apply_mangle(&vm, version)
1078 } else {
1079 Ok(version.to_string())
1080 }
1081 }
1082
1083 pub fn apply_dirversionmangle(
1096 &self,
1097 version: &str,
1098 ) -> Result<String, crate::mangle::MangleError> {
1099 if let Some(vm) = self.dirversionmangle() {
1100 crate::mangle::apply_mangle(&vm, version)
1101 } else {
1102 Ok(version.to_string())
1103 }
1104 }
1105
1106 pub fn apply_filenamemangle(&self, url: &str) -> Result<String, crate::mangle::MangleError> {
1122 if let Some(vm) = self.filenamemangle() {
1123 crate::mangle::apply_mangle(&vm, url)
1124 } else {
1125 Ok(url.to_string())
1126 }
1127 }
1128
1129 pub fn apply_pagemangle(&self, page: &[u8]) -> Result<Vec<u8>, crate::mangle::MangleError> {
1145 if let Some(vm) = self.pagemangle() {
1146 let page_str = String::from_utf8_lossy(page);
1147 let mangled = crate::mangle::apply_mangle(&vm, &page_str)?;
1148 Ok(mangled.into_bytes())
1149 } else {
1150 Ok(page.to_vec())
1151 }
1152 }
1153
1154 pub fn apply_downloadurlmangle(&self, url: &str) -> Result<String, crate::mangle::MangleError> {
1170 if let Some(vm) = self.downloadurlmangle() {
1171 crate::mangle::apply_mangle(&vm, url)
1172 } else {
1173 Ok(url.to_string())
1174 }
1175 }
1176
1177 #[cfg(feature = "discover")]
1198 pub async fn discover(
1199 &self,
1200 package: impl FnOnce() -> String,
1201 ) -> Result<Vec<crate::Release>, Box<dyn std::error::Error>> {
1202 let url = self.format_url(package);
1203 let user_agent = self
1204 .user_agent()
1205 .unwrap_or_else(|| crate::DEFAULT_USER_AGENT.to_string());
1206 let searchmode = self.searchmode().unwrap_or(crate::SearchMode::Html);
1207
1208 let client = reqwest::Client::builder().user_agent(user_agent).build()?;
1209
1210 let response = client.get(url.as_str()).send().await?;
1211 let body = response.bytes().await?;
1212
1213 let mangled_body = self.apply_pagemangle(&body)?;
1215
1216 let matching_pattern = self
1217 .matching_pattern()
1218 .ok_or("matching_pattern is required")?;
1219
1220 let package_name = String::new(); let results = crate::search::search(
1222 match searchmode {
1223 crate::SearchMode::Html => "html",
1224 crate::SearchMode::Plain => "plain",
1225 },
1226 std::io::Cursor::new(mangled_body.as_ref() as &[u8]),
1227 &subst(&matching_pattern, || package_name.clone()),
1228 &package_name,
1229 url.as_str(),
1230 )?;
1231
1232 let mut releases = Vec::new();
1233 for (version, full_url) in results {
1234 let mangled_version = self.apply_uversionmangle(&version)?;
1236
1237 let mangled_url = self.apply_downloadurlmangle(&full_url)?;
1239
1240 let pgpsigurl = if let Some(mangle) = self.pgpsigurlmangle() {
1242 Some(crate::mangle::apply_mangle(&mangle, &mangled_url)?)
1243 } else {
1244 None
1245 };
1246
1247 let target_filename = if self.filenamemangle().is_some() {
1249 Some(self.apply_filenamemangle(&mangled_url)?)
1250 } else {
1251 None
1252 };
1253
1254 let package_version = if self.oversionmangle().is_some() {
1256 Some(self.apply_oversionmangle(&mangled_version)?)
1257 } else {
1258 None
1259 };
1260
1261 releases.push(crate::Release::new_full(
1262 mangled_version,
1263 mangled_url,
1264 pgpsigurl,
1265 target_filename,
1266 package_version,
1267 ));
1268 }
1269
1270 Ok(releases)
1271 }
1272
1273 #[cfg(all(feature = "discover", feature = "blocking"))]
1292 pub fn discover_blocking(
1293 &self,
1294 package: impl FnOnce() -> String,
1295 ) -> Result<Vec<crate::Release>, Box<dyn std::error::Error>> {
1296 let url = self.format_url(package);
1297 let user_agent = self
1298 .user_agent()
1299 .unwrap_or_else(|| crate::DEFAULT_USER_AGENT.to_string());
1300 let searchmode = self.searchmode().unwrap_or(crate::SearchMode::Html);
1301
1302 let client = reqwest::blocking::Client::builder()
1303 .user_agent(user_agent)
1304 .build()?;
1305
1306 let response = client.get(url.as_str()).send()?;
1307 let body = response.bytes()?;
1308
1309 let mangled_body = self.apply_pagemangle(&body)?;
1311
1312 let matching_pattern = self
1313 .matching_pattern()
1314 .ok_or("matching_pattern is required")?;
1315
1316 let package_name = String::new(); let results = crate::search::search(
1318 match searchmode {
1319 crate::SearchMode::Html => "html",
1320 crate::SearchMode::Plain => "plain",
1321 },
1322 std::io::Cursor::new(mangled_body.as_ref() as &[u8]),
1323 &subst(&matching_pattern, || package_name.clone()),
1324 &package_name,
1325 url.as_str(),
1326 )?;
1327
1328 let mut releases = Vec::new();
1329 for (version, full_url) in results {
1330 let mangled_version = self.apply_uversionmangle(&version)?;
1332
1333 let mangled_url = self.apply_downloadurlmangle(&full_url)?;
1335
1336 let pgpsigurl = if let Some(mangle) = self.pgpsigurlmangle() {
1338 Some(crate::mangle::apply_mangle(&mangle, &mangled_url)?)
1339 } else {
1340 None
1341 };
1342
1343 let target_filename = if self.filenamemangle().is_some() {
1345 Some(self.apply_filenamemangle(&mangled_url)?)
1346 } else {
1347 None
1348 };
1349
1350 let package_version = if self.oversionmangle().is_some() {
1352 Some(self.apply_oversionmangle(&mangled_version)?)
1353 } else {
1354 None
1355 };
1356
1357 releases.push(crate::Release::new_full(
1358 mangled_version,
1359 mangled_url,
1360 pgpsigurl,
1361 target_filename,
1362 package_version,
1363 ));
1364 }
1365
1366 Ok(releases)
1367 }
1368
1369 pub fn opts(&self) -> std::collections::HashMap<String, String> {
1371 let mut options = std::collections::HashMap::new();
1372
1373 if let Some(ol) = self.option_list() {
1374 for opt in ol.options() {
1375 let key = opt.key();
1376 let value = opt.value();
1377 if let (Some(key), Some(value)) = (key, value) {
1378 options.insert(key.to_string(), value.to_string());
1379 }
1380 }
1381 }
1382
1383 options
1384 }
1385
1386 fn items(&self) -> impl Iterator<Item = String> + '_ {
1387 self.0.children_with_tokens().filter_map(|it| match it {
1388 SyntaxElement::Token(token) => {
1389 if token.kind() == VALUE || token.kind() == KEY {
1390 Some(token.text().to_string())
1391 } else {
1392 None
1393 }
1394 }
1395 SyntaxElement::Node(node) => {
1396 match node.kind() {
1398 URL => Url::cast(node).map(|n| n.url()),
1399 MATCHING_PATTERN => MatchingPattern::cast(node).map(|n| n.pattern()),
1400 VERSION_POLICY => VersionPolicyNode::cast(node).map(|n| n.policy()),
1401 SCRIPT => ScriptNode::cast(node).map(|n| n.script()),
1402 _ => None,
1403 }
1404 }
1405 })
1406 }
1407
1408 pub fn url_node(&self) -> Option<Url> {
1410 self.0.children().find_map(Url::cast)
1411 }
1412
1413 pub fn url(&self) -> String {
1415 self.url_node().map(|it| it.url()).unwrap_or_else(|| {
1416 self.items().next().unwrap()
1418 })
1419 }
1420
1421 pub fn matching_pattern_node(&self) -> Option<MatchingPattern> {
1423 self.0.children().find_map(MatchingPattern::cast)
1424 }
1425
1426 pub fn matching_pattern(&self) -> Option<String> {
1428 self.matching_pattern_node()
1429 .map(|it| it.pattern())
1430 .or_else(|| {
1431 self.items().nth(1)
1433 })
1434 }
1435
1436 pub fn version_node(&self) -> Option<VersionPolicyNode> {
1438 self.0.children().find_map(VersionPolicyNode::cast)
1439 }
1440
1441 pub fn version(&self) -> Result<Option<crate::VersionPolicy>, String> {
1443 self.version_node()
1444 .map(|it| it.policy().parse())
1445 .transpose()
1446 .or_else(|_e| {
1447 self.items().nth(2).map(|it| it.parse()).transpose()
1449 })
1450 }
1451
1452 pub fn script_node(&self) -> Option<ScriptNode> {
1454 self.0.children().find_map(ScriptNode::cast)
1455 }
1456
1457 pub fn script(&self) -> Option<String> {
1459 self.script_node().map(|it| it.script()).or_else(|| {
1460 self.items().nth(3)
1462 })
1463 }
1464
1465 pub fn format_url(&self, package: impl FnOnce() -> String) -> url::Url {
1467 subst(self.url().as_str(), package).parse().unwrap()
1468 }
1469
1470 pub fn set_url(&mut self, new_url: &str) {
1472 let mut builder = GreenNodeBuilder::new();
1474 builder.start_node(URL.into());
1475 builder.token(VALUE.into(), new_url);
1476 builder.finish_node();
1477 let new_url_green = builder.finish();
1478
1479 let new_url_node = SyntaxNode::new_root_mut(new_url_green);
1481
1482 let url_pos = self
1484 .0
1485 .children_with_tokens()
1486 .position(|child| matches!(child, SyntaxElement::Node(node) if node.kind() == URL));
1487
1488 if let Some(pos) = url_pos {
1489 self.0
1491 .splice_children(pos..pos + 1, vec![new_url_node.into()]);
1492 }
1493 }
1494
1495 pub fn set_matching_pattern(&mut self, new_pattern: &str) {
1501 let mut builder = GreenNodeBuilder::new();
1503 builder.start_node(MATCHING_PATTERN.into());
1504 builder.token(VALUE.into(), new_pattern);
1505 builder.finish_node();
1506 let new_pattern_green = builder.finish();
1507
1508 let new_pattern_node = SyntaxNode::new_root_mut(new_pattern_green);
1510
1511 let pattern_pos = self.0.children_with_tokens().position(
1513 |child| matches!(child, SyntaxElement::Node(node) if node.kind() == MATCHING_PATTERN),
1514 );
1515
1516 if let Some(pos) = pattern_pos {
1517 self.0
1519 .splice_children(pos..pos + 1, vec![new_pattern_node.into()]);
1520 }
1521 }
1523
1524 pub fn set_version_policy(&mut self, new_policy: &str) {
1530 let mut builder = GreenNodeBuilder::new();
1532 builder.start_node(VERSION_POLICY.into());
1533 builder.token(VALUE.into(), new_policy);
1535 builder.finish_node();
1536 let new_policy_green = builder.finish();
1537
1538 let new_policy_node = SyntaxNode::new_root_mut(new_policy_green);
1540
1541 let policy_pos = self.0.children_with_tokens().position(
1543 |child| matches!(child, SyntaxElement::Node(node) if node.kind() == VERSION_POLICY),
1544 );
1545
1546 if let Some(pos) = policy_pos {
1547 self.0
1549 .splice_children(pos..pos + 1, vec![new_policy_node.into()]);
1550 }
1551 }
1553
1554 pub fn set_script(&mut self, new_script: &str) {
1560 let mut builder = GreenNodeBuilder::new();
1562 builder.start_node(SCRIPT.into());
1563 builder.token(VALUE.into(), new_script);
1565 builder.finish_node();
1566 let new_script_green = builder.finish();
1567
1568 let new_script_node = SyntaxNode::new_root_mut(new_script_green);
1570
1571 let script_pos = self
1573 .0
1574 .children_with_tokens()
1575 .position(|child| matches!(child, SyntaxElement::Node(node) if node.kind() == SCRIPT));
1576
1577 if let Some(pos) = script_pos {
1578 self.0
1580 .splice_children(pos..pos + 1, vec![new_script_node.into()]);
1581 }
1582 }
1584
1585 pub fn set_opt(&mut self, key: &str, value: &str) {
1591 let opts_pos = self.0.children_with_tokens().position(
1593 |child| matches!(child, SyntaxElement::Node(node) if node.kind() == OPTS_LIST),
1594 );
1595
1596 if let Some(_opts_idx) = opts_pos {
1597 if let Some(mut ol) = self.option_list() {
1598 if let Some(mut opt) = ol.find_option(key) {
1600 opt.set_value(value);
1602 } else {
1604 ol.add_option(key, value);
1606 }
1608 }
1609 } else {
1610 let mut builder = GreenNodeBuilder::new();
1612 builder.start_node(OPTS_LIST.into());
1613 builder.token(KEY.into(), "opts");
1614 builder.token(EQUALS.into(), "=");
1615 builder.start_node(OPTION.into());
1616 builder.token(KEY.into(), key);
1617 builder.token(EQUALS.into(), "=");
1618 builder.token(VALUE.into(), value);
1619 builder.finish_node();
1620 builder.finish_node();
1621 let new_opts_green = builder.finish();
1622 let new_opts_node = SyntaxNode::new_root_mut(new_opts_green);
1623
1624 let url_pos = self
1626 .0
1627 .children_with_tokens()
1628 .position(|child| matches!(child, SyntaxElement::Node(node) if node.kind() == URL));
1629
1630 if let Some(url_idx) = url_pos {
1631 let mut combined_builder = GreenNodeBuilder::new();
1634 combined_builder.start_node(ROOT.into()); combined_builder.token(WHITESPACE.into(), " ");
1636 combined_builder.finish_node();
1637 let temp_green = combined_builder.finish();
1638 let temp_root = SyntaxNode::new_root_mut(temp_green);
1639 let space_element = temp_root.children_with_tokens().next().unwrap();
1640
1641 self.0
1642 .splice_children(url_idx..url_idx, vec![new_opts_node.into(), space_element]);
1643 } else {
1644 self.0.splice_children(0..0, vec![new_opts_node.into()]);
1645 }
1646 }
1647 }
1648
1649 pub fn del_opt(&mut self, key: &str) {
1656 if let Some(mut ol) = self.option_list() {
1657 let option_count = ol.0.children().filter(|n| n.kind() == OPTION).count();
1658
1659 if option_count == 1 && ol.has_option(key) {
1660 let opts_pos = self.0.children().position(|node| node.kind() == OPTS_LIST);
1662
1663 if let Some(opts_idx) = opts_pos {
1664 self.0.splice_children(opts_idx..opts_idx + 1, vec![]);
1666
1667 while self.0.children_with_tokens().next().map_or(false, |e| {
1669 matches!(
1670 e,
1671 SyntaxElement::Token(t) if t.kind() == WHITESPACE || t.kind() == CONTINUATION
1672 )
1673 }) {
1674 self.0.splice_children(0..1, vec![]);
1675 }
1676 }
1677 } else {
1678 ol.remove_option(key);
1680 }
1681 }
1682 }
1683}
1684
1685const SUBSTITUTIONS: &[(&str, &str)] = &[
1686 ("@ANY_VERSION@", r"[-_]?(\d[\-+\.:\~\da-zA-Z]*)"),
1691 (
1694 "@ARCHIVE_EXT@",
1695 r"(?i)\.(?:tar\.xz|tar\.bz2|tar\.gz|zip|tgz|tbz|txz)",
1696 ),
1697 (
1700 "@SIGNATURE_EXT@",
1701 r"(?i)\.(?:tar\.xz|tar\.bz2|tar\.gz|zip|tgz|tbz|txz)\.(?:asc|pgp|gpg|sig|sign)",
1702 ),
1703 ("@DEB_EXT@", r"[\+~](debian|dfsg|ds|deb)(\.)?(\d+)?$"),
1705];
1706
1707pub fn subst(text: &str, package: impl FnOnce() -> String) -> String {
1708 let mut substs = SUBSTITUTIONS.to_vec();
1709 let package_name;
1710 if text.contains("@PACKAGE@") {
1711 package_name = Some(package());
1712 substs.push(("@PACKAGE@", package_name.as_deref().unwrap()));
1713 }
1714
1715 let mut text = text.to_string();
1716
1717 for (k, v) in substs {
1718 text = text.replace(k, v);
1719 }
1720
1721 text
1722}
1723
1724#[test]
1725fn test_subst() {
1726 assert_eq!(
1727 subst("@ANY_VERSION@", || unreachable!()),
1728 r"[-_]?(\d[\-+\.:\~\da-zA-Z]*)"
1729 );
1730 assert_eq!(subst("@PACKAGE@", || "dulwich".to_string()), "dulwich");
1731}
1732
1733impl std::fmt::Debug for OptionList {
1734 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1735 f.debug_struct("OptionList")
1736 .field("text", &self.0.text().to_string())
1737 .finish()
1738 }
1739}
1740
1741impl OptionList {
1742 pub fn options(&self) -> impl Iterator<Item = _Option> + '_ {
1744 self.0.children().filter_map(_Option::cast)
1745 }
1746
1747 pub fn find_option(&self, key: &str) -> Option<_Option> {
1749 self.options().find(|opt| opt.key().as_deref() == Some(key))
1750 }
1751
1752 pub fn has_option(&self, key: &str) -> bool {
1753 self.options().any(|it| it.key().as_deref() == Some(key))
1754 }
1755
1756 pub fn get_option(&self, key: &str) -> Option<String> {
1757 for child in self.options() {
1758 if child.key().as_deref() == Some(key) {
1759 return child.value();
1760 }
1761 }
1762 None
1763 }
1764
1765 fn add_option(&mut self, key: &str, value: &str) {
1767 let option_count = self.0.children().filter(|n| n.kind() == OPTION).count();
1768
1769 let mut builder = GreenNodeBuilder::new();
1771 builder.start_node(ROOT.into()); if option_count > 0 {
1774 builder.start_node(OPTION_SEPARATOR.into());
1775 builder.token(COMMA.into(), ",");
1776 builder.finish_node();
1777 }
1778
1779 builder.start_node(OPTION.into());
1780 builder.token(KEY.into(), key);
1781 builder.token(EQUALS.into(), "=");
1782 builder.token(VALUE.into(), value);
1783 builder.finish_node();
1784
1785 builder.finish_node(); let combined_green = builder.finish();
1787
1788 let temp_root = SyntaxNode::new_root_mut(combined_green);
1790 let new_children: Vec<_> = temp_root.children_with_tokens().collect();
1791
1792 let insert_pos = self.0.children_with_tokens().count();
1793 self.0.splice_children(insert_pos..insert_pos, new_children);
1794 }
1795
1796 fn remove_option(&mut self, key: &str) -> bool {
1798 if let Some(mut opt) = self.find_option(key) {
1799 opt.remove();
1800 true
1801 } else {
1802 false
1803 }
1804 }
1805}
1806
1807impl _Option {
1808 pub fn key(&self) -> Option<String> {
1810 self.0.children_with_tokens().find_map(|it| match it {
1811 SyntaxElement::Token(token) => {
1812 if token.kind() == KEY {
1813 Some(token.text().to_string())
1814 } else {
1815 None
1816 }
1817 }
1818 _ => None,
1819 })
1820 }
1821
1822 pub fn value(&self) -> Option<String> {
1824 self.0
1825 .children_with_tokens()
1826 .filter_map(|it| match it {
1827 SyntaxElement::Token(token) => {
1828 if token.kind() == VALUE || token.kind() == KEY {
1829 Some(token.text().to_string())
1830 } else {
1831 None
1832 }
1833 }
1834 _ => None,
1835 })
1836 .nth(1)
1837 }
1838
1839 pub fn set_value(&mut self, new_value: &str) {
1841 let key = self.key().expect("Option must have a key");
1842
1843 let mut builder = GreenNodeBuilder::new();
1845 builder.start_node(OPTION.into());
1846 builder.token(KEY.into(), &key);
1847 builder.token(EQUALS.into(), "=");
1848 builder.token(VALUE.into(), new_value);
1849 builder.finish_node();
1850 let new_option_green = builder.finish();
1851 let new_option_node = SyntaxNode::new_root_mut(new_option_green);
1852
1853 if let Some(parent) = self.0.parent() {
1855 let idx = self.0.index();
1856 parent.splice_children(idx..idx + 1, vec![new_option_node.into()]);
1857 }
1858 }
1859
1860 pub fn remove(&mut self) {
1862 let next_sep = self
1864 .0
1865 .next_sibling()
1866 .filter(|n| n.kind() == OPTION_SEPARATOR);
1867 let prev_sep = self
1868 .0
1869 .prev_sibling()
1870 .filter(|n| n.kind() == OPTION_SEPARATOR);
1871
1872 if let Some(sep) = next_sep {
1874 sep.detach();
1875 } else if let Some(sep) = prev_sep {
1876 sep.detach();
1877 }
1878
1879 self.0.detach();
1881 }
1882}
1883
1884impl Url {
1885 pub fn url(&self) -> String {
1887 self.0
1888 .children_with_tokens()
1889 .find_map(|it| match it {
1890 SyntaxElement::Token(token) => {
1891 if token.kind() == VALUE {
1892 Some(token.text().to_string())
1893 } else {
1894 None
1895 }
1896 }
1897 _ => None,
1898 })
1899 .unwrap()
1900 }
1901}
1902
1903impl MatchingPattern {
1904 pub fn pattern(&self) -> String {
1906 self.0
1907 .children_with_tokens()
1908 .find_map(|it| match it {
1909 SyntaxElement::Token(token) => {
1910 if token.kind() == VALUE {
1911 Some(token.text().to_string())
1912 } else {
1913 None
1914 }
1915 }
1916 _ => None,
1917 })
1918 .unwrap()
1919 }
1920}
1921
1922impl VersionPolicyNode {
1923 pub fn policy(&self) -> String {
1925 self.0
1926 .children_with_tokens()
1927 .find_map(|it| match it {
1928 SyntaxElement::Token(token) => {
1929 if token.kind() == VALUE || token.kind() == KEY {
1931 Some(token.text().to_string())
1932 } else {
1933 None
1934 }
1935 }
1936 _ => None,
1937 })
1938 .unwrap()
1939 }
1940}
1941
1942impl ScriptNode {
1943 pub fn script(&self) -> String {
1945 self.0
1946 .children_with_tokens()
1947 .find_map(|it| match it {
1948 SyntaxElement::Token(token) => {
1949 if token.kind() == VALUE || token.kind() == KEY {
1951 Some(token.text().to_string())
1952 } else {
1953 None
1954 }
1955 }
1956 _ => None,
1957 })
1958 .unwrap()
1959 }
1960}
1961
1962#[test]
1963fn test_entry_node_structure() {
1964 let wf: super::WatchFile = r#"version=4
1966opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
1967"#
1968 .parse()
1969 .unwrap();
1970
1971 let entry = wf.entries().next().unwrap();
1972
1973 assert_eq!(entry.0.children().find(|n| n.kind() == URL).is_some(), true);
1975 assert_eq!(entry.url(), "https://example.com/releases");
1976
1977 assert_eq!(
1979 entry
1980 .0
1981 .children()
1982 .find(|n| n.kind() == MATCHING_PATTERN)
1983 .is_some(),
1984 true
1985 );
1986 assert_eq!(
1987 entry.matching_pattern(),
1988 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
1989 );
1990
1991 assert_eq!(
1993 entry
1994 .0
1995 .children()
1996 .find(|n| n.kind() == VERSION_POLICY)
1997 .is_some(),
1998 true
1999 );
2000 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2001
2002 assert_eq!(
2004 entry.0.children().find(|n| n.kind() == SCRIPT).is_some(),
2005 true
2006 );
2007 assert_eq!(entry.script(), Some("uupdate".into()));
2008}
2009
2010#[test]
2011fn test_entry_node_structure_partial() {
2012 let wf: super::WatchFile = r#"version=4
2014https://github.com/example/tags .*/v?(\d\S+)\.tar\.gz
2015"#
2016 .parse()
2017 .unwrap();
2018
2019 let entry = wf.entries().next().unwrap();
2020
2021 assert_eq!(entry.0.children().find(|n| n.kind() == URL).is_some(), true);
2023 assert_eq!(
2024 entry
2025 .0
2026 .children()
2027 .find(|n| n.kind() == MATCHING_PATTERN)
2028 .is_some(),
2029 true
2030 );
2031
2032 assert_eq!(
2034 entry
2035 .0
2036 .children()
2037 .find(|n| n.kind() == VERSION_POLICY)
2038 .is_some(),
2039 false
2040 );
2041 assert_eq!(
2042 entry.0.children().find(|n| n.kind() == SCRIPT).is_some(),
2043 false
2044 );
2045
2046 assert_eq!(entry.url(), "https://github.com/example/tags");
2048 assert_eq!(
2049 entry.matching_pattern(),
2050 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2051 );
2052 assert_eq!(entry.version(), Ok(None));
2053 assert_eq!(entry.script(), None);
2054}
2055
2056#[test]
2057fn test_parse_v1() {
2058 const WATCHV1: &str = r#"version=4
2059opts=bare,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1\.tar\.gz/ \
2060 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2061"#;
2062 let parsed = parse(WATCHV1);
2063 let node = parsed.syntax();
2065 assert_eq!(
2066 format!("{:#?}", node),
2067 r#"ROOT@0..161
2068 VERSION@0..10
2069 KEY@0..7 "version"
2070 EQUALS@7..8 "="
2071 VALUE@8..9 "4"
2072 NEWLINE@9..10 "\n"
2073 ENTRY@10..161
2074 OPTS_LIST@10..86
2075 KEY@10..14 "opts"
2076 EQUALS@14..15 "="
2077 OPTION@15..19
2078 KEY@15..19 "bare"
2079 OPTION_SEPARATOR@19..20
2080 COMMA@19..20 ","
2081 OPTION@20..86
2082 KEY@20..34 "filenamemangle"
2083 EQUALS@34..35 "="
2084 VALUE@35..86 "s/.+\\/v?(\\d\\S+)\\.tar\\ ..."
2085 WHITESPACE@86..87 " "
2086 CONTINUATION@87..89 "\\\n"
2087 WHITESPACE@89..91 " "
2088 URL@91..138
2089 VALUE@91..138 "https://github.com/sy ..."
2090 WHITESPACE@138..139 " "
2091 MATCHING_PATTERN@139..160
2092 VALUE@139..160 ".*/v?(\\d\\S+)\\.tar\\.gz"
2093 NEWLINE@160..161 "\n"
2094"#
2095 );
2096
2097 let root = parsed.root();
2098 assert_eq!(root.version(), 4);
2099 let entries = root.entries().collect::<Vec<_>>();
2100 assert_eq!(entries.len(), 1);
2101 let entry = &entries[0];
2102 assert_eq!(
2103 entry.url(),
2104 "https://github.com/syncthing/syncthing-gtk/tags"
2105 );
2106 assert_eq!(
2107 entry.matching_pattern(),
2108 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2109 );
2110 assert_eq!(entry.version(), Ok(None));
2111 assert_eq!(entry.script(), None);
2112
2113 assert_eq!(node.text(), WATCHV1);
2114}
2115
2116#[test]
2117fn test_parse_v2() {
2118 let parsed = parse(
2119 r#"version=4
2120https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2121# comment
2122"#,
2123 );
2124 assert_eq!(parsed.errors, Vec::<String>::new());
2125 let node = parsed.syntax();
2126 assert_eq!(
2127 format!("{:#?}", node),
2128 r###"ROOT@0..90
2129 VERSION@0..10
2130 KEY@0..7 "version"
2131 EQUALS@7..8 "="
2132 VALUE@8..9 "4"
2133 NEWLINE@9..10 "\n"
2134 ENTRY@10..80
2135 URL@10..57
2136 VALUE@10..57 "https://github.com/sy ..."
2137 WHITESPACE@57..58 " "
2138 MATCHING_PATTERN@58..79
2139 VALUE@58..79 ".*/v?(\\d\\S+)\\.tar\\.gz"
2140 NEWLINE@79..80 "\n"
2141 COMMENT@80..89 "# comment"
2142 NEWLINE@89..90 "\n"
2143"###
2144 );
2145
2146 let root = parsed.root();
2147 assert_eq!(root.version(), 4);
2148 let entries = root.entries().collect::<Vec<_>>();
2149 assert_eq!(entries.len(), 1);
2150 let entry = &entries[0];
2151 assert_eq!(
2152 entry.url(),
2153 "https://github.com/syncthing/syncthing-gtk/tags"
2154 );
2155 assert_eq!(
2156 entry.format_url(|| "syncthing-gtk".to_string()),
2157 "https://github.com/syncthing/syncthing-gtk/tags"
2158 .parse()
2159 .unwrap()
2160 );
2161}
2162
2163#[test]
2164fn test_parse_v3() {
2165 let parsed = parse(
2166 r#"version=4
2167https://github.com/syncthing/@PACKAGE@/tags .*/v?(\d\S+)\.tar\.gz
2168# comment
2169"#,
2170 );
2171 assert_eq!(parsed.errors, Vec::<String>::new());
2172 let root = parsed.root();
2173 assert_eq!(root.version(), 4);
2174 let entries = root.entries().collect::<Vec<_>>();
2175 assert_eq!(entries.len(), 1);
2176 let entry = &entries[0];
2177 assert_eq!(entry.url(), "https://github.com/syncthing/@PACKAGE@/tags");
2178 assert_eq!(
2179 entry.format_url(|| "syncthing-gtk".to_string()),
2180 "https://github.com/syncthing/syncthing-gtk/tags"
2181 .parse()
2182 .unwrap()
2183 );
2184}
2185
2186#[test]
2187fn test_parse_v4() {
2188 let cl: super::WatchFile = r#"version=4
2189opts=repack,compression=xz,dversionmangle=s/\+ds//,repacksuffix=+ds \
2190 https://github.com/example/example-cat/tags \
2191 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2192"#
2193 .parse()
2194 .unwrap();
2195 assert_eq!(cl.version(), 4);
2196 let entries = cl.entries().collect::<Vec<_>>();
2197 assert_eq!(entries.len(), 1);
2198 let entry = &entries[0];
2199 assert_eq!(entry.url(), "https://github.com/example/example-cat/tags");
2200 assert_eq!(
2201 entry.matching_pattern(),
2202 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2203 );
2204 assert!(entry.repack());
2205 assert_eq!(entry.compression(), Ok(Some(Compression::Xz)));
2206 assert_eq!(entry.dversionmangle(), Some("s/\\+ds//".into()));
2207 assert_eq!(entry.repacksuffix(), Some("+ds".into()));
2208 assert_eq!(entry.script(), Some("uupdate".into()));
2209 assert_eq!(
2210 entry.format_url(|| "example-cat".to_string()),
2211 "https://github.com/example/example-cat/tags"
2212 .parse()
2213 .unwrap()
2214 );
2215 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
2216}
2217
2218#[test]
2219fn test_git_mode() {
2220 let text = r#"version=3
2221opts="mode=git, gitmode=shallow, pgpmode=gittag" \
2222https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git \
2223refs/tags/(.*) debian
2224"#;
2225 let parsed = parse(text);
2226 assert_eq!(parsed.errors, Vec::<String>::new());
2227 let cl = parsed.root();
2228 assert_eq!(cl.version(), 3);
2229 let entries = cl.entries().collect::<Vec<_>>();
2230 assert_eq!(entries.len(), 1);
2231 let entry = &entries[0];
2232 assert_eq!(
2233 entry.url(),
2234 "https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git"
2235 );
2236 assert_eq!(entry.matching_pattern(), Some("refs/tags/(.*)".into()));
2237 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
2238 assert_eq!(entry.script(), None);
2239 assert_eq!(entry.gitmode(), Ok(GitMode::Shallow));
2240 assert_eq!(entry.pgpmode(), Ok(PgpMode::GitTag));
2241 assert_eq!(entry.mode(), Ok(Mode::Git));
2242}
2243
2244#[test]
2245fn test_parse_quoted() {
2246 const WATCHV1: &str = r#"version=4
2247opts="bare, filenamemangle=blah" \
2248 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2249"#;
2250 let parsed = parse(WATCHV1);
2251 let node = parsed.syntax();
2253
2254 let root = parsed.root();
2255 assert_eq!(root.version(), 4);
2256 let entries = root.entries().collect::<Vec<_>>();
2257 assert_eq!(entries.len(), 1);
2258 let entry = &entries[0];
2259
2260 assert_eq!(
2261 entry.url(),
2262 "https://github.com/syncthing/syncthing-gtk/tags"
2263 );
2264 assert_eq!(
2265 entry.matching_pattern(),
2266 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2267 );
2268 assert_eq!(entry.version(), Ok(None));
2269 assert_eq!(entry.script(), None);
2270
2271 assert_eq!(node.text(), WATCHV1);
2272}
2273
2274#[test]
2275fn test_set_url() {
2276 let wf: super::WatchFile = r#"version=4
2278https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2279"#
2280 .parse()
2281 .unwrap();
2282
2283 let mut entry = wf.entries().next().unwrap();
2284 assert_eq!(
2285 entry.url(),
2286 "https://github.com/syncthing/syncthing-gtk/tags"
2287 );
2288
2289 entry.set_url("https://newurl.example.org/path");
2290 assert_eq!(entry.url(), "https://newurl.example.org/path");
2291 assert_eq!(
2292 entry.matching_pattern(),
2293 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2294 );
2295
2296 assert_eq!(
2298 entry.to_string(),
2299 "https://newurl.example.org/path .*/v?(\\d\\S+)\\.tar\\.gz\n"
2300 );
2301}
2302
2303#[test]
2304fn test_set_url_with_options() {
2305 let wf: super::WatchFile = r#"version=4
2307opts=foo=blah https://foo.com/bar .*/v?(\d\S+)\.tar\.gz
2308"#
2309 .parse()
2310 .unwrap();
2311
2312 let mut entry = wf.entries().next().unwrap();
2313 assert_eq!(entry.url(), "https://foo.com/bar");
2314 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2315
2316 entry.set_url("https://example.com/baz");
2317 assert_eq!(entry.url(), "https://example.com/baz");
2318
2319 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2321 assert_eq!(
2322 entry.matching_pattern(),
2323 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2324 );
2325
2326 assert_eq!(
2328 entry.to_string(),
2329 "opts=foo=blah https://example.com/baz .*/v?(\\d\\S+)\\.tar\\.gz\n"
2330 );
2331}
2332
2333#[test]
2334fn test_set_url_complex() {
2335 let wf: super::WatchFile = r#"version=4
2337opts=bare,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1\.tar\.gz/ \
2338 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2339"#
2340 .parse()
2341 .unwrap();
2342
2343 let mut entry = wf.entries().next().unwrap();
2344 assert_eq!(
2345 entry.url(),
2346 "https://github.com/syncthing/syncthing-gtk/tags"
2347 );
2348
2349 entry.set_url("https://gitlab.com/newproject/tags");
2350 assert_eq!(entry.url(), "https://gitlab.com/newproject/tags");
2351
2352 assert!(entry.bare());
2354 assert_eq!(
2355 entry.filenamemangle(),
2356 Some("s/.+\\/v?(\\d\\S+)\\.tar\\.gz/syncthing-gtk-$1\\.tar\\.gz/".into())
2357 );
2358 assert_eq!(
2359 entry.matching_pattern(),
2360 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2361 );
2362
2363 assert_eq!(
2365 entry.to_string(),
2366 r#"opts=bare,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1\.tar\.gz/ \
2367 https://gitlab.com/newproject/tags .*/v?(\d\S+)\.tar\.gz
2368"#
2369 );
2370}
2371
2372#[test]
2373fn test_set_url_with_all_fields() {
2374 let wf: super::WatchFile = r#"version=4
2376opts=repack,compression=xz,dversionmangle=s/\+ds//,repacksuffix=+ds \
2377 https://github.com/example/example-cat/tags \
2378 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2379"#
2380 .parse()
2381 .unwrap();
2382
2383 let mut entry = wf.entries().next().unwrap();
2384 assert_eq!(entry.url(), "https://github.com/example/example-cat/tags");
2385 assert_eq!(
2386 entry.matching_pattern(),
2387 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2388 );
2389 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2390 assert_eq!(entry.script(), Some("uupdate".into()));
2391
2392 entry.set_url("https://gitlab.example.org/project/releases");
2393 assert_eq!(entry.url(), "https://gitlab.example.org/project/releases");
2394
2395 assert!(entry.repack());
2397 assert_eq!(entry.compression(), Ok(Some(super::Compression::Xz)));
2398 assert_eq!(entry.dversionmangle(), Some("s/\\+ds//".into()));
2399 assert_eq!(entry.repacksuffix(), Some("+ds".into()));
2400 assert_eq!(
2401 entry.matching_pattern(),
2402 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2403 );
2404 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2405 assert_eq!(entry.script(), Some("uupdate".into()));
2406
2407 assert_eq!(
2409 entry.to_string(),
2410 r#"opts=repack,compression=xz,dversionmangle=s/\+ds//,repacksuffix=+ds \
2411 https://gitlab.example.org/project/releases \
2412 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2413"#
2414 );
2415}
2416
2417#[test]
2418fn test_set_url_quoted_options() {
2419 let wf: super::WatchFile = r#"version=4
2421opts="bare, filenamemangle=blah" \
2422 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2423"#
2424 .parse()
2425 .unwrap();
2426
2427 let mut entry = wf.entries().next().unwrap();
2428 assert_eq!(
2429 entry.url(),
2430 "https://github.com/syncthing/syncthing-gtk/tags"
2431 );
2432
2433 entry.set_url("https://example.org/new/path");
2434 assert_eq!(entry.url(), "https://example.org/new/path");
2435
2436 assert_eq!(
2438 entry.to_string(),
2439 r#"opts="bare, filenamemangle=blah" \
2440 https://example.org/new/path .*/v?(\d\S+)\.tar\.gz
2441"#
2442 );
2443}
2444
2445#[test]
2446fn test_set_opt_update_existing() {
2447 let wf: super::WatchFile = r#"version=4
2449opts=foo=blah,bar=baz https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2450"#
2451 .parse()
2452 .unwrap();
2453
2454 let mut entry = wf.entries().next().unwrap();
2455 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2456 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2457
2458 entry.set_opt("foo", "updated");
2459 assert_eq!(entry.get_option("foo"), Some("updated".to_string()));
2460 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2461
2462 assert_eq!(
2464 entry.to_string(),
2465 "opts=foo=updated,bar=baz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2466 );
2467}
2468
2469#[test]
2470fn test_set_opt_add_new() {
2471 let wf: super::WatchFile = r#"version=4
2473opts=foo=blah https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2474"#
2475 .parse()
2476 .unwrap();
2477
2478 let mut entry = wf.entries().next().unwrap();
2479 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2480 assert_eq!(entry.get_option("bar"), None);
2481
2482 entry.set_opt("bar", "baz");
2483 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2484 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2485
2486 assert_eq!(
2488 entry.to_string(),
2489 "opts=foo=blah,bar=baz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2490 );
2491}
2492
2493#[test]
2494fn test_set_opt_create_options_list() {
2495 let wf: super::WatchFile = r#"version=4
2497https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2498"#
2499 .parse()
2500 .unwrap();
2501
2502 let mut entry = wf.entries().next().unwrap();
2503 assert_eq!(entry.option_list(), None);
2504
2505 entry.set_opt("compression", "xz");
2506 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
2507
2508 assert_eq!(
2510 entry.to_string(),
2511 "opts=compression=xz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2512 );
2513}
2514
2515#[test]
2516fn test_del_opt_remove_single() {
2517 let wf: super::WatchFile = r#"version=4
2519opts=foo=blah,bar=baz,qux=quux https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2520"#
2521 .parse()
2522 .unwrap();
2523
2524 let mut entry = wf.entries().next().unwrap();
2525 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2526 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2527 assert_eq!(entry.get_option("qux"), Some("quux".to_string()));
2528
2529 entry.del_opt("bar");
2530 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2531 assert_eq!(entry.get_option("bar"), None);
2532 assert_eq!(entry.get_option("qux"), Some("quux".to_string()));
2533
2534 assert_eq!(
2536 entry.to_string(),
2537 "opts=foo=blah,qux=quux https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2538 );
2539}
2540
2541#[test]
2542fn test_del_opt_remove_first() {
2543 let wf: super::WatchFile = r#"version=4
2545opts=foo=blah,bar=baz https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2546"#
2547 .parse()
2548 .unwrap();
2549
2550 let mut entry = wf.entries().next().unwrap();
2551 entry.del_opt("foo");
2552 assert_eq!(entry.get_option("foo"), None);
2553 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2554
2555 assert_eq!(
2557 entry.to_string(),
2558 "opts=bar=baz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2559 );
2560}
2561
2562#[test]
2563fn test_del_opt_remove_last() {
2564 let wf: super::WatchFile = r#"version=4
2566opts=foo=blah,bar=baz https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2567"#
2568 .parse()
2569 .unwrap();
2570
2571 let mut entry = wf.entries().next().unwrap();
2572 entry.del_opt("bar");
2573 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2574 assert_eq!(entry.get_option("bar"), None);
2575
2576 assert_eq!(
2578 entry.to_string(),
2579 "opts=foo=blah https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2580 );
2581}
2582
2583#[test]
2584fn test_del_opt_remove_only_option() {
2585 let wf: super::WatchFile = r#"version=4
2587opts=foo=blah https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2588"#
2589 .parse()
2590 .unwrap();
2591
2592 let mut entry = wf.entries().next().unwrap();
2593 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2594
2595 entry.del_opt("foo");
2596 assert_eq!(entry.get_option("foo"), None);
2597 assert_eq!(entry.option_list(), None);
2598
2599 assert_eq!(
2601 entry.to_string(),
2602 "https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2603 );
2604}
2605
2606#[test]
2607fn test_del_opt_nonexistent() {
2608 let wf: super::WatchFile = r#"version=4
2610opts=foo=blah https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2611"#
2612 .parse()
2613 .unwrap();
2614
2615 let mut entry = wf.entries().next().unwrap();
2616 let original = entry.to_string();
2617
2618 entry.del_opt("nonexistent");
2619 assert_eq!(entry.to_string(), original);
2620}
2621
2622#[test]
2623fn test_set_opt_multiple_operations() {
2624 let wf: super::WatchFile = r#"version=4
2626https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2627"#
2628 .parse()
2629 .unwrap();
2630
2631 let mut entry = wf.entries().next().unwrap();
2632
2633 entry.set_opt("compression", "xz");
2634 entry.set_opt("repack", "");
2635 entry.set_opt("dversionmangle", "s/\\+ds//");
2636
2637 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
2638 assert_eq!(
2639 entry.get_option("dversionmangle"),
2640 Some("s/\\+ds//".to_string())
2641 );
2642}
2643
2644#[test]
2645fn test_set_matching_pattern() {
2646 let wf: super::WatchFile = r#"version=4
2648https://github.com/example/tags .*/v?(\d\S+)\.tar\.gz
2649"#
2650 .parse()
2651 .unwrap();
2652
2653 let mut entry = wf.entries().next().unwrap();
2654 assert_eq!(
2655 entry.matching_pattern(),
2656 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2657 );
2658
2659 entry.set_matching_pattern("(?:.*?/)?v?([\\d.]+)\\.tar\\.gz");
2660 assert_eq!(
2661 entry.matching_pattern(),
2662 Some("(?:.*?/)?v?([\\d.]+)\\.tar\\.gz".into())
2663 );
2664
2665 assert_eq!(entry.url(), "https://github.com/example/tags");
2667
2668 assert_eq!(
2670 entry.to_string(),
2671 "https://github.com/example/tags (?:.*?/)?v?([\\d.]+)\\.tar\\.gz\n"
2672 );
2673}
2674
2675#[test]
2676fn test_set_matching_pattern_with_all_fields() {
2677 let wf: super::WatchFile = r#"version=4
2679opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2680"#
2681 .parse()
2682 .unwrap();
2683
2684 let mut entry = wf.entries().next().unwrap();
2685 assert_eq!(
2686 entry.matching_pattern(),
2687 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2688 );
2689
2690 entry.set_matching_pattern(".*/version-([\\d.]+)\\.tar\\.xz");
2691 assert_eq!(
2692 entry.matching_pattern(),
2693 Some(".*/version-([\\d.]+)\\.tar\\.xz".into())
2694 );
2695
2696 assert_eq!(entry.url(), "https://example.com/releases");
2698 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2699 assert_eq!(entry.script(), Some("uupdate".into()));
2700 assert_eq!(entry.compression(), Ok(Some(super::Compression::Xz)));
2701
2702 assert_eq!(
2704 entry.to_string(),
2705 "opts=compression=xz https://example.com/releases .*/version-([\\d.]+)\\.tar\\.xz debian uupdate\n"
2706 );
2707}
2708
2709#[test]
2710fn test_set_version_policy() {
2711 let wf: super::WatchFile = r#"version=4
2713https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2714"#
2715 .parse()
2716 .unwrap();
2717
2718 let mut entry = wf.entries().next().unwrap();
2719 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2720
2721 entry.set_version_policy("previous");
2722 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Previous)));
2723
2724 assert_eq!(entry.url(), "https://example.com/releases");
2726 assert_eq!(
2727 entry.matching_pattern(),
2728 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2729 );
2730 assert_eq!(entry.script(), Some("uupdate".into()));
2731
2732 assert_eq!(
2734 entry.to_string(),
2735 "https://example.com/releases (?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz previous uupdate\n"
2736 );
2737}
2738
2739#[test]
2740fn test_set_version_policy_with_options() {
2741 let wf: super::WatchFile = r#"version=4
2743opts=repack,compression=xz \
2744 https://github.com/example/example-cat/tags \
2745 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2746"#
2747 .parse()
2748 .unwrap();
2749
2750 let mut entry = wf.entries().next().unwrap();
2751 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2752
2753 entry.set_version_policy("ignore");
2754 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Ignore)));
2755
2756 assert_eq!(entry.url(), "https://github.com/example/example-cat/tags");
2758 assert_eq!(
2759 entry.matching_pattern(),
2760 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2761 );
2762 assert_eq!(entry.script(), Some("uupdate".into()));
2763 assert!(entry.repack());
2764
2765 assert_eq!(
2767 entry.to_string(),
2768 r#"opts=repack,compression=xz \
2769 https://github.com/example/example-cat/tags \
2770 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz ignore uupdate
2771"#
2772 );
2773}
2774
2775#[test]
2776fn test_set_script() {
2777 let wf: super::WatchFile = r#"version=4
2779https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2780"#
2781 .parse()
2782 .unwrap();
2783
2784 let mut entry = wf.entries().next().unwrap();
2785 assert_eq!(entry.script(), Some("uupdate".into()));
2786
2787 entry.set_script("uscan");
2788 assert_eq!(entry.script(), Some("uscan".into()));
2789
2790 assert_eq!(entry.url(), "https://example.com/releases");
2792 assert_eq!(
2793 entry.matching_pattern(),
2794 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2795 );
2796 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2797
2798 assert_eq!(
2800 entry.to_string(),
2801 "https://example.com/releases (?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz debian uscan\n"
2802 );
2803}
2804
2805#[test]
2806fn test_set_script_with_options() {
2807 let wf: super::WatchFile = r#"version=4
2809opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2810"#
2811 .parse()
2812 .unwrap();
2813
2814 let mut entry = wf.entries().next().unwrap();
2815 assert_eq!(entry.script(), Some("uupdate".into()));
2816
2817 entry.set_script("custom-script.sh");
2818 assert_eq!(entry.script(), Some("custom-script.sh".into()));
2819
2820 assert_eq!(entry.url(), "https://example.com/releases");
2822 assert_eq!(
2823 entry.matching_pattern(),
2824 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2825 );
2826 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2827 assert_eq!(entry.compression(), Ok(Some(super::Compression::Xz)));
2828
2829 assert_eq!(
2831 entry.to_string(),
2832 "opts=compression=xz https://example.com/releases (?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz debian custom-script.sh\n"
2833 );
2834}
2835
2836#[test]
2837fn test_apply_dversionmangle() {
2838 let wf: super::WatchFile = r#"version=4
2840opts=dversionmangle=s/\+dfsg$// https://example.com/ .*
2841"#
2842 .parse()
2843 .unwrap();
2844 let entry = wf.entries().next().unwrap();
2845 assert_eq!(entry.apply_dversionmangle("1.0+dfsg").unwrap(), "1.0");
2846 assert_eq!(entry.apply_dversionmangle("1.0").unwrap(), "1.0");
2847
2848 let wf: super::WatchFile = r#"version=4
2850opts=versionmangle=s/^v// https://example.com/ .*
2851"#
2852 .parse()
2853 .unwrap();
2854 let entry = wf.entries().next().unwrap();
2855 assert_eq!(entry.apply_dversionmangle("v1.0").unwrap(), "1.0");
2856
2857 let wf: super::WatchFile = r#"version=4
2859opts=dversionmangle=s/\+ds//,versionmangle=s/^v// https://example.com/ .*
2860"#
2861 .parse()
2862 .unwrap();
2863 let entry = wf.entries().next().unwrap();
2864 assert_eq!(entry.apply_dversionmangle("1.0+ds").unwrap(), "1.0");
2865
2866 let wf: super::WatchFile = r#"version=4
2868https://example.com/ .*
2869"#
2870 .parse()
2871 .unwrap();
2872 let entry = wf.entries().next().unwrap();
2873 assert_eq!(entry.apply_dversionmangle("1.0+dfsg").unwrap(), "1.0+dfsg");
2874}
2875
2876#[test]
2877fn test_apply_oversionmangle() {
2878 let wf: super::WatchFile = r#"version=4
2880opts=oversionmangle=s/$/-1/ https://example.com/ .*
2881"#
2882 .parse()
2883 .unwrap();
2884 let entry = wf.entries().next().unwrap();
2885 assert_eq!(entry.apply_oversionmangle("1.0").unwrap(), "1.0-1");
2886 assert_eq!(entry.apply_oversionmangle("2.5.3").unwrap(), "2.5.3-1");
2887
2888 let wf: super::WatchFile = r#"version=4
2890opts=oversionmangle=s/$/.dfsg/ https://example.com/ .*
2891"#
2892 .parse()
2893 .unwrap();
2894 let entry = wf.entries().next().unwrap();
2895 assert_eq!(entry.apply_oversionmangle("1.0").unwrap(), "1.0.dfsg");
2896
2897 let wf: super::WatchFile = r#"version=4
2899https://example.com/ .*
2900"#
2901 .parse()
2902 .unwrap();
2903 let entry = wf.entries().next().unwrap();
2904 assert_eq!(entry.apply_oversionmangle("1.0").unwrap(), "1.0");
2905}
2906
2907#[test]
2908fn test_apply_dirversionmangle() {
2909 let wf: super::WatchFile = r#"version=4
2911opts=dirversionmangle=s/^v// https://example.com/ .*
2912"#
2913 .parse()
2914 .unwrap();
2915 let entry = wf.entries().next().unwrap();
2916 assert_eq!(entry.apply_dirversionmangle("v1.0").unwrap(), "1.0");
2917 assert_eq!(entry.apply_dirversionmangle("v2.5.3").unwrap(), "2.5.3");
2918
2919 let wf: super::WatchFile = r#"version=4
2921opts=dirversionmangle=s/v(\d)/$1/ https://example.com/ .*
2922"#
2923 .parse()
2924 .unwrap();
2925 let entry = wf.entries().next().unwrap();
2926 assert_eq!(entry.apply_dirversionmangle("v1.0").unwrap(), "1.0");
2927
2928 let wf: super::WatchFile = r#"version=4
2930https://example.com/ .*
2931"#
2932 .parse()
2933 .unwrap();
2934 let entry = wf.entries().next().unwrap();
2935 assert_eq!(entry.apply_dirversionmangle("v1.0").unwrap(), "v1.0");
2936}
2937
2938#[test]
2939fn test_apply_filenamemangle() {
2940 let wf: super::WatchFile = r#"version=4
2942opts=filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/mypackage-$1.tar.gz/ https://example.com/ .*
2943"#
2944 .parse()
2945 .unwrap();
2946 let entry = wf.entries().next().unwrap();
2947 assert_eq!(
2948 entry
2949 .apply_filenamemangle("https://example.com/v1.0.tar.gz")
2950 .unwrap(),
2951 "mypackage-1.0.tar.gz"
2952 );
2953 assert_eq!(
2954 entry
2955 .apply_filenamemangle("https://example.com/2.5.3.tar.gz")
2956 .unwrap(),
2957 "mypackage-2.5.3.tar.gz"
2958 );
2959
2960 let wf: super::WatchFile = r#"version=4
2962opts=filenamemangle=s/.*\/(.*)/$1/ https://example.com/ .*
2963"#
2964 .parse()
2965 .unwrap();
2966 let entry = wf.entries().next().unwrap();
2967 assert_eq!(
2968 entry
2969 .apply_filenamemangle("https://example.com/path/to/file.tar.gz")
2970 .unwrap(),
2971 "file.tar.gz"
2972 );
2973
2974 let wf: super::WatchFile = r#"version=4
2976https://example.com/ .*
2977"#
2978 .parse()
2979 .unwrap();
2980 let entry = wf.entries().next().unwrap();
2981 assert_eq!(
2982 entry
2983 .apply_filenamemangle("https://example.com/file.tar.gz")
2984 .unwrap(),
2985 "https://example.com/file.tar.gz"
2986 );
2987}
2988
2989#[test]
2990fn test_apply_pagemangle() {
2991 let wf: super::WatchFile = r#"version=4
2993opts=pagemangle=s/&/&/g https://example.com/ .*
2994"#
2995 .parse()
2996 .unwrap();
2997 let entry = wf.entries().next().unwrap();
2998 assert_eq!(
2999 entry.apply_pagemangle(b"foo & bar").unwrap(),
3000 b"foo & bar"
3001 );
3002 assert_eq!(
3003 entry
3004 .apply_pagemangle(b"& foo & bar &")
3005 .unwrap(),
3006 b"& foo & bar &"
3007 );
3008
3009 let wf: super::WatchFile = r#"version=4
3011opts=pagemangle=s/<[^>]+>//g https://example.com/ .*
3012"#
3013 .parse()
3014 .unwrap();
3015 let entry = wf.entries().next().unwrap();
3016 assert_eq!(entry.apply_pagemangle(b"<div>text</div>").unwrap(), b"text");
3017
3018 let wf: super::WatchFile = r#"version=4
3020https://example.com/ .*
3021"#
3022 .parse()
3023 .unwrap();
3024 let entry = wf.entries().next().unwrap();
3025 assert_eq!(
3026 entry.apply_pagemangle(b"foo & bar").unwrap(),
3027 b"foo & bar"
3028 );
3029}
3030
3031#[test]
3032fn test_apply_downloadurlmangle() {
3033 let wf: super::WatchFile = r#"version=4
3035opts=downloadurlmangle=s|/archive/|/download/| https://example.com/ .*
3036"#
3037 .parse()
3038 .unwrap();
3039 let entry = wf.entries().next().unwrap();
3040 assert_eq!(
3041 entry
3042 .apply_downloadurlmangle("https://example.com/archive/file.tar.gz")
3043 .unwrap(),
3044 "https://example.com/download/file.tar.gz"
3045 );
3046
3047 let wf: super::WatchFile = r#"version=4
3049opts=downloadurlmangle=s/github\.com/raw.githubusercontent.com/ https://example.com/ .*
3050"#
3051 .parse()
3052 .unwrap();
3053 let entry = wf.entries().next().unwrap();
3054 assert_eq!(
3055 entry
3056 .apply_downloadurlmangle("https://github.com/user/repo/file.tar.gz")
3057 .unwrap(),
3058 "https://raw.githubusercontent.com/user/repo/file.tar.gz"
3059 );
3060
3061 let wf: super::WatchFile = r#"version=4
3063https://example.com/ .*
3064"#
3065 .parse()
3066 .unwrap();
3067 let entry = wf.entries().next().unwrap();
3068 assert_eq!(
3069 entry
3070 .apply_downloadurlmangle("https://example.com/archive/file.tar.gz")
3071 .unwrap(),
3072 "https://example.com/archive/file.tar.gz"
3073 );
3074}
3075
3076#[test]
3077fn test_entry_builder_minimal() {
3078 let entry = super::EntryBuilder::new("https://github.com/example/tags")
3080 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3081 .build();
3082
3083 assert_eq!(entry.url(), "https://github.com/example/tags");
3084 assert_eq!(
3085 entry.matching_pattern().as_deref(),
3086 Some(".*/v?(\\d\\S+)\\.tar\\.gz")
3087 );
3088 assert_eq!(entry.version(), Ok(None));
3089 assert_eq!(entry.script(), None);
3090 assert!(entry.opts().is_empty());
3091}
3092
3093#[test]
3094fn test_entry_builder_url_only() {
3095 let entry = super::EntryBuilder::new("https://example.com/releases").build();
3097
3098 assert_eq!(entry.url(), "https://example.com/releases");
3099 assert_eq!(entry.matching_pattern(), None);
3100 assert_eq!(entry.version(), Ok(None));
3101 assert_eq!(entry.script(), None);
3102 assert!(entry.opts().is_empty());
3103}
3104
3105#[test]
3106fn test_entry_builder_with_all_fields() {
3107 let entry = super::EntryBuilder::new("https://github.com/example/tags")
3109 .matching_pattern(".*/v?(\\d[\\d.]*)\\.tar\\.gz")
3110 .version_policy("debian")
3111 .script("uupdate")
3112 .opt("compression", "xz")
3113 .flag("repack")
3114 .build();
3115
3116 assert_eq!(entry.url(), "https://github.com/example/tags");
3117 assert_eq!(
3118 entry.matching_pattern().as_deref(),
3119 Some(".*/v?(\\d[\\d.]*)\\.tar\\.gz")
3120 );
3121 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
3122 assert_eq!(entry.script(), Some("uupdate".into()));
3123 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
3124 assert!(entry.has_option("repack"));
3125 assert!(entry.repack());
3126}
3127
3128#[test]
3129fn test_entry_builder_multiple_options() {
3130 let entry = super::EntryBuilder::new("https://example.com/tags")
3132 .matching_pattern(".*/v?(\\d+\\.\\d+)\\.tar\\.gz")
3133 .opt("compression", "xz")
3134 .opt("dversionmangle", "s/\\+ds//")
3135 .opt("repacksuffix", "+ds")
3136 .build();
3137
3138 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
3139 assert_eq!(
3140 entry.get_option("dversionmangle"),
3141 Some("s/\\+ds//".to_string())
3142 );
3143 assert_eq!(entry.get_option("repacksuffix"), Some("+ds".to_string()));
3144}
3145
3146#[test]
3147fn test_entry_builder_via_entry() {
3148 let entry = super::Entry::builder("https://github.com/example/tags")
3150 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3151 .version_policy("debian")
3152 .build();
3153
3154 assert_eq!(entry.url(), "https://github.com/example/tags");
3155 assert_eq!(
3156 entry.matching_pattern().as_deref(),
3157 Some(".*/v?(\\d\\S+)\\.tar\\.gz")
3158 );
3159 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
3160}
3161
3162#[test]
3163fn test_watchfile_add_entry_to_empty() {
3164 let mut wf = super::WatchFile::new(Some(4));
3166
3167 let entry = super::EntryBuilder::new("https://github.com/example/tags")
3168 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3169 .build();
3170
3171 wf.add_entry(entry);
3172
3173 assert_eq!(wf.version(), 4);
3174 assert_eq!(wf.entries().count(), 1);
3175
3176 let added_entry = wf.entries().next().unwrap();
3177 assert_eq!(added_entry.url(), "https://github.com/example/tags");
3178 assert_eq!(
3179 added_entry.matching_pattern().as_deref(),
3180 Some(".*/v?(\\d\\S+)\\.tar\\.gz")
3181 );
3182}
3183
3184#[test]
3185fn test_watchfile_add_multiple_entries() {
3186 let mut wf = super::WatchFile::new(Some(4));
3188
3189 wf.add_entry(
3190 super::EntryBuilder::new("https://github.com/example1/tags")
3191 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3192 .build(),
3193 );
3194
3195 wf.add_entry(
3196 super::EntryBuilder::new("https://github.com/example2/releases")
3197 .matching_pattern(".*/(\\d+\\.\\d+)\\.tar\\.gz")
3198 .opt("compression", "xz")
3199 .build(),
3200 );
3201
3202 assert_eq!(wf.entries().count(), 2);
3203
3204 let entries: Vec<_> = wf.entries().collect();
3205 assert_eq!(entries[0].url(), "https://github.com/example1/tags");
3206 assert_eq!(entries[1].url(), "https://github.com/example2/releases");
3207 assert_eq!(entries[1].get_option("compression"), Some("xz".to_string()));
3208}
3209
3210#[test]
3211fn test_watchfile_add_entry_to_existing() {
3212 let mut wf: super::WatchFile = r#"version=4
3214https://example.com/old .*/v?(\\d\\S+)\\.tar\\.gz
3215"#
3216 .parse()
3217 .unwrap();
3218
3219 assert_eq!(wf.entries().count(), 1);
3220
3221 wf.add_entry(
3222 super::EntryBuilder::new("https://github.com/example/new")
3223 .matching_pattern(".*/v?(\\d+\\.\\d+)\\.tar\\.gz")
3224 .opt("compression", "xz")
3225 .version_policy("debian")
3226 .build(),
3227 );
3228
3229 assert_eq!(wf.entries().count(), 2);
3230
3231 let entries: Vec<_> = wf.entries().collect();
3232 assert_eq!(entries[0].url(), "https://example.com/old");
3233 assert_eq!(entries[1].url(), "https://github.com/example/new");
3234 assert_eq!(entries[1].version(), Ok(Some(VersionPolicy::Debian)));
3235}
3236
3237#[test]
3238fn test_entry_builder_formatting() {
3239 let entry = super::EntryBuilder::new("https://github.com/example/tags")
3241 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3242 .opt("compression", "xz")
3243 .flag("repack")
3244 .version_policy("debian")
3245 .script("uupdate")
3246 .build();
3247
3248 let entry_str = entry.to_string();
3249
3250 assert!(entry_str.starts_with("opts="));
3252 assert!(entry_str.contains("https://github.com/example/tags"));
3254 assert!(entry_str.contains(".*/v?(\\d\\S+)\\.tar\\.gz"));
3256 assert!(entry_str.contains("debian"));
3258 assert!(entry_str.contains("uupdate"));
3260 assert!(entry_str.ends_with('\n'));
3262}
3263
3264#[test]
3265fn test_watchfile_add_entry_preserves_format() {
3266 let mut wf = super::WatchFile::new(Some(4));
3268
3269 wf.add_entry(
3270 super::EntryBuilder::new("https://github.com/example/tags")
3271 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3272 .build(),
3273 );
3274
3275 let wf_str = wf.to_string();
3276
3277 assert!(wf_str.starts_with("version=4\n"));
3279 assert!(wf_str.contains("https://github.com/example/tags"));
3281
3282 let reparsed: super::WatchFile = wf_str.parse().unwrap();
3284 assert_eq!(reparsed.version(), 4);
3285 assert_eq!(reparsed.entries().count(), 1);
3286}
3287
3288#[test]
3289fn test_line_col() {
3290 let text = r#"version=4
3291opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
3292"#;
3293 let wf = text.parse::<super::WatchFile>().unwrap();
3294
3295 let version_node = wf.version_node().unwrap();
3297 assert_eq!(version_node.line(), 0);
3298 assert_eq!(version_node.column(), 0);
3299 assert_eq!(version_node.line_col(), (0, 0));
3300
3301 let entries: Vec<_> = wf.entries().collect();
3303 assert_eq!(entries.len(), 1);
3304
3305 assert_eq!(entries[0].line(), 1);
3307 assert_eq!(entries[0].column(), 0);
3308 assert_eq!(entries[0].line_col(), (1, 0));
3309
3310 let option_list = entries[0].option_list().unwrap();
3312 assert_eq!(option_list.line(), 1); let url_node = entries[0].url_node().unwrap();
3315 assert_eq!(url_node.line(), 1); let pattern_node = entries[0].matching_pattern_node().unwrap();
3318 assert_eq!(pattern_node.line(), 1); let version_policy_node = entries[0].version_node().unwrap();
3321 assert_eq!(version_policy_node.line(), 1); let script_node = entries[0].script_node().unwrap();
3324 assert_eq!(script_node.line(), 1); let options: Vec<_> = option_list.options().collect();
3328 assert_eq!(options.len(), 1);
3329 assert_eq!(options[0].key(), Some("compression".to_string()));
3330 assert_eq!(options[0].value(), Some("xz".to_string()));
3331 assert_eq!(options[0].line(), 1); let compression_opt = option_list.find_option("compression").unwrap();
3335 assert_eq!(compression_opt.line(), 1);
3336 assert_eq!(compression_opt.column(), 5); assert_eq!(compression_opt.line_col(), (1, 5));
3338}