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
613impl FromStr for WatchFile {
614 type Err = ParseError;
615
616 fn from_str(s: &str) -> Result<Self, Self::Err> {
617 let parsed = parse(s);
618 if parsed.errors.is_empty() {
619 Ok(parsed.root())
620 } else {
621 Err(ParseError(parsed.errors))
622 }
623 }
624}
625
626impl Version {
627 pub fn version(&self) -> u32 {
629 self.0
630 .children_with_tokens()
631 .find_map(|it| match it {
632 SyntaxElement::Token(token) => {
633 if token.kind() == VALUE {
634 Some(token.text().parse().unwrap())
635 } else {
636 None
637 }
638 }
639 _ => None,
640 })
641 .unwrap_or(DEFAULT_VERSION)
642 }
643
644 pub fn from_reader<R: std::io::Read>(reader: R) -> Result<WatchFile, ParseError> {
646 let mut buf_reader = std::io::BufReader::new(reader);
647 let mut content = String::new();
648 buf_reader
649 .read_to_string(&mut content)
650 .map_err(|e| ParseError(vec![e.to_string()]))?;
651 content.parse()
652 }
653
654 pub fn from_reader_relaxed<R: std::io::Read>(
656 mut r: R,
657 ) -> Result<(Self, Vec<String>), std::io::Error> {
658 let mut content = String::new();
659 r.read_to_string(&mut content)?;
660 let parsed = parse(&content);
661 Ok((parsed.root().version_node().unwrap(), parsed.errors))
662 }
663
664 pub fn from_str_relaxed(s: &str) -> (Self, Vec<String>) {
666 let parsed = parse(s);
667 (parsed.root().version_node().unwrap(), parsed.errors)
668 }
669}
670
671#[derive(Debug, Clone, Default)]
695pub struct EntryBuilder {
696 url: Option<String>,
697 matching_pattern: Option<String>,
698 version_policy: Option<String>,
699 script: Option<String>,
700 opts: std::collections::HashMap<String, String>,
701}
702
703impl EntryBuilder {
704 pub fn new(url: impl Into<String>) -> Self {
706 EntryBuilder {
707 url: Some(url.into()),
708 matching_pattern: None,
709 version_policy: None,
710 script: None,
711 opts: std::collections::HashMap::new(),
712 }
713 }
714
715 pub fn matching_pattern(mut self, pattern: impl Into<String>) -> Self {
717 self.matching_pattern = Some(pattern.into());
718 self
719 }
720
721 pub fn version_policy(mut self, policy: impl Into<String>) -> Self {
723 self.version_policy = Some(policy.into());
724 self
725 }
726
727 pub fn script(mut self, script: impl Into<String>) -> Self {
729 self.script = Some(script.into());
730 self
731 }
732
733 pub fn opt(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
735 self.opts.insert(key.into(), value.into());
736 self
737 }
738
739 pub fn flag(mut self, key: impl Into<String>) -> Self {
743 self.opts.insert(key.into(), String::new());
744 self
745 }
746
747 pub fn build(self) -> Entry {
753 let url = self.url.expect("URL is required for entry");
754
755 let mut builder = GreenNodeBuilder::new();
756
757 builder.start_node(ENTRY.into());
758
759 if !self.opts.is_empty() {
761 builder.start_node(OPTS_LIST.into());
762 builder.token(KEY.into(), "opts");
763 builder.token(EQUALS.into(), "=");
764
765 let mut first = true;
766 for (key, value) in self.opts.iter() {
767 if !first {
768 builder.token(COMMA.into(), ",");
769 }
770 first = false;
771
772 builder.start_node(OPTION.into());
773 builder.token(KEY.into(), key);
774 if !value.is_empty() {
775 builder.token(EQUALS.into(), "=");
776 builder.token(VALUE.into(), value);
777 }
778 builder.finish_node();
779 }
780
781 builder.finish_node();
782 builder.token(WHITESPACE.into(), " ");
783 }
784
785 builder.start_node(URL.into());
787 builder.token(VALUE.into(), &url);
788 builder.finish_node();
789
790 if let Some(pattern) = self.matching_pattern {
792 builder.token(WHITESPACE.into(), " ");
793 builder.start_node(MATCHING_PATTERN.into());
794 builder.token(VALUE.into(), &pattern);
795 builder.finish_node();
796 }
797
798 if let Some(policy) = self.version_policy {
800 builder.token(WHITESPACE.into(), " ");
801 builder.start_node(VERSION_POLICY.into());
802 builder.token(VALUE.into(), &policy);
803 builder.finish_node();
804 }
805
806 if let Some(script_val) = self.script {
808 builder.token(WHITESPACE.into(), " ");
809 builder.start_node(SCRIPT.into());
810 builder.token(VALUE.into(), &script_val);
811 builder.finish_node();
812 }
813
814 builder.token(NEWLINE.into(), "\n");
815 builder.finish_node();
816
817 Entry(SyntaxNode::new_root_mut(builder.finish()))
818 }
819}
820
821impl Entry {
822 pub fn builder(url: impl Into<String>) -> EntryBuilder {
836 EntryBuilder::new(url)
837 }
838
839 pub fn option_list(&self) -> Option<OptionList> {
841 self.0.children().find_map(OptionList::cast)
842 }
843
844 pub fn get_option(&self, key: &str) -> Option<String> {
846 self.option_list().and_then(|ol| ol.get_option(key))
847 }
848
849 pub fn has_option(&self, key: &str) -> bool {
851 self.option_list().map_or(false, |ol| ol.has_option(key))
852 }
853
854 pub fn component(&self) -> Option<String> {
856 self.get_option("component")
857 }
858
859 pub fn ctype(&self) -> Result<Option<ComponentType>, ()> {
861 self.get_option("ctype").map(|s| s.parse()).transpose()
862 }
863
864 pub fn compression(&self) -> Result<Option<Compression>, ()> {
866 self.get_option("compression")
867 .map(|s| s.parse())
868 .transpose()
869 }
870
871 pub fn repack(&self) -> bool {
873 self.has_option("repack")
874 }
875
876 pub fn repacksuffix(&self) -> Option<String> {
878 self.get_option("repacksuffix")
879 }
880
881 pub fn mode(&self) -> Result<Mode, ()> {
883 Ok(self
884 .get_option("mode")
885 .map(|s| s.parse())
886 .transpose()?
887 .unwrap_or_default())
888 }
889
890 pub fn pretty(&self) -> Result<Pretty, ()> {
892 Ok(self
893 .get_option("pretty")
894 .map(|s| s.parse())
895 .transpose()?
896 .unwrap_or_default())
897 }
898
899 pub fn date(&self) -> String {
902 self.get_option("date")
903 .unwrap_or_else(|| "%Y%m%d".to_string())
904 }
905
906 pub fn gitexport(&self) -> Result<GitExport, ()> {
908 Ok(self
909 .get_option("gitexport")
910 .map(|s| s.parse())
911 .transpose()?
912 .unwrap_or_default())
913 }
914
915 pub fn gitmode(&self) -> Result<GitMode, ()> {
917 Ok(self
918 .get_option("gitmode")
919 .map(|s| s.parse())
920 .transpose()?
921 .unwrap_or_default())
922 }
923
924 pub fn pgpmode(&self) -> Result<PgpMode, ()> {
926 Ok(self
927 .get_option("pgpmode")
928 .map(|s| s.parse())
929 .transpose()?
930 .unwrap_or_default())
931 }
932
933 pub fn searchmode(&self) -> Result<SearchMode, ()> {
935 Ok(self
936 .get_option("searchmode")
937 .map(|s| s.parse())
938 .transpose()?
939 .unwrap_or_default())
940 }
941
942 pub fn decompress(&self) -> bool {
944 self.has_option("decompress")
945 }
946
947 pub fn bare(&self) -> bool {
950 self.has_option("bare")
951 }
952
953 pub fn user_agent(&self) -> Option<String> {
955 self.get_option("user-agent")
956 }
957
958 pub fn passive(&self) -> Option<bool> {
960 if self.has_option("passive") || self.has_option("pasv") {
961 Some(true)
962 } else if self.has_option("active") || self.has_option("nopasv") {
963 Some(false)
964 } else {
965 None
966 }
967 }
968
969 pub fn unzipoptions(&self) -> Option<String> {
972 self.get_option("unzipopt")
973 }
974
975 pub fn dversionmangle(&self) -> Option<String> {
977 self.get_option("dversionmangle")
978 .or_else(|| self.get_option("versionmangle"))
979 }
980
981 pub fn dirversionmangle(&self) -> Option<String> {
985 self.get_option("dirversionmangle")
986 }
987
988 pub fn pagemangle(&self) -> Option<String> {
990 self.get_option("pagemangle")
991 }
992
993 pub fn uversionmangle(&self) -> Option<String> {
997 self.get_option("uversionmangle")
998 .or_else(|| self.get_option("versionmangle"))
999 }
1000
1001 pub fn versionmangle(&self) -> Option<String> {
1003 self.get_option("versionmangle")
1004 }
1005
1006 pub fn hrefdecode(&self) -> bool {
1011 self.get_option("hrefdecode").is_some()
1012 }
1013
1014 pub fn downloadurlmangle(&self) -> Option<String> {
1017 self.get_option("downloadurlmangle")
1018 }
1019
1020 pub fn filenamemangle(&self) -> Option<String> {
1028 self.get_option("filenamemangle")
1029 }
1030
1031 pub fn pgpsigurlmangle(&self) -> Option<String> {
1033 self.get_option("pgpsigurlmangle")
1034 }
1035
1036 pub fn oversionmangle(&self) -> Option<String> {
1039 self.get_option("oversionmangle")
1040 }
1041
1042 pub fn apply_uversionmangle(
1055 &self,
1056 version: &str,
1057 ) -> Result<String, crate::mangle::MangleError> {
1058 if let Some(vm) = self.uversionmangle() {
1059 crate::mangle::apply_mangle(&vm, version)
1060 } else {
1061 Ok(version.to_string())
1062 }
1063 }
1064
1065 pub fn apply_dversionmangle(
1078 &self,
1079 version: &str,
1080 ) -> Result<String, crate::mangle::MangleError> {
1081 if let Some(vm) = self.dversionmangle() {
1082 crate::mangle::apply_mangle(&vm, version)
1083 } else {
1084 Ok(version.to_string())
1085 }
1086 }
1087
1088 pub fn apply_oversionmangle(
1101 &self,
1102 version: &str,
1103 ) -> Result<String, crate::mangle::MangleError> {
1104 if let Some(vm) = self.oversionmangle() {
1105 crate::mangle::apply_mangle(&vm, version)
1106 } else {
1107 Ok(version.to_string())
1108 }
1109 }
1110
1111 pub fn apply_dirversionmangle(
1124 &self,
1125 version: &str,
1126 ) -> Result<String, crate::mangle::MangleError> {
1127 if let Some(vm) = self.dirversionmangle() {
1128 crate::mangle::apply_mangle(&vm, version)
1129 } else {
1130 Ok(version.to_string())
1131 }
1132 }
1133
1134 pub fn apply_filenamemangle(&self, url: &str) -> Result<String, crate::mangle::MangleError> {
1150 if let Some(vm) = self.filenamemangle() {
1151 crate::mangle::apply_mangle(&vm, url)
1152 } else {
1153 Ok(url.to_string())
1154 }
1155 }
1156
1157 pub fn apply_pagemangle(&self, page: &[u8]) -> Result<Vec<u8>, crate::mangle::MangleError> {
1173 if let Some(vm) = self.pagemangle() {
1174 let page_str = String::from_utf8_lossy(page);
1175 let mangled = crate::mangle::apply_mangle(&vm, &page_str)?;
1176 Ok(mangled.into_bytes())
1177 } else {
1178 Ok(page.to_vec())
1179 }
1180 }
1181
1182 pub fn apply_downloadurlmangle(&self, url: &str) -> Result<String, crate::mangle::MangleError> {
1198 if let Some(vm) = self.downloadurlmangle() {
1199 crate::mangle::apply_mangle(&vm, url)
1200 } else {
1201 Ok(url.to_string())
1202 }
1203 }
1204
1205 #[cfg(feature = "discover")]
1226 pub async fn discover(
1227 &self,
1228 package: impl FnOnce() -> String,
1229 ) -> Result<Vec<crate::Release>, Box<dyn std::error::Error>> {
1230 let url = self.format_url(package);
1231 let user_agent = self
1232 .user_agent()
1233 .unwrap_or_else(|| crate::DEFAULT_USER_AGENT.to_string());
1234 let searchmode = self.searchmode().unwrap_or(crate::SearchMode::Html);
1235
1236 let client = reqwest::Client::builder().user_agent(user_agent).build()?;
1237
1238 let response = client.get(url.as_str()).send().await?;
1239 let body = response.bytes().await?;
1240
1241 let mangled_body = self.apply_pagemangle(&body)?;
1243
1244 let matching_pattern = self
1245 .matching_pattern()
1246 .ok_or("matching_pattern is required")?;
1247
1248 let package_name = String::new(); let results = crate::search::search(
1250 match searchmode {
1251 crate::SearchMode::Html => "html",
1252 crate::SearchMode::Plain => "plain",
1253 },
1254 std::io::Cursor::new(mangled_body.as_ref() as &[u8]),
1255 &subst(&matching_pattern, || package_name.clone()),
1256 &package_name,
1257 url.as_str(),
1258 )?;
1259
1260 let mut releases = Vec::new();
1261 for (version, full_url) in results {
1262 let mangled_version = self.apply_uversionmangle(&version)?;
1264
1265 let mangled_url = self.apply_downloadurlmangle(&full_url)?;
1267
1268 let pgpsigurl = if let Some(mangle) = self.pgpsigurlmangle() {
1270 Some(crate::mangle::apply_mangle(&mangle, &mangled_url)?)
1271 } else {
1272 None
1273 };
1274
1275 let target_filename = if self.filenamemangle().is_some() {
1277 Some(self.apply_filenamemangle(&mangled_url)?)
1278 } else {
1279 None
1280 };
1281
1282 let package_version = if self.oversionmangle().is_some() {
1284 Some(self.apply_oversionmangle(&mangled_version)?)
1285 } else {
1286 None
1287 };
1288
1289 releases.push(crate::Release::new_full(
1290 mangled_version,
1291 mangled_url,
1292 pgpsigurl,
1293 target_filename,
1294 package_version,
1295 ));
1296 }
1297
1298 Ok(releases)
1299 }
1300
1301 #[cfg(all(feature = "discover", feature = "blocking"))]
1320 pub fn discover_blocking(
1321 &self,
1322 package: impl FnOnce() -> String,
1323 ) -> Result<Vec<crate::Release>, Box<dyn std::error::Error>> {
1324 let url = self.format_url(package);
1325 let user_agent = self
1326 .user_agent()
1327 .unwrap_or_else(|| crate::DEFAULT_USER_AGENT.to_string());
1328 let searchmode = self.searchmode().unwrap_or(crate::SearchMode::Html);
1329
1330 let client = reqwest::blocking::Client::builder()
1331 .user_agent(user_agent)
1332 .build()?;
1333
1334 let response = client.get(url.as_str()).send()?;
1335 let body = response.bytes()?;
1336
1337 let mangled_body = self.apply_pagemangle(&body)?;
1339
1340 let matching_pattern = self
1341 .matching_pattern()
1342 .ok_or("matching_pattern is required")?;
1343
1344 let package_name = String::new(); let results = crate::search::search(
1346 match searchmode {
1347 crate::SearchMode::Html => "html",
1348 crate::SearchMode::Plain => "plain",
1349 },
1350 std::io::Cursor::new(mangled_body.as_ref() as &[u8]),
1351 &subst(&matching_pattern, || package_name.clone()),
1352 &package_name,
1353 url.as_str(),
1354 )?;
1355
1356 let mut releases = Vec::new();
1357 for (version, full_url) in results {
1358 let mangled_version = self.apply_uversionmangle(&version)?;
1360
1361 let mangled_url = self.apply_downloadurlmangle(&full_url)?;
1363
1364 let pgpsigurl = if let Some(mangle) = self.pgpsigurlmangle() {
1366 Some(crate::mangle::apply_mangle(&mangle, &mangled_url)?)
1367 } else {
1368 None
1369 };
1370
1371 let target_filename = if self.filenamemangle().is_some() {
1373 Some(self.apply_filenamemangle(&mangled_url)?)
1374 } else {
1375 None
1376 };
1377
1378 let package_version = if self.oversionmangle().is_some() {
1380 Some(self.apply_oversionmangle(&mangled_version)?)
1381 } else {
1382 None
1383 };
1384
1385 releases.push(crate::Release::new_full(
1386 mangled_version,
1387 mangled_url,
1388 pgpsigurl,
1389 target_filename,
1390 package_version,
1391 ));
1392 }
1393
1394 Ok(releases)
1395 }
1396
1397 pub fn opts(&self) -> std::collections::HashMap<String, String> {
1399 let mut options = std::collections::HashMap::new();
1400
1401 if let Some(ol) = self.option_list() {
1402 for opt in ol.options() {
1403 let key = opt.key();
1404 let value = opt.value();
1405 if let (Some(key), Some(value)) = (key, value) {
1406 options.insert(key.to_string(), value.to_string());
1407 }
1408 }
1409 }
1410
1411 options
1412 }
1413
1414 fn items(&self) -> impl Iterator<Item = String> + '_ {
1415 self.0.children_with_tokens().filter_map(|it| match it {
1416 SyntaxElement::Token(token) => {
1417 if token.kind() == VALUE || token.kind() == KEY {
1418 Some(token.text().to_string())
1419 } else {
1420 None
1421 }
1422 }
1423 SyntaxElement::Node(node) => {
1424 match node.kind() {
1426 URL => Url::cast(node).map(|n| n.url()),
1427 MATCHING_PATTERN => MatchingPattern::cast(node).map(|n| n.pattern()),
1428 VERSION_POLICY => VersionPolicyNode::cast(node).map(|n| n.policy()),
1429 SCRIPT => ScriptNode::cast(node).map(|n| n.script()),
1430 _ => None,
1431 }
1432 }
1433 })
1434 }
1435
1436 pub fn url_node(&self) -> Option<Url> {
1438 self.0.children().find_map(Url::cast)
1439 }
1440
1441 pub fn url(&self) -> String {
1443 self.url_node().map(|it| it.url()).unwrap_or_else(|| {
1444 self.items().next().unwrap()
1446 })
1447 }
1448
1449 pub fn matching_pattern_node(&self) -> Option<MatchingPattern> {
1451 self.0.children().find_map(MatchingPattern::cast)
1452 }
1453
1454 pub fn matching_pattern(&self) -> Option<String> {
1456 self.matching_pattern_node()
1457 .map(|it| it.pattern())
1458 .or_else(|| {
1459 self.items().nth(1)
1461 })
1462 }
1463
1464 pub fn version_node(&self) -> Option<VersionPolicyNode> {
1466 self.0.children().find_map(VersionPolicyNode::cast)
1467 }
1468
1469 pub fn version(&self) -> Result<Option<crate::VersionPolicy>, String> {
1471 self.version_node()
1472 .map(|it| it.policy().parse())
1473 .transpose()
1474 .or_else(|_e| {
1475 self.items().nth(2).map(|it| it.parse()).transpose()
1477 })
1478 }
1479
1480 pub fn script_node(&self) -> Option<ScriptNode> {
1482 self.0.children().find_map(ScriptNode::cast)
1483 }
1484
1485 pub fn script(&self) -> Option<String> {
1487 self.script_node().map(|it| it.script()).or_else(|| {
1488 self.items().nth(3)
1490 })
1491 }
1492
1493 pub fn format_url(&self, package: impl FnOnce() -> String) -> url::Url {
1495 subst(self.url().as_str(), package).parse().unwrap()
1496 }
1497
1498 pub fn set_url(&mut self, new_url: &str) {
1500 let mut builder = GreenNodeBuilder::new();
1502 builder.start_node(URL.into());
1503 builder.token(VALUE.into(), new_url);
1504 builder.finish_node();
1505 let new_url_green = builder.finish();
1506
1507 let new_url_node = SyntaxNode::new_root_mut(new_url_green);
1509
1510 let url_pos = self
1512 .0
1513 .children_with_tokens()
1514 .position(|child| matches!(child, SyntaxElement::Node(node) if node.kind() == URL));
1515
1516 if let Some(pos) = url_pos {
1517 self.0
1519 .splice_children(pos..pos + 1, vec![new_url_node.into()]);
1520 }
1521 }
1522
1523 pub fn set_matching_pattern(&mut self, new_pattern: &str) {
1529 let mut builder = GreenNodeBuilder::new();
1531 builder.start_node(MATCHING_PATTERN.into());
1532 builder.token(VALUE.into(), new_pattern);
1533 builder.finish_node();
1534 let new_pattern_green = builder.finish();
1535
1536 let new_pattern_node = SyntaxNode::new_root_mut(new_pattern_green);
1538
1539 let pattern_pos = self.0.children_with_tokens().position(
1541 |child| matches!(child, SyntaxElement::Node(node) if node.kind() == MATCHING_PATTERN),
1542 );
1543
1544 if let Some(pos) = pattern_pos {
1545 self.0
1547 .splice_children(pos..pos + 1, vec![new_pattern_node.into()]);
1548 }
1549 }
1551
1552 pub fn set_version_policy(&mut self, new_policy: &str) {
1558 let mut builder = GreenNodeBuilder::new();
1560 builder.start_node(VERSION_POLICY.into());
1561 builder.token(VALUE.into(), new_policy);
1563 builder.finish_node();
1564 let new_policy_green = builder.finish();
1565
1566 let new_policy_node = SyntaxNode::new_root_mut(new_policy_green);
1568
1569 let policy_pos = self.0.children_with_tokens().position(
1571 |child| matches!(child, SyntaxElement::Node(node) if node.kind() == VERSION_POLICY),
1572 );
1573
1574 if let Some(pos) = policy_pos {
1575 self.0
1577 .splice_children(pos..pos + 1, vec![new_policy_node.into()]);
1578 }
1579 }
1581
1582 pub fn set_script(&mut self, new_script: &str) {
1588 let mut builder = GreenNodeBuilder::new();
1590 builder.start_node(SCRIPT.into());
1591 builder.token(VALUE.into(), new_script);
1593 builder.finish_node();
1594 let new_script_green = builder.finish();
1595
1596 let new_script_node = SyntaxNode::new_root_mut(new_script_green);
1598
1599 let script_pos = self
1601 .0
1602 .children_with_tokens()
1603 .position(|child| matches!(child, SyntaxElement::Node(node) if node.kind() == SCRIPT));
1604
1605 if let Some(pos) = script_pos {
1606 self.0
1608 .splice_children(pos..pos + 1, vec![new_script_node.into()]);
1609 }
1610 }
1612
1613 pub fn set_opt(&mut self, key: &str, value: &str) {
1619 let opts_pos = self.0.children_with_tokens().position(
1621 |child| matches!(child, SyntaxElement::Node(node) if node.kind() == OPTS_LIST),
1622 );
1623
1624 if let Some(_opts_idx) = opts_pos {
1625 if let Some(mut ol) = self.option_list() {
1626 if let Some(mut opt) = ol.find_option(key) {
1628 opt.set_value(value);
1630 } else {
1632 ol.add_option(key, value);
1634 }
1636 }
1637 } else {
1638 let mut builder = GreenNodeBuilder::new();
1640 builder.start_node(OPTS_LIST.into());
1641 builder.token(KEY.into(), "opts");
1642 builder.token(EQUALS.into(), "=");
1643 builder.start_node(OPTION.into());
1644 builder.token(KEY.into(), key);
1645 builder.token(EQUALS.into(), "=");
1646 builder.token(VALUE.into(), value);
1647 builder.finish_node();
1648 builder.finish_node();
1649 let new_opts_green = builder.finish();
1650 let new_opts_node = SyntaxNode::new_root_mut(new_opts_green);
1651
1652 let url_pos = self
1654 .0
1655 .children_with_tokens()
1656 .position(|child| matches!(child, SyntaxElement::Node(node) if node.kind() == URL));
1657
1658 if let Some(url_idx) = url_pos {
1659 let mut combined_builder = GreenNodeBuilder::new();
1662 combined_builder.start_node(ROOT.into()); combined_builder.token(WHITESPACE.into(), " ");
1664 combined_builder.finish_node();
1665 let temp_green = combined_builder.finish();
1666 let temp_root = SyntaxNode::new_root_mut(temp_green);
1667 let space_element = temp_root.children_with_tokens().next().unwrap();
1668
1669 self.0
1670 .splice_children(url_idx..url_idx, vec![new_opts_node.into(), space_element]);
1671 } else {
1672 self.0.splice_children(0..0, vec![new_opts_node.into()]);
1673 }
1674 }
1675 }
1676
1677 pub fn del_opt(&mut self, key: &str) {
1684 if let Some(mut ol) = self.option_list() {
1685 let option_count = ol.0.children().filter(|n| n.kind() == OPTION).count();
1686
1687 if option_count == 1 && ol.has_option(key) {
1688 let opts_pos = self.0.children().position(|node| node.kind() == OPTS_LIST);
1690
1691 if let Some(opts_idx) = opts_pos {
1692 self.0.splice_children(opts_idx..opts_idx + 1, vec![]);
1694
1695 while self.0.children_with_tokens().next().map_or(false, |e| {
1697 matches!(
1698 e,
1699 SyntaxElement::Token(t) if t.kind() == WHITESPACE || t.kind() == CONTINUATION
1700 )
1701 }) {
1702 self.0.splice_children(0..1, vec![]);
1703 }
1704 }
1705 } else {
1706 ol.remove_option(key);
1708 }
1709 }
1710 }
1711}
1712
1713const SUBSTITUTIONS: &[(&str, &str)] = &[
1714 ("@ANY_VERSION@", r"[-_]?(\d[\-+\.:\~\da-zA-Z]*)"),
1719 (
1722 "@ARCHIVE_EXT@",
1723 r"(?i)\.(?:tar\.xz|tar\.bz2|tar\.gz|zip|tgz|tbz|txz)",
1724 ),
1725 (
1728 "@SIGNATURE_EXT@",
1729 r"(?i)\.(?:tar\.xz|tar\.bz2|tar\.gz|zip|tgz|tbz|txz)\.(?:asc|pgp|gpg|sig|sign)",
1730 ),
1731 ("@DEB_EXT@", r"[\+~](debian|dfsg|ds|deb)(\.)?(\d+)?$"),
1733];
1734
1735pub fn subst(text: &str, package: impl FnOnce() -> String) -> String {
1736 let mut substs = SUBSTITUTIONS.to_vec();
1737 let package_name;
1738 if text.contains("@PACKAGE@") {
1739 package_name = Some(package());
1740 substs.push(("@PACKAGE@", package_name.as_deref().unwrap()));
1741 }
1742
1743 let mut text = text.to_string();
1744
1745 for (k, v) in substs {
1746 text = text.replace(k, v);
1747 }
1748
1749 text
1750}
1751
1752#[test]
1753fn test_subst() {
1754 assert_eq!(
1755 subst("@ANY_VERSION@", || unreachable!()),
1756 r"[-_]?(\d[\-+\.:\~\da-zA-Z]*)"
1757 );
1758 assert_eq!(subst("@PACKAGE@", || "dulwich".to_string()), "dulwich");
1759}
1760
1761impl std::fmt::Debug for OptionList {
1762 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1763 f.debug_struct("OptionList")
1764 .field("text", &self.0.text().to_string())
1765 .finish()
1766 }
1767}
1768
1769impl OptionList {
1770 pub fn options(&self) -> impl Iterator<Item = _Option> + '_ {
1772 self.0.children().filter_map(_Option::cast)
1773 }
1774
1775 pub fn find_option(&self, key: &str) -> Option<_Option> {
1777 self.options().find(|opt| opt.key().as_deref() == Some(key))
1778 }
1779
1780 pub fn has_option(&self, key: &str) -> bool {
1781 self.options().any(|it| it.key().as_deref() == Some(key))
1782 }
1783
1784 pub fn get_option(&self, key: &str) -> Option<String> {
1785 for child in self.options() {
1786 if child.key().as_deref() == Some(key) {
1787 return child.value();
1788 }
1789 }
1790 None
1791 }
1792
1793 fn add_option(&mut self, key: &str, value: &str) {
1795 let option_count = self.0.children().filter(|n| n.kind() == OPTION).count();
1796
1797 let mut builder = GreenNodeBuilder::new();
1799 builder.start_node(ROOT.into()); if option_count > 0 {
1802 builder.start_node(OPTION_SEPARATOR.into());
1803 builder.token(COMMA.into(), ",");
1804 builder.finish_node();
1805 }
1806
1807 builder.start_node(OPTION.into());
1808 builder.token(KEY.into(), key);
1809 builder.token(EQUALS.into(), "=");
1810 builder.token(VALUE.into(), value);
1811 builder.finish_node();
1812
1813 builder.finish_node(); let combined_green = builder.finish();
1815
1816 let temp_root = SyntaxNode::new_root_mut(combined_green);
1818 let new_children: Vec<_> = temp_root.children_with_tokens().collect();
1819
1820 let insert_pos = self.0.children_with_tokens().count();
1821 self.0.splice_children(insert_pos..insert_pos, new_children);
1822 }
1823
1824 fn remove_option(&mut self, key: &str) -> bool {
1826 if let Some(mut opt) = self.find_option(key) {
1827 opt.remove();
1828 true
1829 } else {
1830 false
1831 }
1832 }
1833}
1834
1835impl _Option {
1836 pub fn key(&self) -> Option<String> {
1838 self.0.children_with_tokens().find_map(|it| match it {
1839 SyntaxElement::Token(token) => {
1840 if token.kind() == KEY {
1841 Some(token.text().to_string())
1842 } else {
1843 None
1844 }
1845 }
1846 _ => None,
1847 })
1848 }
1849
1850 pub fn value(&self) -> Option<String> {
1852 self.0
1853 .children_with_tokens()
1854 .filter_map(|it| match it {
1855 SyntaxElement::Token(token) => {
1856 if token.kind() == VALUE || token.kind() == KEY {
1857 Some(token.text().to_string())
1858 } else {
1859 None
1860 }
1861 }
1862 _ => None,
1863 })
1864 .nth(1)
1865 }
1866
1867 pub fn set_value(&mut self, new_value: &str) {
1869 let key = self.key().expect("Option must have a key");
1870
1871 let mut builder = GreenNodeBuilder::new();
1873 builder.start_node(OPTION.into());
1874 builder.token(KEY.into(), &key);
1875 builder.token(EQUALS.into(), "=");
1876 builder.token(VALUE.into(), new_value);
1877 builder.finish_node();
1878 let new_option_green = builder.finish();
1879 let new_option_node = SyntaxNode::new_root_mut(new_option_green);
1880
1881 if let Some(parent) = self.0.parent() {
1883 let idx = self.0.index();
1884 parent.splice_children(idx..idx + 1, vec![new_option_node.into()]);
1885 }
1886 }
1887
1888 pub fn remove(&mut self) {
1890 let next_sep = self
1892 .0
1893 .next_sibling()
1894 .filter(|n| n.kind() == OPTION_SEPARATOR);
1895 let prev_sep = self
1896 .0
1897 .prev_sibling()
1898 .filter(|n| n.kind() == OPTION_SEPARATOR);
1899
1900 if let Some(sep) = next_sep {
1902 sep.detach();
1903 } else if let Some(sep) = prev_sep {
1904 sep.detach();
1905 }
1906
1907 self.0.detach();
1909 }
1910}
1911
1912impl Url {
1913 pub fn url(&self) -> String {
1915 self.0
1916 .children_with_tokens()
1917 .find_map(|it| match it {
1918 SyntaxElement::Token(token) => {
1919 if token.kind() == VALUE {
1920 Some(token.text().to_string())
1921 } else {
1922 None
1923 }
1924 }
1925 _ => None,
1926 })
1927 .unwrap()
1928 }
1929}
1930
1931impl MatchingPattern {
1932 pub fn pattern(&self) -> String {
1934 self.0
1935 .children_with_tokens()
1936 .find_map(|it| match it {
1937 SyntaxElement::Token(token) => {
1938 if token.kind() == VALUE {
1939 Some(token.text().to_string())
1940 } else {
1941 None
1942 }
1943 }
1944 _ => None,
1945 })
1946 .unwrap()
1947 }
1948}
1949
1950impl VersionPolicyNode {
1951 pub fn policy(&self) -> String {
1953 self.0
1954 .children_with_tokens()
1955 .find_map(|it| match it {
1956 SyntaxElement::Token(token) => {
1957 if token.kind() == VALUE || token.kind() == KEY {
1959 Some(token.text().to_string())
1960 } else {
1961 None
1962 }
1963 }
1964 _ => None,
1965 })
1966 .unwrap()
1967 }
1968}
1969
1970impl ScriptNode {
1971 pub fn script(&self) -> String {
1973 self.0
1974 .children_with_tokens()
1975 .find_map(|it| match it {
1976 SyntaxElement::Token(token) => {
1977 if token.kind() == VALUE || token.kind() == KEY {
1979 Some(token.text().to_string())
1980 } else {
1981 None
1982 }
1983 }
1984 _ => None,
1985 })
1986 .unwrap()
1987 }
1988}
1989
1990#[test]
1991fn test_entry_node_structure() {
1992 let wf: super::WatchFile = r#"version=4
1994opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
1995"#
1996 .parse()
1997 .unwrap();
1998
1999 let entry = wf.entries().next().unwrap();
2000
2001 assert_eq!(entry.0.children().find(|n| n.kind() == URL).is_some(), true);
2003 assert_eq!(entry.url(), "https://example.com/releases");
2004
2005 assert_eq!(
2007 entry
2008 .0
2009 .children()
2010 .find(|n| n.kind() == MATCHING_PATTERN)
2011 .is_some(),
2012 true
2013 );
2014 assert_eq!(
2015 entry.matching_pattern(),
2016 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2017 );
2018
2019 assert_eq!(
2021 entry
2022 .0
2023 .children()
2024 .find(|n| n.kind() == VERSION_POLICY)
2025 .is_some(),
2026 true
2027 );
2028 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2029
2030 assert_eq!(
2032 entry.0.children().find(|n| n.kind() == SCRIPT).is_some(),
2033 true
2034 );
2035 assert_eq!(entry.script(), Some("uupdate".into()));
2036}
2037
2038#[test]
2039fn test_entry_node_structure_partial() {
2040 let wf: super::WatchFile = r#"version=4
2042https://github.com/example/tags .*/v?(\d\S+)\.tar\.gz
2043"#
2044 .parse()
2045 .unwrap();
2046
2047 let entry = wf.entries().next().unwrap();
2048
2049 assert_eq!(entry.0.children().find(|n| n.kind() == URL).is_some(), true);
2051 assert_eq!(
2052 entry
2053 .0
2054 .children()
2055 .find(|n| n.kind() == MATCHING_PATTERN)
2056 .is_some(),
2057 true
2058 );
2059
2060 assert_eq!(
2062 entry
2063 .0
2064 .children()
2065 .find(|n| n.kind() == VERSION_POLICY)
2066 .is_some(),
2067 false
2068 );
2069 assert_eq!(
2070 entry.0.children().find(|n| n.kind() == SCRIPT).is_some(),
2071 false
2072 );
2073
2074 assert_eq!(entry.url(), "https://github.com/example/tags");
2076 assert_eq!(
2077 entry.matching_pattern(),
2078 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2079 );
2080 assert_eq!(entry.version(), Ok(None));
2081 assert_eq!(entry.script(), None);
2082}
2083
2084#[test]
2085fn test_parse_v1() {
2086 const WATCHV1: &str = r#"version=4
2087opts=bare,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1\.tar\.gz/ \
2088 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2089"#;
2090 let parsed = parse(WATCHV1);
2091 let node = parsed.syntax();
2093 assert_eq!(
2094 format!("{:#?}", node),
2095 r#"ROOT@0..161
2096 VERSION@0..10
2097 KEY@0..7 "version"
2098 EQUALS@7..8 "="
2099 VALUE@8..9 "4"
2100 NEWLINE@9..10 "\n"
2101 ENTRY@10..161
2102 OPTS_LIST@10..86
2103 KEY@10..14 "opts"
2104 EQUALS@14..15 "="
2105 OPTION@15..19
2106 KEY@15..19 "bare"
2107 OPTION_SEPARATOR@19..20
2108 COMMA@19..20 ","
2109 OPTION@20..86
2110 KEY@20..34 "filenamemangle"
2111 EQUALS@34..35 "="
2112 VALUE@35..86 "s/.+\\/v?(\\d\\S+)\\.tar\\ ..."
2113 WHITESPACE@86..87 " "
2114 CONTINUATION@87..89 "\\\n"
2115 WHITESPACE@89..91 " "
2116 URL@91..138
2117 VALUE@91..138 "https://github.com/sy ..."
2118 WHITESPACE@138..139 " "
2119 MATCHING_PATTERN@139..160
2120 VALUE@139..160 ".*/v?(\\d\\S+)\\.tar\\.gz"
2121 NEWLINE@160..161 "\n"
2122"#
2123 );
2124
2125 let root = parsed.root();
2126 assert_eq!(root.version(), 4);
2127 let entries = root.entries().collect::<Vec<_>>();
2128 assert_eq!(entries.len(), 1);
2129 let entry = &entries[0];
2130 assert_eq!(
2131 entry.url(),
2132 "https://github.com/syncthing/syncthing-gtk/tags"
2133 );
2134 assert_eq!(
2135 entry.matching_pattern(),
2136 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2137 );
2138 assert_eq!(entry.version(), Ok(None));
2139 assert_eq!(entry.script(), None);
2140
2141 assert_eq!(node.text(), WATCHV1);
2142}
2143
2144#[test]
2145fn test_parse_v2() {
2146 let parsed = parse(
2147 r#"version=4
2148https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2149# comment
2150"#,
2151 );
2152 assert_eq!(parsed.errors, Vec::<String>::new());
2153 let node = parsed.syntax();
2154 assert_eq!(
2155 format!("{:#?}", node),
2156 r###"ROOT@0..90
2157 VERSION@0..10
2158 KEY@0..7 "version"
2159 EQUALS@7..8 "="
2160 VALUE@8..9 "4"
2161 NEWLINE@9..10 "\n"
2162 ENTRY@10..80
2163 URL@10..57
2164 VALUE@10..57 "https://github.com/sy ..."
2165 WHITESPACE@57..58 " "
2166 MATCHING_PATTERN@58..79
2167 VALUE@58..79 ".*/v?(\\d\\S+)\\.tar\\.gz"
2168 NEWLINE@79..80 "\n"
2169 COMMENT@80..89 "# comment"
2170 NEWLINE@89..90 "\n"
2171"###
2172 );
2173
2174 let root = parsed.root();
2175 assert_eq!(root.version(), 4);
2176 let entries = root.entries().collect::<Vec<_>>();
2177 assert_eq!(entries.len(), 1);
2178 let entry = &entries[0];
2179 assert_eq!(
2180 entry.url(),
2181 "https://github.com/syncthing/syncthing-gtk/tags"
2182 );
2183 assert_eq!(
2184 entry.format_url(|| "syncthing-gtk".to_string()),
2185 "https://github.com/syncthing/syncthing-gtk/tags"
2186 .parse()
2187 .unwrap()
2188 );
2189}
2190
2191#[test]
2192fn test_parse_v3() {
2193 let parsed = parse(
2194 r#"version=4
2195https://github.com/syncthing/@PACKAGE@/tags .*/v?(\d\S+)\.tar\.gz
2196# comment
2197"#,
2198 );
2199 assert_eq!(parsed.errors, Vec::<String>::new());
2200 let root = parsed.root();
2201 assert_eq!(root.version(), 4);
2202 let entries = root.entries().collect::<Vec<_>>();
2203 assert_eq!(entries.len(), 1);
2204 let entry = &entries[0];
2205 assert_eq!(entry.url(), "https://github.com/syncthing/@PACKAGE@/tags");
2206 assert_eq!(
2207 entry.format_url(|| "syncthing-gtk".to_string()),
2208 "https://github.com/syncthing/syncthing-gtk/tags"
2209 .parse()
2210 .unwrap()
2211 );
2212}
2213
2214#[test]
2215fn test_parse_v4() {
2216 let cl: super::WatchFile = r#"version=4
2217opts=repack,compression=xz,dversionmangle=s/\+ds//,repacksuffix=+ds \
2218 https://github.com/example/example-cat/tags \
2219 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2220"#
2221 .parse()
2222 .unwrap();
2223 assert_eq!(cl.version(), 4);
2224 let entries = cl.entries().collect::<Vec<_>>();
2225 assert_eq!(entries.len(), 1);
2226 let entry = &entries[0];
2227 assert_eq!(entry.url(), "https://github.com/example/example-cat/tags");
2228 assert_eq!(
2229 entry.matching_pattern(),
2230 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2231 );
2232 assert!(entry.repack());
2233 assert_eq!(entry.compression(), Ok(Some(Compression::Xz)));
2234 assert_eq!(entry.dversionmangle(), Some("s/\\+ds//".into()));
2235 assert_eq!(entry.repacksuffix(), Some("+ds".into()));
2236 assert_eq!(entry.script(), Some("uupdate".into()));
2237 assert_eq!(
2238 entry.format_url(|| "example-cat".to_string()),
2239 "https://github.com/example/example-cat/tags"
2240 .parse()
2241 .unwrap()
2242 );
2243 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
2244}
2245
2246#[test]
2247fn test_git_mode() {
2248 let text = r#"version=3
2249opts="mode=git, gitmode=shallow, pgpmode=gittag" \
2250https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git \
2251refs/tags/(.*) debian
2252"#;
2253 let parsed = parse(text);
2254 assert_eq!(parsed.errors, Vec::<String>::new());
2255 let cl = parsed.root();
2256 assert_eq!(cl.version(), 3);
2257 let entries = cl.entries().collect::<Vec<_>>();
2258 assert_eq!(entries.len(), 1);
2259 let entry = &entries[0];
2260 assert_eq!(
2261 entry.url(),
2262 "https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git"
2263 );
2264 assert_eq!(entry.matching_pattern(), Some("refs/tags/(.*)".into()));
2265 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
2266 assert_eq!(entry.script(), None);
2267 assert_eq!(entry.gitmode(), Ok(GitMode::Shallow));
2268 assert_eq!(entry.pgpmode(), Ok(PgpMode::GitTag));
2269 assert_eq!(entry.mode(), Ok(Mode::Git));
2270}
2271
2272#[test]
2273fn test_parse_quoted() {
2274 const WATCHV1: &str = r#"version=4
2275opts="bare, filenamemangle=blah" \
2276 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2277"#;
2278 let parsed = parse(WATCHV1);
2279 let node = parsed.syntax();
2281
2282 let root = parsed.root();
2283 assert_eq!(root.version(), 4);
2284 let entries = root.entries().collect::<Vec<_>>();
2285 assert_eq!(entries.len(), 1);
2286 let entry = &entries[0];
2287
2288 assert_eq!(
2289 entry.url(),
2290 "https://github.com/syncthing/syncthing-gtk/tags"
2291 );
2292 assert_eq!(
2293 entry.matching_pattern(),
2294 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2295 );
2296 assert_eq!(entry.version(), Ok(None));
2297 assert_eq!(entry.script(), None);
2298
2299 assert_eq!(node.text(), WATCHV1);
2300}
2301
2302#[test]
2303fn test_set_url() {
2304 let wf: super::WatchFile = r#"version=4
2306https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2307"#
2308 .parse()
2309 .unwrap();
2310
2311 let mut entry = wf.entries().next().unwrap();
2312 assert_eq!(
2313 entry.url(),
2314 "https://github.com/syncthing/syncthing-gtk/tags"
2315 );
2316
2317 entry.set_url("https://newurl.example.org/path");
2318 assert_eq!(entry.url(), "https://newurl.example.org/path");
2319 assert_eq!(
2320 entry.matching_pattern(),
2321 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2322 );
2323
2324 assert_eq!(
2326 entry.to_string(),
2327 "https://newurl.example.org/path .*/v?(\\d\\S+)\\.tar\\.gz\n"
2328 );
2329}
2330
2331#[test]
2332fn test_set_url_with_options() {
2333 let wf: super::WatchFile = r#"version=4
2335opts=foo=blah https://foo.com/bar .*/v?(\d\S+)\.tar\.gz
2336"#
2337 .parse()
2338 .unwrap();
2339
2340 let mut entry = wf.entries().next().unwrap();
2341 assert_eq!(entry.url(), "https://foo.com/bar");
2342 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2343
2344 entry.set_url("https://example.com/baz");
2345 assert_eq!(entry.url(), "https://example.com/baz");
2346
2347 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2349 assert_eq!(
2350 entry.matching_pattern(),
2351 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2352 );
2353
2354 assert_eq!(
2356 entry.to_string(),
2357 "opts=foo=blah https://example.com/baz .*/v?(\\d\\S+)\\.tar\\.gz\n"
2358 );
2359}
2360
2361#[test]
2362fn test_set_url_complex() {
2363 let wf: super::WatchFile = r#"version=4
2365opts=bare,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1\.tar\.gz/ \
2366 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2367"#
2368 .parse()
2369 .unwrap();
2370
2371 let mut entry = wf.entries().next().unwrap();
2372 assert_eq!(
2373 entry.url(),
2374 "https://github.com/syncthing/syncthing-gtk/tags"
2375 );
2376
2377 entry.set_url("https://gitlab.com/newproject/tags");
2378 assert_eq!(entry.url(), "https://gitlab.com/newproject/tags");
2379
2380 assert!(entry.bare());
2382 assert_eq!(
2383 entry.filenamemangle(),
2384 Some("s/.+\\/v?(\\d\\S+)\\.tar\\.gz/syncthing-gtk-$1\\.tar\\.gz/".into())
2385 );
2386 assert_eq!(
2387 entry.matching_pattern(),
2388 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2389 );
2390
2391 assert_eq!(
2393 entry.to_string(),
2394 r#"opts=bare,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1\.tar\.gz/ \
2395 https://gitlab.com/newproject/tags .*/v?(\d\S+)\.tar\.gz
2396"#
2397 );
2398}
2399
2400#[test]
2401fn test_set_url_with_all_fields() {
2402 let wf: super::WatchFile = r#"version=4
2404opts=repack,compression=xz,dversionmangle=s/\+ds//,repacksuffix=+ds \
2405 https://github.com/example/example-cat/tags \
2406 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2407"#
2408 .parse()
2409 .unwrap();
2410
2411 let mut entry = wf.entries().next().unwrap();
2412 assert_eq!(entry.url(), "https://github.com/example/example-cat/tags");
2413 assert_eq!(
2414 entry.matching_pattern(),
2415 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2416 );
2417 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2418 assert_eq!(entry.script(), Some("uupdate".into()));
2419
2420 entry.set_url("https://gitlab.example.org/project/releases");
2421 assert_eq!(entry.url(), "https://gitlab.example.org/project/releases");
2422
2423 assert!(entry.repack());
2425 assert_eq!(entry.compression(), Ok(Some(super::Compression::Xz)));
2426 assert_eq!(entry.dversionmangle(), Some("s/\\+ds//".into()));
2427 assert_eq!(entry.repacksuffix(), Some("+ds".into()));
2428 assert_eq!(
2429 entry.matching_pattern(),
2430 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2431 );
2432 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2433 assert_eq!(entry.script(), Some("uupdate".into()));
2434
2435 assert_eq!(
2437 entry.to_string(),
2438 r#"opts=repack,compression=xz,dversionmangle=s/\+ds//,repacksuffix=+ds \
2439 https://gitlab.example.org/project/releases \
2440 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2441"#
2442 );
2443}
2444
2445#[test]
2446fn test_set_url_quoted_options() {
2447 let wf: super::WatchFile = r#"version=4
2449opts="bare, filenamemangle=blah" \
2450 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2451"#
2452 .parse()
2453 .unwrap();
2454
2455 let mut entry = wf.entries().next().unwrap();
2456 assert_eq!(
2457 entry.url(),
2458 "https://github.com/syncthing/syncthing-gtk/tags"
2459 );
2460
2461 entry.set_url("https://example.org/new/path");
2462 assert_eq!(entry.url(), "https://example.org/new/path");
2463
2464 assert_eq!(
2466 entry.to_string(),
2467 r#"opts="bare, filenamemangle=blah" \
2468 https://example.org/new/path .*/v?(\d\S+)\.tar\.gz
2469"#
2470 );
2471}
2472
2473#[test]
2474fn test_set_opt_update_existing() {
2475 let wf: super::WatchFile = r#"version=4
2477opts=foo=blah,bar=baz https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2478"#
2479 .parse()
2480 .unwrap();
2481
2482 let mut entry = wf.entries().next().unwrap();
2483 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2484 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2485
2486 entry.set_opt("foo", "updated");
2487 assert_eq!(entry.get_option("foo"), Some("updated".to_string()));
2488 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2489
2490 assert_eq!(
2492 entry.to_string(),
2493 "opts=foo=updated,bar=baz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2494 );
2495}
2496
2497#[test]
2498fn test_set_opt_add_new() {
2499 let wf: super::WatchFile = r#"version=4
2501opts=foo=blah https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2502"#
2503 .parse()
2504 .unwrap();
2505
2506 let mut entry = wf.entries().next().unwrap();
2507 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2508 assert_eq!(entry.get_option("bar"), None);
2509
2510 entry.set_opt("bar", "baz");
2511 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2512 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2513
2514 assert_eq!(
2516 entry.to_string(),
2517 "opts=foo=blah,bar=baz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2518 );
2519}
2520
2521#[test]
2522fn test_set_opt_create_options_list() {
2523 let wf: super::WatchFile = r#"version=4
2525https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2526"#
2527 .parse()
2528 .unwrap();
2529
2530 let mut entry = wf.entries().next().unwrap();
2531 assert_eq!(entry.option_list(), None);
2532
2533 entry.set_opt("compression", "xz");
2534 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
2535
2536 assert_eq!(
2538 entry.to_string(),
2539 "opts=compression=xz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2540 );
2541}
2542
2543#[test]
2544fn test_del_opt_remove_single() {
2545 let wf: super::WatchFile = r#"version=4
2547opts=foo=blah,bar=baz,qux=quux https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2548"#
2549 .parse()
2550 .unwrap();
2551
2552 let mut entry = wf.entries().next().unwrap();
2553 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2554 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2555 assert_eq!(entry.get_option("qux"), Some("quux".to_string()));
2556
2557 entry.del_opt("bar");
2558 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2559 assert_eq!(entry.get_option("bar"), None);
2560 assert_eq!(entry.get_option("qux"), Some("quux".to_string()));
2561
2562 assert_eq!(
2564 entry.to_string(),
2565 "opts=foo=blah,qux=quux https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2566 );
2567}
2568
2569#[test]
2570fn test_del_opt_remove_first() {
2571 let wf: super::WatchFile = r#"version=4
2573opts=foo=blah,bar=baz https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2574"#
2575 .parse()
2576 .unwrap();
2577
2578 let mut entry = wf.entries().next().unwrap();
2579 entry.del_opt("foo");
2580 assert_eq!(entry.get_option("foo"), None);
2581 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2582
2583 assert_eq!(
2585 entry.to_string(),
2586 "opts=bar=baz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2587 );
2588}
2589
2590#[test]
2591fn test_del_opt_remove_last() {
2592 let wf: super::WatchFile = r#"version=4
2594opts=foo=blah,bar=baz https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2595"#
2596 .parse()
2597 .unwrap();
2598
2599 let mut entry = wf.entries().next().unwrap();
2600 entry.del_opt("bar");
2601 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2602 assert_eq!(entry.get_option("bar"), None);
2603
2604 assert_eq!(
2606 entry.to_string(),
2607 "opts=foo=blah https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2608 );
2609}
2610
2611#[test]
2612fn test_del_opt_remove_only_option() {
2613 let wf: super::WatchFile = r#"version=4
2615opts=foo=blah https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2616"#
2617 .parse()
2618 .unwrap();
2619
2620 let mut entry = wf.entries().next().unwrap();
2621 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2622
2623 entry.del_opt("foo");
2624 assert_eq!(entry.get_option("foo"), None);
2625 assert_eq!(entry.option_list(), None);
2626
2627 assert_eq!(
2629 entry.to_string(),
2630 "https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2631 );
2632}
2633
2634#[test]
2635fn test_del_opt_nonexistent() {
2636 let wf: super::WatchFile = r#"version=4
2638opts=foo=blah https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2639"#
2640 .parse()
2641 .unwrap();
2642
2643 let mut entry = wf.entries().next().unwrap();
2644 let original = entry.to_string();
2645
2646 entry.del_opt("nonexistent");
2647 assert_eq!(entry.to_string(), original);
2648}
2649
2650#[test]
2651fn test_set_opt_multiple_operations() {
2652 let wf: super::WatchFile = r#"version=4
2654https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2655"#
2656 .parse()
2657 .unwrap();
2658
2659 let mut entry = wf.entries().next().unwrap();
2660
2661 entry.set_opt("compression", "xz");
2662 entry.set_opt("repack", "");
2663 entry.set_opt("dversionmangle", "s/\\+ds//");
2664
2665 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
2666 assert_eq!(
2667 entry.get_option("dversionmangle"),
2668 Some("s/\\+ds//".to_string())
2669 );
2670}
2671
2672#[test]
2673fn test_set_matching_pattern() {
2674 let wf: super::WatchFile = r#"version=4
2676https://github.com/example/tags .*/v?(\d\S+)\.tar\.gz
2677"#
2678 .parse()
2679 .unwrap();
2680
2681 let mut entry = wf.entries().next().unwrap();
2682 assert_eq!(
2683 entry.matching_pattern(),
2684 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2685 );
2686
2687 entry.set_matching_pattern("(?:.*?/)?v?([\\d.]+)\\.tar\\.gz");
2688 assert_eq!(
2689 entry.matching_pattern(),
2690 Some("(?:.*?/)?v?([\\d.]+)\\.tar\\.gz".into())
2691 );
2692
2693 assert_eq!(entry.url(), "https://github.com/example/tags");
2695
2696 assert_eq!(
2698 entry.to_string(),
2699 "https://github.com/example/tags (?:.*?/)?v?([\\d.]+)\\.tar\\.gz\n"
2700 );
2701}
2702
2703#[test]
2704fn test_set_matching_pattern_with_all_fields() {
2705 let wf: super::WatchFile = r#"version=4
2707opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2708"#
2709 .parse()
2710 .unwrap();
2711
2712 let mut entry = wf.entries().next().unwrap();
2713 assert_eq!(
2714 entry.matching_pattern(),
2715 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2716 );
2717
2718 entry.set_matching_pattern(".*/version-([\\d.]+)\\.tar\\.xz");
2719 assert_eq!(
2720 entry.matching_pattern(),
2721 Some(".*/version-([\\d.]+)\\.tar\\.xz".into())
2722 );
2723
2724 assert_eq!(entry.url(), "https://example.com/releases");
2726 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2727 assert_eq!(entry.script(), Some("uupdate".into()));
2728 assert_eq!(entry.compression(), Ok(Some(super::Compression::Xz)));
2729
2730 assert_eq!(
2732 entry.to_string(),
2733 "opts=compression=xz https://example.com/releases .*/version-([\\d.]+)\\.tar\\.xz debian uupdate\n"
2734 );
2735}
2736
2737#[test]
2738fn test_set_version_policy() {
2739 let wf: super::WatchFile = r#"version=4
2741https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2742"#
2743 .parse()
2744 .unwrap();
2745
2746 let mut entry = wf.entries().next().unwrap();
2747 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2748
2749 entry.set_version_policy("previous");
2750 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Previous)));
2751
2752 assert_eq!(entry.url(), "https://example.com/releases");
2754 assert_eq!(
2755 entry.matching_pattern(),
2756 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2757 );
2758 assert_eq!(entry.script(), Some("uupdate".into()));
2759
2760 assert_eq!(
2762 entry.to_string(),
2763 "https://example.com/releases (?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz previous uupdate\n"
2764 );
2765}
2766
2767#[test]
2768fn test_set_version_policy_with_options() {
2769 let wf: super::WatchFile = r#"version=4
2771opts=repack,compression=xz \
2772 https://github.com/example/example-cat/tags \
2773 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2774"#
2775 .parse()
2776 .unwrap();
2777
2778 let mut entry = wf.entries().next().unwrap();
2779 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2780
2781 entry.set_version_policy("ignore");
2782 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Ignore)));
2783
2784 assert_eq!(entry.url(), "https://github.com/example/example-cat/tags");
2786 assert_eq!(
2787 entry.matching_pattern(),
2788 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2789 );
2790 assert_eq!(entry.script(), Some("uupdate".into()));
2791 assert!(entry.repack());
2792
2793 assert_eq!(
2795 entry.to_string(),
2796 r#"opts=repack,compression=xz \
2797 https://github.com/example/example-cat/tags \
2798 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz ignore uupdate
2799"#
2800 );
2801}
2802
2803#[test]
2804fn test_set_script() {
2805 let wf: super::WatchFile = r#"version=4
2807https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2808"#
2809 .parse()
2810 .unwrap();
2811
2812 let mut entry = wf.entries().next().unwrap();
2813 assert_eq!(entry.script(), Some("uupdate".into()));
2814
2815 entry.set_script("uscan");
2816 assert_eq!(entry.script(), Some("uscan".into()));
2817
2818 assert_eq!(entry.url(), "https://example.com/releases");
2820 assert_eq!(
2821 entry.matching_pattern(),
2822 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2823 );
2824 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2825
2826 assert_eq!(
2828 entry.to_string(),
2829 "https://example.com/releases (?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz debian uscan\n"
2830 );
2831}
2832
2833#[test]
2834fn test_set_script_with_options() {
2835 let wf: super::WatchFile = r#"version=4
2837opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2838"#
2839 .parse()
2840 .unwrap();
2841
2842 let mut entry = wf.entries().next().unwrap();
2843 assert_eq!(entry.script(), Some("uupdate".into()));
2844
2845 entry.set_script("custom-script.sh");
2846 assert_eq!(entry.script(), Some("custom-script.sh".into()));
2847
2848 assert_eq!(entry.url(), "https://example.com/releases");
2850 assert_eq!(
2851 entry.matching_pattern(),
2852 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2853 );
2854 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2855 assert_eq!(entry.compression(), Ok(Some(super::Compression::Xz)));
2856
2857 assert_eq!(
2859 entry.to_string(),
2860 "opts=compression=xz https://example.com/releases (?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz debian custom-script.sh\n"
2861 );
2862}
2863
2864#[test]
2865fn test_apply_dversionmangle() {
2866 let wf: super::WatchFile = r#"version=4
2868opts=dversionmangle=s/\+dfsg$// https://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");
2874 assert_eq!(entry.apply_dversionmangle("1.0").unwrap(), "1.0");
2875
2876 let wf: super::WatchFile = r#"version=4
2878opts=versionmangle=s/^v// https://example.com/ .*
2879"#
2880 .parse()
2881 .unwrap();
2882 let entry = wf.entries().next().unwrap();
2883 assert_eq!(entry.apply_dversionmangle("v1.0").unwrap(), "1.0");
2884
2885 let wf: super::WatchFile = r#"version=4
2887opts=dversionmangle=s/\+ds//,versionmangle=s/^v// https://example.com/ .*
2888"#
2889 .parse()
2890 .unwrap();
2891 let entry = wf.entries().next().unwrap();
2892 assert_eq!(entry.apply_dversionmangle("1.0+ds").unwrap(), "1.0");
2893
2894 let wf: super::WatchFile = r#"version=4
2896https://example.com/ .*
2897"#
2898 .parse()
2899 .unwrap();
2900 let entry = wf.entries().next().unwrap();
2901 assert_eq!(entry.apply_dversionmangle("1.0+dfsg").unwrap(), "1.0+dfsg");
2902}
2903
2904#[test]
2905fn test_apply_oversionmangle() {
2906 let wf: super::WatchFile = r#"version=4
2908opts=oversionmangle=s/$/-1/ https://example.com/ .*
2909"#
2910 .parse()
2911 .unwrap();
2912 let entry = wf.entries().next().unwrap();
2913 assert_eq!(entry.apply_oversionmangle("1.0").unwrap(), "1.0-1");
2914 assert_eq!(entry.apply_oversionmangle("2.5.3").unwrap(), "2.5.3-1");
2915
2916 let wf: super::WatchFile = r#"version=4
2918opts=oversionmangle=s/$/.dfsg/ https://example.com/ .*
2919"#
2920 .parse()
2921 .unwrap();
2922 let entry = wf.entries().next().unwrap();
2923 assert_eq!(entry.apply_oversionmangle("1.0").unwrap(), "1.0.dfsg");
2924
2925 let wf: super::WatchFile = r#"version=4
2927https://example.com/ .*
2928"#
2929 .parse()
2930 .unwrap();
2931 let entry = wf.entries().next().unwrap();
2932 assert_eq!(entry.apply_oversionmangle("1.0").unwrap(), "1.0");
2933}
2934
2935#[test]
2936fn test_apply_dirversionmangle() {
2937 let wf: super::WatchFile = r#"version=4
2939opts=dirversionmangle=s/^v// https://example.com/ .*
2940"#
2941 .parse()
2942 .unwrap();
2943 let entry = wf.entries().next().unwrap();
2944 assert_eq!(entry.apply_dirversionmangle("v1.0").unwrap(), "1.0");
2945 assert_eq!(entry.apply_dirversionmangle("v2.5.3").unwrap(), "2.5.3");
2946
2947 let wf: super::WatchFile = r#"version=4
2949opts=dirversionmangle=s/v(\d)/$1/ https://example.com/ .*
2950"#
2951 .parse()
2952 .unwrap();
2953 let entry = wf.entries().next().unwrap();
2954 assert_eq!(entry.apply_dirversionmangle("v1.0").unwrap(), "1.0");
2955
2956 let wf: super::WatchFile = r#"version=4
2958https://example.com/ .*
2959"#
2960 .parse()
2961 .unwrap();
2962 let entry = wf.entries().next().unwrap();
2963 assert_eq!(entry.apply_dirversionmangle("v1.0").unwrap(), "v1.0");
2964}
2965
2966#[test]
2967fn test_apply_filenamemangle() {
2968 let wf: super::WatchFile = r#"version=4
2970opts=filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/mypackage-$1.tar.gz/ https://example.com/ .*
2971"#
2972 .parse()
2973 .unwrap();
2974 let entry = wf.entries().next().unwrap();
2975 assert_eq!(
2976 entry
2977 .apply_filenamemangle("https://example.com/v1.0.tar.gz")
2978 .unwrap(),
2979 "mypackage-1.0.tar.gz"
2980 );
2981 assert_eq!(
2982 entry
2983 .apply_filenamemangle("https://example.com/2.5.3.tar.gz")
2984 .unwrap(),
2985 "mypackage-2.5.3.tar.gz"
2986 );
2987
2988 let wf: super::WatchFile = r#"version=4
2990opts=filenamemangle=s/.*\/(.*)/$1/ https://example.com/ .*
2991"#
2992 .parse()
2993 .unwrap();
2994 let entry = wf.entries().next().unwrap();
2995 assert_eq!(
2996 entry
2997 .apply_filenamemangle("https://example.com/path/to/file.tar.gz")
2998 .unwrap(),
2999 "file.tar.gz"
3000 );
3001
3002 let wf: super::WatchFile = r#"version=4
3004https://example.com/ .*
3005"#
3006 .parse()
3007 .unwrap();
3008 let entry = wf.entries().next().unwrap();
3009 assert_eq!(
3010 entry
3011 .apply_filenamemangle("https://example.com/file.tar.gz")
3012 .unwrap(),
3013 "https://example.com/file.tar.gz"
3014 );
3015}
3016
3017#[test]
3018fn test_apply_pagemangle() {
3019 let wf: super::WatchFile = r#"version=4
3021opts=pagemangle=s/&/&/g https://example.com/ .*
3022"#
3023 .parse()
3024 .unwrap();
3025 let entry = wf.entries().next().unwrap();
3026 assert_eq!(
3027 entry.apply_pagemangle(b"foo & bar").unwrap(),
3028 b"foo & bar"
3029 );
3030 assert_eq!(
3031 entry
3032 .apply_pagemangle(b"& foo & bar &")
3033 .unwrap(),
3034 b"& foo & bar &"
3035 );
3036
3037 let wf: super::WatchFile = r#"version=4
3039opts=pagemangle=s/<[^>]+>//g https://example.com/ .*
3040"#
3041 .parse()
3042 .unwrap();
3043 let entry = wf.entries().next().unwrap();
3044 assert_eq!(entry.apply_pagemangle(b"<div>text</div>").unwrap(), b"text");
3045
3046 let wf: super::WatchFile = r#"version=4
3048https://example.com/ .*
3049"#
3050 .parse()
3051 .unwrap();
3052 let entry = wf.entries().next().unwrap();
3053 assert_eq!(
3054 entry.apply_pagemangle(b"foo & bar").unwrap(),
3055 b"foo & bar"
3056 );
3057}
3058
3059#[test]
3060fn test_apply_downloadurlmangle() {
3061 let wf: super::WatchFile = r#"version=4
3063opts=downloadurlmangle=s|/archive/|/download/| https://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/download/file.tar.gz"
3073 );
3074
3075 let wf: super::WatchFile = r#"version=4
3077opts=downloadurlmangle=s/github\.com/raw.githubusercontent.com/ https://example.com/ .*
3078"#
3079 .parse()
3080 .unwrap();
3081 let entry = wf.entries().next().unwrap();
3082 assert_eq!(
3083 entry
3084 .apply_downloadurlmangle("https://github.com/user/repo/file.tar.gz")
3085 .unwrap(),
3086 "https://raw.githubusercontent.com/user/repo/file.tar.gz"
3087 );
3088
3089 let wf: super::WatchFile = r#"version=4
3091https://example.com/ .*
3092"#
3093 .parse()
3094 .unwrap();
3095 let entry = wf.entries().next().unwrap();
3096 assert_eq!(
3097 entry
3098 .apply_downloadurlmangle("https://example.com/archive/file.tar.gz")
3099 .unwrap(),
3100 "https://example.com/archive/file.tar.gz"
3101 );
3102}
3103
3104#[test]
3105fn test_entry_builder_minimal() {
3106 let entry = super::EntryBuilder::new("https://github.com/example/tags")
3108 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3109 .build();
3110
3111 assert_eq!(entry.url(), "https://github.com/example/tags");
3112 assert_eq!(
3113 entry.matching_pattern().as_deref(),
3114 Some(".*/v?(\\d\\S+)\\.tar\\.gz")
3115 );
3116 assert_eq!(entry.version(), Ok(None));
3117 assert_eq!(entry.script(), None);
3118 assert!(entry.opts().is_empty());
3119}
3120
3121#[test]
3122fn test_entry_builder_url_only() {
3123 let entry = super::EntryBuilder::new("https://example.com/releases").build();
3125
3126 assert_eq!(entry.url(), "https://example.com/releases");
3127 assert_eq!(entry.matching_pattern(), None);
3128 assert_eq!(entry.version(), Ok(None));
3129 assert_eq!(entry.script(), None);
3130 assert!(entry.opts().is_empty());
3131}
3132
3133#[test]
3134fn test_entry_builder_with_all_fields() {
3135 let entry = super::EntryBuilder::new("https://github.com/example/tags")
3137 .matching_pattern(".*/v?(\\d[\\d.]*)\\.tar\\.gz")
3138 .version_policy("debian")
3139 .script("uupdate")
3140 .opt("compression", "xz")
3141 .flag("repack")
3142 .build();
3143
3144 assert_eq!(entry.url(), "https://github.com/example/tags");
3145 assert_eq!(
3146 entry.matching_pattern().as_deref(),
3147 Some(".*/v?(\\d[\\d.]*)\\.tar\\.gz")
3148 );
3149 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
3150 assert_eq!(entry.script(), Some("uupdate".into()));
3151 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
3152 assert!(entry.has_option("repack"));
3153 assert!(entry.repack());
3154}
3155
3156#[test]
3157fn test_entry_builder_multiple_options() {
3158 let entry = super::EntryBuilder::new("https://example.com/tags")
3160 .matching_pattern(".*/v?(\\d+\\.\\d+)\\.tar\\.gz")
3161 .opt("compression", "xz")
3162 .opt("dversionmangle", "s/\\+ds//")
3163 .opt("repacksuffix", "+ds")
3164 .build();
3165
3166 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
3167 assert_eq!(
3168 entry.get_option("dversionmangle"),
3169 Some("s/\\+ds//".to_string())
3170 );
3171 assert_eq!(entry.get_option("repacksuffix"), Some("+ds".to_string()));
3172}
3173
3174#[test]
3175fn test_entry_builder_via_entry() {
3176 let entry = super::Entry::builder("https://github.com/example/tags")
3178 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3179 .version_policy("debian")
3180 .build();
3181
3182 assert_eq!(entry.url(), "https://github.com/example/tags");
3183 assert_eq!(
3184 entry.matching_pattern().as_deref(),
3185 Some(".*/v?(\\d\\S+)\\.tar\\.gz")
3186 );
3187 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
3188}
3189
3190#[test]
3191fn test_watchfile_add_entry_to_empty() {
3192 let mut wf = super::WatchFile::new(Some(4));
3194
3195 let entry = super::EntryBuilder::new("https://github.com/example/tags")
3196 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3197 .build();
3198
3199 wf.add_entry(entry);
3200
3201 assert_eq!(wf.version(), 4);
3202 assert_eq!(wf.entries().count(), 1);
3203
3204 let added_entry = wf.entries().next().unwrap();
3205 assert_eq!(added_entry.url(), "https://github.com/example/tags");
3206 assert_eq!(
3207 added_entry.matching_pattern().as_deref(),
3208 Some(".*/v?(\\d\\S+)\\.tar\\.gz")
3209 );
3210}
3211
3212#[test]
3213fn test_watchfile_add_multiple_entries() {
3214 let mut wf = super::WatchFile::new(Some(4));
3216
3217 wf.add_entry(
3218 super::EntryBuilder::new("https://github.com/example1/tags")
3219 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3220 .build(),
3221 );
3222
3223 wf.add_entry(
3224 super::EntryBuilder::new("https://github.com/example2/releases")
3225 .matching_pattern(".*/(\\d+\\.\\d+)\\.tar\\.gz")
3226 .opt("compression", "xz")
3227 .build(),
3228 );
3229
3230 assert_eq!(wf.entries().count(), 2);
3231
3232 let entries: Vec<_> = wf.entries().collect();
3233 assert_eq!(entries[0].url(), "https://github.com/example1/tags");
3234 assert_eq!(entries[1].url(), "https://github.com/example2/releases");
3235 assert_eq!(entries[1].get_option("compression"), Some("xz".to_string()));
3236}
3237
3238#[test]
3239fn test_watchfile_add_entry_to_existing() {
3240 let mut wf: super::WatchFile = r#"version=4
3242https://example.com/old .*/v?(\\d\\S+)\\.tar\\.gz
3243"#
3244 .parse()
3245 .unwrap();
3246
3247 assert_eq!(wf.entries().count(), 1);
3248
3249 wf.add_entry(
3250 super::EntryBuilder::new("https://github.com/example/new")
3251 .matching_pattern(".*/v?(\\d+\\.\\d+)\\.tar\\.gz")
3252 .opt("compression", "xz")
3253 .version_policy("debian")
3254 .build(),
3255 );
3256
3257 assert_eq!(wf.entries().count(), 2);
3258
3259 let entries: Vec<_> = wf.entries().collect();
3260 assert_eq!(entries[0].url(), "https://example.com/old");
3261 assert_eq!(entries[1].url(), "https://github.com/example/new");
3262 assert_eq!(entries[1].version(), Ok(Some(VersionPolicy::Debian)));
3263}
3264
3265#[test]
3266fn test_entry_builder_formatting() {
3267 let entry = super::EntryBuilder::new("https://github.com/example/tags")
3269 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3270 .opt("compression", "xz")
3271 .flag("repack")
3272 .version_policy("debian")
3273 .script("uupdate")
3274 .build();
3275
3276 let entry_str = entry.to_string();
3277
3278 assert!(entry_str.starts_with("opts="));
3280 assert!(entry_str.contains("https://github.com/example/tags"));
3282 assert!(entry_str.contains(".*/v?(\\d\\S+)\\.tar\\.gz"));
3284 assert!(entry_str.contains("debian"));
3286 assert!(entry_str.contains("uupdate"));
3288 assert!(entry_str.ends_with('\n'));
3290}
3291
3292#[test]
3293fn test_watchfile_add_entry_preserves_format() {
3294 let mut wf = super::WatchFile::new(Some(4));
3296
3297 wf.add_entry(
3298 super::EntryBuilder::new("https://github.com/example/tags")
3299 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3300 .build(),
3301 );
3302
3303 let wf_str = wf.to_string();
3304
3305 assert!(wf_str.starts_with("version=4\n"));
3307 assert!(wf_str.contains("https://github.com/example/tags"));
3309
3310 let reparsed: super::WatchFile = wf_str.parse().unwrap();
3312 assert_eq!(reparsed.version(), 4);
3313 assert_eq!(reparsed.entries().count(), 1);
3314}
3315
3316#[test]
3317fn test_line_col() {
3318 let text = r#"version=4
3319opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
3320"#;
3321 let wf = text.parse::<super::WatchFile>().unwrap();
3322
3323 let version_node = wf.version_node().unwrap();
3325 assert_eq!(version_node.line(), 0);
3326 assert_eq!(version_node.column(), 0);
3327 assert_eq!(version_node.line_col(), (0, 0));
3328
3329 let entries: Vec<_> = wf.entries().collect();
3331 assert_eq!(entries.len(), 1);
3332
3333 assert_eq!(entries[0].line(), 1);
3335 assert_eq!(entries[0].column(), 0);
3336 assert_eq!(entries[0].line_col(), (1, 0));
3337
3338 let option_list = entries[0].option_list().unwrap();
3340 assert_eq!(option_list.line(), 1); let url_node = entries[0].url_node().unwrap();
3343 assert_eq!(url_node.line(), 1); let pattern_node = entries[0].matching_pattern_node().unwrap();
3346 assert_eq!(pattern_node.line(), 1); let version_policy_node = entries[0].version_node().unwrap();
3349 assert_eq!(version_policy_node.line(), 1); let script_node = entries[0].script_node().unwrap();
3352 assert_eq!(script_node.line(), 1); let options: Vec<_> = option_list.options().collect();
3356 assert_eq!(options.len(), 1);
3357 assert_eq!(options[0].key(), Some("compression".to_string()));
3358 assert_eq!(options[0].value(), Some("xz".to_string()));
3359 assert_eq!(options[0].line(), 1); let compression_opt = option_list.find_option("compression").unwrap();
3363 assert_eq!(compression_opt.line(), 1);
3364 assert_eq!(compression_opt.column(), 5); assert_eq!(compression_opt.line_col(), (1, 5));
3366}