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