1use crate::lex::lex;
2use crate::types::*;
3use crate::SyntaxKind;
4use crate::SyntaxKind::*;
5use crate::DEFAULT_VERSION;
6use std::io::Read;
7use std::str::FromStr;
8
9#[derive(Debug, Clone, PartialEq, Eq, Hash)]
10pub struct ParseError(Vec<String>);
12
13impl std::fmt::Display for ParseError {
14 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
15 for err in &self.0 {
16 writeln!(f, "{}", err)?;
17 }
18 Ok(())
19 }
20}
21
22impl std::error::Error for ParseError {}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
28enum Lang {}
29impl rowan::Language for Lang {
30 type Kind = SyntaxKind;
31 fn kind_from_raw(raw: rowan::SyntaxKind) -> Self::Kind {
32 unsafe { std::mem::transmute::<u16, SyntaxKind>(raw.0) }
33 }
34 fn kind_to_raw(kind: Self::Kind) -> rowan::SyntaxKind {
35 kind.into()
36 }
37}
38
39use rowan::GreenNode;
42
43use rowan::GreenNodeBuilder;
47
48struct Parse {
51 green_node: GreenNode,
52 #[allow(unused)]
53 errors: Vec<String>,
54 #[allow(unused)]
55 version: i32,
56}
57
58fn parse(text: &str) -> Parse {
59 struct Parser {
60 tokens: Vec<(SyntaxKind, String)>,
63 builder: GreenNodeBuilder<'static>,
65 errors: Vec<String>,
68 }
69
70 impl Parser {
71 fn parse_version(&mut self) -> Option<i32> {
72 let mut version = None;
73 if self.tokens.last() == Some(&(KEY, "version".to_string())) {
74 self.builder.start_node(VERSION.into());
75 self.bump();
76 self.skip_ws();
77 if self.current() != Some(EQUALS) {
78 self.builder.start_node(ERROR.into());
79 self.errors.push("expected `=`".to_string());
80 self.bump();
81 self.builder.finish_node();
82 } else {
83 self.bump();
84 }
85 if self.current() != Some(VALUE) {
86 self.builder.start_node(ERROR.into());
87 self.errors
88 .push(format!("expected value, got {:?}", self.current()));
89 self.bump();
90 self.builder.finish_node();
91 } else {
92 let version_str = self.tokens.last().unwrap().1.clone();
93 match version_str.parse() {
94 Ok(v) => {
95 version = Some(v);
96 self.bump();
97 }
98 Err(_) => {
99 self.builder.start_node(ERROR.into());
100 self.errors
101 .push(format!("invalid version: {}", version_str));
102 self.bump();
103 self.builder.finish_node();
104 }
105 }
106 }
107 if self.current() != Some(NEWLINE) {
108 self.builder.start_node(ERROR.into());
109 self.errors.push("expected newline".to_string());
110 self.bump();
111 self.builder.finish_node();
112 } else {
113 self.bump();
114 }
115 self.builder.finish_node();
116 }
117 version
118 }
119
120 fn parse_watch_entry(&mut self) -> bool {
121 self.skip_ws();
122 if self.current().is_none() {
123 return false;
124 }
125 if self.current() == Some(NEWLINE) {
126 self.bump();
127 return false;
128 }
129 self.builder.start_node(ENTRY.into());
130 self.parse_options_list();
131 for i in 0..4 {
132 if self.current() == Some(NEWLINE) {
133 break;
134 }
135 if self.current() == Some(CONTINUATION) {
136 self.bump();
137 self.skip_ws();
138 continue;
139 }
140 if self.current() != Some(VALUE) && self.current() != Some(KEY) {
141 self.builder.start_node(ERROR.into());
142 self.errors.push(format!(
143 "expected value, got {:?} (i={})",
144 self.current(),
145 i
146 ));
147 if self.current().is_some() {
148 self.bump();
149 }
150 self.builder.finish_node();
151 } else {
152 match i {
154 0 => {
155 self.builder.start_node(URL.into());
157 self.bump();
158 self.builder.finish_node();
159 }
160 1 => {
161 self.builder.start_node(MATCHING_PATTERN.into());
163 self.bump();
164 self.builder.finish_node();
165 }
166 2 => {
167 self.builder.start_node(VERSION_POLICY.into());
169 self.bump();
170 self.builder.finish_node();
171 }
172 3 => {
173 self.builder.start_node(SCRIPT.into());
175 self.bump();
176 self.builder.finish_node();
177 }
178 _ => {
179 self.bump();
180 }
181 }
182 }
183 self.skip_ws();
184 }
185 if self.current() != Some(NEWLINE) && self.current().is_some() {
186 self.builder.start_node(ERROR.into());
187 self.errors
188 .push(format!("expected newline, not {:?}", self.current()));
189 if self.current().is_some() {
190 self.bump();
191 }
192 self.builder.finish_node();
193 } else {
194 self.bump();
195 }
196 self.builder.finish_node();
197 true
198 }
199
200 fn parse_option(&mut self) -> bool {
201 if self.current().is_none() {
202 return false;
203 }
204 while self.current() == Some(CONTINUATION) {
205 self.bump();
206 }
207 if self.current() == Some(WHITESPACE) {
208 return false;
209 }
210 self.builder.start_node(OPTION.into());
211 if self.current() != Some(KEY) {
212 self.builder.start_node(ERROR.into());
213 self.errors.push("expected key".to_string());
214 self.bump();
215 self.builder.finish_node();
216 } else {
217 self.bump();
218 }
219 if self.current() == Some(EQUALS) {
220 self.bump();
221 if self.current() != Some(VALUE) && self.current() != Some(KEY) {
222 self.builder.start_node(ERROR.into());
223 self.errors
224 .push(format!("expected value, got {:?}", self.current()));
225 self.bump();
226 self.builder.finish_node();
227 } else {
228 self.bump();
229 }
230 } else if self.current() == Some(COMMA) {
231 } else {
232 self.builder.start_node(ERROR.into());
233 self.errors.push("expected `=`".to_string());
234 if self.current().is_some() {
235 self.bump();
236 }
237 self.builder.finish_node();
238 }
239 self.builder.finish_node();
240 true
241 }
242
243 fn parse_options_list(&mut self) {
244 self.skip_ws();
245 if self.tokens.last() == Some(&(KEY, "opts".to_string()))
246 || self.tokens.last() == Some(&(KEY, "options".to_string()))
247 {
248 self.builder.start_node(OPTS_LIST.into());
249 self.bump();
250 self.skip_ws();
251 if self.current() != Some(EQUALS) {
252 self.builder.start_node(ERROR.into());
253 self.errors.push("expected `=`".to_string());
254 if self.current().is_some() {
255 self.bump();
256 }
257 self.builder.finish_node();
258 } else {
259 self.bump();
260 }
261 let quoted = if self.current() == Some(QUOTE) {
262 self.bump();
263 true
264 } else {
265 false
266 };
267 loop {
268 if quoted {
269 if self.current() == Some(QUOTE) {
270 self.bump();
271 break;
272 }
273 self.skip_ws();
274 }
275 if !self.parse_option() {
276 break;
277 }
278 if self.current() == Some(COMMA) {
279 self.builder.start_node(OPTION_SEPARATOR.into());
280 self.bump();
281 self.builder.finish_node();
282 } else if !quoted {
283 break;
284 }
285 }
286 self.builder.finish_node();
287 self.skip_ws();
288 }
289 }
290
291 fn parse(mut self) -> Parse {
292 let mut version = 1;
293 self.builder.start_node(ROOT.into());
295 if let Some(v) = self.parse_version() {
296 version = v;
297 }
298 loop {
300 if !self.parse_watch_entry() {
301 break;
302 }
303 }
304 self.skip_ws();
306 self.builder.finish_node();
308
309 Parse {
311 green_node: self.builder.finish(),
312 errors: self.errors,
313 version,
314 }
315 }
316 fn bump(&mut self) {
318 let (kind, text) = self.tokens.pop().unwrap();
319 self.builder.token(kind.into(), text.as_str());
320 }
321 fn current(&self) -> Option<SyntaxKind> {
323 self.tokens.last().map(|(kind, _)| *kind)
324 }
325 fn skip_ws(&mut self) {
326 while self.current() == Some(WHITESPACE)
327 || self.current() == Some(CONTINUATION)
328 || self.current() == Some(COMMENT)
329 {
330 self.bump()
331 }
332 }
333 }
334
335 let mut tokens = lex(text);
336 tokens.reverse();
337 Parser {
338 tokens,
339 builder: GreenNodeBuilder::new(),
340 errors: Vec::new(),
341 }
342 .parse()
343}
344
345type SyntaxNode = rowan::SyntaxNode<Lang>;
352#[allow(unused)]
353type SyntaxToken = rowan::SyntaxToken<Lang>;
354#[allow(unused)]
355type SyntaxElement = rowan::NodeOrToken<SyntaxNode, SyntaxToken>;
356
357impl Parse {
358 fn syntax(&self) -> SyntaxNode {
359 SyntaxNode::new_root_mut(self.green_node.clone())
360 }
361
362 fn root(&self) -> WatchFile {
363 WatchFile::cast(self.syntax()).unwrap()
364 }
365}
366
367fn line_col_at_offset(node: &SyntaxNode, offset: rowan::TextSize) -> (usize, usize) {
370 let root = node.ancestors().last().unwrap_or_else(|| node.clone());
371 let mut line = 0;
372 let mut last_newline_offset = rowan::TextSize::from(0);
373
374 for element in root.preorder_with_tokens() {
375 if let rowan::WalkEvent::Enter(rowan::NodeOrToken::Token(token)) = element {
376 if token.text_range().start() >= offset {
377 break;
378 }
379
380 for (idx, _) in token.text().match_indices('\n') {
382 line += 1;
383 last_newline_offset =
384 token.text_range().start() + rowan::TextSize::from((idx + 1) as u32);
385 }
386 }
387 }
388
389 let column: usize = (offset - last_newline_offset).into();
390 (line, column)
391}
392
393macro_rules! ast_node {
394 ($ast:ident, $kind:ident) => {
395 #[derive(Clone, PartialEq, Eq, Hash)]
396 #[repr(transparent)]
397 pub struct $ast(SyntaxNode);
399 impl $ast {
400 #[allow(unused)]
401 fn cast(node: SyntaxNode) -> Option<Self> {
402 if node.kind() == $kind {
403 Some(Self(node))
404 } else {
405 None
406 }
407 }
408
409 pub fn line(&self) -> usize {
411 line_col_at_offset(&self.0, self.0.text_range().start()).0
412 }
413
414 pub fn column(&self) -> usize {
416 line_col_at_offset(&self.0, self.0.text_range().start()).1
417 }
418
419 pub fn line_col(&self) -> (usize, usize) {
422 line_col_at_offset(&self.0, self.0.text_range().start())
423 }
424 }
425
426 impl ToString for $ast {
427 fn to_string(&self) -> String {
428 self.0.text().to_string()
429 }
430 }
431 };
432}
433
434ast_node!(WatchFile, ROOT);
435ast_node!(Version, VERSION);
436ast_node!(Entry, ENTRY);
437ast_node!(OptionList, OPTS_LIST);
438ast_node!(_Option, OPTION);
439ast_node!(Url, URL);
440ast_node!(MatchingPattern, MATCHING_PATTERN);
441ast_node!(VersionPolicyNode, VERSION_POLICY);
442ast_node!(ScriptNode, SCRIPT);
443
444impl WatchFile {
445 pub fn new(version: Option<u32>) -> WatchFile {
447 let mut builder = GreenNodeBuilder::new();
448
449 builder.start_node(ROOT.into());
450 if let Some(version) = version {
451 builder.start_node(VERSION.into());
452 builder.token(KEY.into(), "version");
453 builder.token(EQUALS.into(), "=");
454 builder.token(VALUE.into(), version.to_string().as_str());
455 builder.token(NEWLINE.into(), "\n");
456 builder.finish_node();
457 }
458 builder.finish_node();
459 WatchFile(SyntaxNode::new_root_mut(builder.finish()))
460 }
461
462 pub fn version_node(&self) -> Option<Version> {
464 self.0.children().find_map(Version::cast)
465 }
466
467 pub fn version(&self) -> u32 {
469 self.version_node()
470 .map(|it| it.version())
471 .unwrap_or(DEFAULT_VERSION)
472 }
473
474 pub fn entries(&self) -> impl Iterator<Item = Entry> + '_ {
476 self.0.children().filter_map(Entry::cast)
477 }
478
479 pub fn set_version(&mut self, new_version: u32) {
481 let mut builder = GreenNodeBuilder::new();
483 builder.start_node(VERSION.into());
484 builder.token(KEY.into(), "version");
485 builder.token(EQUALS.into(), "=");
486 builder.token(VALUE.into(), new_version.to_string().as_str());
487 builder.token(NEWLINE.into(), "\n");
488 builder.finish_node();
489 let new_version_green = builder.finish();
490
491 let new_version_node = SyntaxNode::new_root_mut(new_version_green);
493
494 let version_pos = self.0.children().position(|child| child.kind() == VERSION);
496
497 if let Some(pos) = version_pos {
498 self.0
500 .splice_children(pos..pos + 1, vec![new_version_node.into()]);
501 } else {
502 self.0.splice_children(0..0, vec![new_version_node.into()]);
504 }
505 }
506
507 #[cfg(feature = "discover")]
527 pub async fn uscan(
528 &self,
529 package: impl Fn() -> String,
530 ) -> Result<Vec<Vec<crate::Release>>, Box<dyn std::error::Error>> {
531 let mut all_releases = Vec::new();
532
533 for entry in self.entries() {
534 let releases = entry.discover(|| package()).await?;
535 all_releases.push(releases);
536 }
537
538 Ok(all_releases)
539 }
540
541 #[cfg(all(feature = "discover", feature = "blocking"))]
559 pub fn uscan_blocking(
560 &self,
561 package: impl Fn() -> String,
562 ) -> Result<Vec<Vec<crate::Release>>, Box<dyn std::error::Error>> {
563 let mut all_releases = Vec::new();
564
565 for entry in self.entries() {
566 let releases = entry.discover_blocking(|| package())?;
567 all_releases.push(releases);
568 }
569
570 Ok(all_releases)
571 }
572
573 pub fn add_entry(&mut self, entry: Entry) {
600 let insert_pos = self.0.children_with_tokens().count();
602
603 let entry_green = entry.0.green().into_owned();
605 let new_entry_node = SyntaxNode::new_root_mut(entry_green);
606
607 self.0
609 .splice_children(insert_pos..insert_pos, vec![new_entry_node.into()]);
610 }
611
612 pub fn from_reader<R: std::io::Read>(reader: R) -> Result<WatchFile, ParseError> {
614 let mut buf_reader = std::io::BufReader::new(reader);
615 let mut content = String::new();
616 buf_reader
617 .read_to_string(&mut content)
618 .map_err(|e| ParseError(vec![e.to_string()]))?;
619 content.parse()
620 }
621
622 pub fn from_reader_relaxed<R: std::io::Read>(mut r: R) -> Result<Self, std::io::Error> {
624 let mut content = String::new();
625 r.read_to_string(&mut content)?;
626 let parsed = parse(&content);
627 Ok(parsed.root())
628 }
629
630 pub fn from_str_relaxed(s: &str) -> Self {
632 let parsed = parse(s);
633 parsed.root()
634 }
635}
636
637impl FromStr for WatchFile {
638 type Err = ParseError;
639
640 fn from_str(s: &str) -> Result<Self, Self::Err> {
641 let parsed = parse(s);
642 if parsed.errors.is_empty() {
643 Ok(parsed.root())
644 } else {
645 Err(ParseError(parsed.errors))
646 }
647 }
648}
649
650impl Version {
651 pub fn version(&self) -> u32 {
653 self.0
654 .children_with_tokens()
655 .find_map(|it| match it {
656 SyntaxElement::Token(token) => {
657 if token.kind() == VALUE {
658 Some(token.text().parse().unwrap())
659 } else {
660 None
661 }
662 }
663 _ => None,
664 })
665 .unwrap_or(DEFAULT_VERSION)
666 }
667}
668
669#[derive(Debug, Clone, Default)]
693pub struct EntryBuilder {
694 url: Option<String>,
695 matching_pattern: Option<String>,
696 version_policy: Option<String>,
697 script: Option<String>,
698 opts: std::collections::HashMap<String, String>,
699}
700
701impl EntryBuilder {
702 pub fn new(url: impl Into<String>) -> Self {
704 EntryBuilder {
705 url: Some(url.into()),
706 matching_pattern: None,
707 version_policy: None,
708 script: None,
709 opts: std::collections::HashMap::new(),
710 }
711 }
712
713 pub fn matching_pattern(mut self, pattern: impl Into<String>) -> Self {
715 self.matching_pattern = Some(pattern.into());
716 self
717 }
718
719 pub fn version_policy(mut self, policy: impl Into<String>) -> Self {
721 self.version_policy = Some(policy.into());
722 self
723 }
724
725 pub fn script(mut self, script: impl Into<String>) -> Self {
727 self.script = Some(script.into());
728 self
729 }
730
731 pub fn opt(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
733 self.opts.insert(key.into(), value.into());
734 self
735 }
736
737 pub fn flag(mut self, key: impl Into<String>) -> Self {
741 self.opts.insert(key.into(), String::new());
742 self
743 }
744
745 pub fn build(self) -> Entry {
751 let url = self.url.expect("URL is required for entry");
752
753 let mut builder = GreenNodeBuilder::new();
754
755 builder.start_node(ENTRY.into());
756
757 if !self.opts.is_empty() {
759 builder.start_node(OPTS_LIST.into());
760 builder.token(KEY.into(), "opts");
761 builder.token(EQUALS.into(), "=");
762
763 let mut first = true;
764 for (key, value) in self.opts.iter() {
765 if !first {
766 builder.token(COMMA.into(), ",");
767 }
768 first = false;
769
770 builder.start_node(OPTION.into());
771 builder.token(KEY.into(), key);
772 if !value.is_empty() {
773 builder.token(EQUALS.into(), "=");
774 builder.token(VALUE.into(), value);
775 }
776 builder.finish_node();
777 }
778
779 builder.finish_node();
780 builder.token(WHITESPACE.into(), " ");
781 }
782
783 builder.start_node(URL.into());
785 builder.token(VALUE.into(), &url);
786 builder.finish_node();
787
788 if let Some(pattern) = self.matching_pattern {
790 builder.token(WHITESPACE.into(), " ");
791 builder.start_node(MATCHING_PATTERN.into());
792 builder.token(VALUE.into(), &pattern);
793 builder.finish_node();
794 }
795
796 if let Some(policy) = self.version_policy {
798 builder.token(WHITESPACE.into(), " ");
799 builder.start_node(VERSION_POLICY.into());
800 builder.token(VALUE.into(), &policy);
801 builder.finish_node();
802 }
803
804 if let Some(script_val) = self.script {
806 builder.token(WHITESPACE.into(), " ");
807 builder.start_node(SCRIPT.into());
808 builder.token(VALUE.into(), &script_val);
809 builder.finish_node();
810 }
811
812 builder.token(NEWLINE.into(), "\n");
813 builder.finish_node();
814
815 Entry(SyntaxNode::new_root_mut(builder.finish()))
816 }
817}
818
819impl Entry {
820 pub fn builder(url: impl Into<String>) -> EntryBuilder {
834 EntryBuilder::new(url)
835 }
836
837 pub fn option_list(&self) -> Option<OptionList> {
839 self.0.children().find_map(OptionList::cast)
840 }
841
842 pub fn get_option(&self, key: &str) -> Option<String> {
844 self.option_list().and_then(|ol| ol.get_option(key))
845 }
846
847 pub fn has_option(&self, key: &str) -> bool {
849 self.option_list().map_or(false, |ol| ol.has_option(key))
850 }
851
852 pub fn component(&self) -> Option<String> {
854 self.get_option("component")
855 }
856
857 pub fn ctype(&self) -> Result<Option<ComponentType>, ()> {
859 self.get_option("ctype").map(|s| s.parse()).transpose()
860 }
861
862 pub fn compression(&self) -> Result<Option<Compression>, ()> {
864 self.get_option("compression")
865 .map(|s| s.parse())
866 .transpose()
867 }
868
869 pub fn repack(&self) -> bool {
871 self.has_option("repack")
872 }
873
874 pub fn repacksuffix(&self) -> Option<String> {
876 self.get_option("repacksuffix")
877 }
878
879 pub fn mode(&self) -> Result<Mode, ()> {
881 Ok(self
882 .get_option("mode")
883 .map(|s| s.parse())
884 .transpose()?
885 .unwrap_or_default())
886 }
887
888 pub fn pretty(&self) -> Result<Pretty, ()> {
890 Ok(self
891 .get_option("pretty")
892 .map(|s| s.parse())
893 .transpose()?
894 .unwrap_or_default())
895 }
896
897 pub fn date(&self) -> String {
900 self.get_option("date")
901 .unwrap_or_else(|| "%Y%m%d".to_string())
902 }
903
904 pub fn gitexport(&self) -> Result<GitExport, ()> {
906 Ok(self
907 .get_option("gitexport")
908 .map(|s| s.parse())
909 .transpose()?
910 .unwrap_or_default())
911 }
912
913 pub fn gitmode(&self) -> Result<GitMode, ()> {
915 Ok(self
916 .get_option("gitmode")
917 .map(|s| s.parse())
918 .transpose()?
919 .unwrap_or_default())
920 }
921
922 pub fn pgpmode(&self) -> Result<PgpMode, ()> {
924 Ok(self
925 .get_option("pgpmode")
926 .map(|s| s.parse())
927 .transpose()?
928 .unwrap_or_default())
929 }
930
931 pub fn searchmode(&self) -> Result<SearchMode, ()> {
933 Ok(self
934 .get_option("searchmode")
935 .map(|s| s.parse())
936 .transpose()?
937 .unwrap_or_default())
938 }
939
940 pub fn decompress(&self) -> bool {
942 self.has_option("decompress")
943 }
944
945 pub fn bare(&self) -> bool {
948 self.has_option("bare")
949 }
950
951 pub fn user_agent(&self) -> Option<String> {
953 self.get_option("user-agent")
954 }
955
956 pub fn passive(&self) -> Option<bool> {
958 if self.has_option("passive") || self.has_option("pasv") {
959 Some(true)
960 } else if self.has_option("active") || self.has_option("nopasv") {
961 Some(false)
962 } else {
963 None
964 }
965 }
966
967 pub fn unzipoptions(&self) -> Option<String> {
970 self.get_option("unzipopt")
971 }
972
973 pub fn dversionmangle(&self) -> Option<String> {
975 self.get_option("dversionmangle")
976 .or_else(|| self.get_option("versionmangle"))
977 }
978
979 pub fn dirversionmangle(&self) -> Option<String> {
983 self.get_option("dirversionmangle")
984 }
985
986 pub fn pagemangle(&self) -> Option<String> {
988 self.get_option("pagemangle")
989 }
990
991 pub fn uversionmangle(&self) -> Option<String> {
995 self.get_option("uversionmangle")
996 .or_else(|| self.get_option("versionmangle"))
997 }
998
999 pub fn versionmangle(&self) -> Option<String> {
1001 self.get_option("versionmangle")
1002 }
1003
1004 pub fn hrefdecode(&self) -> bool {
1009 self.get_option("hrefdecode").is_some()
1010 }
1011
1012 pub fn downloadurlmangle(&self) -> Option<String> {
1015 self.get_option("downloadurlmangle")
1016 }
1017
1018 pub fn filenamemangle(&self) -> Option<String> {
1026 self.get_option("filenamemangle")
1027 }
1028
1029 pub fn pgpsigurlmangle(&self) -> Option<String> {
1031 self.get_option("pgpsigurlmangle")
1032 }
1033
1034 pub fn oversionmangle(&self) -> Option<String> {
1037 self.get_option("oversionmangle")
1038 }
1039
1040 pub fn apply_uversionmangle(
1053 &self,
1054 version: &str,
1055 ) -> Result<String, crate::mangle::MangleError> {
1056 if let Some(vm) = self.uversionmangle() {
1057 crate::mangle::apply_mangle(&vm, version)
1058 } else {
1059 Ok(version.to_string())
1060 }
1061 }
1062
1063 pub fn apply_dversionmangle(
1076 &self,
1077 version: &str,
1078 ) -> Result<String, crate::mangle::MangleError> {
1079 if let Some(vm) = self.dversionmangle() {
1080 crate::mangle::apply_mangle(&vm, version)
1081 } else {
1082 Ok(version.to_string())
1083 }
1084 }
1085
1086 pub fn apply_oversionmangle(
1099 &self,
1100 version: &str,
1101 ) -> Result<String, crate::mangle::MangleError> {
1102 if let Some(vm) = self.oversionmangle() {
1103 crate::mangle::apply_mangle(&vm, version)
1104 } else {
1105 Ok(version.to_string())
1106 }
1107 }
1108
1109 pub fn apply_dirversionmangle(
1122 &self,
1123 version: &str,
1124 ) -> Result<String, crate::mangle::MangleError> {
1125 if let Some(vm) = self.dirversionmangle() {
1126 crate::mangle::apply_mangle(&vm, version)
1127 } else {
1128 Ok(version.to_string())
1129 }
1130 }
1131
1132 pub fn apply_filenamemangle(&self, url: &str) -> Result<String, crate::mangle::MangleError> {
1148 if let Some(vm) = self.filenamemangle() {
1149 crate::mangle::apply_mangle(&vm, url)
1150 } else {
1151 Ok(url.to_string())
1152 }
1153 }
1154
1155 pub fn apply_pagemangle(&self, page: &[u8]) -> Result<Vec<u8>, crate::mangle::MangleError> {
1171 if let Some(vm) = self.pagemangle() {
1172 let page_str = String::from_utf8_lossy(page);
1173 let mangled = crate::mangle::apply_mangle(&vm, &page_str)?;
1174 Ok(mangled.into_bytes())
1175 } else {
1176 Ok(page.to_vec())
1177 }
1178 }
1179
1180 pub fn apply_downloadurlmangle(&self, url: &str) -> Result<String, crate::mangle::MangleError> {
1196 if let Some(vm) = self.downloadurlmangle() {
1197 crate::mangle::apply_mangle(&vm, url)
1198 } else {
1199 Ok(url.to_string())
1200 }
1201 }
1202
1203 #[cfg(feature = "discover")]
1224 pub async fn discover(
1225 &self,
1226 package: impl FnOnce() -> String,
1227 ) -> Result<Vec<crate::Release>, Box<dyn std::error::Error>> {
1228 let url = self.format_url(package);
1229 let user_agent = self
1230 .user_agent()
1231 .unwrap_or_else(|| crate::DEFAULT_USER_AGENT.to_string());
1232 let searchmode = self.searchmode().unwrap_or(crate::SearchMode::Html);
1233
1234 let client = reqwest::Client::builder().user_agent(user_agent).build()?;
1235
1236 let response = client.get(url.as_str()).send().await?;
1237 let body = response.bytes().await?;
1238
1239 let mangled_body = self.apply_pagemangle(&body)?;
1241
1242 let matching_pattern = self
1243 .matching_pattern()
1244 .ok_or("matching_pattern is required")?;
1245
1246 let package_name = String::new(); let results = crate::search::search(
1248 match searchmode {
1249 crate::SearchMode::Html => "html",
1250 crate::SearchMode::Plain => "plain",
1251 },
1252 std::io::Cursor::new(mangled_body.as_ref() as &[u8]),
1253 &subst(&matching_pattern, || package_name.clone()),
1254 &package_name,
1255 url.as_str(),
1256 )?;
1257
1258 let mut releases = Vec::new();
1259 for (version, full_url) in results {
1260 let mangled_version = self.apply_uversionmangle(&version)?;
1262
1263 let mangled_url = self.apply_downloadurlmangle(&full_url)?;
1265
1266 let pgpsigurl = if let Some(mangle) = self.pgpsigurlmangle() {
1268 Some(crate::mangle::apply_mangle(&mangle, &mangled_url)?)
1269 } else {
1270 None
1271 };
1272
1273 let target_filename = if self.filenamemangle().is_some() {
1275 Some(self.apply_filenamemangle(&mangled_url)?)
1276 } else {
1277 None
1278 };
1279
1280 let package_version = if self.oversionmangle().is_some() {
1282 Some(self.apply_oversionmangle(&mangled_version)?)
1283 } else {
1284 None
1285 };
1286
1287 releases.push(crate::Release::new_full(
1288 mangled_version,
1289 mangled_url,
1290 pgpsigurl,
1291 target_filename,
1292 package_version,
1293 ));
1294 }
1295
1296 Ok(releases)
1297 }
1298
1299 #[cfg(all(feature = "discover", feature = "blocking"))]
1318 pub fn discover_blocking(
1319 &self,
1320 package: impl FnOnce() -> String,
1321 ) -> Result<Vec<crate::Release>, Box<dyn std::error::Error>> {
1322 let url = self.format_url(package);
1323 let user_agent = self
1324 .user_agent()
1325 .unwrap_or_else(|| crate::DEFAULT_USER_AGENT.to_string());
1326 let searchmode = self.searchmode().unwrap_or(crate::SearchMode::Html);
1327
1328 let client = reqwest::blocking::Client::builder()
1329 .user_agent(user_agent)
1330 .build()?;
1331
1332 let response = client.get(url.as_str()).send()?;
1333 let body = response.bytes()?;
1334
1335 let mangled_body = self.apply_pagemangle(&body)?;
1337
1338 let matching_pattern = self
1339 .matching_pattern()
1340 .ok_or("matching_pattern is required")?;
1341
1342 let package_name = String::new(); let results = crate::search::search(
1344 match searchmode {
1345 crate::SearchMode::Html => "html",
1346 crate::SearchMode::Plain => "plain",
1347 },
1348 std::io::Cursor::new(mangled_body.as_ref() as &[u8]),
1349 &subst(&matching_pattern, || package_name.clone()),
1350 &package_name,
1351 url.as_str(),
1352 )?;
1353
1354 let mut releases = Vec::new();
1355 for (version, full_url) in results {
1356 let mangled_version = self.apply_uversionmangle(&version)?;
1358
1359 let mangled_url = self.apply_downloadurlmangle(&full_url)?;
1361
1362 let pgpsigurl = if let Some(mangle) = self.pgpsigurlmangle() {
1364 Some(crate::mangle::apply_mangle(&mangle, &mangled_url)?)
1365 } else {
1366 None
1367 };
1368
1369 let target_filename = if self.filenamemangle().is_some() {
1371 Some(self.apply_filenamemangle(&mangled_url)?)
1372 } else {
1373 None
1374 };
1375
1376 let package_version = if self.oversionmangle().is_some() {
1378 Some(self.apply_oversionmangle(&mangled_version)?)
1379 } else {
1380 None
1381 };
1382
1383 releases.push(crate::Release::new_full(
1384 mangled_version,
1385 mangled_url,
1386 pgpsigurl,
1387 target_filename,
1388 package_version,
1389 ));
1390 }
1391
1392 Ok(releases)
1393 }
1394
1395 pub fn opts(&self) -> std::collections::HashMap<String, String> {
1397 let mut options = std::collections::HashMap::new();
1398
1399 if let Some(ol) = self.option_list() {
1400 for opt in ol.options() {
1401 let key = opt.key();
1402 let value = opt.value();
1403 if let (Some(key), Some(value)) = (key, value) {
1404 options.insert(key.to_string(), value.to_string());
1405 }
1406 }
1407 }
1408
1409 options
1410 }
1411
1412 fn items(&self) -> impl Iterator<Item = String> + '_ {
1413 self.0.children_with_tokens().filter_map(|it| match it {
1414 SyntaxElement::Token(token) => {
1415 if token.kind() == VALUE || token.kind() == KEY {
1416 Some(token.text().to_string())
1417 } else {
1418 None
1419 }
1420 }
1421 SyntaxElement::Node(node) => {
1422 match node.kind() {
1424 URL => Url::cast(node).map(|n| n.url()),
1425 MATCHING_PATTERN => MatchingPattern::cast(node).map(|n| n.pattern()),
1426 VERSION_POLICY => VersionPolicyNode::cast(node).map(|n| n.policy()),
1427 SCRIPT => ScriptNode::cast(node).map(|n| n.script()),
1428 _ => None,
1429 }
1430 }
1431 })
1432 }
1433
1434 pub fn url_node(&self) -> Option<Url> {
1436 self.0.children().find_map(Url::cast)
1437 }
1438
1439 pub fn url(&self) -> String {
1441 self.url_node().map(|it| it.url()).unwrap_or_else(|| {
1442 self.items().next().unwrap()
1444 })
1445 }
1446
1447 pub fn matching_pattern_node(&self) -> Option<MatchingPattern> {
1449 self.0.children().find_map(MatchingPattern::cast)
1450 }
1451
1452 pub fn matching_pattern(&self) -> Option<String> {
1454 self.matching_pattern_node()
1455 .map(|it| it.pattern())
1456 .or_else(|| {
1457 self.items().nth(1)
1459 })
1460 }
1461
1462 pub fn version_node(&self) -> Option<VersionPolicyNode> {
1464 self.0.children().find_map(VersionPolicyNode::cast)
1465 }
1466
1467 pub fn version(&self) -> Result<Option<crate::VersionPolicy>, String> {
1469 self.version_node()
1470 .map(|it| it.policy().parse())
1471 .transpose()
1472 .or_else(|_e| {
1473 self.items().nth(2).map(|it| it.parse()).transpose()
1475 })
1476 }
1477
1478 pub fn script_node(&self) -> Option<ScriptNode> {
1480 self.0.children().find_map(ScriptNode::cast)
1481 }
1482
1483 pub fn script(&self) -> Option<String> {
1485 self.script_node().map(|it| it.script()).or_else(|| {
1486 self.items().nth(3)
1488 })
1489 }
1490
1491 pub fn format_url(&self, package: impl FnOnce() -> String) -> url::Url {
1493 subst(self.url().as_str(), package).parse().unwrap()
1494 }
1495
1496 pub fn set_url(&mut self, new_url: &str) {
1498 let mut builder = GreenNodeBuilder::new();
1500 builder.start_node(URL.into());
1501 builder.token(VALUE.into(), new_url);
1502 builder.finish_node();
1503 let new_url_green = builder.finish();
1504
1505 let new_url_node = SyntaxNode::new_root_mut(new_url_green);
1507
1508 let url_pos = self
1510 .0
1511 .children_with_tokens()
1512 .position(|child| matches!(child, SyntaxElement::Node(node) if node.kind() == URL));
1513
1514 if let Some(pos) = url_pos {
1515 self.0
1517 .splice_children(pos..pos + 1, vec![new_url_node.into()]);
1518 }
1519 }
1520
1521 pub fn set_matching_pattern(&mut self, new_pattern: &str) {
1527 let mut builder = GreenNodeBuilder::new();
1529 builder.start_node(MATCHING_PATTERN.into());
1530 builder.token(VALUE.into(), new_pattern);
1531 builder.finish_node();
1532 let new_pattern_green = builder.finish();
1533
1534 let new_pattern_node = SyntaxNode::new_root_mut(new_pattern_green);
1536
1537 let pattern_pos = self.0.children_with_tokens().position(
1539 |child| matches!(child, SyntaxElement::Node(node) if node.kind() == MATCHING_PATTERN),
1540 );
1541
1542 if let Some(pos) = pattern_pos {
1543 self.0
1545 .splice_children(pos..pos + 1, vec![new_pattern_node.into()]);
1546 }
1547 }
1549
1550 pub fn set_version_policy(&mut self, new_policy: &str) {
1556 let mut builder = GreenNodeBuilder::new();
1558 builder.start_node(VERSION_POLICY.into());
1559 builder.token(VALUE.into(), new_policy);
1561 builder.finish_node();
1562 let new_policy_green = builder.finish();
1563
1564 let new_policy_node = SyntaxNode::new_root_mut(new_policy_green);
1566
1567 let policy_pos = self.0.children_with_tokens().position(
1569 |child| matches!(child, SyntaxElement::Node(node) if node.kind() == VERSION_POLICY),
1570 );
1571
1572 if let Some(pos) = policy_pos {
1573 self.0
1575 .splice_children(pos..pos + 1, vec![new_policy_node.into()]);
1576 }
1577 }
1579
1580 pub fn set_script(&mut self, new_script: &str) {
1586 let mut builder = GreenNodeBuilder::new();
1588 builder.start_node(SCRIPT.into());
1589 builder.token(VALUE.into(), new_script);
1591 builder.finish_node();
1592 let new_script_green = builder.finish();
1593
1594 let new_script_node = SyntaxNode::new_root_mut(new_script_green);
1596
1597 let script_pos = self
1599 .0
1600 .children_with_tokens()
1601 .position(|child| matches!(child, SyntaxElement::Node(node) if node.kind() == SCRIPT));
1602
1603 if let Some(pos) = script_pos {
1604 self.0
1606 .splice_children(pos..pos + 1, vec![new_script_node.into()]);
1607 }
1608 }
1610
1611 pub fn set_opt(&mut self, key: &str, value: &str) {
1617 let opts_pos = self.0.children_with_tokens().position(
1619 |child| matches!(child, SyntaxElement::Node(node) if node.kind() == OPTS_LIST),
1620 );
1621
1622 if let Some(_opts_idx) = opts_pos {
1623 if let Some(mut ol) = self.option_list() {
1624 if let Some(mut opt) = ol.find_option(key) {
1626 opt.set_value(value);
1628 } else {
1630 ol.add_option(key, value);
1632 }
1634 }
1635 } else {
1636 let mut builder = GreenNodeBuilder::new();
1638 builder.start_node(OPTS_LIST.into());
1639 builder.token(KEY.into(), "opts");
1640 builder.token(EQUALS.into(), "=");
1641 builder.start_node(OPTION.into());
1642 builder.token(KEY.into(), key);
1643 builder.token(EQUALS.into(), "=");
1644 builder.token(VALUE.into(), value);
1645 builder.finish_node();
1646 builder.finish_node();
1647 let new_opts_green = builder.finish();
1648 let new_opts_node = SyntaxNode::new_root_mut(new_opts_green);
1649
1650 let url_pos = self
1652 .0
1653 .children_with_tokens()
1654 .position(|child| matches!(child, SyntaxElement::Node(node) if node.kind() == URL));
1655
1656 if let Some(url_idx) = url_pos {
1657 let mut combined_builder = GreenNodeBuilder::new();
1660 combined_builder.start_node(ROOT.into()); combined_builder.token(WHITESPACE.into(), " ");
1662 combined_builder.finish_node();
1663 let temp_green = combined_builder.finish();
1664 let temp_root = SyntaxNode::new_root_mut(temp_green);
1665 let space_element = temp_root.children_with_tokens().next().unwrap();
1666
1667 self.0
1668 .splice_children(url_idx..url_idx, vec![new_opts_node.into(), space_element]);
1669 } else {
1670 self.0.splice_children(0..0, vec![new_opts_node.into()]);
1671 }
1672 }
1673 }
1674
1675 pub fn del_opt(&mut self, key: &str) {
1682 if let Some(mut ol) = self.option_list() {
1683 let option_count = ol.0.children().filter(|n| n.kind() == OPTION).count();
1684
1685 if option_count == 1 && ol.has_option(key) {
1686 let opts_pos = self.0.children().position(|node| node.kind() == OPTS_LIST);
1688
1689 if let Some(opts_idx) = opts_pos {
1690 self.0.splice_children(opts_idx..opts_idx + 1, vec![]);
1692
1693 while self.0.children_with_tokens().next().map_or(false, |e| {
1695 matches!(
1696 e,
1697 SyntaxElement::Token(t) if t.kind() == WHITESPACE || t.kind() == CONTINUATION
1698 )
1699 }) {
1700 self.0.splice_children(0..1, vec![]);
1701 }
1702 }
1703 } else {
1704 ol.remove_option(key);
1706 }
1707 }
1708 }
1709}
1710
1711const SUBSTITUTIONS: &[(&str, &str)] = &[
1712 ("@ANY_VERSION@", r"[-_]?(\d[\-+\.:\~\da-zA-Z]*)"),
1717 (
1720 "@ARCHIVE_EXT@",
1721 r"(?i)\.(?:tar\.xz|tar\.bz2|tar\.gz|zip|tgz|tbz|txz)",
1722 ),
1723 (
1726 "@SIGNATURE_EXT@",
1727 r"(?i)\.(?:tar\.xz|tar\.bz2|tar\.gz|zip|tgz|tbz|txz)\.(?:asc|pgp|gpg|sig|sign)",
1728 ),
1729 ("@DEB_EXT@", r"[\+~](debian|dfsg|ds|deb)(\.)?(\d+)?$"),
1731];
1732
1733pub fn subst(text: &str, package: impl FnOnce() -> String) -> String {
1734 let mut substs = SUBSTITUTIONS.to_vec();
1735 let package_name;
1736 if text.contains("@PACKAGE@") {
1737 package_name = Some(package());
1738 substs.push(("@PACKAGE@", package_name.as_deref().unwrap()));
1739 }
1740
1741 let mut text = text.to_string();
1742
1743 for (k, v) in substs {
1744 text = text.replace(k, v);
1745 }
1746
1747 text
1748}
1749
1750#[test]
1751fn test_subst() {
1752 assert_eq!(
1753 subst("@ANY_VERSION@", || unreachable!()),
1754 r"[-_]?(\d[\-+\.:\~\da-zA-Z]*)"
1755 );
1756 assert_eq!(subst("@PACKAGE@", || "dulwich".to_string()), "dulwich");
1757}
1758
1759impl std::fmt::Debug for OptionList {
1760 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1761 f.debug_struct("OptionList")
1762 .field("text", &self.0.text().to_string())
1763 .finish()
1764 }
1765}
1766
1767impl OptionList {
1768 pub fn options(&self) -> impl Iterator<Item = _Option> + '_ {
1770 self.0.children().filter_map(_Option::cast)
1771 }
1772
1773 pub fn find_option(&self, key: &str) -> Option<_Option> {
1775 self.options().find(|opt| opt.key().as_deref() == Some(key))
1776 }
1777
1778 pub fn has_option(&self, key: &str) -> bool {
1779 self.options().any(|it| it.key().as_deref() == Some(key))
1780 }
1781
1782 pub fn get_option(&self, key: &str) -> Option<String> {
1783 for child in self.options() {
1784 if child.key().as_deref() == Some(key) {
1785 return child.value();
1786 }
1787 }
1788 None
1789 }
1790
1791 fn add_option(&mut self, key: &str, value: &str) {
1793 let option_count = self.0.children().filter(|n| n.kind() == OPTION).count();
1794
1795 let mut builder = GreenNodeBuilder::new();
1797 builder.start_node(ROOT.into()); if option_count > 0 {
1800 builder.start_node(OPTION_SEPARATOR.into());
1801 builder.token(COMMA.into(), ",");
1802 builder.finish_node();
1803 }
1804
1805 builder.start_node(OPTION.into());
1806 builder.token(KEY.into(), key);
1807 builder.token(EQUALS.into(), "=");
1808 builder.token(VALUE.into(), value);
1809 builder.finish_node();
1810
1811 builder.finish_node(); let combined_green = builder.finish();
1813
1814 let temp_root = SyntaxNode::new_root_mut(combined_green);
1816 let new_children: Vec<_> = temp_root.children_with_tokens().collect();
1817
1818 let insert_pos = self.0.children_with_tokens().count();
1819 self.0.splice_children(insert_pos..insert_pos, new_children);
1820 }
1821
1822 fn remove_option(&mut self, key: &str) -> bool {
1824 if let Some(mut opt) = self.find_option(key) {
1825 opt.remove();
1826 true
1827 } else {
1828 false
1829 }
1830 }
1831}
1832
1833impl _Option {
1834 pub fn key(&self) -> Option<String> {
1836 self.0.children_with_tokens().find_map(|it| match it {
1837 SyntaxElement::Token(token) => {
1838 if token.kind() == KEY {
1839 Some(token.text().to_string())
1840 } else {
1841 None
1842 }
1843 }
1844 _ => None,
1845 })
1846 }
1847
1848 pub fn value(&self) -> Option<String> {
1850 self.0
1851 .children_with_tokens()
1852 .filter_map(|it| match it {
1853 SyntaxElement::Token(token) => {
1854 if token.kind() == VALUE || token.kind() == KEY {
1855 Some(token.text().to_string())
1856 } else {
1857 None
1858 }
1859 }
1860 _ => None,
1861 })
1862 .nth(1)
1863 }
1864
1865 pub fn set_value(&mut self, new_value: &str) {
1867 let key = self.key().expect("Option must have a key");
1868
1869 let mut builder = GreenNodeBuilder::new();
1871 builder.start_node(OPTION.into());
1872 builder.token(KEY.into(), &key);
1873 builder.token(EQUALS.into(), "=");
1874 builder.token(VALUE.into(), new_value);
1875 builder.finish_node();
1876 let new_option_green = builder.finish();
1877 let new_option_node = SyntaxNode::new_root_mut(new_option_green);
1878
1879 if let Some(parent) = self.0.parent() {
1881 let idx = self.0.index();
1882 parent.splice_children(idx..idx + 1, vec![new_option_node.into()]);
1883 }
1884 }
1885
1886 pub fn remove(&mut self) {
1888 let next_sep = self
1890 .0
1891 .next_sibling()
1892 .filter(|n| n.kind() == OPTION_SEPARATOR);
1893 let prev_sep = self
1894 .0
1895 .prev_sibling()
1896 .filter(|n| n.kind() == OPTION_SEPARATOR);
1897
1898 if let Some(sep) = next_sep {
1900 sep.detach();
1901 } else if let Some(sep) = prev_sep {
1902 sep.detach();
1903 }
1904
1905 self.0.detach();
1907 }
1908}
1909
1910impl Url {
1911 pub fn url(&self) -> String {
1913 self.0
1914 .children_with_tokens()
1915 .find_map(|it| match it {
1916 SyntaxElement::Token(token) => {
1917 if token.kind() == VALUE {
1918 Some(token.text().to_string())
1919 } else {
1920 None
1921 }
1922 }
1923 _ => None,
1924 })
1925 .unwrap()
1926 }
1927}
1928
1929impl MatchingPattern {
1930 pub fn pattern(&self) -> String {
1932 self.0
1933 .children_with_tokens()
1934 .find_map(|it| match it {
1935 SyntaxElement::Token(token) => {
1936 if token.kind() == VALUE {
1937 Some(token.text().to_string())
1938 } else {
1939 None
1940 }
1941 }
1942 _ => None,
1943 })
1944 .unwrap()
1945 }
1946}
1947
1948impl VersionPolicyNode {
1949 pub fn policy(&self) -> String {
1951 self.0
1952 .children_with_tokens()
1953 .find_map(|it| match it {
1954 SyntaxElement::Token(token) => {
1955 if token.kind() == VALUE || token.kind() == KEY {
1957 Some(token.text().to_string())
1958 } else {
1959 None
1960 }
1961 }
1962 _ => None,
1963 })
1964 .unwrap()
1965 }
1966}
1967
1968impl ScriptNode {
1969 pub fn script(&self) -> String {
1971 self.0
1972 .children_with_tokens()
1973 .find_map(|it| match it {
1974 SyntaxElement::Token(token) => {
1975 if token.kind() == VALUE || token.kind() == KEY {
1977 Some(token.text().to_string())
1978 } else {
1979 None
1980 }
1981 }
1982 _ => None,
1983 })
1984 .unwrap()
1985 }
1986}
1987
1988#[cfg(test)]
1989mod tests {
1990 use super::*;
1991
1992 #[test]
1993 fn test_entry_node_structure() {
1994 let wf: super::WatchFile = r#"version=4
1996opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
1997"#
1998 .parse()
1999 .unwrap();
2000
2001 let entry = wf.entries().next().unwrap();
2002
2003 assert_eq!(entry.0.children().find(|n| n.kind() == URL).is_some(), true);
2005 assert_eq!(entry.url(), "https://example.com/releases");
2006
2007 assert_eq!(
2009 entry
2010 .0
2011 .children()
2012 .find(|n| n.kind() == MATCHING_PATTERN)
2013 .is_some(),
2014 true
2015 );
2016 assert_eq!(
2017 entry.matching_pattern(),
2018 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2019 );
2020
2021 assert_eq!(
2023 entry
2024 .0
2025 .children()
2026 .find(|n| n.kind() == VERSION_POLICY)
2027 .is_some(),
2028 true
2029 );
2030 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2031
2032 assert_eq!(
2034 entry.0.children().find(|n| n.kind() == SCRIPT).is_some(),
2035 true
2036 );
2037 assert_eq!(entry.script(), Some("uupdate".into()));
2038 }
2039
2040 #[test]
2041 fn test_entry_node_structure_partial() {
2042 let wf: super::WatchFile = r#"version=4
2044https://github.com/example/tags .*/v?(\d\S+)\.tar\.gz
2045"#
2046 .parse()
2047 .unwrap();
2048
2049 let entry = wf.entries().next().unwrap();
2050
2051 assert_eq!(entry.0.children().find(|n| n.kind() == URL).is_some(), true);
2053 assert_eq!(
2054 entry
2055 .0
2056 .children()
2057 .find(|n| n.kind() == MATCHING_PATTERN)
2058 .is_some(),
2059 true
2060 );
2061
2062 assert_eq!(
2064 entry
2065 .0
2066 .children()
2067 .find(|n| n.kind() == VERSION_POLICY)
2068 .is_some(),
2069 false
2070 );
2071 assert_eq!(
2072 entry.0.children().find(|n| n.kind() == SCRIPT).is_some(),
2073 false
2074 );
2075
2076 assert_eq!(entry.url(), "https://github.com/example/tags");
2078 assert_eq!(
2079 entry.matching_pattern(),
2080 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2081 );
2082 assert_eq!(entry.version(), Ok(None));
2083 assert_eq!(entry.script(), None);
2084 }
2085
2086 #[test]
2087 fn test_parse_v1() {
2088 const WATCHV1: &str = r#"version=4
2089opts=bare,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1\.tar\.gz/ \
2090 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2091"#;
2092 let parsed = parse(WATCHV1);
2093 let node = parsed.syntax();
2095 assert_eq!(
2096 format!("{:#?}", node),
2097 r#"ROOT@0..161
2098 VERSION@0..10
2099 KEY@0..7 "version"
2100 EQUALS@7..8 "="
2101 VALUE@8..9 "4"
2102 NEWLINE@9..10 "\n"
2103 ENTRY@10..161
2104 OPTS_LIST@10..86
2105 KEY@10..14 "opts"
2106 EQUALS@14..15 "="
2107 OPTION@15..19
2108 KEY@15..19 "bare"
2109 OPTION_SEPARATOR@19..20
2110 COMMA@19..20 ","
2111 OPTION@20..86
2112 KEY@20..34 "filenamemangle"
2113 EQUALS@34..35 "="
2114 VALUE@35..86 "s/.+\\/v?(\\d\\S+)\\.tar\\ ..."
2115 WHITESPACE@86..87 " "
2116 CONTINUATION@87..89 "\\\n"
2117 WHITESPACE@89..91 " "
2118 URL@91..138
2119 VALUE@91..138 "https://github.com/sy ..."
2120 WHITESPACE@138..139 " "
2121 MATCHING_PATTERN@139..160
2122 VALUE@139..160 ".*/v?(\\d\\S+)\\.tar\\.gz"
2123 NEWLINE@160..161 "\n"
2124"#
2125 );
2126
2127 let root = parsed.root();
2128 assert_eq!(root.version(), 4);
2129 let entries = root.entries().collect::<Vec<_>>();
2130 assert_eq!(entries.len(), 1);
2131 let entry = &entries[0];
2132 assert_eq!(
2133 entry.url(),
2134 "https://github.com/syncthing/syncthing-gtk/tags"
2135 );
2136 assert_eq!(
2137 entry.matching_pattern(),
2138 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2139 );
2140 assert_eq!(entry.version(), Ok(None));
2141 assert_eq!(entry.script(), None);
2142
2143 assert_eq!(node.text(), WATCHV1);
2144 }
2145
2146 #[test]
2147 fn test_parse_v2() {
2148 let parsed = parse(
2149 r#"version=4
2150https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2151# comment
2152"#,
2153 );
2154 assert_eq!(parsed.errors, Vec::<String>::new());
2155 let node = parsed.syntax();
2156 assert_eq!(
2157 format!("{:#?}", node),
2158 r###"ROOT@0..90
2159 VERSION@0..10
2160 KEY@0..7 "version"
2161 EQUALS@7..8 "="
2162 VALUE@8..9 "4"
2163 NEWLINE@9..10 "\n"
2164 ENTRY@10..80
2165 URL@10..57
2166 VALUE@10..57 "https://github.com/sy ..."
2167 WHITESPACE@57..58 " "
2168 MATCHING_PATTERN@58..79
2169 VALUE@58..79 ".*/v?(\\d\\S+)\\.tar\\.gz"
2170 NEWLINE@79..80 "\n"
2171 COMMENT@80..89 "# comment"
2172 NEWLINE@89..90 "\n"
2173"###
2174 );
2175
2176 let root = parsed.root();
2177 assert_eq!(root.version(), 4);
2178 let entries = root.entries().collect::<Vec<_>>();
2179 assert_eq!(entries.len(), 1);
2180 let entry = &entries[0];
2181 assert_eq!(
2182 entry.url(),
2183 "https://github.com/syncthing/syncthing-gtk/tags"
2184 );
2185 assert_eq!(
2186 entry.format_url(|| "syncthing-gtk".to_string()),
2187 "https://github.com/syncthing/syncthing-gtk/tags"
2188 .parse()
2189 .unwrap()
2190 );
2191 }
2192
2193 #[test]
2194 fn test_parse_v3() {
2195 let parsed = parse(
2196 r#"version=4
2197https://github.com/syncthing/@PACKAGE@/tags .*/v?(\d\S+)\.tar\.gz
2198# comment
2199"#,
2200 );
2201 assert_eq!(parsed.errors, Vec::<String>::new());
2202 let root = parsed.root();
2203 assert_eq!(root.version(), 4);
2204 let entries = root.entries().collect::<Vec<_>>();
2205 assert_eq!(entries.len(), 1);
2206 let entry = &entries[0];
2207 assert_eq!(entry.url(), "https://github.com/syncthing/@PACKAGE@/tags");
2208 assert_eq!(
2209 entry.format_url(|| "syncthing-gtk".to_string()),
2210 "https://github.com/syncthing/syncthing-gtk/tags"
2211 .parse()
2212 .unwrap()
2213 );
2214 }
2215
2216 #[test]
2217 fn test_parse_v4() {
2218 let cl: super::WatchFile = r#"version=4
2219opts=repack,compression=xz,dversionmangle=s/\+ds//,repacksuffix=+ds \
2220 https://github.com/example/example-cat/tags \
2221 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2222"#
2223 .parse()
2224 .unwrap();
2225 assert_eq!(cl.version(), 4);
2226 let entries = cl.entries().collect::<Vec<_>>();
2227 assert_eq!(entries.len(), 1);
2228 let entry = &entries[0];
2229 assert_eq!(entry.url(), "https://github.com/example/example-cat/tags");
2230 assert_eq!(
2231 entry.matching_pattern(),
2232 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2233 );
2234 assert!(entry.repack());
2235 assert_eq!(entry.compression(), Ok(Some(Compression::Xz)));
2236 assert_eq!(entry.dversionmangle(), Some("s/\\+ds//".into()));
2237 assert_eq!(entry.repacksuffix(), Some("+ds".into()));
2238 assert_eq!(entry.script(), Some("uupdate".into()));
2239 assert_eq!(
2240 entry.format_url(|| "example-cat".to_string()),
2241 "https://github.com/example/example-cat/tags"
2242 .parse()
2243 .unwrap()
2244 );
2245 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
2246 }
2247
2248 #[test]
2249 fn test_git_mode() {
2250 let text = r#"version=3
2251opts="mode=git, gitmode=shallow, pgpmode=gittag" \
2252https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git \
2253refs/tags/(.*) debian
2254"#;
2255 let parsed = parse(text);
2256 assert_eq!(parsed.errors, Vec::<String>::new());
2257 let cl = parsed.root();
2258 assert_eq!(cl.version(), 3);
2259 let entries = cl.entries().collect::<Vec<_>>();
2260 assert_eq!(entries.len(), 1);
2261 let entry = &entries[0];
2262 assert_eq!(
2263 entry.url(),
2264 "https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git"
2265 );
2266 assert_eq!(entry.matching_pattern(), Some("refs/tags/(.*)".into()));
2267 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
2268 assert_eq!(entry.script(), None);
2269 assert_eq!(entry.gitmode(), Ok(GitMode::Shallow));
2270 assert_eq!(entry.pgpmode(), Ok(PgpMode::GitTag));
2271 assert_eq!(entry.mode(), Ok(Mode::Git));
2272 }
2273
2274 #[test]
2275 fn test_parse_quoted() {
2276 const WATCHV1: &str = r#"version=4
2277opts="bare, filenamemangle=blah" \
2278 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2279"#;
2280 let parsed = parse(WATCHV1);
2281 let node = parsed.syntax();
2283
2284 let root = parsed.root();
2285 assert_eq!(root.version(), 4);
2286 let entries = root.entries().collect::<Vec<_>>();
2287 assert_eq!(entries.len(), 1);
2288 let entry = &entries[0];
2289
2290 assert_eq!(
2291 entry.url(),
2292 "https://github.com/syncthing/syncthing-gtk/tags"
2293 );
2294 assert_eq!(
2295 entry.matching_pattern(),
2296 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2297 );
2298 assert_eq!(entry.version(), Ok(None));
2299 assert_eq!(entry.script(), None);
2300
2301 assert_eq!(node.text(), WATCHV1);
2302 }
2303
2304 #[test]
2305 fn test_set_url() {
2306 let wf: super::WatchFile = r#"version=4
2308https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2309"#
2310 .parse()
2311 .unwrap();
2312
2313 let mut entry = wf.entries().next().unwrap();
2314 assert_eq!(
2315 entry.url(),
2316 "https://github.com/syncthing/syncthing-gtk/tags"
2317 );
2318
2319 entry.set_url("https://newurl.example.org/path");
2320 assert_eq!(entry.url(), "https://newurl.example.org/path");
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 "https://newurl.example.org/path .*/v?(\\d\\S+)\\.tar\\.gz\n"
2330 );
2331 }
2332
2333 #[test]
2334 fn test_set_url_with_options() {
2335 let wf: super::WatchFile = r#"version=4
2337opts=foo=blah https://foo.com/bar .*/v?(\d\S+)\.tar\.gz
2338"#
2339 .parse()
2340 .unwrap();
2341
2342 let mut entry = wf.entries().next().unwrap();
2343 assert_eq!(entry.url(), "https://foo.com/bar");
2344 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2345
2346 entry.set_url("https://example.com/baz");
2347 assert_eq!(entry.url(), "https://example.com/baz");
2348
2349 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2351 assert_eq!(
2352 entry.matching_pattern(),
2353 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2354 );
2355
2356 assert_eq!(
2358 entry.to_string(),
2359 "opts=foo=blah https://example.com/baz .*/v?(\\d\\S+)\\.tar\\.gz\n"
2360 );
2361 }
2362
2363 #[test]
2364 fn test_set_url_complex() {
2365 let wf: super::WatchFile = r#"version=4
2367opts=bare,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1\.tar\.gz/ \
2368 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2369"#
2370 .parse()
2371 .unwrap();
2372
2373 let mut entry = wf.entries().next().unwrap();
2374 assert_eq!(
2375 entry.url(),
2376 "https://github.com/syncthing/syncthing-gtk/tags"
2377 );
2378
2379 entry.set_url("https://gitlab.com/newproject/tags");
2380 assert_eq!(entry.url(), "https://gitlab.com/newproject/tags");
2381
2382 assert!(entry.bare());
2384 assert_eq!(
2385 entry.filenamemangle(),
2386 Some("s/.+\\/v?(\\d\\S+)\\.tar\\.gz/syncthing-gtk-$1\\.tar\\.gz/".into())
2387 );
2388 assert_eq!(
2389 entry.matching_pattern(),
2390 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2391 );
2392
2393 assert_eq!(
2395 entry.to_string(),
2396 r#"opts=bare,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1\.tar\.gz/ \
2397 https://gitlab.com/newproject/tags .*/v?(\d\S+)\.tar\.gz
2398"#
2399 );
2400 }
2401
2402 #[test]
2403 fn test_set_url_with_all_fields() {
2404 let wf: super::WatchFile = r#"version=4
2406opts=repack,compression=xz,dversionmangle=s/\+ds//,repacksuffix=+ds \
2407 https://github.com/example/example-cat/tags \
2408 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2409"#
2410 .parse()
2411 .unwrap();
2412
2413 let mut entry = wf.entries().next().unwrap();
2414 assert_eq!(entry.url(), "https://github.com/example/example-cat/tags");
2415 assert_eq!(
2416 entry.matching_pattern(),
2417 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2418 );
2419 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2420 assert_eq!(entry.script(), Some("uupdate".into()));
2421
2422 entry.set_url("https://gitlab.example.org/project/releases");
2423 assert_eq!(entry.url(), "https://gitlab.example.org/project/releases");
2424
2425 assert!(entry.repack());
2427 assert_eq!(entry.compression(), Ok(Some(super::Compression::Xz)));
2428 assert_eq!(entry.dversionmangle(), Some("s/\\+ds//".into()));
2429 assert_eq!(entry.repacksuffix(), Some("+ds".into()));
2430 assert_eq!(
2431 entry.matching_pattern(),
2432 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2433 );
2434 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2435 assert_eq!(entry.script(), Some("uupdate".into()));
2436
2437 assert_eq!(
2439 entry.to_string(),
2440 r#"opts=repack,compression=xz,dversionmangle=s/\+ds//,repacksuffix=+ds \
2441 https://gitlab.example.org/project/releases \
2442 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2443"#
2444 );
2445 }
2446
2447 #[test]
2448 fn test_set_url_quoted_options() {
2449 let wf: super::WatchFile = r#"version=4
2451opts="bare, filenamemangle=blah" \
2452 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2453"#
2454 .parse()
2455 .unwrap();
2456
2457 let mut entry = wf.entries().next().unwrap();
2458 assert_eq!(
2459 entry.url(),
2460 "https://github.com/syncthing/syncthing-gtk/tags"
2461 );
2462
2463 entry.set_url("https://example.org/new/path");
2464 assert_eq!(entry.url(), "https://example.org/new/path");
2465
2466 assert_eq!(
2468 entry.to_string(),
2469 r#"opts="bare, filenamemangle=blah" \
2470 https://example.org/new/path .*/v?(\d\S+)\.tar\.gz
2471"#
2472 );
2473 }
2474
2475 #[test]
2476 fn test_set_opt_update_existing() {
2477 let wf: super::WatchFile = r#"version=4
2479opts=foo=blah,bar=baz https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2480"#
2481 .parse()
2482 .unwrap();
2483
2484 let mut entry = wf.entries().next().unwrap();
2485 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2486 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2487
2488 entry.set_opt("foo", "updated");
2489 assert_eq!(entry.get_option("foo"), Some("updated".to_string()));
2490 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2491
2492 assert_eq!(
2494 entry.to_string(),
2495 "opts=foo=updated,bar=baz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2496 );
2497 }
2498
2499 #[test]
2500 fn test_set_opt_add_new() {
2501 let wf: super::WatchFile = r#"version=4
2503opts=foo=blah https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2504"#
2505 .parse()
2506 .unwrap();
2507
2508 let mut entry = wf.entries().next().unwrap();
2509 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2510 assert_eq!(entry.get_option("bar"), None);
2511
2512 entry.set_opt("bar", "baz");
2513 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2514 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2515
2516 assert_eq!(
2518 entry.to_string(),
2519 "opts=foo=blah,bar=baz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2520 );
2521 }
2522
2523 #[test]
2524 fn test_set_opt_create_options_list() {
2525 let wf: super::WatchFile = r#"version=4
2527https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2528"#
2529 .parse()
2530 .unwrap();
2531
2532 let mut entry = wf.entries().next().unwrap();
2533 assert_eq!(entry.option_list(), None);
2534
2535 entry.set_opt("compression", "xz");
2536 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
2537
2538 assert_eq!(
2540 entry.to_string(),
2541 "opts=compression=xz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2542 );
2543 }
2544
2545 #[test]
2546 fn test_del_opt_remove_single() {
2547 let wf: super::WatchFile = r#"version=4
2549opts=foo=blah,bar=baz,qux=quux https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2550"#
2551 .parse()
2552 .unwrap();
2553
2554 let mut entry = wf.entries().next().unwrap();
2555 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2556 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2557 assert_eq!(entry.get_option("qux"), Some("quux".to_string()));
2558
2559 entry.del_opt("bar");
2560 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2561 assert_eq!(entry.get_option("bar"), None);
2562 assert_eq!(entry.get_option("qux"), Some("quux".to_string()));
2563
2564 assert_eq!(
2566 entry.to_string(),
2567 "opts=foo=blah,qux=quux https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2568 );
2569 }
2570
2571 #[test]
2572 fn test_del_opt_remove_first() {
2573 let wf: super::WatchFile = r#"version=4
2575opts=foo=blah,bar=baz https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2576"#
2577 .parse()
2578 .unwrap();
2579
2580 let mut entry = wf.entries().next().unwrap();
2581 entry.del_opt("foo");
2582 assert_eq!(entry.get_option("foo"), None);
2583 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2584
2585 assert_eq!(
2587 entry.to_string(),
2588 "opts=bar=baz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2589 );
2590 }
2591
2592 #[test]
2593 fn test_del_opt_remove_last() {
2594 let wf: super::WatchFile = r#"version=4
2596opts=foo=blah,bar=baz https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2597"#
2598 .parse()
2599 .unwrap();
2600
2601 let mut entry = wf.entries().next().unwrap();
2602 entry.del_opt("bar");
2603 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2604 assert_eq!(entry.get_option("bar"), None);
2605
2606 assert_eq!(
2608 entry.to_string(),
2609 "opts=foo=blah https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2610 );
2611 }
2612
2613 #[test]
2614 fn test_del_opt_remove_only_option() {
2615 let wf: super::WatchFile = r#"version=4
2617opts=foo=blah https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2618"#
2619 .parse()
2620 .unwrap();
2621
2622 let mut entry = wf.entries().next().unwrap();
2623 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2624
2625 entry.del_opt("foo");
2626 assert_eq!(entry.get_option("foo"), None);
2627 assert_eq!(entry.option_list(), None);
2628
2629 assert_eq!(
2631 entry.to_string(),
2632 "https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2633 );
2634 }
2635
2636 #[test]
2637 fn test_del_opt_nonexistent() {
2638 let wf: super::WatchFile = r#"version=4
2640opts=foo=blah https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2641"#
2642 .parse()
2643 .unwrap();
2644
2645 let mut entry = wf.entries().next().unwrap();
2646 let original = entry.to_string();
2647
2648 entry.del_opt("nonexistent");
2649 assert_eq!(entry.to_string(), original);
2650 }
2651
2652 #[test]
2653 fn test_set_opt_multiple_operations() {
2654 let wf: super::WatchFile = r#"version=4
2656https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2657"#
2658 .parse()
2659 .unwrap();
2660
2661 let mut entry = wf.entries().next().unwrap();
2662
2663 entry.set_opt("compression", "xz");
2664 entry.set_opt("repack", "");
2665 entry.set_opt("dversionmangle", "s/\\+ds//");
2666
2667 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
2668 assert_eq!(
2669 entry.get_option("dversionmangle"),
2670 Some("s/\\+ds//".to_string())
2671 );
2672 }
2673
2674 #[test]
2675 fn test_set_matching_pattern() {
2676 let wf: super::WatchFile = r#"version=4
2678https://github.com/example/tags .*/v?(\d\S+)\.tar\.gz
2679"#
2680 .parse()
2681 .unwrap();
2682
2683 let mut entry = wf.entries().next().unwrap();
2684 assert_eq!(
2685 entry.matching_pattern(),
2686 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2687 );
2688
2689 entry.set_matching_pattern("(?:.*?/)?v?([\\d.]+)\\.tar\\.gz");
2690 assert_eq!(
2691 entry.matching_pattern(),
2692 Some("(?:.*?/)?v?([\\d.]+)\\.tar\\.gz".into())
2693 );
2694
2695 assert_eq!(entry.url(), "https://github.com/example/tags");
2697
2698 assert_eq!(
2700 entry.to_string(),
2701 "https://github.com/example/tags (?:.*?/)?v?([\\d.]+)\\.tar\\.gz\n"
2702 );
2703 }
2704
2705 #[test]
2706 fn test_set_matching_pattern_with_all_fields() {
2707 let wf: super::WatchFile = r#"version=4
2709opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2710"#
2711 .parse()
2712 .unwrap();
2713
2714 let mut entry = wf.entries().next().unwrap();
2715 assert_eq!(
2716 entry.matching_pattern(),
2717 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2718 );
2719
2720 entry.set_matching_pattern(".*/version-([\\d.]+)\\.tar\\.xz");
2721 assert_eq!(
2722 entry.matching_pattern(),
2723 Some(".*/version-([\\d.]+)\\.tar\\.xz".into())
2724 );
2725
2726 assert_eq!(entry.url(), "https://example.com/releases");
2728 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2729 assert_eq!(entry.script(), Some("uupdate".into()));
2730 assert_eq!(entry.compression(), Ok(Some(super::Compression::Xz)));
2731
2732 assert_eq!(
2734 entry.to_string(),
2735 "opts=compression=xz https://example.com/releases .*/version-([\\d.]+)\\.tar\\.xz debian uupdate\n"
2736 );
2737 }
2738
2739 #[test]
2740 fn test_set_version_policy() {
2741 let wf: super::WatchFile = r#"version=4
2743https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2744"#
2745 .parse()
2746 .unwrap();
2747
2748 let mut entry = wf.entries().next().unwrap();
2749 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2750
2751 entry.set_version_policy("previous");
2752 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Previous)));
2753
2754 assert_eq!(entry.url(), "https://example.com/releases");
2756 assert_eq!(
2757 entry.matching_pattern(),
2758 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2759 );
2760 assert_eq!(entry.script(), Some("uupdate".into()));
2761
2762 assert_eq!(
2764 entry.to_string(),
2765 "https://example.com/releases (?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz previous uupdate\n"
2766 );
2767 }
2768
2769 #[test]
2770 fn test_set_version_policy_with_options() {
2771 let wf: super::WatchFile = r#"version=4
2773opts=repack,compression=xz \
2774 https://github.com/example/example-cat/tags \
2775 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2776"#
2777 .parse()
2778 .unwrap();
2779
2780 let mut entry = wf.entries().next().unwrap();
2781 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2782
2783 entry.set_version_policy("ignore");
2784 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Ignore)));
2785
2786 assert_eq!(entry.url(), "https://github.com/example/example-cat/tags");
2788 assert_eq!(
2789 entry.matching_pattern(),
2790 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2791 );
2792 assert_eq!(entry.script(), Some("uupdate".into()));
2793 assert!(entry.repack());
2794
2795 assert_eq!(
2797 entry.to_string(),
2798 r#"opts=repack,compression=xz \
2799 https://github.com/example/example-cat/tags \
2800 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz ignore uupdate
2801"#
2802 );
2803 }
2804
2805 #[test]
2806 fn test_set_script() {
2807 let wf: super::WatchFile = r#"version=4
2809https://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("uscan");
2818 assert_eq!(entry.script(), Some("uscan".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
2828 assert_eq!(
2830 entry.to_string(),
2831 "https://example.com/releases (?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz debian uscan\n"
2832 );
2833 }
2834
2835 #[test]
2836 fn test_set_script_with_options() {
2837 let wf: super::WatchFile = r#"version=4
2839opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2840"#
2841 .parse()
2842 .unwrap();
2843
2844 let mut entry = wf.entries().next().unwrap();
2845 assert_eq!(entry.script(), Some("uupdate".into()));
2846
2847 entry.set_script("custom-script.sh");
2848 assert_eq!(entry.script(), Some("custom-script.sh".into()));
2849
2850 assert_eq!(entry.url(), "https://example.com/releases");
2852 assert_eq!(
2853 entry.matching_pattern(),
2854 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2855 );
2856 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2857 assert_eq!(entry.compression(), Ok(Some(super::Compression::Xz)));
2858
2859 assert_eq!(
2861 entry.to_string(),
2862 "opts=compression=xz https://example.com/releases (?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz debian custom-script.sh\n"
2863 );
2864 }
2865
2866 #[test]
2867 fn test_apply_dversionmangle() {
2868 let wf: super::WatchFile = r#"version=4
2870opts=dversionmangle=s/\+dfsg$// https://example.com/ .*
2871"#
2872 .parse()
2873 .unwrap();
2874 let entry = wf.entries().next().unwrap();
2875 assert_eq!(entry.apply_dversionmangle("1.0+dfsg").unwrap(), "1.0");
2876 assert_eq!(entry.apply_dversionmangle("1.0").unwrap(), "1.0");
2877
2878 let wf: super::WatchFile = r#"version=4
2880opts=versionmangle=s/^v// https://example.com/ .*
2881"#
2882 .parse()
2883 .unwrap();
2884 let entry = wf.entries().next().unwrap();
2885 assert_eq!(entry.apply_dversionmangle("v1.0").unwrap(), "1.0");
2886
2887 let wf: super::WatchFile = r#"version=4
2889opts=dversionmangle=s/\+ds//,versionmangle=s/^v// https://example.com/ .*
2890"#
2891 .parse()
2892 .unwrap();
2893 let entry = wf.entries().next().unwrap();
2894 assert_eq!(entry.apply_dversionmangle("1.0+ds").unwrap(), "1.0");
2895
2896 let wf: super::WatchFile = r#"version=4
2898https://example.com/ .*
2899"#
2900 .parse()
2901 .unwrap();
2902 let entry = wf.entries().next().unwrap();
2903 assert_eq!(entry.apply_dversionmangle("1.0+dfsg").unwrap(), "1.0+dfsg");
2904 }
2905
2906 #[test]
2907 fn test_apply_oversionmangle() {
2908 let wf: super::WatchFile = r#"version=4
2910opts=oversionmangle=s/$/-1/ https://example.com/ .*
2911"#
2912 .parse()
2913 .unwrap();
2914 let entry = wf.entries().next().unwrap();
2915 assert_eq!(entry.apply_oversionmangle("1.0").unwrap(), "1.0-1");
2916 assert_eq!(entry.apply_oversionmangle("2.5.3").unwrap(), "2.5.3-1");
2917
2918 let wf: super::WatchFile = r#"version=4
2920opts=oversionmangle=s/$/.dfsg/ https://example.com/ .*
2921"#
2922 .parse()
2923 .unwrap();
2924 let entry = wf.entries().next().unwrap();
2925 assert_eq!(entry.apply_oversionmangle("1.0").unwrap(), "1.0.dfsg");
2926
2927 let wf: super::WatchFile = r#"version=4
2929https://example.com/ .*
2930"#
2931 .parse()
2932 .unwrap();
2933 let entry = wf.entries().next().unwrap();
2934 assert_eq!(entry.apply_oversionmangle("1.0").unwrap(), "1.0");
2935 }
2936
2937 #[test]
2938 fn test_apply_dirversionmangle() {
2939 let wf: super::WatchFile = r#"version=4
2941opts=dirversionmangle=s/^v// https://example.com/ .*
2942"#
2943 .parse()
2944 .unwrap();
2945 let entry = wf.entries().next().unwrap();
2946 assert_eq!(entry.apply_dirversionmangle("v1.0").unwrap(), "1.0");
2947 assert_eq!(entry.apply_dirversionmangle("v2.5.3").unwrap(), "2.5.3");
2948
2949 let wf: super::WatchFile = r#"version=4
2951opts=dirversionmangle=s/v(\d)/$1/ https://example.com/ .*
2952"#
2953 .parse()
2954 .unwrap();
2955 let entry = wf.entries().next().unwrap();
2956 assert_eq!(entry.apply_dirversionmangle("v1.0").unwrap(), "1.0");
2957
2958 let wf: super::WatchFile = r#"version=4
2960https://example.com/ .*
2961"#
2962 .parse()
2963 .unwrap();
2964 let entry = wf.entries().next().unwrap();
2965 assert_eq!(entry.apply_dirversionmangle("v1.0").unwrap(), "v1.0");
2966 }
2967
2968 #[test]
2969 fn test_apply_filenamemangle() {
2970 let wf: super::WatchFile = r#"version=4
2972opts=filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/mypackage-$1.tar.gz/ https://example.com/ .*
2973"#
2974 .parse()
2975 .unwrap();
2976 let entry = wf.entries().next().unwrap();
2977 assert_eq!(
2978 entry
2979 .apply_filenamemangle("https://example.com/v1.0.tar.gz")
2980 .unwrap(),
2981 "mypackage-1.0.tar.gz"
2982 );
2983 assert_eq!(
2984 entry
2985 .apply_filenamemangle("https://example.com/2.5.3.tar.gz")
2986 .unwrap(),
2987 "mypackage-2.5.3.tar.gz"
2988 );
2989
2990 let wf: super::WatchFile = r#"version=4
2992opts=filenamemangle=s/.*\/(.*)/$1/ https://example.com/ .*
2993"#
2994 .parse()
2995 .unwrap();
2996 let entry = wf.entries().next().unwrap();
2997 assert_eq!(
2998 entry
2999 .apply_filenamemangle("https://example.com/path/to/file.tar.gz")
3000 .unwrap(),
3001 "file.tar.gz"
3002 );
3003
3004 let wf: super::WatchFile = r#"version=4
3006https://example.com/ .*
3007"#
3008 .parse()
3009 .unwrap();
3010 let entry = wf.entries().next().unwrap();
3011 assert_eq!(
3012 entry
3013 .apply_filenamemangle("https://example.com/file.tar.gz")
3014 .unwrap(),
3015 "https://example.com/file.tar.gz"
3016 );
3017 }
3018
3019 #[test]
3020 fn test_apply_pagemangle() {
3021 let wf: super::WatchFile = r#"version=4
3023opts=pagemangle=s/&/&/g https://example.com/ .*
3024"#
3025 .parse()
3026 .unwrap();
3027 let entry = wf.entries().next().unwrap();
3028 assert_eq!(
3029 entry.apply_pagemangle(b"foo & bar").unwrap(),
3030 b"foo & bar"
3031 );
3032 assert_eq!(
3033 entry
3034 .apply_pagemangle(b"& foo & bar &")
3035 .unwrap(),
3036 b"& foo & bar &"
3037 );
3038
3039 let wf: super::WatchFile = r#"version=4
3041opts=pagemangle=s/<[^>]+>//g https://example.com/ .*
3042"#
3043 .parse()
3044 .unwrap();
3045 let entry = wf.entries().next().unwrap();
3046 assert_eq!(entry.apply_pagemangle(b"<div>text</div>").unwrap(), b"text");
3047
3048 let wf: super::WatchFile = r#"version=4
3050https://example.com/ .*
3051"#
3052 .parse()
3053 .unwrap();
3054 let entry = wf.entries().next().unwrap();
3055 assert_eq!(
3056 entry.apply_pagemangle(b"foo & bar").unwrap(),
3057 b"foo & bar"
3058 );
3059 }
3060
3061 #[test]
3062 fn test_apply_downloadurlmangle() {
3063 let wf: super::WatchFile = r#"version=4
3065opts=downloadurlmangle=s|/archive/|/download/| https://example.com/ .*
3066"#
3067 .parse()
3068 .unwrap();
3069 let entry = wf.entries().next().unwrap();
3070 assert_eq!(
3071 entry
3072 .apply_downloadurlmangle("https://example.com/archive/file.tar.gz")
3073 .unwrap(),
3074 "https://example.com/download/file.tar.gz"
3075 );
3076
3077 let wf: super::WatchFile = r#"version=4
3079opts=downloadurlmangle=s/github\.com/raw.githubusercontent.com/ https://example.com/ .*
3080"#
3081 .parse()
3082 .unwrap();
3083 let entry = wf.entries().next().unwrap();
3084 assert_eq!(
3085 entry
3086 .apply_downloadurlmangle("https://github.com/user/repo/file.tar.gz")
3087 .unwrap(),
3088 "https://raw.githubusercontent.com/user/repo/file.tar.gz"
3089 );
3090
3091 let wf: super::WatchFile = r#"version=4
3093https://example.com/ .*
3094"#
3095 .parse()
3096 .unwrap();
3097 let entry = wf.entries().next().unwrap();
3098 assert_eq!(
3099 entry
3100 .apply_downloadurlmangle("https://example.com/archive/file.tar.gz")
3101 .unwrap(),
3102 "https://example.com/archive/file.tar.gz"
3103 );
3104 }
3105
3106 #[test]
3107 fn test_entry_builder_minimal() {
3108 let entry = super::EntryBuilder::new("https://github.com/example/tags")
3110 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3111 .build();
3112
3113 assert_eq!(entry.url(), "https://github.com/example/tags");
3114 assert_eq!(
3115 entry.matching_pattern().as_deref(),
3116 Some(".*/v?(\\d\\S+)\\.tar\\.gz")
3117 );
3118 assert_eq!(entry.version(), Ok(None));
3119 assert_eq!(entry.script(), None);
3120 assert!(entry.opts().is_empty());
3121 }
3122
3123 #[test]
3124 fn test_entry_builder_url_only() {
3125 let entry = super::EntryBuilder::new("https://example.com/releases").build();
3127
3128 assert_eq!(entry.url(), "https://example.com/releases");
3129 assert_eq!(entry.matching_pattern(), None);
3130 assert_eq!(entry.version(), Ok(None));
3131 assert_eq!(entry.script(), None);
3132 assert!(entry.opts().is_empty());
3133 }
3134
3135 #[test]
3136 fn test_entry_builder_with_all_fields() {
3137 let entry = super::EntryBuilder::new("https://github.com/example/tags")
3139 .matching_pattern(".*/v?(\\d[\\d.]*)\\.tar\\.gz")
3140 .version_policy("debian")
3141 .script("uupdate")
3142 .opt("compression", "xz")
3143 .flag("repack")
3144 .build();
3145
3146 assert_eq!(entry.url(), "https://github.com/example/tags");
3147 assert_eq!(
3148 entry.matching_pattern().as_deref(),
3149 Some(".*/v?(\\d[\\d.]*)\\.tar\\.gz")
3150 );
3151 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
3152 assert_eq!(entry.script(), Some("uupdate".into()));
3153 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
3154 assert!(entry.has_option("repack"));
3155 assert!(entry.repack());
3156 }
3157
3158 #[test]
3159 fn test_entry_builder_multiple_options() {
3160 let entry = super::EntryBuilder::new("https://example.com/tags")
3162 .matching_pattern(".*/v?(\\d+\\.\\d+)\\.tar\\.gz")
3163 .opt("compression", "xz")
3164 .opt("dversionmangle", "s/\\+ds//")
3165 .opt("repacksuffix", "+ds")
3166 .build();
3167
3168 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
3169 assert_eq!(
3170 entry.get_option("dversionmangle"),
3171 Some("s/\\+ds//".to_string())
3172 );
3173 assert_eq!(entry.get_option("repacksuffix"), Some("+ds".to_string()));
3174 }
3175
3176 #[test]
3177 fn test_entry_builder_via_entry() {
3178 let entry = super::Entry::builder("https://github.com/example/tags")
3180 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3181 .version_policy("debian")
3182 .build();
3183
3184 assert_eq!(entry.url(), "https://github.com/example/tags");
3185 assert_eq!(
3186 entry.matching_pattern().as_deref(),
3187 Some(".*/v?(\\d\\S+)\\.tar\\.gz")
3188 );
3189 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
3190 }
3191
3192 #[test]
3193 fn test_watchfile_add_entry_to_empty() {
3194 let mut wf = super::WatchFile::new(Some(4));
3196
3197 let entry = super::EntryBuilder::new("https://github.com/example/tags")
3198 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3199 .build();
3200
3201 wf.add_entry(entry);
3202
3203 assert_eq!(wf.version(), 4);
3204 assert_eq!(wf.entries().count(), 1);
3205
3206 let added_entry = wf.entries().next().unwrap();
3207 assert_eq!(added_entry.url(), "https://github.com/example/tags");
3208 assert_eq!(
3209 added_entry.matching_pattern().as_deref(),
3210 Some(".*/v?(\\d\\S+)\\.tar\\.gz")
3211 );
3212 }
3213
3214 #[test]
3215 fn test_watchfile_add_multiple_entries() {
3216 let mut wf = super::WatchFile::new(Some(4));
3218
3219 wf.add_entry(
3220 super::EntryBuilder::new("https://github.com/example1/tags")
3221 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3222 .build(),
3223 );
3224
3225 wf.add_entry(
3226 super::EntryBuilder::new("https://github.com/example2/releases")
3227 .matching_pattern(".*/(\\d+\\.\\d+)\\.tar\\.gz")
3228 .opt("compression", "xz")
3229 .build(),
3230 );
3231
3232 assert_eq!(wf.entries().count(), 2);
3233
3234 let entries: Vec<_> = wf.entries().collect();
3235 assert_eq!(entries[0].url(), "https://github.com/example1/tags");
3236 assert_eq!(entries[1].url(), "https://github.com/example2/releases");
3237 assert_eq!(entries[1].get_option("compression"), Some("xz".to_string()));
3238 }
3239
3240 #[test]
3241 fn test_watchfile_add_entry_to_existing() {
3242 let mut wf: super::WatchFile = r#"version=4
3244https://example.com/old .*/v?(\\d\\S+)\\.tar\\.gz
3245"#
3246 .parse()
3247 .unwrap();
3248
3249 assert_eq!(wf.entries().count(), 1);
3250
3251 wf.add_entry(
3252 super::EntryBuilder::new("https://github.com/example/new")
3253 .matching_pattern(".*/v?(\\d+\\.\\d+)\\.tar\\.gz")
3254 .opt("compression", "xz")
3255 .version_policy("debian")
3256 .build(),
3257 );
3258
3259 assert_eq!(wf.entries().count(), 2);
3260
3261 let entries: Vec<_> = wf.entries().collect();
3262 assert_eq!(entries[0].url(), "https://example.com/old");
3263 assert_eq!(entries[1].url(), "https://github.com/example/new");
3264 assert_eq!(entries[1].version(), Ok(Some(VersionPolicy::Debian)));
3265 }
3266
3267 #[test]
3268 fn test_entry_builder_formatting() {
3269 let entry = super::EntryBuilder::new("https://github.com/example/tags")
3271 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3272 .opt("compression", "xz")
3273 .flag("repack")
3274 .version_policy("debian")
3275 .script("uupdate")
3276 .build();
3277
3278 let entry_str = entry.to_string();
3279
3280 assert!(entry_str.starts_with("opts="));
3282 assert!(entry_str.contains("https://github.com/example/tags"));
3284 assert!(entry_str.contains(".*/v?(\\d\\S+)\\.tar\\.gz"));
3286 assert!(entry_str.contains("debian"));
3288 assert!(entry_str.contains("uupdate"));
3290 assert!(entry_str.ends_with('\n'));
3292 }
3293
3294 #[test]
3295 fn test_watchfile_add_entry_preserves_format() {
3296 let mut wf = super::WatchFile::new(Some(4));
3298
3299 wf.add_entry(
3300 super::EntryBuilder::new("https://github.com/example/tags")
3301 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3302 .build(),
3303 );
3304
3305 let wf_str = wf.to_string();
3306
3307 assert!(wf_str.starts_with("version=4\n"));
3309 assert!(wf_str.contains("https://github.com/example/tags"));
3311
3312 let reparsed: super::WatchFile = wf_str.parse().unwrap();
3314 assert_eq!(reparsed.version(), 4);
3315 assert_eq!(reparsed.entries().count(), 1);
3316 }
3317
3318 #[test]
3319 fn test_line_col() {
3320 let text = r#"version=4
3321opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
3322"#;
3323 let wf = text.parse::<super::WatchFile>().unwrap();
3324
3325 let version_node = wf.version_node().unwrap();
3327 assert_eq!(version_node.line(), 0);
3328 assert_eq!(version_node.column(), 0);
3329 assert_eq!(version_node.line_col(), (0, 0));
3330
3331 let entries: Vec<_> = wf.entries().collect();
3333 assert_eq!(entries.len(), 1);
3334
3335 assert_eq!(entries[0].line(), 1);
3337 assert_eq!(entries[0].column(), 0);
3338 assert_eq!(entries[0].line_col(), (1, 0));
3339
3340 let option_list = entries[0].option_list().unwrap();
3342 assert_eq!(option_list.line(), 1); let url_node = entries[0].url_node().unwrap();
3345 assert_eq!(url_node.line(), 1); let pattern_node = entries[0].matching_pattern_node().unwrap();
3348 assert_eq!(pattern_node.line(), 1); let version_policy_node = entries[0].version_node().unwrap();
3351 assert_eq!(version_policy_node.line(), 1); let script_node = entries[0].script_node().unwrap();
3354 assert_eq!(script_node.line(), 1); let options: Vec<_> = option_list.options().collect();
3358 assert_eq!(options.len(), 1);
3359 assert_eq!(options[0].key(), Some("compression".to_string()));
3360 assert_eq!(options[0].value(), Some("xz".to_string()));
3361 assert_eq!(options[0].line(), 1); let compression_opt = option_list.find_option("compression").unwrap();
3365 assert_eq!(compression_opt.line(), 1);
3366 assert_eq!(compression_opt.column(), 5); assert_eq!(compression_opt.line_col(), (1, 5));
3368 }
3369
3370 #[test]
3371 fn test_parse_str_relaxed() {
3372 let wf: super::WatchFile = super::WatchFile::from_str_relaxed(
3373 r#"version=4
3374ERRORS IN THIS LINE
3375opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d
3376"#,
3377 );
3378 assert_eq!(wf.version(), 4);
3379 assert_eq!(wf.entries().count(), 2);
3380
3381 let entries = wf.entries().collect::<Vec<_>>();
3382
3383 let entry = &entries[0];
3384 assert_eq!(entry.url(), "ERRORS");
3385
3386 let entry = &entries[1];
3387 assert_eq!(entry.url(), "https://example.com/releases");
3388 assert_eq!(entry.matching_pattern().as_deref(), Some("(?:.*?/)?v?(\\d"));
3389 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
3390 }
3391}