1use crate::lex::lex;
2use crate::types::{
3 ComponentType, Compression, GitExport, GitMode, Mode, PgpMode, Pretty, SearchMode,
4};
5use crate::SyntaxKind;
6use crate::SyntaxKind::*;
7use crate::DEFAULT_VERSION;
8use std::io::Read;
9use std::marker::PhantomData;
10use std::str::FromStr;
11
12#[cfg(test)]
13use crate::types::VersionPolicy;
14
15pub(crate) fn watch_option_to_key(option: &crate::types::WatchOption) -> &'static str {
17 use crate::types::WatchOption;
18
19 match option {
20 WatchOption::Component(_) => "component",
21 WatchOption::Compression(_) => "compression",
22 WatchOption::UserAgent(_) => "user-agent",
23 WatchOption::Pagemangle(_) => "pagemangle",
24 WatchOption::Uversionmangle(_) => "uversionmangle",
25 WatchOption::Dversionmangle(_) => "dversionmangle",
26 WatchOption::Dirversionmangle(_) => "dirversionmangle",
27 WatchOption::Oversionmangle(_) => "oversionmangle",
28 WatchOption::Downloadurlmangle(_) => "downloadurlmangle",
29 WatchOption::Pgpsigurlmangle(_) => "pgpsigurlmangle",
30 WatchOption::Filenamemangle(_) => "filenamemangle",
31 WatchOption::VersionPolicy(_) => "version-policy",
32 WatchOption::Searchmode(_) => "searchmode",
33 WatchOption::Mode(_) => "mode",
34 WatchOption::Pgpmode(_) => "pgpmode",
35 WatchOption::Gitexport(_) => "gitexport",
36 WatchOption::Gitmode(_) => "gitmode",
37 WatchOption::Pretty(_) => "pretty",
38 WatchOption::Ctype(_) => "ctype",
39 WatchOption::Repacksuffix(_) => "repacksuffix",
40 WatchOption::Unzipopt(_) => "unzipopt",
41 WatchOption::Script(_) => "script",
42 WatchOption::Decompress => "decompress",
43 WatchOption::Bare => "bare",
44 WatchOption::Repack => "repack",
45 }
46}
47
48pub(crate) fn watch_option_to_value(option: &crate::types::WatchOption) -> String {
50 use crate::types::WatchOption;
51
52 match option {
53 WatchOption::Component(v) => v.clone(),
54 WatchOption::Compression(v) => v.to_string(),
55 WatchOption::UserAgent(v) => v.clone(),
56 WatchOption::Pagemangle(v) => v.clone(),
57 WatchOption::Uversionmangle(v) => v.clone(),
58 WatchOption::Dversionmangle(v) => v.clone(),
59 WatchOption::Dirversionmangle(v) => v.clone(),
60 WatchOption::Oversionmangle(v) => v.clone(),
61 WatchOption::Downloadurlmangle(v) => v.clone(),
62 WatchOption::Pgpsigurlmangle(v) => v.clone(),
63 WatchOption::Filenamemangle(v) => v.clone(),
64 WatchOption::VersionPolicy(v) => v.to_string(),
65 WatchOption::Searchmode(v) => v.to_string(),
66 WatchOption::Mode(v) => v.to_string(),
67 WatchOption::Pgpmode(v) => v.to_string(),
68 WatchOption::Gitexport(v) => v.to_string(),
69 WatchOption::Gitmode(v) => v.to_string(),
70 WatchOption::Pretty(v) => v.to_string(),
71 WatchOption::Ctype(v) => v.to_string(),
72 WatchOption::Repacksuffix(v) => v.clone(),
73 WatchOption::Unzipopt(v) => v.clone(),
74 WatchOption::Script(v) => v.clone(),
75 WatchOption::Decompress => String::new(),
76 WatchOption::Bare => String::new(),
77 WatchOption::Repack => String::new(),
78 }
79}
80
81#[derive(Debug, Clone, PartialEq, Eq, Hash)]
82pub struct ParseError(pub Vec<String>);
84
85impl std::fmt::Display for ParseError {
86 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
87 for err in &self.0 {
88 writeln!(f, "{}", err)?;
89 }
90 Ok(())
91 }
92}
93
94impl std::error::Error for ParseError {}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
100pub enum Lang {}
101impl rowan::Language for Lang {
102 type Kind = SyntaxKind;
103 fn kind_from_raw(raw: rowan::SyntaxKind) -> Self::Kind {
104 unsafe { std::mem::transmute::<u16, SyntaxKind>(raw.0) }
105 }
106 fn kind_to_raw(kind: Self::Kind) -> rowan::SyntaxKind {
107 kind.into()
108 }
109}
110
111use rowan::GreenNode;
114
115use rowan::GreenNodeBuilder;
119
120#[derive(Debug, Clone, PartialEq, Eq)]
123pub struct Parse<T> {
124 green: GreenNode,
126 errors: Vec<String>,
128 _ty: PhantomData<T>,
130}
131
132impl<T> Parse<T> {
133 pub(crate) fn new(green: GreenNode, errors: Vec<String>) -> Self {
135 Parse {
136 green,
137 errors,
138 _ty: PhantomData,
139 }
140 }
141
142 pub fn green(&self) -> &GreenNode {
144 &self.green
145 }
146
147 pub fn errors(&self) -> &[String] {
149 &self.errors
150 }
151
152 pub fn is_ok(&self) -> bool {
154 self.errors.is_empty()
155 }
156}
157
158impl Parse<WatchFile> {
159 pub fn tree(&self) -> WatchFile {
161 WatchFile::cast(SyntaxNode::new_root_mut(self.green.clone()))
162 .expect("root node should be a WatchFile")
163 }
164}
165
166unsafe impl<T> Send for Parse<T> {}
169unsafe impl<T> Sync for Parse<T> {}
170
171struct InternalParse {
173 green_node: GreenNode,
174 errors: Vec<String>,
175}
176
177fn is_field_token(kind: Option<SyntaxKind>) -> bool {
183 matches!(kind, Some(KEY | VALUE | EQUALS | COMMA | QUOTE))
184}
185
186fn parse(text: &str) -> InternalParse {
187 struct Parser {
188 tokens: Vec<(SyntaxKind, String)>,
191 builder: GreenNodeBuilder<'static>,
193 errors: Vec<String>,
196 }
197
198 impl Parser {
199 fn parse_version(&mut self) -> Option<u32> {
200 let mut version = None;
201 if self.tokens.last() == Some(&(KEY, "version".to_string())) {
202 self.builder.start_node(VERSION.into());
203 self.bump();
204 self.skip_ws();
205 if self.current() != Some(EQUALS) {
206 self.builder.start_node(ERROR.into());
207 self.errors.push("expected `=`".to_string());
208 self.bump();
209 self.builder.finish_node();
210 } else {
211 self.bump();
212 }
213 if self.current() != Some(VALUE) {
214 self.builder.start_node(ERROR.into());
215 self.errors
216 .push(format!("expected value, got {:?}", self.current()));
217 self.bump();
218 self.builder.finish_node();
219 } else if let Some((_, value)) = self.tokens.last() {
220 let version_str = value;
221 match version_str.parse() {
222 Ok(v) => {
223 version = Some(v);
224 self.bump();
225 }
226 Err(_) => {
227 self.builder.start_node(ERROR.into());
228 self.errors
229 .push(format!("invalid version: {}", version_str));
230 self.bump();
231 self.builder.finish_node();
232 }
233 }
234 } else {
235 self.builder.start_node(ERROR.into());
236 self.errors.push("expected version value".to_string());
237 self.builder.finish_node();
238 }
239 if self.current() != Some(NEWLINE) {
240 self.builder.start_node(ERROR.into());
241 self.errors.push("expected newline".to_string());
242 self.bump();
243 self.builder.finish_node();
244 } else {
245 self.bump();
246 }
247 self.builder.finish_node();
248 }
249 version
250 }
251
252 fn parse_watch_entry(&mut self) -> bool {
253 loop {
255 self.skip_ws();
256 if self.current() == Some(NEWLINE) {
257 self.bump();
258 } else {
259 break;
260 }
261 }
262 if self.current().is_none() {
263 return false;
264 }
265 self.builder.start_node(ENTRY.into());
266 self.parse_options_list();
267 for i in 0..4 {
268 if self.current() == Some(NEWLINE) || self.current().is_none() {
269 break;
270 }
271 if self.current() == Some(CONTINUATION) {
272 self.bump();
273 self.skip_ws();
274 continue;
275 }
276 if !matches!(self.current(), Some(KEY | VALUE)) {
279 self.builder.start_node(ERROR.into());
280 self.errors.push(format!(
281 "expected value, got {:?} (i={})",
282 self.current(),
283 i
284 ));
285 if self.current().is_some() {
286 self.bump();
287 }
288 self.builder.finish_node();
289 } else {
290 let kind = match i {
295 0 => URL,
296 1 => MATCHING_PATTERN,
297 2 => VERSION_POLICY,
298 3 => SCRIPT,
299 _ => unreachable!(),
300 };
301 self.builder.start_node(kind.into());
302 while is_field_token(self.current()) {
303 self.bump();
304 }
305 self.builder.finish_node();
306 }
307 self.skip_ws();
308 }
309 if self.current() != Some(NEWLINE) && self.current().is_some() {
310 self.builder.start_node(ERROR.into());
311 self.errors
312 .push(format!("expected newline, not {:?}", self.current()));
313 if self.current().is_some() {
314 self.bump();
315 }
316 self.builder.finish_node();
317 } else if self.current().is_some() {
318 self.bump();
320 }
321 self.builder.finish_node();
322 true
323 }
324
325 fn parse_option(&mut self, quoted: bool) -> bool {
331 if self.current().is_none() {
332 return false;
333 }
334 while self.current() == Some(CONTINUATION) {
335 self.bump();
336 }
337 if !quoted && self.current() == Some(WHITESPACE) {
338 return false;
339 }
340 if quoted && self.current() == Some(QUOTE) {
341 return false;
342 }
343 if !quoted && self.current() != Some(KEY) {
348 return false;
349 }
350 self.builder.start_node(OPTION.into());
351 if self.current() != Some(KEY) {
352 self.builder.start_node(ERROR.into());
353 self.errors.push("expected key".to_string());
354 self.bump();
355 self.builder.finish_node();
356 } else {
357 self.bump();
358 }
359 if self.current() == Some(EQUALS) {
360 self.bump();
361 let mut consumed_value = false;
366 loop {
367 match self.current() {
368 Some(KEY) | Some(VALUE) => {
369 self.bump();
370 consumed_value = true;
371 }
372 Some(EQUALS) if consumed_value => self.bump(),
373 Some(WHITESPACE) if quoted => {
374 break;
379 }
380 _ => break,
381 }
382 }
383 if !consumed_value {
384 self.builder.start_node(ERROR.into());
385 self.errors
386 .push(format!("expected value, got {:?}", self.current()));
387 if self.current().is_some() {
388 self.bump();
389 }
390 self.builder.finish_node();
391 }
392 } else if self.current() == Some(COMMA) {
393 } else {
394 self.builder.start_node(ERROR.into());
395 self.errors.push("expected `=`".to_string());
396 if self.current().is_some() {
397 self.bump();
398 }
399 self.builder.finish_node();
400 }
401 self.builder.finish_node();
402 true
403 }
404
405 fn parse_options_list(&mut self) {
406 self.skip_ws();
407 if self.tokens.last() == Some(&(KEY, "opts".to_string()))
408 || self.tokens.last() == Some(&(KEY, "options".to_string()))
409 {
410 self.builder.start_node(OPTS_LIST.into());
411 self.bump();
412 self.skip_ws();
413 if self.current() != Some(EQUALS) {
414 self.builder.start_node(ERROR.into());
415 self.errors.push("expected `=`".to_string());
416 if self.current().is_some() {
417 self.bump();
418 }
419 self.builder.finish_node();
420 } else {
421 self.bump();
422 }
423 let quoted = if self.current() == Some(QUOTE) {
424 self.bump();
425 true
426 } else {
427 false
428 };
429 loop {
430 if quoted {
431 self.skip_ws();
437 if self.current() == Some(QUOTE) {
438 self.bump();
439 break;
440 }
441 }
442 if !self.parse_option(quoted) {
443 break;
444 }
445 if quoted {
446 self.skip_ws();
449 }
450 if self.current() == Some(COMMA) {
451 self.builder.start_node(OPTION_SEPARATOR.into());
452 self.bump();
453 self.builder.finish_node();
454 } else if !quoted {
455 break;
456 }
457 }
458 self.builder.finish_node();
459 self.skip_ws();
460 }
461 }
462
463 fn parse(mut self) -> InternalParse {
464 self.builder.start_node(ROOT.into());
466 while self.current() == Some(WHITESPACE)
468 || self.current() == Some(CONTINUATION)
469 || self.current() == Some(COMMENT)
470 || self.current() == Some(NEWLINE)
471 {
472 self.bump();
473 }
474 if let Some(_v) = self.parse_version() {
475 }
477 loop {
479 if !self.parse_watch_entry() {
480 break;
481 }
482 }
483 self.skip_ws();
485 if self.current().is_some() {
488 self.builder.start_node(ERROR.into());
489 self.errors
490 .push("unexpected tokens after last entry".to_string());
491 while self.current().is_some() {
492 self.bump();
493 }
494 self.builder.finish_node();
495 }
496 self.builder.finish_node();
498
499 InternalParse {
501 green_node: self.builder.finish(),
502 errors: self.errors,
503 }
504 }
505 fn bump(&mut self) {
507 if let Some((kind, text)) = self.tokens.pop() {
508 self.builder.token(kind.into(), text.as_str());
509 }
510 }
511 fn current(&self) -> Option<SyntaxKind> {
513 self.tokens.last().map(|(kind, _)| *kind)
514 }
515 fn skip_ws(&mut self) {
516 while self.current() == Some(WHITESPACE)
517 || self.current() == Some(CONTINUATION)
518 || self.current() == Some(COMMENT)
519 {
520 self.bump()
521 }
522 }
523 }
524
525 let mut tokens = lex(text);
526 tokens.reverse();
527 Parser {
528 tokens,
529 builder: GreenNodeBuilder::new(),
530 errors: Vec::new(),
531 }
532 .parse()
533}
534
535type SyntaxNode = rowan::SyntaxNode<Lang>;
541#[allow(unused)]
542type SyntaxToken = rowan::SyntaxToken<Lang>;
543#[allow(unused)]
544type SyntaxElement = rowan::NodeOrToken<SyntaxNode, SyntaxToken>;
545
546impl InternalParse {
547 fn syntax(&self) -> SyntaxNode {
548 SyntaxNode::new_root_mut(self.green_node.clone())
549 }
550
551 fn root(&self) -> WatchFile {
552 WatchFile::cast(self.syntax()).expect("root node should be a WatchFile")
553 }
554}
555
556fn line_col_at_offset(node: &SyntaxNode, offset: rowan::TextSize) -> (usize, usize) {
559 let root = node.ancestors().last().unwrap_or_else(|| node.clone());
560 let mut line = 0;
561 let mut last_newline_offset = rowan::TextSize::from(0);
562
563 for element in root.preorder_with_tokens() {
564 if let rowan::WalkEvent::Enter(rowan::NodeOrToken::Token(token)) = element {
565 if token.text_range().start() >= offset {
566 break;
567 }
568
569 for (idx, _) in token.text().match_indices('\n') {
571 line += 1;
572 last_newline_offset =
573 token.text_range().start() + rowan::TextSize::from((idx + 1) as u32);
574 }
575 }
576 }
577
578 let column: usize = (offset - last_newline_offset).into();
579 (line, column)
580}
581
582macro_rules! ast_node {
583 ($ast:ident, $kind:ident) => {
584 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
585 #[repr(transparent)]
586 pub struct $ast(SyntaxNode);
588 impl $ast {
589 #[allow(unused)]
590 fn cast(node: SyntaxNode) -> Option<Self> {
591 if node.kind() == $kind {
592 Some(Self(node))
593 } else {
594 None
595 }
596 }
597
598 pub fn text_range(&self) -> rowan::TextRange {
605 self.0.text_range()
606 }
607
608 pub fn line(&self) -> usize {
610 line_col_at_offset(&self.0, self.0.text_range().start()).0
611 }
612
613 pub fn column(&self) -> usize {
615 line_col_at_offset(&self.0, self.0.text_range().start()).1
616 }
617
618 pub fn line_col(&self) -> (usize, usize) {
621 line_col_at_offset(&self.0, self.0.text_range().start())
622 }
623 }
624
625 impl std::fmt::Display for $ast {
626 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
627 write!(f, "{}", self.0.text())
628 }
629 }
630 };
631}
632
633ast_node!(WatchFile, ROOT);
634ast_node!(Version, VERSION);
635ast_node!(Entry, ENTRY);
636ast_node!(_Option, OPTION);
637ast_node!(Url, URL);
638ast_node!(MatchingPattern, MATCHING_PATTERN);
639ast_node!(VersionPolicyNode, VERSION_POLICY);
640ast_node!(ScriptNode, SCRIPT);
641
642#[derive(Clone, PartialEq, Eq, Hash)]
644#[repr(transparent)]
645pub struct OptionList(SyntaxNode);
647
648impl OptionList {
649 #[allow(unused)]
650 fn cast(node: SyntaxNode) -> Option<Self> {
651 if node.kind() == OPTS_LIST {
652 Some(Self(node))
653 } else {
654 None
655 }
656 }
657
658 pub fn text_range(&self) -> rowan::TextRange {
660 self.0.text_range()
661 }
662
663 pub fn line(&self) -> usize {
665 line_col_at_offset(&self.0, self.0.text_range().start()).0
666 }
667
668 pub fn column(&self) -> usize {
670 line_col_at_offset(&self.0, self.0.text_range().start()).1
671 }
672
673 pub fn line_col(&self) -> (usize, usize) {
676 line_col_at_offset(&self.0, self.0.text_range().start())
677 }
678}
679
680impl std::fmt::Display for OptionList {
681 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
682 write!(f, "{}", self.0.text())
683 }
684}
685
686impl WatchFile {
687 pub fn syntax(&self) -> &SyntaxNode {
689 &self.0
690 }
691
692 pub fn snapshot(&self) -> Self {
695 WatchFile(SyntaxNode::new_root_mut(self.0.green().into_owned()))
696 }
697
698 pub fn tree_eq(&self, other: &Self) -> bool {
702 let a = self.0.green();
703 let b = other.0.green();
704 let a_ref: &rowan::GreenNodeData = &a;
705 let b_ref: &rowan::GreenNodeData = &b;
706 std::ptr::eq(a_ref as *const _, b_ref as *const _) || a_ref == b_ref
707 }
708
709 pub fn new(version: Option<u32>) -> WatchFile {
711 let mut builder = GreenNodeBuilder::new();
712
713 builder.start_node(ROOT.into());
714 if let Some(version) = version {
715 builder.start_node(VERSION.into());
716 builder.token(KEY.into(), "version");
717 builder.token(EQUALS.into(), "=");
718 builder.token(VALUE.into(), version.to_string().as_str());
719 builder.token(NEWLINE.into(), "\n");
720 builder.finish_node();
721 }
722 builder.finish_node();
723 WatchFile(SyntaxNode::new_root_mut(builder.finish()))
724 }
725
726 pub fn version_node(&self) -> Option<Version> {
728 self.0.children().find_map(Version::cast)
729 }
730
731 pub fn version(&self) -> u32 {
733 self.version_node()
734 .map(|it| it.version())
735 .unwrap_or(DEFAULT_VERSION)
736 }
737
738 pub fn entries(&self) -> impl Iterator<Item = Entry> + '_ {
740 self.0.children().filter_map(Entry::cast)
741 }
742
743 pub fn set_version(&mut self, new_version: u32) {
745 let mut builder = GreenNodeBuilder::new();
747 builder.start_node(VERSION.into());
748 builder.token(KEY.into(), "version");
749 builder.token(EQUALS.into(), "=");
750 builder.token(VALUE.into(), new_version.to_string().as_str());
751 builder.token(NEWLINE.into(), "\n");
752 builder.finish_node();
753 let new_version_green = builder.finish();
754
755 let new_version_node = SyntaxNode::new_root_mut(new_version_green);
757
758 let version_pos = self.0.children().position(|child| child.kind() == VERSION);
760
761 if let Some(pos) = version_pos {
762 self.0
764 .splice_children(pos..pos + 1, vec![new_version_node.into()]);
765 } else {
766 self.0.splice_children(0..0, vec![new_version_node.into()]);
768 }
769 }
770
771 #[cfg(feature = "discover")]
791 pub async fn uscan(
792 &self,
793 package: impl Fn() -> String + Send + Sync,
794 ) -> Result<Vec<Vec<crate::Release>>, Box<dyn std::error::Error>> {
795 let mut all_releases = Vec::new();
796
797 for entry in self.entries() {
798 let parsed_entry = crate::parse::ParsedEntry::LineBased(entry);
799 let releases = parsed_entry.discover(|| package()).await?;
800 all_releases.push(releases);
801 }
802
803 Ok(all_releases)
804 }
805
806 #[cfg(all(feature = "discover", feature = "blocking"))]
824 pub fn uscan_blocking(
825 &self,
826 package: impl Fn() -> String,
827 ) -> Result<Vec<Vec<crate::Release>>, Box<dyn std::error::Error>> {
828 let mut all_releases = Vec::new();
829
830 for entry in self.entries() {
831 let parsed_entry = crate::parse::ParsedEntry::LineBased(entry);
832 let releases = parsed_entry.discover_blocking(|| package())?;
833 all_releases.push(releases);
834 }
835
836 Ok(all_releases)
837 }
838
839 pub fn add_entry(&mut self, entry: Entry) -> Entry {
866 let insert_pos = self.0.children_with_tokens().count();
868
869 let entry_green = entry.0.green().into_owned();
871 let new_entry_node = SyntaxNode::new_root_mut(entry_green);
872
873 self.0
875 .splice_children(insert_pos..insert_pos, vec![new_entry_node.into()]);
876
877 Entry::cast(
879 self.0
880 .children()
881 .nth(insert_pos)
882 .expect("Entry was just inserted"),
883 )
884 .expect("Inserted node should be an Entry")
885 }
886
887 pub fn from_reader<R: std::io::Read>(reader: R) -> Result<WatchFile, ParseError> {
889 let mut buf_reader = std::io::BufReader::new(reader);
890 let mut content = String::new();
891 buf_reader
892 .read_to_string(&mut content)
893 .map_err(|e| ParseError(vec![e.to_string()]))?;
894 content.parse()
895 }
896
897 pub fn from_reader_relaxed<R: std::io::Read>(mut r: R) -> Result<Self, std::io::Error> {
899 let mut content = String::new();
900 r.read_to_string(&mut content)?;
901 let parsed = parse(&content);
902 Ok(parsed.root())
903 }
904
905 pub fn from_str_relaxed(s: &str) -> Self {
907 let parsed = parse(s);
908 parsed.root()
909 }
910}
911
912impl FromStr for WatchFile {
913 type Err = ParseError;
914
915 fn from_str(s: &str) -> Result<Self, Self::Err> {
916 let parsed = parse(s);
917 if parsed.errors.is_empty() {
918 Ok(parsed.root())
919 } else {
920 Err(ParseError(parsed.errors))
921 }
922 }
923}
924
925pub fn parse_watch_file(text: &str) -> Parse<WatchFile> {
928 let parsed = parse(text);
929 Parse::new(parsed.green_node, parsed.errors)
930}
931
932impl Version {
933 pub fn version(&self) -> u32 {
935 self.0
936 .children_with_tokens()
937 .find_map(|it| match it {
938 SyntaxElement::Token(token) => {
939 if token.kind() == VALUE {
940 token.text().parse().ok()
941 } else {
942 None
943 }
944 }
945 _ => None,
946 })
947 .unwrap_or(DEFAULT_VERSION)
948 }
949}
950
951#[derive(Debug, Clone, Default)]
975pub struct EntryBuilder {
976 url: Option<String>,
977 matching_pattern: Option<String>,
978 version_policy: Option<String>,
979 script: Option<String>,
980 opts: std::collections::HashMap<String, String>,
981}
982
983impl EntryBuilder {
984 pub fn new(url: impl Into<String>) -> Self {
986 EntryBuilder {
987 url: Some(url.into()),
988 matching_pattern: None,
989 version_policy: None,
990 script: None,
991 opts: std::collections::HashMap::new(),
992 }
993 }
994
995 pub fn matching_pattern(mut self, pattern: impl Into<String>) -> Self {
997 self.matching_pattern = Some(pattern.into());
998 self
999 }
1000
1001 pub fn version_policy(mut self, policy: impl Into<String>) -> Self {
1003 self.version_policy = Some(policy.into());
1004 self
1005 }
1006
1007 pub fn script(mut self, script: impl Into<String>) -> Self {
1009 self.script = Some(script.into());
1010 self
1011 }
1012
1013 pub fn opt(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
1015 self.opts.insert(key.into(), value.into());
1016 self
1017 }
1018
1019 pub fn flag(mut self, key: impl Into<String>) -> Self {
1023 self.opts.insert(key.into(), String::new());
1024 self
1025 }
1026
1027 pub fn build(self) -> Entry {
1033 let url = self.url.expect("URL is required for entry");
1034
1035 let mut builder = GreenNodeBuilder::new();
1036
1037 builder.start_node(ENTRY.into());
1038
1039 if !self.opts.is_empty() {
1041 builder.start_node(OPTS_LIST.into());
1042 builder.token(KEY.into(), "opts");
1043 builder.token(EQUALS.into(), "=");
1044
1045 let mut first = true;
1046 for (key, value) in self.opts.iter() {
1047 if !first {
1048 builder.token(COMMA.into(), ",");
1049 }
1050 first = false;
1051
1052 builder.start_node(OPTION.into());
1053 builder.token(KEY.into(), key);
1054 if !value.is_empty() {
1055 builder.token(EQUALS.into(), "=");
1056 builder.token(VALUE.into(), value);
1057 }
1058 builder.finish_node();
1059 }
1060
1061 builder.finish_node();
1062 builder.token(WHITESPACE.into(), " ");
1063 }
1064
1065 builder.start_node(URL.into());
1067 builder.token(VALUE.into(), &url);
1068 builder.finish_node();
1069
1070 if let Some(pattern) = self.matching_pattern {
1072 builder.token(WHITESPACE.into(), " ");
1073 builder.start_node(MATCHING_PATTERN.into());
1074 builder.token(VALUE.into(), &pattern);
1075 builder.finish_node();
1076 }
1077
1078 if let Some(policy) = self.version_policy {
1080 builder.token(WHITESPACE.into(), " ");
1081 builder.start_node(VERSION_POLICY.into());
1082 builder.token(VALUE.into(), &policy);
1083 builder.finish_node();
1084 }
1085
1086 if let Some(script_val) = self.script {
1088 builder.token(WHITESPACE.into(), " ");
1089 builder.start_node(SCRIPT.into());
1090 builder.token(VALUE.into(), &script_val);
1091 builder.finish_node();
1092 }
1093
1094 builder.token(NEWLINE.into(), "\n");
1095 builder.finish_node();
1096
1097 Entry(SyntaxNode::new_root_mut(builder.finish()))
1098 }
1099}
1100
1101impl Entry {
1102 pub fn syntax(&self) -> &SyntaxNode {
1104 &self.0
1105 }
1106
1107 pub fn builder(url: impl Into<String>) -> EntryBuilder {
1121 EntryBuilder::new(url)
1122 }
1123
1124 pub fn option_list(&self) -> Option<OptionList> {
1126 self.0.children().find_map(OptionList::cast)
1127 }
1128
1129 pub fn get_option(&self, key: &str) -> Option<String> {
1131 self.option_list().and_then(|ol| ol.get_option(key))
1132 }
1133
1134 pub fn has_option(&self, key: &str) -> bool {
1136 self.option_list().is_some_and(|ol| ol.has_option(key))
1137 }
1138
1139 pub fn component(&self) -> Option<String> {
1141 self.get_option("component")
1142 }
1143
1144 pub fn ctype(&self) -> Result<Option<ComponentType>, ()> {
1146 self.try_ctype().map_err(|_| ())
1147 }
1148
1149 pub fn try_ctype(&self) -> Result<Option<ComponentType>, crate::types::ParseError> {
1151 self.get_option("ctype").map(|s| s.parse()).transpose()
1152 }
1153
1154 pub fn compression(&self) -> Result<Option<Compression>, ()> {
1156 self.try_compression().map_err(|_| ())
1157 }
1158
1159 pub fn try_compression(&self) -> Result<Option<Compression>, crate::types::ParseError> {
1161 self.get_option("compression")
1162 .map(|s| s.parse())
1163 .transpose()
1164 }
1165
1166 pub fn repack(&self) -> bool {
1168 self.has_option("repack")
1169 }
1170
1171 pub fn repacksuffix(&self) -> Option<String> {
1173 self.get_option("repacksuffix")
1174 }
1175
1176 pub fn mode(&self) -> Result<Mode, ()> {
1178 self.try_mode().map_err(|_| ())
1179 }
1180
1181 pub fn try_mode(&self) -> Result<Mode, crate::types::ParseError> {
1183 Ok(self
1184 .get_option("mode")
1185 .map(|s| s.parse())
1186 .transpose()?
1187 .unwrap_or_default())
1188 }
1189
1190 pub fn pretty(&self) -> Result<Pretty, ()> {
1192 self.try_pretty().map_err(|_| ())
1193 }
1194
1195 pub fn try_pretty(&self) -> Result<Pretty, crate::types::ParseError> {
1197 Ok(self
1198 .get_option("pretty")
1199 .map(|s| s.parse())
1200 .transpose()?
1201 .unwrap_or_default())
1202 }
1203
1204 pub fn date(&self) -> String {
1207 self.get_option("date").unwrap_or_else(|| "%Y%m%d".into())
1208 }
1209
1210 pub fn gitexport(&self) -> Result<GitExport, ()> {
1212 self.try_gitexport().map_err(|_| ())
1213 }
1214
1215 pub fn try_gitexport(&self) -> Result<GitExport, crate::types::ParseError> {
1217 Ok(self
1218 .get_option("gitexport")
1219 .map(|s| s.parse())
1220 .transpose()?
1221 .unwrap_or_default())
1222 }
1223
1224 pub fn gitmode(&self) -> Result<GitMode, ()> {
1226 self.try_gitmode().map_err(|_| ())
1227 }
1228
1229 pub fn try_gitmode(&self) -> Result<GitMode, crate::types::ParseError> {
1231 Ok(self
1232 .get_option("gitmode")
1233 .map(|s| s.parse())
1234 .transpose()?
1235 .unwrap_or_default())
1236 }
1237
1238 pub fn pgpmode(&self) -> Result<PgpMode, ()> {
1240 self.try_pgpmode().map_err(|_| ())
1241 }
1242
1243 pub fn try_pgpmode(&self) -> Result<PgpMode, crate::types::ParseError> {
1245 Ok(self
1246 .get_option("pgpmode")
1247 .map(|s| s.parse())
1248 .transpose()?
1249 .unwrap_or_default())
1250 }
1251
1252 pub fn searchmode(&self) -> Result<SearchMode, ()> {
1254 self.try_searchmode().map_err(|_| ())
1255 }
1256
1257 pub fn try_searchmode(&self) -> Result<SearchMode, crate::types::ParseError> {
1259 Ok(self
1260 .get_option("searchmode")
1261 .map(|s| s.parse())
1262 .transpose()?
1263 .unwrap_or_default())
1264 }
1265
1266 pub fn decompress(&self) -> bool {
1268 self.has_option("decompress")
1269 }
1270
1271 pub fn bare(&self) -> bool {
1274 self.has_option("bare")
1275 }
1276
1277 pub fn user_agent(&self) -> Option<String> {
1279 self.get_option("user-agent")
1280 }
1281
1282 pub fn passive(&self) -> Option<bool> {
1284 if self.has_option("passive") || self.has_option("pasv") {
1285 Some(true)
1286 } else if self.has_option("active") || self.has_option("nopasv") {
1287 Some(false)
1288 } else {
1289 None
1290 }
1291 }
1292
1293 pub fn unzipoptions(&self) -> Option<String> {
1296 self.get_option("unzipopt")
1297 }
1298
1299 pub fn dversionmangle(&self) -> Option<String> {
1301 self.get_option("dversionmangle")
1302 .or_else(|| self.get_option("versionmangle"))
1303 }
1304
1305 pub fn dirversionmangle(&self) -> Option<String> {
1309 self.get_option("dirversionmangle")
1310 }
1311
1312 pub fn pagemangle(&self) -> Option<String> {
1314 self.get_option("pagemangle")
1315 }
1316
1317 pub fn uversionmangle(&self) -> Option<String> {
1321 self.get_option("uversionmangle")
1322 .or_else(|| self.get_option("versionmangle"))
1323 }
1324
1325 pub fn versionmangle(&self) -> Option<String> {
1327 self.get_option("versionmangle")
1328 }
1329
1330 pub fn hrefdecode(&self) -> bool {
1335 self.get_option("hrefdecode").is_some()
1336 }
1337
1338 pub fn downloadurlmangle(&self) -> Option<String> {
1341 self.get_option("downloadurlmangle")
1342 }
1343
1344 pub fn filenamemangle(&self) -> Option<String> {
1352 self.get_option("filenamemangle")
1353 }
1354
1355 pub fn pgpsigurlmangle(&self) -> Option<String> {
1357 self.get_option("pgpsigurlmangle")
1358 }
1359
1360 pub fn oversionmangle(&self) -> Option<String> {
1363 self.get_option("oversionmangle")
1364 }
1365
1366 pub fn apply_uversionmangle(
1379 &self,
1380 version: &str,
1381 ) -> Result<String, crate::mangle::MangleError> {
1382 if let Some(vm) = self.uversionmangle() {
1383 crate::mangle::apply_mangle(&vm, version)
1384 } else {
1385 Ok(version.to_string())
1386 }
1387 }
1388
1389 pub fn apply_dversionmangle(
1402 &self,
1403 version: &str,
1404 ) -> Result<String, crate::mangle::MangleError> {
1405 if let Some(vm) = self.dversionmangle() {
1406 crate::mangle::apply_mangle(&vm, version)
1407 } else {
1408 Ok(version.to_string())
1409 }
1410 }
1411
1412 pub fn apply_oversionmangle(
1425 &self,
1426 version: &str,
1427 ) -> Result<String, crate::mangle::MangleError> {
1428 if let Some(vm) = self.oversionmangle() {
1429 crate::mangle::apply_mangle(&vm, version)
1430 } else {
1431 Ok(version.to_string())
1432 }
1433 }
1434
1435 pub fn apply_dirversionmangle(
1448 &self,
1449 version: &str,
1450 ) -> Result<String, crate::mangle::MangleError> {
1451 if let Some(vm) = self.dirversionmangle() {
1452 crate::mangle::apply_mangle(&vm, version)
1453 } else {
1454 Ok(version.to_string())
1455 }
1456 }
1457
1458 pub fn apply_filenamemangle(&self, url: &str) -> Result<String, crate::mangle::MangleError> {
1474 if let Some(vm) = self.filenamemangle() {
1475 crate::mangle::apply_mangle(&vm, url)
1476 } else {
1477 Ok(url.to_string())
1478 }
1479 }
1480
1481 pub fn apply_pagemangle(&self, page: &[u8]) -> Result<Vec<u8>, crate::mangle::MangleError> {
1497 if let Some(vm) = self.pagemangle() {
1498 let page_str = String::from_utf8_lossy(page);
1499 let mangled = crate::mangle::apply_mangle(&vm, &page_str)?;
1500 Ok(mangled.into_bytes())
1501 } else {
1502 Ok(page.to_vec())
1503 }
1504 }
1505
1506 pub fn apply_downloadurlmangle(&self, url: &str) -> Result<String, crate::mangle::MangleError> {
1522 if let Some(vm) = self.downloadurlmangle() {
1523 crate::mangle::apply_mangle(&vm, url)
1524 } else {
1525 Ok(url.to_string())
1526 }
1527 }
1528
1529 pub fn opts(&self) -> std::collections::HashMap<String, String> {
1531 let mut options = std::collections::HashMap::new();
1532
1533 if let Some(ol) = self.option_list() {
1534 for opt in ol.options() {
1535 let key = opt.key();
1536 let value = opt.value();
1537 if let (Some(key), Some(value)) = (key, value) {
1538 options.insert(key.to_string(), value.to_string());
1539 }
1540 }
1541 }
1542
1543 options
1544 }
1545
1546 fn items(&self) -> impl Iterator<Item = String> + '_ {
1547 self.0.children_with_tokens().filter_map(|it| match it {
1548 SyntaxElement::Token(token) => {
1549 if token.kind() == VALUE || token.kind() == KEY {
1550 Some(token.text().to_string())
1551 } else {
1552 None
1553 }
1554 }
1555 SyntaxElement::Node(node) => {
1556 match node.kind() {
1558 URL => Url::cast(node).map(|n| n.url()),
1559 MATCHING_PATTERN => MatchingPattern::cast(node).map(|n| n.pattern()),
1560 VERSION_POLICY => VersionPolicyNode::cast(node).map(|n| n.policy()),
1561 SCRIPT => ScriptNode::cast(node).map(|n| n.script()),
1562 _ => None,
1563 }
1564 }
1565 })
1566 }
1567
1568 pub fn url_node(&self) -> Option<Url> {
1570 self.0.children().find_map(Url::cast)
1571 }
1572
1573 pub fn url(&self) -> String {
1575 self.url_node()
1576 .map(|it| it.url())
1577 .or_else(|| self.items().next())
1578 .unwrap_or_default()
1579 }
1580
1581 pub fn matching_pattern_node(&self) -> Option<MatchingPattern> {
1583 self.0.children().find_map(MatchingPattern::cast)
1584 }
1585
1586 pub fn matching_pattern(&self) -> Option<String> {
1588 self.matching_pattern_node()
1589 .map(|it| it.pattern())
1590 .or_else(|| {
1591 self.items().nth(1)
1593 })
1594 }
1595
1596 pub fn version_node(&self) -> Option<VersionPolicyNode> {
1598 self.0.children().find_map(VersionPolicyNode::cast)
1599 }
1600
1601 pub fn version(&self) -> Result<Option<crate::VersionPolicy>, String> {
1603 self.version_node()
1604 .map(|it| it.policy().parse())
1605 .transpose()
1606 .map_err(|e: crate::types::ParseError| e.to_string())
1607 .or_else(|_e| {
1608 self.items()
1610 .nth(2)
1611 .map(|it| it.parse())
1612 .transpose()
1613 .map_err(|e: crate::types::ParseError| e.to_string())
1614 })
1615 }
1616
1617 pub fn script_node(&self) -> Option<ScriptNode> {
1619 self.0.children().find_map(ScriptNode::cast)
1620 }
1621
1622 pub fn script(&self) -> Option<String> {
1624 self.script_node().map(|it| it.script()).or_else(|| {
1625 self.items().nth(3)
1627 })
1628 }
1629
1630 pub fn format_url(
1632 &self,
1633 package: impl FnOnce() -> String,
1634 component: impl FnOnce() -> String,
1635 ) -> url::Url {
1636 crate::subst::subst(self.url().as_str(), package, component)
1637 .parse()
1638 .unwrap()
1639 }
1640
1641 pub fn set_url(&mut self, new_url: &str) {
1643 let mut builder = GreenNodeBuilder::new();
1645 builder.start_node(URL.into());
1646 builder.token(VALUE.into(), new_url);
1647 builder.finish_node();
1648 let new_url_green = builder.finish();
1649
1650 let new_url_node = SyntaxNode::new_root_mut(new_url_green);
1652
1653 let url_pos = self
1655 .0
1656 .children_with_tokens()
1657 .position(|child| matches!(child, SyntaxElement::Node(node) if node.kind() == URL));
1658
1659 if let Some(pos) = url_pos {
1660 self.0
1662 .splice_children(pos..pos + 1, vec![new_url_node.into()]);
1663 }
1664 }
1665
1666 pub fn set_matching_pattern(&mut self, new_pattern: &str) {
1672 let mut builder = GreenNodeBuilder::new();
1674 builder.start_node(MATCHING_PATTERN.into());
1675 builder.token(VALUE.into(), new_pattern);
1676 builder.finish_node();
1677 let new_pattern_green = builder.finish();
1678
1679 let new_pattern_node = SyntaxNode::new_root_mut(new_pattern_green);
1681
1682 let pattern_pos = self.0.children_with_tokens().position(
1684 |child| matches!(child, SyntaxElement::Node(node) if node.kind() == MATCHING_PATTERN),
1685 );
1686
1687 if let Some(pos) = pattern_pos {
1688 self.0
1690 .splice_children(pos..pos + 1, vec![new_pattern_node.into()]);
1691 }
1692 }
1694
1695 pub fn set_version_policy(&mut self, new_policy: &str) {
1701 let mut builder = GreenNodeBuilder::new();
1703 builder.start_node(VERSION_POLICY.into());
1704 builder.token(VALUE.into(), new_policy);
1706 builder.finish_node();
1707 let new_policy_green = builder.finish();
1708
1709 let new_policy_node = SyntaxNode::new_root_mut(new_policy_green);
1711
1712 let policy_pos = self.0.children_with_tokens().position(
1714 |child| matches!(child, SyntaxElement::Node(node) if node.kind() == VERSION_POLICY),
1715 );
1716
1717 if let Some(pos) = policy_pos {
1718 self.0
1720 .splice_children(pos..pos + 1, vec![new_policy_node.into()]);
1721 }
1722 }
1724
1725 pub fn set_script(&mut self, new_script: &str) {
1731 let mut builder = GreenNodeBuilder::new();
1733 builder.start_node(SCRIPT.into());
1734 builder.token(VALUE.into(), new_script);
1736 builder.finish_node();
1737 let new_script_green = builder.finish();
1738
1739 let new_script_node = SyntaxNode::new_root_mut(new_script_green);
1741
1742 let script_pos = self
1744 .0
1745 .children_with_tokens()
1746 .position(|child| matches!(child, SyntaxElement::Node(node) if node.kind() == SCRIPT));
1747
1748 if let Some(pos) = script_pos {
1749 self.0
1751 .splice_children(pos..pos + 1, vec![new_script_node.into()]);
1752 }
1753 }
1755
1756 pub fn set_option(&mut self, option: crate::types::WatchOption) {
1762 let key = watch_option_to_key(&option);
1763 let value = watch_option_to_value(&option);
1764 self.set_opt(key, &value);
1765 }
1766
1767 pub fn set_opt(&mut self, key: &str, value: &str) {
1773 let opts_pos = self.0.children_with_tokens().position(
1775 |child| matches!(child, SyntaxElement::Node(node) if node.kind() == OPTS_LIST),
1776 );
1777
1778 if let Some(_opts_idx) = opts_pos {
1779 if let Some(mut ol) = self.option_list() {
1780 if let Some(mut opt) = ol.find_option(key) {
1782 opt.set_value(value);
1784 } else {
1786 ol.add_option(key, value);
1788 }
1790 }
1791 } else {
1792 let mut builder = GreenNodeBuilder::new();
1794 builder.start_node(OPTS_LIST.into());
1795 builder.token(KEY.into(), "opts");
1796 builder.token(EQUALS.into(), "=");
1797 builder.start_node(OPTION.into());
1798 builder.token(KEY.into(), key);
1799 builder.token(EQUALS.into(), "=");
1800 builder.token(VALUE.into(), value);
1801 builder.finish_node();
1802 builder.finish_node();
1803 let new_opts_green = builder.finish();
1804 let new_opts_node = SyntaxNode::new_root_mut(new_opts_green);
1805
1806 let url_pos = self
1808 .0
1809 .children_with_tokens()
1810 .position(|child| matches!(child, SyntaxElement::Node(node) if node.kind() == URL));
1811
1812 if let Some(url_idx) = url_pos {
1813 let mut combined_builder = GreenNodeBuilder::new();
1816 combined_builder.start_node(ROOT.into()); combined_builder.token(WHITESPACE.into(), " ");
1818 combined_builder.finish_node();
1819 let temp_green = combined_builder.finish();
1820 let temp_root = SyntaxNode::new_root_mut(temp_green);
1821 let space_element = temp_root.children_with_tokens().next().unwrap();
1822
1823 self.0
1824 .splice_children(url_idx..url_idx, vec![new_opts_node.into(), space_element]);
1825 } else {
1826 self.0.splice_children(0..0, vec![new_opts_node.into()]);
1827 }
1828 }
1829 }
1830
1831 pub fn del_opt(&mut self, option: crate::types::WatchOption) {
1838 let key = watch_option_to_key(&option);
1839 if let Some(mut ol) = self.option_list() {
1840 let option_count = ol.0.children().filter(|n| n.kind() == OPTION).count();
1841
1842 if option_count == 1 && ol.has_option(key) {
1843 let opts_pos = self.0.children().position(|node| node.kind() == OPTS_LIST);
1845
1846 if let Some(opts_idx) = opts_pos {
1847 self.0.splice_children(opts_idx..opts_idx + 1, vec![]);
1849
1850 while self.0.children_with_tokens().next().is_some_and(|e| {
1852 matches!(
1853 e,
1854 SyntaxElement::Token(t) if t.kind() == WHITESPACE || t.kind() == CONTINUATION
1855 )
1856 }) {
1857 self.0.splice_children(0..1, vec![]);
1858 }
1859 }
1860 } else {
1861 ol.remove_option(key);
1863 }
1864 }
1865 }
1866
1867 pub fn del_opt_str(&mut self, key: &str) {
1874 if let Some(mut ol) = self.option_list() {
1875 let option_count = ol.0.children().filter(|n| n.kind() == OPTION).count();
1876
1877 if option_count == 1 && ol.has_option(key) {
1878 let opts_pos = self.0.children().position(|node| node.kind() == OPTS_LIST);
1880
1881 if let Some(opts_idx) = opts_pos {
1882 self.0.splice_children(opts_idx..opts_idx + 1, vec![]);
1884
1885 while self.0.children_with_tokens().next().is_some_and(|e| {
1887 matches!(
1888 e,
1889 SyntaxElement::Token(t) if t.kind() == WHITESPACE || t.kind() == CONTINUATION
1890 )
1891 }) {
1892 self.0.splice_children(0..1, vec![]);
1893 }
1894 }
1895 } else {
1896 ol.remove_option(key);
1898 }
1899 }
1900 }
1901}
1902
1903impl std::fmt::Debug for OptionList {
1904 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1905 f.debug_struct("OptionList")
1906 .field("text", &self.0.text().to_string())
1907 .finish()
1908 }
1909}
1910
1911impl OptionList {
1912 pub fn options(&self) -> impl Iterator<Item = _Option> + '_ {
1914 self.0.children().filter_map(_Option::cast)
1915 }
1916
1917 pub fn find_option(&self, key: &str) -> Option<_Option> {
1919 self.options().find(|opt| opt.key().as_deref() == Some(key))
1920 }
1921
1922 pub fn has_option(&self, key: &str) -> bool {
1924 self.options().any(|it| it.key().as_deref() == Some(key))
1925 }
1926
1927 #[cfg(feature = "deb822")]
1930 pub(crate) fn iter_key_values(&self) -> impl Iterator<Item = (String, String)> + '_ {
1931 self.options().filter_map(|opt| {
1932 if let (Some(key), Some(value)) = (opt.key(), opt.value()) {
1933 Some((key, value))
1934 } else {
1935 None
1936 }
1937 })
1938 }
1939
1940 pub fn get_option(&self, key: &str) -> Option<String> {
1942 for child in self.options() {
1943 if child.key().as_deref() == Some(key) {
1944 return child.value();
1945 }
1946 }
1947 None
1948 }
1949
1950 fn add_option(&mut self, key: &str, value: &str) {
1952 let option_count = self.0.children().filter(|n| n.kind() == OPTION).count();
1953
1954 let mut builder = GreenNodeBuilder::new();
1956 builder.start_node(ROOT.into()); if option_count > 0 {
1959 builder.start_node(OPTION_SEPARATOR.into());
1960 builder.token(COMMA.into(), ",");
1961 builder.finish_node();
1962 }
1963
1964 builder.start_node(OPTION.into());
1965 builder.token(KEY.into(), key);
1966 builder.token(EQUALS.into(), "=");
1967 builder.token(VALUE.into(), value);
1968 builder.finish_node();
1969
1970 builder.finish_node(); let combined_green = builder.finish();
1972
1973 let temp_root = SyntaxNode::new_root_mut(combined_green);
1975 let new_children: Vec<_> = temp_root.children_with_tokens().collect();
1976
1977 let insert_pos = self.0.children_with_tokens().count();
1978 self.0.splice_children(insert_pos..insert_pos, new_children);
1979 }
1980
1981 fn remove_option(&mut self, key: &str) -> bool {
1983 if let Some(mut opt) = self.find_option(key) {
1984 opt.remove();
1985 true
1986 } else {
1987 false
1988 }
1989 }
1990}
1991
1992impl _Option {
1993 pub fn key(&self) -> Option<String> {
1995 self.0.children_with_tokens().find_map(|it| match it {
1996 SyntaxElement::Token(token) => {
1997 if token.kind() == KEY {
1998 Some(token.text().to_string())
1999 } else {
2000 None
2001 }
2002 }
2003 _ => None,
2004 })
2005 }
2006
2007 pub fn value(&self) -> Option<String> {
2009 self.0
2010 .children_with_tokens()
2011 .filter_map(|it| match it {
2012 SyntaxElement::Token(token) => {
2013 if token.kind() == VALUE || token.kind() == KEY {
2014 Some(token.text().to_string())
2015 } else {
2016 None
2017 }
2018 }
2019 _ => None,
2020 })
2021 .nth(1)
2022 }
2023
2024 pub fn set_value(&mut self, new_value: &str) {
2026 let key = self.key().expect("Option must have a key");
2027
2028 let mut builder = GreenNodeBuilder::new();
2030 builder.start_node(OPTION.into());
2031 builder.token(KEY.into(), &key);
2032 builder.token(EQUALS.into(), "=");
2033 builder.token(VALUE.into(), new_value);
2034 builder.finish_node();
2035 let new_option_green = builder.finish();
2036 let new_option_node = SyntaxNode::new_root_mut(new_option_green);
2037
2038 if let Some(parent) = self.0.parent() {
2040 let idx = self.0.index();
2041 parent.splice_children(idx..idx + 1, vec![new_option_node.into()]);
2042 }
2043 }
2044
2045 pub fn remove(&mut self) {
2047 let next_sep = self
2049 .0
2050 .next_sibling()
2051 .filter(|n| n.kind() == OPTION_SEPARATOR);
2052 let prev_sep = self
2053 .0
2054 .prev_sibling()
2055 .filter(|n| n.kind() == OPTION_SEPARATOR);
2056
2057 if let Some(sep) = next_sep {
2059 sep.detach();
2060 } else if let Some(sep) = prev_sep {
2061 sep.detach();
2062 }
2063
2064 self.0.detach();
2066 }
2067}
2068
2069fn join_tokens(node: &SyntaxNode, keep: impl Fn(SyntaxKind) -> bool) -> String {
2074 let mut out = String::new();
2075 for it in node.children_with_tokens() {
2076 if let SyntaxElement::Token(token) = it {
2077 if keep(token.kind()) {
2078 out.push_str(token.text());
2079 }
2080 }
2081 }
2082 out
2083}
2084
2085impl Url {
2086 pub fn url(&self) -> String {
2088 join_tokens(&self.0, |k| {
2089 matches!(k, KEY | VALUE | EQUALS | COMMA | QUOTE)
2090 })
2091 }
2092}
2093
2094impl MatchingPattern {
2095 pub fn pattern(&self) -> String {
2097 join_tokens(&self.0, |k| {
2098 matches!(k, KEY | VALUE | EQUALS | COMMA | QUOTE)
2099 })
2100 }
2101}
2102
2103impl VersionPolicyNode {
2104 pub fn policy(&self) -> String {
2106 join_tokens(&self.0, |k| {
2107 matches!(k, KEY | VALUE | EQUALS | COMMA | QUOTE)
2108 })
2109 }
2110}
2111
2112impl ScriptNode {
2113 pub fn script(&self) -> String {
2115 join_tokens(&self.0, |k| {
2116 matches!(k, KEY | VALUE | EQUALS | COMMA | QUOTE)
2117 })
2118 }
2119}
2120
2121#[cfg(test)]
2122mod tests {
2123 use super::*;
2124
2125 #[test]
2126 fn test_entry_node_structure() {
2127 let wf: super::WatchFile = r#"version=4
2129opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2130"#
2131 .parse()
2132 .unwrap();
2133
2134 let entry = wf.entries().next().unwrap();
2135
2136 assert_eq!(entry.0.children().find(|n| n.kind() == URL).is_some(), true);
2138 assert_eq!(entry.url(), "https://example.com/releases");
2139
2140 assert_eq!(
2142 entry
2143 .0
2144 .children()
2145 .find(|n| n.kind() == MATCHING_PATTERN)
2146 .is_some(),
2147 true
2148 );
2149 assert_eq!(
2150 entry.matching_pattern(),
2151 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2152 );
2153
2154 assert_eq!(
2156 entry
2157 .0
2158 .children()
2159 .find(|n| n.kind() == VERSION_POLICY)
2160 .is_some(),
2161 true
2162 );
2163 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2164
2165 assert_eq!(
2167 entry.0.children().find(|n| n.kind() == SCRIPT).is_some(),
2168 true
2169 );
2170 assert_eq!(entry.script(), Some("uupdate".into()));
2171 }
2172
2173 #[test]
2174 fn test_entry_node_structure_partial() {
2175 let wf: super::WatchFile = r#"version=4
2177https://github.com/example/tags .*/v?(\d\S+)\.tar\.gz
2178"#
2179 .parse()
2180 .unwrap();
2181
2182 let entry = wf.entries().next().unwrap();
2183
2184 assert_eq!(entry.0.children().find(|n| n.kind() == URL).is_some(), true);
2186 assert_eq!(
2187 entry
2188 .0
2189 .children()
2190 .find(|n| n.kind() == MATCHING_PATTERN)
2191 .is_some(),
2192 true
2193 );
2194
2195 assert_eq!(
2197 entry
2198 .0
2199 .children()
2200 .find(|n| n.kind() == VERSION_POLICY)
2201 .is_some(),
2202 false
2203 );
2204 assert_eq!(
2205 entry.0.children().find(|n| n.kind() == SCRIPT).is_some(),
2206 false
2207 );
2208
2209 assert_eq!(entry.url(), "https://github.com/example/tags");
2211 assert_eq!(
2212 entry.matching_pattern(),
2213 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2214 );
2215 assert_eq!(entry.version(), Ok(None));
2216 assert_eq!(entry.script(), None);
2217 }
2218
2219 #[test]
2220 fn test_parse_v1() {
2221 const WATCHV1: &str = r#"version=4
2222opts=bare,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1\.tar\.gz/ \
2223 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2224"#;
2225 let parsed = parse(WATCHV1);
2226 let node = parsed.syntax();
2228 assert_eq!(
2229 format!("{:#?}", node),
2230 r#"ROOT@0..161
2231 VERSION@0..10
2232 KEY@0..7 "version"
2233 EQUALS@7..8 "="
2234 VALUE@8..9 "4"
2235 NEWLINE@9..10 "\n"
2236 ENTRY@10..161
2237 OPTS_LIST@10..86
2238 KEY@10..14 "opts"
2239 EQUALS@14..15 "="
2240 OPTION@15..19
2241 KEY@15..19 "bare"
2242 OPTION_SEPARATOR@19..20
2243 COMMA@19..20 ","
2244 OPTION@20..86
2245 KEY@20..34 "filenamemangle"
2246 EQUALS@34..35 "="
2247 VALUE@35..86 "s/.+\\/v?(\\d\\S+)\\.tar\\ ..."
2248 WHITESPACE@86..87 " "
2249 CONTINUATION@87..89 "\\\n"
2250 WHITESPACE@89..91 " "
2251 URL@91..138
2252 VALUE@91..138 "https://github.com/sy ..."
2253 WHITESPACE@138..139 " "
2254 MATCHING_PATTERN@139..160
2255 VALUE@139..160 ".*/v?(\\d\\S+)\\.tar\\.gz"
2256 NEWLINE@160..161 "\n"
2257"#
2258 );
2259
2260 let root = parsed.root();
2261 assert_eq!(root.version(), 4);
2262 let entries = root.entries().collect::<Vec<_>>();
2263 assert_eq!(entries.len(), 1);
2264 let entry = &entries[0];
2265 assert_eq!(
2266 entry.url(),
2267 "https://github.com/syncthing/syncthing-gtk/tags"
2268 );
2269 assert_eq!(
2270 entry.matching_pattern(),
2271 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2272 );
2273 assert_eq!(entry.version(), Ok(None));
2274 assert_eq!(entry.script(), None);
2275
2276 assert_eq!(node.text(), WATCHV1);
2277 }
2278
2279 #[test]
2280 fn test_parse_v2() {
2281 let parsed = parse(
2282 r#"version=4
2283https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2284# comment
2285"#,
2286 );
2287 assert_eq!(parsed.errors, Vec::<String>::new());
2288 let node = parsed.syntax();
2289 assert_eq!(
2290 format!("{:#?}", node),
2291 r###"ROOT@0..90
2292 VERSION@0..10
2293 KEY@0..7 "version"
2294 EQUALS@7..8 "="
2295 VALUE@8..9 "4"
2296 NEWLINE@9..10 "\n"
2297 ENTRY@10..80
2298 URL@10..57
2299 VALUE@10..57 "https://github.com/sy ..."
2300 WHITESPACE@57..58 " "
2301 MATCHING_PATTERN@58..79
2302 VALUE@58..79 ".*/v?(\\d\\S+)\\.tar\\.gz"
2303 NEWLINE@79..80 "\n"
2304 COMMENT@80..89 "# comment"
2305 NEWLINE@89..90 "\n"
2306"###
2307 );
2308
2309 let root = parsed.root();
2310 assert_eq!(root.version(), 4);
2311 let entries = root.entries().collect::<Vec<_>>();
2312 assert_eq!(entries.len(), 1);
2313 let entry = &entries[0];
2314 assert_eq!(
2315 entry.url(),
2316 "https://github.com/syncthing/syncthing-gtk/tags"
2317 );
2318 assert_eq!(
2319 entry.format_url(|| "syncthing-gtk".to_string(), || String::new()),
2320 "https://github.com/syncthing/syncthing-gtk/tags"
2321 .parse()
2322 .unwrap()
2323 );
2324 }
2325
2326 #[test]
2327 fn test_parse_v3() {
2328 let parsed = parse(
2329 r#"version=4
2330https://github.com/syncthing/@PACKAGE@/tags .*/v?(\d\S+)\.tar\.gz
2331# comment
2332"#,
2333 );
2334 assert_eq!(parsed.errors, Vec::<String>::new());
2335 let root = parsed.root();
2336 assert_eq!(root.version(), 4);
2337 let entries = root.entries().collect::<Vec<_>>();
2338 assert_eq!(entries.len(), 1);
2339 let entry = &entries[0];
2340 assert_eq!(entry.url(), "https://github.com/syncthing/@PACKAGE@/tags");
2341 assert_eq!(
2342 entry.format_url(|| "syncthing-gtk".to_string(), || String::new()),
2343 "https://github.com/syncthing/syncthing-gtk/tags"
2344 .parse()
2345 .unwrap()
2346 );
2347 }
2348
2349 #[test]
2350 fn test_thread_safe_parsing() {
2351 let text = r#"version=4
2352https://github.com/example/example/tags example-(.*)\.tar\.gz
2353"#;
2354
2355 let parsed = parse_watch_file(text);
2356 assert!(parsed.is_ok());
2357 assert_eq!(parsed.errors().len(), 0);
2358
2359 let watchfile = parsed.tree();
2361 assert_eq!(watchfile.version(), 4);
2362 let entries: Vec<_> = watchfile.entries().collect();
2363 assert_eq!(entries.len(), 1);
2364 }
2365
2366 #[test]
2367 fn test_parse_clone_and_eq() {
2368 let text = r#"version=4
2369https://github.com/example/example/tags example-(.*)\.tar\.gz
2370"#;
2371
2372 let parsed1 = parse_watch_file(text);
2373 let parsed2 = parsed1.clone();
2374
2375 assert_eq!(parsed1, parsed2);
2377
2378 let watchfile1 = parsed1.tree();
2380 let watchfile2 = watchfile1.clone();
2381 assert_eq!(watchfile1, watchfile2);
2382 }
2383
2384 #[test]
2385 fn test_parse_v4() {
2386 let cl: super::WatchFile = r#"version=4
2387opts=repack,compression=xz,dversionmangle=s/\+ds//,repacksuffix=+ds \
2388 https://github.com/example/example-cat/tags \
2389 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2390"#
2391 .parse()
2392 .unwrap();
2393 assert_eq!(cl.version(), 4);
2394 let entries = cl.entries().collect::<Vec<_>>();
2395 assert_eq!(entries.len(), 1);
2396 let entry = &entries[0];
2397 assert_eq!(entry.url(), "https://github.com/example/example-cat/tags");
2398 assert_eq!(
2399 entry.matching_pattern(),
2400 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2401 );
2402 assert!(entry.repack());
2403 assert_eq!(entry.compression(), Ok(Some(Compression::Xz)));
2404 assert_eq!(entry.dversionmangle(), Some("s/\\+ds//".into()));
2405 assert_eq!(entry.repacksuffix(), Some("+ds".into()));
2406 assert_eq!(entry.script(), Some("uupdate".into()));
2407 assert_eq!(
2408 entry.format_url(|| "example-cat".to_string(), || String::new()),
2409 "https://github.com/example/example-cat/tags"
2410 .parse()
2411 .unwrap()
2412 );
2413 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
2414 }
2415
2416 #[test]
2417 fn test_git_mode() {
2418 let text = r#"version=3
2419opts="mode=git, gitmode=shallow, pgpmode=gittag" \
2420https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git \
2421refs/tags/(.*) debian
2422"#;
2423 let parsed = parse(text);
2424 assert_eq!(parsed.errors, Vec::<String>::new());
2425 let cl = parsed.root();
2426 assert_eq!(cl.version(), 3);
2427 let entries = cl.entries().collect::<Vec<_>>();
2428 assert_eq!(entries.len(), 1);
2429 let entry = &entries[0];
2430 assert_eq!(
2431 entry.url(),
2432 "https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git"
2433 );
2434 assert_eq!(entry.matching_pattern(), Some("refs/tags/(.*)".into()));
2435 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
2436 assert_eq!(entry.script(), None);
2437 assert_eq!(entry.gitmode(), Ok(GitMode::Shallow));
2438 assert_eq!(entry.pgpmode(), Ok(PgpMode::GitTag));
2439 assert_eq!(entry.mode(), Ok(Mode::Git));
2440 }
2441
2442 #[test]
2443 fn test_parse_quoted() {
2444 const WATCHV1: &str = r#"version=4
2445opts="bare, filenamemangle=blah" \
2446 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2447"#;
2448 let parsed = parse(WATCHV1);
2449 let node = parsed.syntax();
2451
2452 let root = parsed.root();
2453 assert_eq!(root.version(), 4);
2454 let entries = root.entries().collect::<Vec<_>>();
2455 assert_eq!(entries.len(), 1);
2456 let entry = &entries[0];
2457
2458 assert_eq!(
2459 entry.url(),
2460 "https://github.com/syncthing/syncthing-gtk/tags"
2461 );
2462 assert_eq!(
2463 entry.matching_pattern(),
2464 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2465 );
2466 assert_eq!(entry.version(), Ok(None));
2467 assert_eq!(entry.script(), None);
2468
2469 assert_eq!(node.text(), WATCHV1);
2470 }
2471
2472 #[test]
2473 fn test_set_url() {
2474 let wf: super::WatchFile = r#"version=4
2476https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2477"#
2478 .parse()
2479 .unwrap();
2480
2481 let mut entry = wf.entries().next().unwrap();
2482 assert_eq!(
2483 entry.url(),
2484 "https://github.com/syncthing/syncthing-gtk/tags"
2485 );
2486
2487 entry.set_url("https://newurl.example.org/path");
2488 assert_eq!(entry.url(), "https://newurl.example.org/path");
2489 assert_eq!(
2490 entry.matching_pattern(),
2491 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2492 );
2493
2494 assert_eq!(
2496 entry.to_string(),
2497 "https://newurl.example.org/path .*/v?(\\d\\S+)\\.tar\\.gz\n"
2498 );
2499 }
2500
2501 #[test]
2502 fn test_set_url_with_options() {
2503 let wf: super::WatchFile = r#"version=4
2505opts=foo=blah https://foo.com/bar .*/v?(\d\S+)\.tar\.gz
2506"#
2507 .parse()
2508 .unwrap();
2509
2510 let mut entry = wf.entries().next().unwrap();
2511 assert_eq!(entry.url(), "https://foo.com/bar");
2512 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2513
2514 entry.set_url("https://example.com/baz");
2515 assert_eq!(entry.url(), "https://example.com/baz");
2516
2517 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2519 assert_eq!(
2520 entry.matching_pattern(),
2521 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2522 );
2523
2524 assert_eq!(
2526 entry.to_string(),
2527 "opts=foo=blah https://example.com/baz .*/v?(\\d\\S+)\\.tar\\.gz\n"
2528 );
2529 }
2530
2531 #[test]
2532 fn test_set_url_complex() {
2533 let wf: super::WatchFile = r#"version=4
2535opts=bare,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1\.tar\.gz/ \
2536 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2537"#
2538 .parse()
2539 .unwrap();
2540
2541 let mut entry = wf.entries().next().unwrap();
2542 assert_eq!(
2543 entry.url(),
2544 "https://github.com/syncthing/syncthing-gtk/tags"
2545 );
2546
2547 entry.set_url("https://gitlab.com/newproject/tags");
2548 assert_eq!(entry.url(), "https://gitlab.com/newproject/tags");
2549
2550 assert!(entry.bare());
2552 assert_eq!(
2553 entry.filenamemangle(),
2554 Some("s/.+\\/v?(\\d\\S+)\\.tar\\.gz/syncthing-gtk-$1\\.tar\\.gz/".into())
2555 );
2556 assert_eq!(
2557 entry.matching_pattern(),
2558 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2559 );
2560
2561 assert_eq!(
2563 entry.to_string(),
2564 r#"opts=bare,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1\.tar\.gz/ \
2565 https://gitlab.com/newproject/tags .*/v?(\d\S+)\.tar\.gz
2566"#
2567 );
2568 }
2569
2570 #[test]
2571 fn test_set_url_with_all_fields() {
2572 let wf: super::WatchFile = r#"version=4
2574opts=repack,compression=xz,dversionmangle=s/\+ds//,repacksuffix=+ds \
2575 https://github.com/example/example-cat/tags \
2576 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2577"#
2578 .parse()
2579 .unwrap();
2580
2581 let mut entry = wf.entries().next().unwrap();
2582 assert_eq!(entry.url(), "https://github.com/example/example-cat/tags");
2583 assert_eq!(
2584 entry.matching_pattern(),
2585 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2586 );
2587 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2588 assert_eq!(entry.script(), Some("uupdate".into()));
2589
2590 entry.set_url("https://gitlab.example.org/project/releases");
2591 assert_eq!(entry.url(), "https://gitlab.example.org/project/releases");
2592
2593 assert!(entry.repack());
2595 assert_eq!(entry.compression(), Ok(Some(super::Compression::Xz)));
2596 assert_eq!(entry.dversionmangle(), Some("s/\\+ds//".into()));
2597 assert_eq!(entry.repacksuffix(), Some("+ds".into()));
2598 assert_eq!(
2599 entry.matching_pattern(),
2600 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2601 );
2602 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2603 assert_eq!(entry.script(), Some("uupdate".into()));
2604
2605 assert_eq!(
2607 entry.to_string(),
2608 r#"opts=repack,compression=xz,dversionmangle=s/\+ds//,repacksuffix=+ds \
2609 https://gitlab.example.org/project/releases \
2610 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2611"#
2612 );
2613 }
2614
2615 #[test]
2616 fn test_set_url_quoted_options() {
2617 let wf: super::WatchFile = r#"version=4
2619opts="bare, filenamemangle=blah" \
2620 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2621"#
2622 .parse()
2623 .unwrap();
2624
2625 let mut entry = wf.entries().next().unwrap();
2626 assert_eq!(
2627 entry.url(),
2628 "https://github.com/syncthing/syncthing-gtk/tags"
2629 );
2630
2631 entry.set_url("https://example.org/new/path");
2632 assert_eq!(entry.url(), "https://example.org/new/path");
2633
2634 assert_eq!(
2636 entry.to_string(),
2637 r#"opts="bare, filenamemangle=blah" \
2638 https://example.org/new/path .*/v?(\d\S+)\.tar\.gz
2639"#
2640 );
2641 }
2642
2643 #[test]
2644 fn test_set_opt_update_existing() {
2645 let wf: super::WatchFile = r#"version=4
2647opts=foo=blah,bar=baz https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2648"#
2649 .parse()
2650 .unwrap();
2651
2652 let mut entry = wf.entries().next().unwrap();
2653 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2654 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2655
2656 entry.set_opt("foo", "updated");
2657 assert_eq!(entry.get_option("foo"), Some("updated".to_string()));
2658 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2659
2660 assert_eq!(
2662 entry.to_string(),
2663 "opts=foo=updated,bar=baz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2664 );
2665 }
2666
2667 #[test]
2668 fn test_set_opt_add_new() {
2669 let wf: super::WatchFile = r#"version=4
2671opts=foo=blah https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2672"#
2673 .parse()
2674 .unwrap();
2675
2676 let mut entry = wf.entries().next().unwrap();
2677 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2678 assert_eq!(entry.get_option("bar"), None);
2679
2680 entry.set_opt("bar", "baz");
2681 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2682 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2683
2684 assert_eq!(
2686 entry.to_string(),
2687 "opts=foo=blah,bar=baz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2688 );
2689 }
2690
2691 #[test]
2692 fn test_set_opt_create_options_list() {
2693 let wf: super::WatchFile = r#"version=4
2695https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2696"#
2697 .parse()
2698 .unwrap();
2699
2700 let mut entry = wf.entries().next().unwrap();
2701 assert_eq!(entry.option_list(), None);
2702
2703 entry.set_opt("compression", "xz");
2704 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
2705
2706 assert_eq!(
2708 entry.to_string(),
2709 "opts=compression=xz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2710 );
2711 }
2712
2713 #[test]
2714 fn test_del_opt_remove_single() {
2715 let wf: super::WatchFile = r#"version=4
2717opts=foo=blah,bar=baz,qux=quux https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2718"#
2719 .parse()
2720 .unwrap();
2721
2722 let mut entry = wf.entries().next().unwrap();
2723 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2724 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2725 assert_eq!(entry.get_option("qux"), Some("quux".to_string()));
2726
2727 entry.del_opt_str("bar");
2728 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2729 assert_eq!(entry.get_option("bar"), None);
2730 assert_eq!(entry.get_option("qux"), Some("quux".to_string()));
2731
2732 assert_eq!(
2734 entry.to_string(),
2735 "opts=foo=blah,qux=quux https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2736 );
2737 }
2738
2739 #[test]
2740 fn test_del_opt_remove_first() {
2741 let wf: super::WatchFile = r#"version=4
2743opts=foo=blah,bar=baz https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2744"#
2745 .parse()
2746 .unwrap();
2747
2748 let mut entry = wf.entries().next().unwrap();
2749 entry.del_opt_str("foo");
2750 assert_eq!(entry.get_option("foo"), None);
2751 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2752
2753 assert_eq!(
2755 entry.to_string(),
2756 "opts=bar=baz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2757 );
2758 }
2759
2760 #[test]
2761 fn test_del_opt_remove_last() {
2762 let wf: super::WatchFile = r#"version=4
2764opts=foo=blah,bar=baz https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2765"#
2766 .parse()
2767 .unwrap();
2768
2769 let mut entry = wf.entries().next().unwrap();
2770 entry.del_opt_str("bar");
2771 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2772 assert_eq!(entry.get_option("bar"), None);
2773
2774 assert_eq!(
2776 entry.to_string(),
2777 "opts=foo=blah https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2778 );
2779 }
2780
2781 #[test]
2782 fn test_del_opt_remove_only_option() {
2783 let wf: super::WatchFile = r#"version=4
2785opts=foo=blah https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2786"#
2787 .parse()
2788 .unwrap();
2789
2790 let mut entry = wf.entries().next().unwrap();
2791 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2792
2793 entry.del_opt_str("foo");
2794 assert_eq!(entry.get_option("foo"), None);
2795 assert_eq!(entry.option_list(), None);
2796
2797 assert_eq!(
2799 entry.to_string(),
2800 "https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2801 );
2802 }
2803
2804 #[test]
2805 fn test_del_opt_nonexistent() {
2806 let wf: super::WatchFile = r#"version=4
2808opts=foo=blah https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2809"#
2810 .parse()
2811 .unwrap();
2812
2813 let mut entry = wf.entries().next().unwrap();
2814 let original = entry.to_string();
2815
2816 entry.del_opt_str("nonexistent");
2817 assert_eq!(entry.to_string(), original);
2818 }
2819
2820 #[test]
2821 fn test_set_opt_multiple_operations() {
2822 let wf: super::WatchFile = r#"version=4
2824https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2825"#
2826 .parse()
2827 .unwrap();
2828
2829 let mut entry = wf.entries().next().unwrap();
2830
2831 entry.set_opt("compression", "xz");
2832 entry.set_opt("repack", "");
2833 entry.set_opt("dversionmangle", "s/\\+ds//");
2834
2835 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
2836 assert_eq!(
2837 entry.get_option("dversionmangle"),
2838 Some("s/\\+ds//".to_string())
2839 );
2840 }
2841
2842 #[test]
2843 fn test_set_matching_pattern() {
2844 let wf: super::WatchFile = r#"version=4
2846https://github.com/example/tags .*/v?(\d\S+)\.tar\.gz
2847"#
2848 .parse()
2849 .unwrap();
2850
2851 let mut entry = wf.entries().next().unwrap();
2852 assert_eq!(
2853 entry.matching_pattern(),
2854 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2855 );
2856
2857 entry.set_matching_pattern("(?:.*?/)?v?([\\d.]+)\\.tar\\.gz");
2858 assert_eq!(
2859 entry.matching_pattern(),
2860 Some("(?:.*?/)?v?([\\d.]+)\\.tar\\.gz".into())
2861 );
2862
2863 assert_eq!(entry.url(), "https://github.com/example/tags");
2865
2866 assert_eq!(
2868 entry.to_string(),
2869 "https://github.com/example/tags (?:.*?/)?v?([\\d.]+)\\.tar\\.gz\n"
2870 );
2871 }
2872
2873 #[test]
2874 fn test_set_matching_pattern_with_all_fields() {
2875 let wf: super::WatchFile = r#"version=4
2877opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2878"#
2879 .parse()
2880 .unwrap();
2881
2882 let mut entry = wf.entries().next().unwrap();
2883 assert_eq!(
2884 entry.matching_pattern(),
2885 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2886 );
2887
2888 entry.set_matching_pattern(".*/version-([\\d.]+)\\.tar\\.xz");
2889 assert_eq!(
2890 entry.matching_pattern(),
2891 Some(".*/version-([\\d.]+)\\.tar\\.xz".into())
2892 );
2893
2894 assert_eq!(entry.url(), "https://example.com/releases");
2896 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2897 assert_eq!(entry.script(), Some("uupdate".into()));
2898 assert_eq!(entry.compression(), Ok(Some(super::Compression::Xz)));
2899
2900 assert_eq!(
2902 entry.to_string(),
2903 "opts=compression=xz https://example.com/releases .*/version-([\\d.]+)\\.tar\\.xz debian uupdate\n"
2904 );
2905 }
2906
2907 #[test]
2908 fn test_set_version_policy() {
2909 let wf: super::WatchFile = r#"version=4
2911https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2912"#
2913 .parse()
2914 .unwrap();
2915
2916 let mut entry = wf.entries().next().unwrap();
2917 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2918
2919 entry.set_version_policy("previous");
2920 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Previous)));
2921
2922 assert_eq!(entry.url(), "https://example.com/releases");
2924 assert_eq!(
2925 entry.matching_pattern(),
2926 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2927 );
2928 assert_eq!(entry.script(), Some("uupdate".into()));
2929
2930 assert_eq!(
2932 entry.to_string(),
2933 "https://example.com/releases (?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz previous uupdate\n"
2934 );
2935 }
2936
2937 #[test]
2938 fn test_set_version_policy_with_options() {
2939 let wf: super::WatchFile = r#"version=4
2941opts=repack,compression=xz \
2942 https://github.com/example/example-cat/tags \
2943 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2944"#
2945 .parse()
2946 .unwrap();
2947
2948 let mut entry = wf.entries().next().unwrap();
2949 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2950
2951 entry.set_version_policy("ignore");
2952 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Ignore)));
2953
2954 assert_eq!(entry.url(), "https://github.com/example/example-cat/tags");
2956 assert_eq!(
2957 entry.matching_pattern(),
2958 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2959 );
2960 assert_eq!(entry.script(), Some("uupdate".into()));
2961 assert!(entry.repack());
2962
2963 assert_eq!(
2965 entry.to_string(),
2966 r#"opts=repack,compression=xz \
2967 https://github.com/example/example-cat/tags \
2968 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz ignore uupdate
2969"#
2970 );
2971 }
2972
2973 #[test]
2974 fn test_set_script() {
2975 let wf: super::WatchFile = r#"version=4
2977https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2978"#
2979 .parse()
2980 .unwrap();
2981
2982 let mut entry = wf.entries().next().unwrap();
2983 assert_eq!(entry.script(), Some("uupdate".into()));
2984
2985 entry.set_script("uscan");
2986 assert_eq!(entry.script(), Some("uscan".into()));
2987
2988 assert_eq!(entry.url(), "https://example.com/releases");
2990 assert_eq!(
2991 entry.matching_pattern(),
2992 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2993 );
2994 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2995
2996 assert_eq!(
2998 entry.to_string(),
2999 "https://example.com/releases (?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz debian uscan\n"
3000 );
3001 }
3002
3003 #[test]
3004 fn test_set_script_with_options() {
3005 let wf: super::WatchFile = r#"version=4
3007opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
3008"#
3009 .parse()
3010 .unwrap();
3011
3012 let mut entry = wf.entries().next().unwrap();
3013 assert_eq!(entry.script(), Some("uupdate".into()));
3014
3015 entry.set_script("custom-script.sh");
3016 assert_eq!(entry.script(), Some("custom-script.sh".into()));
3017
3018 assert_eq!(entry.url(), "https://example.com/releases");
3020 assert_eq!(
3021 entry.matching_pattern(),
3022 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
3023 );
3024 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
3025 assert_eq!(entry.compression(), Ok(Some(super::Compression::Xz)));
3026
3027 assert_eq!(
3029 entry.to_string(),
3030 "opts=compression=xz https://example.com/releases (?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz debian custom-script.sh\n"
3031 );
3032 }
3033
3034 #[test]
3035 fn test_apply_dversionmangle() {
3036 let wf: super::WatchFile = r#"version=4
3038opts=dversionmangle=s/\+dfsg$// https://example.com/ .*
3039"#
3040 .parse()
3041 .unwrap();
3042 let entry = wf.entries().next().unwrap();
3043 assert_eq!(entry.apply_dversionmangle("1.0+dfsg").unwrap(), "1.0");
3044 assert_eq!(entry.apply_dversionmangle("1.0").unwrap(), "1.0");
3045
3046 let wf: super::WatchFile = r#"version=4
3048opts=versionmangle=s/^v// https://example.com/ .*
3049"#
3050 .parse()
3051 .unwrap();
3052 let entry = wf.entries().next().unwrap();
3053 assert_eq!(entry.apply_dversionmangle("v1.0").unwrap(), "1.0");
3054
3055 let wf: super::WatchFile = r#"version=4
3057opts=dversionmangle=s/\+ds//,versionmangle=s/^v// https://example.com/ .*
3058"#
3059 .parse()
3060 .unwrap();
3061 let entry = wf.entries().next().unwrap();
3062 assert_eq!(entry.apply_dversionmangle("1.0+ds").unwrap(), "1.0");
3063
3064 let wf: super::WatchFile = r#"version=4
3066https://example.com/ .*
3067"#
3068 .parse()
3069 .unwrap();
3070 let entry = wf.entries().next().unwrap();
3071 assert_eq!(entry.apply_dversionmangle("1.0+dfsg").unwrap(), "1.0+dfsg");
3072 }
3073
3074 #[test]
3075 fn test_apply_oversionmangle() {
3076 let wf: super::WatchFile = r#"version=4
3078opts=oversionmangle=s/$/-1/ https://example.com/ .*
3079"#
3080 .parse()
3081 .unwrap();
3082 let entry = wf.entries().next().unwrap();
3083 assert_eq!(entry.apply_oversionmangle("1.0").unwrap(), "1.0-1");
3084 assert_eq!(entry.apply_oversionmangle("2.5.3").unwrap(), "2.5.3-1");
3085
3086 let wf: super::WatchFile = r#"version=4
3088opts=oversionmangle=s/$/.dfsg/ https://example.com/ .*
3089"#
3090 .parse()
3091 .unwrap();
3092 let entry = wf.entries().next().unwrap();
3093 assert_eq!(entry.apply_oversionmangle("1.0").unwrap(), "1.0.dfsg");
3094
3095 let wf: super::WatchFile = r#"version=4
3097https://example.com/ .*
3098"#
3099 .parse()
3100 .unwrap();
3101 let entry = wf.entries().next().unwrap();
3102 assert_eq!(entry.apply_oversionmangle("1.0").unwrap(), "1.0");
3103 }
3104
3105 #[test]
3106 fn test_apply_dirversionmangle() {
3107 let wf: super::WatchFile = r#"version=4
3109opts=dirversionmangle=s/^v// https://example.com/ .*
3110"#
3111 .parse()
3112 .unwrap();
3113 let entry = wf.entries().next().unwrap();
3114 assert_eq!(entry.apply_dirversionmangle("v1.0").unwrap(), "1.0");
3115 assert_eq!(entry.apply_dirversionmangle("v2.5.3").unwrap(), "2.5.3");
3116
3117 let wf: super::WatchFile = r#"version=4
3119opts=dirversionmangle=s/v(\d)/$1/ https://example.com/ .*
3120"#
3121 .parse()
3122 .unwrap();
3123 let entry = wf.entries().next().unwrap();
3124 assert_eq!(entry.apply_dirversionmangle("v1.0").unwrap(), "1.0");
3125
3126 let wf: super::WatchFile = r#"version=4
3128https://example.com/ .*
3129"#
3130 .parse()
3131 .unwrap();
3132 let entry = wf.entries().next().unwrap();
3133 assert_eq!(entry.apply_dirversionmangle("v1.0").unwrap(), "v1.0");
3134 }
3135
3136 #[test]
3137 fn test_apply_filenamemangle() {
3138 let wf: super::WatchFile = r#"version=4
3140opts=filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/mypackage-$1.tar.gz/ https://example.com/ .*
3141"#
3142 .parse()
3143 .unwrap();
3144 let entry = wf.entries().next().unwrap();
3145 assert_eq!(
3146 entry
3147 .apply_filenamemangle("https://example.com/v1.0.tar.gz")
3148 .unwrap(),
3149 "mypackage-1.0.tar.gz"
3150 );
3151 assert_eq!(
3152 entry
3153 .apply_filenamemangle("https://example.com/2.5.3.tar.gz")
3154 .unwrap(),
3155 "mypackage-2.5.3.tar.gz"
3156 );
3157
3158 let wf: super::WatchFile = r#"version=4
3160opts=filenamemangle=s/.*\/(.*)/$1/ https://example.com/ .*
3161"#
3162 .parse()
3163 .unwrap();
3164 let entry = wf.entries().next().unwrap();
3165 assert_eq!(
3166 entry
3167 .apply_filenamemangle("https://example.com/path/to/file.tar.gz")
3168 .unwrap(),
3169 "file.tar.gz"
3170 );
3171
3172 let wf: super::WatchFile = r#"version=4
3174https://example.com/ .*
3175"#
3176 .parse()
3177 .unwrap();
3178 let entry = wf.entries().next().unwrap();
3179 assert_eq!(
3180 entry
3181 .apply_filenamemangle("https://example.com/file.tar.gz")
3182 .unwrap(),
3183 "https://example.com/file.tar.gz"
3184 );
3185 }
3186
3187 #[test]
3188 fn test_apply_pagemangle() {
3189 let wf: super::WatchFile = r#"version=4
3191opts=pagemangle=s/&/&/g https://example.com/ .*
3192"#
3193 .parse()
3194 .unwrap();
3195 let entry = wf.entries().next().unwrap();
3196 assert_eq!(
3197 entry.apply_pagemangle(b"foo & bar").unwrap(),
3198 b"foo & bar"
3199 );
3200 assert_eq!(
3201 entry
3202 .apply_pagemangle(b"& foo & bar &")
3203 .unwrap(),
3204 b"& foo & bar &"
3205 );
3206
3207 let wf: super::WatchFile = r#"version=4
3209opts=pagemangle=s/<[^>]+>//g https://example.com/ .*
3210"#
3211 .parse()
3212 .unwrap();
3213 let entry = wf.entries().next().unwrap();
3214 assert_eq!(entry.apply_pagemangle(b"<div>text</div>").unwrap(), b"text");
3215
3216 let wf: super::WatchFile = r#"version=4
3218https://example.com/ .*
3219"#
3220 .parse()
3221 .unwrap();
3222 let entry = wf.entries().next().unwrap();
3223 assert_eq!(
3224 entry.apply_pagemangle(b"foo & bar").unwrap(),
3225 b"foo & bar"
3226 );
3227 }
3228
3229 #[test]
3230 fn test_apply_downloadurlmangle() {
3231 let wf: super::WatchFile = r#"version=4
3233opts=downloadurlmangle=s|/archive/|/download/| https://example.com/ .*
3234"#
3235 .parse()
3236 .unwrap();
3237 let entry = wf.entries().next().unwrap();
3238 assert_eq!(
3239 entry
3240 .apply_downloadurlmangle("https://example.com/archive/file.tar.gz")
3241 .unwrap(),
3242 "https://example.com/download/file.tar.gz"
3243 );
3244
3245 let wf: super::WatchFile = r#"version=4
3247opts=downloadurlmangle=s/github\.com/raw.githubusercontent.com/ https://example.com/ .*
3248"#
3249 .parse()
3250 .unwrap();
3251 let entry = wf.entries().next().unwrap();
3252 assert_eq!(
3253 entry
3254 .apply_downloadurlmangle("https://github.com/user/repo/file.tar.gz")
3255 .unwrap(),
3256 "https://raw.githubusercontent.com/user/repo/file.tar.gz"
3257 );
3258
3259 let wf: super::WatchFile = r#"version=4
3261https://example.com/ .*
3262"#
3263 .parse()
3264 .unwrap();
3265 let entry = wf.entries().next().unwrap();
3266 assert_eq!(
3267 entry
3268 .apply_downloadurlmangle("https://example.com/archive/file.tar.gz")
3269 .unwrap(),
3270 "https://example.com/archive/file.tar.gz"
3271 );
3272 }
3273
3274 #[test]
3275 fn test_entry_builder_minimal() {
3276 let entry = super::EntryBuilder::new("https://github.com/example/tags")
3278 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3279 .build();
3280
3281 assert_eq!(entry.url(), "https://github.com/example/tags");
3282 assert_eq!(
3283 entry.matching_pattern().as_deref(),
3284 Some(".*/v?(\\d\\S+)\\.tar\\.gz")
3285 );
3286 assert_eq!(entry.version(), Ok(None));
3287 assert_eq!(entry.script(), None);
3288 assert!(entry.opts().is_empty());
3289 }
3290
3291 #[test]
3292 fn test_entry_builder_url_only() {
3293 let entry = super::EntryBuilder::new("https://example.com/releases").build();
3295
3296 assert_eq!(entry.url(), "https://example.com/releases");
3297 assert_eq!(entry.matching_pattern(), None);
3298 assert_eq!(entry.version(), Ok(None));
3299 assert_eq!(entry.script(), None);
3300 assert!(entry.opts().is_empty());
3301 }
3302
3303 #[test]
3304 fn test_entry_builder_with_all_fields() {
3305 let entry = super::EntryBuilder::new("https://github.com/example/tags")
3307 .matching_pattern(".*/v?(\\d[\\d.]*)\\.tar\\.gz")
3308 .version_policy("debian")
3309 .script("uupdate")
3310 .opt("compression", "xz")
3311 .flag("repack")
3312 .build();
3313
3314 assert_eq!(entry.url(), "https://github.com/example/tags");
3315 assert_eq!(
3316 entry.matching_pattern().as_deref(),
3317 Some(".*/v?(\\d[\\d.]*)\\.tar\\.gz")
3318 );
3319 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
3320 assert_eq!(entry.script(), Some("uupdate".into()));
3321 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
3322 assert!(entry.has_option("repack"));
3323 assert!(entry.repack());
3324 }
3325
3326 #[test]
3327 fn test_entry_builder_multiple_options() {
3328 let entry = super::EntryBuilder::new("https://example.com/tags")
3330 .matching_pattern(".*/v?(\\d+\\.\\d+)\\.tar\\.gz")
3331 .opt("compression", "xz")
3332 .opt("dversionmangle", "s/\\+ds//")
3333 .opt("repacksuffix", "+ds")
3334 .build();
3335
3336 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
3337 assert_eq!(
3338 entry.get_option("dversionmangle"),
3339 Some("s/\\+ds//".to_string())
3340 );
3341 assert_eq!(entry.get_option("repacksuffix"), Some("+ds".to_string()));
3342 }
3343
3344 #[test]
3345 fn test_entry_builder_via_entry() {
3346 let entry = super::Entry::builder("https://github.com/example/tags")
3348 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3349 .version_policy("debian")
3350 .build();
3351
3352 assert_eq!(entry.url(), "https://github.com/example/tags");
3353 assert_eq!(
3354 entry.matching_pattern().as_deref(),
3355 Some(".*/v?(\\d\\S+)\\.tar\\.gz")
3356 );
3357 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
3358 }
3359
3360 #[test]
3361 fn test_watchfile_add_entry_to_empty() {
3362 let mut wf = super::WatchFile::new(Some(4));
3364
3365 let entry = super::EntryBuilder::new("https://github.com/example/tags")
3366 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3367 .build();
3368
3369 wf.add_entry(entry);
3370
3371 assert_eq!(wf.version(), 4);
3372 assert_eq!(wf.entries().count(), 1);
3373
3374 let added_entry = wf.entries().next().unwrap();
3375 assert_eq!(added_entry.url(), "https://github.com/example/tags");
3376 assert_eq!(
3377 added_entry.matching_pattern().as_deref(),
3378 Some(".*/v?(\\d\\S+)\\.tar\\.gz")
3379 );
3380 }
3381
3382 #[test]
3383 fn test_watchfile_add_multiple_entries() {
3384 let mut wf = super::WatchFile::new(Some(4));
3386
3387 wf.add_entry(
3388 super::EntryBuilder::new("https://github.com/example1/tags")
3389 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3390 .build(),
3391 );
3392
3393 wf.add_entry(
3394 super::EntryBuilder::new("https://github.com/example2/releases")
3395 .matching_pattern(".*/(\\d+\\.\\d+)\\.tar\\.gz")
3396 .opt("compression", "xz")
3397 .build(),
3398 );
3399
3400 assert_eq!(wf.entries().count(), 2);
3401
3402 let entries: Vec<_> = wf.entries().collect();
3403 assert_eq!(entries[0].url(), "https://github.com/example1/tags");
3404 assert_eq!(entries[1].url(), "https://github.com/example2/releases");
3405 assert_eq!(entries[1].get_option("compression"), Some("xz".to_string()));
3406 }
3407
3408 #[test]
3409 fn test_watchfile_add_entry_to_existing() {
3410 let mut wf: super::WatchFile = r#"version=4
3412https://example.com/old .*/v?(\\d\\S+)\\.tar\\.gz
3413"#
3414 .parse()
3415 .unwrap();
3416
3417 assert_eq!(wf.entries().count(), 1);
3418
3419 wf.add_entry(
3420 super::EntryBuilder::new("https://github.com/example/new")
3421 .matching_pattern(".*/v?(\\d+\\.\\d+)\\.tar\\.gz")
3422 .opt("compression", "xz")
3423 .version_policy("debian")
3424 .build(),
3425 );
3426
3427 assert_eq!(wf.entries().count(), 2);
3428
3429 let entries: Vec<_> = wf.entries().collect();
3430 assert_eq!(entries[0].url(), "https://example.com/old");
3431 assert_eq!(entries[1].url(), "https://github.com/example/new");
3432 assert_eq!(entries[1].version(), Ok(Some(VersionPolicy::Debian)));
3433 }
3434
3435 #[test]
3436 fn test_entry_builder_formatting() {
3437 let entry = super::EntryBuilder::new("https://github.com/example/tags")
3439 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3440 .opt("compression", "xz")
3441 .flag("repack")
3442 .version_policy("debian")
3443 .script("uupdate")
3444 .build();
3445
3446 let entry_str = entry.to_string();
3447
3448 assert!(entry_str.starts_with("opts="));
3450 assert!(entry_str.contains("https://github.com/example/tags"));
3452 assert!(entry_str.contains(".*/v?(\\d\\S+)\\.tar\\.gz"));
3454 assert!(entry_str.contains("debian"));
3456 assert!(entry_str.contains("uupdate"));
3458 assert!(entry_str.ends_with('\n'));
3460 }
3461
3462 #[test]
3463 fn test_watchfile_add_entry_preserves_format() {
3464 let mut wf = super::WatchFile::new(Some(4));
3466
3467 wf.add_entry(
3468 super::EntryBuilder::new("https://github.com/example/tags")
3469 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3470 .build(),
3471 );
3472
3473 let wf_str = wf.to_string();
3474
3475 assert!(wf_str.starts_with("version=4\n"));
3477 assert!(wf_str.contains("https://github.com/example/tags"));
3479
3480 let reparsed: super::WatchFile = wf_str.parse().unwrap();
3482 assert_eq!(reparsed.version(), 4);
3483 assert_eq!(reparsed.entries().count(), 1);
3484 }
3485
3486 #[test]
3487 fn test_line_col() {
3488 let text = r#"version=4
3489opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
3490"#;
3491 let wf = text.parse::<super::WatchFile>().unwrap();
3492
3493 let version_node = wf.version_node().unwrap();
3495 assert_eq!(version_node.line(), 0);
3496 assert_eq!(version_node.column(), 0);
3497 assert_eq!(version_node.line_col(), (0, 0));
3498
3499 let entries: Vec<_> = wf.entries().collect();
3501 assert_eq!(entries.len(), 1);
3502
3503 assert_eq!(entries[0].line(), 1);
3505 assert_eq!(entries[0].column(), 0);
3506 assert_eq!(entries[0].line_col(), (1, 0));
3507
3508 let option_list = entries[0].option_list().unwrap();
3510 assert_eq!(option_list.line(), 1); let url_node = entries[0].url_node().unwrap();
3513 assert_eq!(url_node.line(), 1); let pattern_node = entries[0].matching_pattern_node().unwrap();
3516 assert_eq!(pattern_node.line(), 1); let version_policy_node = entries[0].version_node().unwrap();
3519 assert_eq!(version_policy_node.line(), 1); let script_node = entries[0].script_node().unwrap();
3522 assert_eq!(script_node.line(), 1); let options: Vec<_> = option_list.options().collect();
3526 assert_eq!(options.len(), 1);
3527 assert_eq!(options[0].key(), Some("compression".to_string()));
3528 assert_eq!(options[0].value(), Some("xz".to_string()));
3529 assert_eq!(options[0].line(), 1); let compression_opt = option_list.find_option("compression").unwrap();
3533 assert_eq!(compression_opt.line(), 1);
3534 assert_eq!(compression_opt.column(), 5); assert_eq!(compression_opt.line_col(), (1, 5));
3536 }
3537
3538 #[test]
3539 fn test_parse_str_relaxed() {
3540 let wf: super::WatchFile = super::WatchFile::from_str_relaxed(
3541 r#"version=4
3542ERRORS IN THIS LINE
3543opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d
3544"#,
3545 );
3546 assert_eq!(wf.version(), 4);
3547 assert_eq!(wf.entries().count(), 2);
3548
3549 let entries = wf.entries().collect::<Vec<_>>();
3550
3551 let entry = &entries[0];
3552 assert_eq!(entry.url(), "ERRORS");
3553
3554 let entry = &entries[1];
3555 assert_eq!(entry.url(), "https://example.com/releases");
3556 assert_eq!(entry.matching_pattern().as_deref(), Some("(?:.*?/)?v?(\\d"));
3557 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
3558 }
3559
3560 #[test]
3561 fn test_parse_entry_with_comment_before() {
3562 let input = concat!(
3566 "version=4\n",
3567 "# try also https://pypi.debian.net/tomoscan/watch\n",
3568 "opts=uversionmangle=s/(rc|a|b|c)/~$1/;s/\\.dev/~dev/ \\\n",
3569 "https://pypi.debian.net/tomoscan/tomoscan-(.+)\\.(?:zip|tgz|tbz|txz|(?:tar\\.(?:gz|bz2|xz)))\n"
3570 );
3571 let wf: super::WatchFile = input.parse().unwrap();
3572 assert_eq!(wf.to_string(), input);
3574 assert_eq!(wf.entries().count(), 1);
3575 let entry = wf.entries().next().unwrap();
3576 assert_eq!(
3577 entry.url(),
3578 "https://pypi.debian.net/tomoscan/tomoscan-(.+)\\.(?:zip|tgz|tbz|txz|(?:tar\\.(?:gz|bz2|xz)))"
3579 );
3580 assert_eq!(
3581 entry.get_option("uversionmangle"),
3582 Some("s/(rc|a|b|c)/~$1/;s/\\.dev/~dev/".to_string())
3583 );
3584 }
3585
3586 #[test]
3587 fn test_parse_multiple_comments_before_entry() {
3588 let input = concat!(
3591 "version=4\n",
3592 "# first comment\n",
3593 "# second comment\n",
3594 "# third comment\n",
3595 "https://example.com/foo foo-(.*).tar.gz\n",
3596 );
3597 let wf: super::WatchFile = input.parse().unwrap();
3598 assert_eq!(wf.to_string(), input);
3599 assert_eq!(wf.entries().count(), 1);
3600 assert_eq!(
3601 wf.entries().next().unwrap().url(),
3602 "https://example.com/foo"
3603 );
3604 }
3605
3606 #[test]
3607 fn test_parse_blank_lines_between_entries() {
3608 let input = concat!(
3610 "version=4\n",
3611 "https://example.com/foo .*/foo-(\\d+)\\.tar\\.gz\n",
3612 "\n",
3613 "https://example.com/bar .*/bar-(\\d+)\\.tar\\.gz\n",
3614 );
3615 let wf: super::WatchFile = input.parse().unwrap();
3616 assert_eq!(wf.to_string(), input);
3617 assert_eq!(wf.entries().count(), 2);
3618 }
3619
3620 #[test]
3621 fn test_parse_trailing_unparseable_tokens_produce_error() {
3622 let input = "version=4\nhttps://example.com/foo foo-(.*).tar.gz\n=garbage\n";
3625 let result = input.parse::<super::WatchFile>();
3626 assert!(result.is_err(), "expected parse error for trailing garbage");
3627 let wf = super::WatchFile::from_str_relaxed(input);
3629 assert_eq!(wf.to_string(), input);
3630 }
3631
3632 #[test]
3633 fn test_parse_roundtrip_full_file() {
3634 let inputs = [
3636 "version=4\nhttps://example.com/foo foo-(.*).tar.gz\n",
3637 "version=4\n# a comment\nhttps://example.com/foo foo-(.*).tar.gz\n",
3638 concat!(
3639 "version=4\n",
3640 "opts=uversionmangle=s/rc/~rc/ \\\n",
3641 " https://example.com/foo foo-(.*).tar.gz\n",
3642 ),
3643 concat!(
3644 "version=4\n",
3645 "# comment before entry\n",
3646 "opts=uversionmangle=s/rc/~rc/ \\\n",
3647 "https://example.com/foo foo-(.*).tar.gz\n",
3648 "# comment between entries\n",
3649 "https://example.com/bar bar-(.*).tar.gz\n",
3650 ),
3651 ];
3652 for input in &inputs {
3653 let wf: super::WatchFile = input.parse().unwrap();
3654 assert_eq!(
3655 wf.to_string(),
3656 *input,
3657 "round-trip failed for input: {:?}",
3658 input
3659 );
3660 }
3661 }
3662
3663 #[test]
3664 fn test_parse_url_with_equals_in_query_string() {
3665 let input = concat!(
3668 "version=4\n",
3669 "https://api.github.com/repos/x/releases?per_page=100 \\\n",
3670 " https://github.com/x/v[^/]+/x.tar.gz\n",
3671 );
3672 let wf: super::WatchFile = input.parse().unwrap();
3673 let entries: Vec<_> = wf.entries().collect();
3674 assert_eq!(entries.len(), 1);
3675 assert_eq!(
3676 entries[0].url(),
3677 "https://api.github.com/repos/x/releases?per_page=100"
3678 );
3679 assert_eq!(
3680 entries[0].matching_pattern().as_deref(),
3681 Some("https://github.com/x/v[^/]+/x.tar.gz"),
3682 );
3683 assert_eq!(wf.to_string(), input);
3684 }
3685
3686 #[test]
3687 fn test_entry_url_does_not_panic_when_empty() {
3688 let input = "version=4\n=garbage\n";
3691 let wf = super::WatchFile::from_str_relaxed(input);
3692 for entry in wf.entries() {
3693 let _ = entry.url();
3694 }
3695 }
3696
3697 #[test]
3698 fn test_parse_url_node_with_equals_join_tokens() {
3699 let input = "version=4\nhttps://example.com/x?y=1&z=2 .*tar.gz\n";
3702 let wf: super::WatchFile = input.parse().unwrap();
3703 let entry = wf.entries().next().unwrap();
3704 assert_eq!(entry.url(), "https://example.com/x?y=1&z=2");
3705 }
3706
3707 #[test]
3708 fn test_parse_quoted_opts_with_trailing_comma_continuation() {
3709 let input = concat!(
3715 "version=4\n\n",
3716 "opts=\"\\\n",
3717 "pgpmode=none,\\\n",
3718 "repack,compression=xz,repacksuffix=+dfsg,\\\n",
3719 "dversionmangle=s{[+~]dfsg\\d*}{},\\\n",
3720 "\" https://github.com/varlink/go/releases \\\n",
3721 " .*/archive/v?(\\d[\\d\\.]+)\\.tar\\.gz\n",
3722 );
3723 let wf: super::WatchFile = input.parse().unwrap();
3724 let entries: Vec<_> = wf.entries().collect();
3725 assert_eq!(entries.len(), 1);
3726 assert_eq!(entries[0].url(), "https://github.com/varlink/go/releases");
3727 assert_eq!(
3728 entries[0].matching_pattern().as_deref(),
3729 Some(".*/archive/v?(\\d[\\d\\.]+)\\.tar\\.gz"),
3730 );
3731 assert_eq!(wf.to_string(), input);
3732 }
3733
3734 #[test]
3735 fn test_parse_quoted_opts_with_spaces_around_comma() {
3736 let input = concat!(
3739 "version=4\n",
3740 "opts=\"filenamemangle=s/.+\\/v?(\\d\\S*)\\.tar\\.gz/v$1.tar.gz/ , uversionmangle=tr%-rc%~rc%\" \\\n",
3741 " https://github.com/analogdevicesinc/libiio/tags .*/v(\\d\\S*)\\.tar\\.gz\n",
3742 );
3743 let wf: super::WatchFile = input.parse().unwrap();
3744 let entries: Vec<_> = wf.entries().collect();
3745 assert_eq!(entries.len(), 1);
3746 assert_eq!(
3747 entries[0].url(),
3748 "https://github.com/analogdevicesinc/libiio/tags",
3749 );
3750 assert_eq!(wf.to_string(), input);
3751 }
3752
3753 #[test]
3754 fn test_parse_unquoted_opts_trailing_comma_then_url() {
3755 let input = concat!(
3759 "version=3\n",
3760 "opts=uversionmangle=s/(rc|a|b|c)/~$1/,\\\n",
3761 "https://github.com/openstack/rally/tags .*/(\\d\\S*)\\.tar\\.gz\n",
3762 );
3763 let wf: super::WatchFile = input.parse().unwrap();
3764 let entries: Vec<_> = wf.entries().collect();
3765 assert_eq!(entries.len(), 1);
3766 assert_eq!(entries[0].url(), "https://github.com/openstack/rally/tags");
3767 assert_eq!(
3768 entries[0].matching_pattern().as_deref(),
3769 Some(".*/(\\d\\S*)\\.tar\\.gz"),
3770 );
3771 assert_eq!(wf.to_string(), input);
3772 }
3773
3774 #[test]
3775 fn test_parse_unquoted_opts_value_with_equals() {
3776 let input = concat!(
3780 "version=4\n",
3781 "opts=dversionmangle=s/\\~dfsg//,downloadurlmangle=s/.*ref=//,pgpsigurlmangle=s/$/.asc/ \\\n",
3782 "\thttps://downloads.asterisk.org/pub/telephony/libpri/releases/ libpri-([0-9.]*)\\.tar\\.gz debian uupdate\n",
3783 );
3784 let wf: super::WatchFile = input.parse().unwrap();
3785 let entries: Vec<_> = wf.entries().collect();
3786 assert_eq!(entries.len(), 1);
3787 assert_eq!(
3788 entries[0].url(),
3789 "https://downloads.asterisk.org/pub/telephony/libpri/releases/"
3790 );
3791 assert_eq!(
3792 entries[0].matching_pattern().as_deref(),
3793 Some("libpri-([0-9.]*)\\.tar\\.gz"),
3794 );
3795 assert_eq!(wf.to_string(), input);
3796 }
3797}