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 line(&self) -> usize {
600 line_col_at_offset(&self.0, self.0.text_range().start()).0
601 }
602
603 pub fn column(&self) -> usize {
605 line_col_at_offset(&self.0, self.0.text_range().start()).1
606 }
607
608 pub fn line_col(&self) -> (usize, usize) {
611 line_col_at_offset(&self.0, self.0.text_range().start())
612 }
613 }
614
615 impl std::fmt::Display for $ast {
616 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
617 write!(f, "{}", self.0.text())
618 }
619 }
620 };
621}
622
623ast_node!(WatchFile, ROOT);
624ast_node!(Version, VERSION);
625ast_node!(Entry, ENTRY);
626ast_node!(_Option, OPTION);
627ast_node!(Url, URL);
628ast_node!(MatchingPattern, MATCHING_PATTERN);
629ast_node!(VersionPolicyNode, VERSION_POLICY);
630ast_node!(ScriptNode, SCRIPT);
631
632#[derive(Clone, PartialEq, Eq, Hash)]
634#[repr(transparent)]
635pub struct OptionList(SyntaxNode);
637
638impl OptionList {
639 #[allow(unused)]
640 fn cast(node: SyntaxNode) -> Option<Self> {
641 if node.kind() == OPTS_LIST {
642 Some(Self(node))
643 } else {
644 None
645 }
646 }
647
648 pub fn line(&self) -> usize {
650 line_col_at_offset(&self.0, self.0.text_range().start()).0
651 }
652
653 pub fn column(&self) -> usize {
655 line_col_at_offset(&self.0, self.0.text_range().start()).1
656 }
657
658 pub fn line_col(&self) -> (usize, usize) {
661 line_col_at_offset(&self.0, self.0.text_range().start())
662 }
663}
664
665impl std::fmt::Display for OptionList {
666 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
667 write!(f, "{}", self.0.text())
668 }
669}
670
671impl WatchFile {
672 pub fn syntax(&self) -> &SyntaxNode {
674 &self.0
675 }
676
677 pub fn new(version: Option<u32>) -> WatchFile {
679 let mut builder = GreenNodeBuilder::new();
680
681 builder.start_node(ROOT.into());
682 if let Some(version) = version {
683 builder.start_node(VERSION.into());
684 builder.token(KEY.into(), "version");
685 builder.token(EQUALS.into(), "=");
686 builder.token(VALUE.into(), version.to_string().as_str());
687 builder.token(NEWLINE.into(), "\n");
688 builder.finish_node();
689 }
690 builder.finish_node();
691 WatchFile(SyntaxNode::new_root_mut(builder.finish()))
692 }
693
694 pub fn version_node(&self) -> Option<Version> {
696 self.0.children().find_map(Version::cast)
697 }
698
699 pub fn version(&self) -> u32 {
701 self.version_node()
702 .map(|it| it.version())
703 .unwrap_or(DEFAULT_VERSION)
704 }
705
706 pub fn entries(&self) -> impl Iterator<Item = Entry> + '_ {
708 self.0.children().filter_map(Entry::cast)
709 }
710
711 pub fn set_version(&mut self, new_version: u32) {
713 let mut builder = GreenNodeBuilder::new();
715 builder.start_node(VERSION.into());
716 builder.token(KEY.into(), "version");
717 builder.token(EQUALS.into(), "=");
718 builder.token(VALUE.into(), new_version.to_string().as_str());
719 builder.token(NEWLINE.into(), "\n");
720 builder.finish_node();
721 let new_version_green = builder.finish();
722
723 let new_version_node = SyntaxNode::new_root_mut(new_version_green);
725
726 let version_pos = self.0.children().position(|child| child.kind() == VERSION);
728
729 if let Some(pos) = version_pos {
730 self.0
732 .splice_children(pos..pos + 1, vec![new_version_node.into()]);
733 } else {
734 self.0.splice_children(0..0, vec![new_version_node.into()]);
736 }
737 }
738
739 #[cfg(feature = "discover")]
759 pub async fn uscan(
760 &self,
761 package: impl Fn() -> String + Send + Sync,
762 ) -> Result<Vec<Vec<crate::Release>>, Box<dyn std::error::Error>> {
763 let mut all_releases = Vec::new();
764
765 for entry in self.entries() {
766 let parsed_entry = crate::parse::ParsedEntry::LineBased(entry);
767 let releases = parsed_entry.discover(|| package()).await?;
768 all_releases.push(releases);
769 }
770
771 Ok(all_releases)
772 }
773
774 #[cfg(all(feature = "discover", feature = "blocking"))]
792 pub fn uscan_blocking(
793 &self,
794 package: impl Fn() -> String,
795 ) -> Result<Vec<Vec<crate::Release>>, Box<dyn std::error::Error>> {
796 let mut all_releases = Vec::new();
797
798 for entry in self.entries() {
799 let parsed_entry = crate::parse::ParsedEntry::LineBased(entry);
800 let releases = parsed_entry.discover_blocking(|| package())?;
801 all_releases.push(releases);
802 }
803
804 Ok(all_releases)
805 }
806
807 pub fn add_entry(&mut self, entry: Entry) -> Entry {
834 let insert_pos = self.0.children_with_tokens().count();
836
837 let entry_green = entry.0.green().into_owned();
839 let new_entry_node = SyntaxNode::new_root_mut(entry_green);
840
841 self.0
843 .splice_children(insert_pos..insert_pos, vec![new_entry_node.into()]);
844
845 Entry::cast(
847 self.0
848 .children()
849 .nth(insert_pos)
850 .expect("Entry was just inserted"),
851 )
852 .expect("Inserted node should be an Entry")
853 }
854
855 pub fn from_reader<R: std::io::Read>(reader: R) -> Result<WatchFile, ParseError> {
857 let mut buf_reader = std::io::BufReader::new(reader);
858 let mut content = String::new();
859 buf_reader
860 .read_to_string(&mut content)
861 .map_err(|e| ParseError(vec![e.to_string()]))?;
862 content.parse()
863 }
864
865 pub fn from_reader_relaxed<R: std::io::Read>(mut r: R) -> Result<Self, std::io::Error> {
867 let mut content = String::new();
868 r.read_to_string(&mut content)?;
869 let parsed = parse(&content);
870 Ok(parsed.root())
871 }
872
873 pub fn from_str_relaxed(s: &str) -> Self {
875 let parsed = parse(s);
876 parsed.root()
877 }
878}
879
880impl FromStr for WatchFile {
881 type Err = ParseError;
882
883 fn from_str(s: &str) -> Result<Self, Self::Err> {
884 let parsed = parse(s);
885 if parsed.errors.is_empty() {
886 Ok(parsed.root())
887 } else {
888 Err(ParseError(parsed.errors))
889 }
890 }
891}
892
893pub fn parse_watch_file(text: &str) -> Parse<WatchFile> {
896 let parsed = parse(text);
897 Parse::new(parsed.green_node, parsed.errors)
898}
899
900impl Version {
901 pub fn version(&self) -> u32 {
903 self.0
904 .children_with_tokens()
905 .find_map(|it| match it {
906 SyntaxElement::Token(token) => {
907 if token.kind() == VALUE {
908 token.text().parse().ok()
909 } else {
910 None
911 }
912 }
913 _ => None,
914 })
915 .unwrap_or(DEFAULT_VERSION)
916 }
917}
918
919#[derive(Debug, Clone, Default)]
943pub struct EntryBuilder {
944 url: Option<String>,
945 matching_pattern: Option<String>,
946 version_policy: Option<String>,
947 script: Option<String>,
948 opts: std::collections::HashMap<String, String>,
949}
950
951impl EntryBuilder {
952 pub fn new(url: impl Into<String>) -> Self {
954 EntryBuilder {
955 url: Some(url.into()),
956 matching_pattern: None,
957 version_policy: None,
958 script: None,
959 opts: std::collections::HashMap::new(),
960 }
961 }
962
963 pub fn matching_pattern(mut self, pattern: impl Into<String>) -> Self {
965 self.matching_pattern = Some(pattern.into());
966 self
967 }
968
969 pub fn version_policy(mut self, policy: impl Into<String>) -> Self {
971 self.version_policy = Some(policy.into());
972 self
973 }
974
975 pub fn script(mut self, script: impl Into<String>) -> Self {
977 self.script = Some(script.into());
978 self
979 }
980
981 pub fn opt(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
983 self.opts.insert(key.into(), value.into());
984 self
985 }
986
987 pub fn flag(mut self, key: impl Into<String>) -> Self {
991 self.opts.insert(key.into(), String::new());
992 self
993 }
994
995 pub fn build(self) -> Entry {
1001 let url = self.url.expect("URL is required for entry");
1002
1003 let mut builder = GreenNodeBuilder::new();
1004
1005 builder.start_node(ENTRY.into());
1006
1007 if !self.opts.is_empty() {
1009 builder.start_node(OPTS_LIST.into());
1010 builder.token(KEY.into(), "opts");
1011 builder.token(EQUALS.into(), "=");
1012
1013 let mut first = true;
1014 for (key, value) in self.opts.iter() {
1015 if !first {
1016 builder.token(COMMA.into(), ",");
1017 }
1018 first = false;
1019
1020 builder.start_node(OPTION.into());
1021 builder.token(KEY.into(), key);
1022 if !value.is_empty() {
1023 builder.token(EQUALS.into(), "=");
1024 builder.token(VALUE.into(), value);
1025 }
1026 builder.finish_node();
1027 }
1028
1029 builder.finish_node();
1030 builder.token(WHITESPACE.into(), " ");
1031 }
1032
1033 builder.start_node(URL.into());
1035 builder.token(VALUE.into(), &url);
1036 builder.finish_node();
1037
1038 if let Some(pattern) = self.matching_pattern {
1040 builder.token(WHITESPACE.into(), " ");
1041 builder.start_node(MATCHING_PATTERN.into());
1042 builder.token(VALUE.into(), &pattern);
1043 builder.finish_node();
1044 }
1045
1046 if let Some(policy) = self.version_policy {
1048 builder.token(WHITESPACE.into(), " ");
1049 builder.start_node(VERSION_POLICY.into());
1050 builder.token(VALUE.into(), &policy);
1051 builder.finish_node();
1052 }
1053
1054 if let Some(script_val) = self.script {
1056 builder.token(WHITESPACE.into(), " ");
1057 builder.start_node(SCRIPT.into());
1058 builder.token(VALUE.into(), &script_val);
1059 builder.finish_node();
1060 }
1061
1062 builder.token(NEWLINE.into(), "\n");
1063 builder.finish_node();
1064
1065 Entry(SyntaxNode::new_root_mut(builder.finish()))
1066 }
1067}
1068
1069impl Entry {
1070 pub fn syntax(&self) -> &SyntaxNode {
1072 &self.0
1073 }
1074
1075 pub fn builder(url: impl Into<String>) -> EntryBuilder {
1089 EntryBuilder::new(url)
1090 }
1091
1092 pub fn option_list(&self) -> Option<OptionList> {
1094 self.0.children().find_map(OptionList::cast)
1095 }
1096
1097 pub fn get_option(&self, key: &str) -> Option<String> {
1099 self.option_list().and_then(|ol| ol.get_option(key))
1100 }
1101
1102 pub fn has_option(&self, key: &str) -> bool {
1104 self.option_list().is_some_and(|ol| ol.has_option(key))
1105 }
1106
1107 pub fn component(&self) -> Option<String> {
1109 self.get_option("component")
1110 }
1111
1112 pub fn ctype(&self) -> Result<Option<ComponentType>, ()> {
1114 self.try_ctype().map_err(|_| ())
1115 }
1116
1117 pub fn try_ctype(&self) -> Result<Option<ComponentType>, crate::types::ParseError> {
1119 self.get_option("ctype").map(|s| s.parse()).transpose()
1120 }
1121
1122 pub fn compression(&self) -> Result<Option<Compression>, ()> {
1124 self.try_compression().map_err(|_| ())
1125 }
1126
1127 pub fn try_compression(&self) -> Result<Option<Compression>, crate::types::ParseError> {
1129 self.get_option("compression")
1130 .map(|s| s.parse())
1131 .transpose()
1132 }
1133
1134 pub fn repack(&self) -> bool {
1136 self.has_option("repack")
1137 }
1138
1139 pub fn repacksuffix(&self) -> Option<String> {
1141 self.get_option("repacksuffix")
1142 }
1143
1144 pub fn mode(&self) -> Result<Mode, ()> {
1146 self.try_mode().map_err(|_| ())
1147 }
1148
1149 pub fn try_mode(&self) -> Result<Mode, crate::types::ParseError> {
1151 Ok(self
1152 .get_option("mode")
1153 .map(|s| s.parse())
1154 .transpose()?
1155 .unwrap_or_default())
1156 }
1157
1158 pub fn pretty(&self) -> Result<Pretty, ()> {
1160 self.try_pretty().map_err(|_| ())
1161 }
1162
1163 pub fn try_pretty(&self) -> Result<Pretty, crate::types::ParseError> {
1165 Ok(self
1166 .get_option("pretty")
1167 .map(|s| s.parse())
1168 .transpose()?
1169 .unwrap_or_default())
1170 }
1171
1172 pub fn date(&self) -> String {
1175 self.get_option("date").unwrap_or_else(|| "%Y%m%d".into())
1176 }
1177
1178 pub fn gitexport(&self) -> Result<GitExport, ()> {
1180 self.try_gitexport().map_err(|_| ())
1181 }
1182
1183 pub fn try_gitexport(&self) -> Result<GitExport, crate::types::ParseError> {
1185 Ok(self
1186 .get_option("gitexport")
1187 .map(|s| s.parse())
1188 .transpose()?
1189 .unwrap_or_default())
1190 }
1191
1192 pub fn gitmode(&self) -> Result<GitMode, ()> {
1194 self.try_gitmode().map_err(|_| ())
1195 }
1196
1197 pub fn try_gitmode(&self) -> Result<GitMode, crate::types::ParseError> {
1199 Ok(self
1200 .get_option("gitmode")
1201 .map(|s| s.parse())
1202 .transpose()?
1203 .unwrap_or_default())
1204 }
1205
1206 pub fn pgpmode(&self) -> Result<PgpMode, ()> {
1208 self.try_pgpmode().map_err(|_| ())
1209 }
1210
1211 pub fn try_pgpmode(&self) -> Result<PgpMode, crate::types::ParseError> {
1213 Ok(self
1214 .get_option("pgpmode")
1215 .map(|s| s.parse())
1216 .transpose()?
1217 .unwrap_or_default())
1218 }
1219
1220 pub fn searchmode(&self) -> Result<SearchMode, ()> {
1222 self.try_searchmode().map_err(|_| ())
1223 }
1224
1225 pub fn try_searchmode(&self) -> Result<SearchMode, crate::types::ParseError> {
1227 Ok(self
1228 .get_option("searchmode")
1229 .map(|s| s.parse())
1230 .transpose()?
1231 .unwrap_or_default())
1232 }
1233
1234 pub fn decompress(&self) -> bool {
1236 self.has_option("decompress")
1237 }
1238
1239 pub fn bare(&self) -> bool {
1242 self.has_option("bare")
1243 }
1244
1245 pub fn user_agent(&self) -> Option<String> {
1247 self.get_option("user-agent")
1248 }
1249
1250 pub fn passive(&self) -> Option<bool> {
1252 if self.has_option("passive") || self.has_option("pasv") {
1253 Some(true)
1254 } else if self.has_option("active") || self.has_option("nopasv") {
1255 Some(false)
1256 } else {
1257 None
1258 }
1259 }
1260
1261 pub fn unzipoptions(&self) -> Option<String> {
1264 self.get_option("unzipopt")
1265 }
1266
1267 pub fn dversionmangle(&self) -> Option<String> {
1269 self.get_option("dversionmangle")
1270 .or_else(|| self.get_option("versionmangle"))
1271 }
1272
1273 pub fn dirversionmangle(&self) -> Option<String> {
1277 self.get_option("dirversionmangle")
1278 }
1279
1280 pub fn pagemangle(&self) -> Option<String> {
1282 self.get_option("pagemangle")
1283 }
1284
1285 pub fn uversionmangle(&self) -> Option<String> {
1289 self.get_option("uversionmangle")
1290 .or_else(|| self.get_option("versionmangle"))
1291 }
1292
1293 pub fn versionmangle(&self) -> Option<String> {
1295 self.get_option("versionmangle")
1296 }
1297
1298 pub fn hrefdecode(&self) -> bool {
1303 self.get_option("hrefdecode").is_some()
1304 }
1305
1306 pub fn downloadurlmangle(&self) -> Option<String> {
1309 self.get_option("downloadurlmangle")
1310 }
1311
1312 pub fn filenamemangle(&self) -> Option<String> {
1320 self.get_option("filenamemangle")
1321 }
1322
1323 pub fn pgpsigurlmangle(&self) -> Option<String> {
1325 self.get_option("pgpsigurlmangle")
1326 }
1327
1328 pub fn oversionmangle(&self) -> Option<String> {
1331 self.get_option("oversionmangle")
1332 }
1333
1334 pub fn apply_uversionmangle(
1347 &self,
1348 version: &str,
1349 ) -> Result<String, crate::mangle::MangleError> {
1350 if let Some(vm) = self.uversionmangle() {
1351 crate::mangle::apply_mangle(&vm, version)
1352 } else {
1353 Ok(version.to_string())
1354 }
1355 }
1356
1357 pub fn apply_dversionmangle(
1370 &self,
1371 version: &str,
1372 ) -> Result<String, crate::mangle::MangleError> {
1373 if let Some(vm) = self.dversionmangle() {
1374 crate::mangle::apply_mangle(&vm, version)
1375 } else {
1376 Ok(version.to_string())
1377 }
1378 }
1379
1380 pub fn apply_oversionmangle(
1393 &self,
1394 version: &str,
1395 ) -> Result<String, crate::mangle::MangleError> {
1396 if let Some(vm) = self.oversionmangle() {
1397 crate::mangle::apply_mangle(&vm, version)
1398 } else {
1399 Ok(version.to_string())
1400 }
1401 }
1402
1403 pub fn apply_dirversionmangle(
1416 &self,
1417 version: &str,
1418 ) -> Result<String, crate::mangle::MangleError> {
1419 if let Some(vm) = self.dirversionmangle() {
1420 crate::mangle::apply_mangle(&vm, version)
1421 } else {
1422 Ok(version.to_string())
1423 }
1424 }
1425
1426 pub fn apply_filenamemangle(&self, url: &str) -> Result<String, crate::mangle::MangleError> {
1442 if let Some(vm) = self.filenamemangle() {
1443 crate::mangle::apply_mangle(&vm, url)
1444 } else {
1445 Ok(url.to_string())
1446 }
1447 }
1448
1449 pub fn apply_pagemangle(&self, page: &[u8]) -> Result<Vec<u8>, crate::mangle::MangleError> {
1465 if let Some(vm) = self.pagemangle() {
1466 let page_str = String::from_utf8_lossy(page);
1467 let mangled = crate::mangle::apply_mangle(&vm, &page_str)?;
1468 Ok(mangled.into_bytes())
1469 } else {
1470 Ok(page.to_vec())
1471 }
1472 }
1473
1474 pub fn apply_downloadurlmangle(&self, url: &str) -> Result<String, crate::mangle::MangleError> {
1490 if let Some(vm) = self.downloadurlmangle() {
1491 crate::mangle::apply_mangle(&vm, url)
1492 } else {
1493 Ok(url.to_string())
1494 }
1495 }
1496
1497 pub fn opts(&self) -> std::collections::HashMap<String, String> {
1499 let mut options = std::collections::HashMap::new();
1500
1501 if let Some(ol) = self.option_list() {
1502 for opt in ol.options() {
1503 let key = opt.key();
1504 let value = opt.value();
1505 if let (Some(key), Some(value)) = (key, value) {
1506 options.insert(key.to_string(), value.to_string());
1507 }
1508 }
1509 }
1510
1511 options
1512 }
1513
1514 fn items(&self) -> impl Iterator<Item = String> + '_ {
1515 self.0.children_with_tokens().filter_map(|it| match it {
1516 SyntaxElement::Token(token) => {
1517 if token.kind() == VALUE || token.kind() == KEY {
1518 Some(token.text().to_string())
1519 } else {
1520 None
1521 }
1522 }
1523 SyntaxElement::Node(node) => {
1524 match node.kind() {
1526 URL => Url::cast(node).map(|n| n.url()),
1527 MATCHING_PATTERN => MatchingPattern::cast(node).map(|n| n.pattern()),
1528 VERSION_POLICY => VersionPolicyNode::cast(node).map(|n| n.policy()),
1529 SCRIPT => ScriptNode::cast(node).map(|n| n.script()),
1530 _ => None,
1531 }
1532 }
1533 })
1534 }
1535
1536 pub fn url_node(&self) -> Option<Url> {
1538 self.0.children().find_map(Url::cast)
1539 }
1540
1541 pub fn url(&self) -> String {
1543 self.url_node()
1544 .map(|it| it.url())
1545 .or_else(|| self.items().next())
1546 .unwrap_or_default()
1547 }
1548
1549 pub fn matching_pattern_node(&self) -> Option<MatchingPattern> {
1551 self.0.children().find_map(MatchingPattern::cast)
1552 }
1553
1554 pub fn matching_pattern(&self) -> Option<String> {
1556 self.matching_pattern_node()
1557 .map(|it| it.pattern())
1558 .or_else(|| {
1559 self.items().nth(1)
1561 })
1562 }
1563
1564 pub fn version_node(&self) -> Option<VersionPolicyNode> {
1566 self.0.children().find_map(VersionPolicyNode::cast)
1567 }
1568
1569 pub fn version(&self) -> Result<Option<crate::VersionPolicy>, String> {
1571 self.version_node()
1572 .map(|it| it.policy().parse())
1573 .transpose()
1574 .map_err(|e: crate::types::ParseError| e.to_string())
1575 .or_else(|_e| {
1576 self.items()
1578 .nth(2)
1579 .map(|it| it.parse())
1580 .transpose()
1581 .map_err(|e: crate::types::ParseError| e.to_string())
1582 })
1583 }
1584
1585 pub fn script_node(&self) -> Option<ScriptNode> {
1587 self.0.children().find_map(ScriptNode::cast)
1588 }
1589
1590 pub fn script(&self) -> Option<String> {
1592 self.script_node().map(|it| it.script()).or_else(|| {
1593 self.items().nth(3)
1595 })
1596 }
1597
1598 pub fn format_url(
1600 &self,
1601 package: impl FnOnce() -> String,
1602 component: impl FnOnce() -> String,
1603 ) -> url::Url {
1604 crate::subst::subst(self.url().as_str(), package, component)
1605 .parse()
1606 .unwrap()
1607 }
1608
1609 pub fn set_url(&mut self, new_url: &str) {
1611 let mut builder = GreenNodeBuilder::new();
1613 builder.start_node(URL.into());
1614 builder.token(VALUE.into(), new_url);
1615 builder.finish_node();
1616 let new_url_green = builder.finish();
1617
1618 let new_url_node = SyntaxNode::new_root_mut(new_url_green);
1620
1621 let url_pos = self
1623 .0
1624 .children_with_tokens()
1625 .position(|child| matches!(child, SyntaxElement::Node(node) if node.kind() == URL));
1626
1627 if let Some(pos) = url_pos {
1628 self.0
1630 .splice_children(pos..pos + 1, vec![new_url_node.into()]);
1631 }
1632 }
1633
1634 pub fn set_matching_pattern(&mut self, new_pattern: &str) {
1640 let mut builder = GreenNodeBuilder::new();
1642 builder.start_node(MATCHING_PATTERN.into());
1643 builder.token(VALUE.into(), new_pattern);
1644 builder.finish_node();
1645 let new_pattern_green = builder.finish();
1646
1647 let new_pattern_node = SyntaxNode::new_root_mut(new_pattern_green);
1649
1650 let pattern_pos = self.0.children_with_tokens().position(
1652 |child| matches!(child, SyntaxElement::Node(node) if node.kind() == MATCHING_PATTERN),
1653 );
1654
1655 if let Some(pos) = pattern_pos {
1656 self.0
1658 .splice_children(pos..pos + 1, vec![new_pattern_node.into()]);
1659 }
1660 }
1662
1663 pub fn set_version_policy(&mut self, new_policy: &str) {
1669 let mut builder = GreenNodeBuilder::new();
1671 builder.start_node(VERSION_POLICY.into());
1672 builder.token(VALUE.into(), new_policy);
1674 builder.finish_node();
1675 let new_policy_green = builder.finish();
1676
1677 let new_policy_node = SyntaxNode::new_root_mut(new_policy_green);
1679
1680 let policy_pos = self.0.children_with_tokens().position(
1682 |child| matches!(child, SyntaxElement::Node(node) if node.kind() == VERSION_POLICY),
1683 );
1684
1685 if let Some(pos) = policy_pos {
1686 self.0
1688 .splice_children(pos..pos + 1, vec![new_policy_node.into()]);
1689 }
1690 }
1692
1693 pub fn set_script(&mut self, new_script: &str) {
1699 let mut builder = GreenNodeBuilder::new();
1701 builder.start_node(SCRIPT.into());
1702 builder.token(VALUE.into(), new_script);
1704 builder.finish_node();
1705 let new_script_green = builder.finish();
1706
1707 let new_script_node = SyntaxNode::new_root_mut(new_script_green);
1709
1710 let script_pos = self
1712 .0
1713 .children_with_tokens()
1714 .position(|child| matches!(child, SyntaxElement::Node(node) if node.kind() == SCRIPT));
1715
1716 if let Some(pos) = script_pos {
1717 self.0
1719 .splice_children(pos..pos + 1, vec![new_script_node.into()]);
1720 }
1721 }
1723
1724 pub fn set_option(&mut self, option: crate::types::WatchOption) {
1730 let key = watch_option_to_key(&option);
1731 let value = watch_option_to_value(&option);
1732 self.set_opt(key, &value);
1733 }
1734
1735 pub fn set_opt(&mut self, key: &str, value: &str) {
1741 let opts_pos = self.0.children_with_tokens().position(
1743 |child| matches!(child, SyntaxElement::Node(node) if node.kind() == OPTS_LIST),
1744 );
1745
1746 if let Some(_opts_idx) = opts_pos {
1747 if let Some(mut ol) = self.option_list() {
1748 if let Some(mut opt) = ol.find_option(key) {
1750 opt.set_value(value);
1752 } else {
1754 ol.add_option(key, value);
1756 }
1758 }
1759 } else {
1760 let mut builder = GreenNodeBuilder::new();
1762 builder.start_node(OPTS_LIST.into());
1763 builder.token(KEY.into(), "opts");
1764 builder.token(EQUALS.into(), "=");
1765 builder.start_node(OPTION.into());
1766 builder.token(KEY.into(), key);
1767 builder.token(EQUALS.into(), "=");
1768 builder.token(VALUE.into(), value);
1769 builder.finish_node();
1770 builder.finish_node();
1771 let new_opts_green = builder.finish();
1772 let new_opts_node = SyntaxNode::new_root_mut(new_opts_green);
1773
1774 let url_pos = self
1776 .0
1777 .children_with_tokens()
1778 .position(|child| matches!(child, SyntaxElement::Node(node) if node.kind() == URL));
1779
1780 if let Some(url_idx) = url_pos {
1781 let mut combined_builder = GreenNodeBuilder::new();
1784 combined_builder.start_node(ROOT.into()); combined_builder.token(WHITESPACE.into(), " ");
1786 combined_builder.finish_node();
1787 let temp_green = combined_builder.finish();
1788 let temp_root = SyntaxNode::new_root_mut(temp_green);
1789 let space_element = temp_root.children_with_tokens().next().unwrap();
1790
1791 self.0
1792 .splice_children(url_idx..url_idx, vec![new_opts_node.into(), space_element]);
1793 } else {
1794 self.0.splice_children(0..0, vec![new_opts_node.into()]);
1795 }
1796 }
1797 }
1798
1799 pub fn del_opt(&mut self, option: crate::types::WatchOption) {
1806 let key = watch_option_to_key(&option);
1807 if let Some(mut ol) = self.option_list() {
1808 let option_count = ol.0.children().filter(|n| n.kind() == OPTION).count();
1809
1810 if option_count == 1 && ol.has_option(key) {
1811 let opts_pos = self.0.children().position(|node| node.kind() == OPTS_LIST);
1813
1814 if let Some(opts_idx) = opts_pos {
1815 self.0.splice_children(opts_idx..opts_idx + 1, vec![]);
1817
1818 while self.0.children_with_tokens().next().is_some_and(|e| {
1820 matches!(
1821 e,
1822 SyntaxElement::Token(t) if t.kind() == WHITESPACE || t.kind() == CONTINUATION
1823 )
1824 }) {
1825 self.0.splice_children(0..1, vec![]);
1826 }
1827 }
1828 } else {
1829 ol.remove_option(key);
1831 }
1832 }
1833 }
1834
1835 pub fn del_opt_str(&mut self, key: &str) {
1842 if let Some(mut ol) = self.option_list() {
1843 let option_count = ol.0.children().filter(|n| n.kind() == OPTION).count();
1844
1845 if option_count == 1 && ol.has_option(key) {
1846 let opts_pos = self.0.children().position(|node| node.kind() == OPTS_LIST);
1848
1849 if let Some(opts_idx) = opts_pos {
1850 self.0.splice_children(opts_idx..opts_idx + 1, vec![]);
1852
1853 while self.0.children_with_tokens().next().is_some_and(|e| {
1855 matches!(
1856 e,
1857 SyntaxElement::Token(t) if t.kind() == WHITESPACE || t.kind() == CONTINUATION
1858 )
1859 }) {
1860 self.0.splice_children(0..1, vec![]);
1861 }
1862 }
1863 } else {
1864 ol.remove_option(key);
1866 }
1867 }
1868 }
1869}
1870
1871impl std::fmt::Debug for OptionList {
1872 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1873 f.debug_struct("OptionList")
1874 .field("text", &self.0.text().to_string())
1875 .finish()
1876 }
1877}
1878
1879impl OptionList {
1880 pub fn options(&self) -> impl Iterator<Item = _Option> + '_ {
1882 self.0.children().filter_map(_Option::cast)
1883 }
1884
1885 pub fn find_option(&self, key: &str) -> Option<_Option> {
1887 self.options().find(|opt| opt.key().as_deref() == Some(key))
1888 }
1889
1890 pub fn has_option(&self, key: &str) -> bool {
1892 self.options().any(|it| it.key().as_deref() == Some(key))
1893 }
1894
1895 #[cfg(feature = "deb822")]
1898 pub(crate) fn iter_key_values(&self) -> impl Iterator<Item = (String, String)> + '_ {
1899 self.options().filter_map(|opt| {
1900 if let (Some(key), Some(value)) = (opt.key(), opt.value()) {
1901 Some((key, value))
1902 } else {
1903 None
1904 }
1905 })
1906 }
1907
1908 pub fn get_option(&self, key: &str) -> Option<String> {
1910 for child in self.options() {
1911 if child.key().as_deref() == Some(key) {
1912 return child.value();
1913 }
1914 }
1915 None
1916 }
1917
1918 fn add_option(&mut self, key: &str, value: &str) {
1920 let option_count = self.0.children().filter(|n| n.kind() == OPTION).count();
1921
1922 let mut builder = GreenNodeBuilder::new();
1924 builder.start_node(ROOT.into()); if option_count > 0 {
1927 builder.start_node(OPTION_SEPARATOR.into());
1928 builder.token(COMMA.into(), ",");
1929 builder.finish_node();
1930 }
1931
1932 builder.start_node(OPTION.into());
1933 builder.token(KEY.into(), key);
1934 builder.token(EQUALS.into(), "=");
1935 builder.token(VALUE.into(), value);
1936 builder.finish_node();
1937
1938 builder.finish_node(); let combined_green = builder.finish();
1940
1941 let temp_root = SyntaxNode::new_root_mut(combined_green);
1943 let new_children: Vec<_> = temp_root.children_with_tokens().collect();
1944
1945 let insert_pos = self.0.children_with_tokens().count();
1946 self.0.splice_children(insert_pos..insert_pos, new_children);
1947 }
1948
1949 fn remove_option(&mut self, key: &str) -> bool {
1951 if let Some(mut opt) = self.find_option(key) {
1952 opt.remove();
1953 true
1954 } else {
1955 false
1956 }
1957 }
1958}
1959
1960impl _Option {
1961 pub fn key(&self) -> Option<String> {
1963 self.0.children_with_tokens().find_map(|it| match it {
1964 SyntaxElement::Token(token) => {
1965 if token.kind() == KEY {
1966 Some(token.text().to_string())
1967 } else {
1968 None
1969 }
1970 }
1971 _ => None,
1972 })
1973 }
1974
1975 pub fn value(&self) -> Option<String> {
1977 self.0
1978 .children_with_tokens()
1979 .filter_map(|it| match it {
1980 SyntaxElement::Token(token) => {
1981 if token.kind() == VALUE || token.kind() == KEY {
1982 Some(token.text().to_string())
1983 } else {
1984 None
1985 }
1986 }
1987 _ => None,
1988 })
1989 .nth(1)
1990 }
1991
1992 pub fn set_value(&mut self, new_value: &str) {
1994 let key = self.key().expect("Option must have a key");
1995
1996 let mut builder = GreenNodeBuilder::new();
1998 builder.start_node(OPTION.into());
1999 builder.token(KEY.into(), &key);
2000 builder.token(EQUALS.into(), "=");
2001 builder.token(VALUE.into(), new_value);
2002 builder.finish_node();
2003 let new_option_green = builder.finish();
2004 let new_option_node = SyntaxNode::new_root_mut(new_option_green);
2005
2006 if let Some(parent) = self.0.parent() {
2008 let idx = self.0.index();
2009 parent.splice_children(idx..idx + 1, vec![new_option_node.into()]);
2010 }
2011 }
2012
2013 pub fn remove(&mut self) {
2015 let next_sep = self
2017 .0
2018 .next_sibling()
2019 .filter(|n| n.kind() == OPTION_SEPARATOR);
2020 let prev_sep = self
2021 .0
2022 .prev_sibling()
2023 .filter(|n| n.kind() == OPTION_SEPARATOR);
2024
2025 if let Some(sep) = next_sep {
2027 sep.detach();
2028 } else if let Some(sep) = prev_sep {
2029 sep.detach();
2030 }
2031
2032 self.0.detach();
2034 }
2035}
2036
2037fn join_tokens(node: &SyntaxNode, keep: impl Fn(SyntaxKind) -> bool) -> String {
2042 let mut out = String::new();
2043 for it in node.children_with_tokens() {
2044 if let SyntaxElement::Token(token) = it {
2045 if keep(token.kind()) {
2046 out.push_str(token.text());
2047 }
2048 }
2049 }
2050 out
2051}
2052
2053impl Url {
2054 pub fn url(&self) -> String {
2056 join_tokens(&self.0, |k| {
2057 matches!(k, KEY | VALUE | EQUALS | COMMA | QUOTE)
2058 })
2059 }
2060}
2061
2062impl MatchingPattern {
2063 pub fn pattern(&self) -> String {
2065 join_tokens(&self.0, |k| {
2066 matches!(k, KEY | VALUE | EQUALS | COMMA | QUOTE)
2067 })
2068 }
2069}
2070
2071impl VersionPolicyNode {
2072 pub fn policy(&self) -> String {
2074 join_tokens(&self.0, |k| {
2075 matches!(k, KEY | VALUE | EQUALS | COMMA | QUOTE)
2076 })
2077 }
2078}
2079
2080impl ScriptNode {
2081 pub fn script(&self) -> String {
2083 join_tokens(&self.0, |k| {
2084 matches!(k, KEY | VALUE | EQUALS | COMMA | QUOTE)
2085 })
2086 }
2087}
2088
2089#[cfg(test)]
2090mod tests {
2091 use super::*;
2092
2093 #[test]
2094 fn test_entry_node_structure() {
2095 let wf: super::WatchFile = r#"version=4
2097opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2098"#
2099 .parse()
2100 .unwrap();
2101
2102 let entry = wf.entries().next().unwrap();
2103
2104 assert_eq!(entry.0.children().find(|n| n.kind() == URL).is_some(), true);
2106 assert_eq!(entry.url(), "https://example.com/releases");
2107
2108 assert_eq!(
2110 entry
2111 .0
2112 .children()
2113 .find(|n| n.kind() == MATCHING_PATTERN)
2114 .is_some(),
2115 true
2116 );
2117 assert_eq!(
2118 entry.matching_pattern(),
2119 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2120 );
2121
2122 assert_eq!(
2124 entry
2125 .0
2126 .children()
2127 .find(|n| n.kind() == VERSION_POLICY)
2128 .is_some(),
2129 true
2130 );
2131 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2132
2133 assert_eq!(
2135 entry.0.children().find(|n| n.kind() == SCRIPT).is_some(),
2136 true
2137 );
2138 assert_eq!(entry.script(), Some("uupdate".into()));
2139 }
2140
2141 #[test]
2142 fn test_entry_node_structure_partial() {
2143 let wf: super::WatchFile = r#"version=4
2145https://github.com/example/tags .*/v?(\d\S+)\.tar\.gz
2146"#
2147 .parse()
2148 .unwrap();
2149
2150 let entry = wf.entries().next().unwrap();
2151
2152 assert_eq!(entry.0.children().find(|n| n.kind() == URL).is_some(), true);
2154 assert_eq!(
2155 entry
2156 .0
2157 .children()
2158 .find(|n| n.kind() == MATCHING_PATTERN)
2159 .is_some(),
2160 true
2161 );
2162
2163 assert_eq!(
2165 entry
2166 .0
2167 .children()
2168 .find(|n| n.kind() == VERSION_POLICY)
2169 .is_some(),
2170 false
2171 );
2172 assert_eq!(
2173 entry.0.children().find(|n| n.kind() == SCRIPT).is_some(),
2174 false
2175 );
2176
2177 assert_eq!(entry.url(), "https://github.com/example/tags");
2179 assert_eq!(
2180 entry.matching_pattern(),
2181 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2182 );
2183 assert_eq!(entry.version(), Ok(None));
2184 assert_eq!(entry.script(), None);
2185 }
2186
2187 #[test]
2188 fn test_parse_v1() {
2189 const WATCHV1: &str = r#"version=4
2190opts=bare,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1\.tar\.gz/ \
2191 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2192"#;
2193 let parsed = parse(WATCHV1);
2194 let node = parsed.syntax();
2196 assert_eq!(
2197 format!("{:#?}", node),
2198 r#"ROOT@0..161
2199 VERSION@0..10
2200 KEY@0..7 "version"
2201 EQUALS@7..8 "="
2202 VALUE@8..9 "4"
2203 NEWLINE@9..10 "\n"
2204 ENTRY@10..161
2205 OPTS_LIST@10..86
2206 KEY@10..14 "opts"
2207 EQUALS@14..15 "="
2208 OPTION@15..19
2209 KEY@15..19 "bare"
2210 OPTION_SEPARATOR@19..20
2211 COMMA@19..20 ","
2212 OPTION@20..86
2213 KEY@20..34 "filenamemangle"
2214 EQUALS@34..35 "="
2215 VALUE@35..86 "s/.+\\/v?(\\d\\S+)\\.tar\\ ..."
2216 WHITESPACE@86..87 " "
2217 CONTINUATION@87..89 "\\\n"
2218 WHITESPACE@89..91 " "
2219 URL@91..138
2220 VALUE@91..138 "https://github.com/sy ..."
2221 WHITESPACE@138..139 " "
2222 MATCHING_PATTERN@139..160
2223 VALUE@139..160 ".*/v?(\\d\\S+)\\.tar\\.gz"
2224 NEWLINE@160..161 "\n"
2225"#
2226 );
2227
2228 let root = parsed.root();
2229 assert_eq!(root.version(), 4);
2230 let entries = root.entries().collect::<Vec<_>>();
2231 assert_eq!(entries.len(), 1);
2232 let entry = &entries[0];
2233 assert_eq!(
2234 entry.url(),
2235 "https://github.com/syncthing/syncthing-gtk/tags"
2236 );
2237 assert_eq!(
2238 entry.matching_pattern(),
2239 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2240 );
2241 assert_eq!(entry.version(), Ok(None));
2242 assert_eq!(entry.script(), None);
2243
2244 assert_eq!(node.text(), WATCHV1);
2245 }
2246
2247 #[test]
2248 fn test_parse_v2() {
2249 let parsed = parse(
2250 r#"version=4
2251https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2252# comment
2253"#,
2254 );
2255 assert_eq!(parsed.errors, Vec::<String>::new());
2256 let node = parsed.syntax();
2257 assert_eq!(
2258 format!("{:#?}", node),
2259 r###"ROOT@0..90
2260 VERSION@0..10
2261 KEY@0..7 "version"
2262 EQUALS@7..8 "="
2263 VALUE@8..9 "4"
2264 NEWLINE@9..10 "\n"
2265 ENTRY@10..80
2266 URL@10..57
2267 VALUE@10..57 "https://github.com/sy ..."
2268 WHITESPACE@57..58 " "
2269 MATCHING_PATTERN@58..79
2270 VALUE@58..79 ".*/v?(\\d\\S+)\\.tar\\.gz"
2271 NEWLINE@79..80 "\n"
2272 COMMENT@80..89 "# comment"
2273 NEWLINE@89..90 "\n"
2274"###
2275 );
2276
2277 let root = parsed.root();
2278 assert_eq!(root.version(), 4);
2279 let entries = root.entries().collect::<Vec<_>>();
2280 assert_eq!(entries.len(), 1);
2281 let entry = &entries[0];
2282 assert_eq!(
2283 entry.url(),
2284 "https://github.com/syncthing/syncthing-gtk/tags"
2285 );
2286 assert_eq!(
2287 entry.format_url(|| "syncthing-gtk".to_string(), || String::new()),
2288 "https://github.com/syncthing/syncthing-gtk/tags"
2289 .parse()
2290 .unwrap()
2291 );
2292 }
2293
2294 #[test]
2295 fn test_parse_v3() {
2296 let parsed = parse(
2297 r#"version=4
2298https://github.com/syncthing/@PACKAGE@/tags .*/v?(\d\S+)\.tar\.gz
2299# comment
2300"#,
2301 );
2302 assert_eq!(parsed.errors, Vec::<String>::new());
2303 let root = parsed.root();
2304 assert_eq!(root.version(), 4);
2305 let entries = root.entries().collect::<Vec<_>>();
2306 assert_eq!(entries.len(), 1);
2307 let entry = &entries[0];
2308 assert_eq!(entry.url(), "https://github.com/syncthing/@PACKAGE@/tags");
2309 assert_eq!(
2310 entry.format_url(|| "syncthing-gtk".to_string(), || String::new()),
2311 "https://github.com/syncthing/syncthing-gtk/tags"
2312 .parse()
2313 .unwrap()
2314 );
2315 }
2316
2317 #[test]
2318 fn test_thread_safe_parsing() {
2319 let text = r#"version=4
2320https://github.com/example/example/tags example-(.*)\.tar\.gz
2321"#;
2322
2323 let parsed = parse_watch_file(text);
2324 assert!(parsed.is_ok());
2325 assert_eq!(parsed.errors().len(), 0);
2326
2327 let watchfile = parsed.tree();
2329 assert_eq!(watchfile.version(), 4);
2330 let entries: Vec<_> = watchfile.entries().collect();
2331 assert_eq!(entries.len(), 1);
2332 }
2333
2334 #[test]
2335 fn test_parse_clone_and_eq() {
2336 let text = r#"version=4
2337https://github.com/example/example/tags example-(.*)\.tar\.gz
2338"#;
2339
2340 let parsed1 = parse_watch_file(text);
2341 let parsed2 = parsed1.clone();
2342
2343 assert_eq!(parsed1, parsed2);
2345
2346 let watchfile1 = parsed1.tree();
2348 let watchfile2 = watchfile1.clone();
2349 assert_eq!(watchfile1, watchfile2);
2350 }
2351
2352 #[test]
2353 fn test_parse_v4() {
2354 let cl: super::WatchFile = r#"version=4
2355opts=repack,compression=xz,dversionmangle=s/\+ds//,repacksuffix=+ds \
2356 https://github.com/example/example-cat/tags \
2357 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2358"#
2359 .parse()
2360 .unwrap();
2361 assert_eq!(cl.version(), 4);
2362 let entries = cl.entries().collect::<Vec<_>>();
2363 assert_eq!(entries.len(), 1);
2364 let entry = &entries[0];
2365 assert_eq!(entry.url(), "https://github.com/example/example-cat/tags");
2366 assert_eq!(
2367 entry.matching_pattern(),
2368 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2369 );
2370 assert!(entry.repack());
2371 assert_eq!(entry.compression(), Ok(Some(Compression::Xz)));
2372 assert_eq!(entry.dversionmangle(), Some("s/\\+ds//".into()));
2373 assert_eq!(entry.repacksuffix(), Some("+ds".into()));
2374 assert_eq!(entry.script(), Some("uupdate".into()));
2375 assert_eq!(
2376 entry.format_url(|| "example-cat".to_string(), || String::new()),
2377 "https://github.com/example/example-cat/tags"
2378 .parse()
2379 .unwrap()
2380 );
2381 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
2382 }
2383
2384 #[test]
2385 fn test_git_mode() {
2386 let text = r#"version=3
2387opts="mode=git, gitmode=shallow, pgpmode=gittag" \
2388https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git \
2389refs/tags/(.*) debian
2390"#;
2391 let parsed = parse(text);
2392 assert_eq!(parsed.errors, Vec::<String>::new());
2393 let cl = parsed.root();
2394 assert_eq!(cl.version(), 3);
2395 let entries = cl.entries().collect::<Vec<_>>();
2396 assert_eq!(entries.len(), 1);
2397 let entry = &entries[0];
2398 assert_eq!(
2399 entry.url(),
2400 "https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git"
2401 );
2402 assert_eq!(entry.matching_pattern(), Some("refs/tags/(.*)".into()));
2403 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
2404 assert_eq!(entry.script(), None);
2405 assert_eq!(entry.gitmode(), Ok(GitMode::Shallow));
2406 assert_eq!(entry.pgpmode(), Ok(PgpMode::GitTag));
2407 assert_eq!(entry.mode(), Ok(Mode::Git));
2408 }
2409
2410 #[test]
2411 fn test_parse_quoted() {
2412 const WATCHV1: &str = r#"version=4
2413opts="bare, filenamemangle=blah" \
2414 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2415"#;
2416 let parsed = parse(WATCHV1);
2417 let node = parsed.syntax();
2419
2420 let root = parsed.root();
2421 assert_eq!(root.version(), 4);
2422 let entries = root.entries().collect::<Vec<_>>();
2423 assert_eq!(entries.len(), 1);
2424 let entry = &entries[0];
2425
2426 assert_eq!(
2427 entry.url(),
2428 "https://github.com/syncthing/syncthing-gtk/tags"
2429 );
2430 assert_eq!(
2431 entry.matching_pattern(),
2432 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2433 );
2434 assert_eq!(entry.version(), Ok(None));
2435 assert_eq!(entry.script(), None);
2436
2437 assert_eq!(node.text(), WATCHV1);
2438 }
2439
2440 #[test]
2441 fn test_set_url() {
2442 let wf: super::WatchFile = r#"version=4
2444https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2445"#
2446 .parse()
2447 .unwrap();
2448
2449 let mut entry = wf.entries().next().unwrap();
2450 assert_eq!(
2451 entry.url(),
2452 "https://github.com/syncthing/syncthing-gtk/tags"
2453 );
2454
2455 entry.set_url("https://newurl.example.org/path");
2456 assert_eq!(entry.url(), "https://newurl.example.org/path");
2457 assert_eq!(
2458 entry.matching_pattern(),
2459 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2460 );
2461
2462 assert_eq!(
2464 entry.to_string(),
2465 "https://newurl.example.org/path .*/v?(\\d\\S+)\\.tar\\.gz\n"
2466 );
2467 }
2468
2469 #[test]
2470 fn test_set_url_with_options() {
2471 let wf: super::WatchFile = r#"version=4
2473opts=foo=blah https://foo.com/bar .*/v?(\d\S+)\.tar\.gz
2474"#
2475 .parse()
2476 .unwrap();
2477
2478 let mut entry = wf.entries().next().unwrap();
2479 assert_eq!(entry.url(), "https://foo.com/bar");
2480 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2481
2482 entry.set_url("https://example.com/baz");
2483 assert_eq!(entry.url(), "https://example.com/baz");
2484
2485 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2487 assert_eq!(
2488 entry.matching_pattern(),
2489 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2490 );
2491
2492 assert_eq!(
2494 entry.to_string(),
2495 "opts=foo=blah https://example.com/baz .*/v?(\\d\\S+)\\.tar\\.gz\n"
2496 );
2497 }
2498
2499 #[test]
2500 fn test_set_url_complex() {
2501 let wf: super::WatchFile = r#"version=4
2503opts=bare,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1\.tar\.gz/ \
2504 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2505"#
2506 .parse()
2507 .unwrap();
2508
2509 let mut entry = wf.entries().next().unwrap();
2510 assert_eq!(
2511 entry.url(),
2512 "https://github.com/syncthing/syncthing-gtk/tags"
2513 );
2514
2515 entry.set_url("https://gitlab.com/newproject/tags");
2516 assert_eq!(entry.url(), "https://gitlab.com/newproject/tags");
2517
2518 assert!(entry.bare());
2520 assert_eq!(
2521 entry.filenamemangle(),
2522 Some("s/.+\\/v?(\\d\\S+)\\.tar\\.gz/syncthing-gtk-$1\\.tar\\.gz/".into())
2523 );
2524 assert_eq!(
2525 entry.matching_pattern(),
2526 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2527 );
2528
2529 assert_eq!(
2531 entry.to_string(),
2532 r#"opts=bare,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1\.tar\.gz/ \
2533 https://gitlab.com/newproject/tags .*/v?(\d\S+)\.tar\.gz
2534"#
2535 );
2536 }
2537
2538 #[test]
2539 fn test_set_url_with_all_fields() {
2540 let wf: super::WatchFile = r#"version=4
2542opts=repack,compression=xz,dversionmangle=s/\+ds//,repacksuffix=+ds \
2543 https://github.com/example/example-cat/tags \
2544 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2545"#
2546 .parse()
2547 .unwrap();
2548
2549 let mut entry = wf.entries().next().unwrap();
2550 assert_eq!(entry.url(), "https://github.com/example/example-cat/tags");
2551 assert_eq!(
2552 entry.matching_pattern(),
2553 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2554 );
2555 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2556 assert_eq!(entry.script(), Some("uupdate".into()));
2557
2558 entry.set_url("https://gitlab.example.org/project/releases");
2559 assert_eq!(entry.url(), "https://gitlab.example.org/project/releases");
2560
2561 assert!(entry.repack());
2563 assert_eq!(entry.compression(), Ok(Some(super::Compression::Xz)));
2564 assert_eq!(entry.dversionmangle(), Some("s/\\+ds//".into()));
2565 assert_eq!(entry.repacksuffix(), Some("+ds".into()));
2566 assert_eq!(
2567 entry.matching_pattern(),
2568 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2569 );
2570 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2571 assert_eq!(entry.script(), Some("uupdate".into()));
2572
2573 assert_eq!(
2575 entry.to_string(),
2576 r#"opts=repack,compression=xz,dversionmangle=s/\+ds//,repacksuffix=+ds \
2577 https://gitlab.example.org/project/releases \
2578 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2579"#
2580 );
2581 }
2582
2583 #[test]
2584 fn test_set_url_quoted_options() {
2585 let wf: super::WatchFile = r#"version=4
2587opts="bare, filenamemangle=blah" \
2588 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2589"#
2590 .parse()
2591 .unwrap();
2592
2593 let mut entry = wf.entries().next().unwrap();
2594 assert_eq!(
2595 entry.url(),
2596 "https://github.com/syncthing/syncthing-gtk/tags"
2597 );
2598
2599 entry.set_url("https://example.org/new/path");
2600 assert_eq!(entry.url(), "https://example.org/new/path");
2601
2602 assert_eq!(
2604 entry.to_string(),
2605 r#"opts="bare, filenamemangle=blah" \
2606 https://example.org/new/path .*/v?(\d\S+)\.tar\.gz
2607"#
2608 );
2609 }
2610
2611 #[test]
2612 fn test_set_opt_update_existing() {
2613 let wf: super::WatchFile = r#"version=4
2615opts=foo=blah,bar=baz 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 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2623
2624 entry.set_opt("foo", "updated");
2625 assert_eq!(entry.get_option("foo"), Some("updated".to_string()));
2626 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2627
2628 assert_eq!(
2630 entry.to_string(),
2631 "opts=foo=updated,bar=baz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2632 );
2633 }
2634
2635 #[test]
2636 fn test_set_opt_add_new() {
2637 let wf: super::WatchFile = r#"version=4
2639opts=foo=blah https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2640"#
2641 .parse()
2642 .unwrap();
2643
2644 let mut entry = wf.entries().next().unwrap();
2645 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2646 assert_eq!(entry.get_option("bar"), None);
2647
2648 entry.set_opt("bar", "baz");
2649 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2650 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2651
2652 assert_eq!(
2654 entry.to_string(),
2655 "opts=foo=blah,bar=baz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2656 );
2657 }
2658
2659 #[test]
2660 fn test_set_opt_create_options_list() {
2661 let wf: super::WatchFile = r#"version=4
2663https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2664"#
2665 .parse()
2666 .unwrap();
2667
2668 let mut entry = wf.entries().next().unwrap();
2669 assert_eq!(entry.option_list(), None);
2670
2671 entry.set_opt("compression", "xz");
2672 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
2673
2674 assert_eq!(
2676 entry.to_string(),
2677 "opts=compression=xz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2678 );
2679 }
2680
2681 #[test]
2682 fn test_del_opt_remove_single() {
2683 let wf: super::WatchFile = r#"version=4
2685opts=foo=blah,bar=baz,qux=quux https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2686"#
2687 .parse()
2688 .unwrap();
2689
2690 let mut entry = wf.entries().next().unwrap();
2691 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2692 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2693 assert_eq!(entry.get_option("qux"), Some("quux".to_string()));
2694
2695 entry.del_opt_str("bar");
2696 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2697 assert_eq!(entry.get_option("bar"), None);
2698 assert_eq!(entry.get_option("qux"), Some("quux".to_string()));
2699
2700 assert_eq!(
2702 entry.to_string(),
2703 "opts=foo=blah,qux=quux https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2704 );
2705 }
2706
2707 #[test]
2708 fn test_del_opt_remove_first() {
2709 let wf: super::WatchFile = r#"version=4
2711opts=foo=blah,bar=baz https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2712"#
2713 .parse()
2714 .unwrap();
2715
2716 let mut entry = wf.entries().next().unwrap();
2717 entry.del_opt_str("foo");
2718 assert_eq!(entry.get_option("foo"), None);
2719 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2720
2721 assert_eq!(
2723 entry.to_string(),
2724 "opts=bar=baz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2725 );
2726 }
2727
2728 #[test]
2729 fn test_del_opt_remove_last() {
2730 let wf: super::WatchFile = r#"version=4
2732opts=foo=blah,bar=baz https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2733"#
2734 .parse()
2735 .unwrap();
2736
2737 let mut entry = wf.entries().next().unwrap();
2738 entry.del_opt_str("bar");
2739 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2740 assert_eq!(entry.get_option("bar"), None);
2741
2742 assert_eq!(
2744 entry.to_string(),
2745 "opts=foo=blah https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2746 );
2747 }
2748
2749 #[test]
2750 fn test_del_opt_remove_only_option() {
2751 let wf: super::WatchFile = r#"version=4
2753opts=foo=blah https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2754"#
2755 .parse()
2756 .unwrap();
2757
2758 let mut entry = wf.entries().next().unwrap();
2759 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2760
2761 entry.del_opt_str("foo");
2762 assert_eq!(entry.get_option("foo"), None);
2763 assert_eq!(entry.option_list(), None);
2764
2765 assert_eq!(
2767 entry.to_string(),
2768 "https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2769 );
2770 }
2771
2772 #[test]
2773 fn test_del_opt_nonexistent() {
2774 let wf: super::WatchFile = r#"version=4
2776opts=foo=blah https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2777"#
2778 .parse()
2779 .unwrap();
2780
2781 let mut entry = wf.entries().next().unwrap();
2782 let original = entry.to_string();
2783
2784 entry.del_opt_str("nonexistent");
2785 assert_eq!(entry.to_string(), original);
2786 }
2787
2788 #[test]
2789 fn test_set_opt_multiple_operations() {
2790 let wf: super::WatchFile = r#"version=4
2792https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2793"#
2794 .parse()
2795 .unwrap();
2796
2797 let mut entry = wf.entries().next().unwrap();
2798
2799 entry.set_opt("compression", "xz");
2800 entry.set_opt("repack", "");
2801 entry.set_opt("dversionmangle", "s/\\+ds//");
2802
2803 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
2804 assert_eq!(
2805 entry.get_option("dversionmangle"),
2806 Some("s/\\+ds//".to_string())
2807 );
2808 }
2809
2810 #[test]
2811 fn test_set_matching_pattern() {
2812 let wf: super::WatchFile = r#"version=4
2814https://github.com/example/tags .*/v?(\d\S+)\.tar\.gz
2815"#
2816 .parse()
2817 .unwrap();
2818
2819 let mut entry = wf.entries().next().unwrap();
2820 assert_eq!(
2821 entry.matching_pattern(),
2822 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2823 );
2824
2825 entry.set_matching_pattern("(?:.*?/)?v?([\\d.]+)\\.tar\\.gz");
2826 assert_eq!(
2827 entry.matching_pattern(),
2828 Some("(?:.*?/)?v?([\\d.]+)\\.tar\\.gz".into())
2829 );
2830
2831 assert_eq!(entry.url(), "https://github.com/example/tags");
2833
2834 assert_eq!(
2836 entry.to_string(),
2837 "https://github.com/example/tags (?:.*?/)?v?([\\d.]+)\\.tar\\.gz\n"
2838 );
2839 }
2840
2841 #[test]
2842 fn test_set_matching_pattern_with_all_fields() {
2843 let wf: super::WatchFile = r#"version=4
2845opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2846"#
2847 .parse()
2848 .unwrap();
2849
2850 let mut entry = wf.entries().next().unwrap();
2851 assert_eq!(
2852 entry.matching_pattern(),
2853 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2854 );
2855
2856 entry.set_matching_pattern(".*/version-([\\d.]+)\\.tar\\.xz");
2857 assert_eq!(
2858 entry.matching_pattern(),
2859 Some(".*/version-([\\d.]+)\\.tar\\.xz".into())
2860 );
2861
2862 assert_eq!(entry.url(), "https://example.com/releases");
2864 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2865 assert_eq!(entry.script(), Some("uupdate".into()));
2866 assert_eq!(entry.compression(), Ok(Some(super::Compression::Xz)));
2867
2868 assert_eq!(
2870 entry.to_string(),
2871 "opts=compression=xz https://example.com/releases .*/version-([\\d.]+)\\.tar\\.xz debian uupdate\n"
2872 );
2873 }
2874
2875 #[test]
2876 fn test_set_version_policy() {
2877 let wf: super::WatchFile = r#"version=4
2879https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2880"#
2881 .parse()
2882 .unwrap();
2883
2884 let mut entry = wf.entries().next().unwrap();
2885 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2886
2887 entry.set_version_policy("previous");
2888 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Previous)));
2889
2890 assert_eq!(entry.url(), "https://example.com/releases");
2892 assert_eq!(
2893 entry.matching_pattern(),
2894 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2895 );
2896 assert_eq!(entry.script(), Some("uupdate".into()));
2897
2898 assert_eq!(
2900 entry.to_string(),
2901 "https://example.com/releases (?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz previous uupdate\n"
2902 );
2903 }
2904
2905 #[test]
2906 fn test_set_version_policy_with_options() {
2907 let wf: super::WatchFile = r#"version=4
2909opts=repack,compression=xz \
2910 https://github.com/example/example-cat/tags \
2911 (?:.*?/)?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("ignore");
2920 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Ignore)));
2921
2922 assert_eq!(entry.url(), "https://github.com/example/example-cat/tags");
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 assert!(entry.repack());
2930
2931 assert_eq!(
2933 entry.to_string(),
2934 r#"opts=repack,compression=xz \
2935 https://github.com/example/example-cat/tags \
2936 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz ignore uupdate
2937"#
2938 );
2939 }
2940
2941 #[test]
2942 fn test_set_script() {
2943 let wf: super::WatchFile = r#"version=4
2945https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2946"#
2947 .parse()
2948 .unwrap();
2949
2950 let mut entry = wf.entries().next().unwrap();
2951 assert_eq!(entry.script(), Some("uupdate".into()));
2952
2953 entry.set_script("uscan");
2954 assert_eq!(entry.script(), Some("uscan".into()));
2955
2956 assert_eq!(entry.url(), "https://example.com/releases");
2958 assert_eq!(
2959 entry.matching_pattern(),
2960 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2961 );
2962 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2963
2964 assert_eq!(
2966 entry.to_string(),
2967 "https://example.com/releases (?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz debian uscan\n"
2968 );
2969 }
2970
2971 #[test]
2972 fn test_set_script_with_options() {
2973 let wf: super::WatchFile = r#"version=4
2975opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2976"#
2977 .parse()
2978 .unwrap();
2979
2980 let mut entry = wf.entries().next().unwrap();
2981 assert_eq!(entry.script(), Some("uupdate".into()));
2982
2983 entry.set_script("custom-script.sh");
2984 assert_eq!(entry.script(), Some("custom-script.sh".into()));
2985
2986 assert_eq!(entry.url(), "https://example.com/releases");
2988 assert_eq!(
2989 entry.matching_pattern(),
2990 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2991 );
2992 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2993 assert_eq!(entry.compression(), Ok(Some(super::Compression::Xz)));
2994
2995 assert_eq!(
2997 entry.to_string(),
2998 "opts=compression=xz https://example.com/releases (?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz debian custom-script.sh\n"
2999 );
3000 }
3001
3002 #[test]
3003 fn test_apply_dversionmangle() {
3004 let wf: super::WatchFile = r#"version=4
3006opts=dversionmangle=s/\+dfsg$// https://example.com/ .*
3007"#
3008 .parse()
3009 .unwrap();
3010 let entry = wf.entries().next().unwrap();
3011 assert_eq!(entry.apply_dversionmangle("1.0+dfsg").unwrap(), "1.0");
3012 assert_eq!(entry.apply_dversionmangle("1.0").unwrap(), "1.0");
3013
3014 let wf: super::WatchFile = r#"version=4
3016opts=versionmangle=s/^v// https://example.com/ .*
3017"#
3018 .parse()
3019 .unwrap();
3020 let entry = wf.entries().next().unwrap();
3021 assert_eq!(entry.apply_dversionmangle("v1.0").unwrap(), "1.0");
3022
3023 let wf: super::WatchFile = r#"version=4
3025opts=dversionmangle=s/\+ds//,versionmangle=s/^v// https://example.com/ .*
3026"#
3027 .parse()
3028 .unwrap();
3029 let entry = wf.entries().next().unwrap();
3030 assert_eq!(entry.apply_dversionmangle("1.0+ds").unwrap(), "1.0");
3031
3032 let wf: super::WatchFile = r#"version=4
3034https://example.com/ .*
3035"#
3036 .parse()
3037 .unwrap();
3038 let entry = wf.entries().next().unwrap();
3039 assert_eq!(entry.apply_dversionmangle("1.0+dfsg").unwrap(), "1.0+dfsg");
3040 }
3041
3042 #[test]
3043 fn test_apply_oversionmangle() {
3044 let wf: super::WatchFile = r#"version=4
3046opts=oversionmangle=s/$/-1/ https://example.com/ .*
3047"#
3048 .parse()
3049 .unwrap();
3050 let entry = wf.entries().next().unwrap();
3051 assert_eq!(entry.apply_oversionmangle("1.0").unwrap(), "1.0-1");
3052 assert_eq!(entry.apply_oversionmangle("2.5.3").unwrap(), "2.5.3-1");
3053
3054 let wf: super::WatchFile = r#"version=4
3056opts=oversionmangle=s/$/.dfsg/ https://example.com/ .*
3057"#
3058 .parse()
3059 .unwrap();
3060 let entry = wf.entries().next().unwrap();
3061 assert_eq!(entry.apply_oversionmangle("1.0").unwrap(), "1.0.dfsg");
3062
3063 let wf: super::WatchFile = r#"version=4
3065https://example.com/ .*
3066"#
3067 .parse()
3068 .unwrap();
3069 let entry = wf.entries().next().unwrap();
3070 assert_eq!(entry.apply_oversionmangle("1.0").unwrap(), "1.0");
3071 }
3072
3073 #[test]
3074 fn test_apply_dirversionmangle() {
3075 let wf: super::WatchFile = r#"version=4
3077opts=dirversionmangle=s/^v// https://example.com/ .*
3078"#
3079 .parse()
3080 .unwrap();
3081 let entry = wf.entries().next().unwrap();
3082 assert_eq!(entry.apply_dirversionmangle("v1.0").unwrap(), "1.0");
3083 assert_eq!(entry.apply_dirversionmangle("v2.5.3").unwrap(), "2.5.3");
3084
3085 let wf: super::WatchFile = r#"version=4
3087opts=dirversionmangle=s/v(\d)/$1/ https://example.com/ .*
3088"#
3089 .parse()
3090 .unwrap();
3091 let entry = wf.entries().next().unwrap();
3092 assert_eq!(entry.apply_dirversionmangle("v1.0").unwrap(), "1.0");
3093
3094 let wf: super::WatchFile = r#"version=4
3096https://example.com/ .*
3097"#
3098 .parse()
3099 .unwrap();
3100 let entry = wf.entries().next().unwrap();
3101 assert_eq!(entry.apply_dirversionmangle("v1.0").unwrap(), "v1.0");
3102 }
3103
3104 #[test]
3105 fn test_apply_filenamemangle() {
3106 let wf: super::WatchFile = r#"version=4
3108opts=filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/mypackage-$1.tar.gz/ https://example.com/ .*
3109"#
3110 .parse()
3111 .unwrap();
3112 let entry = wf.entries().next().unwrap();
3113 assert_eq!(
3114 entry
3115 .apply_filenamemangle("https://example.com/v1.0.tar.gz")
3116 .unwrap(),
3117 "mypackage-1.0.tar.gz"
3118 );
3119 assert_eq!(
3120 entry
3121 .apply_filenamemangle("https://example.com/2.5.3.tar.gz")
3122 .unwrap(),
3123 "mypackage-2.5.3.tar.gz"
3124 );
3125
3126 let wf: super::WatchFile = r#"version=4
3128opts=filenamemangle=s/.*\/(.*)/$1/ https://example.com/ .*
3129"#
3130 .parse()
3131 .unwrap();
3132 let entry = wf.entries().next().unwrap();
3133 assert_eq!(
3134 entry
3135 .apply_filenamemangle("https://example.com/path/to/file.tar.gz")
3136 .unwrap(),
3137 "file.tar.gz"
3138 );
3139
3140 let wf: super::WatchFile = r#"version=4
3142https://example.com/ .*
3143"#
3144 .parse()
3145 .unwrap();
3146 let entry = wf.entries().next().unwrap();
3147 assert_eq!(
3148 entry
3149 .apply_filenamemangle("https://example.com/file.tar.gz")
3150 .unwrap(),
3151 "https://example.com/file.tar.gz"
3152 );
3153 }
3154
3155 #[test]
3156 fn test_apply_pagemangle() {
3157 let wf: super::WatchFile = r#"version=4
3159opts=pagemangle=s/&/&/g https://example.com/ .*
3160"#
3161 .parse()
3162 .unwrap();
3163 let entry = wf.entries().next().unwrap();
3164 assert_eq!(
3165 entry.apply_pagemangle(b"foo & bar").unwrap(),
3166 b"foo & bar"
3167 );
3168 assert_eq!(
3169 entry
3170 .apply_pagemangle(b"& foo & bar &")
3171 .unwrap(),
3172 b"& foo & bar &"
3173 );
3174
3175 let wf: super::WatchFile = r#"version=4
3177opts=pagemangle=s/<[^>]+>//g https://example.com/ .*
3178"#
3179 .parse()
3180 .unwrap();
3181 let entry = wf.entries().next().unwrap();
3182 assert_eq!(entry.apply_pagemangle(b"<div>text</div>").unwrap(), b"text");
3183
3184 let wf: super::WatchFile = r#"version=4
3186https://example.com/ .*
3187"#
3188 .parse()
3189 .unwrap();
3190 let entry = wf.entries().next().unwrap();
3191 assert_eq!(
3192 entry.apply_pagemangle(b"foo & bar").unwrap(),
3193 b"foo & bar"
3194 );
3195 }
3196
3197 #[test]
3198 fn test_apply_downloadurlmangle() {
3199 let wf: super::WatchFile = r#"version=4
3201opts=downloadurlmangle=s|/archive/|/download/| https://example.com/ .*
3202"#
3203 .parse()
3204 .unwrap();
3205 let entry = wf.entries().next().unwrap();
3206 assert_eq!(
3207 entry
3208 .apply_downloadurlmangle("https://example.com/archive/file.tar.gz")
3209 .unwrap(),
3210 "https://example.com/download/file.tar.gz"
3211 );
3212
3213 let wf: super::WatchFile = r#"version=4
3215opts=downloadurlmangle=s/github\.com/raw.githubusercontent.com/ https://example.com/ .*
3216"#
3217 .parse()
3218 .unwrap();
3219 let entry = wf.entries().next().unwrap();
3220 assert_eq!(
3221 entry
3222 .apply_downloadurlmangle("https://github.com/user/repo/file.tar.gz")
3223 .unwrap(),
3224 "https://raw.githubusercontent.com/user/repo/file.tar.gz"
3225 );
3226
3227 let wf: super::WatchFile = r#"version=4
3229https://example.com/ .*
3230"#
3231 .parse()
3232 .unwrap();
3233 let entry = wf.entries().next().unwrap();
3234 assert_eq!(
3235 entry
3236 .apply_downloadurlmangle("https://example.com/archive/file.tar.gz")
3237 .unwrap(),
3238 "https://example.com/archive/file.tar.gz"
3239 );
3240 }
3241
3242 #[test]
3243 fn test_entry_builder_minimal() {
3244 let entry = super::EntryBuilder::new("https://github.com/example/tags")
3246 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3247 .build();
3248
3249 assert_eq!(entry.url(), "https://github.com/example/tags");
3250 assert_eq!(
3251 entry.matching_pattern().as_deref(),
3252 Some(".*/v?(\\d\\S+)\\.tar\\.gz")
3253 );
3254 assert_eq!(entry.version(), Ok(None));
3255 assert_eq!(entry.script(), None);
3256 assert!(entry.opts().is_empty());
3257 }
3258
3259 #[test]
3260 fn test_entry_builder_url_only() {
3261 let entry = super::EntryBuilder::new("https://example.com/releases").build();
3263
3264 assert_eq!(entry.url(), "https://example.com/releases");
3265 assert_eq!(entry.matching_pattern(), None);
3266 assert_eq!(entry.version(), Ok(None));
3267 assert_eq!(entry.script(), None);
3268 assert!(entry.opts().is_empty());
3269 }
3270
3271 #[test]
3272 fn test_entry_builder_with_all_fields() {
3273 let entry = super::EntryBuilder::new("https://github.com/example/tags")
3275 .matching_pattern(".*/v?(\\d[\\d.]*)\\.tar\\.gz")
3276 .version_policy("debian")
3277 .script("uupdate")
3278 .opt("compression", "xz")
3279 .flag("repack")
3280 .build();
3281
3282 assert_eq!(entry.url(), "https://github.com/example/tags");
3283 assert_eq!(
3284 entry.matching_pattern().as_deref(),
3285 Some(".*/v?(\\d[\\d.]*)\\.tar\\.gz")
3286 );
3287 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
3288 assert_eq!(entry.script(), Some("uupdate".into()));
3289 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
3290 assert!(entry.has_option("repack"));
3291 assert!(entry.repack());
3292 }
3293
3294 #[test]
3295 fn test_entry_builder_multiple_options() {
3296 let entry = super::EntryBuilder::new("https://example.com/tags")
3298 .matching_pattern(".*/v?(\\d+\\.\\d+)\\.tar\\.gz")
3299 .opt("compression", "xz")
3300 .opt("dversionmangle", "s/\\+ds//")
3301 .opt("repacksuffix", "+ds")
3302 .build();
3303
3304 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
3305 assert_eq!(
3306 entry.get_option("dversionmangle"),
3307 Some("s/\\+ds//".to_string())
3308 );
3309 assert_eq!(entry.get_option("repacksuffix"), Some("+ds".to_string()));
3310 }
3311
3312 #[test]
3313 fn test_entry_builder_via_entry() {
3314 let entry = super::Entry::builder("https://github.com/example/tags")
3316 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3317 .version_policy("debian")
3318 .build();
3319
3320 assert_eq!(entry.url(), "https://github.com/example/tags");
3321 assert_eq!(
3322 entry.matching_pattern().as_deref(),
3323 Some(".*/v?(\\d\\S+)\\.tar\\.gz")
3324 );
3325 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
3326 }
3327
3328 #[test]
3329 fn test_watchfile_add_entry_to_empty() {
3330 let mut wf = super::WatchFile::new(Some(4));
3332
3333 let entry = super::EntryBuilder::new("https://github.com/example/tags")
3334 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3335 .build();
3336
3337 wf.add_entry(entry);
3338
3339 assert_eq!(wf.version(), 4);
3340 assert_eq!(wf.entries().count(), 1);
3341
3342 let added_entry = wf.entries().next().unwrap();
3343 assert_eq!(added_entry.url(), "https://github.com/example/tags");
3344 assert_eq!(
3345 added_entry.matching_pattern().as_deref(),
3346 Some(".*/v?(\\d\\S+)\\.tar\\.gz")
3347 );
3348 }
3349
3350 #[test]
3351 fn test_watchfile_add_multiple_entries() {
3352 let mut wf = super::WatchFile::new(Some(4));
3354
3355 wf.add_entry(
3356 super::EntryBuilder::new("https://github.com/example1/tags")
3357 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3358 .build(),
3359 );
3360
3361 wf.add_entry(
3362 super::EntryBuilder::new("https://github.com/example2/releases")
3363 .matching_pattern(".*/(\\d+\\.\\d+)\\.tar\\.gz")
3364 .opt("compression", "xz")
3365 .build(),
3366 );
3367
3368 assert_eq!(wf.entries().count(), 2);
3369
3370 let entries: Vec<_> = wf.entries().collect();
3371 assert_eq!(entries[0].url(), "https://github.com/example1/tags");
3372 assert_eq!(entries[1].url(), "https://github.com/example2/releases");
3373 assert_eq!(entries[1].get_option("compression"), Some("xz".to_string()));
3374 }
3375
3376 #[test]
3377 fn test_watchfile_add_entry_to_existing() {
3378 let mut wf: super::WatchFile = r#"version=4
3380https://example.com/old .*/v?(\\d\\S+)\\.tar\\.gz
3381"#
3382 .parse()
3383 .unwrap();
3384
3385 assert_eq!(wf.entries().count(), 1);
3386
3387 wf.add_entry(
3388 super::EntryBuilder::new("https://github.com/example/new")
3389 .matching_pattern(".*/v?(\\d+\\.\\d+)\\.tar\\.gz")
3390 .opt("compression", "xz")
3391 .version_policy("debian")
3392 .build(),
3393 );
3394
3395 assert_eq!(wf.entries().count(), 2);
3396
3397 let entries: Vec<_> = wf.entries().collect();
3398 assert_eq!(entries[0].url(), "https://example.com/old");
3399 assert_eq!(entries[1].url(), "https://github.com/example/new");
3400 assert_eq!(entries[1].version(), Ok(Some(VersionPolicy::Debian)));
3401 }
3402
3403 #[test]
3404 fn test_entry_builder_formatting() {
3405 let entry = super::EntryBuilder::new("https://github.com/example/tags")
3407 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3408 .opt("compression", "xz")
3409 .flag("repack")
3410 .version_policy("debian")
3411 .script("uupdate")
3412 .build();
3413
3414 let entry_str = entry.to_string();
3415
3416 assert!(entry_str.starts_with("opts="));
3418 assert!(entry_str.contains("https://github.com/example/tags"));
3420 assert!(entry_str.contains(".*/v?(\\d\\S+)\\.tar\\.gz"));
3422 assert!(entry_str.contains("debian"));
3424 assert!(entry_str.contains("uupdate"));
3426 assert!(entry_str.ends_with('\n'));
3428 }
3429
3430 #[test]
3431 fn test_watchfile_add_entry_preserves_format() {
3432 let mut wf = super::WatchFile::new(Some(4));
3434
3435 wf.add_entry(
3436 super::EntryBuilder::new("https://github.com/example/tags")
3437 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3438 .build(),
3439 );
3440
3441 let wf_str = wf.to_string();
3442
3443 assert!(wf_str.starts_with("version=4\n"));
3445 assert!(wf_str.contains("https://github.com/example/tags"));
3447
3448 let reparsed: super::WatchFile = wf_str.parse().unwrap();
3450 assert_eq!(reparsed.version(), 4);
3451 assert_eq!(reparsed.entries().count(), 1);
3452 }
3453
3454 #[test]
3455 fn test_line_col() {
3456 let text = r#"version=4
3457opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
3458"#;
3459 let wf = text.parse::<super::WatchFile>().unwrap();
3460
3461 let version_node = wf.version_node().unwrap();
3463 assert_eq!(version_node.line(), 0);
3464 assert_eq!(version_node.column(), 0);
3465 assert_eq!(version_node.line_col(), (0, 0));
3466
3467 let entries: Vec<_> = wf.entries().collect();
3469 assert_eq!(entries.len(), 1);
3470
3471 assert_eq!(entries[0].line(), 1);
3473 assert_eq!(entries[0].column(), 0);
3474 assert_eq!(entries[0].line_col(), (1, 0));
3475
3476 let option_list = entries[0].option_list().unwrap();
3478 assert_eq!(option_list.line(), 1); let url_node = entries[0].url_node().unwrap();
3481 assert_eq!(url_node.line(), 1); let pattern_node = entries[0].matching_pattern_node().unwrap();
3484 assert_eq!(pattern_node.line(), 1); let version_policy_node = entries[0].version_node().unwrap();
3487 assert_eq!(version_policy_node.line(), 1); let script_node = entries[0].script_node().unwrap();
3490 assert_eq!(script_node.line(), 1); let options: Vec<_> = option_list.options().collect();
3494 assert_eq!(options.len(), 1);
3495 assert_eq!(options[0].key(), Some("compression".to_string()));
3496 assert_eq!(options[0].value(), Some("xz".to_string()));
3497 assert_eq!(options[0].line(), 1); let compression_opt = option_list.find_option("compression").unwrap();
3501 assert_eq!(compression_opt.line(), 1);
3502 assert_eq!(compression_opt.column(), 5); assert_eq!(compression_opt.line_col(), (1, 5));
3504 }
3505
3506 #[test]
3507 fn test_parse_str_relaxed() {
3508 let wf: super::WatchFile = super::WatchFile::from_str_relaxed(
3509 r#"version=4
3510ERRORS IN THIS LINE
3511opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d
3512"#,
3513 );
3514 assert_eq!(wf.version(), 4);
3515 assert_eq!(wf.entries().count(), 2);
3516
3517 let entries = wf.entries().collect::<Vec<_>>();
3518
3519 let entry = &entries[0];
3520 assert_eq!(entry.url(), "ERRORS");
3521
3522 let entry = &entries[1];
3523 assert_eq!(entry.url(), "https://example.com/releases");
3524 assert_eq!(entry.matching_pattern().as_deref(), Some("(?:.*?/)?v?(\\d"));
3525 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
3526 }
3527
3528 #[test]
3529 fn test_parse_entry_with_comment_before() {
3530 let input = concat!(
3534 "version=4\n",
3535 "# try also https://pypi.debian.net/tomoscan/watch\n",
3536 "opts=uversionmangle=s/(rc|a|b|c)/~$1/;s/\\.dev/~dev/ \\\n",
3537 "https://pypi.debian.net/tomoscan/tomoscan-(.+)\\.(?:zip|tgz|tbz|txz|(?:tar\\.(?:gz|bz2|xz)))\n"
3538 );
3539 let wf: super::WatchFile = input.parse().unwrap();
3540 assert_eq!(wf.to_string(), input);
3542 assert_eq!(wf.entries().count(), 1);
3543 let entry = wf.entries().next().unwrap();
3544 assert_eq!(
3545 entry.url(),
3546 "https://pypi.debian.net/tomoscan/tomoscan-(.+)\\.(?:zip|tgz|tbz|txz|(?:tar\\.(?:gz|bz2|xz)))"
3547 );
3548 assert_eq!(
3549 entry.get_option("uversionmangle"),
3550 Some("s/(rc|a|b|c)/~$1/;s/\\.dev/~dev/".to_string())
3551 );
3552 }
3553
3554 #[test]
3555 fn test_parse_multiple_comments_before_entry() {
3556 let input = concat!(
3559 "version=4\n",
3560 "# first comment\n",
3561 "# second comment\n",
3562 "# third comment\n",
3563 "https://example.com/foo foo-(.*).tar.gz\n",
3564 );
3565 let wf: super::WatchFile = input.parse().unwrap();
3566 assert_eq!(wf.to_string(), input);
3567 assert_eq!(wf.entries().count(), 1);
3568 assert_eq!(
3569 wf.entries().next().unwrap().url(),
3570 "https://example.com/foo"
3571 );
3572 }
3573
3574 #[test]
3575 fn test_parse_blank_lines_between_entries() {
3576 let input = concat!(
3578 "version=4\n",
3579 "https://example.com/foo .*/foo-(\\d+)\\.tar\\.gz\n",
3580 "\n",
3581 "https://example.com/bar .*/bar-(\\d+)\\.tar\\.gz\n",
3582 );
3583 let wf: super::WatchFile = input.parse().unwrap();
3584 assert_eq!(wf.to_string(), input);
3585 assert_eq!(wf.entries().count(), 2);
3586 }
3587
3588 #[test]
3589 fn test_parse_trailing_unparseable_tokens_produce_error() {
3590 let input = "version=4\nhttps://example.com/foo foo-(.*).tar.gz\n=garbage\n";
3593 let result = input.parse::<super::WatchFile>();
3594 assert!(result.is_err(), "expected parse error for trailing garbage");
3595 let wf = super::WatchFile::from_str_relaxed(input);
3597 assert_eq!(wf.to_string(), input);
3598 }
3599
3600 #[test]
3601 fn test_parse_roundtrip_full_file() {
3602 let inputs = [
3604 "version=4\nhttps://example.com/foo foo-(.*).tar.gz\n",
3605 "version=4\n# a comment\nhttps://example.com/foo foo-(.*).tar.gz\n",
3606 concat!(
3607 "version=4\n",
3608 "opts=uversionmangle=s/rc/~rc/ \\\n",
3609 " https://example.com/foo foo-(.*).tar.gz\n",
3610 ),
3611 concat!(
3612 "version=4\n",
3613 "# comment before entry\n",
3614 "opts=uversionmangle=s/rc/~rc/ \\\n",
3615 "https://example.com/foo foo-(.*).tar.gz\n",
3616 "# comment between entries\n",
3617 "https://example.com/bar bar-(.*).tar.gz\n",
3618 ),
3619 ];
3620 for input in &inputs {
3621 let wf: super::WatchFile = input.parse().unwrap();
3622 assert_eq!(
3623 wf.to_string(),
3624 *input,
3625 "round-trip failed for input: {:?}",
3626 input
3627 );
3628 }
3629 }
3630
3631 #[test]
3632 fn test_parse_url_with_equals_in_query_string() {
3633 let input = concat!(
3636 "version=4\n",
3637 "https://api.github.com/repos/x/releases?per_page=100 \\\n",
3638 " https://github.com/x/v[^/]+/x.tar.gz\n",
3639 );
3640 let wf: super::WatchFile = input.parse().unwrap();
3641 let entries: Vec<_> = wf.entries().collect();
3642 assert_eq!(entries.len(), 1);
3643 assert_eq!(
3644 entries[0].url(),
3645 "https://api.github.com/repos/x/releases?per_page=100"
3646 );
3647 assert_eq!(
3648 entries[0].matching_pattern().as_deref(),
3649 Some("https://github.com/x/v[^/]+/x.tar.gz"),
3650 );
3651 assert_eq!(wf.to_string(), input);
3652 }
3653
3654 #[test]
3655 fn test_entry_url_does_not_panic_when_empty() {
3656 let input = "version=4\n=garbage\n";
3659 let wf = super::WatchFile::from_str_relaxed(input);
3660 for entry in wf.entries() {
3661 let _ = entry.url();
3662 }
3663 }
3664
3665 #[test]
3666 fn test_parse_url_node_with_equals_join_tokens() {
3667 let input = "version=4\nhttps://example.com/x?y=1&z=2 .*tar.gz\n";
3670 let wf: super::WatchFile = input.parse().unwrap();
3671 let entry = wf.entries().next().unwrap();
3672 assert_eq!(entry.url(), "https://example.com/x?y=1&z=2");
3673 }
3674
3675 #[test]
3676 fn test_parse_quoted_opts_with_trailing_comma_continuation() {
3677 let input = concat!(
3683 "version=4\n\n",
3684 "opts=\"\\\n",
3685 "pgpmode=none,\\\n",
3686 "repack,compression=xz,repacksuffix=+dfsg,\\\n",
3687 "dversionmangle=s{[+~]dfsg\\d*}{},\\\n",
3688 "\" https://github.com/varlink/go/releases \\\n",
3689 " .*/archive/v?(\\d[\\d\\.]+)\\.tar\\.gz\n",
3690 );
3691 let wf: super::WatchFile = input.parse().unwrap();
3692 let entries: Vec<_> = wf.entries().collect();
3693 assert_eq!(entries.len(), 1);
3694 assert_eq!(entries[0].url(), "https://github.com/varlink/go/releases");
3695 assert_eq!(
3696 entries[0].matching_pattern().as_deref(),
3697 Some(".*/archive/v?(\\d[\\d\\.]+)\\.tar\\.gz"),
3698 );
3699 assert_eq!(wf.to_string(), input);
3700 }
3701
3702 #[test]
3703 fn test_parse_quoted_opts_with_spaces_around_comma() {
3704 let input = concat!(
3707 "version=4\n",
3708 "opts=\"filenamemangle=s/.+\\/v?(\\d\\S*)\\.tar\\.gz/v$1.tar.gz/ , uversionmangle=tr%-rc%~rc%\" \\\n",
3709 " https://github.com/analogdevicesinc/libiio/tags .*/v(\\d\\S*)\\.tar\\.gz\n",
3710 );
3711 let wf: super::WatchFile = input.parse().unwrap();
3712 let entries: Vec<_> = wf.entries().collect();
3713 assert_eq!(entries.len(), 1);
3714 assert_eq!(
3715 entries[0].url(),
3716 "https://github.com/analogdevicesinc/libiio/tags",
3717 );
3718 assert_eq!(wf.to_string(), input);
3719 }
3720
3721 #[test]
3722 fn test_parse_unquoted_opts_trailing_comma_then_url() {
3723 let input = concat!(
3727 "version=3\n",
3728 "opts=uversionmangle=s/(rc|a|b|c)/~$1/,\\\n",
3729 "https://github.com/openstack/rally/tags .*/(\\d\\S*)\\.tar\\.gz\n",
3730 );
3731 let wf: super::WatchFile = input.parse().unwrap();
3732 let entries: Vec<_> = wf.entries().collect();
3733 assert_eq!(entries.len(), 1);
3734 assert_eq!(entries[0].url(), "https://github.com/openstack/rally/tags");
3735 assert_eq!(
3736 entries[0].matching_pattern().as_deref(),
3737 Some(".*/(\\d\\S*)\\.tar\\.gz"),
3738 );
3739 assert_eq!(wf.to_string(), input);
3740 }
3741
3742 #[test]
3743 fn test_parse_unquoted_opts_value_with_equals() {
3744 let input = concat!(
3748 "version=4\n",
3749 "opts=dversionmangle=s/\\~dfsg//,downloadurlmangle=s/.*ref=//,pgpsigurlmangle=s/$/.asc/ \\\n",
3750 "\thttps://downloads.asterisk.org/pub/telephony/libpri/releases/ libpri-([0-9.]*)\\.tar\\.gz debian uupdate\n",
3751 );
3752 let wf: super::WatchFile = input.parse().unwrap();
3753 let entries: Vec<_> = wf.entries().collect();
3754 assert_eq!(entries.len(), 1);
3755 assert_eq!(
3756 entries[0].url(),
3757 "https://downloads.asterisk.org/pub/telephony/libpri/releases/"
3758 );
3759 assert_eq!(
3760 entries[0].matching_pattern().as_deref(),
3761 Some("libpri-([0-9.]*)\\.tar\\.gz"),
3762 );
3763 assert_eq!(wf.to_string(), input);
3764 }
3765}