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 if kind == SCRIPT {
303 while !matches!(self.current(), Some(NEWLINE) | None) {
307 self.bump();
308 }
309 } else {
310 while is_field_token(self.current()) {
311 self.bump();
312 }
313 }
314 self.builder.finish_node();
315 }
316 self.skip_ws();
317 }
318 if self.current() != Some(NEWLINE) && self.current().is_some() {
319 self.builder.start_node(ERROR.into());
320 self.errors
321 .push(format!("expected newline, not {:?}", self.current()));
322 if self.current().is_some() {
323 self.bump();
324 }
325 self.builder.finish_node();
326 } else if self.current().is_some() {
327 self.bump();
329 }
330 self.builder.finish_node();
331 true
332 }
333
334 fn parse_option(&mut self, quoted: bool) -> bool {
340 if self.current().is_none() {
341 return false;
342 }
343 while self.current() == Some(CONTINUATION) {
344 self.bump();
345 }
346 if !quoted && self.current() == Some(WHITESPACE) {
347 return false;
348 }
349 if quoted && self.current() == Some(QUOTE) {
350 return false;
351 }
352 if !quoted && self.current() != Some(KEY) {
357 return false;
358 }
359 self.builder.start_node(OPTION.into());
360 if self.current() != Some(KEY) {
361 self.builder.start_node(ERROR.into());
362 self.errors.push("expected key".to_string());
363 self.bump();
364 self.builder.finish_node();
365 } else {
366 self.bump();
367 }
368 if self.current() == Some(EQUALS) {
369 self.bump();
370 let mut consumed_value = false;
375 loop {
376 match self.current() {
377 Some(KEY) | Some(VALUE) => {
378 self.bump();
379 consumed_value = true;
380 }
381 Some(EQUALS) if consumed_value => self.bump(),
382 Some(WHITESPACE) if quoted => {
383 break;
388 }
389 _ => break,
390 }
391 }
392 if !consumed_value {
393 self.builder.start_node(ERROR.into());
394 self.errors
395 .push(format!("expected value, got {:?}", self.current()));
396 if self.current().is_some() {
397 self.bump();
398 }
399 self.builder.finish_node();
400 }
401 } else if self.current() == Some(COMMA) {
402 } else {
403 self.builder.start_node(ERROR.into());
404 self.errors.push("expected `=`".to_string());
405 if self.current().is_some() {
406 self.bump();
407 }
408 self.builder.finish_node();
409 }
410 self.builder.finish_node();
411 true
412 }
413
414 fn parse_options_list(&mut self) {
415 self.skip_ws();
416 if self.tokens.last() == Some(&(KEY, "opts".to_string()))
417 || self.tokens.last() == Some(&(KEY, "options".to_string()))
418 {
419 self.builder.start_node(OPTS_LIST.into());
420 self.bump();
421 self.skip_ws();
422 if self.current() != Some(EQUALS) {
423 self.builder.start_node(ERROR.into());
424 self.errors.push("expected `=`".to_string());
425 if self.current().is_some() {
426 self.bump();
427 }
428 self.builder.finish_node();
429 } else {
430 self.bump();
431 }
432 let quoted = if self.current() == Some(QUOTE) {
433 self.bump();
434 true
435 } else {
436 false
437 };
438 loop {
439 if quoted {
440 self.skip_ws();
446 if self.current() == Some(QUOTE) {
447 self.bump();
448 break;
449 }
450 }
451 if !self.parse_option(quoted) {
452 break;
453 }
454 if quoted {
455 self.skip_ws();
458 }
459 if self.current() == Some(COMMA) {
460 self.builder.start_node(OPTION_SEPARATOR.into());
461 self.bump();
462 self.builder.finish_node();
463 } else if !quoted {
464 break;
465 }
466 }
467 self.builder.finish_node();
468 self.skip_ws();
469 }
470 }
471
472 fn parse(mut self) -> InternalParse {
473 self.builder.start_node(ROOT.into());
475 while self.current() == Some(WHITESPACE)
477 || self.current() == Some(CONTINUATION)
478 || self.current() == Some(COMMENT)
479 || self.current() == Some(NEWLINE)
480 {
481 self.bump();
482 }
483 if let Some(_v) = self.parse_version() {
484 }
486 loop {
488 if !self.parse_watch_entry() {
489 break;
490 }
491 }
492 self.skip_ws();
494 if self.current().is_some() {
497 self.builder.start_node(ERROR.into());
498 self.errors
499 .push("unexpected tokens after last entry".to_string());
500 while self.current().is_some() {
501 self.bump();
502 }
503 self.builder.finish_node();
504 }
505 self.builder.finish_node();
507
508 InternalParse {
510 green_node: self.builder.finish(),
511 errors: self.errors,
512 }
513 }
514 fn bump(&mut self) {
516 if let Some((kind, text)) = self.tokens.pop() {
517 self.builder.token(kind.into(), text.as_str());
518 }
519 }
520 fn current(&self) -> Option<SyntaxKind> {
522 self.tokens.last().map(|(kind, _)| *kind)
523 }
524 fn skip_ws(&mut self) {
525 while self.current() == Some(WHITESPACE)
526 || self.current() == Some(CONTINUATION)
527 || self.current() == Some(COMMENT)
528 {
529 self.bump()
530 }
531 }
532 }
533
534 let mut tokens = lex(text);
535 tokens.reverse();
536 Parser {
537 tokens,
538 builder: GreenNodeBuilder::new(),
539 errors: Vec::new(),
540 }
541 .parse()
542}
543
544type SyntaxNode = rowan::SyntaxNode<Lang>;
550#[allow(unused)]
551type SyntaxToken = rowan::SyntaxToken<Lang>;
552#[allow(unused)]
553type SyntaxElement = rowan::NodeOrToken<SyntaxNode, SyntaxToken>;
554
555impl InternalParse {
556 fn syntax(&self) -> SyntaxNode {
557 SyntaxNode::new_root_mut(self.green_node.clone())
558 }
559
560 fn root(&self) -> WatchFile {
561 WatchFile::cast(self.syntax()).expect("root node should be a WatchFile")
562 }
563}
564
565fn line_col_at_offset(node: &SyntaxNode, offset: rowan::TextSize) -> (usize, usize) {
568 let root = node.ancestors().last().unwrap_or_else(|| node.clone());
569 let mut line = 0;
570 let mut last_newline_offset = rowan::TextSize::from(0);
571
572 for element in root.preorder_with_tokens() {
573 if let rowan::WalkEvent::Enter(rowan::NodeOrToken::Token(token)) = element {
574 if token.text_range().start() >= offset {
575 break;
576 }
577
578 for (idx, _) in token.text().match_indices('\n') {
580 line += 1;
581 last_newline_offset =
582 token.text_range().start() + rowan::TextSize::from((idx + 1) as u32);
583 }
584 }
585 }
586
587 let column: usize = (offset - last_newline_offset).into();
588 (line, column)
589}
590
591macro_rules! ast_node {
592 ($ast:ident, $kind:ident) => {
593 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
594 #[repr(transparent)]
595 pub struct $ast(SyntaxNode);
597 impl $ast {
598 #[allow(unused)]
599 fn cast(node: SyntaxNode) -> Option<Self> {
600 if node.kind() == $kind {
601 Some(Self(node))
602 } else {
603 None
604 }
605 }
606
607 pub fn text_range(&self) -> rowan::TextRange {
614 self.0.text_range()
615 }
616
617 pub fn line(&self) -> usize {
619 line_col_at_offset(&self.0, self.0.text_range().start()).0
620 }
621
622 pub fn column(&self) -> usize {
624 line_col_at_offset(&self.0, self.0.text_range().start()).1
625 }
626
627 pub fn line_col(&self) -> (usize, usize) {
630 line_col_at_offset(&self.0, self.0.text_range().start())
631 }
632 }
633
634 impl std::fmt::Display for $ast {
635 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
636 write!(f, "{}", self.0.text())
637 }
638 }
639 };
640}
641
642ast_node!(WatchFile, ROOT);
643ast_node!(Version, VERSION);
644ast_node!(Entry, ENTRY);
645ast_node!(_Option, OPTION);
646ast_node!(Url, URL);
647ast_node!(MatchingPattern, MATCHING_PATTERN);
648ast_node!(VersionPolicyNode, VERSION_POLICY);
649ast_node!(ScriptNode, SCRIPT);
650
651#[derive(Clone, PartialEq, Eq, Hash)]
653#[repr(transparent)]
654pub struct OptionList(SyntaxNode);
656
657impl OptionList {
658 #[allow(unused)]
659 fn cast(node: SyntaxNode) -> Option<Self> {
660 if node.kind() == OPTS_LIST {
661 Some(Self(node))
662 } else {
663 None
664 }
665 }
666
667 pub fn text_range(&self) -> rowan::TextRange {
669 self.0.text_range()
670 }
671
672 pub fn line(&self) -> usize {
674 line_col_at_offset(&self.0, self.0.text_range().start()).0
675 }
676
677 pub fn column(&self) -> usize {
679 line_col_at_offset(&self.0, self.0.text_range().start()).1
680 }
681
682 pub fn line_col(&self) -> (usize, usize) {
685 line_col_at_offset(&self.0, self.0.text_range().start())
686 }
687}
688
689impl std::fmt::Display for OptionList {
690 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
691 write!(f, "{}", self.0.text())
692 }
693}
694
695impl WatchFile {
696 pub fn syntax(&self) -> &SyntaxNode {
698 &self.0
699 }
700
701 pub fn snapshot(&self) -> Self {
704 WatchFile(SyntaxNode::new_root_mut(self.0.green().into_owned()))
705 }
706
707 pub fn tree_eq(&self, other: &Self) -> bool {
711 let a = self.0.green();
712 let b = other.0.green();
713 let a_ref: &rowan::GreenNodeData = &a;
714 let b_ref: &rowan::GreenNodeData = &b;
715 std::ptr::eq(a_ref as *const _, b_ref as *const _) || a_ref == b_ref
716 }
717
718 pub fn new(version: Option<u32>) -> WatchFile {
720 let mut builder = GreenNodeBuilder::new();
721
722 builder.start_node(ROOT.into());
723 if let Some(version) = version {
724 builder.start_node(VERSION.into());
725 builder.token(KEY.into(), "version");
726 builder.token(EQUALS.into(), "=");
727 builder.token(VALUE.into(), version.to_string().as_str());
728 builder.token(NEWLINE.into(), "\n");
729 builder.finish_node();
730 }
731 builder.finish_node();
732 WatchFile(SyntaxNode::new_root_mut(builder.finish()))
733 }
734
735 pub fn version_node(&self) -> Option<Version> {
737 self.0.children().find_map(Version::cast)
738 }
739
740 pub fn version(&self) -> u32 {
742 self.version_node()
743 .map(|it| it.version())
744 .unwrap_or(DEFAULT_VERSION)
745 }
746
747 pub fn entries(&self) -> impl Iterator<Item = Entry> + '_ {
749 self.0.children().filter_map(Entry::cast)
750 }
751
752 pub fn set_version(&mut self, new_version: u32) {
754 let mut builder = GreenNodeBuilder::new();
756 builder.start_node(VERSION.into());
757 builder.token(KEY.into(), "version");
758 builder.token(EQUALS.into(), "=");
759 builder.token(VALUE.into(), new_version.to_string().as_str());
760 builder.token(NEWLINE.into(), "\n");
761 builder.finish_node();
762 let new_version_green = builder.finish();
763
764 let new_version_node = SyntaxNode::new_root_mut(new_version_green);
766
767 let version_pos = self.0.children().position(|child| child.kind() == VERSION);
769
770 if let Some(pos) = version_pos {
771 self.0
773 .splice_children(pos..pos + 1, vec![new_version_node.into()]);
774 } else {
775 self.0.splice_children(0..0, vec![new_version_node.into()]);
777 }
778 }
779
780 #[cfg(feature = "discover")]
800 pub async fn uscan(
801 &self,
802 package: impl Fn() -> String + Send + Sync,
803 ) -> Result<Vec<Vec<crate::Release>>, Box<dyn std::error::Error>> {
804 let mut all_releases = Vec::new();
805
806 for entry in self.entries() {
807 let parsed_entry = crate::parse::ParsedEntry::LineBased(entry);
808 let releases = parsed_entry.discover(|| package()).await?;
809 all_releases.push(releases);
810 }
811
812 Ok(all_releases)
813 }
814
815 #[cfg(all(feature = "discover", feature = "blocking"))]
833 pub fn uscan_blocking(
834 &self,
835 package: impl Fn() -> String,
836 ) -> Result<Vec<Vec<crate::Release>>, Box<dyn std::error::Error>> {
837 let mut all_releases = Vec::new();
838
839 for entry in self.entries() {
840 let parsed_entry = crate::parse::ParsedEntry::LineBased(entry);
841 let releases = parsed_entry.discover_blocking(|| package())?;
842 all_releases.push(releases);
843 }
844
845 Ok(all_releases)
846 }
847
848 pub fn add_entry(&mut self, entry: Entry) -> Entry {
875 let insert_pos = self.0.children_with_tokens().count();
877
878 let entry_green = entry.0.green().into_owned();
880 let new_entry_node = SyntaxNode::new_root_mut(entry_green);
881
882 self.0
884 .splice_children(insert_pos..insert_pos, vec![new_entry_node.into()]);
885
886 Entry::cast(
888 self.0
889 .children()
890 .nth(insert_pos)
891 .expect("Entry was just inserted"),
892 )
893 .expect("Inserted node should be an Entry")
894 }
895
896 pub fn from_reader<R: std::io::Read>(reader: R) -> Result<WatchFile, ParseError> {
898 let mut buf_reader = std::io::BufReader::new(reader);
899 let mut content = String::new();
900 buf_reader
901 .read_to_string(&mut content)
902 .map_err(|e| ParseError(vec![e.to_string()]))?;
903 content.parse()
904 }
905
906 pub fn from_reader_relaxed<R: std::io::Read>(mut r: R) -> Result<Self, std::io::Error> {
908 let mut content = String::new();
909 r.read_to_string(&mut content)?;
910 let parsed = parse(&content);
911 Ok(parsed.root())
912 }
913
914 pub fn from_str_relaxed(s: &str) -> Self {
916 let parsed = parse(s);
917 parsed.root()
918 }
919}
920
921impl FromStr for WatchFile {
922 type Err = ParseError;
923
924 fn from_str(s: &str) -> Result<Self, Self::Err> {
925 let parsed = parse(s);
926 if parsed.errors.is_empty() {
927 Ok(parsed.root())
928 } else {
929 Err(ParseError(parsed.errors))
930 }
931 }
932}
933
934pub fn parse_watch_file(text: &str) -> Parse<WatchFile> {
937 let parsed = parse(text);
938 Parse::new(parsed.green_node, parsed.errors)
939}
940
941impl Version {
942 pub fn version(&self) -> u32 {
944 self.0
945 .children_with_tokens()
946 .find_map(|it| match it {
947 SyntaxElement::Token(token) => {
948 if token.kind() == VALUE {
949 token.text().parse().ok()
950 } else {
951 None
952 }
953 }
954 _ => None,
955 })
956 .unwrap_or(DEFAULT_VERSION)
957 }
958}
959
960#[derive(Debug, Clone, Default)]
984pub struct EntryBuilder {
985 url: Option<String>,
986 matching_pattern: Option<String>,
987 version_policy: Option<String>,
988 script: Option<String>,
989 opts: std::collections::HashMap<String, String>,
990}
991
992impl EntryBuilder {
993 pub fn new(url: impl Into<String>) -> Self {
995 EntryBuilder {
996 url: Some(url.into()),
997 matching_pattern: None,
998 version_policy: None,
999 script: None,
1000 opts: std::collections::HashMap::new(),
1001 }
1002 }
1003
1004 pub fn matching_pattern(mut self, pattern: impl Into<String>) -> Self {
1006 self.matching_pattern = Some(pattern.into());
1007 self
1008 }
1009
1010 pub fn version_policy(mut self, policy: impl Into<String>) -> Self {
1012 self.version_policy = Some(policy.into());
1013 self
1014 }
1015
1016 pub fn script(mut self, script: impl Into<String>) -> Self {
1018 self.script = Some(script.into());
1019 self
1020 }
1021
1022 pub fn opt(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
1024 self.opts.insert(key.into(), value.into());
1025 self
1026 }
1027
1028 pub fn flag(mut self, key: impl Into<String>) -> Self {
1032 self.opts.insert(key.into(), String::new());
1033 self
1034 }
1035
1036 pub fn build(self) -> Entry {
1042 let url = self.url.expect("URL is required for entry");
1043
1044 let mut builder = GreenNodeBuilder::new();
1045
1046 builder.start_node(ENTRY.into());
1047
1048 if !self.opts.is_empty() {
1050 builder.start_node(OPTS_LIST.into());
1051 builder.token(KEY.into(), "opts");
1052 builder.token(EQUALS.into(), "=");
1053
1054 let mut first = true;
1055 for (key, value) in self.opts.iter() {
1056 if !first {
1057 builder.token(COMMA.into(), ",");
1058 }
1059 first = false;
1060
1061 builder.start_node(OPTION.into());
1062 builder.token(KEY.into(), key);
1063 if !value.is_empty() {
1064 builder.token(EQUALS.into(), "=");
1065 builder.token(VALUE.into(), value);
1066 }
1067 builder.finish_node();
1068 }
1069
1070 builder.finish_node();
1071 builder.token(WHITESPACE.into(), " ");
1072 }
1073
1074 builder.start_node(URL.into());
1076 builder.token(VALUE.into(), &url);
1077 builder.finish_node();
1078
1079 if let Some(pattern) = self.matching_pattern {
1081 builder.token(WHITESPACE.into(), " ");
1082 builder.start_node(MATCHING_PATTERN.into());
1083 builder.token(VALUE.into(), &pattern);
1084 builder.finish_node();
1085 }
1086
1087 if let Some(policy) = self.version_policy {
1089 builder.token(WHITESPACE.into(), " ");
1090 builder.start_node(VERSION_POLICY.into());
1091 builder.token(VALUE.into(), &policy);
1092 builder.finish_node();
1093 }
1094
1095 if let Some(script_val) = self.script {
1097 builder.token(WHITESPACE.into(), " ");
1098 builder.start_node(SCRIPT.into());
1099 builder.token(VALUE.into(), &script_val);
1100 builder.finish_node();
1101 }
1102
1103 builder.token(NEWLINE.into(), "\n");
1104 builder.finish_node();
1105
1106 Entry(SyntaxNode::new_root_mut(builder.finish()))
1107 }
1108}
1109
1110impl Entry {
1111 pub fn syntax(&self) -> &SyntaxNode {
1113 &self.0
1114 }
1115
1116 pub fn builder(url: impl Into<String>) -> EntryBuilder {
1130 EntryBuilder::new(url)
1131 }
1132
1133 pub fn option_list(&self) -> Option<OptionList> {
1135 self.0.children().find_map(OptionList::cast)
1136 }
1137
1138 pub fn get_option(&self, key: &str) -> Option<String> {
1140 self.option_list().and_then(|ol| ol.get_option(key))
1141 }
1142
1143 pub fn has_option(&self, key: &str) -> bool {
1145 self.option_list().is_some_and(|ol| ol.has_option(key))
1146 }
1147
1148 pub fn component(&self) -> Option<String> {
1150 self.get_option("component")
1151 }
1152
1153 pub fn ctype(&self) -> Result<Option<ComponentType>, ()> {
1155 self.try_ctype().map_err(|_| ())
1156 }
1157
1158 pub fn try_ctype(&self) -> Result<Option<ComponentType>, crate::types::ParseError> {
1160 self.get_option("ctype").map(|s| s.parse()).transpose()
1161 }
1162
1163 pub fn compression(&self) -> Result<Option<Compression>, ()> {
1165 self.try_compression().map_err(|_| ())
1166 }
1167
1168 pub fn try_compression(&self) -> Result<Option<Compression>, crate::types::ParseError> {
1170 self.get_option("compression")
1171 .map(|s| s.parse())
1172 .transpose()
1173 }
1174
1175 pub fn repack(&self) -> bool {
1177 self.has_option("repack")
1178 }
1179
1180 pub fn repacksuffix(&self) -> Option<String> {
1182 self.get_option("repacksuffix")
1183 }
1184
1185 pub fn mode(&self) -> Result<Mode, ()> {
1187 self.try_mode().map_err(|_| ())
1188 }
1189
1190 pub fn try_mode(&self) -> Result<Mode, crate::types::ParseError> {
1192 Ok(self
1193 .get_option("mode")
1194 .map(|s| s.parse())
1195 .transpose()?
1196 .unwrap_or_default())
1197 }
1198
1199 pub fn pretty(&self) -> Result<Pretty, ()> {
1201 self.try_pretty().map_err(|_| ())
1202 }
1203
1204 pub fn try_pretty(&self) -> Result<Pretty, crate::types::ParseError> {
1206 Ok(self
1207 .get_option("pretty")
1208 .map(|s| s.parse())
1209 .transpose()?
1210 .unwrap_or_default())
1211 }
1212
1213 pub fn date(&self) -> String {
1216 self.get_option("date").unwrap_or_else(|| "%Y%m%d".into())
1217 }
1218
1219 pub fn gitexport(&self) -> Result<GitExport, ()> {
1221 self.try_gitexport().map_err(|_| ())
1222 }
1223
1224 pub fn try_gitexport(&self) -> Result<GitExport, crate::types::ParseError> {
1226 Ok(self
1227 .get_option("gitexport")
1228 .map(|s| s.parse())
1229 .transpose()?
1230 .unwrap_or_default())
1231 }
1232
1233 pub fn gitmode(&self) -> Result<GitMode, ()> {
1235 self.try_gitmode().map_err(|_| ())
1236 }
1237
1238 pub fn try_gitmode(&self) -> Result<GitMode, crate::types::ParseError> {
1240 Ok(self
1241 .get_option("gitmode")
1242 .map(|s| s.parse())
1243 .transpose()?
1244 .unwrap_or_default())
1245 }
1246
1247 pub fn pgpmode(&self) -> Result<PgpMode, ()> {
1249 self.try_pgpmode().map_err(|_| ())
1250 }
1251
1252 pub fn try_pgpmode(&self) -> Result<PgpMode, crate::types::ParseError> {
1254 Ok(self
1255 .get_option("pgpmode")
1256 .map(|s| s.parse())
1257 .transpose()?
1258 .unwrap_or_default())
1259 }
1260
1261 pub fn searchmode(&self) -> Result<SearchMode, ()> {
1263 self.try_searchmode().map_err(|_| ())
1264 }
1265
1266 pub fn try_searchmode(&self) -> Result<SearchMode, crate::types::ParseError> {
1268 Ok(self
1269 .get_option("searchmode")
1270 .map(|s| s.parse())
1271 .transpose()?
1272 .unwrap_or_default())
1273 }
1274
1275 pub fn decompress(&self) -> bool {
1277 self.has_option("decompress")
1278 }
1279
1280 pub fn bare(&self) -> bool {
1283 self.has_option("bare")
1284 }
1285
1286 pub fn user_agent(&self) -> Option<String> {
1288 self.get_option("user-agent")
1289 }
1290
1291 pub fn passive(&self) -> Option<bool> {
1293 if self.has_option("passive") || self.has_option("pasv") {
1294 Some(true)
1295 } else if self.has_option("active") || self.has_option("nopasv") {
1296 Some(false)
1297 } else {
1298 None
1299 }
1300 }
1301
1302 pub fn unzipoptions(&self) -> Option<String> {
1305 self.get_option("unzipopt")
1306 }
1307
1308 pub fn dversionmangle(&self) -> Option<String> {
1310 self.get_option("dversionmangle")
1311 .or_else(|| self.get_option("versionmangle"))
1312 }
1313
1314 pub fn dirversionmangle(&self) -> Option<String> {
1318 self.get_option("dirversionmangle")
1319 }
1320
1321 pub fn pagemangle(&self) -> Option<String> {
1323 self.get_option("pagemangle")
1324 }
1325
1326 pub fn uversionmangle(&self) -> Option<String> {
1330 self.get_option("uversionmangle")
1331 .or_else(|| self.get_option("versionmangle"))
1332 }
1333
1334 pub fn versionmangle(&self) -> Option<String> {
1336 self.get_option("versionmangle")
1337 }
1338
1339 pub fn hrefdecode(&self) -> bool {
1344 self.get_option("hrefdecode").is_some()
1345 }
1346
1347 pub fn downloadurlmangle(&self) -> Option<String> {
1350 self.get_option("downloadurlmangle")
1351 }
1352
1353 pub fn filenamemangle(&self) -> Option<String> {
1361 self.get_option("filenamemangle")
1362 }
1363
1364 pub fn pgpsigurlmangle(&self) -> Option<String> {
1366 self.get_option("pgpsigurlmangle")
1367 }
1368
1369 pub fn oversionmangle(&self) -> Option<String> {
1372 self.get_option("oversionmangle")
1373 }
1374
1375 pub fn apply_uversionmangle(
1388 &self,
1389 version: &str,
1390 ) -> Result<String, crate::mangle::MangleError> {
1391 if let Some(vm) = self.uversionmangle() {
1392 crate::mangle::apply_mangle(&vm, version)
1393 } else {
1394 Ok(version.to_string())
1395 }
1396 }
1397
1398 pub fn apply_dversionmangle(
1411 &self,
1412 version: &str,
1413 ) -> Result<String, crate::mangle::MangleError> {
1414 if let Some(vm) = self.dversionmangle() {
1415 crate::mangle::apply_mangle(&vm, version)
1416 } else {
1417 Ok(version.to_string())
1418 }
1419 }
1420
1421 pub fn apply_oversionmangle(
1434 &self,
1435 version: &str,
1436 ) -> Result<String, crate::mangle::MangleError> {
1437 if let Some(vm) = self.oversionmangle() {
1438 crate::mangle::apply_mangle(&vm, version)
1439 } else {
1440 Ok(version.to_string())
1441 }
1442 }
1443
1444 pub fn apply_dirversionmangle(
1457 &self,
1458 version: &str,
1459 ) -> Result<String, crate::mangle::MangleError> {
1460 if let Some(vm) = self.dirversionmangle() {
1461 crate::mangle::apply_mangle(&vm, version)
1462 } else {
1463 Ok(version.to_string())
1464 }
1465 }
1466
1467 pub fn apply_filenamemangle(&self, url: &str) -> Result<String, crate::mangle::MangleError> {
1483 if let Some(vm) = self.filenamemangle() {
1484 crate::mangle::apply_mangle(&vm, url)
1485 } else {
1486 Ok(url.to_string())
1487 }
1488 }
1489
1490 pub fn apply_pagemangle(&self, page: &[u8]) -> Result<Vec<u8>, crate::mangle::MangleError> {
1506 if let Some(vm) = self.pagemangle() {
1507 let page_str = String::from_utf8_lossy(page);
1508 let mangled = crate::mangle::apply_mangle(&vm, &page_str)?;
1509 Ok(mangled.into_bytes())
1510 } else {
1511 Ok(page.to_vec())
1512 }
1513 }
1514
1515 pub fn apply_downloadurlmangle(&self, url: &str) -> Result<String, crate::mangle::MangleError> {
1531 if let Some(vm) = self.downloadurlmangle() {
1532 crate::mangle::apply_mangle(&vm, url)
1533 } else {
1534 Ok(url.to_string())
1535 }
1536 }
1537
1538 pub fn opts(&self) -> std::collections::HashMap<String, String> {
1540 let mut options = std::collections::HashMap::new();
1541
1542 if let Some(ol) = self.option_list() {
1543 for opt in ol.options() {
1544 let key = opt.key();
1545 let value = opt.value();
1546 if let (Some(key), Some(value)) = (key, value) {
1547 options.insert(key.to_string(), value.to_string());
1548 }
1549 }
1550 }
1551
1552 options
1553 }
1554
1555 fn items(&self) -> impl Iterator<Item = String> + '_ {
1556 self.0.children_with_tokens().filter_map(|it| match it {
1557 SyntaxElement::Token(token) => {
1558 if token.kind() == VALUE || token.kind() == KEY {
1559 Some(token.text().to_string())
1560 } else {
1561 None
1562 }
1563 }
1564 SyntaxElement::Node(node) => {
1565 match node.kind() {
1567 URL => Url::cast(node).map(|n| n.url()),
1568 MATCHING_PATTERN => MatchingPattern::cast(node).map(|n| n.pattern()),
1569 VERSION_POLICY => VersionPolicyNode::cast(node).map(|n| n.policy()),
1570 SCRIPT => ScriptNode::cast(node).map(|n| n.script()),
1571 _ => None,
1572 }
1573 }
1574 })
1575 }
1576
1577 pub fn url_node(&self) -> Option<Url> {
1579 self.0.children().find_map(Url::cast)
1580 }
1581
1582 pub fn url(&self) -> String {
1584 self.url_node()
1585 .map(|it| it.url())
1586 .or_else(|| self.items().next())
1587 .unwrap_or_default()
1588 }
1589
1590 pub fn matching_pattern_node(&self) -> Option<MatchingPattern> {
1592 self.0.children().find_map(MatchingPattern::cast)
1593 }
1594
1595 pub fn matching_pattern(&self) -> Option<String> {
1597 self.matching_pattern_node()
1598 .map(|it| it.pattern())
1599 .or_else(|| {
1600 self.items().nth(1)
1602 })
1603 }
1604
1605 pub fn version_node(&self) -> Option<VersionPolicyNode> {
1607 self.0.children().find_map(VersionPolicyNode::cast)
1608 }
1609
1610 pub fn version(&self) -> Result<Option<crate::VersionPolicy>, String> {
1612 self.version_node()
1613 .map(|it| it.policy().parse())
1614 .transpose()
1615 .map_err(|e: crate::types::ParseError| e.to_string())
1616 .or_else(|_e| {
1617 self.items()
1619 .nth(2)
1620 .map(|it| it.parse())
1621 .transpose()
1622 .map_err(|e: crate::types::ParseError| e.to_string())
1623 })
1624 }
1625
1626 pub fn script_node(&self) -> Option<ScriptNode> {
1628 self.0.children().find_map(ScriptNode::cast)
1629 }
1630
1631 pub fn script(&self) -> Option<String> {
1633 self.script_node().map(|it| it.script()).or_else(|| {
1634 self.items().nth(3)
1636 })
1637 }
1638
1639 pub fn format_url(
1641 &self,
1642 package: impl FnOnce() -> String,
1643 component: impl FnOnce() -> String,
1644 ) -> url::Url {
1645 crate::subst::subst(self.url().as_str(), package, component)
1646 .parse()
1647 .unwrap()
1648 }
1649
1650 pub fn set_url(&mut self, new_url: &str) {
1652 let mut builder = GreenNodeBuilder::new();
1654 builder.start_node(URL.into());
1655 builder.token(VALUE.into(), new_url);
1656 builder.finish_node();
1657 let new_url_green = builder.finish();
1658
1659 let new_url_node = SyntaxNode::new_root_mut(new_url_green);
1661
1662 let url_pos = self
1664 .0
1665 .children_with_tokens()
1666 .position(|child| matches!(child, SyntaxElement::Node(node) if node.kind() == URL));
1667
1668 if let Some(pos) = url_pos {
1669 self.0
1671 .splice_children(pos..pos + 1, vec![new_url_node.into()]);
1672 }
1673 }
1674
1675 pub fn set_matching_pattern(&mut self, new_pattern: &str) {
1681 let mut builder = GreenNodeBuilder::new();
1683 builder.start_node(MATCHING_PATTERN.into());
1684 builder.token(VALUE.into(), new_pattern);
1685 builder.finish_node();
1686 let new_pattern_green = builder.finish();
1687
1688 let new_pattern_node = SyntaxNode::new_root_mut(new_pattern_green);
1690
1691 let pattern_pos = self.0.children_with_tokens().position(
1693 |child| matches!(child, SyntaxElement::Node(node) if node.kind() == MATCHING_PATTERN),
1694 );
1695
1696 if let Some(pos) = pattern_pos {
1697 self.0
1699 .splice_children(pos..pos + 1, vec![new_pattern_node.into()]);
1700 }
1701 }
1703
1704 pub fn set_version_policy(&mut self, new_policy: &str) {
1710 let mut builder = GreenNodeBuilder::new();
1712 builder.start_node(VERSION_POLICY.into());
1713 builder.token(VALUE.into(), new_policy);
1715 builder.finish_node();
1716 let new_policy_green = builder.finish();
1717
1718 let new_policy_node = SyntaxNode::new_root_mut(new_policy_green);
1720
1721 let policy_pos = self.0.children_with_tokens().position(
1723 |child| matches!(child, SyntaxElement::Node(node) if node.kind() == VERSION_POLICY),
1724 );
1725
1726 if let Some(pos) = policy_pos {
1727 self.0
1729 .splice_children(pos..pos + 1, vec![new_policy_node.into()]);
1730 }
1731 }
1733
1734 pub fn set_script(&mut self, new_script: &str) {
1740 let mut builder = GreenNodeBuilder::new();
1742 builder.start_node(SCRIPT.into());
1743 builder.token(VALUE.into(), new_script);
1745 builder.finish_node();
1746 let new_script_green = builder.finish();
1747
1748 let new_script_node = SyntaxNode::new_root_mut(new_script_green);
1750
1751 let script_pos = self
1753 .0
1754 .children_with_tokens()
1755 .position(|child| matches!(child, SyntaxElement::Node(node) if node.kind() == SCRIPT));
1756
1757 if let Some(pos) = script_pos {
1758 self.0
1760 .splice_children(pos..pos + 1, vec![new_script_node.into()]);
1761 }
1762 }
1764
1765 pub fn set_option(&mut self, option: crate::types::WatchOption) {
1771 let key = watch_option_to_key(&option);
1772 let value = watch_option_to_value(&option);
1773 self.set_opt(key, &value);
1774 }
1775
1776 pub fn set_opt(&mut self, key: &str, value: &str) {
1782 let opts_pos = self.0.children_with_tokens().position(
1784 |child| matches!(child, SyntaxElement::Node(node) if node.kind() == OPTS_LIST),
1785 );
1786
1787 if let Some(_opts_idx) = opts_pos {
1788 if let Some(mut ol) = self.option_list() {
1789 if let Some(mut opt) = ol.find_option(key) {
1791 opt.set_value(value);
1793 } else {
1795 ol.add_option(key, value);
1797 }
1799 }
1800 } else {
1801 let mut builder = GreenNodeBuilder::new();
1803 builder.start_node(OPTS_LIST.into());
1804 builder.token(KEY.into(), "opts");
1805 builder.token(EQUALS.into(), "=");
1806 builder.start_node(OPTION.into());
1807 builder.token(KEY.into(), key);
1808 builder.token(EQUALS.into(), "=");
1809 builder.token(VALUE.into(), value);
1810 builder.finish_node();
1811 builder.finish_node();
1812 let new_opts_green = builder.finish();
1813 let new_opts_node = SyntaxNode::new_root_mut(new_opts_green);
1814
1815 let url_pos = self
1817 .0
1818 .children_with_tokens()
1819 .position(|child| matches!(child, SyntaxElement::Node(node) if node.kind() == URL));
1820
1821 if let Some(url_idx) = url_pos {
1822 let mut combined_builder = GreenNodeBuilder::new();
1825 combined_builder.start_node(ROOT.into()); combined_builder.token(WHITESPACE.into(), " ");
1827 combined_builder.finish_node();
1828 let temp_green = combined_builder.finish();
1829 let temp_root = SyntaxNode::new_root_mut(temp_green);
1830 let space_element = temp_root.children_with_tokens().next().unwrap();
1831
1832 self.0
1833 .splice_children(url_idx..url_idx, vec![new_opts_node.into(), space_element]);
1834 } else {
1835 self.0.splice_children(0..0, vec![new_opts_node.into()]);
1836 }
1837 }
1838 }
1839
1840 pub fn del_opt(&mut self, option: crate::types::WatchOption) {
1847 let key = watch_option_to_key(&option);
1848 if let Some(mut ol) = self.option_list() {
1849 let option_count = ol.0.children().filter(|n| n.kind() == OPTION).count();
1850
1851 if option_count == 1 && ol.has_option(key) {
1852 let opts_pos = self.0.children().position(|node| node.kind() == OPTS_LIST);
1854
1855 if let Some(opts_idx) = opts_pos {
1856 self.0.splice_children(opts_idx..opts_idx + 1, vec![]);
1858
1859 while self.0.children_with_tokens().next().is_some_and(|e| {
1861 matches!(
1862 e,
1863 SyntaxElement::Token(t) if t.kind() == WHITESPACE || t.kind() == CONTINUATION
1864 )
1865 }) {
1866 self.0.splice_children(0..1, vec![]);
1867 }
1868 }
1869 } else {
1870 ol.remove_option(key);
1872 }
1873 }
1874 }
1875
1876 pub fn del_opt_str(&mut self, key: &str) {
1883 if let Some(mut ol) = self.option_list() {
1884 let option_count = ol.0.children().filter(|n| n.kind() == OPTION).count();
1885
1886 if option_count == 1 && ol.has_option(key) {
1887 let opts_pos = self.0.children().position(|node| node.kind() == OPTS_LIST);
1889
1890 if let Some(opts_idx) = opts_pos {
1891 self.0.splice_children(opts_idx..opts_idx + 1, vec![]);
1893
1894 while self.0.children_with_tokens().next().is_some_and(|e| {
1896 matches!(
1897 e,
1898 SyntaxElement::Token(t) if t.kind() == WHITESPACE || t.kind() == CONTINUATION
1899 )
1900 }) {
1901 self.0.splice_children(0..1, vec![]);
1902 }
1903 }
1904 } else {
1905 ol.remove_option(key);
1907 }
1908 }
1909 }
1910}
1911
1912impl std::fmt::Debug for OptionList {
1913 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1914 f.debug_struct("OptionList")
1915 .field("text", &self.0.text().to_string())
1916 .finish()
1917 }
1918}
1919
1920impl OptionList {
1921 pub fn options(&self) -> impl Iterator<Item = _Option> + '_ {
1923 self.0.children().filter_map(_Option::cast)
1924 }
1925
1926 pub fn find_option(&self, key: &str) -> Option<_Option> {
1928 self.options().find(|opt| opt.key().as_deref() == Some(key))
1929 }
1930
1931 pub fn has_option(&self, key: &str) -> bool {
1933 self.options().any(|it| it.key().as_deref() == Some(key))
1934 }
1935
1936 #[cfg(feature = "deb822")]
1939 pub(crate) fn iter_key_values(&self) -> impl Iterator<Item = (String, String)> + '_ {
1940 self.options().filter_map(|opt| {
1941 if let (Some(key), Some(value)) = (opt.key(), opt.value()) {
1942 Some((key, value))
1943 } else {
1944 None
1945 }
1946 })
1947 }
1948
1949 pub fn get_option(&self, key: &str) -> Option<String> {
1951 for child in self.options() {
1952 if child.key().as_deref() == Some(key) {
1953 return child.value();
1954 }
1955 }
1956 None
1957 }
1958
1959 fn add_option(&mut self, key: &str, value: &str) {
1961 let option_count = self.0.children().filter(|n| n.kind() == OPTION).count();
1962
1963 let mut builder = GreenNodeBuilder::new();
1965 builder.start_node(ROOT.into()); if option_count > 0 {
1968 builder.start_node(OPTION_SEPARATOR.into());
1969 builder.token(COMMA.into(), ",");
1970 builder.finish_node();
1971 }
1972
1973 builder.start_node(OPTION.into());
1974 builder.token(KEY.into(), key);
1975 builder.token(EQUALS.into(), "=");
1976 builder.token(VALUE.into(), value);
1977 builder.finish_node();
1978
1979 builder.finish_node(); let combined_green = builder.finish();
1981
1982 let temp_root = SyntaxNode::new_root_mut(combined_green);
1984 let new_children: Vec<_> = temp_root.children_with_tokens().collect();
1985
1986 let insert_pos = self.0.children_with_tokens().count();
1987 self.0.splice_children(insert_pos..insert_pos, new_children);
1988 }
1989
1990 fn remove_option(&mut self, key: &str) -> bool {
1992 if let Some(mut opt) = self.find_option(key) {
1993 opt.remove();
1994 true
1995 } else {
1996 false
1997 }
1998 }
1999}
2000
2001impl _Option {
2002 pub fn key(&self) -> Option<String> {
2004 self.0.children_with_tokens().find_map(|it| match it {
2005 SyntaxElement::Token(token) => {
2006 if token.kind() == KEY {
2007 Some(token.text().to_string())
2008 } else {
2009 None
2010 }
2011 }
2012 _ => None,
2013 })
2014 }
2015
2016 pub fn value(&self) -> Option<String> {
2018 self.0
2019 .children_with_tokens()
2020 .filter_map(|it| match it {
2021 SyntaxElement::Token(token) => {
2022 if token.kind() == VALUE || token.kind() == KEY {
2023 Some(token.text().to_string())
2024 } else {
2025 None
2026 }
2027 }
2028 _ => None,
2029 })
2030 .nth(1)
2031 }
2032
2033 pub fn set_value(&mut self, new_value: &str) {
2035 let key = self.key().expect("Option must have a key");
2036
2037 let mut builder = GreenNodeBuilder::new();
2039 builder.start_node(OPTION.into());
2040 builder.token(KEY.into(), &key);
2041 builder.token(EQUALS.into(), "=");
2042 builder.token(VALUE.into(), new_value);
2043 builder.finish_node();
2044 let new_option_green = builder.finish();
2045 let new_option_node = SyntaxNode::new_root_mut(new_option_green);
2046
2047 if let Some(parent) = self.0.parent() {
2049 let idx = self.0.index();
2050 parent.splice_children(idx..idx + 1, vec![new_option_node.into()]);
2051 }
2052 }
2053
2054 pub fn remove(&mut self) {
2056 let next_sep = self
2058 .0
2059 .next_sibling()
2060 .filter(|n| n.kind() == OPTION_SEPARATOR);
2061 let prev_sep = self
2062 .0
2063 .prev_sibling()
2064 .filter(|n| n.kind() == OPTION_SEPARATOR);
2065
2066 if let Some(sep) = next_sep {
2068 sep.detach();
2069 } else if let Some(sep) = prev_sep {
2070 sep.detach();
2071 }
2072
2073 self.0.detach();
2075 }
2076}
2077
2078fn join_tokens(node: &SyntaxNode, keep: impl Fn(SyntaxKind) -> bool) -> String {
2083 let mut out = String::new();
2084 for it in node.children_with_tokens() {
2085 if let SyntaxElement::Token(token) = it {
2086 if keep(token.kind()) {
2087 out.push_str(token.text());
2088 }
2089 }
2090 }
2091 out
2092}
2093
2094impl Url {
2095 pub fn url(&self) -> String {
2097 join_tokens(&self.0, |k| {
2098 matches!(k, KEY | VALUE | EQUALS | COMMA | QUOTE)
2099 })
2100 }
2101}
2102
2103impl MatchingPattern {
2104 pub fn pattern(&self) -> String {
2106 join_tokens(&self.0, |k| {
2107 matches!(k, KEY | VALUE | EQUALS | COMMA | QUOTE)
2108 })
2109 }
2110}
2111
2112impl VersionPolicyNode {
2113 pub fn policy(&self) -> String {
2115 join_tokens(&self.0, |k| {
2116 matches!(k, KEY | VALUE | EQUALS | COMMA | QUOTE)
2117 })
2118 }
2119}
2120
2121impl ScriptNode {
2122 pub fn script(&self) -> String {
2129 let mut out = String::new();
2130 for it in self.0.children_with_tokens() {
2131 if let SyntaxElement::Token(token) = it {
2132 match token.kind() {
2133 KEY | VALUE | EQUALS | COMMA | QUOTE => out.push_str(token.text()),
2134 WHITESPACE => out.push_str(token.text()),
2135 _ => {}
2136 }
2137 }
2138 }
2139 out.trim().to_string()
2140 }
2141}
2142
2143#[cfg(test)]
2144mod tests {
2145 use super::*;
2146
2147 #[test]
2148 fn test_entry_node_structure() {
2149 let wf: super::WatchFile = r#"version=4
2151opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2152"#
2153 .parse()
2154 .unwrap();
2155
2156 let entry = wf.entries().next().unwrap();
2157
2158 assert_eq!(entry.0.children().find(|n| n.kind() == URL).is_some(), true);
2160 assert_eq!(entry.url(), "https://example.com/releases");
2161
2162 assert_eq!(
2164 entry
2165 .0
2166 .children()
2167 .find(|n| n.kind() == MATCHING_PATTERN)
2168 .is_some(),
2169 true
2170 );
2171 assert_eq!(
2172 entry.matching_pattern(),
2173 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2174 );
2175
2176 assert_eq!(
2178 entry
2179 .0
2180 .children()
2181 .find(|n| n.kind() == VERSION_POLICY)
2182 .is_some(),
2183 true
2184 );
2185 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2186
2187 assert_eq!(
2189 entry.0.children().find(|n| n.kind() == SCRIPT).is_some(),
2190 true
2191 );
2192 assert_eq!(entry.script(), Some("uupdate".into()));
2193 }
2194
2195 #[test]
2196 fn test_entry_node_structure_partial() {
2197 let wf: super::WatchFile = r#"version=4
2199https://github.com/example/tags .*/v?(\d\S+)\.tar\.gz
2200"#
2201 .parse()
2202 .unwrap();
2203
2204 let entry = wf.entries().next().unwrap();
2205
2206 assert_eq!(entry.0.children().find(|n| n.kind() == URL).is_some(), true);
2208 assert_eq!(
2209 entry
2210 .0
2211 .children()
2212 .find(|n| n.kind() == MATCHING_PATTERN)
2213 .is_some(),
2214 true
2215 );
2216
2217 assert_eq!(
2219 entry
2220 .0
2221 .children()
2222 .find(|n| n.kind() == VERSION_POLICY)
2223 .is_some(),
2224 false
2225 );
2226 assert_eq!(
2227 entry.0.children().find(|n| n.kind() == SCRIPT).is_some(),
2228 false
2229 );
2230
2231 assert_eq!(entry.url(), "https://github.com/example/tags");
2233 assert_eq!(
2234 entry.matching_pattern(),
2235 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2236 );
2237 assert_eq!(entry.version(), Ok(None));
2238 assert_eq!(entry.script(), None);
2239 }
2240
2241 #[test]
2242 fn test_parse_v1() {
2243 const WATCHV1: &str = r#"version=4
2244opts=bare,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1\.tar\.gz/ \
2245 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2246"#;
2247 let parsed = parse(WATCHV1);
2248 let node = parsed.syntax();
2250 assert_eq!(
2251 format!("{:#?}", node),
2252 r#"ROOT@0..161
2253 VERSION@0..10
2254 KEY@0..7 "version"
2255 EQUALS@7..8 "="
2256 VALUE@8..9 "4"
2257 NEWLINE@9..10 "\n"
2258 ENTRY@10..161
2259 OPTS_LIST@10..86
2260 KEY@10..14 "opts"
2261 EQUALS@14..15 "="
2262 OPTION@15..19
2263 KEY@15..19 "bare"
2264 OPTION_SEPARATOR@19..20
2265 COMMA@19..20 ","
2266 OPTION@20..86
2267 KEY@20..34 "filenamemangle"
2268 EQUALS@34..35 "="
2269 VALUE@35..86 "s/.+\\/v?(\\d\\S+)\\.tar\\ ..."
2270 WHITESPACE@86..87 " "
2271 CONTINUATION@87..89 "\\\n"
2272 WHITESPACE@89..91 " "
2273 URL@91..138
2274 VALUE@91..138 "https://github.com/sy ..."
2275 WHITESPACE@138..139 " "
2276 MATCHING_PATTERN@139..160
2277 VALUE@139..160 ".*/v?(\\d\\S+)\\.tar\\.gz"
2278 NEWLINE@160..161 "\n"
2279"#
2280 );
2281
2282 let root = parsed.root();
2283 assert_eq!(root.version(), 4);
2284 let entries = root.entries().collect::<Vec<_>>();
2285 assert_eq!(entries.len(), 1);
2286 let entry = &entries[0];
2287 assert_eq!(
2288 entry.url(),
2289 "https://github.com/syncthing/syncthing-gtk/tags"
2290 );
2291 assert_eq!(
2292 entry.matching_pattern(),
2293 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2294 );
2295 assert_eq!(entry.version(), Ok(None));
2296 assert_eq!(entry.script(), None);
2297
2298 assert_eq!(node.text(), WATCHV1);
2299 }
2300
2301 #[test]
2302 fn test_parse_v2() {
2303 let parsed = parse(
2304 r#"version=4
2305https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2306# comment
2307"#,
2308 );
2309 assert_eq!(parsed.errors, Vec::<String>::new());
2310 let node = parsed.syntax();
2311 assert_eq!(
2312 format!("{:#?}", node),
2313 r###"ROOT@0..90
2314 VERSION@0..10
2315 KEY@0..7 "version"
2316 EQUALS@7..8 "="
2317 VALUE@8..9 "4"
2318 NEWLINE@9..10 "\n"
2319 ENTRY@10..80
2320 URL@10..57
2321 VALUE@10..57 "https://github.com/sy ..."
2322 WHITESPACE@57..58 " "
2323 MATCHING_PATTERN@58..79
2324 VALUE@58..79 ".*/v?(\\d\\S+)\\.tar\\.gz"
2325 NEWLINE@79..80 "\n"
2326 COMMENT@80..89 "# comment"
2327 NEWLINE@89..90 "\n"
2328"###
2329 );
2330
2331 let root = parsed.root();
2332 assert_eq!(root.version(), 4);
2333 let entries = root.entries().collect::<Vec<_>>();
2334 assert_eq!(entries.len(), 1);
2335 let entry = &entries[0];
2336 assert_eq!(
2337 entry.url(),
2338 "https://github.com/syncthing/syncthing-gtk/tags"
2339 );
2340 assert_eq!(
2341 entry.format_url(|| "syncthing-gtk".to_string(), || String::new()),
2342 "https://github.com/syncthing/syncthing-gtk/tags"
2343 .parse()
2344 .unwrap()
2345 );
2346 }
2347
2348 #[test]
2349 fn test_parse_v3() {
2350 let parsed = parse(
2351 r#"version=4
2352https://github.com/syncthing/@PACKAGE@/tags .*/v?(\d\S+)\.tar\.gz
2353# comment
2354"#,
2355 );
2356 assert_eq!(parsed.errors, Vec::<String>::new());
2357 let root = parsed.root();
2358 assert_eq!(root.version(), 4);
2359 let entries = root.entries().collect::<Vec<_>>();
2360 assert_eq!(entries.len(), 1);
2361 let entry = &entries[0];
2362 assert_eq!(entry.url(), "https://github.com/syncthing/@PACKAGE@/tags");
2363 assert_eq!(
2364 entry.format_url(|| "syncthing-gtk".to_string(), || String::new()),
2365 "https://github.com/syncthing/syncthing-gtk/tags"
2366 .parse()
2367 .unwrap()
2368 );
2369 }
2370
2371 #[test]
2372 fn test_thread_safe_parsing() {
2373 let text = r#"version=4
2374https://github.com/example/example/tags example-(.*)\.tar\.gz
2375"#;
2376
2377 let parsed = parse_watch_file(text);
2378 assert!(parsed.is_ok());
2379 assert_eq!(parsed.errors().len(), 0);
2380
2381 let watchfile = parsed.tree();
2383 assert_eq!(watchfile.version(), 4);
2384 let entries: Vec<_> = watchfile.entries().collect();
2385 assert_eq!(entries.len(), 1);
2386 }
2387
2388 #[test]
2389 fn test_parse_clone_and_eq() {
2390 let text = r#"version=4
2391https://github.com/example/example/tags example-(.*)\.tar\.gz
2392"#;
2393
2394 let parsed1 = parse_watch_file(text);
2395 let parsed2 = parsed1.clone();
2396
2397 assert_eq!(parsed1, parsed2);
2399
2400 let watchfile1 = parsed1.tree();
2402 let watchfile2 = watchfile1.clone();
2403 assert_eq!(watchfile1, watchfile2);
2404 }
2405
2406 #[test]
2407 fn test_parse_v4() {
2408 let cl: super::WatchFile = r#"version=4
2409opts=repack,compression=xz,dversionmangle=s/\+ds//,repacksuffix=+ds \
2410 https://github.com/example/example-cat/tags \
2411 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2412"#
2413 .parse()
2414 .unwrap();
2415 assert_eq!(cl.version(), 4);
2416 let entries = cl.entries().collect::<Vec<_>>();
2417 assert_eq!(entries.len(), 1);
2418 let entry = &entries[0];
2419 assert_eq!(entry.url(), "https://github.com/example/example-cat/tags");
2420 assert_eq!(
2421 entry.matching_pattern(),
2422 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2423 );
2424 assert!(entry.repack());
2425 assert_eq!(entry.compression(), Ok(Some(Compression::Xz)));
2426 assert_eq!(entry.dversionmangle(), Some("s/\\+ds//".into()));
2427 assert_eq!(entry.repacksuffix(), Some("+ds".into()));
2428 assert_eq!(entry.script(), Some("uupdate".into()));
2429 assert_eq!(
2430 entry.format_url(|| "example-cat".to_string(), || String::new()),
2431 "https://github.com/example/example-cat/tags"
2432 .parse()
2433 .unwrap()
2434 );
2435 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
2436 }
2437
2438 #[test]
2439 fn test_script_with_arguments() {
2440 const WATCH: &str = "version=4\n\
2444 opts=\"mode=git,pgpmode=gittag\" \\\n\
2445 https://github.com/NetworkConfiguration/dhcpcd.git refs/tags/v([\\d\\.]+) \\\n\
2446 debian uupdate --no-symlink\n";
2447 let wf: super::WatchFile = WATCH.parse().unwrap();
2448 assert_eq!(wf.version(), 4);
2449 let entries = wf.entries().collect::<Vec<_>>();
2450 assert_eq!(entries.len(), 1);
2451 let entry = &entries[0];
2452 assert_eq!(
2453 entry.url(),
2454 "https://github.com/NetworkConfiguration/dhcpcd.git"
2455 );
2456 assert_eq!(
2457 entry.matching_pattern(),
2458 Some("refs/tags/v([\\d\\.]+)".into())
2459 );
2460 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
2461 assert_eq!(entry.script(), Some("uupdate --no-symlink".into()));
2462 assert_eq!(entry.mode(), Ok(Mode::Git));
2463 assert_eq!(entry.pgpmode(), Ok(PgpMode::GitTag));
2464 assert_eq!(wf.to_string(), WATCH);
2466 }
2467
2468 #[test]
2469 fn test_git_mode() {
2470 let text = r#"version=3
2471opts="mode=git, gitmode=shallow, pgpmode=gittag" \
2472https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git \
2473refs/tags/(.*) debian
2474"#;
2475 let parsed = parse(text);
2476 assert_eq!(parsed.errors, Vec::<String>::new());
2477 let cl = parsed.root();
2478 assert_eq!(cl.version(), 3);
2479 let entries = cl.entries().collect::<Vec<_>>();
2480 assert_eq!(entries.len(), 1);
2481 let entry = &entries[0];
2482 assert_eq!(
2483 entry.url(),
2484 "https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git"
2485 );
2486 assert_eq!(entry.matching_pattern(), Some("refs/tags/(.*)".into()));
2487 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
2488 assert_eq!(entry.script(), None);
2489 assert_eq!(entry.gitmode(), Ok(GitMode::Shallow));
2490 assert_eq!(entry.pgpmode(), Ok(PgpMode::GitTag));
2491 assert_eq!(entry.mode(), Ok(Mode::Git));
2492 }
2493
2494 #[test]
2495 fn test_parse_quoted() {
2496 const WATCHV1: &str = r#"version=4
2497opts="bare, filenamemangle=blah" \
2498 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2499"#;
2500 let parsed = parse(WATCHV1);
2501 let node = parsed.syntax();
2503
2504 let root = parsed.root();
2505 assert_eq!(root.version(), 4);
2506 let entries = root.entries().collect::<Vec<_>>();
2507 assert_eq!(entries.len(), 1);
2508 let entry = &entries[0];
2509
2510 assert_eq!(
2511 entry.url(),
2512 "https://github.com/syncthing/syncthing-gtk/tags"
2513 );
2514 assert_eq!(
2515 entry.matching_pattern(),
2516 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2517 );
2518 assert_eq!(entry.version(), Ok(None));
2519 assert_eq!(entry.script(), None);
2520
2521 assert_eq!(node.text(), WATCHV1);
2522 }
2523
2524 #[test]
2525 fn test_set_url() {
2526 let wf: super::WatchFile = r#"version=4
2528https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2529"#
2530 .parse()
2531 .unwrap();
2532
2533 let mut entry = wf.entries().next().unwrap();
2534 assert_eq!(
2535 entry.url(),
2536 "https://github.com/syncthing/syncthing-gtk/tags"
2537 );
2538
2539 entry.set_url("https://newurl.example.org/path");
2540 assert_eq!(entry.url(), "https://newurl.example.org/path");
2541 assert_eq!(
2542 entry.matching_pattern(),
2543 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2544 );
2545
2546 assert_eq!(
2548 entry.to_string(),
2549 "https://newurl.example.org/path .*/v?(\\d\\S+)\\.tar\\.gz\n"
2550 );
2551 }
2552
2553 #[test]
2554 fn test_set_url_with_options() {
2555 let wf: super::WatchFile = r#"version=4
2557opts=foo=blah https://foo.com/bar .*/v?(\d\S+)\.tar\.gz
2558"#
2559 .parse()
2560 .unwrap();
2561
2562 let mut entry = wf.entries().next().unwrap();
2563 assert_eq!(entry.url(), "https://foo.com/bar");
2564 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2565
2566 entry.set_url("https://example.com/baz");
2567 assert_eq!(entry.url(), "https://example.com/baz");
2568
2569 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2571 assert_eq!(
2572 entry.matching_pattern(),
2573 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2574 );
2575
2576 assert_eq!(
2578 entry.to_string(),
2579 "opts=foo=blah https://example.com/baz .*/v?(\\d\\S+)\\.tar\\.gz\n"
2580 );
2581 }
2582
2583 #[test]
2584 fn test_set_url_complex() {
2585 let wf: super::WatchFile = r#"version=4
2587opts=bare,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1\.tar\.gz/ \
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://gitlab.com/newproject/tags");
2600 assert_eq!(entry.url(), "https://gitlab.com/newproject/tags");
2601
2602 assert!(entry.bare());
2604 assert_eq!(
2605 entry.filenamemangle(),
2606 Some("s/.+\\/v?(\\d\\S+)\\.tar\\.gz/syncthing-gtk-$1\\.tar\\.gz/".into())
2607 );
2608 assert_eq!(
2609 entry.matching_pattern(),
2610 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2611 );
2612
2613 assert_eq!(
2615 entry.to_string(),
2616 r#"opts=bare,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1\.tar\.gz/ \
2617 https://gitlab.com/newproject/tags .*/v?(\d\S+)\.tar\.gz
2618"#
2619 );
2620 }
2621
2622 #[test]
2623 fn test_set_url_with_all_fields() {
2624 let wf: super::WatchFile = r#"version=4
2626opts=repack,compression=xz,dversionmangle=s/\+ds//,repacksuffix=+ds \
2627 https://github.com/example/example-cat/tags \
2628 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2629"#
2630 .parse()
2631 .unwrap();
2632
2633 let mut entry = wf.entries().next().unwrap();
2634 assert_eq!(entry.url(), "https://github.com/example/example-cat/tags");
2635 assert_eq!(
2636 entry.matching_pattern(),
2637 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2638 );
2639 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2640 assert_eq!(entry.script(), Some("uupdate".into()));
2641
2642 entry.set_url("https://gitlab.example.org/project/releases");
2643 assert_eq!(entry.url(), "https://gitlab.example.org/project/releases");
2644
2645 assert!(entry.repack());
2647 assert_eq!(entry.compression(), Ok(Some(super::Compression::Xz)));
2648 assert_eq!(entry.dversionmangle(), Some("s/\\+ds//".into()));
2649 assert_eq!(entry.repacksuffix(), Some("+ds".into()));
2650 assert_eq!(
2651 entry.matching_pattern(),
2652 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2653 );
2654 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2655 assert_eq!(entry.script(), Some("uupdate".into()));
2656
2657 assert_eq!(
2659 entry.to_string(),
2660 r#"opts=repack,compression=xz,dversionmangle=s/\+ds//,repacksuffix=+ds \
2661 https://gitlab.example.org/project/releases \
2662 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2663"#
2664 );
2665 }
2666
2667 #[test]
2668 fn test_set_url_quoted_options() {
2669 let wf: super::WatchFile = r#"version=4
2671opts="bare, filenamemangle=blah" \
2672 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2673"#
2674 .parse()
2675 .unwrap();
2676
2677 let mut entry = wf.entries().next().unwrap();
2678 assert_eq!(
2679 entry.url(),
2680 "https://github.com/syncthing/syncthing-gtk/tags"
2681 );
2682
2683 entry.set_url("https://example.org/new/path");
2684 assert_eq!(entry.url(), "https://example.org/new/path");
2685
2686 assert_eq!(
2688 entry.to_string(),
2689 r#"opts="bare, filenamemangle=blah" \
2690 https://example.org/new/path .*/v?(\d\S+)\.tar\.gz
2691"#
2692 );
2693 }
2694
2695 #[test]
2696 fn test_set_opt_update_existing() {
2697 let wf: super::WatchFile = r#"version=4
2699opts=foo=blah,bar=baz https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2700"#
2701 .parse()
2702 .unwrap();
2703
2704 let mut entry = wf.entries().next().unwrap();
2705 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2706 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2707
2708 entry.set_opt("foo", "updated");
2709 assert_eq!(entry.get_option("foo"), Some("updated".to_string()));
2710 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2711
2712 assert_eq!(
2714 entry.to_string(),
2715 "opts=foo=updated,bar=baz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2716 );
2717 }
2718
2719 #[test]
2720 fn test_set_opt_add_new() {
2721 let wf: super::WatchFile = r#"version=4
2723opts=foo=blah https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2724"#
2725 .parse()
2726 .unwrap();
2727
2728 let mut entry = wf.entries().next().unwrap();
2729 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2730 assert_eq!(entry.get_option("bar"), None);
2731
2732 entry.set_opt("bar", "baz");
2733 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2734 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2735
2736 assert_eq!(
2738 entry.to_string(),
2739 "opts=foo=blah,bar=baz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2740 );
2741 }
2742
2743 #[test]
2744 fn test_set_opt_create_options_list() {
2745 let wf: super::WatchFile = r#"version=4
2747https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2748"#
2749 .parse()
2750 .unwrap();
2751
2752 let mut entry = wf.entries().next().unwrap();
2753 assert_eq!(entry.option_list(), None);
2754
2755 entry.set_opt("compression", "xz");
2756 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
2757
2758 assert_eq!(
2760 entry.to_string(),
2761 "opts=compression=xz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2762 );
2763 }
2764
2765 #[test]
2766 fn test_del_opt_remove_single() {
2767 let wf: super::WatchFile = r#"version=4
2769opts=foo=blah,bar=baz,qux=quux https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2770"#
2771 .parse()
2772 .unwrap();
2773
2774 let mut entry = wf.entries().next().unwrap();
2775 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2776 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2777 assert_eq!(entry.get_option("qux"), Some("quux".to_string()));
2778
2779 entry.del_opt_str("bar");
2780 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2781 assert_eq!(entry.get_option("bar"), None);
2782 assert_eq!(entry.get_option("qux"), Some("quux".to_string()));
2783
2784 assert_eq!(
2786 entry.to_string(),
2787 "opts=foo=blah,qux=quux https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2788 );
2789 }
2790
2791 #[test]
2792 fn test_del_opt_remove_first() {
2793 let wf: super::WatchFile = r#"version=4
2795opts=foo=blah,bar=baz https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2796"#
2797 .parse()
2798 .unwrap();
2799
2800 let mut entry = wf.entries().next().unwrap();
2801 entry.del_opt_str("foo");
2802 assert_eq!(entry.get_option("foo"), None);
2803 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2804
2805 assert_eq!(
2807 entry.to_string(),
2808 "opts=bar=baz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2809 );
2810 }
2811
2812 #[test]
2813 fn test_del_opt_remove_last() {
2814 let wf: super::WatchFile = r#"version=4
2816opts=foo=blah,bar=baz https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2817"#
2818 .parse()
2819 .unwrap();
2820
2821 let mut entry = wf.entries().next().unwrap();
2822 entry.del_opt_str("bar");
2823 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2824 assert_eq!(entry.get_option("bar"), None);
2825
2826 assert_eq!(
2828 entry.to_string(),
2829 "opts=foo=blah https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2830 );
2831 }
2832
2833 #[test]
2834 fn test_del_opt_remove_only_option() {
2835 let wf: super::WatchFile = r#"version=4
2837opts=foo=blah https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2838"#
2839 .parse()
2840 .unwrap();
2841
2842 let mut entry = wf.entries().next().unwrap();
2843 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2844
2845 entry.del_opt_str("foo");
2846 assert_eq!(entry.get_option("foo"), None);
2847 assert_eq!(entry.option_list(), None);
2848
2849 assert_eq!(
2851 entry.to_string(),
2852 "https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2853 );
2854 }
2855
2856 #[test]
2857 fn test_del_opt_nonexistent() {
2858 let wf: super::WatchFile = r#"version=4
2860opts=foo=blah https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2861"#
2862 .parse()
2863 .unwrap();
2864
2865 let mut entry = wf.entries().next().unwrap();
2866 let original = entry.to_string();
2867
2868 entry.del_opt_str("nonexistent");
2869 assert_eq!(entry.to_string(), original);
2870 }
2871
2872 #[test]
2873 fn test_set_opt_multiple_operations() {
2874 let wf: super::WatchFile = r#"version=4
2876https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2877"#
2878 .parse()
2879 .unwrap();
2880
2881 let mut entry = wf.entries().next().unwrap();
2882
2883 entry.set_opt("compression", "xz");
2884 entry.set_opt("repack", "");
2885 entry.set_opt("dversionmangle", "s/\\+ds//");
2886
2887 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
2888 assert_eq!(
2889 entry.get_option("dversionmangle"),
2890 Some("s/\\+ds//".to_string())
2891 );
2892 }
2893
2894 #[test]
2895 fn test_set_matching_pattern() {
2896 let wf: super::WatchFile = r#"version=4
2898https://github.com/example/tags .*/v?(\d\S+)\.tar\.gz
2899"#
2900 .parse()
2901 .unwrap();
2902
2903 let mut entry = wf.entries().next().unwrap();
2904 assert_eq!(
2905 entry.matching_pattern(),
2906 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2907 );
2908
2909 entry.set_matching_pattern("(?:.*?/)?v?([\\d.]+)\\.tar\\.gz");
2910 assert_eq!(
2911 entry.matching_pattern(),
2912 Some("(?:.*?/)?v?([\\d.]+)\\.tar\\.gz".into())
2913 );
2914
2915 assert_eq!(entry.url(), "https://github.com/example/tags");
2917
2918 assert_eq!(
2920 entry.to_string(),
2921 "https://github.com/example/tags (?:.*?/)?v?([\\d.]+)\\.tar\\.gz\n"
2922 );
2923 }
2924
2925 #[test]
2926 fn test_set_matching_pattern_with_all_fields() {
2927 let wf: super::WatchFile = r#"version=4
2929opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2930"#
2931 .parse()
2932 .unwrap();
2933
2934 let mut entry = wf.entries().next().unwrap();
2935 assert_eq!(
2936 entry.matching_pattern(),
2937 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2938 );
2939
2940 entry.set_matching_pattern(".*/version-([\\d.]+)\\.tar\\.xz");
2941 assert_eq!(
2942 entry.matching_pattern(),
2943 Some(".*/version-([\\d.]+)\\.tar\\.xz".into())
2944 );
2945
2946 assert_eq!(entry.url(), "https://example.com/releases");
2948 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2949 assert_eq!(entry.script(), Some("uupdate".into()));
2950 assert_eq!(entry.compression(), Ok(Some(super::Compression::Xz)));
2951
2952 assert_eq!(
2954 entry.to_string(),
2955 "opts=compression=xz https://example.com/releases .*/version-([\\d.]+)\\.tar\\.xz debian uupdate\n"
2956 );
2957 }
2958
2959 #[test]
2960 fn test_set_version_policy() {
2961 let wf: super::WatchFile = r#"version=4
2963https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2964"#
2965 .parse()
2966 .unwrap();
2967
2968 let mut entry = wf.entries().next().unwrap();
2969 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2970
2971 entry.set_version_policy("previous");
2972 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Previous)));
2973
2974 assert_eq!(entry.url(), "https://example.com/releases");
2976 assert_eq!(
2977 entry.matching_pattern(),
2978 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2979 );
2980 assert_eq!(entry.script(), Some("uupdate".into()));
2981
2982 assert_eq!(
2984 entry.to_string(),
2985 "https://example.com/releases (?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz previous uupdate\n"
2986 );
2987 }
2988
2989 #[test]
2990 fn test_set_version_policy_with_options() {
2991 let wf: super::WatchFile = r#"version=4
2993opts=repack,compression=xz \
2994 https://github.com/example/example-cat/tags \
2995 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2996"#
2997 .parse()
2998 .unwrap();
2999
3000 let mut entry = wf.entries().next().unwrap();
3001 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
3002
3003 entry.set_version_policy("ignore");
3004 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Ignore)));
3005
3006 assert_eq!(entry.url(), "https://github.com/example/example-cat/tags");
3008 assert_eq!(
3009 entry.matching_pattern(),
3010 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
3011 );
3012 assert_eq!(entry.script(), Some("uupdate".into()));
3013 assert!(entry.repack());
3014
3015 assert_eq!(
3017 entry.to_string(),
3018 r#"opts=repack,compression=xz \
3019 https://github.com/example/example-cat/tags \
3020 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz ignore uupdate
3021"#
3022 );
3023 }
3024
3025 #[test]
3026 fn test_set_script() {
3027 let wf: super::WatchFile = r#"version=4
3029https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
3030"#
3031 .parse()
3032 .unwrap();
3033
3034 let mut entry = wf.entries().next().unwrap();
3035 assert_eq!(entry.script(), Some("uupdate".into()));
3036
3037 entry.set_script("uscan");
3038 assert_eq!(entry.script(), Some("uscan".into()));
3039
3040 assert_eq!(entry.url(), "https://example.com/releases");
3042 assert_eq!(
3043 entry.matching_pattern(),
3044 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
3045 );
3046 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
3047
3048 assert_eq!(
3050 entry.to_string(),
3051 "https://example.com/releases (?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz debian uscan\n"
3052 );
3053 }
3054
3055 #[test]
3056 fn test_set_script_with_options() {
3057 let wf: super::WatchFile = r#"version=4
3059opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
3060"#
3061 .parse()
3062 .unwrap();
3063
3064 let mut entry = wf.entries().next().unwrap();
3065 assert_eq!(entry.script(), Some("uupdate".into()));
3066
3067 entry.set_script("custom-script.sh");
3068 assert_eq!(entry.script(), Some("custom-script.sh".into()));
3069
3070 assert_eq!(entry.url(), "https://example.com/releases");
3072 assert_eq!(
3073 entry.matching_pattern(),
3074 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
3075 );
3076 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
3077 assert_eq!(entry.compression(), Ok(Some(super::Compression::Xz)));
3078
3079 assert_eq!(
3081 entry.to_string(),
3082 "opts=compression=xz https://example.com/releases (?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz debian custom-script.sh\n"
3083 );
3084 }
3085
3086 #[test]
3087 fn test_apply_dversionmangle() {
3088 let wf: super::WatchFile = r#"version=4
3090opts=dversionmangle=s/\+dfsg$// https://example.com/ .*
3091"#
3092 .parse()
3093 .unwrap();
3094 let entry = wf.entries().next().unwrap();
3095 assert_eq!(entry.apply_dversionmangle("1.0+dfsg").unwrap(), "1.0");
3096 assert_eq!(entry.apply_dversionmangle("1.0").unwrap(), "1.0");
3097
3098 let wf: super::WatchFile = r#"version=4
3100opts=versionmangle=s/^v// https://example.com/ .*
3101"#
3102 .parse()
3103 .unwrap();
3104 let entry = wf.entries().next().unwrap();
3105 assert_eq!(entry.apply_dversionmangle("v1.0").unwrap(), "1.0");
3106
3107 let wf: super::WatchFile = r#"version=4
3109opts=dversionmangle=s/\+ds//,versionmangle=s/^v// https://example.com/ .*
3110"#
3111 .parse()
3112 .unwrap();
3113 let entry = wf.entries().next().unwrap();
3114 assert_eq!(entry.apply_dversionmangle("1.0+ds").unwrap(), "1.0");
3115
3116 let wf: super::WatchFile = r#"version=4
3118https://example.com/ .*
3119"#
3120 .parse()
3121 .unwrap();
3122 let entry = wf.entries().next().unwrap();
3123 assert_eq!(entry.apply_dversionmangle("1.0+dfsg").unwrap(), "1.0+dfsg");
3124 }
3125
3126 #[test]
3127 fn test_apply_oversionmangle() {
3128 let wf: super::WatchFile = r#"version=4
3130opts=oversionmangle=s/$/-1/ https://example.com/ .*
3131"#
3132 .parse()
3133 .unwrap();
3134 let entry = wf.entries().next().unwrap();
3135 assert_eq!(entry.apply_oversionmangle("1.0").unwrap(), "1.0-1");
3136 assert_eq!(entry.apply_oversionmangle("2.5.3").unwrap(), "2.5.3-1");
3137
3138 let wf: super::WatchFile = r#"version=4
3140opts=oversionmangle=s/$/.dfsg/ https://example.com/ .*
3141"#
3142 .parse()
3143 .unwrap();
3144 let entry = wf.entries().next().unwrap();
3145 assert_eq!(entry.apply_oversionmangle("1.0").unwrap(), "1.0.dfsg");
3146
3147 let wf: super::WatchFile = r#"version=4
3149https://example.com/ .*
3150"#
3151 .parse()
3152 .unwrap();
3153 let entry = wf.entries().next().unwrap();
3154 assert_eq!(entry.apply_oversionmangle("1.0").unwrap(), "1.0");
3155 }
3156
3157 #[test]
3158 fn test_apply_dirversionmangle() {
3159 let wf: super::WatchFile = r#"version=4
3161opts=dirversionmangle=s/^v// https://example.com/ .*
3162"#
3163 .parse()
3164 .unwrap();
3165 let entry = wf.entries().next().unwrap();
3166 assert_eq!(entry.apply_dirversionmangle("v1.0").unwrap(), "1.0");
3167 assert_eq!(entry.apply_dirversionmangle("v2.5.3").unwrap(), "2.5.3");
3168
3169 let wf: super::WatchFile = r#"version=4
3171opts=dirversionmangle=s/v(\d)/$1/ https://example.com/ .*
3172"#
3173 .parse()
3174 .unwrap();
3175 let entry = wf.entries().next().unwrap();
3176 assert_eq!(entry.apply_dirversionmangle("v1.0").unwrap(), "1.0");
3177
3178 let wf: super::WatchFile = r#"version=4
3180https://example.com/ .*
3181"#
3182 .parse()
3183 .unwrap();
3184 let entry = wf.entries().next().unwrap();
3185 assert_eq!(entry.apply_dirversionmangle("v1.0").unwrap(), "v1.0");
3186 }
3187
3188 #[test]
3189 fn test_apply_filenamemangle() {
3190 let wf: super::WatchFile = r#"version=4
3192opts=filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/mypackage-$1.tar.gz/ https://example.com/ .*
3193"#
3194 .parse()
3195 .unwrap();
3196 let entry = wf.entries().next().unwrap();
3197 assert_eq!(
3198 entry
3199 .apply_filenamemangle("https://example.com/v1.0.tar.gz")
3200 .unwrap(),
3201 "mypackage-1.0.tar.gz"
3202 );
3203 assert_eq!(
3204 entry
3205 .apply_filenamemangle("https://example.com/2.5.3.tar.gz")
3206 .unwrap(),
3207 "mypackage-2.5.3.tar.gz"
3208 );
3209
3210 let wf: super::WatchFile = r#"version=4
3212opts=filenamemangle=s/.*\/(.*)/$1/ https://example.com/ .*
3213"#
3214 .parse()
3215 .unwrap();
3216 let entry = wf.entries().next().unwrap();
3217 assert_eq!(
3218 entry
3219 .apply_filenamemangle("https://example.com/path/to/file.tar.gz")
3220 .unwrap(),
3221 "file.tar.gz"
3222 );
3223
3224 let wf: super::WatchFile = r#"version=4
3226https://example.com/ .*
3227"#
3228 .parse()
3229 .unwrap();
3230 let entry = wf.entries().next().unwrap();
3231 assert_eq!(
3232 entry
3233 .apply_filenamemangle("https://example.com/file.tar.gz")
3234 .unwrap(),
3235 "https://example.com/file.tar.gz"
3236 );
3237 }
3238
3239 #[test]
3240 fn test_apply_pagemangle() {
3241 let wf: super::WatchFile = r#"version=4
3243opts=pagemangle=s/&/&/g https://example.com/ .*
3244"#
3245 .parse()
3246 .unwrap();
3247 let entry = wf.entries().next().unwrap();
3248 assert_eq!(
3249 entry.apply_pagemangle(b"foo & bar").unwrap(),
3250 b"foo & bar"
3251 );
3252 assert_eq!(
3253 entry
3254 .apply_pagemangle(b"& foo & bar &")
3255 .unwrap(),
3256 b"& foo & bar &"
3257 );
3258
3259 let wf: super::WatchFile = r#"version=4
3261opts=pagemangle=s/<[^>]+>//g https://example.com/ .*
3262"#
3263 .parse()
3264 .unwrap();
3265 let entry = wf.entries().next().unwrap();
3266 assert_eq!(entry.apply_pagemangle(b"<div>text</div>").unwrap(), b"text");
3267
3268 let wf: super::WatchFile = r#"version=4
3270https://example.com/ .*
3271"#
3272 .parse()
3273 .unwrap();
3274 let entry = wf.entries().next().unwrap();
3275 assert_eq!(
3276 entry.apply_pagemangle(b"foo & bar").unwrap(),
3277 b"foo & bar"
3278 );
3279 }
3280
3281 #[test]
3282 fn test_apply_downloadurlmangle() {
3283 let wf: super::WatchFile = r#"version=4
3285opts=downloadurlmangle=s|/archive/|/download/| https://example.com/ .*
3286"#
3287 .parse()
3288 .unwrap();
3289 let entry = wf.entries().next().unwrap();
3290 assert_eq!(
3291 entry
3292 .apply_downloadurlmangle("https://example.com/archive/file.tar.gz")
3293 .unwrap(),
3294 "https://example.com/download/file.tar.gz"
3295 );
3296
3297 let wf: super::WatchFile = r#"version=4
3299opts=downloadurlmangle=s/github\.com/raw.githubusercontent.com/ https://example.com/ .*
3300"#
3301 .parse()
3302 .unwrap();
3303 let entry = wf.entries().next().unwrap();
3304 assert_eq!(
3305 entry
3306 .apply_downloadurlmangle("https://github.com/user/repo/file.tar.gz")
3307 .unwrap(),
3308 "https://raw.githubusercontent.com/user/repo/file.tar.gz"
3309 );
3310
3311 let wf: super::WatchFile = r#"version=4
3313https://example.com/ .*
3314"#
3315 .parse()
3316 .unwrap();
3317 let entry = wf.entries().next().unwrap();
3318 assert_eq!(
3319 entry
3320 .apply_downloadurlmangle("https://example.com/archive/file.tar.gz")
3321 .unwrap(),
3322 "https://example.com/archive/file.tar.gz"
3323 );
3324 }
3325
3326 #[test]
3327 fn test_entry_builder_minimal() {
3328 let entry = super::EntryBuilder::new("https://github.com/example/tags")
3330 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3331 .build();
3332
3333 assert_eq!(entry.url(), "https://github.com/example/tags");
3334 assert_eq!(
3335 entry.matching_pattern().as_deref(),
3336 Some(".*/v?(\\d\\S+)\\.tar\\.gz")
3337 );
3338 assert_eq!(entry.version(), Ok(None));
3339 assert_eq!(entry.script(), None);
3340 assert!(entry.opts().is_empty());
3341 }
3342
3343 #[test]
3344 fn test_entry_builder_url_only() {
3345 let entry = super::EntryBuilder::new("https://example.com/releases").build();
3347
3348 assert_eq!(entry.url(), "https://example.com/releases");
3349 assert_eq!(entry.matching_pattern(), None);
3350 assert_eq!(entry.version(), Ok(None));
3351 assert_eq!(entry.script(), None);
3352 assert!(entry.opts().is_empty());
3353 }
3354
3355 #[test]
3356 fn test_entry_builder_with_all_fields() {
3357 let entry = super::EntryBuilder::new("https://github.com/example/tags")
3359 .matching_pattern(".*/v?(\\d[\\d.]*)\\.tar\\.gz")
3360 .version_policy("debian")
3361 .script("uupdate")
3362 .opt("compression", "xz")
3363 .flag("repack")
3364 .build();
3365
3366 assert_eq!(entry.url(), "https://github.com/example/tags");
3367 assert_eq!(
3368 entry.matching_pattern().as_deref(),
3369 Some(".*/v?(\\d[\\d.]*)\\.tar\\.gz")
3370 );
3371 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
3372 assert_eq!(entry.script(), Some("uupdate".into()));
3373 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
3374 assert!(entry.has_option("repack"));
3375 assert!(entry.repack());
3376 }
3377
3378 #[test]
3379 fn test_entry_builder_multiple_options() {
3380 let entry = super::EntryBuilder::new("https://example.com/tags")
3382 .matching_pattern(".*/v?(\\d+\\.\\d+)\\.tar\\.gz")
3383 .opt("compression", "xz")
3384 .opt("dversionmangle", "s/\\+ds//")
3385 .opt("repacksuffix", "+ds")
3386 .build();
3387
3388 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
3389 assert_eq!(
3390 entry.get_option("dversionmangle"),
3391 Some("s/\\+ds//".to_string())
3392 );
3393 assert_eq!(entry.get_option("repacksuffix"), Some("+ds".to_string()));
3394 }
3395
3396 #[test]
3397 fn test_entry_builder_via_entry() {
3398 let entry = super::Entry::builder("https://github.com/example/tags")
3400 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3401 .version_policy("debian")
3402 .build();
3403
3404 assert_eq!(entry.url(), "https://github.com/example/tags");
3405 assert_eq!(
3406 entry.matching_pattern().as_deref(),
3407 Some(".*/v?(\\d\\S+)\\.tar\\.gz")
3408 );
3409 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
3410 }
3411
3412 #[test]
3413 fn test_watchfile_add_entry_to_empty() {
3414 let mut wf = super::WatchFile::new(Some(4));
3416
3417 let entry = super::EntryBuilder::new("https://github.com/example/tags")
3418 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3419 .build();
3420
3421 wf.add_entry(entry);
3422
3423 assert_eq!(wf.version(), 4);
3424 assert_eq!(wf.entries().count(), 1);
3425
3426 let added_entry = wf.entries().next().unwrap();
3427 assert_eq!(added_entry.url(), "https://github.com/example/tags");
3428 assert_eq!(
3429 added_entry.matching_pattern().as_deref(),
3430 Some(".*/v?(\\d\\S+)\\.tar\\.gz")
3431 );
3432 }
3433
3434 #[test]
3435 fn test_watchfile_add_multiple_entries() {
3436 let mut wf = super::WatchFile::new(Some(4));
3438
3439 wf.add_entry(
3440 super::EntryBuilder::new("https://github.com/example1/tags")
3441 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3442 .build(),
3443 );
3444
3445 wf.add_entry(
3446 super::EntryBuilder::new("https://github.com/example2/releases")
3447 .matching_pattern(".*/(\\d+\\.\\d+)\\.tar\\.gz")
3448 .opt("compression", "xz")
3449 .build(),
3450 );
3451
3452 assert_eq!(wf.entries().count(), 2);
3453
3454 let entries: Vec<_> = wf.entries().collect();
3455 assert_eq!(entries[0].url(), "https://github.com/example1/tags");
3456 assert_eq!(entries[1].url(), "https://github.com/example2/releases");
3457 assert_eq!(entries[1].get_option("compression"), Some("xz".to_string()));
3458 }
3459
3460 #[test]
3461 fn test_watchfile_add_entry_to_existing() {
3462 let mut wf: super::WatchFile = r#"version=4
3464https://example.com/old .*/v?(\\d\\S+)\\.tar\\.gz
3465"#
3466 .parse()
3467 .unwrap();
3468
3469 assert_eq!(wf.entries().count(), 1);
3470
3471 wf.add_entry(
3472 super::EntryBuilder::new("https://github.com/example/new")
3473 .matching_pattern(".*/v?(\\d+\\.\\d+)\\.tar\\.gz")
3474 .opt("compression", "xz")
3475 .version_policy("debian")
3476 .build(),
3477 );
3478
3479 assert_eq!(wf.entries().count(), 2);
3480
3481 let entries: Vec<_> = wf.entries().collect();
3482 assert_eq!(entries[0].url(), "https://example.com/old");
3483 assert_eq!(entries[1].url(), "https://github.com/example/new");
3484 assert_eq!(entries[1].version(), Ok(Some(VersionPolicy::Debian)));
3485 }
3486
3487 #[test]
3488 fn test_entry_builder_formatting() {
3489 let entry = super::EntryBuilder::new("https://github.com/example/tags")
3491 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3492 .opt("compression", "xz")
3493 .flag("repack")
3494 .version_policy("debian")
3495 .script("uupdate")
3496 .build();
3497
3498 let entry_str = entry.to_string();
3499
3500 assert!(entry_str.starts_with("opts="));
3502 assert!(entry_str.contains("https://github.com/example/tags"));
3504 assert!(entry_str.contains(".*/v?(\\d\\S+)\\.tar\\.gz"));
3506 assert!(entry_str.contains("debian"));
3508 assert!(entry_str.contains("uupdate"));
3510 assert!(entry_str.ends_with('\n'));
3512 }
3513
3514 #[test]
3515 fn test_watchfile_add_entry_preserves_format() {
3516 let mut wf = super::WatchFile::new(Some(4));
3518
3519 wf.add_entry(
3520 super::EntryBuilder::new("https://github.com/example/tags")
3521 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3522 .build(),
3523 );
3524
3525 let wf_str = wf.to_string();
3526
3527 assert!(wf_str.starts_with("version=4\n"));
3529 assert!(wf_str.contains("https://github.com/example/tags"));
3531
3532 let reparsed: super::WatchFile = wf_str.parse().unwrap();
3534 assert_eq!(reparsed.version(), 4);
3535 assert_eq!(reparsed.entries().count(), 1);
3536 }
3537
3538 #[test]
3539 fn test_line_col() {
3540 let text = r#"version=4
3541opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
3542"#;
3543 let wf = text.parse::<super::WatchFile>().unwrap();
3544
3545 let version_node = wf.version_node().unwrap();
3547 assert_eq!(version_node.line(), 0);
3548 assert_eq!(version_node.column(), 0);
3549 assert_eq!(version_node.line_col(), (0, 0));
3550
3551 let entries: Vec<_> = wf.entries().collect();
3553 assert_eq!(entries.len(), 1);
3554
3555 assert_eq!(entries[0].line(), 1);
3557 assert_eq!(entries[0].column(), 0);
3558 assert_eq!(entries[0].line_col(), (1, 0));
3559
3560 let option_list = entries[0].option_list().unwrap();
3562 assert_eq!(option_list.line(), 1); let url_node = entries[0].url_node().unwrap();
3565 assert_eq!(url_node.line(), 1); let pattern_node = entries[0].matching_pattern_node().unwrap();
3568 assert_eq!(pattern_node.line(), 1); let version_policy_node = entries[0].version_node().unwrap();
3571 assert_eq!(version_policy_node.line(), 1); let script_node = entries[0].script_node().unwrap();
3574 assert_eq!(script_node.line(), 1); let options: Vec<_> = option_list.options().collect();
3578 assert_eq!(options.len(), 1);
3579 assert_eq!(options[0].key(), Some("compression".to_string()));
3580 assert_eq!(options[0].value(), Some("xz".to_string()));
3581 assert_eq!(options[0].line(), 1); let compression_opt = option_list.find_option("compression").unwrap();
3585 assert_eq!(compression_opt.line(), 1);
3586 assert_eq!(compression_opt.column(), 5); assert_eq!(compression_opt.line_col(), (1, 5));
3588 }
3589
3590 #[test]
3591 fn test_parse_str_relaxed() {
3592 let wf: super::WatchFile = super::WatchFile::from_str_relaxed(
3593 r#"version=4
3594ERRORS IN THIS LINE
3595opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d
3596"#,
3597 );
3598 assert_eq!(wf.version(), 4);
3599 assert_eq!(wf.entries().count(), 2);
3600
3601 let entries = wf.entries().collect::<Vec<_>>();
3602
3603 let entry = &entries[0];
3604 assert_eq!(entry.url(), "ERRORS");
3605
3606 let entry = &entries[1];
3607 assert_eq!(entry.url(), "https://example.com/releases");
3608 assert_eq!(entry.matching_pattern().as_deref(), Some("(?:.*?/)?v?(\\d"));
3609 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
3610 }
3611
3612 #[test]
3613 fn test_parse_entry_with_comment_before() {
3614 let input = concat!(
3618 "version=4\n",
3619 "# try also https://pypi.debian.net/tomoscan/watch\n",
3620 "opts=uversionmangle=s/(rc|a|b|c)/~$1/;s/\\.dev/~dev/ \\\n",
3621 "https://pypi.debian.net/tomoscan/tomoscan-(.+)\\.(?:zip|tgz|tbz|txz|(?:tar\\.(?:gz|bz2|xz)))\n"
3622 );
3623 let wf: super::WatchFile = input.parse().unwrap();
3624 assert_eq!(wf.to_string(), input);
3626 assert_eq!(wf.entries().count(), 1);
3627 let entry = wf.entries().next().unwrap();
3628 assert_eq!(
3629 entry.url(),
3630 "https://pypi.debian.net/tomoscan/tomoscan-(.+)\\.(?:zip|tgz|tbz|txz|(?:tar\\.(?:gz|bz2|xz)))"
3631 );
3632 assert_eq!(
3633 entry.get_option("uversionmangle"),
3634 Some("s/(rc|a|b|c)/~$1/;s/\\.dev/~dev/".to_string())
3635 );
3636 }
3637
3638 #[test]
3639 fn test_parse_multiple_comments_before_entry() {
3640 let input = concat!(
3643 "version=4\n",
3644 "# first comment\n",
3645 "# second comment\n",
3646 "# third comment\n",
3647 "https://example.com/foo foo-(.*).tar.gz\n",
3648 );
3649 let wf: super::WatchFile = input.parse().unwrap();
3650 assert_eq!(wf.to_string(), input);
3651 assert_eq!(wf.entries().count(), 1);
3652 assert_eq!(
3653 wf.entries().next().unwrap().url(),
3654 "https://example.com/foo"
3655 );
3656 }
3657
3658 #[test]
3659 fn test_parse_blank_lines_between_entries() {
3660 let input = concat!(
3662 "version=4\n",
3663 "https://example.com/foo .*/foo-(\\d+)\\.tar\\.gz\n",
3664 "\n",
3665 "https://example.com/bar .*/bar-(\\d+)\\.tar\\.gz\n",
3666 );
3667 let wf: super::WatchFile = input.parse().unwrap();
3668 assert_eq!(wf.to_string(), input);
3669 assert_eq!(wf.entries().count(), 2);
3670 }
3671
3672 #[test]
3673 fn test_parse_trailing_unparseable_tokens_produce_error() {
3674 let input = "version=4\nhttps://example.com/foo foo-(.*).tar.gz\n=garbage\n";
3677 let result = input.parse::<super::WatchFile>();
3678 assert!(result.is_err(), "expected parse error for trailing garbage");
3679 let wf = super::WatchFile::from_str_relaxed(input);
3681 assert_eq!(wf.to_string(), input);
3682 }
3683
3684 #[test]
3685 fn test_parse_roundtrip_full_file() {
3686 let inputs = [
3688 "version=4\nhttps://example.com/foo foo-(.*).tar.gz\n",
3689 "version=4\n# a comment\nhttps://example.com/foo foo-(.*).tar.gz\n",
3690 concat!(
3691 "version=4\n",
3692 "opts=uversionmangle=s/rc/~rc/ \\\n",
3693 " https://example.com/foo foo-(.*).tar.gz\n",
3694 ),
3695 concat!(
3696 "version=4\n",
3697 "# comment before entry\n",
3698 "opts=uversionmangle=s/rc/~rc/ \\\n",
3699 "https://example.com/foo foo-(.*).tar.gz\n",
3700 "# comment between entries\n",
3701 "https://example.com/bar bar-(.*).tar.gz\n",
3702 ),
3703 ];
3704 for input in &inputs {
3705 let wf: super::WatchFile = input.parse().unwrap();
3706 assert_eq!(
3707 wf.to_string(),
3708 *input,
3709 "round-trip failed for input: {:?}",
3710 input
3711 );
3712 }
3713 }
3714
3715 #[test]
3716 fn test_parse_url_with_equals_in_query_string() {
3717 let input = concat!(
3720 "version=4\n",
3721 "https://api.github.com/repos/x/releases?per_page=100 \\\n",
3722 " https://github.com/x/v[^/]+/x.tar.gz\n",
3723 );
3724 let wf: super::WatchFile = input.parse().unwrap();
3725 let entries: Vec<_> = wf.entries().collect();
3726 assert_eq!(entries.len(), 1);
3727 assert_eq!(
3728 entries[0].url(),
3729 "https://api.github.com/repos/x/releases?per_page=100"
3730 );
3731 assert_eq!(
3732 entries[0].matching_pattern().as_deref(),
3733 Some("https://github.com/x/v[^/]+/x.tar.gz"),
3734 );
3735 assert_eq!(wf.to_string(), input);
3736 }
3737
3738 #[test]
3739 fn test_entry_url_does_not_panic_when_empty() {
3740 let input = "version=4\n=garbage\n";
3743 let wf = super::WatchFile::from_str_relaxed(input);
3744 for entry in wf.entries() {
3745 let _ = entry.url();
3746 }
3747 }
3748
3749 #[test]
3750 fn test_parse_url_node_with_equals_join_tokens() {
3751 let input = "version=4\nhttps://example.com/x?y=1&z=2 .*tar.gz\n";
3754 let wf: super::WatchFile = input.parse().unwrap();
3755 let entry = wf.entries().next().unwrap();
3756 assert_eq!(entry.url(), "https://example.com/x?y=1&z=2");
3757 }
3758
3759 #[test]
3760 fn test_parse_quoted_opts_with_trailing_comma_continuation() {
3761 let input = concat!(
3767 "version=4\n\n",
3768 "opts=\"\\\n",
3769 "pgpmode=none,\\\n",
3770 "repack,compression=xz,repacksuffix=+dfsg,\\\n",
3771 "dversionmangle=s{[+~]dfsg\\d*}{},\\\n",
3772 "\" https://github.com/varlink/go/releases \\\n",
3773 " .*/archive/v?(\\d[\\d\\.]+)\\.tar\\.gz\n",
3774 );
3775 let wf: super::WatchFile = input.parse().unwrap();
3776 let entries: Vec<_> = wf.entries().collect();
3777 assert_eq!(entries.len(), 1);
3778 assert_eq!(entries[0].url(), "https://github.com/varlink/go/releases");
3779 assert_eq!(
3780 entries[0].matching_pattern().as_deref(),
3781 Some(".*/archive/v?(\\d[\\d\\.]+)\\.tar\\.gz"),
3782 );
3783 assert_eq!(wf.to_string(), input);
3784 }
3785
3786 #[test]
3787 fn test_parse_quoted_opts_with_spaces_around_comma() {
3788 let input = concat!(
3791 "version=4\n",
3792 "opts=\"filenamemangle=s/.+\\/v?(\\d\\S*)\\.tar\\.gz/v$1.tar.gz/ , uversionmangle=tr%-rc%~rc%\" \\\n",
3793 " https://github.com/analogdevicesinc/libiio/tags .*/v(\\d\\S*)\\.tar\\.gz\n",
3794 );
3795 let wf: super::WatchFile = input.parse().unwrap();
3796 let entries: Vec<_> = wf.entries().collect();
3797 assert_eq!(entries.len(), 1);
3798 assert_eq!(
3799 entries[0].url(),
3800 "https://github.com/analogdevicesinc/libiio/tags",
3801 );
3802 assert_eq!(wf.to_string(), input);
3803 }
3804
3805 #[test]
3806 fn test_parse_unquoted_opts_trailing_comma_then_url() {
3807 let input = concat!(
3811 "version=3\n",
3812 "opts=uversionmangle=s/(rc|a|b|c)/~$1/,\\\n",
3813 "https://github.com/openstack/rally/tags .*/(\\d\\S*)\\.tar\\.gz\n",
3814 );
3815 let wf: super::WatchFile = input.parse().unwrap();
3816 let entries: Vec<_> = wf.entries().collect();
3817 assert_eq!(entries.len(), 1);
3818 assert_eq!(entries[0].url(), "https://github.com/openstack/rally/tags");
3819 assert_eq!(
3820 entries[0].matching_pattern().as_deref(),
3821 Some(".*/(\\d\\S*)\\.tar\\.gz"),
3822 );
3823 assert_eq!(wf.to_string(), input);
3824 }
3825
3826 #[test]
3827 fn test_parse_unquoted_opts_value_with_equals() {
3828 let input = concat!(
3832 "version=4\n",
3833 "opts=dversionmangle=s/\\~dfsg//,downloadurlmangle=s/.*ref=//,pgpsigurlmangle=s/$/.asc/ \\\n",
3834 "\thttps://downloads.asterisk.org/pub/telephony/libpri/releases/ libpri-([0-9.]*)\\.tar\\.gz debian uupdate\n",
3835 );
3836 let wf: super::WatchFile = input.parse().unwrap();
3837 let entries: Vec<_> = wf.entries().collect();
3838 assert_eq!(entries.len(), 1);
3839 assert_eq!(
3840 entries[0].url(),
3841 "https://downloads.asterisk.org/pub/telephony/libpri/releases/"
3842 );
3843 assert_eq!(
3844 entries[0].matching_pattern().as_deref(),
3845 Some("libpri-([0-9.]*)\\.tar\\.gz"),
3846 );
3847 assert_eq!(wf.to_string(), input);
3848 }
3849}