1use crate::lex::lex;
2use crate::types::{ComponentType, Compression, GitExport, GitMode, Mode, PgpMode, Pretty, SearchMode};
3use crate::SyntaxKind;
4use crate::SyntaxKind::*;
5use crate::DEFAULT_VERSION;
6use std::io::Read;
7use std::marker::PhantomData;
8use std::str::FromStr;
9
10#[cfg(test)]
11use crate::types::VersionPolicy;
12
13#[derive(Debug, Clone, PartialEq, Eq, Hash)]
14pub struct ParseError(pub Vec<String>);
16
17impl std::fmt::Display for ParseError {
18 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
19 for err in &self.0 {
20 writeln!(f, "{}", err)?;
21 }
22 Ok(())
23 }
24}
25
26impl std::error::Error for ParseError {}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
32pub(crate) enum Lang {}
33impl rowan::Language for Lang {
34 type Kind = SyntaxKind;
35 fn kind_from_raw(raw: rowan::SyntaxKind) -> Self::Kind {
36 unsafe { std::mem::transmute::<u16, SyntaxKind>(raw.0) }
37 }
38 fn kind_to_raw(kind: Self::Kind) -> rowan::SyntaxKind {
39 kind.into()
40 }
41}
42
43use rowan::GreenNode;
46
47use rowan::GreenNodeBuilder;
51
52#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct Parse<T> {
56 green: GreenNode,
58 errors: Vec<String>,
60 _ty: PhantomData<T>,
62}
63
64impl<T> Parse<T> {
65 pub(crate) fn new(green: GreenNode, errors: Vec<String>) -> Self {
67 Parse {
68 green,
69 errors,
70 _ty: PhantomData,
71 }
72 }
73
74 pub fn green(&self) -> &GreenNode {
76 &self.green
77 }
78
79 pub fn errors(&self) -> &[String] {
81 &self.errors
82 }
83
84 pub fn is_ok(&self) -> bool {
86 self.errors.is_empty()
87 }
88}
89
90impl Parse<WatchFile> {
91 pub fn tree(&self) -> WatchFile {
93 WatchFile::cast(SyntaxNode::new_root_mut(self.green.clone()))
94 .expect("root node should be a WatchFile")
95 }
96}
97
98struct InternalParse {
100 green_node: GreenNode,
101 errors: Vec<String>,
102}
103
104fn parse(text: &str) -> InternalParse {
105 struct Parser {
106 tokens: Vec<(SyntaxKind, String)>,
109 builder: GreenNodeBuilder<'static>,
111 errors: Vec<String>,
114 }
115
116 impl Parser {
117 fn parse_version(&mut self) -> Option<u32> {
118 let mut version = None;
119 if self.tokens.last() == Some(&(KEY, "version".to_string())) {
120 self.builder.start_node(VERSION.into());
121 self.bump();
122 self.skip_ws();
123 if self.current() != Some(EQUALS) {
124 self.builder.start_node(ERROR.into());
125 self.errors.push("expected `=`".to_string());
126 self.bump();
127 self.builder.finish_node();
128 } else {
129 self.bump();
130 }
131 if self.current() != Some(VALUE) {
132 self.builder.start_node(ERROR.into());
133 self.errors
134 .push(format!("expected value, got {:?}", self.current()));
135 self.bump();
136 self.builder.finish_node();
137 } else if let Some((_, value)) = self.tokens.last() {
138 let version_str = value;
139 match version_str.parse() {
140 Ok(v) => {
141 version = Some(v);
142 self.bump();
143 }
144 Err(_) => {
145 self.builder.start_node(ERROR.into());
146 self.errors
147 .push(format!("invalid version: {}", version_str));
148 self.bump();
149 self.builder.finish_node();
150 }
151 }
152 } else {
153 self.builder.start_node(ERROR.into());
154 self.errors.push("expected version value".to_string());
155 self.builder.finish_node();
156 }
157 if self.current() != Some(NEWLINE) {
158 self.builder.start_node(ERROR.into());
159 self.errors.push("expected newline".to_string());
160 self.bump();
161 self.builder.finish_node();
162 } else {
163 self.bump();
164 }
165 self.builder.finish_node();
166 }
167 version
168 }
169
170 fn parse_watch_entry(&mut self) -> bool {
171 self.skip_ws();
172 if self.current().is_none() {
173 return false;
174 }
175 if self.current() == Some(NEWLINE) {
176 self.bump();
177 return false;
178 }
179 self.builder.start_node(ENTRY.into());
180 self.parse_options_list();
181 for i in 0..4 {
182 if self.current() == Some(NEWLINE) || self.current().is_none() {
183 break;
184 }
185 if self.current() == Some(CONTINUATION) {
186 self.bump();
187 self.skip_ws();
188 continue;
189 }
190 if self.current() != Some(VALUE) && self.current() != Some(KEY) {
191 self.builder.start_node(ERROR.into());
192 self.errors.push(format!(
193 "expected value, got {:?} (i={})",
194 self.current(),
195 i
196 ));
197 if self.current().is_some() {
198 self.bump();
199 }
200 self.builder.finish_node();
201 } else {
202 match i {
204 0 => {
205 self.builder.start_node(URL.into());
207 self.bump();
208 self.builder.finish_node();
209 }
210 1 => {
211 self.builder.start_node(MATCHING_PATTERN.into());
213 self.bump();
214 self.builder.finish_node();
215 }
216 2 => {
217 self.builder.start_node(VERSION_POLICY.into());
219 self.bump();
220 self.builder.finish_node();
221 }
222 3 => {
223 self.builder.start_node(SCRIPT.into());
225 self.bump();
226 self.builder.finish_node();
227 }
228 _ => {
229 self.bump();
230 }
231 }
232 }
233 self.skip_ws();
234 }
235 if self.current() != Some(NEWLINE) && self.current().is_some() {
236 self.builder.start_node(ERROR.into());
237 self.errors
238 .push(format!("expected newline, not {:?}", self.current()));
239 if self.current().is_some() {
240 self.bump();
241 }
242 self.builder.finish_node();
243 } else if self.current().is_some() {
244 self.bump();
246 }
247 self.builder.finish_node();
248 true
249 }
250
251 fn parse_option(&mut self) -> bool {
252 if self.current().is_none() {
253 return false;
254 }
255 while self.current() == Some(CONTINUATION) {
256 self.bump();
257 }
258 if self.current() == Some(WHITESPACE) {
259 return false;
260 }
261 self.builder.start_node(OPTION.into());
262 if self.current() != Some(KEY) {
263 self.builder.start_node(ERROR.into());
264 self.errors.push("expected key".to_string());
265 self.bump();
266 self.builder.finish_node();
267 } else {
268 self.bump();
269 }
270 if self.current() == Some(EQUALS) {
271 self.bump();
272 if self.current() != Some(VALUE) && self.current() != Some(KEY) {
273 self.builder.start_node(ERROR.into());
274 self.errors
275 .push(format!("expected value, got {:?}", self.current()));
276 self.bump();
277 self.builder.finish_node();
278 } else {
279 self.bump();
280 }
281 } else if self.current() == Some(COMMA) {
282 } else {
283 self.builder.start_node(ERROR.into());
284 self.errors.push("expected `=`".to_string());
285 if self.current().is_some() {
286 self.bump();
287 }
288 self.builder.finish_node();
289 }
290 self.builder.finish_node();
291 true
292 }
293
294 fn parse_options_list(&mut self) {
295 self.skip_ws();
296 if self.tokens.last() == Some(&(KEY, "opts".to_string()))
297 || self.tokens.last() == Some(&(KEY, "options".to_string()))
298 {
299 self.builder.start_node(OPTS_LIST.into());
300 self.bump();
301 self.skip_ws();
302 if self.current() != Some(EQUALS) {
303 self.builder.start_node(ERROR.into());
304 self.errors.push("expected `=`".to_string());
305 if self.current().is_some() {
306 self.bump();
307 }
308 self.builder.finish_node();
309 } else {
310 self.bump();
311 }
312 let quoted = if self.current() == Some(QUOTE) {
313 self.bump();
314 true
315 } else {
316 false
317 };
318 loop {
319 if quoted {
320 if self.current() == Some(QUOTE) {
321 self.bump();
322 break;
323 }
324 self.skip_ws();
325 }
326 if !self.parse_option() {
327 break;
328 }
329 if self.current() == Some(COMMA) {
330 self.builder.start_node(OPTION_SEPARATOR.into());
331 self.bump();
332 self.builder.finish_node();
333 } else if !quoted {
334 break;
335 }
336 }
337 self.builder.finish_node();
338 self.skip_ws();
339 }
340 }
341
342 fn parse(mut self) -> InternalParse {
343 self.builder.start_node(ROOT.into());
345 while self.current() == Some(WHITESPACE)
347 || self.current() == Some(CONTINUATION)
348 || self.current() == Some(COMMENT)
349 || self.current() == Some(NEWLINE)
350 {
351 self.bump();
352 }
353 if let Some(_v) = self.parse_version() {
354 }
356 loop {
358 if !self.parse_watch_entry() {
359 break;
360 }
361 }
362 self.skip_ws();
364 self.builder.finish_node();
366
367 InternalParse {
369 green_node: self.builder.finish(),
370 errors: self.errors,
371 }
372 }
373 fn bump(&mut self) {
375 if let Some((kind, text)) = self.tokens.pop() {
376 self.builder.token(kind.into(), text.as_str());
377 }
378 }
379 fn current(&self) -> Option<SyntaxKind> {
381 self.tokens.last().map(|(kind, _)| *kind)
382 }
383 fn skip_ws(&mut self) {
384 while self.current() == Some(WHITESPACE)
385 || self.current() == Some(CONTINUATION)
386 || self.current() == Some(COMMENT)
387 {
388 self.bump()
389 }
390 }
391 }
392
393 let mut tokens = lex(text);
394 tokens.reverse();
395 Parser {
396 tokens,
397 builder: GreenNodeBuilder::new(),
398 errors: Vec::new(),
399 }
400 .parse()
401}
402
403type SyntaxNode = rowan::SyntaxNode<Lang>;
409#[allow(unused)]
410type SyntaxToken = rowan::SyntaxToken<Lang>;
411#[allow(unused)]
412type SyntaxElement = rowan::NodeOrToken<SyntaxNode, SyntaxToken>;
413
414impl InternalParse {
415 fn syntax(&self) -> SyntaxNode {
416 SyntaxNode::new_root_mut(self.green_node.clone())
417 }
418
419 fn root(&self) -> WatchFile {
420 WatchFile::cast(self.syntax()).expect("root node should be a WatchFile")
421 }
422}
423
424fn line_col_at_offset(node: &SyntaxNode, offset: rowan::TextSize) -> (usize, usize) {
427 let root = node.ancestors().last().unwrap_or_else(|| node.clone());
428 let mut line = 0;
429 let mut last_newline_offset = rowan::TextSize::from(0);
430
431 for element in root.preorder_with_tokens() {
432 if let rowan::WalkEvent::Enter(rowan::NodeOrToken::Token(token)) = element {
433 if token.text_range().start() >= offset {
434 break;
435 }
436
437 for (idx, _) in token.text().match_indices('\n') {
439 line += 1;
440 last_newline_offset =
441 token.text_range().start() + rowan::TextSize::from((idx + 1) as u32);
442 }
443 }
444 }
445
446 let column: usize = (offset - last_newline_offset).into();
447 (line, column)
448}
449
450macro_rules! ast_node {
451 ($ast:ident, $kind:ident) => {
452 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
453 #[repr(transparent)]
454 pub struct $ast(SyntaxNode);
456 impl $ast {
457 #[allow(unused)]
458 fn cast(node: SyntaxNode) -> Option<Self> {
459 if node.kind() == $kind {
460 Some(Self(node))
461 } else {
462 None
463 }
464 }
465
466 pub fn line(&self) -> usize {
468 line_col_at_offset(&self.0, self.0.text_range().start()).0
469 }
470
471 pub fn column(&self) -> usize {
473 line_col_at_offset(&self.0, self.0.text_range().start()).1
474 }
475
476 pub fn line_col(&self) -> (usize, usize) {
479 line_col_at_offset(&self.0, self.0.text_range().start())
480 }
481 }
482
483 impl std::fmt::Display for $ast {
484 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
485 write!(f, "{}", self.0.text())
486 }
487 }
488 };
489}
490
491ast_node!(WatchFile, ROOT);
492ast_node!(Version, VERSION);
493ast_node!(Entry, ENTRY);
494ast_node!(_Option, OPTION);
495ast_node!(Url, URL);
496ast_node!(MatchingPattern, MATCHING_PATTERN);
497ast_node!(VersionPolicyNode, VERSION_POLICY);
498ast_node!(ScriptNode, SCRIPT);
499
500#[derive(Clone, PartialEq, Eq, Hash)]
502#[repr(transparent)]
503pub struct OptionList(SyntaxNode);
505
506impl OptionList {
507 #[allow(unused)]
508 fn cast(node: SyntaxNode) -> Option<Self> {
509 if node.kind() == OPTS_LIST {
510 Some(Self(node))
511 } else {
512 None
513 }
514 }
515
516 pub fn line(&self) -> usize {
518 line_col_at_offset(&self.0, self.0.text_range().start()).0
519 }
520
521 pub fn column(&self) -> usize {
523 line_col_at_offset(&self.0, self.0.text_range().start()).1
524 }
525
526 pub fn line_col(&self) -> (usize, usize) {
529 line_col_at_offset(&self.0, self.0.text_range().start())
530 }
531}
532
533impl std::fmt::Display for OptionList {
534 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
535 write!(f, "{}", self.0.text())
536 }
537}
538
539impl WatchFile {
540 #[cfg(feature = "deb822")]
542 pub(crate) fn syntax(&self) -> &SyntaxNode {
543 &self.0
544 }
545
546 pub fn new(version: Option<u32>) -> WatchFile {
548 let mut builder = GreenNodeBuilder::new();
549
550 builder.start_node(ROOT.into());
551 if let Some(version) = version {
552 builder.start_node(VERSION.into());
553 builder.token(KEY.into(), "version");
554 builder.token(EQUALS.into(), "=");
555 builder.token(VALUE.into(), version.to_string().as_str());
556 builder.token(NEWLINE.into(), "\n");
557 builder.finish_node();
558 }
559 builder.finish_node();
560 WatchFile(SyntaxNode::new_root_mut(builder.finish()))
561 }
562
563 pub fn version_node(&self) -> Option<Version> {
565 self.0.children().find_map(Version::cast)
566 }
567
568 pub fn version(&self) -> u32 {
570 self.version_node()
571 .map(|it| it.version())
572 .unwrap_or(DEFAULT_VERSION)
573 }
574
575 pub fn entries(&self) -> impl Iterator<Item = Entry> + '_ {
577 self.0.children().filter_map(Entry::cast)
578 }
579
580 pub fn set_version(&mut self, new_version: u32) {
582 let mut builder = GreenNodeBuilder::new();
584 builder.start_node(VERSION.into());
585 builder.token(KEY.into(), "version");
586 builder.token(EQUALS.into(), "=");
587 builder.token(VALUE.into(), new_version.to_string().as_str());
588 builder.token(NEWLINE.into(), "\n");
589 builder.finish_node();
590 let new_version_green = builder.finish();
591
592 let new_version_node = SyntaxNode::new_root_mut(new_version_green);
594
595 let version_pos = self.0.children().position(|child| child.kind() == VERSION);
597
598 if let Some(pos) = version_pos {
599 self.0
601 .splice_children(pos..pos + 1, vec![new_version_node.into()]);
602 } else {
603 self.0.splice_children(0..0, vec![new_version_node.into()]);
605 }
606 }
607
608 #[cfg(feature = "discover")]
628 pub async fn uscan(
629 &self,
630 package: impl Fn() -> String + Send + Sync,
631 ) -> Result<Vec<Vec<crate::Release>>, Box<dyn std::error::Error>> {
632 let mut all_releases = Vec::new();
633
634 for entry in self.entries() {
635 let parsed_entry = crate::parse::ParsedEntry::LineBased(entry);
636 let releases = parsed_entry.discover(|| package()).await?;
637 all_releases.push(releases);
638 }
639
640 Ok(all_releases)
641 }
642
643 #[cfg(all(feature = "discover", feature = "blocking"))]
661 pub fn uscan_blocking(
662 &self,
663 package: impl Fn() -> String,
664 ) -> Result<Vec<Vec<crate::Release>>, Box<dyn std::error::Error>> {
665 let mut all_releases = Vec::new();
666
667 for entry in self.entries() {
668 let parsed_entry = crate::parse::ParsedEntry::LineBased(entry);
669 let releases = parsed_entry.discover_blocking(|| package())?;
670 all_releases.push(releases);
671 }
672
673 Ok(all_releases)
674 }
675
676 pub fn add_entry(&mut self, entry: Entry) {
703 let insert_pos = self.0.children_with_tokens().count();
705
706 let entry_green = entry.0.green().into_owned();
708 let new_entry_node = SyntaxNode::new_root_mut(entry_green);
709
710 self.0
712 .splice_children(insert_pos..insert_pos, vec![new_entry_node.into()]);
713 }
714
715 pub fn from_reader<R: std::io::Read>(reader: R) -> Result<WatchFile, ParseError> {
717 let mut buf_reader = std::io::BufReader::new(reader);
718 let mut content = String::new();
719 buf_reader
720 .read_to_string(&mut content)
721 .map_err(|e| ParseError(vec![e.to_string()]))?;
722 content.parse()
723 }
724
725 pub fn from_reader_relaxed<R: std::io::Read>(mut r: R) -> Result<Self, std::io::Error> {
727 let mut content = String::new();
728 r.read_to_string(&mut content)?;
729 let parsed = parse(&content);
730 Ok(parsed.root())
731 }
732
733 pub fn from_str_relaxed(s: &str) -> Self {
735 let parsed = parse(s);
736 parsed.root()
737 }
738}
739
740impl FromStr for WatchFile {
741 type Err = ParseError;
742
743 fn from_str(s: &str) -> Result<Self, Self::Err> {
744 let parsed = parse(s);
745 if parsed.errors.is_empty() {
746 Ok(parsed.root())
747 } else {
748 Err(ParseError(parsed.errors))
749 }
750 }
751}
752
753pub fn parse_watch_file(text: &str) -> Parse<WatchFile> {
756 let parsed = parse(text);
757 Parse::new(parsed.green_node, parsed.errors)
758}
759
760impl Version {
761 pub fn version(&self) -> u32 {
763 self.0
764 .children_with_tokens()
765 .find_map(|it| match it {
766 SyntaxElement::Token(token) => {
767 if token.kind() == VALUE {
768 token.text().parse().ok()
769 } else {
770 None
771 }
772 }
773 _ => None,
774 })
775 .unwrap_or(DEFAULT_VERSION)
776 }
777}
778
779#[derive(Debug, Clone, Default)]
803pub struct EntryBuilder {
804 url: Option<String>,
805 matching_pattern: Option<String>,
806 version_policy: Option<String>,
807 script: Option<String>,
808 opts: std::collections::HashMap<String, String>,
809}
810
811impl EntryBuilder {
812 pub fn new(url: impl Into<String>) -> Self {
814 EntryBuilder {
815 url: Some(url.into()),
816 matching_pattern: None,
817 version_policy: None,
818 script: None,
819 opts: std::collections::HashMap::new(),
820 }
821 }
822
823 pub fn matching_pattern(mut self, pattern: impl Into<String>) -> Self {
825 self.matching_pattern = Some(pattern.into());
826 self
827 }
828
829 pub fn version_policy(mut self, policy: impl Into<String>) -> Self {
831 self.version_policy = Some(policy.into());
832 self
833 }
834
835 pub fn script(mut self, script: impl Into<String>) -> Self {
837 self.script = Some(script.into());
838 self
839 }
840
841 pub fn opt(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
843 self.opts.insert(key.into(), value.into());
844 self
845 }
846
847 pub fn flag(mut self, key: impl Into<String>) -> Self {
851 self.opts.insert(key.into(), String::new());
852 self
853 }
854
855 pub fn build(self) -> Entry {
861 let url = self.url.expect("URL is required for entry");
862
863 let mut builder = GreenNodeBuilder::new();
864
865 builder.start_node(ENTRY.into());
866
867 if !self.opts.is_empty() {
869 builder.start_node(OPTS_LIST.into());
870 builder.token(KEY.into(), "opts");
871 builder.token(EQUALS.into(), "=");
872
873 let mut first = true;
874 for (key, value) in self.opts.iter() {
875 if !first {
876 builder.token(COMMA.into(), ",");
877 }
878 first = false;
879
880 builder.start_node(OPTION.into());
881 builder.token(KEY.into(), key);
882 if !value.is_empty() {
883 builder.token(EQUALS.into(), "=");
884 builder.token(VALUE.into(), value);
885 }
886 builder.finish_node();
887 }
888
889 builder.finish_node();
890 builder.token(WHITESPACE.into(), " ");
891 }
892
893 builder.start_node(URL.into());
895 builder.token(VALUE.into(), &url);
896 builder.finish_node();
897
898 if let Some(pattern) = self.matching_pattern {
900 builder.token(WHITESPACE.into(), " ");
901 builder.start_node(MATCHING_PATTERN.into());
902 builder.token(VALUE.into(), &pattern);
903 builder.finish_node();
904 }
905
906 if let Some(policy) = self.version_policy {
908 builder.token(WHITESPACE.into(), " ");
909 builder.start_node(VERSION_POLICY.into());
910 builder.token(VALUE.into(), &policy);
911 builder.finish_node();
912 }
913
914 if let Some(script_val) = self.script {
916 builder.token(WHITESPACE.into(), " ");
917 builder.start_node(SCRIPT.into());
918 builder.token(VALUE.into(), &script_val);
919 builder.finish_node();
920 }
921
922 builder.token(NEWLINE.into(), "\n");
923 builder.finish_node();
924
925 Entry(SyntaxNode::new_root_mut(builder.finish()))
926 }
927}
928
929impl Entry {
930 #[cfg(feature = "deb822")]
932 pub(crate) fn syntax(&self) -> &SyntaxNode {
933 &self.0
934 }
935
936 pub fn builder(url: impl Into<String>) -> EntryBuilder {
950 EntryBuilder::new(url)
951 }
952
953 pub fn option_list(&self) -> Option<OptionList> {
955 self.0.children().find_map(OptionList::cast)
956 }
957
958 pub fn get_option(&self, key: &str) -> Option<String> {
960 self.option_list().and_then(|ol| ol.get_option(key))
961 }
962
963 pub fn has_option(&self, key: &str) -> bool {
965 self.option_list().is_some_and(|ol| ol.has_option(key))
966 }
967
968 pub fn component(&self) -> Option<String> {
970 self.get_option("component")
971 }
972
973 pub fn ctype(&self) -> Result<Option<ComponentType>, ()> {
975 self.try_ctype().map_err(|_| ())
976 }
977
978 pub fn try_ctype(&self) -> Result<Option<ComponentType>, crate::types::ParseError> {
980 self.get_option("ctype").map(|s| s.parse()).transpose()
981 }
982
983 pub fn compression(&self) -> Result<Option<Compression>, ()> {
985 self.try_compression().map_err(|_| ())
986 }
987
988 pub fn try_compression(&self) -> Result<Option<Compression>, crate::types::ParseError> {
990 self.get_option("compression")
991 .map(|s| s.parse())
992 .transpose()
993 }
994
995 pub fn repack(&self) -> bool {
997 self.has_option("repack")
998 }
999
1000 pub fn repacksuffix(&self) -> Option<String> {
1002 self.get_option("repacksuffix")
1003 }
1004
1005 pub fn mode(&self) -> Result<Mode, ()> {
1007 self.try_mode().map_err(|_| ())
1008 }
1009
1010 pub fn try_mode(&self) -> Result<Mode, crate::types::ParseError> {
1012 Ok(self
1013 .get_option("mode")
1014 .map(|s| s.parse())
1015 .transpose()?
1016 .unwrap_or_default())
1017 }
1018
1019 pub fn pretty(&self) -> Result<Pretty, ()> {
1021 self.try_pretty().map_err(|_| ())
1022 }
1023
1024 pub fn try_pretty(&self) -> Result<Pretty, crate::types::ParseError> {
1026 Ok(self
1027 .get_option("pretty")
1028 .map(|s| s.parse())
1029 .transpose()?
1030 .unwrap_or_default())
1031 }
1032
1033 pub fn date(&self) -> String {
1036 self.get_option("date").unwrap_or_else(|| "%Y%m%d".into())
1037 }
1038
1039 pub fn gitexport(&self) -> Result<GitExport, ()> {
1041 self.try_gitexport().map_err(|_| ())
1042 }
1043
1044 pub fn try_gitexport(&self) -> Result<GitExport, crate::types::ParseError> {
1046 Ok(self
1047 .get_option("gitexport")
1048 .map(|s| s.parse())
1049 .transpose()?
1050 .unwrap_or_default())
1051 }
1052
1053 pub fn gitmode(&self) -> Result<GitMode, ()> {
1055 self.try_gitmode().map_err(|_| ())
1056 }
1057
1058 pub fn try_gitmode(&self) -> Result<GitMode, crate::types::ParseError> {
1060 Ok(self
1061 .get_option("gitmode")
1062 .map(|s| s.parse())
1063 .transpose()?
1064 .unwrap_or_default())
1065 }
1066
1067 pub fn pgpmode(&self) -> Result<PgpMode, ()> {
1069 self.try_pgpmode().map_err(|_| ())
1070 }
1071
1072 pub fn try_pgpmode(&self) -> Result<PgpMode, crate::types::ParseError> {
1074 Ok(self
1075 .get_option("pgpmode")
1076 .map(|s| s.parse())
1077 .transpose()?
1078 .unwrap_or_default())
1079 }
1080
1081 pub fn searchmode(&self) -> Result<SearchMode, ()> {
1083 self.try_searchmode().map_err(|_| ())
1084 }
1085
1086 pub fn try_searchmode(&self) -> Result<SearchMode, crate::types::ParseError> {
1088 Ok(self
1089 .get_option("searchmode")
1090 .map(|s| s.parse())
1091 .transpose()?
1092 .unwrap_or_default())
1093 }
1094
1095 pub fn decompress(&self) -> bool {
1097 self.has_option("decompress")
1098 }
1099
1100 pub fn bare(&self) -> bool {
1103 self.has_option("bare")
1104 }
1105
1106 pub fn user_agent(&self) -> Option<String> {
1108 self.get_option("user-agent")
1109 }
1110
1111 pub fn passive(&self) -> Option<bool> {
1113 if self.has_option("passive") || self.has_option("pasv") {
1114 Some(true)
1115 } else if self.has_option("active") || self.has_option("nopasv") {
1116 Some(false)
1117 } else {
1118 None
1119 }
1120 }
1121
1122 pub fn unzipoptions(&self) -> Option<String> {
1125 self.get_option("unzipopt")
1126 }
1127
1128 pub fn dversionmangle(&self) -> Option<String> {
1130 self.get_option("dversionmangle")
1131 .or_else(|| self.get_option("versionmangle"))
1132 }
1133
1134 pub fn dirversionmangle(&self) -> Option<String> {
1138 self.get_option("dirversionmangle")
1139 }
1140
1141 pub fn pagemangle(&self) -> Option<String> {
1143 self.get_option("pagemangle")
1144 }
1145
1146 pub fn uversionmangle(&self) -> Option<String> {
1150 self.get_option("uversionmangle")
1151 .or_else(|| self.get_option("versionmangle"))
1152 }
1153
1154 pub fn versionmangle(&self) -> Option<String> {
1156 self.get_option("versionmangle")
1157 }
1158
1159 pub fn hrefdecode(&self) -> bool {
1164 self.get_option("hrefdecode").is_some()
1165 }
1166
1167 pub fn downloadurlmangle(&self) -> Option<String> {
1170 self.get_option("downloadurlmangle")
1171 }
1172
1173 pub fn filenamemangle(&self) -> Option<String> {
1181 self.get_option("filenamemangle")
1182 }
1183
1184 pub fn pgpsigurlmangle(&self) -> Option<String> {
1186 self.get_option("pgpsigurlmangle")
1187 }
1188
1189 pub fn oversionmangle(&self) -> Option<String> {
1192 self.get_option("oversionmangle")
1193 }
1194
1195 pub fn apply_uversionmangle(
1208 &self,
1209 version: &str,
1210 ) -> Result<String, crate::mangle::MangleError> {
1211 if let Some(vm) = self.uversionmangle() {
1212 crate::mangle::apply_mangle(&vm, version)
1213 } else {
1214 Ok(version.to_string())
1215 }
1216 }
1217
1218 pub fn apply_dversionmangle(
1231 &self,
1232 version: &str,
1233 ) -> Result<String, crate::mangle::MangleError> {
1234 if let Some(vm) = self.dversionmangle() {
1235 crate::mangle::apply_mangle(&vm, version)
1236 } else {
1237 Ok(version.to_string())
1238 }
1239 }
1240
1241 pub fn apply_oversionmangle(
1254 &self,
1255 version: &str,
1256 ) -> Result<String, crate::mangle::MangleError> {
1257 if let Some(vm) = self.oversionmangle() {
1258 crate::mangle::apply_mangle(&vm, version)
1259 } else {
1260 Ok(version.to_string())
1261 }
1262 }
1263
1264 pub fn apply_dirversionmangle(
1277 &self,
1278 version: &str,
1279 ) -> Result<String, crate::mangle::MangleError> {
1280 if let Some(vm) = self.dirversionmangle() {
1281 crate::mangle::apply_mangle(&vm, version)
1282 } else {
1283 Ok(version.to_string())
1284 }
1285 }
1286
1287 pub fn apply_filenamemangle(&self, url: &str) -> Result<String, crate::mangle::MangleError> {
1303 if let Some(vm) = self.filenamemangle() {
1304 crate::mangle::apply_mangle(&vm, url)
1305 } else {
1306 Ok(url.to_string())
1307 }
1308 }
1309
1310 pub fn apply_pagemangle(&self, page: &[u8]) -> Result<Vec<u8>, crate::mangle::MangleError> {
1326 if let Some(vm) = self.pagemangle() {
1327 let page_str = String::from_utf8_lossy(page);
1328 let mangled = crate::mangle::apply_mangle(&vm, &page_str)?;
1329 Ok(mangled.into_bytes())
1330 } else {
1331 Ok(page.to_vec())
1332 }
1333 }
1334
1335 pub fn apply_downloadurlmangle(&self, url: &str) -> Result<String, crate::mangle::MangleError> {
1351 if let Some(vm) = self.downloadurlmangle() {
1352 crate::mangle::apply_mangle(&vm, url)
1353 } else {
1354 Ok(url.to_string())
1355 }
1356 }
1357
1358 pub fn opts(&self) -> std::collections::HashMap<String, String> {
1360 let mut options = std::collections::HashMap::new();
1361
1362 if let Some(ol) = self.option_list() {
1363 for opt in ol.options() {
1364 let key = opt.key();
1365 let value = opt.value();
1366 if let (Some(key), Some(value)) = (key, value) {
1367 options.insert(key.to_string(), value.to_string());
1368 }
1369 }
1370 }
1371
1372 options
1373 }
1374
1375 fn items(&self) -> impl Iterator<Item = String> + '_ {
1376 self.0.children_with_tokens().filter_map(|it| match it {
1377 SyntaxElement::Token(token) => {
1378 if token.kind() == VALUE || token.kind() == KEY {
1379 Some(token.text().to_string())
1380 } else {
1381 None
1382 }
1383 }
1384 SyntaxElement::Node(node) => {
1385 match node.kind() {
1387 URL => Url::cast(node).map(|n| n.url()),
1388 MATCHING_PATTERN => MatchingPattern::cast(node).map(|n| n.pattern()),
1389 VERSION_POLICY => VersionPolicyNode::cast(node).map(|n| n.policy()),
1390 SCRIPT => ScriptNode::cast(node).map(|n| n.script()),
1391 _ => None,
1392 }
1393 }
1394 })
1395 }
1396
1397 pub fn url_node(&self) -> Option<Url> {
1399 self.0.children().find_map(Url::cast)
1400 }
1401
1402 pub fn url(&self) -> String {
1404 self.url_node().map(|it| it.url()).unwrap_or_else(|| {
1405 self.items().next().unwrap()
1407 })
1408 }
1409
1410 pub fn matching_pattern_node(&self) -> Option<MatchingPattern> {
1412 self.0.children().find_map(MatchingPattern::cast)
1413 }
1414
1415 pub fn matching_pattern(&self) -> Option<String> {
1417 self.matching_pattern_node()
1418 .map(|it| it.pattern())
1419 .or_else(|| {
1420 self.items().nth(1)
1422 })
1423 }
1424
1425 pub fn version_node(&self) -> Option<VersionPolicyNode> {
1427 self.0.children().find_map(VersionPolicyNode::cast)
1428 }
1429
1430 pub fn version(&self) -> Result<Option<crate::VersionPolicy>, String> {
1432 self.version_node()
1433 .map(|it| it.policy().parse())
1434 .transpose()
1435 .map_err(|e: crate::types::ParseError| e.to_string())
1436 .or_else(|_e| {
1437 self.items()
1439 .nth(2)
1440 .map(|it| it.parse())
1441 .transpose()
1442 .map_err(|e: crate::types::ParseError| e.to_string())
1443 })
1444 }
1445
1446 pub fn script_node(&self) -> Option<ScriptNode> {
1448 self.0.children().find_map(ScriptNode::cast)
1449 }
1450
1451 pub fn script(&self) -> Option<String> {
1453 self.script_node().map(|it| it.script()).or_else(|| {
1454 self.items().nth(3)
1456 })
1457 }
1458
1459 pub fn format_url(&self, package: impl FnOnce() -> String) -> url::Url {
1461 crate::subst::subst(self.url().as_str(), package)
1462 .parse()
1463 .unwrap()
1464 }
1465
1466 pub fn set_url(&mut self, new_url: &str) {
1468 let mut builder = GreenNodeBuilder::new();
1470 builder.start_node(URL.into());
1471 builder.token(VALUE.into(), new_url);
1472 builder.finish_node();
1473 let new_url_green = builder.finish();
1474
1475 let new_url_node = SyntaxNode::new_root_mut(new_url_green);
1477
1478 let url_pos = self
1480 .0
1481 .children_with_tokens()
1482 .position(|child| matches!(child, SyntaxElement::Node(node) if node.kind() == URL));
1483
1484 if let Some(pos) = url_pos {
1485 self.0
1487 .splice_children(pos..pos + 1, vec![new_url_node.into()]);
1488 }
1489 }
1490
1491 pub fn set_matching_pattern(&mut self, new_pattern: &str) {
1497 let mut builder = GreenNodeBuilder::new();
1499 builder.start_node(MATCHING_PATTERN.into());
1500 builder.token(VALUE.into(), new_pattern);
1501 builder.finish_node();
1502 let new_pattern_green = builder.finish();
1503
1504 let new_pattern_node = SyntaxNode::new_root_mut(new_pattern_green);
1506
1507 let pattern_pos = self.0.children_with_tokens().position(
1509 |child| matches!(child, SyntaxElement::Node(node) if node.kind() == MATCHING_PATTERN),
1510 );
1511
1512 if let Some(pos) = pattern_pos {
1513 self.0
1515 .splice_children(pos..pos + 1, vec![new_pattern_node.into()]);
1516 }
1517 }
1519
1520 pub fn set_version_policy(&mut self, new_policy: &str) {
1526 let mut builder = GreenNodeBuilder::new();
1528 builder.start_node(VERSION_POLICY.into());
1529 builder.token(VALUE.into(), new_policy);
1531 builder.finish_node();
1532 let new_policy_green = builder.finish();
1533
1534 let new_policy_node = SyntaxNode::new_root_mut(new_policy_green);
1536
1537 let policy_pos = self.0.children_with_tokens().position(
1539 |child| matches!(child, SyntaxElement::Node(node) if node.kind() == VERSION_POLICY),
1540 );
1541
1542 if let Some(pos) = policy_pos {
1543 self.0
1545 .splice_children(pos..pos + 1, vec![new_policy_node.into()]);
1546 }
1547 }
1549
1550 pub fn set_script(&mut self, new_script: &str) {
1556 let mut builder = GreenNodeBuilder::new();
1558 builder.start_node(SCRIPT.into());
1559 builder.token(VALUE.into(), new_script);
1561 builder.finish_node();
1562 let new_script_green = builder.finish();
1563
1564 let new_script_node = SyntaxNode::new_root_mut(new_script_green);
1566
1567 let script_pos = self
1569 .0
1570 .children_with_tokens()
1571 .position(|child| matches!(child, SyntaxElement::Node(node) if node.kind() == SCRIPT));
1572
1573 if let Some(pos) = script_pos {
1574 self.0
1576 .splice_children(pos..pos + 1, vec![new_script_node.into()]);
1577 }
1578 }
1580
1581 pub fn set_opt(&mut self, key: &str, value: &str) {
1587 let opts_pos = self.0.children_with_tokens().position(
1589 |child| matches!(child, SyntaxElement::Node(node) if node.kind() == OPTS_LIST),
1590 );
1591
1592 if let Some(_opts_idx) = opts_pos {
1593 if let Some(mut ol) = self.option_list() {
1594 if let Some(mut opt) = ol.find_option(key) {
1596 opt.set_value(value);
1598 } else {
1600 ol.add_option(key, value);
1602 }
1604 }
1605 } else {
1606 let mut builder = GreenNodeBuilder::new();
1608 builder.start_node(OPTS_LIST.into());
1609 builder.token(KEY.into(), "opts");
1610 builder.token(EQUALS.into(), "=");
1611 builder.start_node(OPTION.into());
1612 builder.token(KEY.into(), key);
1613 builder.token(EQUALS.into(), "=");
1614 builder.token(VALUE.into(), value);
1615 builder.finish_node();
1616 builder.finish_node();
1617 let new_opts_green = builder.finish();
1618 let new_opts_node = SyntaxNode::new_root_mut(new_opts_green);
1619
1620 let url_pos = self
1622 .0
1623 .children_with_tokens()
1624 .position(|child| matches!(child, SyntaxElement::Node(node) if node.kind() == URL));
1625
1626 if let Some(url_idx) = url_pos {
1627 let mut combined_builder = GreenNodeBuilder::new();
1630 combined_builder.start_node(ROOT.into()); combined_builder.token(WHITESPACE.into(), " ");
1632 combined_builder.finish_node();
1633 let temp_green = combined_builder.finish();
1634 let temp_root = SyntaxNode::new_root_mut(temp_green);
1635 let space_element = temp_root.children_with_tokens().next().unwrap();
1636
1637 self.0
1638 .splice_children(url_idx..url_idx, vec![new_opts_node.into(), space_element]);
1639 } else {
1640 self.0.splice_children(0..0, vec![new_opts_node.into()]);
1641 }
1642 }
1643 }
1644
1645 pub fn del_opt(&mut self, key: &str) {
1652 if let Some(mut ol) = self.option_list() {
1653 let option_count = ol.0.children().filter(|n| n.kind() == OPTION).count();
1654
1655 if option_count == 1 && ol.has_option(key) {
1656 let opts_pos = self.0.children().position(|node| node.kind() == OPTS_LIST);
1658
1659 if let Some(opts_idx) = opts_pos {
1660 self.0.splice_children(opts_idx..opts_idx + 1, vec![]);
1662
1663 while self.0.children_with_tokens().next().map_or(false, |e| {
1665 matches!(
1666 e,
1667 SyntaxElement::Token(t) if t.kind() == WHITESPACE || t.kind() == CONTINUATION
1668 )
1669 }) {
1670 self.0.splice_children(0..1, vec![]);
1671 }
1672 }
1673 } else {
1674 ol.remove_option(key);
1676 }
1677 }
1678 }
1679}
1680
1681impl std::fmt::Debug for OptionList {
1682 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1683 f.debug_struct("OptionList")
1684 .field("text", &self.0.text().to_string())
1685 .finish()
1686 }
1687}
1688
1689impl OptionList {
1690 pub fn options(&self) -> impl Iterator<Item = _Option> + '_ {
1692 self.0.children().filter_map(_Option::cast)
1693 }
1694
1695 pub fn find_option(&self, key: &str) -> Option<_Option> {
1697 self.options().find(|opt| opt.key().as_deref() == Some(key))
1698 }
1699
1700 pub fn has_option(&self, key: &str) -> bool {
1702 self.options().any(|it| it.key().as_deref() == Some(key))
1703 }
1704
1705 #[cfg(feature = "deb822")]
1708 pub(crate) fn iter_key_values(&self) -> impl Iterator<Item = (String, String)> + '_ {
1709 self.options().filter_map(|opt| {
1710 if let (Some(key), Some(value)) = (opt.key(), opt.value()) {
1711 Some((key, value))
1712 } else {
1713 None
1714 }
1715 })
1716 }
1717
1718 pub fn get_option(&self, key: &str) -> Option<String> {
1720 for child in self.options() {
1721 if child.key().as_deref() == Some(key) {
1722 return child.value();
1723 }
1724 }
1725 None
1726 }
1727
1728 fn add_option(&mut self, key: &str, value: &str) {
1730 let option_count = self.0.children().filter(|n| n.kind() == OPTION).count();
1731
1732 let mut builder = GreenNodeBuilder::new();
1734 builder.start_node(ROOT.into()); if option_count > 0 {
1737 builder.start_node(OPTION_SEPARATOR.into());
1738 builder.token(COMMA.into(), ",");
1739 builder.finish_node();
1740 }
1741
1742 builder.start_node(OPTION.into());
1743 builder.token(KEY.into(), key);
1744 builder.token(EQUALS.into(), "=");
1745 builder.token(VALUE.into(), value);
1746 builder.finish_node();
1747
1748 builder.finish_node(); let combined_green = builder.finish();
1750
1751 let temp_root = SyntaxNode::new_root_mut(combined_green);
1753 let new_children: Vec<_> = temp_root.children_with_tokens().collect();
1754
1755 let insert_pos = self.0.children_with_tokens().count();
1756 self.0.splice_children(insert_pos..insert_pos, new_children);
1757 }
1758
1759 fn remove_option(&mut self, key: &str) -> bool {
1761 if let Some(mut opt) = self.find_option(key) {
1762 opt.remove();
1763 true
1764 } else {
1765 false
1766 }
1767 }
1768}
1769
1770impl _Option {
1771 pub fn key(&self) -> Option<String> {
1773 self.0.children_with_tokens().find_map(|it| match it {
1774 SyntaxElement::Token(token) => {
1775 if token.kind() == KEY {
1776 Some(token.text().to_string())
1777 } else {
1778 None
1779 }
1780 }
1781 _ => None,
1782 })
1783 }
1784
1785 pub fn value(&self) -> Option<String> {
1787 self.0
1788 .children_with_tokens()
1789 .filter_map(|it| match it {
1790 SyntaxElement::Token(token) => {
1791 if token.kind() == VALUE || token.kind() == KEY {
1792 Some(token.text().to_string())
1793 } else {
1794 None
1795 }
1796 }
1797 _ => None,
1798 })
1799 .nth(1)
1800 }
1801
1802 pub fn set_value(&mut self, new_value: &str) {
1804 let key = self.key().expect("Option must have a key");
1805
1806 let mut builder = GreenNodeBuilder::new();
1808 builder.start_node(OPTION.into());
1809 builder.token(KEY.into(), &key);
1810 builder.token(EQUALS.into(), "=");
1811 builder.token(VALUE.into(), new_value);
1812 builder.finish_node();
1813 let new_option_green = builder.finish();
1814 let new_option_node = SyntaxNode::new_root_mut(new_option_green);
1815
1816 if let Some(parent) = self.0.parent() {
1818 let idx = self.0.index();
1819 parent.splice_children(idx..idx + 1, vec![new_option_node.into()]);
1820 }
1821 }
1822
1823 pub fn remove(&mut self) {
1825 let next_sep = self
1827 .0
1828 .next_sibling()
1829 .filter(|n| n.kind() == OPTION_SEPARATOR);
1830 let prev_sep = self
1831 .0
1832 .prev_sibling()
1833 .filter(|n| n.kind() == OPTION_SEPARATOR);
1834
1835 if let Some(sep) = next_sep {
1837 sep.detach();
1838 } else if let Some(sep) = prev_sep {
1839 sep.detach();
1840 }
1841
1842 self.0.detach();
1844 }
1845}
1846
1847impl Url {
1848 pub fn url(&self) -> String {
1850 self.0
1851 .children_with_tokens()
1852 .find_map(|it| match it {
1853 SyntaxElement::Token(token) => {
1854 if token.kind() == VALUE {
1855 Some(token.text().to_string())
1856 } else {
1857 None
1858 }
1859 }
1860 _ => None,
1861 })
1862 .unwrap()
1863 }
1864}
1865
1866impl MatchingPattern {
1867 pub fn pattern(&self) -> String {
1869 self.0
1870 .children_with_tokens()
1871 .find_map(|it| match it {
1872 SyntaxElement::Token(token) => {
1873 if token.kind() == VALUE {
1874 Some(token.text().to_string())
1875 } else {
1876 None
1877 }
1878 }
1879 _ => None,
1880 })
1881 .unwrap()
1882 }
1883}
1884
1885impl VersionPolicyNode {
1886 pub fn policy(&self) -> String {
1888 self.0
1889 .children_with_tokens()
1890 .find_map(|it| match it {
1891 SyntaxElement::Token(token) => {
1892 if token.kind() == VALUE || token.kind() == KEY {
1894 Some(token.text().to_string())
1895 } else {
1896 None
1897 }
1898 }
1899 _ => None,
1900 })
1901 .unwrap()
1902 }
1903}
1904
1905impl ScriptNode {
1906 pub fn script(&self) -> String {
1908 self.0
1909 .children_with_tokens()
1910 .find_map(|it| match it {
1911 SyntaxElement::Token(token) => {
1912 if token.kind() == VALUE || token.kind() == KEY {
1914 Some(token.text().to_string())
1915 } else {
1916 None
1917 }
1918 }
1919 _ => None,
1920 })
1921 .unwrap()
1922 }
1923}
1924
1925#[cfg(test)]
1926mod tests {
1927 use super::*;
1928
1929 #[test]
1930 fn test_entry_node_structure() {
1931 let wf: super::WatchFile = r#"version=4
1933opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
1934"#
1935 .parse()
1936 .unwrap();
1937
1938 let entry = wf.entries().next().unwrap();
1939
1940 assert_eq!(entry.0.children().find(|n| n.kind() == URL).is_some(), true);
1942 assert_eq!(entry.url(), "https://example.com/releases");
1943
1944 assert_eq!(
1946 entry
1947 .0
1948 .children()
1949 .find(|n| n.kind() == MATCHING_PATTERN)
1950 .is_some(),
1951 true
1952 );
1953 assert_eq!(
1954 entry.matching_pattern(),
1955 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
1956 );
1957
1958 assert_eq!(
1960 entry
1961 .0
1962 .children()
1963 .find(|n| n.kind() == VERSION_POLICY)
1964 .is_some(),
1965 true
1966 );
1967 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
1968
1969 assert_eq!(
1971 entry.0.children().find(|n| n.kind() == SCRIPT).is_some(),
1972 true
1973 );
1974 assert_eq!(entry.script(), Some("uupdate".into()));
1975 }
1976
1977 #[test]
1978 fn test_entry_node_structure_partial() {
1979 let wf: super::WatchFile = r#"version=4
1981https://github.com/example/tags .*/v?(\d\S+)\.tar\.gz
1982"#
1983 .parse()
1984 .unwrap();
1985
1986 let entry = wf.entries().next().unwrap();
1987
1988 assert_eq!(entry.0.children().find(|n| n.kind() == URL).is_some(), true);
1990 assert_eq!(
1991 entry
1992 .0
1993 .children()
1994 .find(|n| n.kind() == MATCHING_PATTERN)
1995 .is_some(),
1996 true
1997 );
1998
1999 assert_eq!(
2001 entry
2002 .0
2003 .children()
2004 .find(|n| n.kind() == VERSION_POLICY)
2005 .is_some(),
2006 false
2007 );
2008 assert_eq!(
2009 entry.0.children().find(|n| n.kind() == SCRIPT).is_some(),
2010 false
2011 );
2012
2013 assert_eq!(entry.url(), "https://github.com/example/tags");
2015 assert_eq!(
2016 entry.matching_pattern(),
2017 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2018 );
2019 assert_eq!(entry.version(), Ok(None));
2020 assert_eq!(entry.script(), None);
2021 }
2022
2023 #[test]
2024 fn test_parse_v1() {
2025 const WATCHV1: &str = r#"version=4
2026opts=bare,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1\.tar\.gz/ \
2027 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2028"#;
2029 let parsed = parse(WATCHV1);
2030 let node = parsed.syntax();
2032 assert_eq!(
2033 format!("{:#?}", node),
2034 r#"ROOT@0..161
2035 VERSION@0..10
2036 KEY@0..7 "version"
2037 EQUALS@7..8 "="
2038 VALUE@8..9 "4"
2039 NEWLINE@9..10 "\n"
2040 ENTRY@10..161
2041 OPTS_LIST@10..86
2042 KEY@10..14 "opts"
2043 EQUALS@14..15 "="
2044 OPTION@15..19
2045 KEY@15..19 "bare"
2046 OPTION_SEPARATOR@19..20
2047 COMMA@19..20 ","
2048 OPTION@20..86
2049 KEY@20..34 "filenamemangle"
2050 EQUALS@34..35 "="
2051 VALUE@35..86 "s/.+\\/v?(\\d\\S+)\\.tar\\ ..."
2052 WHITESPACE@86..87 " "
2053 CONTINUATION@87..89 "\\\n"
2054 WHITESPACE@89..91 " "
2055 URL@91..138
2056 VALUE@91..138 "https://github.com/sy ..."
2057 WHITESPACE@138..139 " "
2058 MATCHING_PATTERN@139..160
2059 VALUE@139..160 ".*/v?(\\d\\S+)\\.tar\\.gz"
2060 NEWLINE@160..161 "\n"
2061"#
2062 );
2063
2064 let root = parsed.root();
2065 assert_eq!(root.version(), 4);
2066 let entries = root.entries().collect::<Vec<_>>();
2067 assert_eq!(entries.len(), 1);
2068 let entry = &entries[0];
2069 assert_eq!(
2070 entry.url(),
2071 "https://github.com/syncthing/syncthing-gtk/tags"
2072 );
2073 assert_eq!(
2074 entry.matching_pattern(),
2075 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2076 );
2077 assert_eq!(entry.version(), Ok(None));
2078 assert_eq!(entry.script(), None);
2079
2080 assert_eq!(node.text(), WATCHV1);
2081 }
2082
2083 #[test]
2084 fn test_parse_v2() {
2085 let parsed = parse(
2086 r#"version=4
2087https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2088# comment
2089"#,
2090 );
2091 assert_eq!(parsed.errors, Vec::<String>::new());
2092 let node = parsed.syntax();
2093 assert_eq!(
2094 format!("{:#?}", node),
2095 r###"ROOT@0..90
2096 VERSION@0..10
2097 KEY@0..7 "version"
2098 EQUALS@7..8 "="
2099 VALUE@8..9 "4"
2100 NEWLINE@9..10 "\n"
2101 ENTRY@10..80
2102 URL@10..57
2103 VALUE@10..57 "https://github.com/sy ..."
2104 WHITESPACE@57..58 " "
2105 MATCHING_PATTERN@58..79
2106 VALUE@58..79 ".*/v?(\\d\\S+)\\.tar\\.gz"
2107 NEWLINE@79..80 "\n"
2108 COMMENT@80..89 "# comment"
2109 NEWLINE@89..90 "\n"
2110"###
2111 );
2112
2113 let root = parsed.root();
2114 assert_eq!(root.version(), 4);
2115 let entries = root.entries().collect::<Vec<_>>();
2116 assert_eq!(entries.len(), 1);
2117 let entry = &entries[0];
2118 assert_eq!(
2119 entry.url(),
2120 "https://github.com/syncthing/syncthing-gtk/tags"
2121 );
2122 assert_eq!(
2123 entry.format_url(|| "syncthing-gtk".to_string()),
2124 "https://github.com/syncthing/syncthing-gtk/tags"
2125 .parse()
2126 .unwrap()
2127 );
2128 }
2129
2130 #[test]
2131 fn test_parse_v3() {
2132 let parsed = parse(
2133 r#"version=4
2134https://github.com/syncthing/@PACKAGE@/tags .*/v?(\d\S+)\.tar\.gz
2135# comment
2136"#,
2137 );
2138 assert_eq!(parsed.errors, Vec::<String>::new());
2139 let root = parsed.root();
2140 assert_eq!(root.version(), 4);
2141 let entries = root.entries().collect::<Vec<_>>();
2142 assert_eq!(entries.len(), 1);
2143 let entry = &entries[0];
2144 assert_eq!(entry.url(), "https://github.com/syncthing/@PACKAGE@/tags");
2145 assert_eq!(
2146 entry.format_url(|| "syncthing-gtk".to_string()),
2147 "https://github.com/syncthing/syncthing-gtk/tags"
2148 .parse()
2149 .unwrap()
2150 );
2151 }
2152
2153 #[test]
2154 fn test_thread_safe_parsing() {
2155 let text = r#"version=4
2156https://github.com/example/example/tags example-(.*)\.tar\.gz
2157"#;
2158
2159 let parsed = parse_watch_file(text);
2160 assert!(parsed.is_ok());
2161 assert_eq!(parsed.errors().len(), 0);
2162
2163 let watchfile = parsed.tree();
2165 assert_eq!(watchfile.version(), 4);
2166 let entries: Vec<_> = watchfile.entries().collect();
2167 assert_eq!(entries.len(), 1);
2168 }
2169
2170 #[test]
2171 fn test_parse_clone_and_eq() {
2172 let text = r#"version=4
2173https://github.com/example/example/tags example-(.*)\.tar\.gz
2174"#;
2175
2176 let parsed1 = parse_watch_file(text);
2177 let parsed2 = parsed1.clone();
2178
2179 assert_eq!(parsed1, parsed2);
2181
2182 let watchfile1 = parsed1.tree();
2184 let watchfile2 = watchfile1.clone();
2185 assert_eq!(watchfile1, watchfile2);
2186 }
2187
2188 #[test]
2189 fn test_parse_v4() {
2190 let cl: super::WatchFile = r#"version=4
2191opts=repack,compression=xz,dversionmangle=s/\+ds//,repacksuffix=+ds \
2192 https://github.com/example/example-cat/tags \
2193 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2194"#
2195 .parse()
2196 .unwrap();
2197 assert_eq!(cl.version(), 4);
2198 let entries = cl.entries().collect::<Vec<_>>();
2199 assert_eq!(entries.len(), 1);
2200 let entry = &entries[0];
2201 assert_eq!(entry.url(), "https://github.com/example/example-cat/tags");
2202 assert_eq!(
2203 entry.matching_pattern(),
2204 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2205 );
2206 assert!(entry.repack());
2207 assert_eq!(entry.compression(), Ok(Some(Compression::Xz)));
2208 assert_eq!(entry.dversionmangle(), Some("s/\\+ds//".into()));
2209 assert_eq!(entry.repacksuffix(), Some("+ds".into()));
2210 assert_eq!(entry.script(), Some("uupdate".into()));
2211 assert_eq!(
2212 entry.format_url(|| "example-cat".to_string()),
2213 "https://github.com/example/example-cat/tags"
2214 .parse()
2215 .unwrap()
2216 );
2217 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
2218 }
2219
2220 #[test]
2221 fn test_git_mode() {
2222 let text = r#"version=3
2223opts="mode=git, gitmode=shallow, pgpmode=gittag" \
2224https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git \
2225refs/tags/(.*) debian
2226"#;
2227 let parsed = parse(text);
2228 assert_eq!(parsed.errors, Vec::<String>::new());
2229 let cl = parsed.root();
2230 assert_eq!(cl.version(), 3);
2231 let entries = cl.entries().collect::<Vec<_>>();
2232 assert_eq!(entries.len(), 1);
2233 let entry = &entries[0];
2234 assert_eq!(
2235 entry.url(),
2236 "https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git"
2237 );
2238 assert_eq!(entry.matching_pattern(), Some("refs/tags/(.*)".into()));
2239 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
2240 assert_eq!(entry.script(), None);
2241 assert_eq!(entry.gitmode(), Ok(GitMode::Shallow));
2242 assert_eq!(entry.pgpmode(), Ok(PgpMode::GitTag));
2243 assert_eq!(entry.mode(), Ok(Mode::Git));
2244 }
2245
2246 #[test]
2247 fn test_parse_quoted() {
2248 const WATCHV1: &str = r#"version=4
2249opts="bare, filenamemangle=blah" \
2250 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2251"#;
2252 let parsed = parse(WATCHV1);
2253 let node = parsed.syntax();
2255
2256 let root = parsed.root();
2257 assert_eq!(root.version(), 4);
2258 let entries = root.entries().collect::<Vec<_>>();
2259 assert_eq!(entries.len(), 1);
2260 let entry = &entries[0];
2261
2262 assert_eq!(
2263 entry.url(),
2264 "https://github.com/syncthing/syncthing-gtk/tags"
2265 );
2266 assert_eq!(
2267 entry.matching_pattern(),
2268 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2269 );
2270 assert_eq!(entry.version(), Ok(None));
2271 assert_eq!(entry.script(), None);
2272
2273 assert_eq!(node.text(), WATCHV1);
2274 }
2275
2276 #[test]
2277 fn test_set_url() {
2278 let wf: super::WatchFile = r#"version=4
2280https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2281"#
2282 .parse()
2283 .unwrap();
2284
2285 let mut entry = wf.entries().next().unwrap();
2286 assert_eq!(
2287 entry.url(),
2288 "https://github.com/syncthing/syncthing-gtk/tags"
2289 );
2290
2291 entry.set_url("https://newurl.example.org/path");
2292 assert_eq!(entry.url(), "https://newurl.example.org/path");
2293 assert_eq!(
2294 entry.matching_pattern(),
2295 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2296 );
2297
2298 assert_eq!(
2300 entry.to_string(),
2301 "https://newurl.example.org/path .*/v?(\\d\\S+)\\.tar\\.gz\n"
2302 );
2303 }
2304
2305 #[test]
2306 fn test_set_url_with_options() {
2307 let wf: super::WatchFile = r#"version=4
2309opts=foo=blah https://foo.com/bar .*/v?(\d\S+)\.tar\.gz
2310"#
2311 .parse()
2312 .unwrap();
2313
2314 let mut entry = wf.entries().next().unwrap();
2315 assert_eq!(entry.url(), "https://foo.com/bar");
2316 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2317
2318 entry.set_url("https://example.com/baz");
2319 assert_eq!(entry.url(), "https://example.com/baz");
2320
2321 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2323 assert_eq!(
2324 entry.matching_pattern(),
2325 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2326 );
2327
2328 assert_eq!(
2330 entry.to_string(),
2331 "opts=foo=blah https://example.com/baz .*/v?(\\d\\S+)\\.tar\\.gz\n"
2332 );
2333 }
2334
2335 #[test]
2336 fn test_set_url_complex() {
2337 let wf: super::WatchFile = r#"version=4
2339opts=bare,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1\.tar\.gz/ \
2340 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2341"#
2342 .parse()
2343 .unwrap();
2344
2345 let mut entry = wf.entries().next().unwrap();
2346 assert_eq!(
2347 entry.url(),
2348 "https://github.com/syncthing/syncthing-gtk/tags"
2349 );
2350
2351 entry.set_url("https://gitlab.com/newproject/tags");
2352 assert_eq!(entry.url(), "https://gitlab.com/newproject/tags");
2353
2354 assert!(entry.bare());
2356 assert_eq!(
2357 entry.filenamemangle(),
2358 Some("s/.+\\/v?(\\d\\S+)\\.tar\\.gz/syncthing-gtk-$1\\.tar\\.gz/".into())
2359 );
2360 assert_eq!(
2361 entry.matching_pattern(),
2362 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2363 );
2364
2365 assert_eq!(
2367 entry.to_string(),
2368 r#"opts=bare,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1\.tar\.gz/ \
2369 https://gitlab.com/newproject/tags .*/v?(\d\S+)\.tar\.gz
2370"#
2371 );
2372 }
2373
2374 #[test]
2375 fn test_set_url_with_all_fields() {
2376 let wf: super::WatchFile = r#"version=4
2378opts=repack,compression=xz,dversionmangle=s/\+ds//,repacksuffix=+ds \
2379 https://github.com/example/example-cat/tags \
2380 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2381"#
2382 .parse()
2383 .unwrap();
2384
2385 let mut entry = wf.entries().next().unwrap();
2386 assert_eq!(entry.url(), "https://github.com/example/example-cat/tags");
2387 assert_eq!(
2388 entry.matching_pattern(),
2389 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2390 );
2391 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2392 assert_eq!(entry.script(), Some("uupdate".into()));
2393
2394 entry.set_url("https://gitlab.example.org/project/releases");
2395 assert_eq!(entry.url(), "https://gitlab.example.org/project/releases");
2396
2397 assert!(entry.repack());
2399 assert_eq!(entry.compression(), Ok(Some(super::Compression::Xz)));
2400 assert_eq!(entry.dversionmangle(), Some("s/\\+ds//".into()));
2401 assert_eq!(entry.repacksuffix(), Some("+ds".into()));
2402 assert_eq!(
2403 entry.matching_pattern(),
2404 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2405 );
2406 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2407 assert_eq!(entry.script(), Some("uupdate".into()));
2408
2409 assert_eq!(
2411 entry.to_string(),
2412 r#"opts=repack,compression=xz,dversionmangle=s/\+ds//,repacksuffix=+ds \
2413 https://gitlab.example.org/project/releases \
2414 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2415"#
2416 );
2417 }
2418
2419 #[test]
2420 fn test_set_url_quoted_options() {
2421 let wf: super::WatchFile = r#"version=4
2423opts="bare, filenamemangle=blah" \
2424 https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
2425"#
2426 .parse()
2427 .unwrap();
2428
2429 let mut entry = wf.entries().next().unwrap();
2430 assert_eq!(
2431 entry.url(),
2432 "https://github.com/syncthing/syncthing-gtk/tags"
2433 );
2434
2435 entry.set_url("https://example.org/new/path");
2436 assert_eq!(entry.url(), "https://example.org/new/path");
2437
2438 assert_eq!(
2440 entry.to_string(),
2441 r#"opts="bare, filenamemangle=blah" \
2442 https://example.org/new/path .*/v?(\d\S+)\.tar\.gz
2443"#
2444 );
2445 }
2446
2447 #[test]
2448 fn test_set_opt_update_existing() {
2449 let wf: super::WatchFile = r#"version=4
2451opts=foo=blah,bar=baz https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2452"#
2453 .parse()
2454 .unwrap();
2455
2456 let mut entry = wf.entries().next().unwrap();
2457 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2458 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2459
2460 entry.set_opt("foo", "updated");
2461 assert_eq!(entry.get_option("foo"), Some("updated".to_string()));
2462 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2463
2464 assert_eq!(
2466 entry.to_string(),
2467 "opts=foo=updated,bar=baz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2468 );
2469 }
2470
2471 #[test]
2472 fn test_set_opt_add_new() {
2473 let wf: super::WatchFile = r#"version=4
2475opts=foo=blah https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2476"#
2477 .parse()
2478 .unwrap();
2479
2480 let mut entry = wf.entries().next().unwrap();
2481 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2482 assert_eq!(entry.get_option("bar"), None);
2483
2484 entry.set_opt("bar", "baz");
2485 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2486 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2487
2488 assert_eq!(
2490 entry.to_string(),
2491 "opts=foo=blah,bar=baz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2492 );
2493 }
2494
2495 #[test]
2496 fn test_set_opt_create_options_list() {
2497 let wf: super::WatchFile = r#"version=4
2499https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2500"#
2501 .parse()
2502 .unwrap();
2503
2504 let mut entry = wf.entries().next().unwrap();
2505 assert_eq!(entry.option_list(), None);
2506
2507 entry.set_opt("compression", "xz");
2508 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
2509
2510 assert_eq!(
2512 entry.to_string(),
2513 "opts=compression=xz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2514 );
2515 }
2516
2517 #[test]
2518 fn test_del_opt_remove_single() {
2519 let wf: super::WatchFile = r#"version=4
2521opts=foo=blah,bar=baz,qux=quux https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2522"#
2523 .parse()
2524 .unwrap();
2525
2526 let mut entry = wf.entries().next().unwrap();
2527 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2528 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2529 assert_eq!(entry.get_option("qux"), Some("quux".to_string()));
2530
2531 entry.del_opt("bar");
2532 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2533 assert_eq!(entry.get_option("bar"), None);
2534 assert_eq!(entry.get_option("qux"), Some("quux".to_string()));
2535
2536 assert_eq!(
2538 entry.to_string(),
2539 "opts=foo=blah,qux=quux https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2540 );
2541 }
2542
2543 #[test]
2544 fn test_del_opt_remove_first() {
2545 let wf: super::WatchFile = r#"version=4
2547opts=foo=blah,bar=baz https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2548"#
2549 .parse()
2550 .unwrap();
2551
2552 let mut entry = wf.entries().next().unwrap();
2553 entry.del_opt("foo");
2554 assert_eq!(entry.get_option("foo"), None);
2555 assert_eq!(entry.get_option("bar"), Some("baz".to_string()));
2556
2557 assert_eq!(
2559 entry.to_string(),
2560 "opts=bar=baz https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2561 );
2562 }
2563
2564 #[test]
2565 fn test_del_opt_remove_last() {
2566 let wf: super::WatchFile = r#"version=4
2568opts=foo=blah,bar=baz https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2569"#
2570 .parse()
2571 .unwrap();
2572
2573 let mut entry = wf.entries().next().unwrap();
2574 entry.del_opt("bar");
2575 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2576 assert_eq!(entry.get_option("bar"), None);
2577
2578 assert_eq!(
2580 entry.to_string(),
2581 "opts=foo=blah https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2582 );
2583 }
2584
2585 #[test]
2586 fn test_del_opt_remove_only_option() {
2587 let wf: super::WatchFile = r#"version=4
2589opts=foo=blah https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2590"#
2591 .parse()
2592 .unwrap();
2593
2594 let mut entry = wf.entries().next().unwrap();
2595 assert_eq!(entry.get_option("foo"), Some("blah".to_string()));
2596
2597 entry.del_opt("foo");
2598 assert_eq!(entry.get_option("foo"), None);
2599 assert_eq!(entry.option_list(), None);
2600
2601 assert_eq!(
2603 entry.to_string(),
2604 "https://example.com/releases .*/v?(\\d\\S+)\\.tar\\.gz\n"
2605 );
2606 }
2607
2608 #[test]
2609 fn test_del_opt_nonexistent() {
2610 let wf: super::WatchFile = r#"version=4
2612opts=foo=blah https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2613"#
2614 .parse()
2615 .unwrap();
2616
2617 let mut entry = wf.entries().next().unwrap();
2618 let original = entry.to_string();
2619
2620 entry.del_opt("nonexistent");
2621 assert_eq!(entry.to_string(), original);
2622 }
2623
2624 #[test]
2625 fn test_set_opt_multiple_operations() {
2626 let wf: super::WatchFile = r#"version=4
2628https://example.com/releases .*/v?(\d\S+)\.tar\.gz
2629"#
2630 .parse()
2631 .unwrap();
2632
2633 let mut entry = wf.entries().next().unwrap();
2634
2635 entry.set_opt("compression", "xz");
2636 entry.set_opt("repack", "");
2637 entry.set_opt("dversionmangle", "s/\\+ds//");
2638
2639 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
2640 assert_eq!(
2641 entry.get_option("dversionmangle"),
2642 Some("s/\\+ds//".to_string())
2643 );
2644 }
2645
2646 #[test]
2647 fn test_set_matching_pattern() {
2648 let wf: super::WatchFile = r#"version=4
2650https://github.com/example/tags .*/v?(\d\S+)\.tar\.gz
2651"#
2652 .parse()
2653 .unwrap();
2654
2655 let mut entry = wf.entries().next().unwrap();
2656 assert_eq!(
2657 entry.matching_pattern(),
2658 Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
2659 );
2660
2661 entry.set_matching_pattern("(?:.*?/)?v?([\\d.]+)\\.tar\\.gz");
2662 assert_eq!(
2663 entry.matching_pattern(),
2664 Some("(?:.*?/)?v?([\\d.]+)\\.tar\\.gz".into())
2665 );
2666
2667 assert_eq!(entry.url(), "https://github.com/example/tags");
2669
2670 assert_eq!(
2672 entry.to_string(),
2673 "https://github.com/example/tags (?:.*?/)?v?([\\d.]+)\\.tar\\.gz\n"
2674 );
2675 }
2676
2677 #[test]
2678 fn test_set_matching_pattern_with_all_fields() {
2679 let wf: super::WatchFile = r#"version=4
2681opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2682"#
2683 .parse()
2684 .unwrap();
2685
2686 let mut entry = wf.entries().next().unwrap();
2687 assert_eq!(
2688 entry.matching_pattern(),
2689 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2690 );
2691
2692 entry.set_matching_pattern(".*/version-([\\d.]+)\\.tar\\.xz");
2693 assert_eq!(
2694 entry.matching_pattern(),
2695 Some(".*/version-([\\d.]+)\\.tar\\.xz".into())
2696 );
2697
2698 assert_eq!(entry.url(), "https://example.com/releases");
2700 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2701 assert_eq!(entry.script(), Some("uupdate".into()));
2702 assert_eq!(entry.compression(), Ok(Some(super::Compression::Xz)));
2703
2704 assert_eq!(
2706 entry.to_string(),
2707 "opts=compression=xz https://example.com/releases .*/version-([\\d.]+)\\.tar\\.xz debian uupdate\n"
2708 );
2709 }
2710
2711 #[test]
2712 fn test_set_version_policy() {
2713 let wf: super::WatchFile = r#"version=4
2715https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2716"#
2717 .parse()
2718 .unwrap();
2719
2720 let mut entry = wf.entries().next().unwrap();
2721 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2722
2723 entry.set_version_policy("previous");
2724 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Previous)));
2725
2726 assert_eq!(entry.url(), "https://example.com/releases");
2728 assert_eq!(
2729 entry.matching_pattern(),
2730 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2731 );
2732 assert_eq!(entry.script(), Some("uupdate".into()));
2733
2734 assert_eq!(
2736 entry.to_string(),
2737 "https://example.com/releases (?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz previous uupdate\n"
2738 );
2739 }
2740
2741 #[test]
2742 fn test_set_version_policy_with_options() {
2743 let wf: super::WatchFile = r#"version=4
2745opts=repack,compression=xz \
2746 https://github.com/example/example-cat/tags \
2747 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2748"#
2749 .parse()
2750 .unwrap();
2751
2752 let mut entry = wf.entries().next().unwrap();
2753 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2754
2755 entry.set_version_policy("ignore");
2756 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Ignore)));
2757
2758 assert_eq!(entry.url(), "https://github.com/example/example-cat/tags");
2760 assert_eq!(
2761 entry.matching_pattern(),
2762 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2763 );
2764 assert_eq!(entry.script(), Some("uupdate".into()));
2765 assert!(entry.repack());
2766
2767 assert_eq!(
2769 entry.to_string(),
2770 r#"opts=repack,compression=xz \
2771 https://github.com/example/example-cat/tags \
2772 (?:.*?/)?v?(\d[\d.]*)\.tar\.gz ignore uupdate
2773"#
2774 );
2775 }
2776
2777 #[test]
2778 fn test_set_script() {
2779 let wf: super::WatchFile = r#"version=4
2781https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2782"#
2783 .parse()
2784 .unwrap();
2785
2786 let mut entry = wf.entries().next().unwrap();
2787 assert_eq!(entry.script(), Some("uupdate".into()));
2788
2789 entry.set_script("uscan");
2790 assert_eq!(entry.script(), Some("uscan".into()));
2791
2792 assert_eq!(entry.url(), "https://example.com/releases");
2794 assert_eq!(
2795 entry.matching_pattern(),
2796 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2797 );
2798 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2799
2800 assert_eq!(
2802 entry.to_string(),
2803 "https://example.com/releases (?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz debian uscan\n"
2804 );
2805 }
2806
2807 #[test]
2808 fn test_set_script_with_options() {
2809 let wf: super::WatchFile = r#"version=4
2811opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
2812"#
2813 .parse()
2814 .unwrap();
2815
2816 let mut entry = wf.entries().next().unwrap();
2817 assert_eq!(entry.script(), Some("uupdate".into()));
2818
2819 entry.set_script("custom-script.sh");
2820 assert_eq!(entry.script(), Some("custom-script.sh".into()));
2821
2822 assert_eq!(entry.url(), "https://example.com/releases");
2824 assert_eq!(
2825 entry.matching_pattern(),
2826 Some("(?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz".into())
2827 );
2828 assert_eq!(entry.version(), Ok(Some(super::VersionPolicy::Debian)));
2829 assert_eq!(entry.compression(), Ok(Some(super::Compression::Xz)));
2830
2831 assert_eq!(
2833 entry.to_string(),
2834 "opts=compression=xz https://example.com/releases (?:.*?/)?v?(\\d[\\d.]*)\\.tar\\.gz debian custom-script.sh\n"
2835 );
2836 }
2837
2838 #[test]
2839 fn test_apply_dversionmangle() {
2840 let wf: super::WatchFile = r#"version=4
2842opts=dversionmangle=s/\+dfsg$// https://example.com/ .*
2843"#
2844 .parse()
2845 .unwrap();
2846 let entry = wf.entries().next().unwrap();
2847 assert_eq!(entry.apply_dversionmangle("1.0+dfsg").unwrap(), "1.0");
2848 assert_eq!(entry.apply_dversionmangle("1.0").unwrap(), "1.0");
2849
2850 let wf: super::WatchFile = r#"version=4
2852opts=versionmangle=s/^v// https://example.com/ .*
2853"#
2854 .parse()
2855 .unwrap();
2856 let entry = wf.entries().next().unwrap();
2857 assert_eq!(entry.apply_dversionmangle("v1.0").unwrap(), "1.0");
2858
2859 let wf: super::WatchFile = r#"version=4
2861opts=dversionmangle=s/\+ds//,versionmangle=s/^v// https://example.com/ .*
2862"#
2863 .parse()
2864 .unwrap();
2865 let entry = wf.entries().next().unwrap();
2866 assert_eq!(entry.apply_dversionmangle("1.0+ds").unwrap(), "1.0");
2867
2868 let wf: super::WatchFile = r#"version=4
2870https://example.com/ .*
2871"#
2872 .parse()
2873 .unwrap();
2874 let entry = wf.entries().next().unwrap();
2875 assert_eq!(entry.apply_dversionmangle("1.0+dfsg").unwrap(), "1.0+dfsg");
2876 }
2877
2878 #[test]
2879 fn test_apply_oversionmangle() {
2880 let wf: super::WatchFile = r#"version=4
2882opts=oversionmangle=s/$/-1/ https://example.com/ .*
2883"#
2884 .parse()
2885 .unwrap();
2886 let entry = wf.entries().next().unwrap();
2887 assert_eq!(entry.apply_oversionmangle("1.0").unwrap(), "1.0-1");
2888 assert_eq!(entry.apply_oversionmangle("2.5.3").unwrap(), "2.5.3-1");
2889
2890 let wf: super::WatchFile = r#"version=4
2892opts=oversionmangle=s/$/.dfsg/ https://example.com/ .*
2893"#
2894 .parse()
2895 .unwrap();
2896 let entry = wf.entries().next().unwrap();
2897 assert_eq!(entry.apply_oversionmangle("1.0").unwrap(), "1.0.dfsg");
2898
2899 let wf: super::WatchFile = r#"version=4
2901https://example.com/ .*
2902"#
2903 .parse()
2904 .unwrap();
2905 let entry = wf.entries().next().unwrap();
2906 assert_eq!(entry.apply_oversionmangle("1.0").unwrap(), "1.0");
2907 }
2908
2909 #[test]
2910 fn test_apply_dirversionmangle() {
2911 let wf: super::WatchFile = r#"version=4
2913opts=dirversionmangle=s/^v// https://example.com/ .*
2914"#
2915 .parse()
2916 .unwrap();
2917 let entry = wf.entries().next().unwrap();
2918 assert_eq!(entry.apply_dirversionmangle("v1.0").unwrap(), "1.0");
2919 assert_eq!(entry.apply_dirversionmangle("v2.5.3").unwrap(), "2.5.3");
2920
2921 let wf: super::WatchFile = r#"version=4
2923opts=dirversionmangle=s/v(\d)/$1/ https://example.com/ .*
2924"#
2925 .parse()
2926 .unwrap();
2927 let entry = wf.entries().next().unwrap();
2928 assert_eq!(entry.apply_dirversionmangle("v1.0").unwrap(), "1.0");
2929
2930 let wf: super::WatchFile = r#"version=4
2932https://example.com/ .*
2933"#
2934 .parse()
2935 .unwrap();
2936 let entry = wf.entries().next().unwrap();
2937 assert_eq!(entry.apply_dirversionmangle("v1.0").unwrap(), "v1.0");
2938 }
2939
2940 #[test]
2941 fn test_apply_filenamemangle() {
2942 let wf: super::WatchFile = r#"version=4
2944opts=filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/mypackage-$1.tar.gz/ https://example.com/ .*
2945"#
2946 .parse()
2947 .unwrap();
2948 let entry = wf.entries().next().unwrap();
2949 assert_eq!(
2950 entry
2951 .apply_filenamemangle("https://example.com/v1.0.tar.gz")
2952 .unwrap(),
2953 "mypackage-1.0.tar.gz"
2954 );
2955 assert_eq!(
2956 entry
2957 .apply_filenamemangle("https://example.com/2.5.3.tar.gz")
2958 .unwrap(),
2959 "mypackage-2.5.3.tar.gz"
2960 );
2961
2962 let wf: super::WatchFile = r#"version=4
2964opts=filenamemangle=s/.*\/(.*)/$1/ https://example.com/ .*
2965"#
2966 .parse()
2967 .unwrap();
2968 let entry = wf.entries().next().unwrap();
2969 assert_eq!(
2970 entry
2971 .apply_filenamemangle("https://example.com/path/to/file.tar.gz")
2972 .unwrap(),
2973 "file.tar.gz"
2974 );
2975
2976 let wf: super::WatchFile = r#"version=4
2978https://example.com/ .*
2979"#
2980 .parse()
2981 .unwrap();
2982 let entry = wf.entries().next().unwrap();
2983 assert_eq!(
2984 entry
2985 .apply_filenamemangle("https://example.com/file.tar.gz")
2986 .unwrap(),
2987 "https://example.com/file.tar.gz"
2988 );
2989 }
2990
2991 #[test]
2992 fn test_apply_pagemangle() {
2993 let wf: super::WatchFile = r#"version=4
2995opts=pagemangle=s/&/&/g https://example.com/ .*
2996"#
2997 .parse()
2998 .unwrap();
2999 let entry = wf.entries().next().unwrap();
3000 assert_eq!(
3001 entry.apply_pagemangle(b"foo & bar").unwrap(),
3002 b"foo & bar"
3003 );
3004 assert_eq!(
3005 entry
3006 .apply_pagemangle(b"& foo & bar &")
3007 .unwrap(),
3008 b"& foo & bar &"
3009 );
3010
3011 let wf: super::WatchFile = r#"version=4
3013opts=pagemangle=s/<[^>]+>//g https://example.com/ .*
3014"#
3015 .parse()
3016 .unwrap();
3017 let entry = wf.entries().next().unwrap();
3018 assert_eq!(entry.apply_pagemangle(b"<div>text</div>").unwrap(), b"text");
3019
3020 let wf: super::WatchFile = r#"version=4
3022https://example.com/ .*
3023"#
3024 .parse()
3025 .unwrap();
3026 let entry = wf.entries().next().unwrap();
3027 assert_eq!(
3028 entry.apply_pagemangle(b"foo & bar").unwrap(),
3029 b"foo & bar"
3030 );
3031 }
3032
3033 #[test]
3034 fn test_apply_downloadurlmangle() {
3035 let wf: super::WatchFile = r#"version=4
3037opts=downloadurlmangle=s|/archive/|/download/| https://example.com/ .*
3038"#
3039 .parse()
3040 .unwrap();
3041 let entry = wf.entries().next().unwrap();
3042 assert_eq!(
3043 entry
3044 .apply_downloadurlmangle("https://example.com/archive/file.tar.gz")
3045 .unwrap(),
3046 "https://example.com/download/file.tar.gz"
3047 );
3048
3049 let wf: super::WatchFile = r#"version=4
3051opts=downloadurlmangle=s/github\.com/raw.githubusercontent.com/ https://example.com/ .*
3052"#
3053 .parse()
3054 .unwrap();
3055 let entry = wf.entries().next().unwrap();
3056 assert_eq!(
3057 entry
3058 .apply_downloadurlmangle("https://github.com/user/repo/file.tar.gz")
3059 .unwrap(),
3060 "https://raw.githubusercontent.com/user/repo/file.tar.gz"
3061 );
3062
3063 let wf: super::WatchFile = r#"version=4
3065https://example.com/ .*
3066"#
3067 .parse()
3068 .unwrap();
3069 let entry = wf.entries().next().unwrap();
3070 assert_eq!(
3071 entry
3072 .apply_downloadurlmangle("https://example.com/archive/file.tar.gz")
3073 .unwrap(),
3074 "https://example.com/archive/file.tar.gz"
3075 );
3076 }
3077
3078 #[test]
3079 fn test_entry_builder_minimal() {
3080 let entry = super::EntryBuilder::new("https://github.com/example/tags")
3082 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3083 .build();
3084
3085 assert_eq!(entry.url(), "https://github.com/example/tags");
3086 assert_eq!(
3087 entry.matching_pattern().as_deref(),
3088 Some(".*/v?(\\d\\S+)\\.tar\\.gz")
3089 );
3090 assert_eq!(entry.version(), Ok(None));
3091 assert_eq!(entry.script(), None);
3092 assert!(entry.opts().is_empty());
3093 }
3094
3095 #[test]
3096 fn test_entry_builder_url_only() {
3097 let entry = super::EntryBuilder::new("https://example.com/releases").build();
3099
3100 assert_eq!(entry.url(), "https://example.com/releases");
3101 assert_eq!(entry.matching_pattern(), None);
3102 assert_eq!(entry.version(), Ok(None));
3103 assert_eq!(entry.script(), None);
3104 assert!(entry.opts().is_empty());
3105 }
3106
3107 #[test]
3108 fn test_entry_builder_with_all_fields() {
3109 let entry = super::EntryBuilder::new("https://github.com/example/tags")
3111 .matching_pattern(".*/v?(\\d[\\d.]*)\\.tar\\.gz")
3112 .version_policy("debian")
3113 .script("uupdate")
3114 .opt("compression", "xz")
3115 .flag("repack")
3116 .build();
3117
3118 assert_eq!(entry.url(), "https://github.com/example/tags");
3119 assert_eq!(
3120 entry.matching_pattern().as_deref(),
3121 Some(".*/v?(\\d[\\d.]*)\\.tar\\.gz")
3122 );
3123 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
3124 assert_eq!(entry.script(), Some("uupdate".into()));
3125 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
3126 assert!(entry.has_option("repack"));
3127 assert!(entry.repack());
3128 }
3129
3130 #[test]
3131 fn test_entry_builder_multiple_options() {
3132 let entry = super::EntryBuilder::new("https://example.com/tags")
3134 .matching_pattern(".*/v?(\\d+\\.\\d+)\\.tar\\.gz")
3135 .opt("compression", "xz")
3136 .opt("dversionmangle", "s/\\+ds//")
3137 .opt("repacksuffix", "+ds")
3138 .build();
3139
3140 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
3141 assert_eq!(
3142 entry.get_option("dversionmangle"),
3143 Some("s/\\+ds//".to_string())
3144 );
3145 assert_eq!(entry.get_option("repacksuffix"), Some("+ds".to_string()));
3146 }
3147
3148 #[test]
3149 fn test_entry_builder_via_entry() {
3150 let entry = super::Entry::builder("https://github.com/example/tags")
3152 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3153 .version_policy("debian")
3154 .build();
3155
3156 assert_eq!(entry.url(), "https://github.com/example/tags");
3157 assert_eq!(
3158 entry.matching_pattern().as_deref(),
3159 Some(".*/v?(\\d\\S+)\\.tar\\.gz")
3160 );
3161 assert_eq!(entry.version(), Ok(Some(VersionPolicy::Debian)));
3162 }
3163
3164 #[test]
3165 fn test_watchfile_add_entry_to_empty() {
3166 let mut wf = super::WatchFile::new(Some(4));
3168
3169 let entry = super::EntryBuilder::new("https://github.com/example/tags")
3170 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3171 .build();
3172
3173 wf.add_entry(entry);
3174
3175 assert_eq!(wf.version(), 4);
3176 assert_eq!(wf.entries().count(), 1);
3177
3178 let added_entry = wf.entries().next().unwrap();
3179 assert_eq!(added_entry.url(), "https://github.com/example/tags");
3180 assert_eq!(
3181 added_entry.matching_pattern().as_deref(),
3182 Some(".*/v?(\\d\\S+)\\.tar\\.gz")
3183 );
3184 }
3185
3186 #[test]
3187 fn test_watchfile_add_multiple_entries() {
3188 let mut wf = super::WatchFile::new(Some(4));
3190
3191 wf.add_entry(
3192 super::EntryBuilder::new("https://github.com/example1/tags")
3193 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3194 .build(),
3195 );
3196
3197 wf.add_entry(
3198 super::EntryBuilder::new("https://github.com/example2/releases")
3199 .matching_pattern(".*/(\\d+\\.\\d+)\\.tar\\.gz")
3200 .opt("compression", "xz")
3201 .build(),
3202 );
3203
3204 assert_eq!(wf.entries().count(), 2);
3205
3206 let entries: Vec<_> = wf.entries().collect();
3207 assert_eq!(entries[0].url(), "https://github.com/example1/tags");
3208 assert_eq!(entries[1].url(), "https://github.com/example2/releases");
3209 assert_eq!(entries[1].get_option("compression"), Some("xz".to_string()));
3210 }
3211
3212 #[test]
3213 fn test_watchfile_add_entry_to_existing() {
3214 let mut wf: super::WatchFile = r#"version=4
3216https://example.com/old .*/v?(\\d\\S+)\\.tar\\.gz
3217"#
3218 .parse()
3219 .unwrap();
3220
3221 assert_eq!(wf.entries().count(), 1);
3222
3223 wf.add_entry(
3224 super::EntryBuilder::new("https://github.com/example/new")
3225 .matching_pattern(".*/v?(\\d+\\.\\d+)\\.tar\\.gz")
3226 .opt("compression", "xz")
3227 .version_policy("debian")
3228 .build(),
3229 );
3230
3231 assert_eq!(wf.entries().count(), 2);
3232
3233 let entries: Vec<_> = wf.entries().collect();
3234 assert_eq!(entries[0].url(), "https://example.com/old");
3235 assert_eq!(entries[1].url(), "https://github.com/example/new");
3236 assert_eq!(entries[1].version(), Ok(Some(VersionPolicy::Debian)));
3237 }
3238
3239 #[test]
3240 fn test_entry_builder_formatting() {
3241 let entry = super::EntryBuilder::new("https://github.com/example/tags")
3243 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3244 .opt("compression", "xz")
3245 .flag("repack")
3246 .version_policy("debian")
3247 .script("uupdate")
3248 .build();
3249
3250 let entry_str = entry.to_string();
3251
3252 assert!(entry_str.starts_with("opts="));
3254 assert!(entry_str.contains("https://github.com/example/tags"));
3256 assert!(entry_str.contains(".*/v?(\\d\\S+)\\.tar\\.gz"));
3258 assert!(entry_str.contains("debian"));
3260 assert!(entry_str.contains("uupdate"));
3262 assert!(entry_str.ends_with('\n'));
3264 }
3265
3266 #[test]
3267 fn test_watchfile_add_entry_preserves_format() {
3268 let mut wf = super::WatchFile::new(Some(4));
3270
3271 wf.add_entry(
3272 super::EntryBuilder::new("https://github.com/example/tags")
3273 .matching_pattern(".*/v?(\\d\\S+)\\.tar\\.gz")
3274 .build(),
3275 );
3276
3277 let wf_str = wf.to_string();
3278
3279 assert!(wf_str.starts_with("version=4\n"));
3281 assert!(wf_str.contains("https://github.com/example/tags"));
3283
3284 let reparsed: super::WatchFile = wf_str.parse().unwrap();
3286 assert_eq!(reparsed.version(), 4);
3287 assert_eq!(reparsed.entries().count(), 1);
3288 }
3289
3290 #[test]
3291 fn test_line_col() {
3292 let text = r#"version=4
3293opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian uupdate
3294"#;
3295 let wf = text.parse::<super::WatchFile>().unwrap();
3296
3297 let version_node = wf.version_node().unwrap();
3299 assert_eq!(version_node.line(), 0);
3300 assert_eq!(version_node.column(), 0);
3301 assert_eq!(version_node.line_col(), (0, 0));
3302
3303 let entries: Vec<_> = wf.entries().collect();
3305 assert_eq!(entries.len(), 1);
3306
3307 assert_eq!(entries[0].line(), 1);
3309 assert_eq!(entries[0].column(), 0);
3310 assert_eq!(entries[0].line_col(), (1, 0));
3311
3312 let option_list = entries[0].option_list().unwrap();
3314 assert_eq!(option_list.line(), 1); let url_node = entries[0].url_node().unwrap();
3317 assert_eq!(url_node.line(), 1); let pattern_node = entries[0].matching_pattern_node().unwrap();
3320 assert_eq!(pattern_node.line(), 1); let version_policy_node = entries[0].version_node().unwrap();
3323 assert_eq!(version_policy_node.line(), 1); let script_node = entries[0].script_node().unwrap();
3326 assert_eq!(script_node.line(), 1); let options: Vec<_> = option_list.options().collect();
3330 assert_eq!(options.len(), 1);
3331 assert_eq!(options[0].key(), Some("compression".to_string()));
3332 assert_eq!(options[0].value(), Some("xz".to_string()));
3333 assert_eq!(options[0].line(), 1); let compression_opt = option_list.find_option("compression").unwrap();
3337 assert_eq!(compression_opt.line(), 1);
3338 assert_eq!(compression_opt.column(), 5); assert_eq!(compression_opt.line_col(), (1, 5));
3340 }
3341
3342 #[test]
3343 fn test_parse_str_relaxed() {
3344 let wf: super::WatchFile = super::WatchFile::from_str_relaxed(
3345 r#"version=4
3346ERRORS IN THIS LINE
3347opts=compression=xz https://example.com/releases (?:.*?/)?v?(\d
3348"#,
3349 );
3350 assert_eq!(wf.version(), 4);
3351 assert_eq!(wf.entries().count(), 2);
3352
3353 let entries = wf.entries().collect::<Vec<_>>();
3354
3355 let entry = &entries[0];
3356 assert_eq!(entry.url(), "ERRORS");
3357
3358 let entry = &entries[1];
3359 assert_eq!(entry.url(), "https://example.com/releases");
3360 assert_eq!(entry.matching_pattern().as_deref(), Some("(?:.*?/)?v?(\\d"));
3361 assert_eq!(entry.get_option("compression"), Some("xz".to_string()));
3362 }
3363}