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
15#[derive(Debug, Clone, PartialEq, Eq, Hash)]
16pub struct ParseError(pub Vec<String>);
18
19impl std::fmt::Display for ParseError {
20 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
21 for err in &self.0 {
22 writeln!(f, "{}", err)?;
23 }
24 Ok(())
25 }
26}
27
28impl std::error::Error for ParseError {}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
34pub(crate) enum Lang {}
35impl rowan::Language for Lang {
36 type Kind = SyntaxKind;
37 fn kind_from_raw(raw: rowan::SyntaxKind) -> Self::Kind {
38 unsafe { std::mem::transmute::<u16, SyntaxKind>(raw.0) }
39 }
40 fn kind_to_raw(kind: Self::Kind) -> rowan::SyntaxKind {
41 kind.into()
42 }
43}
44
45use rowan::GreenNode;
48
49use rowan::GreenNodeBuilder;
53
54#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct Parse<T> {
58 green: GreenNode,
60 errors: Vec<String>,
62 _ty: PhantomData<T>,
64}
65
66impl<T> Parse<T> {
67 pub(crate) fn new(green: GreenNode, errors: Vec<String>) -> Self {
69 Parse {
70 green,
71 errors,
72 _ty: PhantomData,
73 }
74 }
75
76 pub fn green(&self) -> &GreenNode {
78 &self.green
79 }
80
81 pub fn errors(&self) -> &[String] {
83 &self.errors
84 }
85
86 pub fn is_ok(&self) -> bool {
88 self.errors.is_empty()
89 }
90}
91
92impl Parse<WatchFile> {
93 pub fn tree(&self) -> WatchFile {
95 WatchFile::cast(SyntaxNode::new_root_mut(self.green.clone()))
96 .expect("root node should be a WatchFile")
97 }
98}
99
100struct InternalParse {
102 green_node: GreenNode,
103 errors: Vec<String>,
104}
105
106fn parse(text: &str) -> InternalParse {
107 struct Parser {
108 tokens: Vec<(SyntaxKind, String)>,
111 builder: GreenNodeBuilder<'static>,
113 errors: Vec<String>,
116 }
117
118 impl Parser {
119 fn parse_version(&mut self) -> Option<u32> {
120 let mut version = None;
121 if self.tokens.last() == Some(&(KEY, "version".to_string())) {
122 self.builder.start_node(VERSION.into());
123 self.bump();
124 self.skip_ws();
125 if self.current() != Some(EQUALS) {
126 self.builder.start_node(ERROR.into());
127 self.errors.push("expected `=`".to_string());
128 self.bump();
129 self.builder.finish_node();
130 } else {
131 self.bump();
132 }
133 if self.current() != Some(VALUE) {
134 self.builder.start_node(ERROR.into());
135 self.errors
136 .push(format!("expected value, got {:?}", self.current()));
137 self.bump();
138 self.builder.finish_node();
139 } else if let Some((_, value)) = self.tokens.last() {
140 let version_str = value;
141 match version_str.parse() {
142 Ok(v) => {
143 version = Some(v);
144 self.bump();
145 }
146 Err(_) => {
147 self.builder.start_node(ERROR.into());
148 self.errors
149 .push(format!("invalid version: {}", version_str));
150 self.bump();
151 self.builder.finish_node();
152 }
153 }
154 } else {
155 self.builder.start_node(ERROR.into());
156 self.errors.push("expected version value".to_string());
157 self.builder.finish_node();
158 }
159 if self.current() != Some(NEWLINE) {
160 self.builder.start_node(ERROR.into());
161 self.errors.push("expected newline".to_string());
162 self.bump();
163 self.builder.finish_node();
164 } else {
165 self.bump();
166 }
167 self.builder.finish_node();
168 }
169 version
170 }
171
172 fn parse_watch_entry(&mut self) -> bool {
173 self.skip_ws();
174 if self.current().is_none() {
175 return false;
176 }
177 if self.current() == Some(NEWLINE) {
178 self.bump();
179 return false;
180 }
181 self.builder.start_node(ENTRY.into());
182 self.parse_options_list();
183 for i in 0..4 {
184 if self.current() == Some(NEWLINE) || self.current().is_none() {
185 break;
186 }
187 if self.current() == Some(CONTINUATION) {
188 self.bump();
189 self.skip_ws();
190 continue;
191 }
192 if self.current() != Some(VALUE) && self.current() != Some(KEY) {
193 self.builder.start_node(ERROR.into());
194 self.errors.push(format!(
195 "expected value, got {:?} (i={})",
196 self.current(),
197 i
198 ));
199 if self.current().is_some() {
200 self.bump();
201 }
202 self.builder.finish_node();
203 } else {
204 match i {
206 0 => {
207 self.builder.start_node(URL.into());
209 self.bump();
210 self.builder.finish_node();
211 }
212 1 => {
213 self.builder.start_node(MATCHING_PATTERN.into());
215 self.bump();
216 self.builder.finish_node();
217 }
218 2 => {
219 self.builder.start_node(VERSION_POLICY.into());
221 self.bump();
222 self.builder.finish_node();
223 }
224 3 => {
225 self.builder.start_node(SCRIPT.into());
227 self.bump();
228 self.builder.finish_node();
229 }
230 _ => {
231 self.bump();
232 }
233 }
234 }
235 self.skip_ws();
236 }
237 if self.current() != Some(NEWLINE) && self.current().is_some() {
238 self.builder.start_node(ERROR.into());
239 self.errors
240 .push(format!("expected newline, not {:?}", self.current()));
241 if self.current().is_some() {
242 self.bump();
243 }
244 self.builder.finish_node();
245 } else if self.current().is_some() {
246 self.bump();
248 }
249 self.builder.finish_node();
250 true
251 }
252
253 fn parse_option(&mut self) -> bool {
254 if self.current().is_none() {
255 return false;
256 }
257 while self.current() == Some(CONTINUATION) {
258 self.bump();
259 }
260 if self.current() == Some(WHITESPACE) {
261 return false;
262 }
263 self.builder.start_node(OPTION.into());
264 if self.current() != Some(KEY) {
265 self.builder.start_node(ERROR.into());
266 self.errors.push("expected key".to_string());
267 self.bump();
268 self.builder.finish_node();
269 } else {
270 self.bump();
271 }
272 if self.current() == Some(EQUALS) {
273 self.bump();
274 if self.current() != Some(VALUE) && self.current() != Some(KEY) {
275 self.builder.start_node(ERROR.into());
276 self.errors
277 .push(format!("expected value, got {:?}", self.current()));
278 self.bump();
279 self.builder.finish_node();
280 } else {
281 self.bump();
282 }
283 } else if self.current() == Some(COMMA) {
284 } else {
285 self.builder.start_node(ERROR.into());
286 self.errors.push("expected `=`".to_string());
287 if self.current().is_some() {
288 self.bump();
289 }
290 self.builder.finish_node();
291 }
292 self.builder.finish_node();
293 true
294 }
295
296 fn parse_options_list(&mut self) {
297 self.skip_ws();
298 if self.tokens.last() == Some(&(KEY, "opts".to_string()))
299 || self.tokens.last() == Some(&(KEY, "options".to_string()))
300 {
301 self.builder.start_node(OPTS_LIST.into());
302 self.bump();
303 self.skip_ws();
304 if self.current() != Some(EQUALS) {
305 self.builder.start_node(ERROR.into());
306 self.errors.push("expected `=`".to_string());
307 if self.current().is_some() {
308 self.bump();
309 }
310 self.builder.finish_node();
311 } else {
312 self.bump();
313 }
314 let quoted = if self.current() == Some(QUOTE) {
315 self.bump();
316 true
317 } else {
318 false
319 };
320 loop {
321 if quoted {
322 if self.current() == Some(QUOTE) {
323 self.bump();
324 break;
325 }
326 self.skip_ws();
327 }
328 if !self.parse_option() {
329 break;
330 }
331 if self.current() == Some(COMMA) {
332 self.builder.start_node(OPTION_SEPARATOR.into());
333 self.bump();
334 self.builder.finish_node();
335 } else if !quoted {
336 break;
337 }
338 }
339 self.builder.finish_node();
340 self.skip_ws();
341 }
342 }
343
344 fn parse(mut self) -> InternalParse {
345 self.builder.start_node(ROOT.into());
347 while self.current() == Some(WHITESPACE)
349 || self.current() == Some(CONTINUATION)
350 || self.current() == Some(COMMENT)
351 || self.current() == Some(NEWLINE)
352 {
353 self.bump();
354 }
355 if let Some(_v) = self.parse_version() {
356 }
358 loop {
360 if !self.parse_watch_entry() {
361 break;
362 }
363 }
364 self.skip_ws();
366 self.builder.finish_node();
368
369 InternalParse {
371 green_node: self.builder.finish(),
372 errors: self.errors,
373 }
374 }
375 fn bump(&mut self) {
377 if let Some((kind, text)) = self.tokens.pop() {
378 self.builder.token(kind.into(), text.as_str());
379 }
380 }
381 fn current(&self) -> Option<SyntaxKind> {
383 self.tokens.last().map(|(kind, _)| *kind)
384 }
385 fn skip_ws(&mut self) {
386 while self.current() == Some(WHITESPACE)
387 || self.current() == Some(CONTINUATION)
388 || self.current() == Some(COMMENT)
389 {
390 self.bump()
391 }
392 }
393 }
394
395 let mut tokens = lex(text);
396 tokens.reverse();
397 Parser {
398 tokens,
399 builder: GreenNodeBuilder::new(),
400 errors: Vec::new(),
401 }
402 .parse()
403}
404
405type SyntaxNode = rowan::SyntaxNode<Lang>;
411#[allow(unused)]
412type SyntaxToken = rowan::SyntaxToken<Lang>;
413#[allow(unused)]
414type SyntaxElement = rowan::NodeOrToken<SyntaxNode, SyntaxToken>;
415
416impl InternalParse {
417 fn syntax(&self) -> SyntaxNode {
418 SyntaxNode::new_root_mut(self.green_node.clone())
419 }
420
421 fn root(&self) -> WatchFile {
422 WatchFile::cast(self.syntax()).expect("root node should be a WatchFile")
423 }
424}
425
426fn line_col_at_offset(node: &SyntaxNode, offset: rowan::TextSize) -> (usize, usize) {
429 let root = node.ancestors().last().unwrap_or_else(|| node.clone());
430 let mut line = 0;
431 let mut last_newline_offset = rowan::TextSize::from(0);
432
433 for element in root.preorder_with_tokens() {
434 if let rowan::WalkEvent::Enter(rowan::NodeOrToken::Token(token)) = element {
435 if token.text_range().start() >= offset {
436 break;
437 }
438
439 for (idx, _) in token.text().match_indices('\n') {
441 line += 1;
442 last_newline_offset =
443 token.text_range().start() + rowan::TextSize::from((idx + 1) as u32);
444 }
445 }
446 }
447
448 let column: usize = (offset - last_newline_offset).into();
449 (line, column)
450}
451
452macro_rules! ast_node {
453 ($ast:ident, $kind:ident) => {
454 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
455 #[repr(transparent)]
456 pub struct $ast(SyntaxNode);
458 impl $ast {
459 #[allow(unused)]
460 fn cast(node: SyntaxNode) -> Option<Self> {
461 if node.kind() == $kind {
462 Some(Self(node))
463 } else {
464 None
465 }
466 }
467
468 pub fn line(&self) -> usize {
470 line_col_at_offset(&self.0, self.0.text_range().start()).0
471 }
472
473 pub fn column(&self) -> usize {
475 line_col_at_offset(&self.0, self.0.text_range().start()).1
476 }
477
478 pub fn line_col(&self) -> (usize, usize) {
481 line_col_at_offset(&self.0, self.0.text_range().start())
482 }
483 }
484
485 impl std::fmt::Display for $ast {
486 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
487 write!(f, "{}", self.0.text())
488 }
489 }
490 };
491}
492
493ast_node!(WatchFile, ROOT);
494ast_node!(Version, VERSION);
495ast_node!(Entry, ENTRY);
496ast_node!(_Option, OPTION);
497ast_node!(Url, URL);
498ast_node!(MatchingPattern, MATCHING_PATTERN);
499ast_node!(VersionPolicyNode, VERSION_POLICY);
500ast_node!(ScriptNode, SCRIPT);
501
502#[derive(Clone, PartialEq, Eq, Hash)]
504#[repr(transparent)]
505pub struct OptionList(SyntaxNode);
507
508impl OptionList {
509 #[allow(unused)]
510 fn cast(node: SyntaxNode) -> Option<Self> {
511 if node.kind() == OPTS_LIST {
512 Some(Self(node))
513 } else {
514 None
515 }
516 }
517
518 pub fn line(&self) -> usize {
520 line_col_at_offset(&self.0, self.0.text_range().start()).0
521 }
522
523 pub fn column(&self) -> usize {
525 line_col_at_offset(&self.0, self.0.text_range().start()).1
526 }
527
528 pub fn line_col(&self) -> (usize, usize) {
531 line_col_at_offset(&self.0, self.0.text_range().start())
532 }
533}
534
535impl std::fmt::Display for OptionList {
536 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
537 write!(f, "{}", self.0.text())
538 }
539}
540
541impl WatchFile {
542 #[cfg(feature = "deb822")]
544 pub(crate) fn syntax(&self) -> &SyntaxNode {
545 &self.0
546 }
547
548 pub fn new(version: Option<u32>) -> WatchFile {
550 let mut builder = GreenNodeBuilder::new();
551
552 builder.start_node(ROOT.into());
553 if let Some(version) = version {
554 builder.start_node(VERSION.into());
555 builder.token(KEY.into(), "version");
556 builder.token(EQUALS.into(), "=");
557 builder.token(VALUE.into(), version.to_string().as_str());
558 builder.token(NEWLINE.into(), "\n");
559 builder.finish_node();
560 }
561 builder.finish_node();
562 WatchFile(SyntaxNode::new_root_mut(builder.finish()))
563 }
564
565 pub fn version_node(&self) -> Option<Version> {
567 self.0.children().find_map(Version::cast)
568 }
569
570 pub fn version(&self) -> u32 {
572 self.version_node()
573 .map(|it| it.version())
574 .unwrap_or(DEFAULT_VERSION)
575 }
576
577 pub fn entries(&self) -> impl Iterator<Item = Entry> + '_ {
579 self.0.children().filter_map(Entry::cast)
580 }
581
582 pub fn set_version(&mut self, new_version: u32) {
584 let mut builder = GreenNodeBuilder::new();
586 builder.start_node(VERSION.into());
587 builder.token(KEY.into(), "version");
588 builder.token(EQUALS.into(), "=");
589 builder.token(VALUE.into(), new_version.to_string().as_str());
590 builder.token(NEWLINE.into(), "\n");
591 builder.finish_node();
592 let new_version_green = builder.finish();
593
594 let new_version_node = SyntaxNode::new_root_mut(new_version_green);
596
597 let version_pos = self.0.children().position(|child| child.kind() == VERSION);
599
600 if let Some(pos) = version_pos {
601 self.0
603 .splice_children(pos..pos + 1, vec![new_version_node.into()]);
604 } else {
605 self.0.splice_children(0..0, vec![new_version_node.into()]);
607 }
608 }
609
610 #[cfg(feature = "discover")]
630 pub async fn uscan(
631 &self,
632 package: impl Fn() -> String + Send + Sync,
633 ) -> Result<Vec<Vec<crate::Release>>, Box<dyn std::error::Error>> {
634 let mut all_releases = Vec::new();
635
636 for entry in self.entries() {
637 let parsed_entry = crate::parse::ParsedEntry::LineBased(entry);
638 let releases = parsed_entry.discover(|| package()).await?;
639 all_releases.push(releases);
640 }
641
642 Ok(all_releases)
643 }
644
645 #[cfg(all(feature = "discover", feature = "blocking"))]
663 pub fn uscan_blocking(
664 &self,
665 package: impl Fn() -> String,
666 ) -> Result<Vec<Vec<crate::Release>>, Box<dyn std::error::Error>> {
667 let mut all_releases = Vec::new();
668
669 for entry in self.entries() {
670 let parsed_entry = crate::parse::ParsedEntry::LineBased(entry);
671 let releases = parsed_entry.discover_blocking(|| package())?;
672 all_releases.push(releases);
673 }
674
675 Ok(all_releases)
676 }
677
678 pub fn add_entry(&mut self, entry: Entry) -> Entry {
705 let insert_pos = self.0.children_with_tokens().count();
707
708 let entry_green = entry.0.green().into_owned();
710 let new_entry_node = SyntaxNode::new_root_mut(entry_green);
711
712 self.0
714 .splice_children(insert_pos..insert_pos, vec![new_entry_node.into()]);
715
716 Entry::cast(
718 self.0
719 .children()
720 .nth(insert_pos)
721 .expect("Entry was just inserted"),
722 )
723 .expect("Inserted node should be an Entry")
724 }
725
726 pub fn from_reader<R: std::io::Read>(reader: R) -> Result<WatchFile, ParseError> {
728 let mut buf_reader = std::io::BufReader::new(reader);
729 let mut content = String::new();
730 buf_reader
731 .read_to_string(&mut content)
732 .map_err(|e| ParseError(vec![e.to_string()]))?;
733 content.parse()
734 }
735
736 pub fn from_reader_relaxed<R: std::io::Read>(mut r: R) -> Result<Self, std::io::Error> {
738 let mut content = String::new();
739 r.read_to_string(&mut content)?;
740 let parsed = parse(&content);
741 Ok(parsed.root())
742 }
743
744 pub fn from_str_relaxed(s: &str) -> Self {
746 let parsed = parse(s);
747 parsed.root()
748 }
749}
750
751impl FromStr for WatchFile {
752 type Err = ParseError;
753
754 fn from_str(s: &str) -> Result<Self, Self::Err> {
755 let parsed = parse(s);
756 if parsed.errors.is_empty() {
757 Ok(parsed.root())
758 } else {
759 Err(ParseError(parsed.errors))
760 }
761 }
762}
763
764pub fn parse_watch_file(text: &str) -> Parse<WatchFile> {
767 let parsed = parse(text);
768 Parse::new(parsed.green_node, parsed.errors)
769}
770
771impl Version {
772 pub fn version(&self) -> u32 {
774 self.0
775 .children_with_tokens()
776 .find_map(|it| match it {
777 SyntaxElement::Token(token) => {
778 if token.kind() == VALUE {
779 token.text().parse().ok()
780 } else {
781 None
782 }
783 }
784 _ => None,
785 })
786 .unwrap_or(DEFAULT_VERSION)
787 }
788}
789
790#[derive(Debug, Clone, Default)]
814pub struct EntryBuilder {
815 url: Option<String>,
816 matching_pattern: Option<String>,
817 version_policy: Option<String>,
818 script: Option<String>,
819 opts: std::collections::HashMap<String, String>,
820}
821
822impl EntryBuilder {
823 pub fn new(url: impl Into<String>) -> Self {
825 EntryBuilder {
826 url: Some(url.into()),
827 matching_pattern: None,
828 version_policy: None,
829 script: None,
830 opts: std::collections::HashMap::new(),
831 }
832 }
833
834 pub fn matching_pattern(mut self, pattern: impl Into<String>) -> Self {
836 self.matching_pattern = Some(pattern.into());
837 self
838 }
839
840 pub fn version_policy(mut self, policy: impl Into<String>) -> Self {
842 self.version_policy = Some(policy.into());
843 self
844 }
845
846 pub fn script(mut self, script: impl Into<String>) -> Self {
848 self.script = Some(script.into());
849 self
850 }
851
852 pub fn opt(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
854 self.opts.insert(key.into(), value.into());
855 self
856 }
857
858 pub fn flag(mut self, key: impl Into<String>) -> Self {
862 self.opts.insert(key.into(), String::new());
863 self
864 }
865
866 pub fn build(self) -> Entry {
872 let url = self.url.expect("URL is required for entry");
873
874 let mut builder = GreenNodeBuilder::new();
875
876 builder.start_node(ENTRY.into());
877
878 if !self.opts.is_empty() {
880 builder.start_node(OPTS_LIST.into());
881 builder.token(KEY.into(), "opts");
882 builder.token(EQUALS.into(), "=");
883
884 let mut first = true;
885 for (key, value) in self.opts.iter() {
886 if !first {
887 builder.token(COMMA.into(), ",");
888 }
889 first = false;
890
891 builder.start_node(OPTION.into());
892 builder.token(KEY.into(), key);
893 if !value.is_empty() {
894 builder.token(EQUALS.into(), "=");
895 builder.token(VALUE.into(), value);
896 }
897 builder.finish_node();
898 }
899
900 builder.finish_node();
901 builder.token(WHITESPACE.into(), " ");
902 }
903
904 builder.start_node(URL.into());
906 builder.token(VALUE.into(), &url);
907 builder.finish_node();
908
909 if let Some(pattern) = self.matching_pattern {
911 builder.token(WHITESPACE.into(), " ");
912 builder.start_node(MATCHING_PATTERN.into());
913 builder.token(VALUE.into(), &pattern);
914 builder.finish_node();
915 }
916
917 if let Some(policy) = self.version_policy {
919 builder.token(WHITESPACE.into(), " ");
920 builder.start_node(VERSION_POLICY.into());
921 builder.token(VALUE.into(), &policy);
922 builder.finish_node();
923 }
924
925 if let Some(script_val) = self.script {
927 builder.token(WHITESPACE.into(), " ");
928 builder.start_node(SCRIPT.into());
929 builder.token(VALUE.into(), &script_val);
930 builder.finish_node();
931 }
932
933 builder.token(NEWLINE.into(), "\n");
934 builder.finish_node();
935
936 Entry(SyntaxNode::new_root_mut(builder.finish()))
937 }
938}
939
940impl Entry {
941 #[cfg(feature = "deb822")]
943 pub(crate) fn syntax(&self) -> &SyntaxNode {
944 &self.0
945 }
946
947 pub fn builder(url: impl Into<String>) -> EntryBuilder {
961 EntryBuilder::new(url)
962 }
963
964 pub fn option_list(&self) -> Option<OptionList> {
966 self.0.children().find_map(OptionList::cast)
967 }
968
969 pub fn get_option(&self, key: &str) -> Option<String> {
971 self.option_list().and_then(|ol| ol.get_option(key))
972 }
973
974 pub fn has_option(&self, key: &str) -> bool {
976 self.option_list().is_some_and(|ol| ol.has_option(key))
977 }
978
979 pub fn component(&self) -> Option<String> {
981 self.get_option("component")
982 }
983
984 pub fn ctype(&self) -> Result<Option<ComponentType>, ()> {
986 self.try_ctype().map_err(|_| ())
987 }
988
989 pub fn try_ctype(&self) -> Result<Option<ComponentType>, crate::types::ParseError> {
991 self.get_option("ctype").map(|s| s.parse()).transpose()
992 }
993
994 pub fn compression(&self) -> Result<Option<Compression>, ()> {
996 self.try_compression().map_err(|_| ())
997 }
998
999 pub fn try_compression(&self) -> Result<Option<Compression>, crate::types::ParseError> {
1001 self.get_option("compression")
1002 .map(|s| s.parse())
1003 .transpose()
1004 }
1005
1006 pub fn repack(&self) -> bool {
1008 self.has_option("repack")
1009 }
1010
1011 pub fn repacksuffix(&self) -> Option<String> {
1013 self.get_option("repacksuffix")
1014 }
1015
1016 pub fn mode(&self) -> Result<Mode, ()> {
1018 self.try_mode().map_err(|_| ())
1019 }
1020
1021 pub fn try_mode(&self) -> Result<Mode, crate::types::ParseError> {
1023 Ok(self
1024 .get_option("mode")
1025 .map(|s| s.parse())
1026 .transpose()?
1027 .unwrap_or_default())
1028 }
1029
1030 pub fn pretty(&self) -> Result<Pretty, ()> {
1032 self.try_pretty().map_err(|_| ())
1033 }
1034
1035 pub fn try_pretty(&self) -> Result<Pretty, crate::types::ParseError> {
1037 Ok(self
1038 .get_option("pretty")
1039 .map(|s| s.parse())
1040 .transpose()?
1041 .unwrap_or_default())
1042 }
1043
1044 pub fn date(&self) -> String {
1047 self.get_option("date").unwrap_or_else(|| "%Y%m%d".into())
1048 }
1049
1050 pub fn gitexport(&self) -> Result<GitExport, ()> {
1052 self.try_gitexport().map_err(|_| ())
1053 }
1054
1055 pub fn try_gitexport(&self) -> Result<GitExport, crate::types::ParseError> {
1057 Ok(self
1058 .get_option("gitexport")
1059 .map(|s| s.parse())
1060 .transpose()?
1061 .unwrap_or_default())
1062 }
1063
1064 pub fn gitmode(&self) -> Result<GitMode, ()> {
1066 self.try_gitmode().map_err(|_| ())
1067 }
1068
1069 pub fn try_gitmode(&self) -> Result<GitMode, crate::types::ParseError> {
1071 Ok(self
1072 .get_option("gitmode")
1073 .map(|s| s.parse())
1074 .transpose()?
1075 .unwrap_or_default())
1076 }
1077
1078 pub fn pgpmode(&self) -> Result<PgpMode, ()> {
1080 self.try_pgpmode().map_err(|_| ())
1081 }
1082
1083 pub fn try_pgpmode(&self) -> Result<PgpMode, crate::types::ParseError> {
1085 Ok(self
1086 .get_option("pgpmode")
1087 .map(|s| s.parse())
1088 .transpose()?
1089 .unwrap_or_default())
1090 }
1091
1092 pub fn searchmode(&self) -> Result<SearchMode, ()> {
1094 self.try_searchmode().map_err(|_| ())
1095 }
1096
1097 pub fn try_searchmode(&self) -> Result<SearchMode, crate::types::ParseError> {
1099 Ok(self
1100 .get_option("searchmode")
1101 .map(|s| s.parse())
1102 .transpose()?
1103 .unwrap_or_default())
1104 }
1105
1106 pub fn decompress(&self) -> bool {
1108 self.has_option("decompress")
1109 }
1110
1111 pub fn bare(&self) -> bool {
1114 self.has_option("bare")
1115 }
1116
1117 pub fn user_agent(&self) -> Option<String> {
1119 self.get_option("user-agent")
1120 }
1121
1122 pub fn passive(&self) -> Option<bool> {
1124 if self.has_option("passive") || self.has_option("pasv") {
1125 Some(true)
1126 } else if self.has_option("active") || self.has_option("nopasv") {
1127 Some(false)
1128 } else {
1129 None
1130 }
1131 }
1132
1133 pub fn unzipoptions(&self) -> Option<String> {
1136 self.get_option("unzipopt")
1137 }
1138
1139 pub fn dversionmangle(&self) -> Option<String> {
1141 self.get_option("dversionmangle")
1142 .or_else(|| self.get_option("versionmangle"))
1143 }
1144
1145 pub fn dirversionmangle(&self) -> Option<String> {
1149 self.get_option("dirversionmangle")
1150 }
1151
1152 pub fn pagemangle(&self) -> Option<String> {
1154 self.get_option("pagemangle")
1155 }
1156
1157 pub fn uversionmangle(&self) -> Option<String> {
1161 self.get_option("uversionmangle")
1162 .or_else(|| self.get_option("versionmangle"))
1163 }
1164
1165 pub fn versionmangle(&self) -> Option<String> {
1167 self.get_option("versionmangle")
1168 }
1169
1170 pub fn hrefdecode(&self) -> bool {
1175 self.get_option("hrefdecode").is_some()
1176 }
1177
1178 pub fn downloadurlmangle(&self) -> Option<String> {
1181 self.get_option("downloadurlmangle")
1182 }
1183
1184 pub fn filenamemangle(&self) -> Option<String> {
1192 self.get_option("filenamemangle")
1193 }
1194
1195 pub fn pgpsigurlmangle(&self) -> Option<String> {
1197 self.get_option("pgpsigurlmangle")
1198 }
1199
1200 pub fn oversionmangle(&self) -> Option<String> {
1203 self.get_option("oversionmangle")
1204 }
1205
1206 pub fn apply_uversionmangle(
1219 &self,
1220 version: &str,
1221 ) -> Result<String, crate::mangle::MangleError> {
1222 if let Some(vm) = self.uversionmangle() {
1223 crate::mangle::apply_mangle(&vm, version)
1224 } else {
1225 Ok(version.to_string())
1226 }
1227 }
1228
1229 pub fn apply_dversionmangle(
1242 &self,
1243 version: &str,
1244 ) -> Result<String, crate::mangle::MangleError> {
1245 if let Some(vm) = self.dversionmangle() {
1246 crate::mangle::apply_mangle(&vm, version)
1247 } else {
1248 Ok(version.to_string())
1249 }
1250 }
1251
1252 pub fn apply_oversionmangle(
1265 &self,
1266 version: &str,
1267 ) -> Result<String, crate::mangle::MangleError> {
1268 if let Some(vm) = self.oversionmangle() {
1269 crate::mangle::apply_mangle(&vm, version)
1270 } else {
1271 Ok(version.to_string())
1272 }
1273 }
1274
1275 pub fn apply_dirversionmangle(
1288 &self,
1289 version: &str,
1290 ) -> Result<String, crate::mangle::MangleError> {
1291 if let Some(vm) = self.dirversionmangle() {
1292 crate::mangle::apply_mangle(&vm, version)
1293 } else {
1294 Ok(version.to_string())
1295 }
1296 }
1297
1298 pub fn apply_filenamemangle(&self, url: &str) -> Result<String, crate::mangle::MangleError> {
1314 if let Some(vm) = self.filenamemangle() {
1315 crate::mangle::apply_mangle(&vm, url)
1316 } else {
1317 Ok(url.to_string())
1318 }
1319 }
1320
1321 pub fn apply_pagemangle(&self, page: &[u8]) -> Result<Vec<u8>, crate::mangle::MangleError> {
1337 if let Some(vm) = self.pagemangle() {
1338 let page_str = String::from_utf8_lossy(page);
1339 let mangled = crate::mangle::apply_mangle(&vm, &page_str)?;
1340 Ok(mangled.into_bytes())
1341 } else {
1342 Ok(page.to_vec())
1343 }
1344 }
1345
1346 pub fn apply_downloadurlmangle(&self, url: &str) -> Result<String, crate::mangle::MangleError> {
1362 if let Some(vm) = self.downloadurlmangle() {
1363 crate::mangle::apply_mangle(&vm, url)
1364 } else {
1365 Ok(url.to_string())
1366 }
1367 }
1368
1369 pub fn opts(&self) -> std::collections::HashMap<String, String> {
1371 let mut options = std::collections::HashMap::new();
1372
1373 if let Some(ol) = self.option_list() {
1374 for opt in ol.options() {
1375 let key = opt.key();
1376 let value = opt.value();
1377 if let (Some(key), Some(value)) = (key, value) {
1378 options.insert(key.to_string(), value.to_string());
1379 }
1380 }
1381 }
1382
1383 options
1384 }
1385
1386 fn items(&self) -> impl Iterator<Item = String> + '_ {
1387 self.0.children_with_tokens().filter_map(|it| match it {
1388 SyntaxElement::Token(token) => {
1389 if token.kind() == VALUE || token.kind() == KEY {
1390 Some(token.text().to_string())
1391 } else {
1392 None
1393 }
1394 }
1395 SyntaxElement::Node(node) => {
1396 match node.kind() {
1398 URL => Url::cast(node).map(|n| n.url()),
1399 MATCHING_PATTERN => MatchingPattern::cast(node).map(|n| n.pattern()),
1400 VERSION_POLICY => VersionPolicyNode::cast(node).map(|n| n.policy()),
1401 SCRIPT => ScriptNode::cast(node).map(|n| n.script()),
1402 _ => None,
1403 }
1404 }
1405 })
1406 }
1407
1408 pub fn url_node(&self) -> Option<Url> {
1410 self.0.children().find_map(Url::cast)
1411 }
1412
1413 pub fn url(&self) -> String {
1415 self.url_node().map(|it| it.url()).unwrap_or_else(|| {
1416 self.items().next().unwrap()
1418 })
1419 }
1420
1421 pub fn matching_pattern_node(&self) -> Option<MatchingPattern> {
1423 self.0.children().find_map(MatchingPattern::cast)
1424 }
1425
1426 pub fn matching_pattern(&self) -> Option<String> {
1428 self.matching_pattern_node()
1429 .map(|it| it.pattern())
1430 .or_else(|| {
1431 self.items().nth(1)
1433 })
1434 }
1435
1436 pub fn version_node(&self) -> Option<VersionPolicyNode> {
1438 self.0.children().find_map(VersionPolicyNode::cast)
1439 }
1440
1441 pub fn version(&self) -> Result<Option<crate::VersionPolicy>, String> {
1443 self.version_node()
1444 .map(|it| it.policy().parse())
1445 .transpose()
1446 .map_err(|e: crate::types::ParseError| e.to_string())
1447 .or_else(|_e| {
1448 self.items()
1450 .nth(2)
1451 .map(|it| it.parse())
1452 .transpose()
1453 .map_err(|e: crate::types::ParseError| e.to_string())
1454 })
1455 }
1456
1457 pub fn script_node(&self) -> Option<ScriptNode> {
1459 self.0.children().find_map(ScriptNode::cast)
1460 }
1461
1462 pub fn script(&self) -> Option<String> {
1464 self.script_node().map(|it| it.script()).or_else(|| {
1465 self.items().nth(3)
1467 })
1468 }
1469
1470 pub fn format_url(&self, package: impl FnOnce() -> String) -> url::Url {
1472 crate::subst::subst(self.url().as_str(), package)
1473 .parse()
1474 .unwrap()
1475 }
1476
1477 pub fn set_url(&mut self, new_url: &str) {
1479 let mut builder = GreenNodeBuilder::new();
1481 builder.start_node(URL.into());
1482 builder.token(VALUE.into(), new_url);
1483 builder.finish_node();
1484 let new_url_green = builder.finish();
1485
1486 let new_url_node = SyntaxNode::new_root_mut(new_url_green);
1488
1489 let url_pos = self
1491 .0
1492 .children_with_tokens()
1493 .position(|child| matches!(child, SyntaxElement::Node(node) if node.kind() == URL));
1494
1495 if let Some(pos) = url_pos {
1496 self.0
1498 .splice_children(pos..pos + 1, vec![new_url_node.into()]);
1499 }
1500 }
1501
1502 pub fn set_matching_pattern(&mut self, new_pattern: &str) {
1508 let mut builder = GreenNodeBuilder::new();
1510 builder.start_node(MATCHING_PATTERN.into());
1511 builder.token(VALUE.into(), new_pattern);
1512 builder.finish_node();
1513 let new_pattern_green = builder.finish();
1514
1515 let new_pattern_node = SyntaxNode::new_root_mut(new_pattern_green);
1517
1518 let pattern_pos = self.0.children_with_tokens().position(
1520 |child| matches!(child, SyntaxElement::Node(node) if node.kind() == MATCHING_PATTERN),
1521 );
1522
1523 if let Some(pos) = pattern_pos {
1524 self.0
1526 .splice_children(pos..pos + 1, vec![new_pattern_node.into()]);
1527 }
1528 }
1530
1531 pub fn set_version_policy(&mut self, new_policy: &str) {
1537 let mut builder = GreenNodeBuilder::new();
1539 builder.start_node(VERSION_POLICY.into());
1540 builder.token(VALUE.into(), new_policy);
1542 builder.finish_node();
1543 let new_policy_green = builder.finish();
1544
1545 let new_policy_node = SyntaxNode::new_root_mut(new_policy_green);
1547
1548 let policy_pos = self.0.children_with_tokens().position(
1550 |child| matches!(child, SyntaxElement::Node(node) if node.kind() == VERSION_POLICY),
1551 );
1552
1553 if let Some(pos) = policy_pos {
1554 self.0
1556 .splice_children(pos..pos + 1, vec![new_policy_node.into()]);
1557 }
1558 }
1560
1561 pub fn set_script(&mut self, new_script: &str) {
1567 let mut builder = GreenNodeBuilder::new();
1569 builder.start_node(SCRIPT.into());
1570 builder.token(VALUE.into(), new_script);
1572 builder.finish_node();
1573 let new_script_green = builder.finish();
1574
1575 let new_script_node = SyntaxNode::new_root_mut(new_script_green);
1577
1578 let script_pos = self
1580 .0
1581 .children_with_tokens()
1582 .position(|child| matches!(child, SyntaxElement::Node(node) if node.kind() == SCRIPT));
1583
1584 if let Some(pos) = script_pos {
1585 self.0
1587 .splice_children(pos..pos + 1, vec![new_script_node.into()]);
1588 }
1589 }
1591
1592 pub fn set_opt(&mut self, key: &str, value: &str) {
1598 let opts_pos = self.0.children_with_tokens().position(
1600 |child| matches!(child, SyntaxElement::Node(node) if node.kind() == OPTS_LIST),
1601 );
1602
1603 if let Some(_opts_idx) = opts_pos {
1604 if let Some(mut ol) = self.option_list() {
1605 if let Some(mut opt) = ol.find_option(key) {
1607 opt.set_value(value);
1609 } else {
1611 ol.add_option(key, value);
1613 }
1615 }
1616 } else {
1617 let mut builder = GreenNodeBuilder::new();
1619 builder.start_node(OPTS_LIST.into());
1620 builder.token(KEY.into(), "opts");
1621 builder.token(EQUALS.into(), "=");
1622 builder.start_node(OPTION.into());
1623 builder.token(KEY.into(), key);
1624 builder.token(EQUALS.into(), "=");
1625 builder.token(VALUE.into(), value);
1626 builder.finish_node();
1627 builder.finish_node();
1628 let new_opts_green = builder.finish();
1629 let new_opts_node = SyntaxNode::new_root_mut(new_opts_green);
1630
1631 let url_pos = self
1633 .0
1634 .children_with_tokens()
1635 .position(|child| matches!(child, SyntaxElement::Node(node) if node.kind() == URL));
1636
1637 if let Some(url_idx) = url_pos {
1638 let mut combined_builder = GreenNodeBuilder::new();
1641 combined_builder.start_node(ROOT.into()); combined_builder.token(WHITESPACE.into(), " ");
1643 combined_builder.finish_node();
1644 let temp_green = combined_builder.finish();
1645 let temp_root = SyntaxNode::new_root_mut(temp_green);
1646 let space_element = temp_root.children_with_tokens().next().unwrap();
1647
1648 self.0
1649 .splice_children(url_idx..url_idx, vec![new_opts_node.into(), space_element]);
1650 } else {
1651 self.0.splice_children(0..0, vec![new_opts_node.into()]);
1652 }
1653 }
1654 }
1655
1656 pub fn del_opt(&mut self, key: &str) {
1663 if let Some(mut ol) = self.option_list() {
1664 let option_count = ol.0.children().filter(|n| n.kind() == OPTION).count();
1665
1666 if option_count == 1 && ol.has_option(key) {
1667 let opts_pos = self.0.children().position(|node| node.kind() == OPTS_LIST);
1669
1670 if let Some(opts_idx) = opts_pos {
1671 self.0.splice_children(opts_idx..opts_idx + 1, vec![]);
1673
1674 while self.0.children_with_tokens().next().map_or(false, |e| {
1676 matches!(
1677 e,
1678 SyntaxElement::Token(t) if t.kind() == WHITESPACE || t.kind() == CONTINUATION
1679 )
1680 }) {
1681 self.0.splice_children(0..1, vec![]);
1682 }
1683 }
1684 } else {
1685 ol.remove_option(key);
1687 }
1688 }
1689 }
1690}
1691
1692impl std::fmt::Debug for OptionList {
1693 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1694 f.debug_struct("OptionList")
1695 .field("text", &self.0.text().to_string())
1696 .finish()
1697 }
1698}
1699
1700impl OptionList {
1701 pub fn options(&self) -> impl Iterator<Item = _Option> + '_ {
1703 self.0.children().filter_map(_Option::cast)
1704 }
1705
1706 pub fn find_option(&self, key: &str) -> Option<_Option> {
1708 self.options().find(|opt| opt.key().as_deref() == Some(key))
1709 }
1710
1711 pub fn has_option(&self, key: &str) -> bool {
1713 self.options().any(|it| it.key().as_deref() == Some(key))
1714 }
1715
1716 #[cfg(feature = "deb822")]
1719 pub(crate) fn iter_key_values(&self) -> impl Iterator<Item = (String, String)> + '_ {
1720 self.options().filter_map(|opt| {
1721 if let (Some(key), Some(value)) = (opt.key(), opt.value()) {
1722 Some((key, value))
1723 } else {
1724 None
1725 }
1726 })
1727 }
1728
1729 pub fn get_option(&self, key: &str) -> Option<String> {
1731 for child in self.options() {
1732 if child.key().as_deref() == Some(key) {
1733 return child.value();
1734 }
1735 }
1736 None
1737 }
1738
1739 fn add_option(&mut self, key: &str, value: &str) {
1741 let option_count = self.0.children().filter(|n| n.kind() == OPTION).count();
1742
1743 let mut builder = GreenNodeBuilder::new();
1745 builder.start_node(ROOT.into()); if option_count > 0 {
1748 builder.start_node(OPTION_SEPARATOR.into());
1749 builder.token(COMMA.into(), ",");
1750 builder.finish_node();
1751 }
1752
1753 builder.start_node(OPTION.into());
1754 builder.token(KEY.into(), key);
1755 builder.token(EQUALS.into(), "=");
1756 builder.token(VALUE.into(), value);
1757 builder.finish_node();
1758
1759 builder.finish_node(); let combined_green = builder.finish();
1761
1762 let temp_root = SyntaxNode::new_root_mut(combined_green);
1764 let new_children: Vec<_> = temp_root.children_with_tokens().collect();
1765
1766 let insert_pos = self.0.children_with_tokens().count();
1767 self.0.splice_children(insert_pos..insert_pos, new_children);
1768 }
1769
1770 fn remove_option(&mut self, key: &str) -> bool {
1772 if let Some(mut opt) = self.find_option(key) {
1773 opt.remove();
1774 true
1775 } else {
1776 false
1777 }
1778 }
1779}
1780
1781impl _Option {
1782 pub fn key(&self) -> Option<String> {
1784 self.0.children_with_tokens().find_map(|it| match it {
1785 SyntaxElement::Token(token) => {
1786 if token.kind() == KEY {
1787 Some(token.text().to_string())
1788 } else {
1789 None
1790 }
1791 }
1792 _ => None,
1793 })
1794 }
1795
1796 pub fn value(&self) -> Option<String> {
1798 self.0
1799 .children_with_tokens()
1800 .filter_map(|it| match it {
1801 SyntaxElement::Token(token) => {
1802 if token.kind() == VALUE || token.kind() == KEY {
1803 Some(token.text().to_string())
1804 } else {
1805 None
1806 }
1807 }
1808 _ => None,
1809 })
1810 .nth(1)
1811 }
1812
1813 pub fn set_value(&mut self, new_value: &str) {
1815 let key = self.key().expect("Option must have a key");
1816
1817 let mut builder = GreenNodeBuilder::new();
1819 builder.start_node(OPTION.into());
1820 builder.token(KEY.into(), &key);
1821 builder.token(EQUALS.into(), "=");
1822 builder.token(VALUE.into(), new_value);
1823 builder.finish_node();
1824 let new_option_green = builder.finish();
1825 let new_option_node = SyntaxNode::new_root_mut(new_option_green);
1826
1827 if let Some(parent) = self.0.parent() {
1829 let idx = self.0.index();
1830 parent.splice_children(idx..idx + 1, vec![new_option_node.into()]);
1831 }
1832 }
1833
1834 pub fn remove(&mut self) {
1836 let next_sep = self
1838 .0
1839 .next_sibling()
1840 .filter(|n| n.kind() == OPTION_SEPARATOR);
1841 let prev_sep = self
1842 .0
1843 .prev_sibling()
1844 .filter(|n| n.kind() == OPTION_SEPARATOR);
1845
1846 if let Some(sep) = next_sep {
1848 sep.detach();
1849 } else if let Some(sep) = prev_sep {
1850 sep.detach();
1851 }
1852
1853 self.0.detach();
1855 }
1856}
1857
1858impl Url {
1859 pub fn url(&self) -> String {
1861 self.0
1862 .children_with_tokens()
1863 .find_map(|it| match it {
1864 SyntaxElement::Token(token) => {
1865 if token.kind() == VALUE {
1866 Some(token.text().to_string())
1867 } else {
1868 None
1869 }
1870 }
1871 _ => None,
1872 })
1873 .unwrap()
1874 }
1875}
1876
1877impl MatchingPattern {
1878 pub fn pattern(&self) -> String {
1880 self.0
1881 .children_with_tokens()
1882 .find_map(|it| match it {
1883 SyntaxElement::Token(token) => {
1884 if token.kind() == VALUE {
1885 Some(token.text().to_string())
1886 } else {
1887 None
1888 }
1889 }
1890 _ => None,
1891 })
1892 .unwrap()
1893 }
1894}
1895
1896impl VersionPolicyNode {
1897 pub fn policy(&self) -> String {
1899 self.0
1900 .children_with_tokens()
1901 .find_map(|it| match it {
1902 SyntaxElement::Token(token) => {
1903 if token.kind() == VALUE || token.kind() == KEY {
1905 Some(token.text().to_string())
1906 } else {
1907 None
1908 }
1909 }
1910 _ => None,
1911 })
1912 .unwrap()
1913 }
1914}
1915
1916impl ScriptNode {
1917 pub fn script(&self) -> String {
1919 self.0
1920 .children_with_tokens()
1921 .find_map(|it| match it {
1922 SyntaxElement::Token(token) => {
1923 if token.kind() == VALUE || token.kind() == KEY {
1925 Some(token.text().to_string())
1926 } else {
1927 None
1928 }
1929 }
1930 _ => None,
1931 })
1932 .unwrap()
1933 }
1934}
1935
1936#[cfg(test)]
1937mod tests {
1938 use super::*;
1939
1940 #[test]
1941 fn test_entry_node_structure() {
1942 let wf: super::WatchFile = r#"version=4
1944opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
1945"#
1946 .parse()
1947 .unwrap();
1948
1949 let entry = wf.entries().next().unwrap();
1950
1951 assert_eq!(entry.0.children().find(|n| n.kind() == URL).is_some(), true);
1953 assert_eq!(entry.url(), "https://example.com/releases");
1954
1955 assert_eq!(
1957 entry
1958 .0
1959 .children()
1960 .find(|n| n.kind() == MATCHING_PATTERN)
1961 .is_some(),
1962 true
1963 );
1964 assert_eq!(
1965 entry.matching_pattern(),
1966 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
1967 );
1968
1969 assert_eq!(
1971 entry
1972 .0
1973 .children()
1974 .find(|n| n.kind() == VERSION_POLICY)
1975 .is_some(),
1976 true
1977 );
1978 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
1979
1980 assert_eq!(
1982 entry.0.children().find(|n| n.kind() == SCRIPT).is_some(),
1983 true
1984 );
1985 assert_eq!(entry.script(), Some("uupdate".into()));
1986 }
1987
1988 #[test]
1989 fn test_entry_node_structure_partial() {
1990 let wf: super::WatchFile = r#"version=4
1992https://github.com/example/tags .*/v?(\d\S+)\.tar\.gz
1993"#
1994 .parse()
1995 .unwrap();
1996
1997 let entry = wf.entries().next().unwrap();
1998
1999 assert_eq!(entry.0.children().find(|n| n.kind() == URL).is_some(), true);
2001 assert_eq!(
2002 entry
2003 .0
2004 .children()
2005 .find(|n| n.kind() == MATCHING_PATTERN)
2006 .is_some(),
2007 true
2008 );
2009
2010 assert_eq!(
2012 entry
2013 .0
2014 .children()
2015 .find(|n| n.kind() == VERSION_POLICY)
2016 .is_some(),
2017 false
2018 );
2019 assert_eq!(
2020 entry.0.children().find(|n| n.kind() == SCRIPT).is_some(),
2021 false
2022 );
2023
2024 assert_eq!(entry.url(), "https://github.com/example/tags");
2026 assert_eq!(
2027 entry.matching_pattern(),
2028 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2029 );
2030 assert_eq!(entry.version(), Ok(None));
2031 assert_eq!(entry.script(), None);
2032 }
2033
2034 #[test]
2035 fn test_parse_v1() {
2036 const WATCHV1: &str = r#"version=4
2037opts=bare,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1\.tar\.gz/ \
2038 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2039"#;
2040 let parsed = parse(WATCHV1);
2041 let node = parsed.syntax();
2043 assert_eq!(
2044 format!("{:#?}", node),
2045 r#"ROOT@0..161
2046 VERSION@0..10
2047 KEY@0..7 "version"
2048 EQUALS@7..8 "="
2049 VALUE@8..9 "4"
2050 NEWLINE@9..10 "\n"
2051 ENTRY@10..161
2052 OPTS_LIST@10..86
2053 KEY@10..14 "opts"
2054 EQUALS@14..15 "="
2055 OPTION@15..19
2056 KEY@15..19 "bare"
2057 OPTION_SEPARATOR@19..20
2058 COMMA@19..20 ","
2059 OPTION@20..86
2060 KEY@20..34 "filenamemangle"
2061 EQUALS@34..35 "="
2062 VALUE@35..86 "s/.+\\/v?(\\d\\S+)\\.tar\\ ..."
2063 WHITESPACE@86..87 " "
2064 CONTINUATION@87..89 "\\\n"
2065 WHITESPACE@89..91 " "
2066 URL@91..138
2067 VALUE@91..138 "https://github.com/sy ..."
2068 WHITESPACE@138..139 " "
2069 MATCHING_PATTERN@139..160
2070 VALUE@139..160 ".*/v?(\\d\\S+)\\.tar\\.gz"
2071 NEWLINE@160..161 "\n"
2072"#
2073 );
2074
2075 let root = parsed.root();
2076 assert_eq!(root.version(), 4);
2077 let entries = root.entries().collect::<Vec<_>>();
2078 assert_eq!(entries.len(), 1);
2079 let entry = &entries[0];
2080 assert_eq!(
2081 entry.url(),
2082 "https://github.com/syncthing/syncthing-gtk/tags"
2083 );
2084 assert_eq!(
2085 entry.matching_pattern(),
2086 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2087 );
2088 assert_eq!(entry.version(), Ok(None));
2089 assert_eq!(entry.script(), None);
2090
2091 assert_eq!(node.text(), WATCHV1);
2092 }
2093
2094 #[test]
2095 fn test_parse_v2() {
2096 let parsed = parse(
2097 r#"version=4
2098https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2099# comment
2100"#,
2101 );
2102 assert_eq!(parsed.errors, Vec::<String>::new());
2103 let node = parsed.syntax();
2104 assert_eq!(
2105 format!("{:#?}", node),
2106 r###"ROOT@0..90
2107 VERSION@0..10
2108 KEY@0..7 "version"
2109 EQUALS@7..8 "="
2110 VALUE@8..9 "4"
2111 NEWLINE@9..10 "\n"
2112 ENTRY@10..80
2113 URL@10..57
2114 VALUE@10..57 "https://github.com/sy ..."
2115 WHITESPACE@57..58 " "
2116 MATCHING_PATTERN@58..79
2117 VALUE@58..79 ".*/v?(\\d\\S+)\\.tar\\.gz"
2118 NEWLINE@79..80 "\n"
2119 COMMENT@80..89 "# comment"
2120 NEWLINE@89..90 "\n"
2121"###
2122 );
2123
2124 let root = parsed.root();
2125 assert_eq!(root.version(), 4);
2126 let entries = root.entries().collect::<Vec<_>>();
2127 assert_eq!(entries.len(), 1);
2128 let entry = &entries[0];
2129 assert_eq!(
2130 entry.url(),
2131 "https://github.com/syncthing/syncthing-gtk/tags"
2132 );
2133 assert_eq!(
2134 entry.format_url(|| "syncthing-gtk".to_string()),
2135 "https://github.com/syncthing/syncthing-gtk/tags"
2136 .parse()
2137 .unwrap()
2138 );
2139 }
2140
2141 #[test]
2142 fn test_parse_v3() {
2143 let parsed = parse(
2144 r#"version=4
2145https://github.com/syncthing/@PACKAGE@/tags .*/v?(\d\S+)\.tar\.gz
2146# comment
2147"#,
2148 );
2149 assert_eq!(parsed.errors, Vec::<String>::new());
2150 let root = parsed.root();
2151 assert_eq!(root.version(), 4);
2152 let entries = root.entries().collect::<Vec<_>>();
2153 assert_eq!(entries.len(), 1);
2154 let entry = &entries[0];
2155 assert_eq!(entry.url(), "https://github.com/syncthing/@PACKAGE@/tags");
2156 assert_eq!(
2157 entry.format_url(|| "syncthing-gtk".to_string()),
2158 "https://github.com/syncthing/syncthing-gtk/tags"
2159 .parse()
2160 .unwrap()
2161 );
2162 }
2163
2164 #[test]
2165 fn test_thread_safe_parsing() {
2166 let text = r#"version=4
2167https://github.com/example/example/tags example-(.*)\.tar\.gz
2168"#;
2169
2170 let parsed = parse_watch_file(text);
2171 assert!(parsed.is_ok());
2172 assert_eq!(parsed.errors().len(), 0);
2173
2174 let watchfile = parsed.tree();
2176 assert_eq!(watchfile.version(), 4);
2177 let entries: Vec<_> = watchfile.entries().collect();
2178 assert_eq!(entries.len(), 1);
2179 }
2180
2181 #[test]
2182 fn test_parse_clone_and_eq() {
2183 let text = r#"version=4
2184https://github.com/example/example/tags example-(.*)\.tar\.gz
2185"#;
2186
2187 let parsed1 = parse_watch_file(text);
2188 let parsed2 = parsed1.clone();
2189
2190 assert_eq!(parsed1, parsed2);
2192
2193 let watchfile1 = parsed1.tree();
2195 let watchfile2 = watchfile1.clone();
2196 assert_eq!(watchfile1, watchfile2);
2197 }
2198
2199 #[test]
2200 fn test_parse_v4() {
2201 let cl: super::WatchFile = r#"version=4
2202opts=repack,compression=xz,dversionmangle=s/\+ds//,repacksuffix=+ds \
2203 https://github.com/example/example-cat/tags \
2204 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2205"#
2206 .parse()
2207 .unwrap();
2208 assert_eq!(cl.version(), 4);
2209 let entries = cl.entries().collect::<Vec<_>>();
2210 assert_eq!(entries.len(), 1);
2211 let entry = &entries[0];
2212 assert_eq!(entry.url(), "https://github.com/example/example-cat/tags");
2213 assert_eq!(
2214 entry.matching_pattern(),
2215 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2216 );
2217 assert!(entry.repack());
2218 assert_eq!(entry.compression(), Ok(Some(Compression::Xz)));
2219 assert_eq!(entry.dversionmangle(), Some("s/\\+ds//".into()));
2220 assert_eq!(entry.repacksuffix(), Some("+ds".into()));
2221 assert_eq!(entry.script(), Some("uupdate".into()));
2222 assert_eq!(
2223 entry.format_url(|| "example-cat".to_string()),
2224 "https://github.com/example/example-cat/tags"
2225 .parse()
2226 .unwrap()
2227 );
2228 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
2229 }
2230
2231 #[test]
2232 fn test_git_mode() {
2233 let text = r#"version=3
2234opts="mode=git, gitmode=shallow, pgpmode=gittag" \
2235https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git \
2236refs/tags/(.*) debian
2237"#;
2238 let parsed = parse(text);
2239 assert_eq!(parsed.errors, Vec::<String>::new());
2240 let cl = parsed.root();
2241 assert_eq!(cl.version(), 3);
2242 let entries = cl.entries().collect::<Vec<_>>();
2243 assert_eq!(entries.len(), 1);
2244 let entry = &entries[0];
2245 assert_eq!(
2246 entry.url(),
2247 "https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git"
2248 );
2249 assert_eq!(entry.matching_pattern(), Some("refs/tags/(.*)".into()));
2250 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
2251 assert_eq!(entry.script(), None);
2252 assert_eq!(entry.gitmode(), Ok(GitMode::Shallow));
2253 assert_eq!(entry.pgpmode(), Ok(PgpMode::GitTag));
2254 assert_eq!(entry.mode(), Ok(Mode::Git));
2255 }
2256
2257 #[test]
2258 fn test_parse_quoted() {
2259 const WATCHV1: &str = r#"version=4
2260opts="bare, filenamemangle=blah" \
2261 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2262"#;
2263 let parsed = parse(WATCHV1);
2264 let node = parsed.syntax();
2266
2267 let root = parsed.root();
2268 assert_eq!(root.version(), 4);
2269 let entries = root.entries().collect::<Vec<_>>();
2270 assert_eq!(entries.len(), 1);
2271 let entry = &entries[0];
2272
2273 assert_eq!(
2274 entry.url(),
2275 "https://github.com/syncthing/syncthing-gtk/tags"
2276 );
2277 assert_eq!(
2278 entry.matching_pattern(),
2279 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2280 );
2281 assert_eq!(entry.version(), Ok(None));
2282 assert_eq!(entry.script(), None);
2283
2284 assert_eq!(node.text(), WATCHV1);
2285 }
2286
2287 #[test]
2288 fn test_set_url() {
2289 let wf: super::WatchFile = r#"version=4
2291https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2292"#
2293 .parse()
2294 .unwrap();
2295
2296 let mut entry = wf.entries().next().unwrap();
2297 assert_eq!(
2298 entry.url(),
2299 "https://github.com/syncthing/syncthing-gtk/tags"
2300 );
2301
2302 entry.set_url("https://newurl.example.org/path");
2303 assert_eq!(entry.url(), "https://newurl.example.org/path");
2304 assert_eq!(
2305 entry.matching_pattern(),
2306 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2307 );
2308
2309 assert_eq!(
2311 entry.to_string(),
2312 "https://newurl.example.org/path .*/v?(\\d\\S+)\\.tar\\.gz\n"
2313 );
2314 }
2315
2316 #[test]
2317 fn test_set_url_with_options() {
2318 let wf: super::WatchFile = r#"version=4
2320opts=foo=blah https://foo.com/bar .*/v?(\d\S+)\.tar\.gz
2321"#
2322 .parse()
2323 .unwrap();
2324
2325 let mut entry = wf.entries().next().unwrap();
2326 assert_eq!(entry.url(), "https://foo.com/bar");
2327 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2328
2329 entry.set_url("https://example.com/baz");
2330 assert_eq!(entry.url(), "https://example.com/baz");
2331
2332 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2334 assert_eq!(
2335 entry.matching_pattern(),
2336 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2337 );
2338
2339 assert_eq!(
2341 entry.to_string(),
2342 "opts=foo=blah https://example.com/baz .*/v?(\\d\\S+)\\.tar\\.gz\n"
2343 );
2344 }
2345
2346 #[test]
2347 fn test_set_url_complex() {
2348 let wf: super::WatchFile = r#"version=4
2350opts=bare,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1\.tar\.gz/ \
2351 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2352"#
2353 .parse()
2354 .unwrap();
2355
2356 let mut entry = wf.entries().next().unwrap();
2357 assert_eq!(
2358 entry.url(),
2359 "https://github.com/syncthing/syncthing-gtk/tags"
2360 );
2361
2362 entry.set_url("https://gitlab.com/newproject/tags");
2363 assert_eq!(entry.url(), "https://gitlab.com/newproject/tags");
2364
2365 assert!(entry.bare());
2367 assert_eq!(
2368 entry.filenamemangle(),
2369 Some("s/.+\\/v?(\\d\\S+)\\.tar\\.gz/syncthing-gtk-$1\\.tar\\.gz/".into())
2370 );
2371 assert_eq!(
2372 entry.matching_pattern(),
2373 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2374 );
2375
2376 assert_eq!(
2378 entry.to_string(),
2379 r#"opts=bare,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1\.tar\.gz/ \
2380 https://gitlab.com/newproject/tags .*/v?(\d\S+)\.tar\.gz
2381"#
2382 );
2383 }
2384
2385 #[test]
2386 fn test_set_url_with_all_fields() {
2387 let wf: super::WatchFile = r#"version=4
2389opts=repack,compression=xz,dversionmangle=s/\+ds//,repacksuffix=+ds \
2390 https://github.com/example/example-cat/tags \
2391 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2392"#
2393 .parse()
2394 .unwrap();
2395
2396 let mut entry = wf.entries().next().unwrap();
2397 assert_eq!(entry.url(), "https://github.com/example/example-cat/tags");
2398 assert_eq!(
2399 entry.matching_pattern(),
2400 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2401 );
2402 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2403 assert_eq!(entry.script(), Some("uupdate".into()));
2404
2405 entry.set_url("https://gitlab.example.org/project/releases");
2406 assert_eq!(entry.url(), "https://gitlab.example.org/project/releases");
2407
2408 assert!(entry.repack());
2410 assert_eq!(entry.compression(), Ok(Some(super::Compression::Xz)));
2411 assert_eq!(entry.dversionmangle(), Some("s/\\+ds//".into()));
2412 assert_eq!(entry.repacksuffix(), Some("+ds".into()));
2413 assert_eq!(
2414 entry.matching_pattern(),
2415 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2416 );
2417 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2418 assert_eq!(entry.script(), Some("uupdate".into()));
2419
2420 assert_eq!(
2422 entry.to_string(),
2423 r#"opts=repack,compression=xz,dversionmangle=s/\+ds//,repacksuffix=+ds \
2424 https://gitlab.example.org/project/releases \
2425 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2426"#
2427 );
2428 }
2429
2430 #[test]
2431 fn test_set_url_quoted_options() {
2432 let wf: super::WatchFile = r#"version=4
2434opts="bare, filenamemangle=blah" \
2435 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2436"#
2437 .parse()
2438 .unwrap();
2439
2440 let mut entry = wf.entries().next().unwrap();
2441 assert_eq!(
2442 entry.url(),
2443 "https://github.com/syncthing/syncthing-gtk/tags"
2444 );
2445
2446 entry.set_url("https://example.org/new/path");
2447 assert_eq!(entry.url(), "https://example.org/new/path");
2448
2449 assert_eq!(
2451 entry.to_string(),
2452 r#"opts="bare, filenamemangle=blah" \
2453 https://example.org/new/path .*/v?(\d\S+)\.tar\.gz
2454"#
2455 );
2456 }
2457
2458 #[test]
2459 fn test_set_opt_update_existing() {
2460 let wf: super::WatchFile = r#"version=4
2462opts=foo=blah,bar=baz https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2463"#
2464 .parse()
2465 .unwrap();
2466
2467 let mut entry = wf.entries().next().unwrap();
2468 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2469 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2470
2471 entry.set_opt("foo", "updated");
2472 assert_eq!(entry.get_option("foo"), Some("updated".to_string()));
2473 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2474
2475 assert_eq!(
2477 entry.to_string(),
2478 "opts=foo=updated,bar=baz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2479 );
2480 }
2481
2482 #[test]
2483 fn test_set_opt_add_new() {
2484 let wf: super::WatchFile = r#"version=4
2486opts=foo=blah https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2487"#
2488 .parse()
2489 .unwrap();
2490
2491 let mut entry = wf.entries().next().unwrap();
2492 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2493 assert_eq!(entry.get_option("bar"), None);
2494
2495 entry.set_opt("bar", "baz");
2496 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2497 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2498
2499 assert_eq!(
2501 entry.to_string(),
2502 "opts=foo=blah,bar=baz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2503 );
2504 }
2505
2506 #[test]
2507 fn test_set_opt_create_options_list() {
2508 let wf: super::WatchFile = r#"version=4
2510https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2511"#
2512 .parse()
2513 .unwrap();
2514
2515 let mut entry = wf.entries().next().unwrap();
2516 assert_eq!(entry.option_list(), None);
2517
2518 entry.set_opt("compression", "xz");
2519 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
2520
2521 assert_eq!(
2523 entry.to_string(),
2524 "opts=compression=xz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2525 );
2526 }
2527
2528 #[test]
2529 fn test_del_opt_remove_single() {
2530 let wf: super::WatchFile = r#"version=4
2532opts=foo=blah,bar=baz,qux=quux https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2533"#
2534 .parse()
2535 .unwrap();
2536
2537 let mut entry = wf.entries().next().unwrap();
2538 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2539 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2540 assert_eq!(entry.get_option("qux"), Some("quux".to_string()));
2541
2542 entry.del_opt("bar");
2543 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2544 assert_eq!(entry.get_option("bar"), None);
2545 assert_eq!(entry.get_option("qux"), Some("quux".to_string()));
2546
2547 assert_eq!(
2549 entry.to_string(),
2550 "opts=foo=blah,qux=quux https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2551 );
2552 }
2553
2554 #[test]
2555 fn test_del_opt_remove_first() {
2556 let wf: super::WatchFile = r#"version=4
2558opts=foo=blah,bar=baz https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2559"#
2560 .parse()
2561 .unwrap();
2562
2563 let mut entry = wf.entries().next().unwrap();
2564 entry.del_opt("foo");
2565 assert_eq!(entry.get_option("foo"), None);
2566 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2567
2568 assert_eq!(
2570 entry.to_string(),
2571 "opts=bar=baz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2572 );
2573 }
2574
2575 #[test]
2576 fn test_del_opt_remove_last() {
2577 let wf: super::WatchFile = r#"version=4
2579opts=foo=blah,bar=baz https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2580"#
2581 .parse()
2582 .unwrap();
2583
2584 let mut entry = wf.entries().next().unwrap();
2585 entry.del_opt("bar");
2586 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2587 assert_eq!(entry.get_option("bar"), None);
2588
2589 assert_eq!(
2591 entry.to_string(),
2592 "opts=foo=blah https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2593 );
2594 }
2595
2596 #[test]
2597 fn test_del_opt_remove_only_option() {
2598 let wf: super::WatchFile = r#"version=4
2600opts=foo=blah https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2601"#
2602 .parse()
2603 .unwrap();
2604
2605 let mut entry = wf.entries().next().unwrap();
2606 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2607
2608 entry.del_opt("foo");
2609 assert_eq!(entry.get_option("foo"), None);
2610 assert_eq!(entry.option_list(), None);
2611
2612 assert_eq!(
2614 entry.to_string(),
2615 "https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2616 );
2617 }
2618
2619 #[test]
2620 fn test_del_opt_nonexistent() {
2621 let wf: super::WatchFile = r#"version=4
2623opts=foo=blah https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2624"#
2625 .parse()
2626 .unwrap();
2627
2628 let mut entry = wf.entries().next().unwrap();
2629 let original = entry.to_string();
2630
2631 entry.del_opt("nonexistent");
2632 assert_eq!(entry.to_string(), original);
2633 }
2634
2635 #[test]
2636 fn test_set_opt_multiple_operations() {
2637 let wf: super::WatchFile = r#"version=4
2639https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2640"#
2641 .parse()
2642 .unwrap();
2643
2644 let mut entry = wf.entries().next().unwrap();
2645
2646 entry.set_opt("compression", "xz");
2647 entry.set_opt("repack", "");
2648 entry.set_opt("dversionmangle", "s/\\+ds//");
2649
2650 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
2651 assert_eq!(
2652 entry.get_option("dversionmangle"),
2653 Some("s/\\+ds//".to_string())
2654 );
2655 }
2656
2657 #[test]
2658 fn test_set_matching_pattern() {
2659 let wf: super::WatchFile = r#"version=4
2661https://github.com/example/tags .*/v?(\d\S+)\.tar\.gz
2662"#
2663 .parse()
2664 .unwrap();
2665
2666 let mut entry = wf.entries().next().unwrap();
2667 assert_eq!(
2668 entry.matching_pattern(),
2669 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2670 );
2671
2672 entry.set_matching_pattern("(?:.*?/)?v?([\\d.]+)\\.tar\\.gz");
2673 assert_eq!(
2674 entry.matching_pattern(),
2675 Some("(?:.*?/)?v?([\\d.]+)\\.tar\\.gz".into())
2676 );
2677
2678 assert_eq!(entry.url(), "https://github.com/example/tags");
2680
2681 assert_eq!(
2683 entry.to_string(),
2684 "https://github.com/example/tags (?:.*?/)?v?([\\d.]+)\\.tar\\.gz\n"
2685 );
2686 }
2687
2688 #[test]
2689 fn test_set_matching_pattern_with_all_fields() {
2690 let wf: super::WatchFile = r#"version=4
2692opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2693"#
2694 .parse()
2695 .unwrap();
2696
2697 let mut entry = wf.entries().next().unwrap();
2698 assert_eq!(
2699 entry.matching_pattern(),
2700 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2701 );
2702
2703 entry.set_matching_pattern(".*/version-([\\d.]+)\\.tar\\.xz");
2704 assert_eq!(
2705 entry.matching_pattern(),
2706 Some(".*/version-([\\d.]+)\\.tar\\.xz".into())
2707 );
2708
2709 assert_eq!(entry.url(), "https://example.com/releases");
2711 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2712 assert_eq!(entry.script(), Some("uupdate".into()));
2713 assert_eq!(entry.compression(), Ok(Some(super::Compression::Xz)));
2714
2715 assert_eq!(
2717 entry.to_string(),
2718 "opts=compression=xz https://example.com/releases .*/version-([\\d.]+)\\.tar\\.xz debian uupdate\n"
2719 );
2720 }
2721
2722 #[test]
2723 fn test_set_version_policy() {
2724 let wf: super::WatchFile = r#"version=4
2726https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2727"#
2728 .parse()
2729 .unwrap();
2730
2731 let mut entry = wf.entries().next().unwrap();
2732 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2733
2734 entry.set_version_policy("previous");
2735 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Previous)));
2736
2737 assert_eq!(entry.url(), "https://example.com/releases");
2739 assert_eq!(
2740 entry.matching_pattern(),
2741 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2742 );
2743 assert_eq!(entry.script(), Some("uupdate".into()));
2744
2745 assert_eq!(
2747 entry.to_string(),
2748 "https://example.com/releases (?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz previous uupdate\n"
2749 );
2750 }
2751
2752 #[test]
2753 fn test_set_version_policy_with_options() {
2754 let wf: super::WatchFile = r#"version=4
2756opts=repack,compression=xz \
2757 https://github.com/example/example-cat/tags \
2758 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2759"#
2760 .parse()
2761 .unwrap();
2762
2763 let mut entry = wf.entries().next().unwrap();
2764 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2765
2766 entry.set_version_policy("ignore");
2767 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Ignore)));
2768
2769 assert_eq!(entry.url(), "https://github.com/example/example-cat/tags");
2771 assert_eq!(
2772 entry.matching_pattern(),
2773 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2774 );
2775 assert_eq!(entry.script(), Some("uupdate".into()));
2776 assert!(entry.repack());
2777
2778 assert_eq!(
2780 entry.to_string(),
2781 r#"opts=repack,compression=xz \
2782 https://github.com/example/example-cat/tags \
2783 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz ignore uupdate
2784"#
2785 );
2786 }
2787
2788 #[test]
2789 fn test_set_script() {
2790 let wf: super::WatchFile = r#"version=4
2792https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2793"#
2794 .parse()
2795 .unwrap();
2796
2797 let mut entry = wf.entries().next().unwrap();
2798 assert_eq!(entry.script(), Some("uupdate".into()));
2799
2800 entry.set_script("uscan");
2801 assert_eq!(entry.script(), Some("uscan".into()));
2802
2803 assert_eq!(entry.url(), "https://example.com/releases");
2805 assert_eq!(
2806 entry.matching_pattern(),
2807 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2808 );
2809 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2810
2811 assert_eq!(
2813 entry.to_string(),
2814 "https://example.com/releases (?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz debian uscan\n"
2815 );
2816 }
2817
2818 #[test]
2819 fn test_set_script_with_options() {
2820 let wf: super::WatchFile = r#"version=4
2822opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2823"#
2824 .parse()
2825 .unwrap();
2826
2827 let mut entry = wf.entries().next().unwrap();
2828 assert_eq!(entry.script(), Some("uupdate".into()));
2829
2830 entry.set_script("custom-script.sh");
2831 assert_eq!(entry.script(), Some("custom-script.sh".into()));
2832
2833 assert_eq!(entry.url(), "https://example.com/releases");
2835 assert_eq!(
2836 entry.matching_pattern(),
2837 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2838 );
2839 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2840 assert_eq!(entry.compression(), Ok(Some(super::Compression::Xz)));
2841
2842 assert_eq!(
2844 entry.to_string(),
2845 "opts=compression=xz https://example.com/releases (?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz debian custom-script.sh\n"
2846 );
2847 }
2848
2849 #[test]
2850 fn test_apply_dversionmangle() {
2851 let wf: super::WatchFile = r#"version=4
2853opts=dversionmangle=s/\+dfsg$// https://example.com/ .*
2854"#
2855 .parse()
2856 .unwrap();
2857 let entry = wf.entries().next().unwrap();
2858 assert_eq!(entry.apply_dversionmangle("1.0+dfsg").unwrap(), "1.0");
2859 assert_eq!(entry.apply_dversionmangle("1.0").unwrap(), "1.0");
2860
2861 let wf: super::WatchFile = r#"version=4
2863opts=versionmangle=s/^v// https://example.com/ .*
2864"#
2865 .parse()
2866 .unwrap();
2867 let entry = wf.entries().next().unwrap();
2868 assert_eq!(entry.apply_dversionmangle("v1.0").unwrap(), "1.0");
2869
2870 let wf: super::WatchFile = r#"version=4
2872opts=dversionmangle=s/\+ds//,versionmangle=s/^v// https://example.com/ .*
2873"#
2874 .parse()
2875 .unwrap();
2876 let entry = wf.entries().next().unwrap();
2877 assert_eq!(entry.apply_dversionmangle("1.0+ds").unwrap(), "1.0");
2878
2879 let wf: super::WatchFile = r#"version=4
2881https://example.com/ .*
2882"#
2883 .parse()
2884 .unwrap();
2885 let entry = wf.entries().next().unwrap();
2886 assert_eq!(entry.apply_dversionmangle("1.0+dfsg").unwrap(), "1.0+dfsg");
2887 }
2888
2889 #[test]
2890 fn test_apply_oversionmangle() {
2891 let wf: super::WatchFile = r#"version=4
2893opts=oversionmangle=s/$/-1/ https://example.com/ .*
2894"#
2895 .parse()
2896 .unwrap();
2897 let entry = wf.entries().next().unwrap();
2898 assert_eq!(entry.apply_oversionmangle("1.0").unwrap(), "1.0-1");
2899 assert_eq!(entry.apply_oversionmangle("2.5.3").unwrap(), "2.5.3-1");
2900
2901 let wf: super::WatchFile = r#"version=4
2903opts=oversionmangle=s/$/.dfsg/ https://example.com/ .*
2904"#
2905 .parse()
2906 .unwrap();
2907 let entry = wf.entries().next().unwrap();
2908 assert_eq!(entry.apply_oversionmangle("1.0").unwrap(), "1.0.dfsg");
2909
2910 let wf: super::WatchFile = r#"version=4
2912https://example.com/ .*
2913"#
2914 .parse()
2915 .unwrap();
2916 let entry = wf.entries().next().unwrap();
2917 assert_eq!(entry.apply_oversionmangle("1.0").unwrap(), "1.0");
2918 }
2919
2920 #[test]
2921 fn test_apply_dirversionmangle() {
2922 let wf: super::WatchFile = r#"version=4
2924opts=dirversionmangle=s/^v// https://example.com/ .*
2925"#
2926 .parse()
2927 .unwrap();
2928 let entry = wf.entries().next().unwrap();
2929 assert_eq!(entry.apply_dirversionmangle("v1.0").unwrap(), "1.0");
2930 assert_eq!(entry.apply_dirversionmangle("v2.5.3").unwrap(), "2.5.3");
2931
2932 let wf: super::WatchFile = r#"version=4
2934opts=dirversionmangle=s/v(\d)/$1/ https://example.com/ .*
2935"#
2936 .parse()
2937 .unwrap();
2938 let entry = wf.entries().next().unwrap();
2939 assert_eq!(entry.apply_dirversionmangle("v1.0").unwrap(), "1.0");
2940
2941 let wf: super::WatchFile = r#"version=4
2943https://example.com/ .*
2944"#
2945 .parse()
2946 .unwrap();
2947 let entry = wf.entries().next().unwrap();
2948 assert_eq!(entry.apply_dirversionmangle("v1.0").unwrap(), "v1.0");
2949 }
2950
2951 #[test]
2952 fn test_apply_filenamemangle() {
2953 let wf: super::WatchFile = r#"version=4
2955opts=filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/mypackage-$1.tar.gz/ https://example.com/ .*
2956"#
2957 .parse()
2958 .unwrap();
2959 let entry = wf.entries().next().unwrap();
2960 assert_eq!(
2961 entry
2962 .apply_filenamemangle("https://example.com/v1.0.tar.gz")
2963 .unwrap(),
2964 "mypackage-1.0.tar.gz"
2965 );
2966 assert_eq!(
2967 entry
2968 .apply_filenamemangle("https://example.com/2.5.3.tar.gz")
2969 .unwrap(),
2970 "mypackage-2.5.3.tar.gz"
2971 );
2972
2973 let wf: super::WatchFile = r#"version=4
2975opts=filenamemangle=s/.*\/(.*)/$1/ https://example.com/ .*
2976"#
2977 .parse()
2978 .unwrap();
2979 let entry = wf.entries().next().unwrap();
2980 assert_eq!(
2981 entry
2982 .apply_filenamemangle("https://example.com/path/to/file.tar.gz")
2983 .unwrap(),
2984 "file.tar.gz"
2985 );
2986
2987 let wf: super::WatchFile = r#"version=4
2989https://example.com/ .*
2990"#
2991 .parse()
2992 .unwrap();
2993 let entry = wf.entries().next().unwrap();
2994 assert_eq!(
2995 entry
2996 .apply_filenamemangle("https://example.com/file.tar.gz")
2997 .unwrap(),
2998 "https://example.com/file.tar.gz"
2999 );
3000 }
3001
3002 #[test]
3003 fn test_apply_pagemangle() {
3004 let wf: super::WatchFile = r#"version=4
3006opts=pagemangle=s/&/&/g https://example.com/ .*
3007"#
3008 .parse()
3009 .unwrap();
3010 let entry = wf.entries().next().unwrap();
3011 assert_eq!(
3012 entry.apply_pagemangle(b"foo & bar").unwrap(),
3013 b"foo & bar"
3014 );
3015 assert_eq!(
3016 entry
3017 .apply_pagemangle(b"& foo & bar &")
3018 .unwrap(),
3019 b"& foo & bar &"
3020 );
3021
3022 let wf: super::WatchFile = r#"version=4
3024opts=pagemangle=s/<[^>]+>//g https://example.com/ .*
3025"#
3026 .parse()
3027 .unwrap();
3028 let entry = wf.entries().next().unwrap();
3029 assert_eq!(entry.apply_pagemangle(b"<div>text</div>").unwrap(), b"text");
3030
3031 let wf: super::WatchFile = r#"version=4
3033https://example.com/ .*
3034"#
3035 .parse()
3036 .unwrap();
3037 let entry = wf.entries().next().unwrap();
3038 assert_eq!(
3039 entry.apply_pagemangle(b"foo & bar").unwrap(),
3040 b"foo & bar"
3041 );
3042 }
3043
3044 #[test]
3045 fn test_apply_downloadurlmangle() {
3046 let wf: super::WatchFile = r#"version=4
3048opts=downloadurlmangle=s|/archive/|/download/| https://example.com/ .*
3049"#
3050 .parse()
3051 .unwrap();
3052 let entry = wf.entries().next().unwrap();
3053 assert_eq!(
3054 entry
3055 .apply_downloadurlmangle("https://example.com/archive/file.tar.gz")
3056 .unwrap(),
3057 "https://example.com/download/file.tar.gz"
3058 );
3059
3060 let wf: super::WatchFile = r#"version=4
3062opts=downloadurlmangle=s/github\.com/raw.githubusercontent.com/ https://example.com/ .*
3063"#
3064 .parse()
3065 .unwrap();
3066 let entry = wf.entries().next().unwrap();
3067 assert_eq!(
3068 entry
3069 .apply_downloadurlmangle("https://github.com/user/repo/file.tar.gz")
3070 .unwrap(),
3071 "https://raw.githubusercontent.com/user/repo/file.tar.gz"
3072 );
3073
3074 let wf: super::WatchFile = r#"version=4
3076https://example.com/ .*
3077"#
3078 .parse()
3079 .unwrap();
3080 let entry = wf.entries().next().unwrap();
3081 assert_eq!(
3082 entry
3083 .apply_downloadurlmangle("https://example.com/archive/file.tar.gz")
3084 .unwrap(),
3085 "https://example.com/archive/file.tar.gz"
3086 );
3087 }
3088
3089 #[test]
3090 fn test_entry_builder_minimal() {
3091 let entry = super::EntryBuilder::new("https://github.com/example/tags")
3093 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3094 .build();
3095
3096 assert_eq!(entry.url(), "https://github.com/example/tags");
3097 assert_eq!(
3098 entry.matching_pattern().as_deref(),
3099 Some(".*/v?(\\d\\S+)\\.tar\\.gz")
3100 );
3101 assert_eq!(entry.version(), Ok(None));
3102 assert_eq!(entry.script(), None);
3103 assert!(entry.opts().is_empty());
3104 }
3105
3106 #[test]
3107 fn test_entry_builder_url_only() {
3108 let entry = super::EntryBuilder::new("https://example.com/releases").build();
3110
3111 assert_eq!(entry.url(), "https://example.com/releases");
3112 assert_eq!(entry.matching_pattern(), None);
3113 assert_eq!(entry.version(), Ok(None));
3114 assert_eq!(entry.script(), None);
3115 assert!(entry.opts().is_empty());
3116 }
3117
3118 #[test]
3119 fn test_entry_builder_with_all_fields() {
3120 let entry = super::EntryBuilder::new("https://github.com/example/tags")
3122 .matching_pattern(".*/v?(\\d[\\d.]*)\\.tar\\.gz")
3123 .version_policy("debian")
3124 .script("uupdate")
3125 .opt("compression", "xz")
3126 .flag("repack")
3127 .build();
3128
3129 assert_eq!(entry.url(), "https://github.com/example/tags");
3130 assert_eq!(
3131 entry.matching_pattern().as_deref(),
3132 Some(".*/v?(\\d[\\d.]*)\\.tar\\.gz")
3133 );
3134 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
3135 assert_eq!(entry.script(), Some("uupdate".into()));
3136 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
3137 assert!(entry.has_option("repack"));
3138 assert!(entry.repack());
3139 }
3140
3141 #[test]
3142 fn test_entry_builder_multiple_options() {
3143 let entry = super::EntryBuilder::new("https://example.com/tags")
3145 .matching_pattern(".*/v?(\\d+\\.\\d+)\\.tar\\.gz")
3146 .opt("compression", "xz")
3147 .opt("dversionmangle", "s/\\+ds//")
3148 .opt("repacksuffix", "+ds")
3149 .build();
3150
3151 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
3152 assert_eq!(
3153 entry.get_option("dversionmangle"),
3154 Some("s/\\+ds//".to_string())
3155 );
3156 assert_eq!(entry.get_option("repacksuffix"), Some("+ds".to_string()));
3157 }
3158
3159 #[test]
3160 fn test_entry_builder_via_entry() {
3161 let entry = super::Entry::builder("https://github.com/example/tags")
3163 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3164 .version_policy("debian")
3165 .build();
3166
3167 assert_eq!(entry.url(), "https://github.com/example/tags");
3168 assert_eq!(
3169 entry.matching_pattern().as_deref(),
3170 Some(".*/v?(\\d\\S+)\\.tar\\.gz")
3171 );
3172 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
3173 }
3174
3175 #[test]
3176 fn test_watchfile_add_entry_to_empty() {
3177 let mut wf = super::WatchFile::new(Some(4));
3179
3180 let entry = super::EntryBuilder::new("https://github.com/example/tags")
3181 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3182 .build();
3183
3184 wf.add_entry(entry);
3185
3186 assert_eq!(wf.version(), 4);
3187 assert_eq!(wf.entries().count(), 1);
3188
3189 let added_entry = wf.entries().next().unwrap();
3190 assert_eq!(added_entry.url(), "https://github.com/example/tags");
3191 assert_eq!(
3192 added_entry.matching_pattern().as_deref(),
3193 Some(".*/v?(\\d\\S+)\\.tar\\.gz")
3194 );
3195 }
3196
3197 #[test]
3198 fn test_watchfile_add_multiple_entries() {
3199 let mut wf = super::WatchFile::new(Some(4));
3201
3202 wf.add_entry(
3203 super::EntryBuilder::new("https://github.com/example1/tags")
3204 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3205 .build(),
3206 );
3207
3208 wf.add_entry(
3209 super::EntryBuilder::new("https://github.com/example2/releases")
3210 .matching_pattern(".*/(\\d+\\.\\d+)\\.tar\\.gz")
3211 .opt("compression", "xz")
3212 .build(),
3213 );
3214
3215 assert_eq!(wf.entries().count(), 2);
3216
3217 let entries: Vec<_> = wf.entries().collect();
3218 assert_eq!(entries[0].url(), "https://github.com/example1/tags");
3219 assert_eq!(entries[1].url(), "https://github.com/example2/releases");
3220 assert_eq!(entries[1].get_option("compression"), Some("xz".to_string()));
3221 }
3222
3223 #[test]
3224 fn test_watchfile_add_entry_to_existing() {
3225 let mut wf: super::WatchFile = r#"version=4
3227https://example.com/old .*/v?(\\d\\S+)\\.tar\\.gz
3228"#
3229 .parse()
3230 .unwrap();
3231
3232 assert_eq!(wf.entries().count(), 1);
3233
3234 wf.add_entry(
3235 super::EntryBuilder::new("https://github.com/example/new")
3236 .matching_pattern(".*/v?(\\d+\\.\\d+)\\.tar\\.gz")
3237 .opt("compression", "xz")
3238 .version_policy("debian")
3239 .build(),
3240 );
3241
3242 assert_eq!(wf.entries().count(), 2);
3243
3244 let entries: Vec<_> = wf.entries().collect();
3245 assert_eq!(entries[0].url(), "https://example.com/old");
3246 assert_eq!(entries[1].url(), "https://github.com/example/new");
3247 assert_eq!(entries[1].version(), Ok(Some(VersionPolicy::Debian)));
3248 }
3249
3250 #[test]
3251 fn test_entry_builder_formatting() {
3252 let entry = super::EntryBuilder::new("https://github.com/example/tags")
3254 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3255 .opt("compression", "xz")
3256 .flag("repack")
3257 .version_policy("debian")
3258 .script("uupdate")
3259 .build();
3260
3261 let entry_str = entry.to_string();
3262
3263 assert!(entry_str.starts_with("opts="));
3265 assert!(entry_str.contains("https://github.com/example/tags"));
3267 assert!(entry_str.contains(".*/v?(\\d\\S+)\\.tar\\.gz"));
3269 assert!(entry_str.contains("debian"));
3271 assert!(entry_str.contains("uupdate"));
3273 assert!(entry_str.ends_with('\n'));
3275 }
3276
3277 #[test]
3278 fn test_watchfile_add_entry_preserves_format() {
3279 let mut wf = super::WatchFile::new(Some(4));
3281
3282 wf.add_entry(
3283 super::EntryBuilder::new("https://github.com/example/tags")
3284 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3285 .build(),
3286 );
3287
3288 let wf_str = wf.to_string();
3289
3290 assert!(wf_str.starts_with("version=4\n"));
3292 assert!(wf_str.contains("https://github.com/example/tags"));
3294
3295 let reparsed: super::WatchFile = wf_str.parse().unwrap();
3297 assert_eq!(reparsed.version(), 4);
3298 assert_eq!(reparsed.entries().count(), 1);
3299 }
3300
3301 #[test]
3302 fn test_line_col() {
3303 let text = r#"version=4
3304opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
3305"#;
3306 let wf = text.parse::<super::WatchFile>().unwrap();
3307
3308 let version_node = wf.version_node().unwrap();
3310 assert_eq!(version_node.line(), 0);
3311 assert_eq!(version_node.column(), 0);
3312 assert_eq!(version_node.line_col(), (0, 0));
3313
3314 let entries: Vec<_> = wf.entries().collect();
3316 assert_eq!(entries.len(), 1);
3317
3318 assert_eq!(entries[0].line(), 1);
3320 assert_eq!(entries[0].column(), 0);
3321 assert_eq!(entries[0].line_col(), (1, 0));
3322
3323 let option_list = entries[0].option_list().unwrap();
3325 assert_eq!(option_list.line(), 1); let url_node = entries[0].url_node().unwrap();
3328 assert_eq!(url_node.line(), 1); let pattern_node = entries[0].matching_pattern_node().unwrap();
3331 assert_eq!(pattern_node.line(), 1); let version_policy_node = entries[0].version_node().unwrap();
3334 assert_eq!(version_policy_node.line(), 1); let script_node = entries[0].script_node().unwrap();
3337 assert_eq!(script_node.line(), 1); let options: Vec<_> = option_list.options().collect();
3341 assert_eq!(options.len(), 1);
3342 assert_eq!(options[0].key(), Some("compression".to_string()));
3343 assert_eq!(options[0].value(), Some("xz".to_string()));
3344 assert_eq!(options[0].line(), 1); let compression_opt = option_list.find_option("compression").unwrap();
3348 assert_eq!(compression_opt.line(), 1);
3349 assert_eq!(compression_opt.column(), 5); assert_eq!(compression_opt.line_col(), (1, 5));
3351 }
3352
3353 #[test]
3354 fn test_parse_str_relaxed() {
3355 let wf: super::WatchFile = super::WatchFile::from_str_relaxed(
3356 r#"version=4
3357ERRORS IN THIS LINE
3358opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d
3359"#,
3360 );
3361 assert_eq!(wf.version(), 4);
3362 assert_eq!(wf.entries().count(), 2);
3363
3364 let entries = wf.entries().collect::<Vec<_>>();
3365
3366 let entry = &entries[0];
3367 assert_eq!(entry.url(), "ERRORS");
3368
3369 let entry = &entries[1];
3370 assert_eq!(entry.url(), "https://example.com/releases");
3371 assert_eq!(entry.matching_pattern().as_deref(), Some("(?:.*?/)?v?(\\d"));
3372 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
3373 }
3374}